/* ══════════════════════════════════════════════════════════════════════════
   SPROUT UI v2 — the single source of truth for how the app looks.

   Rules of the road (enforced by teamplan/tests_ui_house_style.py):
     • A template that extends base_v2.html may NOT carry a <style> block or
       an inline style="" attribute. If a page needs something this file
       cannot express, the fix is to add it HERE, once, for everyone.
     • Everything is a token first, a component second. Never hard-code a
       colour, radius or spacing in a component — reach for a var().
     • Club branding flows in through --club-primary / --club-secondary,
       which base.html and base_v2.html both set from the club's Appearance
       settings. That is why nothing below hard-codes the navy.

   Naming: s- prefix = Sprout v2. (pp-* in app.css is the v1 system; the two
   coexist during migration and v2 pages should not use pp-* classes.)
   ══════════════════════════════════════════════════════════════════════════ */

/* ── 1. TOKENS ─────────────────────────────────────────────────────────── */
:root {
  /* Brand. Derived from the club's own colours so a club with a custom
     scheme re-themes the entire app by changing them in one place. */
  --s-brand:        var(--club-primary, #17202f);
  --s-brand-soft:   color-mix(in srgb, var(--s-brand) 80%, #4a5a7d);
  --s-brand-on:     var(--club-primary-on, #ffffff);
  /* The club's colour when it is the *writing* rather than the ground. On a
     white page they are the same thing, which is why this is just --s-brand
     here; in dark mode they part company. See the note in html.dark-mode. */
  --s-brand-ink:    var(--s-brand);
  --s-accent:       var(--club-secondary, #f7c500);
  --s-accent-on:    var(--club-secondary-on, #0b1220);
  --s-accent-text:  var(--club-secondary-text, #8a6e00);

  /* Surfaces & ink */
  --s-bg:           #f4f6fa;
  --s-surface:      #ffffff;
  --s-surface-2:    #f7f9fc;
  --s-ink:          #101828;
  --s-ink-2:        #475467;
  --s-muted:        #7d8da3;
  --s-line:         #e6eaf2;
  --s-line-soft:    #eef1f7;
  /* The plate a sponsor's logo sits on. Deliberately NOT redefined for the
     dark theme: a sponsor's artwork is drawn for white paper and most of it
     is dark ink, so a dark plate hides half the logos a club uploads. */
  --s-logo-plate:   #f4f6fa;

  /* Semantic pairs: text colour, tinted background, tinted border */
  --s-good:  #16a34a;  --s-good-bg:  #e9f8ef;  --s-good-line:  #cdeeda;
  --s-info:  #2563eb;  --s-info-bg:  #e3edff;  --s-info-line:  #c9dcff;
  --s-warn:  #b45309;  --s-warn-bg:  #fef3c7;  --s-warn-line:  #fae3a3;
  --s-bad:   #e11d48;  --s-bad-bg:   #ffe4ea;  --s-bad-line:   #fbc7d2;
  --s-violet:#7c3aed;  --s-violet-bg:#ede9fe;  --s-violet-line:#ddd4fd;
  --s-teal:  #0d9488;  --s-teal-bg:  #ccfbf1;
  --s-pink:  #db2777;  --s-pink-bg:  #fce7f3;
  --s-orange:#ea580c;  --s-orange-bg:#ffedd5;
  --s-slate: #5b6b84;  --s-slate-bg: #e9edf4;

  /* Type scale */
  --s-fs-xs:  0.688rem;   /* 11px — eyebrow labels */
  --s-fs-sm:  0.813rem;   /* 13px — secondary text */
  --s-fs-md:  0.938rem;   /* 15px — body */
  --s-fs-lg:  1.063rem;   /* 17px — card titles */
  --s-fs-xl:  1.375rem;   /* 22px — section headings */
  --s-fs-2xl: 1.75rem;    /* 28px — page greeting */

  /* Spacing */
  --s-sp-1: 4px;  --s-sp-2: 8px;  --s-sp-3: 12px;
  --s-sp-4: 16px; --s-sp-5: 20px; --s-sp-6: 28px;

  /* Radius */
  --s-r-sm: 10px; --s-r: 14px; --s-r-lg: 18px; --s-r-xl: 26px; --s-r-pill: 999px;

  /* Elevation */
  --s-shadow-sm: 0 1px 2px rgba(16,24,40,.04), 0 1px 3px rgba(16,24,40,.06);
  --s-shadow:    0 2px 6px rgba(16,24,40,.06), 0 6px 16px rgba(16,24,40,.06);
  --s-shadow-lg: 0 24px 48px rgba(23,30,60,.28);

  /* Layout metrics — the drawer and content offsets are derived from these,
     and --s-hdr-h is re-measured at runtime by base_v2 so the drawer lands
     flush under the header on every device. */
  --s-hdr-h: 66px;
  --s-nav-h: 68px;   /* measured at runtime by sprout-ui.js; this is the fallback */
  --s-maxw:  var(--s-content-max, 1120px);
}

html.dark-mode {
  --s-bg:        #0b1220;
  --s-surface:   #131c2e;
  --s-surface-2: #18223a;
  --s-ink:       #e8edf7;
  --s-ink-2:     #a9b6cd;
  --s-muted:     #7e8da6;
  --s-line:      #24314c;
  --s-line-soft: #1d2942;

  --s-good-bg:   #10291d;  --s-good-line:   #1d4433;
  --s-info-bg:   #101f3d;  --s-info-line:   #1c3564;
  --s-warn-bg:   #2c2210;  --s-warn-line:   #4a3a15;
  --s-bad-bg:    #2e1220;  --s-bad-line:    #522038;
  --s-violet-bg: #201a3d;  --s-violet-line: #362a63;
  --s-teal-bg:   #0d2c2a;
  --s-pink-bg:   #2c1424;
  --s-orange-bg: #2c1a0d;
  --s-slate-bg:  #1c2740;

  --s-shadow-sm: 0 1px 2px rgba(0,0,0,.3);
  --s-shadow:    0 2px 8px rgba(0,0,0,.35);
  --s-shadow-lg: 0 24px 48px rgba(0,0,0,.55);

  /* A club picks its colour against a white page, so most of them are a
     near-black navy, maroon or forest green. As a *ground* that is still fine
     in dark mode -- it is paired with --s-brand-on, which is white. As *ink*
     on a dark card it disappears, and as a 12% tint of one it disappears
     twice over. So the ink half of the pair gets lifted towards the page's
     own light and the ground half is left alone.

     Only .s-stepn reads this so far. Eight other places still write
     `color: var(--s-brand)` and have the same fault -- they want the same
     substitution, one at a time, each checked in dark mode. */
  --s-brand-ink: color-mix(in srgb, var(--club-primary, #17202f) 30%, #cfe0ff);
}

/* ── 1b. V2 RESET ──────────────────────────────────────────────────────── */
/* base_v2.html marks the document <html class="s-v2">. These rules neutralise
   the globals the v1 shell relies on (app.css sets html,body{height:100%},
   base.html sets a body padding-top for its fixed navbar) so a v2 page starts
   from a clean slate — without either of those files having to change while
   the two shells coexist. */
html.s-v2 { overflow-x: hidden; -webkit-text-size-adjust: 100%; }
html.s-v2, html.s-v2 body { height: auto; min-height: 100%; max-width: 100%; }
/* The same overflow-x: hidden used to sit on <body> as well, the way it is
   usually copied around. It is not belt and braces -- it is the brace that
   breaks the belt. <html> already carries the clip, and that is the one the
   viewport uses; a second one on <body> makes <body> a scroll container of
   its own that never actually scrolls, and every position: sticky inside it
   silently stops sticking. That had already killed .feed-sidebar, which asks
   to stay beside the feed on a laptop and instead scrolls away with it.
   Nothing is lost by removing it: <html>'s clip is what stops the page
   travelling sideways, and <body> is the same width. */
html.s-v2 body {
  margin: 0; padding: 0;
  overscroll-behavior-y: none;
  background: var(--s-brand);
  color: var(--s-ink);
  font-family: 'Nunito', 'Segoe UI', system-ui, -apple-system, sans-serif;
  font-size: var(--s-fs-md);
  line-height: 1.45;
}
html.s-v2 *, html.s-v2 *::before, html.s-v2 *::after { box-sizing: border-box; }
/* Long words wrap instead of pushing the page sideways.

   CSS only offers to break a line at a space, a hyphen or a slash. A stored
   value carrying none of those (an uploaded filename like
   e237993097d0_Your_Empire_Kits_receipt.eml, a supplier's department address,
   a payment reference) paints straight out of whatever box renders it, and
   because the box grows the document grows with it, which on a phone is the
   whole page sitting further right than the screen.

   overflow-wrap is inherited, so this one line covers every v2 component at
   once. Before it there were sixteen of these scattered through the file, each
   added the day someone found the page it broke, and a sweep at 375px found
   forty-five more classes still waiting their turn.

   break-word rather than anywhere on purpose: it acts only when a word will
   not fit at all, and it leaves min-content sizing alone, so no flex or grid
   track changes width because of this rule. The few places that also need
   their track to shrink say min-width: 0 for themselves. */
html.s-v2 body { overflow-wrap: break-word; }

/* The same thing again, for the boxes whose width is decided by their content.

   break-word above wraps a long word but deliberately does not change how wide
   a box asks to be, so a heading or a stat sitting in a flex or grid track is
   still forced out to the width of its longest word and takes the page with
   it. These are the classes where that actually happens: each one was found by
   putting a forty-character word with no space, hyphen or slash in it into
   every text box on 750 pages across three roles and measuring the document,
   not by reading the sheet and guessing. anywhere lets the track shrink too.

   Scoped to a list rather than set on <body> on purpose. Applied to everything
   it also reaches the tables, and a table in .s-scroll-x is meant to keep its
   columns and scroll; letting it shrink instead crams a league table into a
   phone and makes it unreadable. Measured: 568 boxes burst before any of this,
   389 with break-word alone, 2 with both. */
.s-btn, .s-ca__btn, .s-card__sub,
.s-card__title, .s-chip, .s-chip__n, .s-dial__n,
.s-grouph__k, .s-h1, .s-label, .s-label__note,
.s-link, .s-md__ko, .s-md__score, .s-md__v,
.s-msg__av, .s-opt, .s-period__lb, .s-person__nm,
.s-relay__lb, .s-sect__t, .s-seg__opt, .s-shop__n,
.s-shop__p, .s-snap__name, .s-stat__k, .s-stat__s,
.s-stat__u, .s-stat__v, .s-tick__t {
  overflow-wrap: anywhere;
}
/* Except inside a table, where letting a chip shrink is the wrong trade. A
   league table is meant to keep its columns and scroll sideways in its
   .s-scroll-x, and a shrinkable chip in the team column pulls that column in
   until CHAMPIONS reads as CHAMPION on one line and S on the next. Here the
   long word keeps the column wide and the table scrolls, which is what the
   wrapper is for. */
.s-table .s-chip { overflow-wrap: break-word; }
html.s-v2 h1, html.s-v2 h2, html.s-v2 h3,
html.s-v2 h4, html.s-v2 p, html.s-v2 figure { margin: 0; }
html.s-v2 ul.s-plain { list-style: none; margin: 0; padding: 0; }

/* The hidden attribute is how a v2 page hides something the script will later
   show. It has to win outright: a component rule like .composer-settings sets
   display:flex, and an author rule beats the browser's own [hidden] every time
   — so without the !important the attribute would be decorative. */
html.s-v2 [hidden] { display: none !important; }

/* ── 2. SHELL ──────────────────────────────────────────────────────────── */
.s-app {
  min-height: 100vh;
  min-height: 100dvh;
  background: linear-gradient(120deg, var(--s-brand) 0%, var(--s-brand-soft) 100%);
  display: flex;
  flex-direction: column;
  padding-top: var(--s-hdr-h);
}

/* Header: fixed, never moves, paints up behind the status bar / notch. */
.s-hdr {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 7px;
  /* Side insets too: landscape on a notched iPhone puts the cutout on the
     left or right edge, and a fixed bar with plain px padding sits under it. */
  padding: calc(10px + env(safe-area-inset-top, 0px)) calc(12px + env(safe-area-inset-right, 0px)) 12px calc(10px + env(safe-area-inset-left, 0px));
  background: linear-gradient(120deg, var(--s-brand) 0%, var(--s-brand-soft) 100%);
}
.s-hdr__crest {
  height: 44px; width: 44px;
  flex-shrink: 0;
  object-fit: contain;
  filter: drop-shadow(0 3px 6px rgba(0,0,0,.3));
}
.s-hdr__name {
  min-width: 0; flex-shrink: 1;
  margin-right: auto;
  color: var(--s-brand-on);
  font-size: var(--s-fs-lg);
  font-weight: 900;
  letter-spacing: -0.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  text-shadow: 0 1px 2px rgba(0,0,0,.18);
  text-decoration: none;
}
.s-hdr__btn {
  position: relative;
  width: 36px; height: 36px;
  flex-shrink: 0;
  border: none; border-radius: 50%;
  background: var(--s-surface);
  color: var(--s-ink);
  font-size: 14px;
  display: inline-flex; align-items: center; justify-content: center;
  box-shadow: 0 3px 8px rgba(23,30,60,.22);
  cursor: pointer;
  text-decoration: none;
}
.s-hdr__btn--good { color: var(--s-good); }
.s-hdr__btn--bad  { color: var(--s-bad); }
.s-hdr__badge {
  position: absolute; top: -3px; right: -3px;
  min-width: 17px; height: 17px; padding: 0 4px;
  border-radius: var(--s-r-pill);
  background: var(--s-bad); color: #fff;
  font-size: 10px; font-weight: 800; line-height: 17px;
  text-align: center;
  box-shadow: 0 0 0 2px var(--s-brand);
}

/* Drawer toggle: floats clear of the club name, below the header. Taken out
   on 10 Sep 2026 because it sat over the top-right of the first block on a
   phone; back on 11 Sep 2026 (Ben: "I never said get rid of the floating
   drawer, put it back"). The blocks that reach that corner keep it clear
   instead: .s-pagehead and .s-greet carry 52px of right padding. */
.s-fab {
  position: fixed;
  top: calc(var(--s-hdr-h) + 12px);
  right: calc(14px + env(safe-area-inset-right, 0px));
  z-index: 62;
  width: 44px; height: 44px;
  border: none; border-radius: 50%;
  background: var(--s-surface);
  box-shadow: 0 4px 12px rgba(23,30,60,.28);
  cursor: pointer;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 4px;
}
.s-fab span {
  display: block;
  width: 19px; height: 2.5px;
  border-radius: 2px;
  background: var(--s-ink);
  transition: transform .3s, opacity .25s;
}
.s-fab.is-open span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.s-fab.is-open span:nth-child(2) { opacity: 0; }
.s-fab.is-open span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

/* Floating back button — the v1 corner, ported. Sits in the bottom-left,
   clear of the bar; sprout-ui.js lights it only when there is real history
   to go back to and this is not the home page. */
.s-back {
  display: none;
  position: fixed;
  left: calc(12px + env(safe-area-inset-left, 0px));
  bottom: calc(var(--s-nav-h) + 12px);
  z-index: 41;
  width: 44px; height: 44px;
  align-items: center; justify-content: center;
  border: 1px solid var(--s-line-soft);
  border-radius: 50%;
  background: var(--s-surface);
  color: var(--s-ink-2);
  font-size: 1rem;
  box-shadow: 0 4px 12px rgba(23,30,60,.28);
  padding: 0;
  cursor: pointer;
}
.s-back.is-on { display: flex; }
.s-back:active { transform: scale(.94); }

/* The floating + : Quick add, on the coach dashboard only. Bottom-right,
   clear of the bar, the mirror of the back corner (which the dashboard never
   shows, because the dashboard is home) and well away from the menu button
   in the top-right. The page pads its own foot so the last card is not under
   it. (Ben, 11 Sep 2026) */
.s-plus {
  position: fixed;
  right: calc(14px + env(safe-area-inset-right, 0px));
  bottom: calc(var(--s-nav-h) + 14px);
  z-index: 41;
  width: 54px; height: 54px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%;
  background: var(--s-accent); color: var(--s-accent-on);
  font-size: 1.35rem;
  box-shadow: 0 6px 16px rgba(23,30,60,.32);
  text-decoration: none;
}
.s-plus:active { transform: scale(.94); }
.s-plus:focus-visible { outline: 3px solid var(--s-info); outline-offset: 2px; }
.s-v2:has(.s-plus) .s-main { padding-bottom: calc(var(--s-nav-h) + 84px); }
body.modal-open .s-plus,
html.pp-modal-open .s-plus { display: none !important; }
/* The corner is fixed, so at the foot of a page it lands on whatever the last
   thing is. On a training session that was the "All training sessions" link:
   the two overlapped by 30px and a tap on the link's own arrow went back
   instead. .s-main's bottom padding only clears the bar, so the page needs the
   height of the corner as well, but only on the pages that show one. */
.s-v2:has(.s-back.is-on) .s-main { padding-bottom: calc(var(--s-nav-h) + 68px); }
/* On desktop too (Ben, 3 Sep 2026) — only modals and the install banner
   outrank the corner while they are up. */
body.modal-open .s-back,
html.pp-modal-open .s-back { display: none !important; }

/* Windows Chrome draws an overlay scrollbar ~17px wide over anything fixed
   to the right edge — Ben's bell badge sat half under it. Clear it. */
@media (min-width: 992px) {
  .s-hdr { padding-right: 30px; }
  .s-fab { right: 30px; }
}

/* The white sheet with the signature curved shoulders.

   Deliberately NO z-index. `position: relative` alone creates no stacking
   context, so a modal a page puts in {% block content %} stacks against the
   root and can actually sit above the bottom nav. With z-index: 20 here every
   overlay in the page body was silently capped at 20 — the chat lobby's
   "new chat" sheet asked for 81 and still rendered under the nav. The header
   (60), drawer (50) and nav (40) are all fixed and positioned, so they still
   paint above ordinary content either way. */
.s-sheet {
  flex: 1;
  display: flex; flex-direction: column;
  background: var(--s-bg);
  border-radius: var(--s-r-xl) var(--s-r-xl) 0 0;
  position: relative;
}
.s-main {
  flex: 1;
  width: 100%;
  max-width: var(--s-maxw);
  margin: 0 auto;
  padding: var(--s-sp-5) calc(15px + env(safe-area-inset-right, 0px)) calc(var(--s-nav-h) + 28px) calc(15px + env(safe-area-inset-left, 0px));
  display: flex; flex-direction: column;
  gap: var(--s-sp-3);
}

/* Bottom nav — fixed, monochrome, one accent on the active item only. */
.s-nav {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  z-index: 40;
  display: flex; align-items: stretch;
  padding: 9px calc(4px + env(safe-area-inset-right, 0px)) calc(12px + env(safe-area-inset-bottom, 0px)) calc(4px + env(safe-area-inset-left, 0px));
  background: var(--s-surface);
  border-top: 1.5px solid var(--s-line-soft);
}
.s-nav__item {
  flex: 1; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  text-decoration: none;
  position: relative;
  /* The "More" slot is a <button> — reset it so it sits in the row like a link. */
  background: none; border: none; padding: 0; font: inherit; cursor: pointer;
}
.s-nav__ic {
  width: 38px; height: 30px;
  border-radius: 11px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 17px; line-height: 1;
  color: var(--s-ink-2);
}
.s-nav__lb {
  max-width: 100%; padding: 0 1px;
  font-size: 9px; font-weight: 800;
  color: var(--s-ink-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* A word too long for its slot. On a 375px phone a seven-slot bar gives each
   slot 52px, and the longest label in the catalogue, "Tournaments", sets to
   58px. Set a size down and pulled a shade tighter it comes to 50px, so the
   member reads the whole word instead of "Tournament...". Nothing else in the
   catalogue needs it: "Companion", the next longest, is 50px already. */
.s-nav__lb--long { font-size: 8px; letter-spacing: -0.15px; padding: 0; }
.s-nav__item.is-active .s-nav__ic { background: var(--s-good-bg); color: var(--s-good); }
.s-nav__item.is-active .s-nav__lb { color: var(--s-good); }
.s-nav__dot {
  position: absolute; top: -1px; right: 50%;
  transform: translateX(22px);
  min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: var(--s-r-pill);
  background: var(--s-bad); color: #fff;
  font-size: 9px; font-weight: 800; line-height: 16px; text-align: center;
}
body.modal-open .s-nav, html.pp-modal-open .s-nav { display: none; }
/* Scroll lock while the drawer is open, so the gesture moves the panel and not
   the page behind it. Scoped to s-v2 so the v1 shell is untouched. */
html.s-v2.pp-modal-open, html.s-v2.pp-modal-open body { overflow: hidden; }

/* ── 3. DRAWER ─────────────────────────────────────────────────────────── */
.s-drawer-clip {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 100vh; height: 100dvh;
  overflow: hidden;
  pointer-events: none;
  z-index: 50;
}
.s-drawer {
  position: absolute;
  top: 0; left: 0; right: 0;
  padding-top: var(--s-hdr-h);
  transform: translateY(-101%);
  transition: transform .42s cubic-bezier(.32,.72,.28,1.02);
  pointer-events: auto;
  max-height: 100vh; max-height: 100dvh;
  display: flex; flex-direction: column;
}
.s-drawer.is-open { transform: translateY(0); }
.s-drawer__panel {
  background: var(--s-surface);
  border-radius: 0 0 var(--s-r-xl) var(--s-r-xl);
  box-shadow: var(--s-shadow-lg);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s-sp-4) var(--s-sp-4) 10px;
}
.s-backdrop {
  position: fixed; inset: 0;
  z-index: 45;
  background: rgba(15,23,42,.45);
  backdrop-filter: blur(2px);
  opacity: 0; pointer-events: none;
  transition: opacity .35s;
}
.s-backdrop.is-open { opacity: 1; pointer-events: auto; }
.s-drawer__handle {
  display: flex; align-items: center; justify-content: center;
  padding: 10px 0 4px;
}
.s-drawer__handle::before {
  content: '';
  width: 44px; height: 5px;
  border-radius: var(--s-r-pill);
  background: var(--s-line);
}
.s-drawer__hint {
  text-align: center;
  font-size: var(--s-fs-xs); font-weight: 700;
  color: var(--s-muted);
  padding-bottom: 6px;
}

/* The drawer's search box: the first thing in the panel, so it sits level
   with the fixed drawer button in the top-right corner. It keeps the
   button's width clear the same way .s-find does on a page, and drops the
   reserve at 992px, where the panel is centred at 720px and the button is
   outside it. The results block stands in for the drawer body while there
   is text in the box (sprout-ui.js, DRAWER SEARCH). */
.s-drawer__search { position: relative; margin: 0 54px var(--s-sp-3) 0; }
.s-drawer__search-ic {
  position: absolute; left: 13px; top: 50%; transform: translateY(-50%);
  color: var(--s-muted); font-size: 14px; pointer-events: none;
}
.s-drawer__search .s-input { padding-left: 38px; }
/* iOS zooms the page into any field under 16px the moment it is focused. */
@media (max-width: 575.98px) { .s-drawer__search .s-input { font-size: 16px; } }
@media (min-width: 992px) { .s-drawer__search { margin-right: 0; } }
.s-drawer__results .s-card { margin-bottom: var(--s-sp-3); }
.s-drawer__results-note {
  padding: 6px 2px 0;
  font-size: var(--s-fs-xs); font-weight: 700;
  color: var(--s-muted);
}

/* Segmented view switch (Coach / Parent). It used to reserve the drawer
   button's width on the right; the search box above it does that now. */
.s-seg { display: flex; gap: 8px; margin-bottom: var(--s-sp-3); }
.s-seg__opt {
  flex: 1;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 10px;
  border: 1.5px solid var(--s-line);
  border-radius: 13px;
  background: var(--s-surface);
  color: var(--s-ink);
  font-size: var(--s-fs-sm); font-weight: 900;
  text-decoration: none; text-align: center;
}
.s-seg__opt.is-on {
  background: var(--s-info-bg);
  border-color: var(--s-info-line);
  color: var(--s-info);
}

/* ── Pick one of a handful ───────────────────────────────────────────────
   A short, fixed list where all the choices fit on screen at once. A select
   hides them behind a tap and shows one at a time, which is the wrong trade
   when there are six and the point is to compare them.

   Not .s-seg: that is the two halves of one control, stretched to fill the
   row. These wrap, size to their own words, and there may be seven. */
.s-opts { display: flex; flex-wrap: wrap; gap: 8px; }
.s-opt {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 13px;
  border: 1.5px solid var(--s-line);
  border-radius: var(--s-r-pill);
  background: var(--s-surface);
  color: var(--s-ink-2);
  font-size: var(--s-fs-sm); font-weight: 800;
  cursor: pointer;
}
.s-opt:hover { border-color: var(--s-info-line); color: var(--s-ink); }
.s-opt[aria-pressed="true"] {
  border-color: var(--s-info-line);
  background: var(--s-info-bg);
  color: var(--s-info);
}

/* ── Type a name, pick from what appears ─────────────────────────────────
   The list overlays whatever sits under the field, so the field and the list
   go inside .s-ta together — that is what the list positions against. It is
   hidden with the `hidden` attribute rather than a class, so it is hidden
   from a screen reader too and not merely invisible. */
.s-ta { position: relative; }
.s-ta__list {
  position: absolute; z-index: 30; top: 100%; left: 0; right: 0;
  max-height: 240px; overflow-y: auto; -webkit-overflow-scrolling: touch;
  margin-top: 4px;
  border: 1px solid var(--s-line);
  border-radius: var(--s-r);
  background: var(--s-surface);
  box-shadow: var(--s-shadow);
}
.s-ta__list[hidden] { display: none; }
.s-ta__i {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 9px 12px;
  border: none; border-bottom: 1px solid var(--s-line-soft);
  background: none; text-align: left; cursor: pointer;
}
.s-ta__i:last-child { border-bottom: none; }
.s-ta__i:hover, .s-ta__i:focus { background: var(--s-surface-2); }
.s-ta__av {
  width: 30px; height: 30px; flex-shrink: 0;
  border-radius: 50%; object-fit: cover;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--s-fs-xs); font-weight: 900;
  background: var(--s-info-bg); color: var(--s-info);
}
.s-ta__t { display: block; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink); }
.s-ta__s { display: block; font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted); }

/* ── Tabs across the top of a section ────────────────────────────────────
   Training is four sibling pages — dashboard, sessions, attendance, plans —
   and moving between them shouldn't mean opening the drawer every time.

   Not .s-seg: that switches what a single page is showing, so its options are
   two halves of one control. These are real destinations, so they are links,
   sized by their own labels rather than split evenly.

   Scrolls rather than wraps. A wrapping strip changes height as the current
   tab moves along it, which shifts the whole page under the reader mid-tap. */
.s-tabs {
  display: flex; gap: 6px;
  margin-bottom: var(--s-sp-3);
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* A tournament has twelve sections and a phone fits three, so the strip
     scrolls — and with the scrollbar hidden nothing said so: the third tab
     looked like the last tab there was. The fade only bites when there is a
     tab under it; where the whole strip fits, the right edge is empty
     background and the mask is invisible. */
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 26px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 26px), transparent);
}
.s-tabs::-webkit-scrollbar { display: none; }
.s-tabs__t {
  flex: 0 0 auto;
  border: none; font-family: inherit; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 9px 14px;
  border-radius: var(--s-r-pill);
  background: var(--s-surface-2);
  color: var(--s-muted);
  font-size: var(--s-fs-sm); font-weight: 800;
  text-decoration: none; white-space: nowrap;
}
.s-tabs__t:hover { color: var(--s-ink); }
.s-tabs__t.is-on { background: var(--s-info-bg); color: var(--s-info); }

/* A column header you can sort by. Only ever on a desktop table — a phone
   gets the card stack, which has no columns to sort. The arrow is a ::after so
   the header text does not shift by a character's width when it lights up. */
.s-sort { cursor: pointer; user-select: none; white-space: nowrap; }
.s-sort::after {
  content: "↕";
  margin-left: 5px;
  opacity: .35;
  font-weight: 700;
}
.s-sort.is-asc::after  { content: "↑"; opacity: 1; }
.s-sort.is-desc::after { content: "↓"; opacity: 1; }

/* ── The bulk bar ────────────────────────────────────────────────────────
   Rises from the floor once rows are ticked, and stops above the bottom nav
   rather than covering it: nothing is worse than selecting fifteen things and
   finding the way out of the page is underneath the bar telling you so.

   Shown by removing .is-hidden, never by JS writing a display value — see the
   note on that class. */
.s-bulk {
  position: fixed; z-index: 45;
  left: 12px; right: 12px;
  bottom: calc(var(--s-nav-h) + 12px);
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  padding: 10px 12px;
  border-radius: var(--s-r-lg);
  background: var(--s-ink);
  box-shadow: var(--s-shadow-lg);
}
.s-bulk__n {
  font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-surface);
  font-variant-numeric: tabular-nums;
}
.s-bulk .s-select { flex: 1 1 150px; min-width: 0; }
@media (min-width: 992px) {
  .s-bulk { left: 50%; right: auto; transform: translateX(-50%); width: min(620px, calc(100vw - 48px)); flex-wrap: nowrap; }
}

/* Emergency — outline, not a solid slab: present without shouting. */
.s-sos {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 10px;
  margin-bottom: var(--s-sp-3);
  border: 2px solid var(--s-bad);
  border-radius: var(--s-r);
  background: var(--s-surface);
  color: var(--s-bad);
  font-size: var(--s-fs-sm); font-weight: 900;
  text-decoration: none;
}

/* ── The drawer's contents (13 Sep 2026) ────────────────────────────────
   Modelled on the "More" and "You" screens of the team and finance apps
   people already trust: identity first, one row of big pinned tiles, then
   every other page as a compact two-column list under a heading, and the
   account rows last. Nothing is truncated, and colour is confined to a
   small icon on each row, so a drawer of fifty pages reads as a list to
   scan rather than fifty squares to decode. The classes the edit-mode and
   search scripts hang on (.s-tile, .s-tile__lb, .s-tile__act, the ids) are
   unchanged; only their layout is. */

/* Who is signed in. The whole card is the link to their profile. */
.s-drawer__who {
  display: flex; align-items: center; gap: var(--s-sp-3);
  padding: 10px 12px 10px 10px;
  margin-bottom: var(--s-sp-3);
  border-radius: var(--s-r-lg);
  background: var(--s-bg);
  text-decoration: none; color: var(--s-ink);
}
.s-drawer__who-body { flex: 1; min-width: 0; }
.s-drawer__who-name {
  display: block;
  font-size: var(--s-fs-md); font-weight: 900; line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.s-drawer__who-role {
  display: block; margin-top: 2px;
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted);
}

/* Tile grid: the staff shortcuts, favourites and the pinned row. */
.s-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px 6px; }
.s-tiles__label {
  margin: var(--s-sp-4) 2px 8px;
  font-size: var(--s-fs-xs); font-weight: 900;
  letter-spacing: .09em; text-transform: uppercase;
  color: var(--s-muted);
}
.s-tile { position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px; text-decoration: none; }
.s-tile__ic {
  width: 52px; height: 52px;
  border-radius: 16px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 20px;
  background: var(--s-slate-bg); color: var(--s-slate);
}
.s-tile__lb {
  max-width: 100%;
  font-size: var(--s-fs-xs); font-weight: 800;
  color: var(--s-ink); text-align: center;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.s-tile--info   .s-tile__ic { background: var(--s-info-bg);   color: var(--s-info); }
.s-tile--good   .s-tile__ic { background: var(--s-good-bg);   color: var(--s-good); }
.s-tile--warn   .s-tile__ic { background: var(--s-warn-bg);   color: var(--s-warn); }
.s-tile--bad    .s-tile__ic { background: var(--s-bad-bg);    color: var(--s-bad); }
.s-tile--violet .s-tile__ic { background: var(--s-violet-bg); color: var(--s-violet); }
.s-tile--teal   .s-tile__ic { background: var(--s-teal-bg);   color: var(--s-teal); }
.s-tile--pink   .s-tile__ic { background: var(--s-pink-bg);   color: var(--s-pink); }
.s-tile--orange .s-tile__ic { background: var(--s-orange-bg); color: var(--s-orange); }
.s-tile--slate  .s-tile__ic { background: var(--s-slate-bg);  color: var(--s-slate); }

/* The pinned row is the hero: a touch larger than the shortcuts above it. */
.s-tiles--pinned .s-tile__ic { width: 56px; height: 56px; font-size: 22px; }
.s-tiles--quick { margin-top: var(--s-sp-1); }

/* A section's pages: two columns of rows on one tinted card. The label
   wraps to a second line instead of being cut, so "Pulford Pirates Y5" is
   "Pulford Pirates Y5". The page you are on sits on a white pad. */
.s-tiles--list {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2px;
  padding: 4px;
  border-radius: var(--s-r-lg);
  background: var(--s-bg);
}
.s-tiles--list .s-tile {
  flex-direction: row; align-items: center; gap: 10px;
  min-height: 46px;
  padding: 6px 8px;
  border-radius: 12px;
}
.s-tiles--list .s-tile__ic {
  width: 32px; height: 32px; flex-shrink: 0;
  border-radius: 10px;
  font-size: 14px;
}
.s-tiles--list .s-tile__lb {
  flex: 1; min-width: 0;
  font-size: var(--s-fs-sm); font-weight: 700; line-height: 1.2;
  text-align: left; white-space: normal;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.s-tiles--list .s-tile.is-on { background: var(--s-surface); box-shadow: var(--s-shadow-sm); }
.s-tiles--list .s-tile.is-on .s-tile__lb { color: var(--s-brand-ink); }
.s-tiles--list .s-tile.is-on .s-tile__ic { background: var(--s-brand); color: var(--s-brand-on); }
@media (hover: hover) {
  .s-tiles--list .s-tile:hover { background: var(--s-surface); }
}

/* Edit mode: pin the eight you reach for, hide the ones you never open.
   The two controls sit on the row itself rather than in a separate list, so
   the drawer you are editing is the drawer you get. They only exist while the
   panel carries .is-editing; the rest of the time a row is just a link. */
.s-tile__act {
  display: none;
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 26px; height: 26px;
  border-radius: var(--s-r-pill);
  align-items: center; justify-content: center;
  font-size: 11px; line-height: 1;
  background: var(--s-surface); color: var(--s-muted);
  border: 1px solid var(--s-line);
  box-shadow: var(--s-shadow-sm);
  cursor: pointer;
}
.s-tile__act--pin  { right: 36px; }
.s-tile__act--hide { right: 6px; }
.s-drawer__panel.is-editing .s-tile__act { display: inline-flex; }
.s-drawer__panel.is-editing .s-tile { cursor: default; }
/* While editing, the list drops to one column: two controls on every
   row leave a two-column label no room. */
.s-drawer__panel.is-editing .s-tiles--list { grid-template-columns: minmax(0, 1fr); }
.s-drawer__panel.is-editing .s-tiles--list .s-tile__lb { padding-right: 64px; }
.s-tile.is-pinned .s-tile__act--pin {
  background: var(--s-warn); color: #fff; border-color: var(--s-warn);
}
/* A hidden row is gone from the drawer, and comes back while editing at
   half strength so it can be put back. */
.s-tile.is-hidden { display: none; }
.s-drawer__panel.is-editing .s-tile.is-hidden { display: flex; opacity: .38; }
.s-drawer__panel.is-editing .s-tile.is-hidden .s-tile__act--hide {
  background: var(--s-bad); color: #fff; border-color: var(--s-bad);
  opacity: 1;
}
.s-drawer__panel.is-editing .s-tile.is-hidden .s-tile__act { opacity: 1; }

/* The pinned row is a copy of tiles that live further down, so while the
   drawer is being edited it goes away: two rows answering to one page would
   let someone pin a tile in one place and hide it in the other. */
.s-drawer__panel.is-editing #sDrawerPinned,
.s-drawer__panel.is-editing #sDrawerPinnedLabel { display: none; }

/* A section heading with the Edit control on its right. */
.s-drawer__sechead {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--s-sp-2);
  margin: var(--s-sp-4) 2px 8px;
}
.s-drawer__sechead .s-tiles__label { margin: 0; }
.s-drawer__editbar {
  display: flex; align-items: center; gap: var(--s-sp-2);
  margin-left: auto;
}
.s-drawer__editbtn {
  flex: 0 0 auto;
  padding: 5px 12px;
  border-radius: var(--s-r-pill);
  border: 1px solid var(--s-line);
  background: var(--s-surface); color: var(--s-brand-ink);
  font-family: inherit; font-size: var(--s-fs-xs); font-weight: 900;
  cursor: pointer;
}
.s-drawer__panel.is-editing .s-drawer__editbtn {
  background: var(--s-brand); color: var(--s-brand-on); border-color: var(--s-brand);
}
.s-drawer__panel.is-editing .s-drawer__editbar { flex: 1 1 100%; }
.s-drawer__editnote {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--s-fs-xs); font-weight: 700;
  color: var(--s-muted);
}

/* Account and Help: the shell's own rows, with a smaller icon than a page
   uses so the card stays short. Log out is a quiet button, not a red one:
   it is the least used thing in the drawer. */
.s-drawer__rows { margin-bottom: var(--s-sp-2); }
.s-drawer__rows .s-row { padding: 10px var(--s-sp-3); }
.s-drawer__rows .s-row__ic { width: 32px; height: 32px; border-radius: 10px; font-size: 14px; }
.s-drawer__rows .s-row__title { font-size: var(--s-fs-sm); font-weight: 800; }
.s-drawer__rows .s-row__sub { font-size: var(--s-fs-xs); }
.s-drawer__out { margin: var(--s-sp-2) 0 var(--s-sp-1); }
.s-drawer__out .s-btn { color: var(--s-muted); }

/* ── 3b. THE BAR CUSTOMISER ────────────────────────────── */
/* Opens from the drawer, over everything, and edits the bar in place. Reorder
   is arrows rather than drag: a drag handle on a 32px row is a coin toss on a
   phone, and arrows work for a thumb, a keyboard and a screen reader alike. */
.s-navcust {
  position: fixed; inset: 0;
  z-index: 64;
  display: flex; align-items: flex-end; justify-content: center;
  background: rgba(15,23,42,.45);
  opacity: 0;
  transition: opacity .28s;
}
.s-navcust.is-open { opacity: 1; }
.s-navcust__panel {
  width: 100%; max-width: 560px; max-height: 84dvh;
  display: flex; flex-direction: column;
  background: var(--s-bg);
  border-radius: var(--s-r-xl) var(--s-r-xl) 0 0;
  box-shadow: var(--s-shadow-lg);
  padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
  transform: translateY(26px);
  transition: transform .28s;
}
.s-navcust.is-open .s-navcust__panel { transform: none; }
.s-navcust__grab {
  width: 44px; height: 4px; margin: 10px auto 2px;
  border-radius: var(--s-r-pill);
  background: var(--s-line);
}
.s-navcust__title {
  margin: 8px 18px 0;
  font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink);
}
.s-navcust__sub {
  margin: 4px 18px 10px;
  font-size: var(--s-fs-xs); line-height: 1.5; color: var(--s-ink-2);
}
.s-navcust__body {
  flex: 1; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  padding: 0 14px 12px;
}

/* The chosen shortcuts, in bar order. */
.s-navcust__list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 6px;
}
.s-navcust__slot {
  display: flex; align-items: center; gap: 9px;
  padding: 7px 9px;
  background: var(--s-surface);
  border: 1.5px solid var(--s-line);
  border-radius: var(--s-r);
}
.s-navcust__pos {
  width: 16px; flex: none; text-align: center;
  font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-muted);
  font-variant-numeric: tabular-nums;
}
.s-navcust__ic {
  width: 30px; height: 30px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 9px;
  background: var(--s-surface-2); color: var(--s-ink-2);
  font-size: 13px;
}
.s-navcust__name {
  flex: 1; min-width: 0;
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.s-navcust__mv {
  width: 32px; height: 32px; flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; border-radius: var(--s-r-sm);
  background: var(--s-surface-2); color: var(--s-ink-2);
  font-size: 12px; cursor: pointer;
}
.s-navcust__mv:disabled { opacity: .3; cursor: default; }
.s-navcust__mv--drop { color: var(--s-bad); }
.s-navcust__none {
  padding: 16px 12px; text-align: center;
  border: 1.5px dashed var(--s-line); border-radius: var(--s-r);
  font-size: var(--s-fs-xs); color: var(--s-muted);
}

/* Everything this user is allowed to put in the bar. */
.s-navcust__label {
  margin: 16px 4px 8px;
  font-size: var(--s-fs-xs); font-weight: 900;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--s-muted);
}
.s-navcust__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.s-navcust__tile {
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  min-height: 76px; padding: 10px 4px;
  border: 1.5px solid var(--s-line); border-radius: var(--s-r);
  background: var(--s-surface); color: var(--s-ink-2);
  font: inherit; cursor: pointer;
}
.s-navcust__tile i { font-size: 17px; }
.s-navcust__tile span {
  font-size: 9.5px; font-weight: 800; line-height: 1.2; text-align: center;
}
.s-navcust__tile.is-on {
  border-color: var(--s-good); background: var(--s-good-bg); color: var(--s-good);
}
.s-navcust__tile.is-full:not(.is-on) { opacity: .45; }

.s-navcust__err {
  display: none;
  margin: 10px 0 0; padding: 9px 11px;
  border-radius: var(--s-r-sm);
  background: var(--s-bad-bg); color: var(--s-bad);
  font-size: var(--s-fs-xs); font-weight: 800;
}
.s-navcust__err.is-on { display: block; }

.s-navcust__foot {
  display: flex; gap: 10px;
  padding: 10px 14px 0;
  border-top: 1.5px solid var(--s-line-soft);
}
.s-navcust__foot .s-btn { flex: 1; }

@media (min-width: 480px) {
  .s-navcust__grid { grid-template-columns: repeat(5, 1fr); }
  .s-navcust__tile span { font-size: 10.5px; }
}

/* ── 4. PAGE FURNITURE ─────────────────────────────────────────────────── */
.s-h1 {
  font-size: var(--s-fs-2xl); font-weight: 900;
  color: var(--s-ink); line-height: 1.1; letter-spacing: -0.02em;
}
.s-h2 { font-size: var(--s-fs-xl); font-weight: 900; color: var(--s-ink); letter-spacing: -0.01em; }
.s-sub { margin-top: 2px; font-size: var(--s-fs-md); font-weight: 700; color: var(--s-ink-2); }
.s-label {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  margin-top: var(--s-sp-2);
  font-size: var(--s-fs-xs); font-weight: 900;
  letter-spacing: .09em; text-transform: uppercase;
  color: var(--s-slate);
}
.s-label__note {
  font-size: var(--s-fs-sm); font-weight: 800;
  letter-spacing: .02em; text-transform: none;
  color: var(--s-muted);
}
/* An action in a label row sits at the far right, opposite its heading. It
   was pulled in beside the heading for a while because the floating drawer
   button covered the right-hand edge; that button is gone, so "Sync to
   calendar" gets the corner back and reads in full. */
.s-label--act { justify-content: space-between; }
/* A label's uppercase and tracking belong to the label's own word. Anything
   actionable dropped in beside it — a "Manage" link, a sync button — reads as
   sentence case, so it doesn't have to fight the inherited casing inline. */
.s-label .s-btn,
.s-label .s-link,
.s-label a { text-transform: none; letter-spacing: 0; }
.s-muted { color: var(--s-muted); }
.s-nowrap { white-space: nowrap; }

/* ── 5. CARDS ──────────────────────────────────────────────────────────── */
.s-card {
  background: var(--s-surface);
  border: 1px solid var(--s-line);
  border-radius: var(--s-r-lg);
  box-shadow: var(--s-shadow-sm);
  padding: var(--s-sp-4);
}
.s-card--flush { padding: 0; overflow: hidden; }
.s-card--good { background: var(--s-good-bg); border-color: var(--s-good-line); }
.s-card--info { background: var(--s-info-bg); border-color: var(--s-info-line); }
.s-card--warn { background: var(--s-warn-bg); border-color: var(--s-warn-line); }
.s-card--bad  { background: var(--s-bad-bg);  border-color: var(--s-bad-line); }
/* Dashed = optional. Used where a card invites rather than demands. */
.s-card--dashed { border-style: dashed; box-shadow: none; }
.s-card__title { font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink); }
/* A numbered card in a guided sequence. Used where a page is a job with an
   order to it -- take the register, finish setup -- and not as decoration on
   a list of unrelated cards. Sits where the title's icon would. */
.s-stepn {
  width: 24px; height: 24px; flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  margin-right: 3px; vertical-align: -5px;
  font-size: var(--s-fs-sm); font-weight: 900;
  background: color-mix(in srgb, var(--s-brand-ink) 14%, var(--s-surface));
  color: var(--s-brand-ink);
}
/* A title that carries a number becomes a flex row, so the circle is truly
   centred against the text instead of hung -5px off the baseline, and the gap
   between them is one value rather than margin-plus-whatever-whitespace the
   template happened to leave. Falls back to the inline layout above where
   :has() is unsupported. */
.s-card__title:has(> .s-stepn),
.s-sect__t:has(> .s-stepn),
.s-row__title:has(> .s-stepn) { display: flex; align-items: center; gap: 7px; }
.s-card__title:has(> .s-stepn) > .s-stepn,
.s-sect__t:has(> .s-stepn) > .s-stepn,
.s-row__title:has(> .s-stepn) > .s-stepn { margin-right: 0; vertical-align: 0; }
.s-card__sub   { margin-top: 2px; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }
/* A sub-line under a title bar (title on the left, a pill on the right) gets
   a little more room than one under a bare title. Written against .s-v2 so
   it beats the paragraph reset, which is what zeroes a <p class="s-card__sub">. */
.s-v2 .s-card__sub--bar { margin-top: 6px; }
a.s-card { text-decoration: none; display: block; }

/* Status card: round glyph + title/sub, e.g. "You're all caught up". */
.s-status { display: flex; align-items: center; gap: var(--s-sp-3); }
.s-status__ic {
  width: 42px; height: 42px; flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 17px;
  background: var(--s-good); color: #fff;
  box-shadow: 0 2px 6px color-mix(in srgb, var(--s-good) 35%, transparent);
}
.s-status--info .s-status__ic { background: var(--s-info); box-shadow: 0 2px 6px color-mix(in srgb, var(--s-info) 35%, transparent); }
.s-status--warn .s-status__ic { background: var(--s-warn); box-shadow: 0 2px 6px color-mix(in srgb, var(--s-warn) 35%, transparent); }
.s-status--bad  .s-status__ic { background: var(--s-bad);  box-shadow: 0 2px 6px color-mix(in srgb, var(--s-bad) 35%, transparent); }
/* ...and the one that pins the disc to the top. .s-status centres, which is
   right for a title and a half-line under it; give it a title that wraps and
   two lines of explanation and the disc drifts down to sit beside the middle
   of the sentence, pointing at nothing. Against the first line of the thing it
   is marking. Same reasoning, and the same name, as .s-row--top and
   .s-between--top. */
.s-status--top { align-items: flex-start; }

/* ── 6. STATS ──────────────────────────────────────────────────────────── */
.s-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s-sp-2); margin-top: var(--s-sp-3); }
/* Four readings that belong together — a forecast is temp, rain, wind and
   storm risk, and dropping one to fit a three-column grid loses the one a
   coach cancels over. */
.s-stats--2 { grid-template-columns: repeat(2, 1fr); }
.s-stats--4 { grid-template-columns: repeat(4, 1fr); gap: 5px; }
.s-stats--4 .s-stat { padding: var(--s-sp-2) 3px; }
.s-stats--4 .s-stat__v { font-size: var(--s-fs-lg); }
/* Four tiles that will not survive four columns on a phone. A season summary
   carries a sub-line under each number -- the record, the working, the date --
   and at 375px four of those are three words wide and eight lines tall. Two by
   two down there, four across once there is room. */
.s-stats--quad { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 640px) { .s-stats--quad { grid-template-columns: repeat(4, 1fr); } }
/* ...but not when it is the first thing in a card: the card's padding is
   already the space, and the margin doubles it. */
.s-card > .s-stats:first-child { margin-top: 0; }
.s-stat {
  padding: var(--s-sp-3) var(--s-sp-2);
  border-radius: var(--s-r);
  background: var(--s-surface-2);
  text-align: center;
}
.s-stat__v {
  font-size: var(--s-fs-xl); font-weight: 900;
  color: var(--s-ink); line-height: 1.05;
  font-variant-numeric: tabular-nums;
}
.s-stat__k {
  margin-top: 3px;
  font-size: 10px; font-weight: 900;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--s-ink-2);
}
.s-stat--good   { background: var(--s-good-bg); }
.s-stat--good   .s-stat__v { color: var(--s-good); }
.s-stat--info   { background: var(--s-info-bg); }
.s-stat--info   .s-stat__v { color: var(--s-info); }
.s-stat--violet { background: var(--s-violet-bg); }
.s-stat--violet .s-stat__v { color: var(--s-violet); }
.s-stat--warn   { background: var(--s-warn-bg); }
.s-stat--warn   .s-stat__v { color: var(--s-warn); }
/* The set had good/info/violet/warn but no bad, so any page needing a red
   number reached for an inline style. Availability needs one. */
.s-stat--bad    { background: var(--s-bad-bg); }
.s-stat--bad    .s-stat__v { color: var(--s-bad); }
/* A number that is also the way to the thing it counts. A hub reading — four
   staff blocked, three families waiting — always has a page behind it, and a
   count you read and then go hunting for made you do the work twice. Same
   reset a.s-card needs, for the same reason. */
a.s-stat { display: block; text-decoration: none; }

/* Player snapshot: a card that is a link, headed by who it is and followed by
   an .s-stats grid. The grid auto-fits because a child's highlight tiles are
   earned — goals only appear once they are off the mark — so the row is three
   tiles wide for one player and five for another. */
.s-snap__head { display: flex; align-items: center; gap: 10px; }
.s-snap__name { font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink); }
.s-snap__team {
  padding: 3px 9px; border-radius: var(--s-r-pill);
  background: var(--s-surface-2); color: var(--s-ink-2);
  font-size: var(--s-fs-xs); font-weight: 800;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.s-snap__chev { margin-left: auto; flex-shrink: 0; color: var(--s-muted); font-size: 13px; }
.s-snap .s-stats { grid-template-columns: repeat(auto-fit, minmax(72px, 1fr)); }

/* ── 7. LIST ROWS & EVENTS ─────────────────────────────────────────────── */
.s-row {
  display: flex; align-items: center; gap: var(--s-sp-3);
  padding: var(--s-sp-3) var(--s-sp-4);
  border-bottom: 1px solid var(--s-line-soft);
  text-decoration: none;
  color: var(--s-ink);
}
.s-row:last-child { border-bottom: none; }
/* ...and no hairline under the last VISIBLE row either. A dashboard toggles
   rows in and out (a reply nudge, an all-clear), and :last-child is structural:
   it happily leaves a line hanging under the last row you can actually see.
   Browsers without :has() fall back to the rule above. */
.s-row:not(:has(~ .s-row:not(.is-hidden))) { border-bottom: none; }
.s-row__body { flex: 1; min-width: 0; }
/* Rows where the link is the body rather than the whole row, because something
   that is not part of the link — a dismiss button — sits alongside it. */
a.s-row__body { color: inherit; text-decoration: none; }
/* ...and the other shape: the row is not a link, but one line inside its body
   is. There is no chevron to carry the affordance, so it is marked the way a
   link in running text is -- underlined, but on the sheet's own hairline and
   clear of the descenders rather than through them. */
a.s-row__sub { text-decoration-color: var(--s-line); text-underline-offset: 3px; }
/* Blockified so the component doesn't care whether the page reached for a
   <div> or a <span>. A row's title and sub stack; a span that quietly stayed
   inline would run them together on one line and swallow the margin below. */
.s-row__title { display: block; font-size: var(--s-fs-md); font-weight: 800; color: var(--s-ink); }
.s-row__sub { display: block; margin-top: 2px; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }
.s-row__chev { flex-shrink: 0; color: var(--s-muted); font-size: 13px; transition: transform .18s; }
/* A row that does something on this page instead of going to another one.
   The component only owns the hairline underneath, so the border, ground
   and font a <button> arrives with have to go, or one row in the list turns
   up boxed and in the browser's own type. */
button.s-row {
  width: 100%; border: 0; border-bottom: 1px solid var(--s-line-soft);
  background: none; font: inherit; text-align: left; cursor: pointer;
}
button.s-row:last-child { border-bottom: none; }

/* Two things a row needs when it carries a control instead of going
   somewhere. A row whose second line is a field of its own -- a note under
   the name it belongs to -- has to be allowed to wrap, or the field joins
   the first line and squeezes the name to nothing. And a form control on a
   row sizes to itself: everything in the form section is width:100%, which
   is right for a stacked field and wrong for a number sitting at the end of
   a name. */
.s-row--wrap { flex-wrap: wrap; }
.s-row__full { flex: 1 0 100%; }
.s-row > .s-select {
  width: auto; flex-shrink: 0; min-width: 98px;
  font-variant-numeric: tabular-nums;
}

.s-avatar {
  width: 44px; height: 44px; flex-shrink: 0;
  border-radius: 50%;
  object-fit: cover;
  background: var(--s-surface-2);
  border: 2px solid var(--s-line);
}
.s-avatar--initial {
  display: inline-flex; align-items: center; justify-content: center;
  overflow: hidden;
  font-weight: 900; color: var(--s-ink-2);
}
/* The same span holds initials OR a headshot, so a photo has to fill it. */
.s-avatar--initial img { width: 100%; height: 100%; object-fit: cover; }
.s-avatar--sm { width: 30px; height: 30px; border-width: 1px; font-size: var(--s-fs-xs); }
.s-avatar--md { width: 34px; height: 34px; font-size: var(--s-fs-sm); }
/* On brand: a player's circle in a club-coloured disc rather than plain grey. */
.s-avatar--brand { background: var(--s-brand); border-color: var(--s-brand); color: var(--s-brand-on); }

/* A team's own mark: the badge it has uploaded, or the icon it picked instead.
   A rounded square rather than a circle, because a crest is drawn square and a
   circular crop eats the corners of most of them.

   Slate rather than the club accent. A club can choose white as its secondary
   colour -- this preview club has -- and a white plate behind a white-ish crest
   on a white card is an invisible mark. Slate is legible under both themes and
   no colour a club picks can defeat it. */
.s-crest {
  width: 32px; height: 32px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--s-r-sm); overflow: hidden;
  background: var(--s-slate-bg); color: var(--s-slate);
  font-size: 14px;
}
.s-crest img { width: 100%; height: 100%; object-fit: cover; }
.s-crest--lg { width: 44px; height: 44px; border-radius: var(--s-r); font-size: 19px; }

/* A sponsor's own mark. Unlike a crest or a headshot this is somebody else's
   artwork at an aspect ratio nobody here controls -- usually a wide wordmark --
   so it is fitted into the box whole rather than cropped to fill it, and the
   box is a fixed height so a row of them lines up whatever shape they are. */
.s-logo {
  display: flex; align-items: center; justify-content: center;
  height: 132px; padding: var(--s-sp-4);
  background: var(--s-logo-plate);
  border-bottom: 1px solid var(--s-line);
}
.s-logo img { max-height: 100%; max-width: 76%; object-fit: contain; }
.s-logo--sm { height: 78px; padding: var(--s-sp-3); border-bottom: none; }
.s-logo--sm img { max-width: 84%; }

/* The event card lives in section 10b as .s-ev — a static .s-event existed here
   first, then the expanding one replaced it. Two event cards in a design system
   whose whole purpose is one of everything would be a bad joke, so the static
   one is gone rather than kept "just in case". */

/* ── 8. CHIPS & BADGES ─────────────────────────────────────────────────── */
.s-chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px;
  border-radius: var(--s-r-pill);
  font-size: 10px; font-weight: 900;
  letter-spacing: .06em; text-transform: uppercase;
  background: var(--s-slate-bg); color: var(--s-slate);
}
.s-chip--good   { background: var(--s-good-bg);   color: var(--s-good); }
.s-chip--info   { background: var(--s-info-bg);   color: var(--s-info); }
.s-chip--warn   { background: var(--s-warn-bg);   color: var(--s-warn); }
.s-chip--bad    { background: var(--s-bad-bg);    color: var(--s-bad); }
.s-chip--violet { background: var(--s-violet-bg); color: var(--s-violet); }
/* A chip shouts because a chip is a status: UNPAID, CHECKED IN, 8 PRESENT.
   A person's name is not a status, and set in caps it stops reading as a name
   and starts reading as a code. The coach list further down the same card
   already prints names the way people write them, so these match it exactly
   rather than inventing a third way to print a name. */
.s-chip--name {
  padding: 3px 10px;
  font-size: var(--s-fs-sm); font-weight: 800;
  letter-spacing: 0; text-transform: none;
}

/* ── 9. BUTTONS ────────────────────────────────────────────────────────── */
.s-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 10px 16px;
  border: 1.5px solid transparent;
  border-radius: var(--s-r);
  font-size: var(--s-fs-sm); font-weight: 900;
  text-decoration: none; cursor: pointer;
  background: var(--s-surface-2); color: var(--s-ink);
}
.s-btn--primary { background: var(--s-accent); color: var(--s-accent-on); }
.s-btn--ghost   { background: var(--s-surface); border-color: var(--s-line); color: var(--s-ink); }
.s-btn--good    { background: var(--s-good); color: #fff; }
.s-btn--bad     { background: var(--s-surface); border-color: var(--s-bad); color: var(--s-bad); }
/* WhatsApp's own green, and the only brand colour in the sheet. A share
   button that looks like every other button gets read as "post this
   somewhere" and skipped; the green is what tells a club secretary at a
   glance that this is the thing they already use to talk to parents. Its own
   on-colour, because white text on #25d366 fails contrast. */
.s-btn--wa      { background: #25d366; color: #06281a; border-color: #25d366; }
.s-btn--sm      { padding: 6px 11px; font-size: var(--s-fs-xs); }
.s-btn--block   { width: 100%; }
/* A button whose whole label is its icon -- a remove, a close. Square, so it
   does not read as a word-shaped button that has lost its word. Never without
   an aria-label: there is nothing else for a screen reader to read.

   Square means an explicit height as well: padding: 0 leaves the height to
   the line box, which made the 40px-wide button 19px tall -- a squashed pill,
   and a tap target under half the size it looked. */
.s-btn--ico     { width: 40px; height: 40px; padding: 0; }
.s-btn--ico.s-btn--sm { width: 34px; height: 34px; padding: 0; }
.s-btn:disabled, .s-btn[aria-disabled="true"] { opacity: .55; cursor: not-allowed; }

/* ── 10. EMPTY STATES & ALERTS ─────────────────────────────────────────── */
.s-empty { padding: var(--s-sp-6) var(--s-sp-4); text-align: center; }
.s-empty__ic {
  width: 54px; height: 54px; margin: 0 auto var(--s-sp-3);
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 22px;
  background: var(--s-surface-2); color: var(--s-muted);
}
/* The same panel says "done" as well as "nothing here yet" — the referee
   pages land on one after answering. Only the glyph changes colour; the words
   under it are doing the work either way. Same pairs as .s-row__ic. */
.s-empty__ic--good { background: var(--s-good-bg); color: var(--s-good); }
.s-empty__ic--warn { background: var(--s-warn-bg); color: var(--s-warn); }
.s-empty__title { font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink); }
.s-empty__sub { margin-top: 4px; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }
/* An empty state that asks for something gets its buttons under the same
   centre line as the words above them. */
.s-empty__acts { margin-top: 12px; display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; }

.s-alert {
  display: flex; align-items: flex-start; gap: 10px;
  padding: var(--s-sp-3) var(--s-sp-4);
  border: 1px solid var(--s-info-line);
  border-radius: var(--s-r);
  background: var(--s-info-bg);
  color: var(--s-ink);
  font-size: var(--s-fs-sm); font-weight: 700;
}
.s-alert i { margin-top: 2px; color: var(--s-info); }
.s-alert--good { background: var(--s-good-bg); border-color: var(--s-good-line); }
.s-alert--good i { color: var(--s-good); }
.s-alert--warn { background: var(--s-warn-bg); border-color: var(--s-warn-line); }
.s-alert--warn i { color: var(--s-warn); }
.s-alert--bad  { background: var(--s-bad-bg);  border-color: var(--s-bad-line); }
.s-alert--bad i { color: var(--s-bad); }
/* An alert that carries more than one line: a lead ("Not saved.") and then one
   line per message. Django hands over two more often than you would think —
   a form's own clean() and the model's unique_together both fire on the same
   mistake — and without this they run together into a single sentence. */
.s-alert > div > div + div { margin-top: 3px; }
/* The flash the shell prints above the page body. It lands in the one corner
   the floating menu button covers, so it keeps the same 52px clear that the
   page head keeps, and gives it back at the same width the button leaves the
   content at. */
.s-alert--flash { padding-right: 58px; }
@media (min-width: 1240px) { .s-alert--flash { padding-right: var(--s-sp-4); } }
/* Somebody else's words, quoted back. A parent's feedback comment, a coach's
   note on a report. Ruled on the left rather than boxed: it is a passage of
   the page, not a card of its own. */
.s-quote { padding: 7px 0 7px 11px; border-left: 2px solid var(--s-line); }
.s-quote + .s-quote { margin-top: 6px; }
.s-quote__t { font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink); line-height: 1.5; }
.s-quote__by { margin-top: 2px; font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }
.s-quote--violet { border-left-color: var(--s-violet); }
/* A quote can also open a block rather than follow another quote: inside a
   row's body, where the volunteer queue puts what somebody wrote under their
   name, or straight under a card's title. Both want the gap that
   .s-quote + .s-quote gives, and neither is a place .s-quote can see. */
.s-row__sub + .s-quote { margin-top: 6px; }
/* .s-meta closing out a row's body -- the small print under what the row is
   about, like who pressed the button and when. It is a grid, so it brings no
   margin of its own to whatever it follows. */
.s-row__sub + .s-meta, .s-quote + .s-meta { margin-top: 5px; }
.s-card__title + .s-quote, .s-card__sub + .s-quote { margin-top: var(--s-sp-3); }

/* ══════════════════════════════════════════════════════════════════════════
   10b. DASHBOARD COMPONENTS

   Everything below came out of the parent and coach dashboards during the v2
   migration. None of it is dashboard-specific: the expanding event card is
   what fixtures, training and tournaments all need, the RSVP control is the
   same one the register uses, and the poll is the feed's. Each of these
   existed twice already — once as .sd-* and once as .cd-* — which is exactly
   the duplication v2 exists to end. Add to these; don't fork them.
   ══════════════════════════════════════════════════════════════════════════ */

/* A reading-width column. Dashboards and feeds want ~640px even on a big
   screen; a 1120px-wide list of events is a spreadsheet, not a home page. */
/* A reading-width column. The explicit width is not decoration: .s-main is
   a column flex container, and an item with auto cross-axis margins opts
   out of stretch, so this box would shrink-to-fit its own content and sit
   at 640px on a 375px phone with everything past the fold clipped away by
   the shell's overflow-x. width:100% pins it to the column again and the
   auto margins only centre it once there is room to. */
.s-narrow { width: 100%; max-width: 640px; margin: 0 auto; }

/* ── Row icon: the round glyph that opens an action row ─────────────────── */
.s-row__ic {
  width: 38px; height: 38px; flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 15px;
  background: var(--s-slate-bg); color: var(--s-slate);
}
.s-row__ic--good   { background: var(--s-good-bg);   color: var(--s-good); }
.s-row__ic--info   { background: var(--s-info-bg);   color: var(--s-info); }
.s-row__ic--warn   { background: var(--s-warn-bg);   color: var(--s-warn); }
.s-row__ic--bad    { background: var(--s-bad-bg);    color: var(--s-bad); }
.s-row__ic--violet { background: var(--s-violet-bg); color: var(--s-violet); }
/* A row led by an emoji the club picked rather than one of our glyphs -- an
   award type's badge. A glyph is a 15px shape in a tinted circle; an emoji at
   15px is a smudge, so it gets its own size, and the tint comes off because
   the emoji brings its own colour. */
.s-row__ic--emoji { font-size: 21px; background: var(--s-surface-2); }
.s-row--top { align-items: flex-start; }
/* A row whose actions will not fit at the end of it. .s-row__end is a narrow
   right-hand column, and two buttons with a select between them do not go in
   44px at 375px, so they sit under the row's own text instead -- taking the
   same gap the chips above them take, so a row with both reads as one block
   rather than three. */
.s-row__body > .s-inline { margin-top: 8px; }

/* ── A match the app is already fairly sure about ────────────────────
   A parent signed up, typed their child's name, and that name can only mean
   one player on this coach's teams. The page says so and puts a single button
   on it — rather than making the coach go and find the same person again in a
   dropdown of every parent in the club.

   Green because it is an offer that closes a row, not a warning about one:
   the page it lives on is a list of players with nobody attached, and this is
   the one-tap way to take a name off that list.

   The component is the <form> itself, not a wrapper round one. Confirming a
   guess changes the database, so it is a POST, and the element that has to
   carry the layout is the element that has to carry the action. */
.s-suggest {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 11px;
  border: 1px solid var(--s-good-line);
  border-radius: var(--s-r-sm);
  background: var(--s-good-bg);
}
.s-suggest__t {
  flex: 1; min-width: 0;
  font-size: var(--s-fs-sm); font-weight: 700;
  color: var(--s-ink); line-height: 1.45;
}
.s-suggest__t b { font-weight: 900; }
.s-suggest + .s-suggest { margin-top: 8px; }
/* Two parents can both have named the same child, and the guess belongs to
   the player it is about — so it sits inside the row's body, under the line
   that says nobody is linked yet. Same shape as .s-row__sub + .s-quote. */
.s-row__sub + .s-suggest, .s-row__title + .s-suggest { margin-top: 7px; }
/* A row for something that is off. A cancelled event, on a list a secretary
   is scanning rather than reading, needs to say so before the chips are read:
   v1 gave it a small pink pill and left the row otherwise identical to a live
   one, so the difference between "the presentation evening is on" and "the
   presentation evening is off" was three millimetres of colour.

   Struck AND stepped back, not one or the other. Opacity alone reads as
   "older" -- which is what a past event is -- and a strike alone survives on a
   row still competing at full weight with the live ones above it. */
.s-row--off { opacity: .66; }
.s-row--off .s-row__title { text-decoration: line-through; }
/* Some rows lead with a picture of the thing rather than a glyph standing in
   for it: a kit item the club has photographed. Square, because a shirt
   cropped into a circle loses its badge and its number, and bordered so an
   image on a white ground still has an edge. The glyph fallback keeps the
   same box, so a club with photos for half its kit still gets a straight
   left margin down the list. */
.s-row__ic--thumb {
  width: 46px; height: 46px;
  border-radius: var(--s-r-sm);
  border: 1px solid var(--s-line);
  background: var(--s-surface-2); color: var(--s-muted);
  overflow: hidden;
}
.s-row__ic--thumb img { width: 100%; height: 100%; object-fit: cover; }

/* Same box, but for somebody else's logo rather than our own photo. A crop
   takes the name out of a wordmark, so this one fits the image whole; and the
   plate stays light in both themes for the reason .s-logo does. */
.s-row__ic--logo { background: var(--s-logo-plate); padding: 3px; }
.s-row__ic--logo img { width: 100%; height: 100%; object-fit: contain; }

/* A quiet icon-only button — dismiss, acknowledge, close. */
.s-iconbtn {
  flex-shrink: 0;
  border: none; background: none;
  color: var(--s-muted);
  font-size: 15px; line-height: 1;
  padding: 9px 11px; margin: -6px -8px -6px 0;
  border-radius: var(--s-r-sm);
  cursor: pointer;
}
.s-iconbtn:hover { color: var(--s-bad); background: var(--s-bad-bg); }

/* A small text link inside a card body. */
.s-link {
  font-size: var(--s-fs-sm); font-weight: 800;
  color: var(--s-info); text-decoration: none;
  display: inline-flex; align-items: center; gap: 5px;
}
.s-link .fa-chevron-right { font-size: 10px; opacity: .7; }

/* ── Expanding event card ───────────────────────────────────────────────
   Each event is its own card with a gap, not a row in one long panel: six
   sessions in a bordered list read as a single grey block.

   The body opens with a grid track animating 0fr → 1fr, so it slides at its
   real height — no display flip, no max-height guess that clips a long note.
   Vertical padding has to live on the inner wrapper (padding can't collapse
   with the track), and visibility keeps the closed body out of tab order. */
.s-events { display: flex; flex-direction: column; gap: 10px; }
.s-ev {
  background: var(--s-surface);
  border: 1px solid var(--s-line);
  border-radius: var(--s-r-lg);
  box-shadow: var(--s-shadow-sm);
  overflow: hidden;
}
.s-ev__head {
  display: flex; gap: 10px; align-items: stretch;
  padding: var(--s-sp-3) var(--s-sp-4);
  cursor: pointer;
}
/* Left-aligned, not centred: centring floated a single-digit day into the
   middle of its 44px box, so "5" hung indented while "FRI" and "SEP" sat
   elsewhere. One shared left rail lines all three up with the card edge and
   pulls the title block left with it. */
/* A rule down the right of the date separates when from what, and a stretched
   column means it runs the full height of the card rather than stopping under
   "SEP". */
.s-ev__date {
  width: 48px; flex-shrink: 0; text-align: left;
  padding-right: 10px; border-right: 1px solid var(--s-line);
}
.s-ev__dow { font-size: 10px; font-weight: 900; letter-spacing: .06em; text-transform: uppercase; color: var(--s-muted); }
.s-ev__day { font-size: var(--s-fs-xl); font-weight: 900; color: var(--s-ink); line-height: 1.05; font-variant-numeric: tabular-nums; }
.s-ev__mon { font-size: 10px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; color: var(--s-muted); line-height: 1.1; }
.s-ev__main { flex: 1; min-width: 0; }
/* Titles WRAP to three lines rather than scrolling. A marquee slid long names
   ("Pirates U10 vs Saltney Town") permanently left-to-right, so they were
   unreadable at every moment except the ends of the loop. */
.s-ev__title {
  font-size: var(--s-fs-md); font-weight: 900; color: var(--s-ink); line-height: 1.25;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
}
.s-ev.is-open .s-ev__title { -webkit-line-clamp: unset; }
.s-ev__meta { margin-top: 2px; font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink-2); }
/* A kind chip ("Match", "Training", "Cancelled") opens that line, inline with
   the times that follow it. */
.s-ev__meta .s-chip { margin-right: 6px; vertical-align: 1px; }
/* Meet time on its own row. Run on to the end of the time line it wrapped on
   a 375px phone and left the meet time stranded below with no word beside it
   to say what it was. */
.s-ev__meet {
  margin-top: 3px; display: flex; align-items: center; gap: 5px;
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink-2);
}
.s-ev__meet i { font-size: 11px; opacity: .75; flex-shrink: 0; }
/* Shut, a card answers "when and where", and the meet time is a fourth line
   competing with the venue for the one glance it gets. Open it and the meet
   time is there. */
.s-ev:not(.is-open) .s-ev__meet { display: none; }
/* Venue on its own line: appended to the time row it was always the first
   thing ellipsised away on a 375px phone, which is the one detail a parent
   needs to decide whether they can get there. */
.s-ev__loc { margin-top: 3px; display: flex; align-items: center; gap: 5px; font-size: var(--s-fs-sm); color: var(--s-ink-2); }
.s-ev__loc i { font-size: 11px; opacity: .75; flex-shrink: 0; }
.s-ev__loc span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-ev.is-open .s-ev__loc span { overflow: visible; text-overflow: clip; white-space: normal; }
/* Two lines of coach detail without opening the card; the lot once open. */
.s-ev__note {
  margin-top: 4px; font-size: var(--s-fs-sm); color: var(--s-ink-2); line-height: 1.35;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.s-ev.is-open .s-ev__note { -webkit-line-clamp: unset; }
/* The reply chip sits under the subtitle, never beside the title — side by
   side it squeezed long team names to ~136px and clipped them. */
.s-ev__chips { margin-top: 6px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* The "you" pills are rebuilt as a group after every tap, so they need a
   wrapper the script can empty in one go — but the wrapper must not become a
   flex item itself, or three children's answers bunch up as one. */
.s-ev__chips > [data-you] { display: contents; }
.s-ev__chev { flex-shrink: 0; align-self: center; color: var(--s-muted); font-size: 13px; transition: transform .2s; }
.s-ev.is-open .s-ev__chev { transform: rotate(180deg); }

/* The three chips that live in that row, in the order a parent reads them:
   what I answered, whether anything is still owed, and the squad total.

   .s-you carries the answer itself rather than a neutral "replied" tick —
   remembering you said no is the whole point of showing it on a shut card. */
.s-you {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 8px;
  border-radius: var(--s-r-pill);
  font-size: 10px; font-weight: 900;
  max-width: 130px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.s-you i { font-size: 9px; }
.s-you--go { background: var(--s-good-bg); color: var(--s-good); }
.s-you--no { background: var(--s-bad-bg);  color: var(--s-bad); }

/* Amber, and only on events near enough to be worth chasing (data-soon). */
.s-reply {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 9px;
  border-radius: var(--s-r-pill);
  background: var(--s-warn-bg); color: var(--s-warn);
  font-size: 10px; font-weight: 900;
}

/* Muted at zero, green the moment anybody is going.
   Laid out inline, not as a flex row. As a flex container the space the
   template writes between the number and the word was thrown away and the
   5px gap stood in for it, so "8 going" opened up to nearly twice a word
   space and read as two separate things. The counts strip renders the same
   phrase inline; these two now space it identically. */
.s-going { font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-muted); white-space: nowrap; }
.s-going b { font-weight: 900; color: var(--s-ink-2); font-variant-numeric: tabular-nums; }
/* Green means somebody is going, not that you have answered. The count was
   only ever coloured once a family had replied, so a session with four
   players already confirmed sat in grey next to an amber "Awaiting
   response" chip and read as nobody. Zero stays muted: there is nothing
   good to report yet. */
.s-going.is-replied, .s-going.is-replied b,
.s-going.is-any, .s-going.is-any b { color: var(--s-good); }

/* Notices at the top of an opened card: a closed response window, a
   cancellation, or a refused reply — the last of which has to land somewhere
   the parent is already looking rather than being swallowed. All three want
   the same gap before the content beneath them.
   The refused one is found again by [data-rsvp-error], a data hook rather than
   a class, so the script can never grab one of the notices the template
   rendered and overwrite its text. */
.s-ev__notice { margin-bottom: 10px; }

/* Chat and fan-page links along the foot of an opened card, under their own
   hairline like every other row in the panel. */
.s-ev__links { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--s-line); display: flex; gap: 8px; flex-wrap: wrap; }
/* A chat link with nothing beside it is a row, not a chip. It takes the full
   width and sends its chevron to the far edge in the card's own grey, so it
   reads the same way as the chevron on the card head: tap here, go there. */
.s-ev__links > .s-link:only-child:has(.fa-chevron-right) { flex: 1 1 100%; }
.s-ev__links > .s-link:only-child > .fa-chevron-right { margin-left: auto; font-size: 13px; color: var(--s-muted); }

.s-ev__panel {
  display: grid; grid-template-rows: 0fr;
  padding: 0 var(--s-sp-4);
  visibility: hidden;
  transition: grid-template-rows .34s cubic-bezier(.4,0,.2,1), visibility 0s linear .34s;
}
.s-ev__panel-in { min-height: 0; overflow: hidden; padding: 0; transition: padding .34s cubic-bezier(.4,0,.2,1); }
.s-ev.is-open .s-ev__panel { grid-template-rows: 1fr; visibility: visible; transition: grid-template-rows .34s cubic-bezier(.4,0,.2,1), visibility 0s; }
.s-ev.is-open .s-ev__panel-in { padding: 2px 0 var(--s-sp-4); }

/* A called-off event: struck through, dimmed date, but still readable. */
.s-ev--off .s-ev__title { text-decoration: line-through; color: var(--s-muted); }
.s-ev--off .s-ev__date { opacity: .55; }

/* A card still waiting for this family's answer with the date inside seven
   days: a slight amber ring, no fill, so it is noticed without shouting.
   The ring goes with the chip when the answer is given (sdMarkReplied). */
.s-ev--due { border-color: var(--s-warn-line); box-shadow: 0 0 0 2px color-mix(in srgb, var(--s-warn) 28%, transparent), var(--s-shadow-sm); }

/* Rows past the near horizon, revealed by "show everything". */
.s-ev--later { display: none; }
.s-events.is-showing-all .s-ev--later { display: block; }

/* Weather runs along the BOTTOM edge, outside the collapsing body, so it
   reads without opening anything. Rendered only when there is a real cached
   forecast for this exact event, no placeholder and nothing invented.

   No tint behind it. An opened card is a stack of hairline rows (venue, who's
   attending, each child, the counts, chat) and a grey band at the foot made
   the forecast look like a footer rather than the last of those rows. The
   high-risk tint below stays: that one is a signal, not a container. */
.s-wx {
  display: flex; align-items: center; gap: 7px;
  padding: 7px var(--s-sp-4);
  border-top: 1px solid var(--s-line);
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-ink-2);
}
.s-wx > i { flex-shrink: 0; color: var(--s-info); }
.s-wx__t { color: var(--s-ink); font-weight: 900; flex-shrink: 0; }
/* Temperature and description are two facts, not one phrase. */
.s-wx__sep { color: var(--s-muted); flex-shrink: 0; }
.s-wx__d { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-wx__r { margin-left: auto; padding-left: 8px; flex-shrink: 0; font-weight: 900; white-space: nowrap; }
/* Baked ground is its own hazard, and the one in that line worth picking out. */
.s-wx__dry { color: var(--s-warn); font-weight: 900; }
.s-wx--low  .s-wx__r { color: var(--s-good); }
.s-wx--mid  .s-wx__r { color: var(--s-warn); }
.s-wx--high .s-wx__r { color: var(--s-bad); }
.s-wx--high {
  background: color-mix(in srgb, var(--s-bad) 7%, var(--s-surface));
  border-top-color: color-mix(in srgb, var(--s-bad) 30%, var(--s-line));
}

/* Tapping the venue opens the maps app; the page stays where it was.

   A row between hairlines rather than a bordered pill: an opened card is a
   short list of rows (venue, who's attending, each child, the counts, chat)
   and a box drawn round the first of them read as the only thing on the card
   that could be tapped. */
.s-maplink {
  display: flex; align-items: center; gap: 8px;
  margin: 2px 0 0;
  padding: 10px 0;
  border: none;
  border-top: 1px solid var(--s-line);
  border-bottom: 1px solid var(--s-line);
  background: none; color: var(--s-ink);
  font-size: var(--s-fs-sm); font-weight: 700; text-decoration: none;
}
.s-maplink i { color: var(--s-ink-2); flex-shrink: 0; }
.s-maplink__nm { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-maplink__go { margin-left: auto; flex-shrink: 0; display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-info); }
/* The pin on the left is furniture, so it stays in the body colour. The
   arrow on the right belongs to the word "Maps" and is part of the link, so
   it takes the link's colour rather than the greyed-out one .s-maplink i
   hands every icon inside the row. */
.s-maplink__go i { color: inherit; }

/* ── RSVP ───────────────────────────────────────────────────────────────
   One row per child: who, then yes/no. Locked rather than hidden once the
   response window closes, so a parent can still see what they answered. */
.s-rsvp { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-top: 1px solid var(--s-line); }
.s-rsvp__nm { flex: 1; min-width: 0; font-size: var(--s-fs-md); font-weight: 700; color: var(--s-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Yes and no are one control, not two buttons that happen to sit together:
   a single track with the answer filled in. Two separate pills read as two
   unrelated things to press, and an unanswered row looked the same as an
   answered one at arm's length. */
.s-rsvp__acts {
  margin-left: auto; flex-shrink: 0;
  display: flex; gap: 0;
  padding: 1px;
  border: 1px solid var(--s-line); border-radius: var(--s-r-pill);
  background: var(--s-bg);
}
.s-rsvp__acts.is-locked { pointer-events: none; opacity: .4; filter: grayscale(.4); }
/* A register of squares is three answers, not two, and it is repeated down a
   long list. It opts out of the track and keeps its separate buttons. */
.s-rsvp__acts:has(.s-toggle--sq) { padding: 0; border: 0; background: none; gap: 6px; }
.s-toggle {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 6px 13px;
  border: 1px solid var(--s-line); border-radius: var(--s-r-pill);
  background: var(--s-surface); color: var(--s-muted);
  font-size: var(--s-fs-sm); font-weight: 900;
  cursor: pointer; transition: transform .12s;
}
.s-toggle:active { transform: scale(.95); }
/* Inside the track the segments carry no chrome of their own: the track draws
   the outline once, and the chosen segment is the only thing filled in. These
   two sit ABOVE the .is-on rules on purpose. They match them for specificity,
   so whichever comes last wins, and the answer has to be the one that wins. */
.s-rsvp__acts .s-toggle { border-color: transparent; background: none; color: var(--s-ink-2); }
.s-rsvp__acts .s-toggle--sq { border-color: var(--s-line); background: var(--s-surface); color: var(--s-muted); }
.s-toggle--yes.is-on { background: var(--s-good); border-color: var(--s-good); color: #fff; }
.s-toggle--no.is-on  { background: var(--s-bad);  border-color: var(--s-bad);  color: #fff; }
/* Not every question is yes/no. Availability for a fixture is genuinely
   three-way — a parent who answers "probably" is telling the coach something
   an empty row does not.

   Tinted rather than filled, unlike yes and no. Two reasons: --s-warn is a
   dark amber picked to be readable ON --s-warn-bg, so as a slab it comes out
   heavier than either the green or the red and "maybe" ends up the loudest
   thing in the row; and a hedge should not look as settled as an answer. The
   border going to full --s-warn is what makes it read as chosen. */
.s-toggle--maybe.is-on { background: var(--s-warn-bg); border-color: var(--s-warn); color: var(--s-warn); }
.s-toggle:disabled { opacity: .5; }
/* Square, thumb-sized: an icon-only toggle repeated down a register.
   44px is the smallest target both Apple and Google will vouch for. */
.s-toggle--sq { width: 44px; height: 42px; padding: 0; justify-content: center;
  border-radius: var(--s-r-sm); font-size: var(--s-fs-md); }

/* Who's going: circles you tap to read a name. Same control on both sides of
   the app — parents see it on their card, coaches on the register. */
.s-faces { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; margin: 0; padding: 10px 0; }
.s-faces.is-empty { display: none; }
/* "Attending" as a word, not a caps label. Uppercase at 10px with tracking
   read as a section heading and pulled the eye ahead of the faces, which are
   the thing worth looking at. Sentence case at the card's own small size
   names the row and then gets out of the way. */
.s-faces__lbl { margin-right: 3px; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-muted); }
.s-faces__name { margin-left: 4px; min-height: 1em; font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink); }
.s-face {
  width: 28px; height: 28px; flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--s-brand); color: var(--s-brand-on);
  font-size: 10px; font-weight: 900;
  overflow: hidden;
  cursor: pointer; user-select: none; transition: transform .12s;
}
.s-face:active, .s-face.is-on {
  transform: scale(1.12);
  box-shadow: 0 0 0 2px var(--s-surface), 0 0 0 4px var(--s-brand);
}
/* A squad list is a wall of these, and the answer is the point, who is in and
   who is out, so the face carries it rather than a legend somewhere else. */
.s-face--yes { background: var(--s-good-bg); color: var(--s-good); }
.s-face--no  { background: var(--s-bad-bg);  color: var(--s-bad);  }
/* The ring is an overlay, not an inset shadow on the circle itself. An inset
   shadow paints underneath the element's own content, so a child with a
   headshot in it covered the ring completely and lost the answer: on the
   session register a photo sat in a row of ringed initials wearing no colour
   at all. A positioned pseudo-element paints above the image. currentColor
   means each modifier's own colour comes through without repeating it. */
.s-face--yes, .s-face--no, .s-face--maybe { position: relative; }
.s-face--yes::after, .s-face--no::after, .s-face--maybe::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  border-radius: 50%; box-shadow: inset 0 0 0 2px currentColor;
}
.s-face--lg { width: 36px; height: 36px; font-size: var(--s-fs-xs); overflow: hidden; }
/* Everyone past the sixth is a tally, not an initial. Six 28px circles plus
   this chip come to 231px against the 249px the row has on a 375px phone, so
   the pile never wraps to a second line. Deliberately NOT .s-face: sdFace()
   and sdSyncFace() both select on that class and would treat the tally as a
   player. */
.s-faces__more {
  width: 28px; height: 28px; flex-shrink: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--s-line); color: var(--s-ink-2);
  font-size: 10px; font-weight: 900; user-select: none;
}
.s-face img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }

/* ── Poll ───────────────────────────────────────────────────────────────
   The fill bar is a positioned child rather than a background gradient so it
   can animate its width from the vote response without a repaint of the row. */
/* Top-aligned: the question runs to two or three lines, the dismiss X does not. */
.s-poll__head { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 12px; }
.s-poll__q { flex: 1; font-size: var(--s-fs-md); font-weight: 900; color: var(--s-ink); line-height: 1.3; }
.s-poll__opt {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 10px;
  width: 100%; margin-bottom: 8px; padding: 11px 13px;
  border: 1px solid var(--s-line); border-radius: var(--s-r);
  background: var(--s-surface);
  font: inherit; text-align: left; cursor: pointer;
}
.s-poll__opt:active { transform: scale(.99); }
.s-poll__opt.is-mine { border-color: var(--s-accent); box-shadow: 0 0 0 1px var(--s-accent) inset; }
.s-poll__bar {
  position: absolute; inset: 0 auto 0 0; z-index: 0;
  height: 100%; width: 0;
  background: color-mix(in srgb, var(--s-accent) 14%, transparent);
  transition: width .4s ease;
}
.s-poll__opt.is-mine .s-poll__bar { background: color-mix(in srgb, var(--s-accent) 26%, transparent); }
.s-poll__txt { position: relative; z-index: 1; flex: 1; font-size: var(--s-fs-md); font-weight: 700; color: var(--s-ink); }
.s-poll__pct { position: relative; z-index: 1; min-width: 34px; text-align: right; font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-ink-2); font-variant-numeric: tabular-nums; }
.s-poll__foot { margin-top: 4px; font-size: var(--s-fs-xs); color: var(--s-muted); font-weight: 600; }

/* ── Quick links: the two-up tiles a dashboard ends on ──────────────────── */
.s-quicks { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 11px; }
.s-quick {
  position: relative; overflow: hidden;
  display: block; min-height: 92px; padding: var(--s-sp-4);
  border: 1px solid var(--s-line); border-radius: var(--s-r-lg);
  background: var(--s-surface); color: var(--s-ink);
  text-decoration: none;
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.s-quick:hover { border-color: var(--s-accent); box-shadow: var(--s-shadow); }
.s-quick:active { transform: scale(.97); }
.s-quick__ic { font-size: 19px; color: var(--s-accent-text); }
.s-quick__lb { margin-top: 7px; font-size: var(--s-fs-md); font-weight: 900; }
.s-quick__sub { margin-top: 1px; font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.s-quick__badge {
  position: absolute; top: 10px; right: 11px;
  min-width: 20px; height: 20px; padding: 0 6px;
  border-radius: var(--s-r-pill);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--s-accent); color: var(--s-accent-on);
  font-size: var(--s-fs-xs); font-weight: 900;
}
.s-quick__badge.is-zero { background: var(--s-line); color: var(--s-muted); }
.s-quick--wide { grid-column: span 2; min-height: 118px; padding: 0; }
/* A wide tile is unpadded because its usual filling is a photo that bleeds to
   the edges. When the filling is words instead, they need the padding back. */
.s-quick__body { padding: var(--s-sp-4); }

/* A slow cross-fade of photos or headlines inside a wide tile. */
.s-slides { position: absolute; inset: 0; }
.s-slides__i { position: absolute; inset: 0; opacity: 0; transition: opacity 1.1s ease; }
.s-slides__i.is-on { opacity: 1; }
.s-slides__i img { width: 100%; height: 100%; object-fit: cover; }
.s-slides__cap {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 10px 13px;
  background: linear-gradient(transparent, rgba(2,6,23,.72));
  color: #fff;
}
.s-slides__lbl { font-size: var(--s-fs-xs); font-weight: 900; opacity: .85; letter-spacing: .05em; text-transform: uppercase; }
.s-slides__t { font-size: var(--s-fs-md); font-weight: 900; }

/* ── A collapsed group, so the everyday choices stay calm ───────────────── */
.s-more { margin-top: 12px; }
.s-more > summary {
  display: flex; align-items: center; gap: 8px;
  padding: 11px var(--s-sp-4);
  border: 1px solid var(--s-line); border-radius: var(--s-r);
  background: var(--s-surface); color: var(--s-ink);
  font-size: var(--s-fs-sm); font-weight: 900;
  list-style: none; cursor: pointer;
}
.s-more > summary::-webkit-details-marker { display: none; }
.s-more__chev { margin-left: auto; color: var(--s-muted); transition: transform .2s; }
.s-more[open] > .s-more__summary .s-more__chev,
.s-more[open] > summary .s-more__chev { transform: rotate(180deg); }
/* Gap above the revealed contents, and only once they are revealed. */
.s-more[open] > summary { margin-bottom: 11px; }

/* ── A card that opens: a team, a month, a category ────────────────
   .s-more is a bare bar with its contents underneath it. This is the other
   shape — the header *is* the top of the card, and what opens is inside the
   same border. Use it where the group is a real thing people name (a team),
   not a stash of secondary links.

   Sits on .s-card .s-card--flush, which already supplies the border, the
   radius and the padding reset. */
.s-group > summary {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 12px var(--s-sp-4);
  background: var(--s-surface-2);
  border-bottom: 1px solid var(--s-line);
  list-style: none; cursor: pointer;
}
.s-group > summary::-webkit-details-marker { display: none; }
.s-group__t { font-size: var(--s-fs-md); font-weight: 900; color: var(--s-ink); }
.s-group[open] > summary .s-more__chev { transform: rotate(180deg); }
/* Closed, the header is the entire card, so it must not rule a line onto
   nothing. */
.s-group:not([open]) > summary { border-bottom: none; }

/* ── The action group on a dashboard ────────────────────────────────────
   "Action required" on the coach dashboard and "When you get a minute" on
   the parent's: one collapsed card whose header carries the count, so the
   number is read on the way past and the rows cost a tap (Ben, 11 Sep 2026).
   The count is the same red as an unread badge; at zero it gives way to a
   quiet "All clear" so a closed card never reads as an empty one. */
.s-group--act > summary { flex-wrap: nowrap; }
.s-group--act > summary .s-group__t { flex: 1; min-width: 0; }
.s-group__count {
  min-width: 22px; height: 22px; padding: 0 7px;
  border-radius: var(--s-r-pill);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--s-bad); color: #fff;
  font-size: var(--s-fs-xs); font-weight: 900; line-height: 1;
}
.s-group__note { font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-good); }
/* A sub-heading between runs of rows inside the group: "Registers to take
   today" over its rows. Tinted like the card floor so it is not read as a row. */
.s-group__sub {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 8px var(--s-sp-4) 6px;
  border-top: 1px solid var(--s-line-soft);
  background: var(--s-surface-2);
  font-size: var(--s-fs-xs); font-weight: 900; letter-spacing: .04em; text-transform: uppercase;
  color: var(--s-muted);
}
.s-group__subnote { font-weight: 700; text-transform: none; letter-spacing: 0; }
/* An explanation folded in at the foot of the group, inside the same border. */
.s-group__foot { margin: 0; border-top: 1px solid var(--s-line-soft); }
.s-group__foot > summary { border: 0; border-radius: 0; background: var(--s-surface-2); }
.s-group__foot[open] > summary { margin-bottom: 0; }
.s-group__foot > p { padding: 0 var(--s-sp-4) var(--s-sp-3); margin: 0; }

/* ── A strip of actions along the bottom of a card ───────────────────────
   Actions belonging to the card as a whole, rather than to a row inside it:
   a team's settings, a tournament's manage/schedule/edit. Tinted so it reads
   as the card's floor and not as one more row of content.

   The old pages hid these behind a cog dropdown, which is why nobody ever
   found "coaching staff". Down here they cost a glance and no clicks. */
.s-acts {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  padding: var(--s-sp-3);
  border-top: 1px solid var(--s-line);
  background: var(--s-surface-2);
}
/* Pushes the destructive one to the far end, away from the thumb's path.
   .s-acts__end is for when that button is wrapped in its own <form> — a POST
   is the only honest way to spell "delete", and the auto margin has to sit on
   the flex item, which is then the form and not the button. */
.s-acts .s-btn--bad, .s-acts__end { margin-left: auto; }

/* The tail of a card that shows the newest few of something: one way out, to
   the page that has the lot. A rule and a link rather than a button, because
   it is an exit from the card and not the thing the card is for. */
.s-seeall {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  margin-top: var(--s-sp-3); padding-top: var(--s-sp-3);
  border-top: 1px solid var(--s-line-soft);
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-brand);
  text-decoration: none;
}
.s-seeall:hover { text-decoration: underline; }

/* An explanatory sentence inside a card: what the numbers above it mean, or
   what the control below it will do. Muted, because it is context and not
   the content. */
.s-note {
  margin-top: var(--s-sp-3);
  font-size: var(--s-fs-sm); font-weight: 600;
  color: var(--s-ink-2); line-height: 1.5;
}
.s-note:first-child { margin-top: 0; }
.s-note .s-chip { margin-right: 5px; vertical-align: 1px; }
/* A note that introduces a key/value list rather than ending in one --
   the sentence and the first key would otherwise sit on top of each
   other, because .s-kv__i:first-child deliberately has no top padding. */
.s-note > .s-kv { margin-top: var(--s-sp-3); }
/* A note that leads into whatever follows it -- a form, a list -- is a
   lead-in, not a caption, so it needs air under it too. Only when it is
   actually leading into something: a note that ends the card doesn't. */
.s-note:not(:last-child) { margin-bottom: var(--s-sp-3); }

/* A short list where each line is its own point rather than a key and a
   value: what a save is about to do, what a setting changes. The house has
   had no list primitive at all until now, so an unclassed <ul> inherited the
   reset's zero padding and lost its markers entirely. */
.s-bullets {
  margin: 0; padding-left: 18px;
  font-size: var(--s-fs-sm); font-weight: 600;
  color: var(--s-ink-2); line-height: 1.55;
}
.s-bullets li + li { margin-top: 5px; }
.s-bullets strong, .s-bullets b { color: var(--s-ink); font-weight: 800; }
.s-card__title + .s-bullets { margin-top: var(--s-sp-3); }

/* A paragraph of somebody else's typing -- a session plan's objectives, a note
   a coach wrote into a textarea. pre-line because the line breaks they put in
   are part of what they wrote. */
.s-prose {
  margin: 6px 0 0;
  font-size: var(--s-fs-sm); line-height: 1.55;
  color: var(--s-ink); white-space: pre-line;
}
/* When the paragraph is not a note attached to something else but the whole
   reason the page exists -- a broadcast's message, which is all a parent
   opened it for -- it is set at reading size rather than note size. */
.s-prose--lead { font-size: var(--s-fs-md); line-height: 1.6; }

/* Somebody else's *document* -- the body of a club policy, typed into a rich
   admin field and handed over as HTML. Different animal from .s-prose: that
   one is pre-line because the line breaks a coach typed are the formatting.
   Here the formatting is real markup, so pre-line would double every gap and
   the tags need styling of their own.

   Scoped by descendant selector rather than by class, because nothing in the
   club's saved HTML knows about this design system. Everything is set from
   the same tokens the rest of the page uses, so a policy written in 2019 in
   whatever the old editor produced still reads like the app around it. */
.s-doc {
  font-size: var(--s-fs-sm); line-height: 1.6; color: var(--s-ink);
  overflow-wrap: anywhere; overflow-x: auto;
}
.s-doc > :first-child { margin-top: 0; }
.s-doc > :last-child  { margin-bottom: 0; }
.s-doc p, .s-doc ul, .s-doc ol { margin: 0 0 10px; }
.s-doc h1, .s-doc h2, .s-doc h3, .s-doc h4 {
  margin: 14px 0 6px;
  font-size: var(--s-fs-md); font-weight: 900; color: var(--s-ink);
}
.s-doc ul, .s-doc ol { padding-left: 20px; }
.s-doc li { margin: 0 0 4px; }
.s-doc a { color: var(--s-info); text-decoration: underline; }
.s-doc strong, .s-doc b { font-weight: 800; color: var(--s-ink); }
.s-doc blockquote {
  margin: 10px 0; padding: 2px 0 2px 12px;
  border-left: 3px solid var(--s-line); color: var(--s-ink-2);
}
/* Nothing in the saved HTML can be wrapped, so the container is the only
   place to catch what will not fit: a pasted table, a <pre>, a URL with no
   break in it. It scrolls inside itself so the page body never does. */
.s-doc table { width: 100%; border-collapse: collapse; font-size: var(--s-fs-xs); }
.s-doc th, .s-doc td {
  padding: 6px 8px; text-align: left; border-bottom: 1px solid var(--s-line);
}
.s-doc th { font-weight: 900; color: var(--s-ink); }
.s-doc img { max-width: 100%; height: auto; border-radius: var(--s-r-sm); }

/* An embedded document -- the PDF a training plan carries, shown in place so
   a coach can read it without leaving the page.

   Only where there is room for it. At 375px a 16/9 frame is about 200px tall,
   and mobile browsers hand an inline PDF to a viewer that cannot be scrolled
   inside a frame anyway, so below the breakpoint the button beside it is the
   whole answer. Declared as display:none first and switched on inside the
   query, rather than leaning on .s-desk-only, which only restores flex. */
.s-embed { display: none; }
@media (min-width: 992px) {
  .s-embed {
    display: block; width: 100%; aspect-ratio: 16 / 9;
    margin-top: var(--s-sp-3);
    border: 1px solid var(--s-line); border-radius: var(--s-r);
    overflow: hidden; background: var(--s-surface-2);
  }
  .s-embed > iframe { display: block; width: 100%; height: 100%; border: 0; }
}

/* A single inline icon carrying meaning by colour -- a key beside a coach's
   name saying they can open up. Not a badge: it sits in the run of the text. */
.s-ic-good { margin-left: 5px; font-size: .82em; color: var(--s-good); }

/* ── What is wrong with this thing ───────────────────────────────────────
   A list of reasons something is not ready. Each line is a sentence a person
   can act on, so it is set as prose with a marker, not as a row of chips. */
.s-issues { margin: 10px 0 0; padding: 0; list-style: none; display: grid; gap: 5px; }
/* Each issue is a short phrase ("no coach assigned"), and six of them in one
   column on a wide screen is a metre of scrolling for a card the reader is
   meant to take in at a glance. auto-fill rather than a breakpoint, because
   these cards sit two abreast on a laptop and the window width says nothing
   about how much room the card itself has. */
.s-issues {
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  column-gap: var(--s-sp-5);
}
.s-issues li {
  display: flex; gap: 8px;
  font-size: var(--s-fs-sm); font-weight: 600; line-height: 1.45; color: var(--s-ink-2);
}
.s-issues li i { flex: none; margin-top: 3px; font-size: .85em; color: var(--s-warn); }

/* ── A chip that opens ───────────────────────────────────────────────────
   Inside a table cell, where "3 to fix" is enough until it isn't. The summary
   is the chip; the detail unrolls under it without moving the row. */
.s-drop > summary { list-style: none; cursor: pointer; }
.s-drop > summary::-webkit-details-marker { display: none; }
.s-drop > summary::marker { content: ''; }
.s-drop__list {
  margin: 7px 0 0; padding-left: 17px;
  font-size: var(--s-fs-xs); font-weight: 600; line-height: 1.5; color: var(--s-ink-2);
}
.s-drop__list li { margin-bottom: 2px; }
/* Two of these side by side -- what is stopping it, and who has not paid.
   Top-aligned, so opening one does not drag the other's chip down the card. */
.s-drops { align-items: flex-start; margin-top: 9px; }

/* ── A grid of cards ─────────────────────────────────────────────────────
   auto-fill rather than breakpoints: the shell's width is not the only thing
   that varies — the same grid is used inside a full-width page and inside a
   narrower panel, and a media query only knows about the former. */
.s-grid { display: grid; gap: var(--s-sp-3); grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); }
/* Cards in a grid stretch to the tallest one, so anything meant to be the
   card's floor has to be told to sink — otherwise the shorter card's action
   strip stops halfway down and the row reads as broken. */
.s-grid > .s-card { display: flex; flex-direction: column; }
.s-grid > .s-card > .s-acts { margin-top: auto; }
/* ...and the opposite case: cards with nothing at the bottom to sink. A
   disclosure, or a list that simply ends. Stretched to the tallest in the
   row, a team with one coach gets a hand's depth of empty card under it and
   the row reads as a rendering fault rather than as a short list. */
.s-grid--top { align-items: start; }

/* Padding back inside a .s-card--flush, for the parts that are not meant to
   run edge to edge. */
.s-pad { padding: var(--s-sp-3); }
/* Matches .s-card's own padding — for a flush card whose padded section sits
   beside plain cards on the same page and has to share their left edge. */
.s-pad--lg { padding: var(--s-sp-4); }

/* A string with no spaces in it -- a URL, a reference, a filename -- set inside
   something narrow. Grid and flex children size to their longest unbreakable
   word, so without this one long path pushes the whole card off the phone. */
.s-break { overflow-wrap: anywhere; }

/* ── A picture somebody sent in ──────────────────────────────────────────
   A screenshot attached to an issue report. It arrives at whatever size the
   reporter's phone took it: always wider than the card, usually taller than
   the screen. So it is sized to the card and capped in height -- the admin
   needs to see what is on it, not to scroll a photo to reach the buttons --
   and it sits on the surface colour, so a screenshot with a white edge still
   reads as a picture rather than as the card ending early. Tapping opens the
   file itself at full size. */
.s-shot {
  display: block; width: 100%;
  max-height: 420px; object-fit: contain;
  background: var(--s-surface-2);
}

/* --- A whole document shown inside a page ------------------------------
   An email template rendered for review. It is a complete HTML document with
   a <style> block of its own, so dropping it into the page inline would let
   its stylesheet repaint everything around it. It goes in a sandboxed frame
   instead: the email keeps its own styling and cannot reach the page. */
.s-frame {
  display: block; width: 100%; height: 420px;
  border: 1px solid var(--s-line);
  border-radius: var(--s-r);
  background: #fff;
}

/* A stack of small icon-led facts: where, when, who. Distinct from .s-fx__meta,
   which runs them along one line — this one is for a card with room. */
.s-meta { display: grid; gap: 3px; font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }
.s-meta i { width: 14px; margin-right: 4px; text-align: center; }
/* The same list, promoted: the two or three facts that are the point of the
   card rather than a footnote to it -- when it starts, where it is. Still a
   list of facts, so it stays .s-meta and does not become a heading. */
.s-meta--lg { font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink); gap: 5px; }
.s-meta--lg i { color: var(--s-muted); }
/* Two of these in a row -- the facts that decide, then the facts that qualify
   -- need a beat between them, or they read as one list that changes size
   halfway down for no reason. Same for the prose that follows one. */
.s-meta + .s-meta, .s-meta + .s-card__sub, .s-card__sub + .s-meta { margin-top: 7px; }
/* A document set straight under that strip of facts needs more than a beat:
   .s-doc kills the top margin on its own first child, so without this the
   policy's opening heading sits flush against the date and reads as part of
   it. Introduced by the code-of-conduct pages. */
.s-meta + .s-doc { margin-top: var(--s-sp-3); }

/* A list row on a laptop is a wide, short line with room to spare across it,
   so the two to four facts under the title sit side by side instead of one to
   a line. Flex rather than grid columns: a long one (a payment reference, a
   refund reason) wraps onto its own line instead of pushing the row wide. */
@media (min-width: 992px) {
  .s-row__body > .s-meta {
    display: flex; flex-wrap: wrap;
    column-gap: var(--s-sp-4); row-gap: 3px;
  }
  .s-row__body > .s-meta > * { min-width: 0; }
}

/* ── A cover photo at the top of a card ──────────────────────────────────
   The chips sit on the photo, so the photo needs a floor to be legible
   against: a cover can be any colour at all along its bottom edge. */
/* display:block matters: three templates put this class on a <span> or <a>,
   where an inline box ignores the height and the photo renders at its full
   natural size (Ben's "team pic is too big", 3 Sep 2026). */
.s-cover { display: block; position: relative; height: 138px; background: var(--s-surface-2); }
.s-cover img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* The team-page squad photo: half a typical photo's height at full width.
   --cover-x/--cover-y hold the saved crop focus (TeamPhoto.focus_x/_y). */
.s-cover--banner { height: auto; aspect-ratio: 8 / 3; max-height: 280px; }
.s-cover--banner img { object-position: var(--cover-x, 50%) var(--cover-y, 50%); }
.s-cover.is-repositioning img { cursor: grab; touch-action: none; }
.s-cover.is-repositioning img:active { cursor: grabbing; }
.s-cover__tools {
  position: absolute; z-index: 2; top: 8px; right: 8px;
  display: flex; gap: 6px;
}
.s-cover::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(to top, rgba(15,23,42,.6), transparent 58%);
}
/* A wrapping row of chips that labels whatever follows it. The cover variant
   below is the same row pinned onto a photo. */
.s-chips { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 7px; }
/* Chips normally label the thing below them, so the gap is on the bottom.
   When they follow the text instead — a count under a row's description — the
   gap has to move with them. */
.s-chips--after { margin-top: 6px; margin-bottom: 0; }
.s-cover__chips { position: absolute; z-index: 1; left: 10px; right: 10px; bottom: 9px; display: flex; flex-wrap: wrap; gap: 5px; }

/* Older / newer, either side of where you are. */
.s-pager { display: flex; align-items: center; justify-content: center; gap: var(--s-sp-3); margin-top: var(--s-sp-3); }

/* ── Dialog ─────────────────────────────────────────────────────────────── */
.s-dialog {
  width: calc(100% - 32px); max-width: 380px;
  max-height: min(84vh, 640px); overflow-y: auto;
  padding: 18px;
  border: none; border-radius: var(--s-r-xl);
  background: var(--s-surface); color: var(--s-ink);
  box-shadow: 0 20px 60px rgba(15,23,42,.28);
}
.s-dialog::backdrop { background: rgba(15,23,42,.5); }
.s-dialog__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; font-size: var(--s-fs-lg); font-weight: 900; }
.s-dialog__x { border: none; background: none; font-size: 22px; line-height: 1; color: var(--s-muted); cursor: pointer; min-width: 44px; min-height: 44px; margin: -11px -14px -11px 0; }
.s-dialog__sub { margin: 0 0 14px; font-size: var(--s-fs-sm); color: var(--s-ink-2); line-height: 1.45; }
/* A list inside a dialog that is longer than the dialog: pick a recipient,
   pick a player. It scrolls itself rather than growing the dialog past the
   viewport, which on a phone would put the Send button below the fold. */
.s-dialog__list { max-height: 46vh; overflow-y: auto; -webkit-overflow-scrolling: touch;
  margin-bottom: 12px; padding: 2px 11px;
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm); }
/* One choosable thing in that list: pick this plan, pick this template. The
   whole row is the button, because a small "Select" button on the right is a
   thumb-sized target inside a finger-sized row. */
.s-pick {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 11px 2px; border: none; border-bottom: 1px solid var(--s-line-soft);
  background: none; color: inherit; text-align: left; cursor: pointer;
}
.s-pick:last-child { border-bottom: none; }
.s-pick:hover { background: var(--s-surface-2); }
.s-pick__body { flex: 1; min-width: 0; }
.s-pick__t { display: block; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink); }
.s-pick__t i { margin-left: 5px; color: var(--s-bad); }
.s-pick__s {
  display: block; margin-top: 2px;
  font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.s-pick > i { flex: none; color: var(--s-muted); font-size: var(--s-fs-xs); }

/* ── Talk about it ───────────────────────────────────────────────────────
   A button that turns a session or a fixture into a feed post -- and, once
   one exists, the link to that conversation instead. Two states, one slot, so
   the row does not change width when the counts arrive. */
.s-talk { display: inline-flex; }
.s-talk__b { white-space: nowrap; }
.s-talk__s {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 0 4px;
  font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-brand); text-decoration: none;
  white-space: nowrap;
}
.s-talk__s:hover { text-decoration: underline; }

/* ── Counts strip: a run of small labelled numbers under a heading ──────── */
.s-counts { display: flex; flex-wrap: wrap; gap: 6px 12px; padding: 10px 0 2px; font-size: var(--s-fs-sm); font-weight: 900; }
.s-counts .s-c-good { color: var(--s-good); }
.s-counts .s-c-bad  { color: var(--s-bad); }
.s-counts .s-c-mute { color: var(--s-muted); }
/* The dot between the numbers belongs to the strip, not to whoever renders
   one. Three bold colours butted together with only a gap between them ran
   into one another at a glance, and when each template had to remember its
   own separator span, three of the five strips in the app never had one. It
   rides on the number that follows it so it can never wrap onto a line by
   itself, and the strip's own 12px gap sits on its other side. */
.s-counts > * + *::before {
  content: "·";
  margin-right: 12px;
  color: var(--s-muted);
  font-weight: 900;
}

/* ══════════════════════════════════════════════════════════════════════════
   10c. STAFF DASHBOARD COMPONENTS

   The coach side of the house. Everything here came out of the 357-line
   <style> block the staff home used to carry: side rails, side widgets, the
   mini calendar, the season bar, the setup checklist, the RSVP progress
   panel and the widget picker. None of it is bound to that one page — the
   rails are the desktop layout every staff page wants, the widget is the
   card any rail holds, and the checklist is what onboarding needs wherever
   it lands next.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── Desktop rails ──────────────────────────────────────────────────────
   Rails sit in the normal page flow, so the whole thing scrolls as ONE unit.
   They were position:sticky once, each with its own scrollbar, and the centre
   column moving while the sides stayed pinned read as three separate pages. */
.s-shell { display: block; }
.s-shell__main { min-width: 0; }
.s-rail, .s-rail--left { display: none; }

/* The shell is the one layout wider than a reading column, so it lifts the
   ceiling on its own container rather than making every .s-main page wide. */
@media (min-width: 1200px) {
  .s-main:has(> .s-shell) { max-width: 1180px; }
  .s-shell { display: flex; gap: var(--s-sp-6); align-items: flex-start; justify-content: center; }
  .s-shell__main { flex: 1 1 auto; max-width: 760px; }
  .s-rail { display: flex; flex-direction: column; gap: var(--s-sp-4); width: 340px; flex: 0 0 340px; }
  .s-foldin { display: flex; flex-direction: column; gap: var(--s-sp-4); }
}
@media (min-width: 1520px) {
  .s-main:has(> .s-shell) { max-width: 1500px; }
  .s-shell { gap: 32px; }
  .s-rail--left { display: flex; flex-direction: column; gap: var(--s-sp-4); width: 320px; flex: 0 0 320px; }
  /* At this width the left-hand widgets have a rail of their own, so the
     copy of them folded into the right rail has to go or they render twice. */
  .s-foldin { display: none; }
}
.s-rail__head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 2px 2px 0; }
.s-rail__t { font-size: var(--s-fs-xs); font-weight: 900; letter-spacing: .09em; text-transform: uppercase; color: var(--s-slate); }
.s-rail__cust {
  border: none; background: none; cursor: pointer;
  padding: 2px 4px;
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-info);
}

/* ── Greeting ───────────────────────────────────────────────────────────── */
.s-greet { display: flex; align-items: center; justify-content: space-between; gap: var(--s-sp-3); }
.s-greet__tx { min-width: 0; }
.s-greet__role {
  flex-shrink: 0;
  padding: 4px 11px; border-radius: var(--s-r-pill);
  background: var(--s-brand-soft); color: var(--s-brand-on);
  font-size: var(--s-fs-xs); font-weight: 900;
}
/* The menu button floats over the top-right of the sheet on a phone, and on
   the two dashboards the greeting is the block that reaches that corner: the
   coach's role pill sat under it. The same 52px clear that .s-pagehead keeps. */
.s-greet { padding-right: 52px; }
@media (min-width: 1240px) { .s-greet { padding-right: 0; } }

/* ── Progress bar ───────────────────────────────────────────────────────
   The fill is a child with a width, never a background gradient stop: the
   width arrives as live data, so a script sets it from data-bar on load. A
   percentage cannot be a class, and this is the sanctioned way to say so. */
.s-progress { display: block; height: 9px; border-radius: var(--s-r-pill); background: var(--s-line); overflow: hidden; }
.s-progress__fill { display: block; height: 100%; width: 0; border-radius: var(--s-r-pill); background: var(--s-good); transition: width .4s ease; }
.s-progress__fill--brand { background: linear-gradient(90deg, var(--s-brand), var(--s-accent)); }
.s-progress--slim { height: 6px; }

/* -- The match-day relay ------------------------------------------------
   Four fixed stages -- fixture, plan, live, review -- as one strip, so a
   coach can see where the baton is and tap into any stage from any of the
   four screens.

   Not .s-progress: these are named places rather than a percentage, and one
   of them (live) can legitimately be skipped instead of merely unfinished,
   which a bar has no way to say. Four columns at every width -- the fourth
   stage is the one that says what to do next, so it must not drop to a
   second line on a phone. */
.s-relay { display: grid; grid-template-columns: repeat(4, 1fr); }
.s-relay__step {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  padding: 0 2px;
  text-align: center; text-decoration: none; color: var(--s-ink);
}
/* The connector belongs to the step on its right, so a filled one reads as
   "the baton got this far". */
.s-relay__step::before {
  content: ''; position: absolute; z-index: 0;
  top: 15px; left: -50%; width: 100%; height: 3px;
  border-radius: var(--s-r-pill);
  background: var(--s-line);
}
.s-relay__step:first-child::before { display: none; }
.s-relay__step.is-done::before,
.s-relay__step.is-current::before,
.s-relay__step.is-skipped::before { background: var(--s-good); }
.s-relay__ic {
  position: relative; z-index: 1;
  width: 32px; height: 32px; flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 13px;
  /* The ring is the card behind it, so the connector stops short of the
     disc instead of running under its edge. */
  border: 3px solid var(--s-surface);
  background: var(--s-surface-2); color: var(--s-muted);
}
.s-relay__step.is-done .s-relay__ic {
  background: var(--s-good); border-color: var(--s-good); color: #fff;
}
.s-relay__step.is-current .s-relay__ic {
  background: var(--s-accent); border-color: var(--s-surface);
  color: var(--s-accent-on);
  box-shadow: 0 0 0 3px var(--s-accent);
}
/* Skipped is a stage that was passed over, not one that failed and not one
   still to come: an outline, so it reads as deliberately empty. */
.s-relay__step.is-skipped .s-relay__ic {
  background: var(--s-surface); border-style: dashed;
  border-color: var(--s-line); color: var(--s-muted);
}
.s-relay__step.is-upcoming { opacity: .6; }
.s-relay__lb {
  display: block; margin-top: 6px;
  font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-ink);
}
.s-relay__sub {
  display: block; margin-top: 1px; max-width: 94px;
  font-size: var(--s-fs-xs); font-weight: 600; line-height: 1.25;
  color: var(--s-muted);
}

/* ── Setup checklist ────────────────────────────────────────────────────── */
.s-checklist__head { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.s-checklist__t { flex: 1; min-width: 0; font-size: var(--s-fs-md); font-weight: 900; color: var(--s-ink); }
.s-checklist__count { flex-shrink: 0; font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-muted); font-variant-numeric: tabular-nums; }
.s-checklist__steps { margin-top: 12px; display: flex; flex-direction: column; }
.s-check {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 0;
  border-top: 1px solid var(--s-line-soft);
  text-decoration: none; color: var(--s-ink);
}
.s-check:first-child { border-top: none; }
.s-check__ic {
  width: 26px; height: 26px; flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px;
  background: var(--s-surface-2); color: var(--s-muted);
}
.s-check__body { flex: 1; min-width: 0; }
.s-check__lb { display: block; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink); }
.s-check__hint { display: block; margin-top: 1px; font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted); }
.s-check.is-done .s-check__ic { background: var(--s-good); color: #fff; }
.s-check.is-done .s-check__lb { color: var(--s-muted); text-decoration: line-through; }
/* Same component, used as a log of what has happened rather than a list of
   jobs. Striking a done row out is right when the row is work somebody has
   finished and wrong when it is an event that already took place: "You signed
   up", struck through, reads as though it did not. The parent waiting room
   uses this. */
.s-checklist--log .s-check.is-done .s-check__lb { text-decoration: none; }
/* The one step to do next is the only thing on the card worth a colour. */
.s-check.is-next .s-check__ic { background: var(--s-accent); color: var(--s-accent-on); }
.s-check.is-next .s-check__lb { color: var(--s-ink); }

/* ── Where you are in a form that runs over several pages ────────────────
   Registering a child under 18 is four pages: the player, their guardians and
   emergency contacts, what the club needs to know medically, and the consents.
   v1 drew this rail as four hard-coded bootstrap columns reading "step 1
   complete, step 2 now, 3 and 4 still to come" — the same four whichever page
   you were on and whatever the club already held, so coming back in October to
   correct a phone number was met by a tracker insisting the consents had never
   been given.

   Each step is told its own state here, so one rail serves all four pages and
   tells the truth on each. Four across at 375px: the numbers stay at a size
   you can read and the labels wrap underneath rather than shrinking. */
.s-wiz { display: flex; align-items: flex-start; }
.s-wiz__s {
  flex: 1 1 0; min-width: 0; position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  text-align: center;
}
/* The rail runs behind the numbers, each step drawing the piece to its left,
   so the last one stops rather than trailing off the end of the row. */
.s-wiz__s::before {
  content: ""; position: absolute; top: 13px; right: 50%;
  width: 100%; height: 2px; background: var(--s-line);
}
.s-wiz__s:first-child::before { display: none; }
.s-wiz__n {
  position: relative; z-index: 1;
  width: 28px; height: 28px; flex-shrink: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--s-fs-sm); font-weight: 900;
  background: var(--s-surface); color: var(--s-muted);
  border: 2px solid var(--s-line);
}
.s-wiz__t { font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-muted); overflow-wrap: anywhere; }
.s-wiz__s.is-done::before, .s-wiz__s.is-now::before { background: var(--s-good); }
.s-wiz__s.is-done .s-wiz__n { background: var(--s-good); border-color: var(--s-good); color: #fff; }
.s-wiz__s.is-done .s-wiz__t { color: var(--s-ink-2); }
/* The step you are on takes the club's highlight colour rather than its
   primary: --s-brand is a dark navy that is not redefined for dark mode, so a
   filled brand circle disappears on a dark ground. The setup checklist's
   is-next row already settled this the same way. */
.s-wiz__s.is-now .s-wiz__n { background: var(--s-accent); border-color: var(--s-accent); color: var(--s-accent-on); }
.s-wiz__s.is-now .s-wiz__t { color: var(--s-ink); }

/* ── Next up: one event, with how many have replied ─────────────────────── */
.s-nextup__head {
  display: flex; align-items: center; gap: 11px;
  padding: var(--s-sp-3) var(--s-sp-4);
  border-bottom: 1px solid var(--s-line-soft);
  text-decoration: none; color: var(--s-ink);
}
.s-nextup__tx { flex: 1; min-width: 0; }
.s-nextup__kicker { display: block; font-size: 10px; font-weight: 900; letter-spacing: .08em; text-transform: uppercase; color: var(--s-muted); }
.s-nextup__label { display: block; margin-top: 2px; font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink); line-height: 1.2; overflow-wrap: anywhere; }
.s-nextup__when { display: block; margin-top: 1px; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }
.s-nextup__body { padding: var(--s-sp-3) var(--s-sp-4) var(--s-sp-4); }
.s-nextup__stat { display: flex; align-items: baseline; gap: 8px; margin-bottom: 8px; }
.s-nextup__pct { font-size: var(--s-fs-2xl); font-weight: 900; color: var(--s-ink); line-height: 1; font-variant-numeric: tabular-nums; }
.s-nextup__of { font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-muted); }

/* A wrapped run of names — who has not replied, who is short a coach. */
.s-names { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 6px; }
/* A person's name as a tap target. Deliberately not .s-chip: a chip is a 10px
   uppercase letter-spaced label for a status, and a squad of twenty children
   set that way is a shouting match in which no individual name is findable.
   This is sentence case at reading size, which is how a name is written. */
.s-name {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 5px 12px;
  border: 1px solid var(--s-line);
  border-radius: var(--s-r-pill);
  background: var(--s-surface-2);
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink);
  text-decoration: none;
}
.s-name:hover { border-color: var(--s-brand); }
.s-name__flag { font-size: .85em; color: var(--s-warn); }
.s-name__n {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-warn);
}

/* ── Coach availability: answer for yourself without opening the event ──── */
.s-ca { margin: 4px 0 10px; padding: 8px 10px; border: 1px solid var(--s-line); border-radius: var(--s-r); }
.s-ca.is-short { border-color: var(--s-bad-line); background: color-mix(in srgb, var(--s-bad) 5%, transparent); }
.s-ca__top { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink-2); }
.s-ca__lbl { flex: 1; min-width: 0; }
.s-ca__pill { flex-shrink: 0; padding: 2px 8px; border-radius: var(--s-r-pill); background: var(--s-good-bg); color: var(--s-good); font-size: var(--s-fs-xs); font-weight: 900; }
.s-ca.is-short .s-ca__pill { background: var(--s-bad-bg); color: var(--s-bad); }
.s-ca__btns { display: flex; gap: 7px; }
.s-ca__btn {
  flex: 1; min-height: 40px; padding: 8px 6px;
  border: 1.5px solid var(--s-line); border-radius: var(--s-r-sm);
  background: var(--s-surface); color: var(--s-muted);
  font-size: var(--s-fs-sm); font-weight: 900; cursor: pointer;
}
.s-ca__btn:active { transform: translateY(1px); }
.s-ca__btn.is-yes { background: var(--s-good-bg); border-color: var(--s-good); color: var(--s-good); }
.s-ca__btn.is-no  { background: var(--s-bad-bg);  border-color: var(--s-bad);  color: var(--s-bad); }
/* The full panel. The compact version above is what a dashboard row can
   afford; a fixture or session page has room to say who has answered and to
   offer the cover invite, so the same component grows rather than forking. */
.s-ca--full { margin: 0; padding: 0; border: none; }
.s-ca--full.is-short { background: none; }
.s-ca__ask {
  margin-bottom: 7px;
  font-size: 10px; font-weight: 900; letter-spacing: .05em; text-transform: uppercase;
  color: var(--s-muted);
}
.s-ca__grp {
  margin: 11px 0 5px;
  font-size: 10px; font-weight: 900; letter-spacing: .05em; text-transform: uppercase;
  color: var(--s-muted);
}
.s-ca__ppl { display: flex; flex-wrap: wrap; gap: 5px; }
.s-ca__chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 10px; border-radius: var(--s-r-pill);
  font-size: var(--s-fs-sm); font-weight: 800;
  background: var(--s-slate-bg); color: var(--s-slate);
}
.s-ca__chip--yes { background: var(--s-good-bg); color: var(--s-good); }
.s-ca__chip--no  { background: var(--s-bad-bg);  color: var(--s-bad); }
.s-ca__cover { font-size: 9px; font-weight: 900; letter-spacing: .05em; opacity: .75; }
.s-ca__note { margin: 9px 0 0; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); line-height: 1.45; }
.s-ca__pick {
  max-height: 190px; overflow-y: auto;
  margin: 9px 0;
  padding: 4px 11px;
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
}

/* -- Pick several out of a long list --------------------------------------
   The same shape as .s-ca__pick above, which does this for one page's list of
   coaches: a bordered box that scrolls rather than growing without limit. It
   is its own component because the list it was written for is worse -- every
   player in the club, which at a busy club is two hundred names. The team
   headings inside stick to the top while you scroll, so being halfway down
   still tells you whose squad you are looking at. */
.s-picklist {
  max-height: 250px; overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 0 11px;
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  background: var(--s-surface);
}
.s-picklist__g {
  position: sticky; top: 0; z-index: 1;
  margin: 0 -11px; padding: 7px 11px 6px;
  border-bottom: 1px solid var(--s-line);
  background: var(--s-surface-2);
  font-size: 10px; font-weight: 900;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--s-muted);
}
.s-picklist .s-tick { padding: 6px 0; }
.s-picklist__none {
  padding: 14px 0;
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-muted);
}
/* Taller, for a list whose length is bounded by something real -- the club's
   teams are in the low tens where its children are in the hundreds -- and
   where seeing the whole list at once is the point of the page. It still
   scrolls; it just stops cutting eight rows off at three. */
.s-picklist--tall { max-height: 420px; }
/* The list is the control here, so a field in error reddens the box rather
   than the search input above it -- the search box is never the thing that
   was got wrong. */
.s-field--bad .s-picklist { border-color: var(--s-bad); }

/* ── My teams ───────────────────────────────────────────────────────────── */
.s-teams { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 11px; }
.s-team {
  display: block; padding: var(--s-sp-3);
  border: 1px solid var(--s-line); border-radius: var(--s-r);
  background: var(--s-surface); color: var(--s-ink);
  text-decoration: none;
}
.s-team__n { display: block; font-size: var(--s-fs-md); font-weight: 900; }
.s-team__meta { display: block; margin-top: 2px; font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted); }

/* ── Side widget ────────────────────────────────────────────────────────
   One card shape for every rail widget — feed, fees, weather, league table.
   Nothing here is rail-only: the same card is what the medium-width fold-in
   and the mobile season bar render. */
.s-wgt {
  padding: var(--s-sp-3) var(--s-sp-4);
  border: 1px solid var(--s-line); border-radius: var(--s-r-lg);
  background: var(--s-surface);
  box-shadow: var(--s-shadow-sm);
}
.s-wgt__h { display: flex; align-items: center; gap: 9px; margin-bottom: 11px; }
.s-wgt__ic {
  width: 30px; height: 30px; flex-shrink: 0;
  border-radius: var(--s-r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--s-brand-soft); color: var(--s-brand-on);
  font-size: 13px;
}
.s-wgt__t { flex: 1; min-width: 0; font-size: var(--s-fs-md); font-weight: 900; color: var(--s-ink); }
.s-wgt__more { flex-shrink: 0; margin-left: auto; font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-info); text-decoration: none; white-space: nowrap; }
.s-wgt__empty { padding: 2px 0 1px; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-muted); }
.s-wgt__row {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 0;
  border-top: 1px solid var(--s-line-soft);
  text-decoration: none; color: var(--s-ink);
}
.s-wgt__row:first-of-type { border-top: none; }
.s-wgt__rt { flex: 1; min-width: 0; }
.s-wgt__rl { display: block; min-width: 0; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink); line-height: 1.25; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-wgt__rs { display: block; font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted); }
/* A sub-heading inside a widget that lists more than one group (two league
   tables, two age bands) — not a row, so it carries no hairline. */
.s-wgt__group { padding: 8px 0 2px; font-size: var(--s-fs-xs); font-weight: 900; letter-spacing: .06em; text-transform: uppercase; color: var(--s-muted); }
.s-wgt__pill { flex-shrink: 0; padding: 3px 9px; border-radius: var(--s-r-pill); background: var(--s-slate-bg); color: var(--s-slate); font-size: var(--s-fs-xs); font-weight: 900; white-space: nowrap; }
.s-wgt__pill--good { background: var(--s-good-bg);   color: var(--s-good); }
.s-wgt__pill--warn { background: var(--s-warn-bg);   color: var(--s-warn); }
.s-wgt__pill--bad  { background: var(--s-bad-bg);    color: var(--s-bad); }
.s-wgt__pill--info { background: var(--s-info-bg);   color: var(--s-info); }
/* Weather risk arrives from Python as low/mid/high, the same three words the
   event-card forecast strip uses — one vocabulary, two components. */
.s-wgt__pill--low  { background: var(--s-good-bg); color: var(--s-good); }
.s-wgt__pill--mid  { background: var(--s-warn-bg); color: var(--s-warn); }
.s-wgt__pill--high { background: var(--s-bad-bg);  color: var(--s-bad); }
.s-wgt__big { display: flex; align-items: baseline; gap: 8px; }
.s-wgt__n { font-size: var(--s-fs-2xl); font-weight: 900; line-height: 1; color: var(--s-ink); font-variant-numeric: tabular-nums; }
.s-wgt__n--good { color: var(--s-good); }
.s-wgt__n--warn { color: var(--s-warn); }
.s-wgt__n--info { color: var(--s-info); }
.s-wgt__u { font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-muted); }
.s-wgt__cta {
  display: block; margin-top: 11px; padding: 9px;
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  text-align: center; text-decoration: none;
  font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-info);
}
/* Widgets stack inside a card body, so a second .s-wgt__big below a row needs
   the gap the first one gets from the header above it. */
.s-wgt__row + .s-wgt__big, .s-wgt__big + .s-wgt__row, .s-wgt__rt + .s-wgt__big { margin-top: 9px; }

/* Profit & loss: income and expenses side by side under the net figure. */
.s-pnl { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; }
.s-pnl__c { padding: 9px 11px; border-radius: var(--s-r-sm); background: var(--s-surface-2); }
.s-pnl__l { display: block; font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }
.s-pnl__v { display: block; margin-top: 2px; font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink); font-variant-numeric: tabular-nums; }
.s-pnl__v--good { color: var(--s-good); }
.s-pnl__v--warn { color: var(--s-warn); }

/* Kit request: same row, plus the thumbnail that makes a size request obvious. */
.s-kit { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-top: 1px solid var(--s-line-soft); text-decoration: none; color: var(--s-ink); }
.s-kit:first-of-type { border-top: none; }
.s-kit__img {
  width: 40px; height: 40px; flex-shrink: 0;
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: var(--s-surface-2); color: var(--s-muted);
}
.s-kit__img img { width: 100%; height: 100%; object-fit: cover; }

/* An entry in a list you act on rather than read: the row, and under it a
   disclosure holding the form. A kit item asking for a different size, a
   player asking to be linked to a parent -- the shape is the same wherever
   the answer to a row is a form and not a link.

   The two cannot both be .s-row children of the card, because the panel has
   to sit *under* its own row and inside the same hairline — so the item is
   its own block and takes over the border the row would have drawn.

   (Was .s-kititem while the kit page was the only page shaped like this.
   Renamed rather than copied: one shape wants one class, or the second
   page's version drifts from the first the week after it is written.) */
.s-item { border-bottom: 1px solid var(--s-line-soft); }
.s-item:last-child { border-bottom: none; }
.s-item > .s-row { border-bottom: none; padding-bottom: 0; }
/* .s-more brings its own top margin for the stacked-links case; here the row
   above it is already spaced, and the summary bar needs the card's side
   padding back because the card itself is flush. */
.s-item > .s-more { margin: var(--s-sp-2) var(--s-sp-4) var(--s-sp-4); }
/* The form inside the panel, once it is open. */
.s-item__form { margin-top: var(--s-sp-3); }

/* -- The club shop ---------------------------------------------------------
   A catalogue you buy from, which is a different thing from a list you read:
   every entry carries a price and three controls, so it cannot be a .s-row.

   One column on a phone, two from 640px, and the tile stays horizontal at
   both widths. A photo stacked above its name makes a nicer picture and a
   worse list, and this is a list you scan for the one thing you came for. */
.s-shop { display: grid; grid-template-columns: 1fr; gap: var(--s-sp-3); }
.s-shop.is-hidden { display: none; }
@media (min-width: 640px) { .s-shop { grid-template-columns: 1fr 1fr; } }

.s-shop__i {
  display: flex; gap: var(--s-sp-3);
  padding: var(--s-sp-3);
  border: 1px solid var(--s-line);
  border-radius: var(--s-r);
  background: var(--s-surface);
}

.s-shop__ph {
  width: 62px; height: 62px; flex-shrink: 0;
  border-radius: var(--s-r-sm);
  border: 1px solid var(--s-line);
  background: var(--s-surface-2); color: var(--s-muted);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 20px; overflow: hidden;
}
.s-shop__ph img { width: 100%; height: 100%; object-fit: cover; }

.s-shop__b { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 7px; }
.s-shop__n { font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink); line-height: 1.3; }
.s-shop__p { font-size: var(--s-fs-md); font-weight: 900; color: var(--s-ink); }
.s-shop__p span { font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }

/* Size and quantity share a line; the size box takes whatever is left. */
.s-shop__ctl { display: flex; gap: 7px; align-items: center; }
.s-shop__ctl .s-select { flex: 1; min-width: 0; }
/* When the item comes in exactly one size there is nothing to choose, so the
   size is stated rather than offered. Dashed, because it is not a control. */
.s-shop__size1 {
  flex: 1; min-width: 0;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 10px;
  border: 1px dashed var(--s-line);
  border-radius: var(--s-r-sm);
  background: var(--s-surface-2);
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted);
}

/* How many. Two buttons and a number, not <input type=number>: a numeric
   keyboard for a figure that is nearly always 1 is a keyboard nobody asked
   for, and the spinner arrows are a 12px tap target. */
.s-qty {
  display: inline-flex; align-items: center; flex-shrink: 0;
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  overflow: hidden; background: var(--s-surface);
}
.s-qty__b {
  width: 32px; height: 38px; padding: 0;
  border: none; background: none; color: var(--s-ink);
  font-size: var(--s-fs-lg); font-weight: 900; line-height: 1; cursor: pointer;
}
.s-qty__b:hover { background: var(--s-surface-2); }
.s-qty__b:disabled { color: var(--s-line); cursor: not-allowed; }
.s-qty__n {
  min-width: 24px; text-align: center;
  font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-ink);
  font-variant-numeric: tabular-nums;
}

/* Forecast row in the weather widget (distinct from .s-wx, the strip along
   the bottom of an event card — that one has one event, this one has many). */
.s-wxrow { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-top: 1px solid var(--s-line-soft); }
.s-wxrow:first-of-type { border-top: none; }
.s-wxrow__ic { width: 30px; flex-shrink: 0; text-align: center; font-size: 17px; color: var(--s-info); }
.s-wxrow__t { flex-shrink: 0; display: flex; flex-direction: column; align-items: flex-end; gap: 3px; }
.s-wxrow__deg { font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink); font-variant-numeric: tabular-nums; }

/* A running award vote: title and tally on one line, the rest underneath. */
.s-vote { display: block; padding: 9px 0; border-top: 1px solid var(--s-line-soft); text-decoration: none; }
.s-vote:first-of-type { border-top: none; }
.s-vote__top { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 2px; }
.s-stars { display: flex; gap: 4px; margin-top: 8px; font-size: 15px; color: var(--s-accent-text); }
.s-emoji { width: 26px; flex-shrink: 0; text-align: center; font-size: 18px; }

/* ── Mini calendar ──────────────────────────────────────────────────────── */
.s-cal__nav { margin-left: auto; display: flex; align-items: center; gap: 2px; }
.s-cal__btn {
  border: none; background: none; cursor: pointer;
  padding: 3px 6px; border-radius: 7px;
  font-size: var(--s-fs-sm); line-height: 1;
  color: var(--s-muted); text-decoration: none;
}
.s-cal__btn:hover { background: var(--s-surface-2); color: var(--s-info); }
.s-cal__grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; }
.s-cal__dow { margin-bottom: 4px; }
.s-cal__dowc { text-align: center; font-size: 10px; font-weight: 900; color: var(--s-muted); }
.s-cal__c {
  position: relative; aspect-ratio: 1;
  display: flex; align-items: center; justify-content: center;
  border-radius: 8px;
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-ink);
  font-variant-numeric: tabular-nums;
}
/* Padding days keep their box (the grid has to stay seven wide) but not their
   number — a greyed-out 29th from last month reads as this month's. */
.s-cal__c.is-out { color: transparent; }
.s-cal__c.is-today { background: var(--s-brand); color: var(--s-brand-on); }
.s-cal__c.has-ev { cursor: pointer; }
.s-cal__c.has-ev::after { content: ''; position: absolute; bottom: 3px; width: 4px; height: 4px; border-radius: 50%; background: var(--s-accent); }
.s-cal__c.has-ev.is-today::after { background: var(--s-brand-on); }
.s-cal__c.has-ev:hover { background: var(--s-surface-2); }
.s-cal__c.is-sel { outline: 2px solid var(--s-info); outline-offset: -2px; }
.s-cal__events { margin-top: 9px; }
.s-cal__hint { font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted); }
.s-cal__ev { display: flex; align-items: center; gap: 8px; padding: 7px 0; border-top: 1px solid var(--s-line-soft); text-decoration: none; }
.s-cal__ev:first-child { border-top: none; }
.s-cal__lbl { min-width: 0; font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Kind colours. The event types are a fixed Python choices list, so each one
   gets a class — a colour piped through an inline style would be the one thing
   the house-style gate exists to stop.

   The list lives here once. A kind sets two custom properties, and everything
   that colours by kind reads those two rather than carrying a copy of the
   nine: the dot below, the icon disc under it, and the calendar's chips,
   blocks and rails further down. Adding a tenth event type in Python is three
   lines in this block and nothing anywhere else. */
.s-evt,
.s-dot, .s-typeic                                                 { --s-evc: var(--s-slate);  --s-evc-bg: var(--s-slate-bg); }
.s-evt--training,       .s-dot--training,       .s-typeic--training       { --s-evc: var(--s-good);   --s-evc-bg: var(--s-good-bg); }
.s-evt--fixture,        .s-dot--fixture,        .s-typeic--fixture        { --s-evc: var(--s-info);   --s-evc-bg: var(--s-info-bg); }
.s-evt--meeting,        .s-dot--meeting,        .s-typeic--meeting        { --s-evc: var(--s-violet); --s-evc-bg: var(--s-violet-bg); }
.s-evt--social,         .s-dot--social,         .s-typeic--social         { --s-evc: var(--s-pink);   --s-evc-bg: var(--s-pink-bg); }
.s-evt--fundraiser,     .s-dot--fundraiser,     .s-typeic--fundraiser     { --s-evc: var(--s-teal);   --s-evc-bg: var(--s-teal-bg); }
.s-evt--deadline,       .s-dot--deadline,       .s-typeic--deadline       { --s-evc: var(--s-bad);    --s-evc-bg: var(--s-bad-bg); }
.s-evt--volunteer,      .s-dot--volunteer,      .s-typeic--volunteer      { --s-evc: var(--s-orange); --s-evc-bg: var(--s-orange-bg); }
.s-evt--payment_window, .s-dot--payment_window, .s-typeic--payment_window { --s-evc: var(--s-teal);   --s-evc-bg: var(--s-teal-bg); }
.s-evt--other,          .s-dot--other,          .s-typeic--other          { --s-evc: var(--s-slate);  --s-evc-bg: var(--s-slate-bg); }
/* Older names for two of the kinds, still written by the feed and the
   dashboards. They are aliases, not extra kinds. */
.s-dot--match { --s-evc: var(--s-info); --s-evc-bg: var(--s-info-bg); }
.s-dot--event { --s-evc: var(--s-violet); --s-evc-bg: var(--s-violet-bg); }
.s-dot--tournament { --s-evc: var(--s-warn); --s-evc-bg: var(--s-warn-bg); }

.s-dot { width: 7px; height: 7px; flex-shrink: 0; border-radius: 50%; background: var(--s-evc, var(--s-muted)); }

/* The same nine kinds at icon size: a soft disc behind a club event's emoji.
   Event.type_colour hands back a hex, which is exactly the inline style the
   house-style gate exists to stop — and it doesn't need to, because
   event_type is a closed choices list and a closed list is a class. */
.s-typeic {
  width: 34px; height: 34px; flex-shrink: 0;
  border-radius: var(--s-r);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 17px; line-height: 1;
  background: var(--s-evc-bg, var(--s-slate-bg));
}

/* ── Season timeline ────────────────────────────────────────────────────── */
/* On a wide screen the season bar lives in the left rail; below that width the
   rail is gone, so the page renders a copy above the fold instead. */
.s-season-fallback { margin-bottom: var(--s-sp-4); }
@media (min-width: 1400px) { .s-season-fallback { display: none; } }
.s-season__top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 9px; text-decoration: none; }
.s-season__name { display: inline-flex; align-items: center; gap: 6px; min-width: 0; font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-ink); }
.s-season__left { flex-shrink: 0; display: inline-flex; align-items: center; gap: 5px; font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); white-space: nowrap; }
.s-season__left i { font-size: 9px; opacity: .6; }
.s-season__dates { display: flex; justify-content: space-between; margin-top: 6px; font-size: 10px; font-weight: 800; color: var(--s-muted); }
.s-season__cta {
  display: block; margin-top: 12px; padding: 10px;
  border-radius: var(--s-r-sm);
  background: var(--s-brand); color: var(--s-brand-on);
  text-align: center; text-decoration: none;
  font-size: var(--s-fs-sm); font-weight: 900;
}
.s-season__note { margin-top: 10px; text-align: center; font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted); }

/* ── Rotating headline ──────────────────────────────────────────────────
   The text sibling of .s-slides: same idea, no photo. Lines are stacked and
   cross-faded, so the tile keeps one fixed height whatever is showing. */
.s-ticker__lb { font-size: var(--s-fs-xs); font-weight: 900; letter-spacing: .06em; text-transform: uppercase; color: var(--s-muted); }
.s-ticker__hl { position: relative; height: 42px; margin-top: 6px; }
.s-ticker__ln {
  position: absolute; inset: 0;
  display: flex; align-items: center;
  opacity: 0; transition: opacity .9s ease;
  font-size: var(--s-fs-md); font-weight: 800; color: var(--s-ink);
}
.s-ticker__ln.is-on { opacity: 1; }

/* ── Widget picker ──────────────────────────────────────────────────────── */
.s-picker-back { position: fixed; inset: 0; z-index: 1200; background: rgba(15,23,42,.5); display: none; }
.s-picker-back.is-open { display: block; }
.s-picker {
  position: fixed; z-index: 1201;
  left: 50%; top: 50%; transform: translate(-50%,-50%);
  width: min(460px, 92vw); max-height: 86vh; overflow-y: auto;
  padding: var(--s-sp-4);
  border-radius: var(--s-r-xl);
  background: var(--s-surface);
  box-shadow: var(--s-shadow-lg);
  display: none;
}
.s-picker.is-open { display: block; }
.s-picker__t { margin: 0 0 3px; font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink); }
.s-picker__sub { margin: 0 0 14px; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-muted); }
.s-picker__grid { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-bottom: var(--s-sp-4); }
.s-picker__tile {
  display: flex; align-items: center; gap: 9px;
  padding: 11px;
  border: 1px solid var(--s-line); border-radius: var(--s-r);
  background: var(--s-surface);
  text-align: left; cursor: pointer;
}
.s-picker__tile.is-on { border-color: var(--s-good); background: var(--s-good-bg); }
.s-picker__wi { width: 26px; flex-shrink: 0; text-align: center; color: var(--s-brand); }
.s-picker__wl { flex: 1; min-width: 0; font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink); }
.s-picker__ck { flex-shrink: 0; color: var(--s-good); opacity: 0; }
.s-picker__tile.is-on .s-picker__ck { opacity: 1; }

/* ── Page head: how a page that is not a dashboard opens ────────────────
   Title, an optional line saying what the page is for, and the actions that
   belong to the whole page. Actions wrap under the title on a phone rather
   than shrinking, because a row of squeezed buttons is the thing the old UI
   did that made every list page feel different from the last. */
.s-pagehead { margin-bottom: var(--s-sp-4); }
.s-pagehead__row { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-sp-3); flex-wrap: wrap; }
.s-pagehead__kicker {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--s-fs-xs); font-weight: 900;
  letter-spacing: .09em; text-transform: uppercase;
  color: var(--s-muted);
}
.s-pagehead__acts { display: flex; align-items: center; gap: var(--s-sp-2); flex-wrap: wrap; }

/* The menu button floats over the top-right of the sheet, and the page head is
   the one block that reaches that corner: a long name runs under it on a phone,
   and on a laptop the head's own buttons end up beneath it. So the head keeps
   the button's width clear. Above the point where the sheet stops growing
   (1120px) the button is outside the content entirely and the reserve goes.

   The reserve belongs to .s-pagehead and not to .s-pagehead__row. It sat on
   the row first, because every head had one -- and then a head with nothing to
   put on the right had to grow a row it did not need, purely to get the
   padding, which is a layout class used as a spacing hack. The head is the
   block that reaches the corner, with or without a row in it. */
.s-pagehead { padding-right: 52px; }
@media (min-width: 1240px) { .s-pagehead { padding-right: 0; } }

/* Section heading inside a page: a title and, usually, how many. */
.s-sect {
  display: flex; align-items: center; gap: var(--s-sp-2);
  margin: var(--s-sp-5) 0 var(--s-sp-3);
}
.s-sect:first-child { margin-top: 0; }
.s-sect__t { font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink); }
.s-sect__acts { margin-left: auto; display: flex; align-items: center; gap: var(--s-sp-2); }

/* Some sections need a line of explanation, and the row above cannot hold one:
   it sits its children side by side, which is right for a title and a button
   and wrong for a title and the sentence under it. So the stacked variant
   drops the row and keeps the margins. */
.s-sect--stack { display: block; }
/* A #fragment target: keeps the heading clear of the sticky app header when
   something (the Settings hub) links straight to a section. */
.s-anchor { scroll-margin-top: 84px; }
.s-sect__sub {
  margin-top: 2px;
  font-size: var(--s-fs-sm); font-weight: 600;
  color: var(--s-ink-2); line-height: 1.45;
}

/* -- Find-in-page ---------------------------------------------------------
   For an index too long to read: the admin hub is ninety tools in twelve
   groups. The box has to still be there when you are six groups down and have
   realised you do not know what the thing you want is called, so it rides
   with the page instead of sitting at the top of it -- stopping under the
   header, which is fixed, and carrying its own ground so the rows do not show
   through as they pass behind it.

   One line high on purpose. Two would make it a filter panel, and this is one
   question. The count lives in the bar because that is where the question was
   asked: "did that match three tools or thirty" is not answerable by
   scrolling twelve cards to the end. */
.s-find {
  position: sticky;
  top: calc(var(--s-hdr-h) + 8px);
  z-index: 20;
  display: flex; align-items: center; gap: var(--s-sp-2);
  padding: var(--s-sp-2);
  border: 1px solid var(--s-line);
  border-radius: var(--s-r-lg);
  background: var(--s-surface);
  box-shadow: var(--s-shadow-sm);
}
/* The drawer button is fixed in the top-right corner just under the header,
   which is exactly where this bar arrives once it sticks. So the bar keeps
   the button's width clear, the same reserve .s-pagehead__row makes and for
   the same reason -- and drops it above 1240px, where the sheet stops growing
   and the button is outside the content entirely. */
.s-find { padding-right: 52px; }
@media (min-width: 1240px) { .s-find { padding-right: var(--s-sp-2); } }
.s-find__ic { flex-shrink: 0; padding-left: 6px; color: var(--s-muted); font-size: 14px; }
/* The bar is the box. An input drawn with its own border inside a bordered
   bar is two boxes deep for one field. */
.s-find .s-input { border: none; padding: 4px 0; background: none; }
.s-find .s-input:focus { box-shadow: none; }
.s-find__n {
  flex-shrink: 0; padding-right: 6px;
  font-size: var(--s-fs-xs); font-weight: 900;
  color: var(--s-muted); font-variant-numeric: tabular-nums;
}
/* Under 16px iOS zooms the page in on focus and does not zoom back out, so a
   search you typed one word into leaves you reading the results at 130%. */
@media (max-width: 575.98px) { .s-find .s-input { font-size: 16px; } }

/* ── Forms ──────────────────────────────────────────────────────────────
   Django renders its own widgets, and the widget classes are set in Python —
   forms.Select(attrs={'class': 'form-select'}) and friends, in dozens of
   forms. The rebuild is the skin layer only, so v2 dresses what Django
   already emits rather than asking every form to be rewritten: the Bootstrap
   class names are treated as v2 selectors inside .s-v2 and nowhere else.
   That is why {{ form.team }} needs no template change to look right here.

   .s-input / .s-select / .s-textarea are the same thing for controls a
   template writes by hand. Keep them identical — a page should not be able
   to tell which kind it got. */
.s-v2 .form-control,
.s-v2 .form-select,
.s-input, .s-select, .s-textarea {
  width: 100%;
  padding: 10px 12px;
  border: 1.5px solid var(--s-line);
  border-radius: var(--s-r-sm);
  background: var(--s-surface);
  color: var(--s-ink);
  font-size: var(--s-fs-md);
  font-weight: 700;
  font-family: inherit;
  line-height: 1.35;
}
.s-v2 .form-control:focus,
.s-v2 .form-select:focus,
.s-input:focus, .s-select:focus, .s-textarea:focus {
  outline: none;
  border-color: var(--s-info);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--s-info) 18%, transparent);
}
.s-v2 .form-control::placeholder,
.s-input::placeholder, .s-textarea::placeholder { color: var(--s-muted); font-weight: 600; }
.s-v2 .form-control:disabled,
.s-v2 .form-select:disabled,
.s-v2 .form-textarea:disabled,
.s-input:disabled, .s-select:disabled, .s-textarea:disabled { background: var(--s-surface-2); color: var(--s-muted); cursor: not-allowed; }
/* A box the page is keeping filled in for you. readonly rather than disabled
   on purpose: a disabled control posts nothing, and a readonly one is skipped
   by the browser's required check — which is what makes "the emergency
   contact is the guardian" safe to leave switched on. */
.s-v2 .form-control[readonly], .s-v2 .form-select[readonly],
.s-input[readonly], .s-textarea[readonly] { background: var(--s-surface-2); color: var(--s-ink-2); cursor: default; }
.s-textarea { min-height: 92px; resize: vertical; }
/* A Django TextField arrives as rows="10" -- 224px, most of a phone screen,
   for a two-line note. The sheet's own .s-textarea is 92px and draggable;
   give the compat class the same, so a template that renders {{ form.notes }}
   gets the v2 size without anyone touching the form in Python. */
.s-v2 textarea.form-control { height: 92px; resize: vertical; }
/* ...unless the page asked for room. A match report is not a two-line note --
   somebody is writing three paragraphs on a phone, and 92px turns that into
   composing through a porthole. --tall opts back out of the shrink above. */
.s-v2 textarea.s-textarea--tall { height: auto; min-height: 260px; }

/* -- A value you were shown once and have to keep -------------------------
   A magic sign-in link, a temporary password, an API key. Not a field being
   filled in: a string that has to survive the trip into WhatsApp intact. So
   it is read-only, set in a face where l and 1 and O and 0 are different
   shapes, and the button that copies it sits on the same line -- under the
   box it reads as the next control in the form rather than as this box's
   own button. */
.s-copy { display: flex; align-items: stretch; gap: 8px; }
.s-copy .s-input { flex: 1 1 auto; min-width: 0; }
.s-copy .s-btn { flex: 0 0 auto; }
.s-input--mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--s-fs-sm);
  letter-spacing: .01em;
}

/* The same link as a square you can point a camera at, for handing a phone
   across a clubhouse table. The white ground and the padding are the quiet
   zone: a reader needs light around the pattern, and a QR that inherits a
   dark surface is one most camera apps quietly fail to see. */
.s-qr {
  display: block;
  width: 148px; height: 148px;
  padding: 8px;
  border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm);
  background: #ffffff;
}

/* The chevron a native <select> would otherwise draw in the platform's own
   style, redrawn so it matches the rest of the field. */
.s-v2 .form-select, .s-select {
  appearance: none; -webkit-appearance: none;
  padding-right: 34px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%237d8da3'%3E%3Cpath d='M4.5 6.2 8 9.7l3.5-3.5 1 1L8 11.7 3.5 7.2z'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  background-size: 16px;
}
/* Small enough to sit in a section heading beside the title. A season picker
   is a control the page is filtered by, not a field being filled in, so it
   should not arrive the width of the card. */
.s-select--sm {
  width: auto; max-width: 100%;
  padding: 6px 30px 6px 11px;
  font-size: var(--s-fs-sm);
  background-position: right 9px center;
}

/* A labelled control. .s-fields lays a run of them out; it collapses to one
   column on a phone because two half-width selects side by side is where the
   old filter bars started clipping their own option text. */
.s-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.s-field__label {
  font-size: var(--s-fs-xs); font-weight: 900;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--s-slate);
}
.s-field__help { font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-muted); }
.s-field__err  { font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-bad); }
/* A chip inside a field is a readout about the box above it -- the age a date
   of birth works out to. The field is a stretching flex column, so without
   this the chip runs the full width of the input and stops reading as a
   badge. */
.s-field > .s-chip { align-self: flex-start; }
.s-field--bad .s-input, .s-field--bad .s-select, .s-field--bad .s-textarea,
.s-field--bad .form-control, .s-field--bad .form-select { border-color: var(--s-bad); }
.s-fields { display: grid; grid-template-columns: 1fr; gap: var(--s-sp-3); }
@media (min-width: 560px) { .s-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 992px) { .s-fields { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* A run of fields carrying on below a rule: the second half of a form the
   page has already split once, so it reads as a continuation and not as a
   new card. */
.s-fields--sep {
  border-top: 1px solid var(--s-line-soft);
  padding-top: 14px; margin-top: 14px;
}
.s-fields__acts { display: flex; gap: var(--s-sp-2); flex-wrap: wrap; grid-column: 1 / -1; }
/* A field that gets the whole row: a team name, a note, a tick that governs
   the field under it. */
.s-field--wide { grid-column: 1 / -1; }

/* A grid of fields lands directly under the sentence that explains it, and a
   card is a plain block with no gap between its children. Without this the
   first FULL NAME sits on the line under "Who we call if we cannot reach
   you", and reads as the end of that sentence rather than the start of the
   form. Skipped when the grid is the whole card. */
.s-card > .s-fields:not(:first-child) { margin-top: var(--s-sp-3); }

/* A checkbox or radio and its words, on one line, with the whole line
   clickable — a 13px label is not a tap target.

   Named .s-tick, not .s-check: .s-check is already the setup-checklist row
   further up this file, and two components under one class is exactly the
   drift this sheet exists to stop. */
.s-tick { display: flex; align-items: flex-start; gap: 9px; cursor: pointer; padding: 7px 0; }
.s-tick input { width: 18px; height: 18px; margin: 1px 0 0; flex-shrink: 0; accent-color: var(--s-info); }
.s-tick__t { font-size: var(--s-fs-md); font-weight: 700; color: var(--s-ink); }
.s-tick__sub { display: block; margin-top: 1px; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }

/* A set of ticks whose labels are two or three characters -- sizes, day
   initials, shirt numbers. One per line, ten of those run to a screen and a
   half on a phone and the eye has nothing to scan. So they wrap and size to
   their own words instead. Only for short labels: a tick with a sentence
   beside it still belongs in a column. */
.s-tickrow { display: flex; flex-wrap: wrap; gap: 0 20px; }
.s-tickrow .s-tick { padding: 6px 0; }
/* The same row carrying sliders instead of ticks. Sliders are 26px tall and
   sit side by side, so this one needs a row gap the tick version does not. */
.s-tickrow--sw { gap: var(--s-sp-3) 20px; }
.s-tickrow--sw .s-switch { flex: 0 0 auto; }

/* The team list on a rota form: one team per line, a tick and the pitch it
   starts on. The pitch sits at the end of the line on a laptop and drops
   under the name on a phone, where 375px cannot hold a team name and a pitch
   name side by side. Indented under the name rather than the box, so it
   reads as belonging to the team above it. */
.s-rt { display: flex; flex-direction: column; margin-bottom: var(--s-sp-2); }
.s-rt__row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 2px 12px;
  padding: 4px 0; border-top: 1px solid var(--s-line);
}
.s-rt__row:first-child { border-top: 0; }
.s-rt__tick { flex: 1 1 200px; padding: 6px 0; }
.s-rt__sel { flex: 1 1 100%; width: auto; margin: 0 0 6px 27px; }
@media (min-width: 560px) {
  .s-rt__sel { flex: 0 1 240px; margin: 0; }
}

/* -- "optional" -----------------------------------------------------------
   On the team form sixteen of twenty-six fields are required, so starring the
   required ones marks nearly everything and tells you nothing. Mark the ones
   that can be left blank instead: it is the shorter list and the useful one. */
.s-field__opt {
  margin-left: 6px;
  font-weight: 800;
  color: var(--s-muted);
  letter-spacing: .04em;
}

/* -- Nudge a number up or down --------------------------------------------
   A squad size or an age is a number you move by one, not a number you
   retype. The buttons are thumb-sized because the control they replace is a
   13px-tall native spinner that nobody has ever hit on purpose.

   Styled by position, not by class, because the input in the middle is a
   Django-rendered widget whose classes are set in Python. */
.s-nudge { display: flex; align-items: stretch; }
.s-v2 .s-nudge > input {
  flex: 1; min-width: 0;
  text-align: center;
  border-radius: 0;
  border-left-width: 0;
  border-right-width: 0;
  -moz-appearance: textfield;
}
.s-nudge > input::-webkit-outer-spin-button,
.s-nudge > input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.s-nudge > button {
  flex-shrink: 0;
  width: 44px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1.5px solid var(--s-line);
  background: var(--s-surface-2);
  color: var(--s-ink);
  font-size: var(--s-fs-lg); font-weight: 900; font-family: inherit;
  line-height: 1;
  cursor: pointer;
}
.s-nudge > button:first-child { border-radius: var(--s-r-sm) 0 0 var(--s-r-sm); }
.s-nudge > button:last-child  { border-radius: 0 var(--s-r-sm) var(--s-r-sm) 0; }
.s-nudge > button:hover  { background: var(--s-line-soft); }
.s-nudge > button:active { background: var(--s-line); }

/* -- A field with its unit welded on --------------------------------------
   The pound sign belongs to the box rather than to the label, so it cannot be
   typed over and is not read as part of the number. */
.s-money { display: flex; align-items: stretch; }
.s-money__s {
  flex-shrink: 0;
  display: inline-flex; align-items: center;
  padding: 0 12px;
  border: 1.5px solid var(--s-line);
  border-right-width: 0;
  border-radius: var(--s-r-sm) 0 0 var(--s-r-sm);
  background: var(--s-surface-2);
  color: var(--s-ink-2);
  font-size: var(--s-fs-md); font-weight: 900;
}
.s-v2 .s-money > input {
  flex: 1; min-width: 0;
  border-radius: 0 var(--s-r-sm) var(--s-r-sm) 0;
}

/* -- "...or create one right here" ----------------------------------------
   Hangs under the dropdown it belongs to, on its own ground, so it reads as
   part of that field rather than as the next question. The behaviour is
   static/js/pp-inline-create.js, shared by every form in the app, and it
   cares only about the `hidden` attribute -- never about these classes. */
.s-inset {
  margin-top: 9px;
  padding: var(--s-sp-3);
  border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm);
  background: var(--s-surface-2);
  display: grid; gap: 10px;
}
.s-inset[hidden] { display: none; }
.s-inset__t {
  font-size: var(--s-fs-xs); font-weight: 900;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--s-info);
}
.s-inset__pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }

/* -- Rows a person adds and removes ---------------------------------------
   A short list the user grows: kits today, anything similar later. The remove
   control sits on its row, and the fields stack on a phone rather than being
   squeezed to eleven characters each. */
.s-rowset { display: flex; flex-direction: column; gap: 10px; }
.s-rowset__r {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px;
  align-items: start;
  padding: 10px;
  border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm);
  background: var(--s-surface-2);
}
.s-rowset__f { display: grid; gap: 8px; min-width: 0; }
@media (min-width: 560px) {
  .s-rowset__f { grid-template-columns: minmax(0, 12rem) minmax(0, 1fr); }
}
.s-rowset__x {
  width: 38px; height: 38px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1.5px solid var(--s-line);
  border-radius: var(--s-r-sm);
  background: var(--s-surface);
  color: var(--s-muted);
  cursor: pointer;
}
.s-rowset__x:hover { border-color: var(--s-bad-line); color: var(--s-bad); background: var(--s-bad-bg); }

/* -- One section of a long form ------------------------------------------
   .s-card on its own is a padded box with no opinion about its children,
   which is right for a card whose content is one thing. A form section is
   several things in a row -- a heading, a run of fields, a disclosure -- and
   they need even air between them. The heading and its sub-line go in a
   wrapper together, because that pair is the one place inside a card where
   two blocks should touch. */
.s-formcard { display: flex; flex-direction: column; gap: var(--s-sp-3); }
.s-formcard > .s-more { margin-top: 0; }

/* A postcode is written in capitals, so the box writes it in capitals. */
.s-caps { text-transform: uppercase; }

/* -- Pick a file ----------------------------------------------------------
   Django renders a bare <input type="file">, and the button inside it is drawn
   by the operating system: a small grey rectangle in a face nothing else on
   the page uses. It cannot be replaced without replacing the widget, but it
   can be dressed to match the ghost buttons beside it. */
.s-v2 input[type="file"] {
  padding: 8px 10px;
  font-size: var(--s-fs-sm);
  color: var(--s-ink-2);
}
.s-v2 input[type="file"]::file-selector-button {
  margin-right: 10px;
  padding: 7px 12px;
  border: 1.5px solid var(--s-line);
  border-radius: var(--s-r-sm);
  background: var(--s-surface-2);
  color: var(--s-ink);
  font-family: inherit; font-size: var(--s-fs-sm); font-weight: 800;
  cursor: pointer;
}
.s-v2 input[type="file"]::file-selector-button:hover {
  border-color: var(--s-info-line);
  background: var(--s-info-bg);
  color: var(--s-info);
}

/* -- A season, typed one line at a time -----------------------------------
   A league list has seven things per match and there are twenty matches in
   it, which is a spreadsheet -- and a spreadsheet on a phone is a horizontal
   scrollbar with Date and half of Time on the near side of it. So the row is
   a grid that reflows: date and kick-off on the first line, who you are
   playing on the second, home-or-away and the ground on the third. On a
   laptop the same row lays itself out as the single line it wants to be.

   Every track is minmax(0, ...), never a bare fr: a grid item's floor is its
   own content, so one long club name in the opponent <select> is enough to
   push the whole sheet off the right edge of a 375px screen. */
.s-sched__r {
  display: grid;
  /* The date input renders its own dd/mm/yyyy mask and a picker indicator, and
     at 375px there is about 320px to share out: every pixel the other three
     tracks do not need belongs to it, or the mask clips to "dd/mm/y". */
  grid-template-columns: 20px minmax(0, 1fr) 88px 34px;
  grid-template-areas:
    "num  date time x"
    ".    opp  opp  opp"
    ".    pair pair pair";
  gap: 7px 8px;
  padding: var(--s-sp-3);
  border-bottom: 1px solid var(--s-line);
}
.s-sched__r:last-child { border-bottom: none; }
.s-sched__r > *, .s-sched__pair > * { min-width: 0; }
.s-sched__r.is-bad { background: var(--s-bad-bg); }

.s-sched__num   { grid-area: num; }
.s-sched__date  { grid-area: date; }
.s-sched__time  { grid-area: time; }
.s-sched__opp   { grid-area: opp; }
/* Home/Away is two short words and the ground is a club's longest string, so
   they cannot share a column template with the date row above them. The pair
   is its own little grid on a phone and dissolves back into the row on a
   laptop, where the header strip needs each field in a column of its own.
   The two children take no grid-area of their own here: a named area that does
   not exist inside the pair's own grid would put them in an implicit track. */
.s-sched__pair  { grid-area: pair; display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 8px; }
.s-sched__notes { grid-area: notes; display: none; }
/* The heading strip has columns but no named areas, so "notes" here resolves to
   an implicit line: the cell drops to a second row and its auto-sized column
   steals width from the fr tracks, sliding every heading left of its field. */
.s-sched__hd .s-sched__notes { grid-area: auto; }
.s-sched__x     { grid-area: x; }

/* No grid-area, so it auto-places onto an implicit fourth row and a row with
   nothing wrong with it costs no height at all. */
.s-sched__err {
  grid-column: 1 / -1;
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-bad);
}

/* The number and the clear button line up with the *inputs* on the first
   line, not with the top of the cell -- which on a phone starts with a
   micro-label. align-self: end does that without anyone having to keep a
   magic padding in step with the label's line-height. */
.s-sched__num, .s-sched__x {
  align-self: end;
  height: 43px;
  display: inline-flex; align-items: center; justify-content: center;
}
.s-sched__num { font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-muted); }
.s-sched__x {
  border: 0; border-radius: var(--s-r-sm);
  background: none; color: var(--s-muted);
  font-size: var(--s-fs-md); cursor: pointer;
}
.s-sched__x:hover { color: var(--s-bad); background: var(--s-bad-bg); }

/* Android Chrome draws no calendar or clock glyph inside a date/time input
   the way desktop Chrome does, so in the installed app an empty one is an
   anonymous grey box with no hint of what it wants. It gets a micro-label
   instead -- phone-only, because on a laptop the column headings say it once
   for the whole sheet, and a label per cell would say it ten times. */
.s-sched__lab, .s-imp__lab, .s-tg__lab {
  display: block; margin-bottom: 3px;
  font-size: 10px; font-weight: 900;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--s-muted);
}
@media (max-width: 991.98px) {
  /* 16px, against the sheet's own 15px: below 16px iOS Safari zooms the whole
     page in on focus, and in a seven-field row that means every tap moves the
     six fields you were not typing in. */
  .s-sched__r .s-input, .s-sched__r .s-select,
  .s-imp__r .s-input,
  .s-mx--rota .s-select,
  .s-tg__r .s-input, .s-tg__r .s-select { font-size: 16px; }
}

/* The heading strip -- desktop only, because on a phone the micro-labels are
   doing that job in the rows themselves. */
.s-sched__hd { display: none; }

@media (min-width: 992px) {
  .s-sched__r, .s-sched__hd {
    grid-template-columns:
      26px minmax(0, 170px) minmax(0, 116px) minmax(0, 1.4fr) minmax(0, 104px) minmax(0, 1.2fr) 40px;
  }
  .s-sched__r { grid-template-areas: "num date time opp ha loc x"; }
  .s-sched__pair { display: contents; }
  .s-sched__ha { grid-area: ha; }
  .s-sched__loc { grid-area: loc; }
  .s-sched__hd {
    display: grid;
    gap: 7px 8px;
    padding: 9px var(--s-sp-4);
    border-bottom: 1px solid var(--s-line);
    background: var(--s-surface-2);
    font-size: var(--s-fs-xs); font-weight: 900;
    letter-spacing: .07em; text-transform: uppercase;
    color: var(--s-slate);
  }
  .s-sched__lab { display: none; }
  .s-sched__r .s-input, .s-sched__r .s-select { padding: 8px 10px; font-size: var(--s-fs-sm); }
  .s-sched__r .s-select { padding-right: 30px; }
  .s-sched__num, .s-sched__x { height: 38px; }
}

/* Notes are the one column nobody has to fill in, and until the screen is
   genuinely wide its width is better spent on the two selects whose option
   text was being clipped. */
@media (min-width: 1200px) {
  .s-sched__r, .s-sched__hd {
    grid-template-columns:
      26px minmax(0, 170px) minmax(0, 116px) minmax(0, 1.4fr) minmax(0, 104px) minmax(0, 1.2fr) minmax(0, 1fr) 40px;
  }
  .s-sched__r { grid-template-areas: "num date time opp ha loc notes x"; }
  .s-sched__notes { display: block; }
}

/* The "...or type a new one" box inside a row. Same idea as .s-inset, but a
   row has no room for a bordered panel with a heading -- it is one field,
   hanging under the select it belongs to. */
.s-sched__new { margin-top: 5px; }
.s-sched__new[hidden] { display: none; }
.s-sched__new .s-input { border-color: var(--s-info); }

/* -- A squad, typed one line at a time ------------------------------------
   The same problem .s-sched solves, with a different set of columns, so it
   gets its own grid rather than borrowing one whose areas are called date and
   opponent. A player is a photo, two names, a birthday and an FA number. On a
   phone the first name takes the whole first line beside the photo, the
   surname the second, and the two optional numbers share the third; on a
   laptop it lays itself out as the single line it wants to be.

   No row number. The server names a failed row "Row 4 (Alfie)", where 4 is
   the index in the field name -- and the old page renumbered the visible
   column whenever a row was removed while those field names kept their
   original index, so the number on screen and the number in the warning
   drifted apart the first time anyone deleted a row. The name is the half of
   that message that is always true, and it is already on the row. */
.s-imp__r {
  display: grid;
  grid-template-columns: 38px minmax(0, 1fr) 34px;
  grid-template-areas:
    "pic  first x"
    ".    last  last"
    ".    pair  pair";
  gap: 7px 8px;
  padding: var(--s-sp-3);
  border-bottom: 1px solid var(--s-line);
}
.s-imp__r:last-child { border-bottom: none; }
.s-imp__r.is-bad { background: var(--s-bad-bg); }
.s-imp__r > *, .s-imp__pair > * { min-width: 0; }
.s-imp__pic   { grid-area: pic; }
.s-imp__first { grid-area: first; }
.s-imp__last  { grid-area: last; }
/* The two optional numbers share the third line. Their own grid-areas belong
   to the desktop row and are set there: named on a phone, where .s-imp__pair
   is an ordinary two-track grid with no such areas in it, they would resolve
   to nothing and both cells would land in the same cell. */
.s-imp__pair  { grid-area: pair; display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 8px; }
.s-imp__x     { grid-area: x; }

/* Photo and remove line up with the *inputs* on the first line, not the top
   of the cell -- on a phone that cell starts with a micro-label. Same reason,
   and the same fix, as .s-sched__num. */
.s-imp__pic, .s-imp__x {
  align-self: end;
  height: 43px;
  display: inline-flex; align-items: center; justify-content: center;
}
.s-imp__x {
  border: 0; border-radius: var(--s-r-sm);
  background: none; color: var(--s-muted);
  font-size: var(--s-fs-md); cursor: pointer;
}
.s-imp__x:hover { color: var(--s-bad); background: var(--s-bad-bg); }

/* A label wrapping a file input, drawn as an avatar: the camera until a
   picture is chosen, the picture afterwards. On a page whose job is typing
   fifteen names, most people will never touch it -- so it is a quiet grey
   circle at the head of the row, not a column of its own. */
.s-imp__pic { cursor: pointer; }
.s-imp__pic img { display: none; }
.s-imp__pic.has-photo img { display: block; }
.s-imp__pic.has-photo i { display: none; }
.s-imp__pic.has-photo .s-avatar { border-color: var(--s-brand); }

/* The heading strip -- desktop only, because on a phone the micro-labels do
   that job in the rows themselves. */
.s-imp__hd { display: none; }

@media (min-width: 992px) {
  .s-imp__r, .s-imp__hd {
    grid-template-columns: 38px minmax(0, 1fr) minmax(0, 1fr) 152px minmax(0, 118px) 34px;
  }
  .s-imp__r { grid-template-areas: "pic first last dob fa x"; align-items: end; }
  .s-imp__pair { display: contents; }
  .s-imp__dob { grid-area: dob; }
  .s-imp__fa  { grid-area: fa; }
  .s-imp__hd {
    display: grid;
    gap: 7px 8px;
    padding: 9px var(--s-sp-4);
    border-bottom: 1px solid var(--s-line);
    background: var(--s-surface-2);
    font-size: var(--s-fs-xs); font-weight: 900;
    letter-spacing: .07em; text-transform: uppercase;
    color: var(--s-slate);
  }
  .s-imp__lab { display: none; }
  .s-imp__r .s-input { padding: 8px 10px; font-size: var(--s-fs-sm); }
  .s-imp__pic, .s-imp__x { height: 38px; }
}

/* -- The bar that saves it ------------------------------------------------
   Ten rows is more than a screen, so a Save button at the bottom of the form
   is a button you have to go looking for. It sits above the tab bar instead,
   where it can say how many fixtures pressing it will make. On a laptop the
   whole form fits and it goes back to being an ordinary button at the end. */
.s-savebar {
  position: fixed; left: 0; right: 0; z-index: 45;
  bottom: var(--s-nav-h);
  display: flex; align-items: center; gap: 9px;
  padding: 10px var(--s-sp-4);
  background: var(--s-surface);
  border-top: 1px solid var(--s-line);
  box-shadow: 0 -6px 20px rgba(15, 23, 42, .1);
}
.s-savebar .s-btn { flex: 1 1 auto; justify-content: center; }
.s-savebar .s-btn--primary { flex: 2 1 auto; }
/* The form needs to be able to scroll clear of it. */
.s-savebar__gap { height: 74px; }
@media (min-width: 992px) {
  .s-savebar {
    position: static;
    padding: 0; border: 0; box-shadow: none; background: none;
  }
  .s-savebar .s-btn, .s-savebar .s-btn--primary { flex: 0 0 auto; }
  .s-savebar__gap { display: none; }
}

/* One savebar is not a save button. The club shop's is a jump link: the order
   is at the end of a long catalogue and a phone cannot see both at once. A
   laptop can, and a button that scrolls you to the thing already on screen is
   just a second thing to read. */
@media (min-width: 992px) { .s-savebar--jump { display: none; } }

/* ── Table ──────────────────────────────────────────────────────────────
   Desktop by convention: every table in this app has a card list beside it
   for phones, because a horizontally scrolling table is a way of saying "not
   for you". Wrap it in .s-scroll-x anyway — a laptop at 1024px with nine
   columns still has to go somewhere. */
.s-table { width: 100%; border-collapse: collapse; font-size: var(--s-fs-sm); }
.s-table th, .s-table td { padding: 11px var(--s-sp-3); text-align: left; vertical-align: middle; }
.s-table thead th {
  font-size: var(--s-fs-xs); font-weight: 900;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--s-slate);
  background: var(--s-surface-2);
  border-bottom: 1px solid var(--s-line);
  white-space: nowrap;
}
.s-table tbody tr { border-bottom: 1px solid var(--s-line-soft); }
.s-table tbody tr:last-child { border-bottom: none; }
.s-table tbody td { color: var(--s-ink-2); font-weight: 600; }
.s-table__strong { display: block; font-weight: 800; color: var(--s-ink); }
/* The block display is for the span inside a cell. Put straight on the
   cell it takes that <td> out of the table box and the value drifts off
   its own row, so pin it back rather than making every caller wrap. */
td.s-table__strong, th.s-table__strong { display: table-cell; }
.s-table__sub { display: block; margin-top: 1px; font-size: var(--s-fs-xs); color: var(--s-muted); font-weight: 600; }
.s-table__num { text-align: right; font-variant-numeric: tabular-nums; }
/* A number column is narrow by nature and does not need the full text
   gutter. Six of them side by side is a league table on a phone, and
   that is exactly where the gutter decides whether Pts is on screen. */
.s-table th.s-table__num,
.s-table td.s-table__num { padding-left: 7px; padding-right: 9px; }
.s-table__end { text-align: right; }
/* A number column's heading has to sit over its own column, and
   `.s-table thead th` out-specifies a bare utility class. */
.s-table thead th.s-table__num,
.s-table thead th.s-table__end { text-align: right; }
/* The row you came to find. A league table is a wall of numbers, so your own
   team gets a tint and a weight rather than a colour you have to decode. */
.s-table tbody tr.is-mine { background: var(--s-info-bg); }
.s-table tbody tr.is-mine td { color: var(--s-ink); font-weight: 800; }
.s-table__acts { display: flex; gap: 5px; justify-content: flex-end; }
/* A sortable heading is a link, but it must not look like body copy that
   happens to be blue — it inherits the heading and only the arrow moves. */
.s-table__sort { display: inline-flex; align-items: center; gap: 5px; color: inherit; text-decoration: none; }
.s-table__sort i { color: var(--s-muted); font-size: 11px; }
.s-table__sort:hover { color: var(--s-ink); }
/* One card per team, one table per card — with auto layout each table sizes
   its own columns, so stacked cards read as a single table whose rows have
   drifted off the headings. Fixed layout + a width on every non-text column
   gives all of them the same grid. The min-width hands narrow screens to the
   .s-scroll-x wrapper instead of letting fixed columns crush each other. */
.s-table--samegrid { table-layout: fixed; min-width: 860px; }
.s-table--samegrid th.s-table__num { width: 86px; }
.s-table--samegrid th.s-table__fa { width: 64px; }
.s-table--samegrid th.s-table__end { width: 168px; }

/* ── Attendance matrix: names down the side, sessions across ─────────────── */
/* A register grid is wide by definition — one column per session — so it
   scrolls sideways and the name column stays put. The number that actually
   answers "how is this child doing" travels with the name rather than sitting
   at the far right past twenty columns of dots, which on a phone is a swipe
   nobody makes. The dots become the detail you scroll to, not the toll you
   pay to reach the summary. */
.s-mx { border-collapse: separate; border-spacing: 0; width: 100%; font-size: var(--s-fs-sm); }
.s-mx th, .s-mx td { padding: 6px 3px; text-align: center; white-space: nowrap; }
.s-mx thead th {
  background: var(--s-surface-2);
  border-bottom: 1px solid var(--s-line);
  font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-slate);
}
/* The name cell has to paint over the dots sliding under it, so it carries an
   opaque background of its own — a transparent sticky cell shows the grid
   through it and reads as a rendering fault. */
.s-mx__who {
  position: sticky; left: 0; z-index: 3;
  text-align: left; padding-left: var(--s-sp-3); padding-right: 10px;
  background: var(--s-surface);
  border-right: 1px solid var(--s-line);
  min-width: 148px; max-width: 148px;
}
.s-mx thead .s-mx__who { z-index: 4; background: var(--s-surface-2); }
/* Freezing the date row as well means the grid has to own its own vertical
   scrolling, and a scroll area nested inside a scrolling page is a bad trade
   on a phone — the wrong thing moves under your thumb, and the page can only
   be reached by aiming at the margin. With a pointer it is the right trade, so
   above the width where that is true the dates stay put and below it the phone
   just scrolls the page. A squad short enough to fit never notices either way. */
@media (min-width: 900px) and (min-height: 620px) {
  .s-mx__pane { max-height: 66vh; }
  .s-mx thead th { position: sticky; top: 0; z-index: 2; }
  .s-mx thead .s-mx__who { z-index: 4; }
}
/* `.s-mx th, .s-mx td` centres every cell and out-specifies a bare class, so
   the name column needs to say it on the element to win. */
.s-mx th.s-mx__who, .s-mx td.s-mx__who { text-align: left; }
.s-mx tbody tr:hover td { background: var(--s-surface-2); }
.s-mx tbody tr:hover .s-mx__who { background: var(--s-surface-2); }
.s-mx tbody td { border-top: 1px solid var(--s-line-soft); }
.s-mx__nm {
  display: block; font-weight: 800; color: var(--s-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* A rota names teams, not children. "Pulford Pirates U11 Rovers" and "Pulford
   Pirates U13 Athletic" are identical for the first 148px, so the ellipsis
   that keeps a register tidy hid the only part of a team name worth reading.
   The rota grid gives the name two lines and keeps the column width, so the
   dates beside it are where they were. */
.s-mx--rota .s-mx__nm { white-space: normal; overflow: visible; line-height: 1.25; }
.s-mx--rota th, .s-mx--rota td { vertical-align: middle; }

/* ── The rota cell: the pitch, as a control, in the grid ─────────────── */
/* A register cell is a dot you tap. A rota cell is one pitch out of however
   many the club has, so it is a select and it lives in the grid rather than
   behind a row that opens somewhere else. Editing where you are reading is the
   point of a spreadsheet: somebody comparing two weeks should not have to
   leave the comparison to change one of them. */
.s-cell { display: block; margin: 0; }
.s-cell__sel {
  width: 100%; min-width: 106px;
  padding: 5px 24px 5px 8px;
  font-size: var(--s-fs-sm); font-weight: 700;
  background-position: right 7px center; background-size: 14px;
}
/* Saved, and refused. Both clear on the next change, so a cell says what just
   happened to it rather than keeping a history nobody reads. */
.s-cell__sel.is-saved { border-color: var(--s-good); }
.s-cell__sel.is-bad { border-color: var(--s-bad); }
/* The button is the path for a browser with no script. Script saves on the
   change event, so leaving it on screen offers a second way to do a thing
   that is already done. */
.s-cell__go { margin-top: 4px; width: 100%; }
/* The line under the cell: the time the booking runs, or what Fill would put
   there while the cell is still empty. */
.s-cell__hint {
  display: block; margin-top: 3px;
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted);
  overflow: hidden; text-overflow: ellipsis; max-width: 118px;
}
/* Locked. An admin has said leave this one, so it reads as a fact rather than
   arriving as a control that then refuses. */
.s-cell__fixed {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink);
}
/* No session for that team that day. Empty would read as a cell that failed to
   draw, so it carries a short rule instead. */
.s-cell__off {
  display: block; width: 18px; height: 2px; margin: 0 auto;
  border-radius: 1px; background: var(--s-line);
}
.s-mx--rota td { min-width: 122px; }
/* A day can hold more than one thing for one team: a session and a cup tie, or
   two age groups training back to back. The cell was a single control, so the
   second one used to vanish off the rota and turn up on the night with no
   pitch. It stacks now, and each thing carries its own pitch. */
.s-mx--rota td { vertical-align: top; }
.s-cell__ev + .s-cell__ev {
  margin-top: 7px; padding-top: 7px; border-top: 1px solid var(--s-line-soft);
}
/* What the pitch is being held for. Four kinds share a column, so a coach
   reading down a Tuesday can tell a training night from a cup tie without
   opening anything. Each kind gets its own word as well as its own colour,
   because colour alone would say nothing to anyone who cannot separate the
   two blues. */
.s-cell__ty {
  display: inline-block; margin-bottom: 3px; padding: 0 5px;
  border-radius: var(--s-r-sm); font-size: 10px; font-weight: 900;
  letter-spacing: .04em; text-transform: uppercase;
  background: var(--s-good-bg); color: var(--s-good);
}
.s-cell__ty--fixture    { background: var(--s-info-bg); color: var(--s-info); }
.s-cell__ty--cup        { background: var(--s-warn-bg); color: var(--s-warn); }
.s-cell__ty--tournament { background: var(--s-line-soft); color: var(--s-ink-2); }
/* The event's own name under the badge. A team row already says who, so this
   says which: "v Marford" or the tournament's name. */
.s-cell__nm {
  display: block; max-width: 118px; margin-bottom: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-ink-2);
}

/* The child's own summary, riding along under the name. */
.s-mx__pc { display: flex; align-items: center; gap: 6px; margin-top: 3px; }
.s-mx__pc .s-progress { flex: 1; min-width: 0; }
.s-mx__pcv {
  flex-shrink: 0; font-size: var(--s-fs-xs); font-weight: 900;
  font-variant-numeric: tabular-nums; color: var(--s-good);
}
.s-mx__pcv--mid { color: var(--s-warn); }
.s-mx__pcv--low { color: var(--s-bad); }
.s-mx__pc--mid .s-progress__fill { background: var(--s-warn); }
.s-mx__pc--low .s-progress__fill { background: var(--s-bad); }
/* One session's answer for one child. Colour alone would fail anyone who
   can't separate green from grey, so present is filled, absent is an outline
   and a register nobody took is flat and faint — three shapes, not three hues. */
.s-mx__d {
  display: inline-block; width: 22px; height: 22px;
  border-radius: var(--s-r-sm); vertical-align: middle;
  background: var(--s-line-soft); border: 1px solid transparent;
}
.s-mx__d--in  { background: var(--s-good); }
.s-mx__d--out { background: transparent; border-color: var(--s-slate); }
/* The column heading: a date, and what kind of thing it was. */
.s-mx__col { min-width: 46px; }
.s-mx__ty {
  display: inline-block; margin-top: 2px; padding: 0 5px;
  border-radius: var(--s-r-sm); font-size: 10px; font-weight: 900;
  background: var(--s-good-bg); color: var(--s-good);
}
.s-mx__ty--m { background: var(--s-info-bg); color: var(--s-info); }
.s-mx__ty--t { background: var(--s-warn-bg); color: var(--s-warn); }
.s-mx__opp {
  display: block; max-width: 52px; margin: 1px auto 0;
  overflow: hidden; text-overflow: ellipsis;
  font-size: 10px; font-weight: 700; color: var(--s-muted);
}
/* The key. Small, quiet, and it wraps rather than forcing its own scrollbar. */
.s-key { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }
.s-key > span { display: inline-flex; align-items: center; gap: 5px; }
.s-key .s-mx__d { width: 14px; height: 14px; }

/* ── Date tile: the day-and-month block that opens an event row ─────────── */
.s-datetile {
  width: 52px; flex-shrink: 0;
  padding: 7px 4px;
  border-radius: var(--s-r-sm);
  background: var(--s-brand); color: var(--s-brand-on);
  text-align: center;
  align-self: flex-start;
}
.s-datetile__d { font-size: 1.3rem; font-weight: 900; line-height: 1; font-variant-numeric: tabular-nums; }
.s-datetile__m {
  margin-top: 2px;
  font-size: 10px; font-weight: 900;
  letter-spacing: .08em; text-transform: uppercase;
  opacity: .82;
}
/* A tournament's tile. The calendar already answers "what colour is a
   tournament" -- .s-dot--tournament is amber -- so the tile that opens a
   tournament row uses that answer rather than inventing a second one. The
   fill is the dark end of the amber ramp because the numerals sit on it. */
.s-datetile--cup { background: var(--s-warn); color: #fff; }
/* The tile with its chips stacked underneath, for an event's header card.
   The chips used to open the body column above the title, which pushed the
   title down past the tile; stacked under the tile they leave the title level
   with it (Ben's mockup, 11 Sep 2026). One chip per line: the column is only
   as wide as the tile. */
.s-datestack {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 7px; flex-shrink: 0;
}
.s-datestack .s-chips { margin-bottom: 0; flex-direction: column; align-items: flex-start; }
/* A chip in the stack is the column's width, so it is set a shade tighter:
   "Training" comes out at 67px rather than 73px, and with the row's gap at
   10px the title column keeps "Pulford Pirates" on one line at 375px. */
.s-datestack .s-chip { padding: 3px 7px; letter-spacing: .04em; }
.s-evhead { gap: 10px; }
/* The header card's lines get a little more air than a list row's. */
.s-evhead .s-h1 + .s-meta { margin-top: 10px; }
.s-evhead .s-meta--lg { gap: 9px; }
.s-evhead .s-meta + .s-meta { margin-top: 10px; }

/* ── Score ──────────────────────────────────────────────────────────────
   Neutral until the result is known, then tinted. A draw stays neutral on
   purpose: at this age group it is a result, not a near-miss. */
.s-score {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 62px; padding: 2px 9px;
  border: 1px solid var(--s-line);
  border-radius: var(--s-r-pill);
  background: var(--s-surface-2);
  font-size: var(--s-fs-sm); font-weight: 900;
  font-variant-numeric: tabular-nums;
  color: var(--s-ink);
}
.s-score--win  { background: var(--s-good-bg); border-color: var(--s-good-line); color: var(--s-good); }
.s-score--loss { background: var(--s-bad-bg);  border-color: var(--s-bad-line);  color: var(--s-bad); }

/* ── Tally: the scoreboard a volunteer runs from the touchline ────────────
   .s-score above is the read-only pill every other page shows once the result
   is in. This is the thing that fills it in, so it is sized for a cold thumb
   in November rather than for a mouse: 46px buttons, numerals big enough to
   check from arm's length while you are watching the game and not the phone.

   Team names sit in a fixed-height cell so a one-line name and a name that
   wraps to two still leave the two numerals on the same line as each other. */
.s-tally {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-rows: auto auto auto;
  column-gap: var(--s-sp-2);
  text-align: center;
}
/* Each side is display:contents so its parts drop straight into the grid rows
   below. That is what keeps the "-" pinned to the numerals: whether a team's
   name takes one line or two, the numerals are still row 2 and so is it. */
.s-tally__side { display: contents; }
.s-tally__side--home > * { grid-column: 1; }
.s-tally__side--away > * { grid-column: 3; }
.s-tally__t {
  grid-row: 1;
  display: flex; align-items: center; justify-content: center;
  min-height: 2.8em; margin-bottom: 4px;
  font-size: var(--s-fs-sm); font-weight: 800; line-height: 1.3; color: var(--s-ink-2);
}
.s-tally__n {
  grid-row: 2;
  font-size: 3.1rem; font-weight: 900; line-height: 1;
  font-variant-numeric: tabular-nums; color: var(--s-ink);
}
.s-tally__v {
  grid-row: 2; grid-column: 2; align-self: center;
  font-size: var(--s-fs-xl); font-weight: 900; color: var(--s-muted);
}
.s-tally__btns { grid-row: 3; display: flex; gap: var(--s-sp-2); justify-content: center; margin-top: 10px; }
.s-tally__b {
  width: 56px; height: 46px;
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  background: var(--s-surface); color: var(--s-ink);
  font-size: 1.25rem; font-weight: 900; line-height: 1; cursor: pointer;
}
.s-tally__b:hover { background: var(--s-surface-2); }
.s-tally__b:active { transform: scale(.95); }

/* ── Event card: one fixture or session, on a phone ───────────────────
   Date tile, then the things a coach checks on the touchline — who is
   available, whether the pitch is booked, whether there is a plan — then the
   actions. Sits inside .s-card--flush so the strips can run edge to edge.

   The phone card is not a cut-down table row. It carries the same facts,
   arranged for a thumb; that is the whole reason the list is built twice. */
.s-fx { display: grid; grid-template-columns: 52px minmax(0, 1fr); gap: var(--s-sp-3); padding: var(--s-sp-3); }
.s-fx__body { min-width: 0; }
.s-fx__title { min-width: 0; font-size: var(--s-fs-md); font-weight: 900; line-height: 1.2; color: var(--s-ink); }
.s-fx__meta {
  display: flex; flex-wrap: wrap; gap: 3px 10px;
  margin-top: 6px;
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted);
}
.s-fx__meta i { margin-right: 3px; }
.s-fx__chips { margin-top: 8px; gap: 5px; }
/* Two columns, with the primary action spanning both. A row that wraps gives
   you a full-width button next to a 90px one; a grid keeps every action a
   thumb wide however many the fixture happens to have. */
.s-fx__acts { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: var(--s-sp-3); background: var(--s-surface-2); }
.s-fx__acts .s-btn { min-height: 42px; }
.s-fx__acts-wide { grid-column: 1 / -1; }
/* The next fixture is what the page exists to answer, so it gets a ring
   rather than a fill — tinting the whole card would fight the strips in it. */
.s-fx-next { border-color: var(--s-info); box-shadow: 0 0 0 3px var(--s-info-bg), var(--s-shadow-sm); }
/* A fixture that is not happening. The mirror of `.s-row--off`, and struck
   for the same reason: opacity alone reads as "older", which is exactly wrong
   for a match still sitting at the top of a parent's Coming up list. Only
   cancelled earns this -- a postponed match IS still on, just not then, and
   striking it through would tell a parent to stop watching for the new date. */
.s-fx--off { opacity: .7; }
.s-fx--off .s-fx__title { text-decoration: line-through; }

/* Three labelled facts across a card. Values clip rather than wrap: the label
   already says what the number is, but a value on two lines drops that cell
   out of line with the two beside it. */
.s-facts {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  border-top: 1px solid var(--s-line); border-bottom: 1px solid var(--s-line);
}
.s-facts__i { min-width: 0; padding: 9px 8px; text-align: center; border-right: 1px solid var(--s-line); }
.s-facts__i:last-child { border-right: none; }
/* A fourth fact wraps onto a row of its own. Left at a third of the width it
   reads as a column that has lost its neighbours, so a lone trailing cell
   takes the whole row instead. */
.s-facts__i:last-child:nth-child(3n + 1) { grid-column: 1 / -1; }
/* Three-up is the shape, not the rule. A strip carrying two facts (the pitch
   card is one: pitch, booked) left a third of the width blank and squeezed
   "09:00 to 10:00" down to "09:00 to 10..." on a 375px phone. Count the cells
   and take only the columns they need. */
.s-facts:not(:has(.s-facts__i:nth-child(2))) { grid-template-columns: minmax(0, 1fr); }
.s-facts:has(.s-facts__i:nth-child(2)):not(:has(.s-facts__i:nth-child(3))) {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
.s-facts__k { font-size: 10px; font-weight: 900; letter-spacing: .05em; text-transform: uppercase; color: var(--s-muted); }
.s-facts__v {
  margin-top: 2px;
  font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ── Calendar ──────────────────────────────────────────────────────────────
   Three views over one list of events — month, week, agenda — all reading
   the kind colours defined once further up this file. Nothing here names a
   colour of its own; every fill, rail and dot is var(--s-evc) or
   var(--s-evc-bg), which is why the type map is the only place a tenth event
   type has to touch.

   Each rule carries the slate fallback anyway. An undefined custom property
   resolves to nothing, and nothing as a background is transparent — an event
   whose kind this sheet has not heard of should look plain, not invisible. */

/* The period stepper: back, the month or week you are on, forward. */
.s-period { display: flex; align-items: center; gap: 4px; }
.s-period__lb {
  min-width: 138px;
  text-align: center;
  font-size: var(--s-fs-md); font-weight: 900; color: var(--s-ink);
}
/* The search box in the calendar toolbar. It shares a wrapping row with the
   stepper and the view switch, so it takes the slack when there is any and
   drops to its own line when there is not. */
.s-calq { flex: 1 1 150px; max-width: 220px; }
/* What you can do with the event you are looking at, directly under its
   heading rather than trailing whichever section happened to render last. */
.s-evacts { margin-top: var(--s-sp-3); }

/* ─ Month ─────────────────────────────────────────────────────────────────── */
.s-mth__dow, .s-mth__grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }
.s-mth__dow { margin-bottom: 6px; }
.s-mth__dowc {
  text-align: center;
  font-size: 10px; font-weight: 900;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--s-muted);
}
.s-mcell {
  position: relative;
  display: flex; flex-direction: column; gap: 3px;
  min-height: 108px; padding: 5px;
  border: 1px solid var(--s-line); border-radius: 10px;
  background: var(--s-surface);
  overflow: hidden;
}
/* Days either side of the month keep their box — the grid has to stay seven
   wide — but sit back, so the month itself still reads as one shape. */
.s-mcell.is-out { background: var(--s-surface-2); }
.s-mcell.is-out .s-mcell__d { color: var(--s-muted); }
.s-mcell__d {
  font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-ink);
  font-variant-numeric: tabular-nums;
}
.s-mcell.is-today .s-mcell__d {
  /* align-self, because the cell is a flex column and a stretched item would
     make the circle a cell-wide ellipse. */
  align-self: flex-start;
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 21px; height: 21px; border-radius: 50%;
  background: var(--s-brand); color: var(--s-brand-on);
}
/* Managers can start an event on a day. A button, rather than a click handler
   on the whole cell: a cell full of chips has no obvious empty part to aim at,
   and a keyboard cannot aim at all. */
.s-mcell__add {
  position: absolute; top: 4px; right: 4px;
  width: 20px; height: 20px;
  display: inline-flex; align-items: center; justify-content: center;
  border: none; border-radius: 6px;
  background: var(--s-surface-2); color: var(--s-muted);
  font-size: 12px; line-height: 1; cursor: pointer;
  opacity: 0;
}
.s-mcell:hover .s-mcell__add, .s-mcell__add:focus-visible { opacity: 1; }
.s-mth__more {
  padding: 0 3px;
  border: none; background: none;
  font-size: 10px; font-weight: 800; color: var(--s-muted);
  text-align: left; cursor: pointer;
}

/* One event inside a month cell. */
.s-evchip {
  display: flex; align-items: center; gap: 4px;
  width: 100%; padding: 2px 5px;
  border: none; border-left: 3px solid var(--s-evc, var(--s-slate));
  border-radius: 5px;
  background: var(--s-evc-bg, var(--s-slate-bg));
  color: var(--s-evc, var(--s-slate));
  font-size: 10px; font-weight: 800; line-height: 1.35; text-align: left;
  white-space: nowrap; overflow: hidden;
  cursor: pointer;
}
.s-evchip__t { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* A cancelled event stays on the calendar. People turn up to things that were
   called off, so a line through it says more than removing it would. */
.s-evt.is-off { opacity: .55; }
.s-evchip.is-off .s-evchip__t, .s-agev.is-off .s-agev__ti { text-decoration: line-through; }
/* Still waiting on this family. A ring, not another colour — the colour is
   already busy saying what kind of event it is. */
.s-evt.needs-you { box-shadow: inset 0 0 0 1.5px var(--s-warn); }

/* Below this width a chip is two clipped words, so the cell shows its kinds as
   dots instead: a phone still sees what sort of day it is, and taps the day to
   read it. */
.s-mcell__dots { display: none; flex-wrap: wrap; gap: 3px; }
@media (max-width: 560px) {
  .s-mth__dow, .s-mth__grid { gap: 3px; }
  .s-mcell { min-height: 56px; padding: 4px 3px; }
  .s-mcell .s-evchip, .s-mth__more, .s-mcell__add { display: none; }
  .s-mcell__dots { display: flex; }
}

/* ─ Week ──────────────────────────────────────────────────────────────────── */
/* A day column is twenty-four hour rows tall, so where an event sits in the
   day is a percentage of the column. The top and the height are the only two
   things the script sets on a node, the same way the dashboards set a progress
   bar: they are where the event is, not what it looks like, and everything
   about what it looks like is right here. */
.s-wk {
  --s-wk-hr: 34px;
  border: 1px solid var(--s-line); border-radius: var(--s-r);
  background: var(--s-surface);
  overflow: hidden;
}
.s-wk__scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.s-wk__head, .s-wk__body { min-width: 580px; display: grid; grid-template-columns: 44px repeat(7, minmax(0, 1fr)); }
.s-wk__head { border-bottom: 1px solid var(--s-line); background: var(--s-surface-2); }
.s-wk__hc { padding: 6px 2px; text-align: center; border-left: 1px solid var(--s-line); }
.s-wk__hc:first-child { border-left: none; }
.s-wk__hd { font-size: 10px; font-weight: 900; letter-spacing: .05em; text-transform: uppercase; color: var(--s-muted); }
.s-wk__hn { font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-ink); font-variant-numeric: tabular-nums; }
.s-wk__hc.is-today .s-wk__hd, .s-wk__hc.is-today .s-wk__hn { color: var(--s-info); }
.s-wk__body { max-height: 60vh; overflow-y: auto; }
.s-wk__hr, .s-wk__ln { height: var(--s-wk-hr); border-top: 1px solid var(--s-line-soft); }
.s-wk__hr {
  padding-right: 5px; text-align: right;
  font-size: 10px; font-weight: 800; color: var(--s-muted);
  font-variant-numeric: tabular-nums;
}
.s-wk__col { position: relative; border-left: 1px solid var(--s-line); }
.s-wkev {
  position: absolute; left: 2px; right: 2px;
  top: 0; height: 4%;
  min-height: 15px; padding: 2px 5px;
  border: none; border-left: 3px solid var(--s-evc, var(--s-slate));
  border-radius: 5px;
  background: var(--s-evc-bg, var(--s-slate-bg));
  color: var(--s-evc, var(--s-slate));
  font-size: 10px; font-weight: 800; line-height: 1.25; text-align: left;
  overflow: hidden; cursor: pointer;
}
/* How far into today it is. Same again: a percentage, set by the script. */
.s-wk__now {
  position: absolute; left: 0; right: 0;
  top: 0; height: 2px;
  background: var(--s-bad);
  pointer-events: none;
}

/* ─ Agenda ────────────────────────────────────────────────────────────────── */
/* The phone view, and the one no grid beats for "what is on soon". A day with
   nothing in it is not drawn at all. */
.s-agday { margin-top: var(--s-sp-4); }
.s-agday:first-child { margin-top: 0; }
.s-agday__h {
  display: flex; align-items: baseline; gap: 8px;
  margin-bottom: 4px; padding-bottom: 5px;
  border-bottom: 1px solid var(--s-line);
  font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-ink);
}
.s-agday.is-today .s-agday__h { color: var(--s-info); border-bottom-color: var(--s-info-line); }
.s-agday__n { margin-left: auto; font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }
.s-agev {
  display: grid; grid-template-columns: 52px 4px minmax(0, 1fr); align-items: stretch; gap: 9px;
  width: 100%; padding: 9px 2px;
  border: none; border-top: 1px solid var(--s-line-soft);
  background: none; text-align: left; cursor: pointer;
}
.s-agev:first-of-type { border-top: none; }
.s-agev__t {
  padding-top: 2px;
  font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-muted);
  font-variant-numeric: tabular-nums;
}
.s-agev__bar { border-radius: 2px; background: var(--s-evc, var(--s-slate)); }
.s-agev__b { min-width: 0; }
.s-agev__ti { font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-ink); }
.s-agev__m {
  display: flex; flex-wrap: wrap; gap: 2px 10px; margin-top: 2px;
  font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted);
}
.s-agev__m i { margin-right: 3px; }

/* A wider dialog than the default. The event panel and the new-event form both
   carry a form's worth of content, not a confirmation's worth. */
.s-dialog--wide { max-width: 560px; }
/* Inside one, two columns is the ceiling however wide the screen gets. */
@media (min-width: 992px) { .s-fields--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* ── 11. THE FEED ──────────────────────────────────────────────────────── */
/* The feed is the one screen in Sprout that is a product in its own right —
   composer, post card, gallery, reactions, threaded comments, polls, GIF
   picker, lightbox. In v1 all of that lived in a 2,600-line <style> block at
   the top of feed_list.html, which is precisely the fragmentation this sheet
   exists to end. It is rewritten here on the tokens.

   The class names are the ones the page's JavaScript already selects on. They
   stay as they are on purpose: the rebuild is a change of skin, not of
   behaviour, and renaming ~80 hooks across 1,600 lines of working script
   would be a lot of risk bought for a prefix. */

/* Bootstrap's collapse, minus Bootstrap. base_v2 ships no framework JS, so the
   page provides the three calls its script uses; these are the two states. */
.s-v2 .collapse:not(.show) { display: none; }

/* ── 11a. Layout ───────────────────────────────────────────────────────── */
.feed { display: block; }

.feed-topbar {
  display: flex; align-items: center; gap: var(--s-sp-3);
  flex-wrap: wrap; margin-bottom: var(--s-sp-4);
}
.feed-page-title {
  margin: 0; font-size: var(--s-fs-xl); font-weight: 900;
  color: var(--s-ink); letter-spacing: -0.02em;
}
.feed-topbar-actions { margin-left: auto; display: flex; gap: var(--s-sp-2); }

/* The drawer button is fixed to the top-right corner of the content area, so
   on a phone the topbar keeps out from under it: the title reserves the
   corner and the actions drop to a row of their own. */
@media (max-width: 599.98px) {
  .feed-page-title { padding-right: 58px; }
  .feed-topbar { gap: var(--s-sp-2); }
  .feed-topbar-actions { margin-left: 0; width: 100%; }
  .missed-btn { max-width: 100%; }
  .missed-btn-inner { min-width: 0; }
  .missed-label { overflow: hidden; text-overflow: ellipsis; }
}

/* One column on a phone. The sidebar is filters and broadcasts — useful, not
   load-bearing — so below the breakpoint it goes to the end rather than
   competing with the posts for the fold. */
.feed-layout { display: grid; gap: var(--s-sp-4); }
.feed-posts-col { min-width: 0; }
.feed-sidebar { min-width: 0; display: grid; gap: var(--s-sp-3); align-content: start; }
@media (min-width: 900px) {
  .feed-layout { grid-template-columns: minmax(0, 1fr) 300px; align-items: start; }
  .feed-sidebar { position: sticky; top: calc(var(--s-hdr-h) + 12px); }
}

/* ── 11b. "You may have missed" ────────────────────────────────────────── */
.missed-btn {
  display: none; align-items: center; padding: 0; overflow: hidden;
  border: 1px solid var(--s-warn-line); background: var(--s-surface);
  color: var(--s-ink); border-radius: var(--s-r-pill);
  font: inherit; font-size: var(--s-fs-sm); font-weight: 800;
  white-space: nowrap; cursor: pointer; box-shadow: var(--s-shadow-sm);
  transition: transform .18s ease, background .18s ease;
}
.missed-btn:hover { transform: translateY(-2px); }
/* The click handler sits on the button; let the label and count fall through
   to it rather than swallowing taps aimed at the middle of the pill. */
.missed-btn * { pointer-events: none; }
.missed-btn-inner { display: flex; align-items: center; gap: 6px; padding: 7px 14px; }
.missed-icon { font-size: 1rem; line-height: 1; transition: transform .25s ease; }
.missed-btn:hover .missed-icon { transform: scale(1.2); }
.missed-label { letter-spacing: -0.01em; }
.missed-count {
  min-width: 20px; padding: 1px 7px; text-align: center;
  border-radius: var(--s-r-pill); background: var(--s-accent);
  color: var(--s-accent-on); font-size: var(--s-fs-xs); font-weight: 900;
}
.missed-btn--next { background: var(--s-warn-bg); border-color: var(--s-warn); }
.missed-btn--done {
  background: var(--s-good-bg); border-color: var(--s-good-line);
  color: var(--s-good); pointer-events: none;
}
.missed-btn--pop { animation: missedPop .38s ease forwards; }
.missed-btn--fadeout { animation: missedFadeOut .65s ease forwards; }
@keyframes missedPop {
  0% { transform: scale(1); } 40% { transform: scale(.91); }
  70% { transform: scale(1.06); } 100% { transform: scale(1); }
}
@keyframes missedFadeOut {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(.88); }
}

/* The card the jump landed on, so the eye knows where it was put. */
.missed-highlight { position: relative; z-index: 2; animation: missedRing 1.8s ease forwards; }
@keyframes missedRing {
  0%   { box-shadow: 0 0 0 0 var(--s-accent); }
  30%  { box-shadow: 0 0 0 8px color-mix(in srgb, var(--s-accent) 35%, transparent); }
  60%  { box-shadow: 0 0 0 4px color-mix(in srgb, var(--s-accent) 20%, transparent); }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* ── 11c. The XP strip (parents) ───────────────────────────────────────── */
.xp-card {
  display: flex; align-items: center; gap: var(--s-sp-3);
  flex-wrap: wrap; padding: 10px var(--s-sp-3); margin-bottom: var(--s-sp-3);
  background: var(--s-surface); border: 1px solid var(--s-line);
  border-radius: var(--s-r); box-shadow: var(--s-shadow-sm);
}
.xp-level-pill, .xp-rank-pill {
  flex-shrink: 0; padding: 4px 10px; border-radius: var(--s-r-pill);
  font-size: var(--s-fs-xs); font-weight: 900; white-space: nowrap;
  background: var(--s-accent); color: var(--s-accent-on);
}
.xp-rank-pill { background: var(--s-warn-bg); color: var(--s-warn); }
.xp-track-wrap { flex: 1; min-width: 150px; }
.xp-track-labels {
  display: flex; justify-content: space-between; gap: var(--s-sp-2);
  margin-bottom: 4px; font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-ink-2);
}
.xp-track { height: 8px; border-radius: var(--s-r-pill); background: var(--s-line); overflow: hidden; }
.xp-fill {
  display: block; height: 100%; width: 0; border-radius: var(--s-r-pill);
  background: linear-gradient(90deg, var(--s-brand), var(--s-accent));
  transition: width .5s ease;
}
.xp-bar-pop { animation: xpPop .45s ease forwards; }
@keyframes xpPop {
  0% { transform: scale(1); } 40% { transform: scale(1.35); }
  70% { transform: scale(.92); } 100% { transform: scale(1); }
}
/* The coin a reaction throws at the XP bar. The script positions it, starts
   coinFloat, then hands it a transition and flies it to the bar — so this rule
   sets no animation of its own. */
.xp-coin {
  position: fixed; z-index: 99999; pointer-events: none;
  font-size: 1.4rem; line-height: 1;
  transform: translate(-50%, -50%); will-change: transform, opacity;
}
@keyframes coinFloat {
  0%   { transform: translate(-50%, -50%) scale(1);    opacity: 1; }
  60%  { transform: translate(-50%, -120%) scale(1.3); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(.6);   opacity: 0; }
}

/* ── 11d. Search ───────────────────────────────────────────────────────── */
.feed-search-form { margin-bottom: var(--s-sp-3); }
.feed-search { position: relative; }
.feed-search-icon {
  position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
  color: var(--s-muted); font-size: var(--s-fs-sm); pointer-events: none;
}
.feed-search-input {
  width: 100%; padding: 11px 38px; border: 1px solid var(--s-line);
  border-radius: var(--s-r-pill); background: var(--s-surface);
  font-size: var(--s-fs-md); font-weight: 600; color: var(--s-ink);
}
.feed-search-input:focus {
  outline: none; border-color: var(--s-brand);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--s-brand) 16%, transparent);
}
.feed-search-clear {
  position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  color: var(--s-muted); font-size: 1.25rem; line-height: 1; text-decoration: none;
}

/* ── 11e. Sort toggle ──────────────────────────────────────────────────── */
.feed-sort-toggle {
  display: inline-flex; gap: 4px; padding: 3px; margin-bottom: var(--s-sp-3);
  background: var(--s-surface-2); border: 1px solid var(--s-line);
  border-radius: var(--s-r-pill);
}
.feed-sort-opt {
  display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px;
  border-radius: var(--s-r-pill); font-size: var(--s-fs-sm); font-weight: 800;
  color: var(--s-ink-2); text-decoration: none; line-height: 1;
}
.feed-sort-opt.active { background: var(--s-brand); color: var(--s-brand-on); }
.feed-sort-opt i { font-size: var(--s-fs-xs); }

/* ── 11f. Composer trigger ─────────────────────────────────────────────── */
.composer-trigger {
  margin-bottom: var(--s-sp-3); padding: var(--s-sp-3);
  background: var(--s-surface); border: 1px solid var(--s-line);
  border-radius: var(--s-r); box-shadow: var(--s-shadow-sm); cursor: pointer;
}
.ct-row { display: flex; align-items: center; gap: 10px; }
.ct-avatar {
  width: 38px; height: 38px; flex-shrink: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--s-brand); color: var(--s-brand-on);
  font-size: var(--s-fs-sm); font-weight: 900;
}
.ct-prompt {
  flex: 1; min-width: 0; padding: 9px var(--s-sp-3);
  background: var(--s-surface-2); border-radius: var(--s-r-pill);
  font-size: var(--s-fs-md); font-weight: 600; color: var(--s-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ct-divider { height: 1px; margin: var(--s-sp-3) 0 var(--s-sp-2); background: var(--s-line-soft); }
.ct-quick { display: flex; gap: 4px; }
.ct-chip {
  flex: 1; display: inline-flex; align-items: center; justify-content: center;
  gap: 6px; padding: 8px 6px; border: 0; border-radius: var(--s-r-sm);
  background: transparent; font: inherit; font-size: var(--s-fs-sm);
  font-weight: 800; color: var(--s-ink-2); cursor: pointer;
}
.ct-chip:hover { background: var(--s-surface-2); }
.ct-chip .fa-image { color: var(--s-good); }
.ct-chip .fa-poll { color: var(--s-warn); }
.ct-chip .fa-paperclip { color: var(--s-violet); }
.ct-gif {
  font-size: var(--s-fs-xs); font-weight: 900; letter-spacing: -0.02em;
  padding: 1px 5px; border-radius: 5px; background: var(--s-info-bg); color: var(--s-info);
}

/* ── 11g. Composer ─────────────────────────────────────────────────────── */
.composer-card {
  background: var(--s-surface); border: 1px solid var(--s-line);
  border-radius: var(--s-r); box-shadow: var(--s-shadow-sm); overflow: hidden;
}
#composer { margin-bottom: var(--s-sp-4); }
.composer-mobile-close {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px var(--s-sp-3); border-bottom: 1px solid var(--s-line-soft);
  font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-ink);
}
.composer-mobile-close button {
  border: 0; background: none; font-size: 1.5rem; line-height: 1;
  color: var(--s-muted); cursor: pointer;
}
.composer-errors {
  padding: 10px var(--s-sp-4); background: var(--s-bad-bg);
  border-bottom: 1px solid var(--s-bad-line);
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-bad);
}
.composer-inner { padding: var(--s-sp-3) var(--s-sp-3) 0; }
.composer-body-area { display: block; }
.composer-body-area textarea {
  width: 100%; min-height: 74px; padding: 0; border: 0; resize: vertical;
  background: transparent; font-size: var(--s-fs-lg); font-weight: 600;
  color: var(--s-ink); font-family: inherit;
}
.composer-body-area textarea:focus { outline: none; }
.composer-body-area textarea::placeholder { color: var(--s-muted); font-weight: 600; }

.composer-inline-previews { display: grid; gap: var(--s-sp-2); }
.composer-gif-inline { display: none; position: relative; max-width: 240px; }
.composer-gif-inline img { width: 100%; border-radius: var(--s-r-sm); display: block; }
.cgi-close, .clp-close {
  position: absolute; top: 6px; right: 6px; width: 26px; height: 26px;
  border: 0; border-radius: 50%; background: rgba(15, 23, 42, .68);
  color: #fff; font-size: 1.05rem; line-height: 1; cursor: pointer;
}
.composer-link-preview {
  position: relative; display: none; gap: 10px; align-items: center;
  padding: var(--s-sp-2); border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm); background: var(--s-surface-2);
}
.composer-link-preview img { display: none; width: 56px; height: 56px; object-fit: cover; border-radius: 8px; flex-shrink: 0; }
.clp-text { min-width: 0; flex: 1; padding-right: 26px; }
.clp-domain { font-size: var(--s-fs-xs); font-weight: 800; text-transform: uppercase; letter-spacing: .04em; color: var(--s-muted); }
.clp-title { font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Selected photos, before posting. */
.cmp-photo-strip {
  display: flex; gap: 6px; padding: var(--s-sp-2) var(--s-sp-3) 0;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
}
.cmp-photo-cell {
  position: relative; width: 74px; height: 74px; flex-shrink: 0;
  border-radius: var(--s-r-sm); overflow: hidden; background: var(--s-surface-2);
}
.cmp-photo-cell img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cmp-photo-x {
  position: absolute; top: 3px; right: 3px; width: 22px; height: 22px;
  border: 0; border-radius: 50%; background: rgba(15, 23, 42, .7);
  color: #fff; font-size: .95rem; line-height: 1; cursor: pointer;
}
.cmp-photo-cell.is-cover { outline: 2px solid var(--s-accent); outline-offset: -2px; }
.cmp-cover-badge {
  position: absolute; left: 3px; bottom: 3px; padding: 1px 6px;
  border-radius: var(--s-r-pill); background: var(--s-accent);
  color: var(--s-accent-on); font-size: 9px; font-weight: 900;
  text-transform: uppercase; letter-spacing: .04em;
}
.composer-shared-label {
  padding: var(--s-sp-2) var(--s-sp-3) 0; font-size: var(--s-fs-xs);
  font-weight: 900; text-transform: uppercase; letter-spacing: .05em; color: var(--s-good);
}

.composer-actions {
  display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
  padding: var(--s-sp-2) var(--s-sp-3) var(--s-sp-3);
}
.composer-action-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; margin: 0; border: 0; border-radius: 50%;
  background: transparent; color: var(--s-ink-2); font-size: var(--s-fs-md);
  cursor: pointer;
}
.composer-action-btn:hover { background: var(--s-surface-2); }
.composer-action-btn.active { background: var(--s-good-bg); color: var(--s-good); }
.composer-action-btn .fa-image { color: var(--s-good); }
.composer-action-btn input[type="file"] { display: none; }
.composer-action-spacer { flex: 1; }
.composer-settings {
  display: flex; flex-wrap: wrap; gap: 10px; align-items: center;
  padding: 0 var(--s-sp-3) var(--s-sp-3);
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink-2);
}
.composer-settings select { max-width: 100%; }
.composer-teams { display: none; }
.composer-check { display: flex; align-items: center; gap: 5px; }
.composer-poll-indicator {
  display: none; align-items: center; gap: var(--s-sp-2);
  margin: 0 var(--s-sp-3) var(--s-sp-3); padding: 9px var(--s-sp-3);
  background: var(--s-warn-bg); border: 1px solid var(--s-warn-line);
  border-radius: var(--s-r-sm); font-size: var(--s-fs-sm);
  font-weight: 800; color: var(--s-warn);
}
.composer-poll-indicator > span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cpi-btn {
  flex-shrink: 0; border: 0; border-radius: 8px; padding: 4px 10px;
  background: rgba(255, 255, 255, .6); color: var(--s-warn);
  font-size: var(--s-fs-xs); font-weight: 900; cursor: pointer;
}
.cpi-remove { background: none; font-size: 1.2rem; line-height: 1; padding: 0 4px; }

/* On a phone the composer is a bottom sheet, not an inline panel: it has a
   textarea, a toolbar and a Post button, and inline they end up under the
   keyboard. Its script pins the bottom edge to the visual viewport. */
@media (max-width: 640px) {
  .s-v2 #composer.show {
    position: fixed; left: 0; right: 0; z-index: 50;
    bottom: var(--s-nav-h);
    display: flex; flex-direction: column; justify-content: flex-end;
    max-height: var(--composer-maxh, calc(100dvh - 150px));
    margin: 0; background: rgba(15, 23, 42, .35);
  }
  .s-v2 #composer.show .composer-card {
    margin: 0; border-radius: var(--s-r-lg) var(--s-r-lg) 0 0;
    max-height: inherit; overflow-y: auto;
    animation: feedSheetIn .22s ease;
  }
}
@keyframes feedSheetIn { from { transform: translateY(14px); opacity: .6; } }

/* ── 11h. The new-post FAB ─────────────────────────────────────────────── */
/* It sits above the bottom nav, and gets out of the way whenever something
   else owns the bottom of the screen — a focused comment box, an open
   composer — because otherwise it covers that surface's own Send button. */
.feed-fab {
  display: none; align-items: center; justify-content: center;
  position: fixed; right: 16px; z-index: 40;
  bottom: calc(var(--s-nav-h) + 16px);
  width: 52px; height: 52px; border: 0; border-radius: 50%;
  background: var(--s-accent); color: var(--s-accent-on);
  font-size: 1.15rem; box-shadow: var(--s-shadow); cursor: pointer;
}
/* Phone only: on a desktop the composer is already sitting at the top of the
   column, so a floating button to reach it would be a second door to one room. */
@media (max-width: 767.98px) { .feed-fab { display: flex; } }
body.feed-commenting .feed-fab,
body.feed-composer-open .feed-fab { display: none; }
body.feed-composer-open { overflow: hidden; }

/* ── 11i. The post card ────────────────────────────────────────────────── */
.feed-card {
  margin-bottom: var(--s-sp-3); background: var(--s-surface);
  border: 1px solid var(--s-line); border-radius: var(--s-r);
  box-shadow: var(--s-shadow-sm); overflow: hidden;
}
.feed-card-pinned { border-color: var(--s-accent); }
.feed-card-deleted { opacity: .62; }
.feed-card__body { padding: var(--s-sp-3); }

.feed-post-header { display: flex; align-items: flex-start; gap: 10px; }
.feed-author { display: flex; align-items: center; gap: 10px; min-width: 0; }
.feed-author-avatar {
  width: 40px; height: 40px; flex-shrink: 0; border-radius: 50%;
  overflow: hidden; display: flex; align-items: center; justify-content: center;
  background: var(--s-brand); color: var(--s-brand-on);
  font-size: var(--s-fs-sm); font-weight: 900;
}
.feed-author-avatar img { width: 100%; height: 100%; object-fit: cover; }
.feed-author-info { min-width: 0; }
.feed-author-name { font-size: var(--s-fs-md); font-weight: 900; color: var(--s-ink); }
.feed-role-pill {
  display: inline-block; margin-left: 6px; padding: 1px 8px;
  border-radius: var(--s-r-pill); background: var(--s-good-bg);
  color: var(--s-good); font-size: var(--s-fs-xs); font-weight: 800;
  vertical-align: middle;
}
.feed-meta {
  display: flex; align-items: center; gap: 5px; flex-wrap: wrap;
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted);
}
.badge-visibility, .badge-teams {
  padding: 1px 7px; border-radius: var(--s-r-pill);
  font-size: var(--s-fs-xs); font-weight: 800;
  background: var(--s-info-bg); color: var(--s-info);
}
.badge-teams { background: var(--s-violet-bg); color: var(--s-violet); }
.feed-header-title { font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink); }
.feed-flags { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: var(--s-sp-2); }

.feed-post-actions { position: relative; margin-left: auto; flex-shrink: 0; }
.feed-overflow-btn {
  width: 32px; height: 32px; border: 0; border-radius: 50%;
  background: transparent; color: var(--s-muted); font-size: 1.15rem;
  line-height: 1; cursor: pointer;
}
.feed-overflow-btn:hover { background: var(--s-surface-2); }
.feed-overflow-menu {
  display: none; position: absolute; right: 0; top: 34px; z-index: 20;
  min-width: 178px; padding: 5px; background: var(--s-surface);
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  box-shadow: var(--s-shadow);
}
.feed-overflow-menu.show { display: block; }
.feed-overflow-item {
  display: block; width: 100%; padding: 8px 10px; border: 0; border-radius: 7px;
  background: none; text-align: left; text-decoration: none;
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink); cursor: pointer;
}
.feed-overflow-item:hover { background: var(--s-surface-2); }
.feed-overflow-item.is-bad { color: var(--s-bad); }

.feed-body {
  margin-top: var(--s-sp-2); font-size: var(--s-fs-md);
  font-weight: 500; line-height: 1.5; color: var(--s-ink);
  overflow-wrap: anywhere;
}
.feed-body p:last-child { margin-bottom: 0; }
.feed-body.truncated { display: -webkit-box; -webkit-line-clamp: 7; -webkit-box-orient: vertical; overflow: hidden; }
.read-more-btn {
  margin-top: 4px; border: 0; background: none; padding: 0;
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-info); cursor: pointer;
}
.feed-post-flag {
  display: inline-flex; align-items: center; gap: 6px; margin-bottom: var(--s-sp-2);
  padding: 4px var(--s-sp-3); border-radius: var(--s-r-pill);
  font-size: var(--s-fs-xs); font-weight: 900;
  text-transform: uppercase; letter-spacing: .04em;
  background: var(--s-slate-bg); color: var(--s-slate);
}
.feed-post-flag--report { background: var(--s-brand); color: var(--s-brand-on); }

/* ── 11j. Source card (fixture / training / tournament) ────────────────── */
.source-card {
  margin: var(--s-sp-2) 0; padding: var(--s-sp-3);
  background: var(--s-surface-2); border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm);
}
.source-card__top { display: flex; justify-content: space-between; align-items: center; gap: var(--s-sp-2); flex-wrap: wrap; }
.source-pill {
  padding: 3px 10px; border-radius: var(--s-r-pill);
  background: var(--s-brand); color: var(--s-brand-on);
  font-size: var(--s-fs-xs); font-weight: 900;
}
.status-chip {
  display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px;
  border-radius: var(--s-r-pill); background: var(--s-surface);
  border: 1px solid var(--s-line); font-size: var(--s-fs-xs);
  font-weight: 800; color: var(--s-ink-2);
}
.status-chip .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--s-brand); display: inline-block; }
.source-title { margin-top: var(--s-sp-2); font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink); }
.source-subtitle { font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink-2); }
.meta-grid { display: grid; gap: 4px; margin-top: var(--s-sp-2); }
.meta-item { font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }
.meta-item span { font-weight: 900; color: var(--s-ink); margin: 0 4px 0 2px; }
.meta-item .fa-map-marker-alt { color: var(--s-bad); }
.meta-item .fa-th-large { color: var(--s-info); }
.meta-item .fa-users { color: var(--s-good); }
.meta-item .fa-trophy { color: var(--s-warn); }
.cta-link {
  display: inline-flex; align-items: center; gap: 6px; margin-top: var(--s-sp-2);
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-info); text-decoration: none;
}
@media (min-width: 520px) { .meta-grid { grid-template-columns: 1fr 1fr; } }

/* ── 11k. Birthday & season-wrapped cards ──────────────────────────────── */
.bday-card {
  position: relative; overflow: hidden; margin-bottom: var(--s-sp-2);
  padding: var(--s-sp-5) var(--s-sp-3); border-radius: var(--s-r);
  background: linear-gradient(135deg, var(--s-pink-bg), var(--s-warn-bg));
  text-align: center;
}
.bday-cake { font-size: 2.6rem; }
.bday-hb { font-size: var(--s-fs-xl); font-weight: 900; color: var(--s-ink); letter-spacing: -0.02em; }
.bday-confetti { position: absolute; inset: 0; pointer-events: none; }
.bday-confetti span {
  position: absolute; top: -14px; font-size: 1rem;
  animation: feedFall 4.5s linear infinite;
}
.bday-confetti span:nth-child(1) { left: 6%;  animation-delay: 0s; }
.bday-confetti span:nth-child(2) { left: 16%; animation-delay: .4s; }
.bday-confetti span:nth-child(3) { left: 26%; animation-delay: .8s; }
.bday-confetti span:nth-child(4) { left: 36%; animation-delay: 1.2s; }
.bday-confetti span:nth-child(5) { left: 46%; animation-delay: 1.6s; }
.bday-confetti span:nth-child(6) { left: 56%; animation-delay: 2s; }
.bday-confetti span:nth-child(7) { left: 66%; animation-delay: 2.4s; }
.bday-confetti span:nth-child(8) { left: 76%; animation-delay: 2.8s; }
.bday-confetti span:nth-child(9) { left: 86%; animation-delay: 3.2s; }
.bday-confetti span:nth-child(10){ left: 94%; animation-delay: 3.6s; }
@keyframes feedFall { to { transform: translateY(220px) rotate(220deg); opacity: 0; } }

.feed-wrapped {
  position: relative; overflow: hidden; border-radius: var(--s-r-lg);
  padding: var(--s-sp-5); color: #fff;
  background: linear-gradient(135deg, #0f0f1a 0%, #1e1b4b 50%, #0f172a 100%);
}
.feed-wrapped__glow {
  position: absolute; top: -30%; right: -10%; width: 200px; height: 200px;
  pointer-events: none;
  background: radial-gradient(circle, rgba(251, 191, 36, .3), transparent 70%);
}
.feed-wrapped__head { display: flex; align-items: center; gap: var(--s-sp-2); }
.feed-wrapped__emoji { font-size: 2rem; }
.feed-wrapped__t { font-size: var(--s-fs-xl); font-weight: 900; letter-spacing: -0.02em; }
.feed-wrapped__sub { font-size: var(--s-fs-sm); color: #94a3b8; }
.feed-wrapped__stats { display: flex; gap: var(--s-sp-4); margin: var(--s-sp-3) 0; }
.feed-wrapped__stat { text-align: center; }
.feed-wrapped__n { font-size: var(--s-fs-xl); font-weight: 900; }
.feed-wrapped__n--win { color: #22c55e; }
.feed-wrapped__n--draw { color: #fbbf24; }
.feed-wrapped__n--loss { color: #ef4444; }
.feed-wrapped__k { font-size: var(--s-fs-xs); font-weight: 800; color: #94a3b8; }
.feed-wrapped__awards { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: var(--s-sp-3); }
.feed-wrapped__award {
  padding: 4px 10px; border-radius: var(--s-r-pill);
  background: rgba(255, 255, 255, .1); font-size: var(--s-fs-sm); font-weight: 800;
}
.feed-wrapped__cta {
  display: inline-block; padding: 8px 16px; border-radius: var(--s-r-pill);
  background: var(--s-accent); color: var(--s-accent-on);
  font-size: var(--s-fs-sm); font-weight: 900; text-decoration: none;
}

/* ── 11l. Spotlight ────────────────────────────────────────────────────── */
.spotlight-card {
  position: relative; overflow: hidden; margin-bottom: var(--s-sp-3);
  border-radius: var(--s-r-lg); color: #fff;
  background: linear-gradient(135deg, var(--s-brand), color-mix(in srgb, var(--s-brand) 40%, #7c3aed));
}
.spotlight-card.is-active { box-shadow: var(--s-shadow-lg); }
.spotlight-inner { position: relative; z-index: 2; padding: var(--s-sp-5); }
.spotlight-hero { display: grid; gap: var(--s-sp-4); align-items: center; }
.spotlight-copy { min-width: 0; }
.spotlight-badge {
  display: inline-flex; align-items: center; gap: 6px; padding: 4px 11px;
  border-radius: var(--s-r-pill); background: var(--s-accent);
  color: var(--s-accent-on); font-size: var(--s-fs-xs); font-weight: 900;
}
.spotlight-pill {
  padding: 3px 10px; border-radius: var(--s-r-pill);
  background: rgba(255, 255, 255, .16); font-size: var(--s-fs-xs); font-weight: 800;
}
.spotlight-chips { display: flex; align-items: center; gap: var(--s-sp-2); flex-wrap: wrap; margin-bottom: var(--s-sp-3); }
.spotlight-name { font-size: var(--s-fs-2xl); font-weight: 900; letter-spacing: -0.02em; }
.spotlight-team { font-size: var(--s-fs-sm); font-weight: 700; opacity: .8; }
.spotlight-desc { margin: var(--s-sp-2) 0 0; font-size: var(--s-fs-md); line-height: 1.5; opacity: .92; }
.spotlight-media { min-width: 0; }
.spotlight-video { width: 100%; border-radius: var(--s-r); display: block; }
.spotlight-confetti, .spotlight-sparkles { position: absolute; inset: 0; pointer-events: none; z-index: 1; }
.spotlight-confetti span {
  position: absolute; top: -12px; width: 7px; height: 11px; border-radius: 2px;
  background: var(--s-accent); animation: feedFall 5s linear infinite;
}
.spotlight-confetti span:nth-child(1) { left: 5%;  animation-delay: 0s; }
.spotlight-confetti span:nth-child(2) { left: 14%; animation-delay: .5s; }
.spotlight-confetti span:nth-child(3) { left: 23%; animation-delay: 1s; }
.spotlight-confetti span:nth-child(4) { left: 32%; animation-delay: 1.5s; }
.spotlight-confetti span:nth-child(5) { left: 41%; animation-delay: 2s; }
.spotlight-confetti span:nth-child(6) { left: 50%; animation-delay: 2.5s; }
.spotlight-confetti span:nth-child(7) { left: 59%; animation-delay: 3s; }
.spotlight-confetti span:nth-child(8) { left: 68%; animation-delay: 3.5s; }
.spotlight-confetti span:nth-child(9) { left: 77%; animation-delay: 4s; }
.spotlight-confetti span:nth-child(10){ left: 86%; animation-delay: 4.5s; }
.spotlight-confetti span:nth-child(11){ left: 93%; animation-delay: .8s; }
.spotlight-confetti span:nth-child(12){ left: 68%; animation-delay: 1.8s; }
.spotlight-sparkles span {
  position: absolute; width: 5px; height: 5px; border-radius: 50%;
  background: #fff; opacity: .75; animation: feedTwinkle 2.4s ease-in-out infinite;
}
.spotlight-sparkles span:nth-child(1) { top: 15%; left: 10%; animation-delay: 0s; }
.spotlight-sparkles span:nth-child(2) { top: 35%; left: 25%; animation-delay: .4s; }
.spotlight-sparkles span:nth-child(3) { top: 20%; left: 60%; animation-delay: .8s; }
.spotlight-sparkles span:nth-child(4) { top: 45%; left: 80%; animation-delay: 1.2s; }
.spotlight-sparkles span:nth-child(5) { top: 70%; left: 40%; animation-delay: 1.6s; }
.spotlight-sparkles span:nth-child(6) { top: 65%; left: 70%; animation-delay: 2s; }
@keyframes feedTwinkle { 50% { opacity: .1; transform: scale(.4); } }
@media (min-width: 720px) { .spotlight-hero { grid-template-columns: 1fr 220px; } }

/* ── 11m. Media ────────────────────────────────────────────────────────── */
/* The photos bleed to the card edge — a padded photo reads as a thumbnail. */
.feed-media {
  margin: var(--s-sp-3) calc(var(--s-sp-3) * -1);
  background: var(--s-surface-2); overflow: hidden;
}
.feed-media img { width: 100%; display: block; }
.feed-media-gif { max-width: 320px; margin-left: 0; margin-right: 0; border-radius: var(--s-r-sm); }
.feed-gif { border-radius: var(--s-r-sm); }

.feed-gallery { display: grid; gap: 3px; }
.fg-n2 { grid-template-columns: 1fr 1fr; }
.fg-n3 { grid-template-columns: 1fr 1fr; }
.fg-n3 .fg-tile:first-child { grid-row: span 2; }
.fg-n4 { grid-template-columns: 1fr 1fr; }
.fg-tile {
  position: relative; border: 0; padding: 0; cursor: pointer;
  aspect-ratio: 1; overflow: hidden; background: var(--s-surface-2);
}
.fg-tile img { width: 100%; height: 100%; object-fit: cover; display: block; }
.fg-n3 .fg-tile:first-child { aspect-ratio: auto; }
.fg-more::after {
  content: attr(data-more); position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(15, 23, 42, .55); color: #fff;
  font-size: var(--s-fs-xl); font-weight: 900;
}
.feed-album { display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px; }
.feed-album a { display: block; aspect-ratio: 1; overflow: hidden; }
.feed-album img { width: 100%; height: 100%; object-fit: cover; display: block; }
.feed-album-link {
  display: inline-block; margin-top: 6px;
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-info); text-decoration: none;
}

/* ── 11n. Attachments ──────────────────────────────────────────────────── */
.pdf-preview-card {
  margin-bottom: var(--s-sp-3); border: 1px solid var(--s-violet-line);
  border-radius: var(--s-r); overflow: hidden; background: var(--s-surface-2);
}
.pdf-preview-card a { display: block; text-decoration: none; color: inherit; }
.pdf-preview-card__shot { background: var(--s-line-soft); text-align: center; }
.pdf-preview-card__shot img { max-width: 100%; max-height: 320px; display: block; margin: 0 auto; }
.pdf-preview-card__ph { background: var(--s-line-soft); padding: var(--s-sp-6); text-align: center; }
.pdf-preview-card__ph i { font-size: 2.6rem; color: var(--s-bad); }
.pdf-preview-card__ph div { margin-top: var(--s-sp-2); font-size: var(--s-fs-sm); color: var(--s-muted); }
.pdf-preview-card__foot {
  display: flex; align-items: center; gap: var(--s-sp-2);
  padding: var(--s-sp-2) var(--s-sp-3); border-top: 1px solid var(--s-line-soft);
}
.pdf-preview-card__nm { flex: 1; min-width: 0; }
.pdf-preview-card__nm b { display: block; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pdf-preview-card__nm span { font-size: var(--s-fs-xs); color: var(--s-muted); }
.attachment-chip {
  display: flex; align-items: center; gap: var(--s-sp-3); flex-wrap: wrap;
  margin-bottom: var(--s-sp-3); padding: 10px var(--s-sp-3);
  background: var(--s-surface-2); border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm);
}
.attachment-icon { font-size: 1.3rem; }
.attachment-chip__nm { flex: 1; min-width: 0; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink); }
.attachment-chip__nm span { display: block; font-weight: 600; color: var(--s-muted); font-size: var(--s-fs-xs); }

/* ── 11o. Link preview ─────────────────────────────────────────────────── */
.link-preview {
  display: block; margin: var(--s-sp-2) 0; border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm); overflow: hidden; text-decoration: none;
  background: var(--s-surface-2);
}
.link-preview-img { width: 100%; display: block; max-height: 260px; object-fit: cover; }
.link-preview-thumb {
  width: 56px; height: 56px; flex-shrink: 0; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  background: var(--s-surface);
}
.link-preview-thumb img { width: 28px; height: 28px; }
.link-preview-body { padding: 10px var(--s-sp-3); min-width: 0; }
.link-preview-domain {
  display: flex; align-items: center; gap: 5px; font-size: var(--s-fs-xs);
  font-weight: 800; text-transform: uppercase; letter-spacing: .04em; color: var(--s-muted);
}
.link-preview-favicon { width: 14px; height: 14px; }
.link-preview-title { font-size: var(--s-fs-md); font-weight: 800; color: var(--s-ink); }
.link-preview-desc {
  font-size: var(--s-fs-sm); color: var(--s-ink-2); display: -webkit-box;
  -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.lp-compact { display: flex; align-items: center; gap: 10px; padding: var(--s-sp-2); }
.lp-compact .link-preview-body { padding: 0; }
.lp-nothumb .link-preview-thumb { display: none; }

/* ── 11p. Poll on a post ───────────────────────────────────────────────── */
.poll-block {
  margin-top: var(--s-sp-3); padding: var(--s-sp-3);
  background: var(--s-surface-2); border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm);
}
.poll-block__q { display: flex; align-items: center; gap: var(--s-sp-2); flex-wrap: wrap; margin-bottom: var(--s-sp-2); font-size: var(--s-fs-md); font-weight: 900; color: var(--s-ink); }
.poll-block__votes { font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }
.poll-option {
  display: flex; align-items: center; gap: var(--s-sp-2); flex-wrap: wrap;
  padding: var(--s-sp-2) 0; border-top: 1px solid var(--s-line-soft);
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink);
}
.poll-option > div:first-child { flex: 1 1 100%; }
.poll-option .progress {
  flex: 1; min-width: 90px; height: 8px; border-radius: var(--s-r-pill);
  background: var(--s-line); overflow: hidden;
}
.poll-option .progress-bar {
  display: block; height: 100%; width: 0; border-radius: var(--s-r-pill);
  background: var(--s-brand); transition: width .4s ease;
}
.poll-voters { width: 100%; margin-top: 2px; font-size: var(--s-fs-xs); color: var(--s-muted); }
.poll-voters .fa-user-check { color: var(--s-good); }

/* ── 11q. Reactions & the action bar ───────────────────────────────────── */
.feed-engagement { margin-top: var(--s-sp-2); }
.reactors-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.reactors-chip {
  position: relative; display: inline-flex; align-items: center; gap: 4px;
  padding: 2px 9px; border-radius: var(--s-r-pill);
  background: var(--s-surface-2); border: 1px solid var(--s-line);
  font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-ink-2); cursor: default;
}
.reactors-tooltip {
  display: none; position: absolute; left: 0; bottom: calc(100% + 6px);
  z-index: 25; min-width: 150px; max-width: 240px; padding: 7px 10px;
  border-radius: var(--s-r-sm); background: var(--s-ink); color: #fff;
  font-size: var(--s-fs-xs); font-weight: 600; line-height: 1.35;
}
.reactors-chip:hover .reactors-tooltip,
.reactors-chip:focus .reactors-tooltip { display: block; }
.liker-summary { font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }
.reactors-comments {
  margin-left: auto; font-size: var(--s-fs-xs); font-weight: 800;
  color: var(--s-ink-2); cursor: pointer;
}

.feed-actions {
  display: flex; align-items: center; gap: 2px; margin-top: var(--s-sp-2);
  padding-top: var(--s-sp-2); border-top: 1px solid var(--s-line-soft);
}
.feed-action-btn {
  flex: 1; display: inline-flex; align-items: center; justify-content: center;
  gap: 6px; min-height: 40px; padding: 8px 4px; border: 0; border-radius: var(--s-r-sm);
  background: transparent; font-size: var(--s-fs-sm); font-weight: 800;
  color: var(--s-ink-2); cursor: pointer;
}
.feed-action-btn:hover { background: var(--s-surface-2); }
.feed-action-btn.liked { color: var(--s-info); }
.feed-action-btn.is-open { color: var(--s-brand); }
.feed-action-btn.celebrated { color: var(--s-bad); }

/* ── 11r. Comments ─────────────────────────────────────────────────────── */
/* Comments start collapsed so a feed of posts stays a feed. The count in the
   engagement row and the Comment button both open them. */
.comment-block { margin-top: var(--s-sp-3); }
.comment-block.cb-collapsed .comment-item,
.comment-block.cb-collapsed .no-comments-msg { display: none; }
.no-comments-msg { font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-muted); }

.comment-item { margin-bottom: var(--s-sp-2); }
.comment-item__row { display: flex; gap: var(--s-sp-2); align-items: flex-start; }
.comment-col { position: relative; flex: 1; min-width: 0; padding-bottom: 6px; }
.comment-bubble-row { display: flex; align-items: flex-start; gap: 4px; }
.comment-bubble {
  min-width: 0; padding: 8px 12px; border-radius: var(--s-r);
  background: var(--s-surface-2); font-size: var(--s-fs-sm); color: var(--s-ink);
  overflow-wrap: anywhere;
}
.comment-author { font-weight: 900; color: var(--s-ink); }
/* pre-line: a comment typed across three lines arrives as three lines.
   The class is filled two ways -- the server renders comment.body_linked
   straight into it, and the post-a-comment script builds the same div from
   JSON -- so the rule goes here rather than a |linebreaksbr that only the
   server path would get. */
.comment-body-text { font-weight: 500; line-height: 1.45; white-space: pre-line; }
.comment-image-link { display: block; margin-top: 6px; }
.comment-image, .comment-gif { max-width: 200px; border-radius: var(--s-r-sm); display: block; margin-top: 6px; }

.comment-menu { position: relative; flex-shrink: 0; }
.comment-menu-btn {
  width: 28px; height: 28px; border: 0; border-radius: 50%;
  background: transparent; color: var(--s-muted); font-size: 1.05rem;
  line-height: 1; cursor: pointer;
}
.comment-menu-list {
  display: none; position: absolute; right: 0; top: 30px; z-index: 20;
  min-width: 190px; padding: 5px; background: var(--s-surface);
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  box-shadow: var(--s-shadow);
}
.comment-menu-list.show { display: block; }
.comment-menu-item {
  display: block; width: 100%; padding: 8px 10px; border: 0; border-radius: 7px;
  background: none; text-align: left; font-size: var(--s-fs-sm);
  font-weight: 700; color: var(--s-ink); cursor: pointer;
}
.comment-menu-item:hover { background: var(--s-surface-2); }
.comment-menu-item.is-bad { color: var(--s-bad); }

.comment-reaction-chips {
  display: inline-flex; align-items: center; gap: 2px; margin-top: 3px;
  padding: 1px 7px; border-radius: var(--s-r-pill);
  background: var(--s-surface); border: 1px solid var(--s-line);
  font-size: var(--s-fs-xs);
}
.cr-count { font-weight: 800; color: var(--s-ink-2); margin-left: 2px; }

.comment-actions-row {
  display: flex; align-items: center; gap: var(--s-sp-3);
  margin-top: 3px; padding-left: 4px;
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted);
}
.comment-time { font-size: var(--s-fs-xs); color: var(--s-muted); }
.cmt-like-wrap { position: relative; display: inline-block; }
.cmt-like-btn, .cmt-reply-btn {
  border: 0; background: none; padding: 0;
  font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-muted); cursor: pointer;
}
.cmt-liked { color: var(--s-info); }
.cmt-react-picker {
  display: none; position: absolute; left: 0; bottom: calc(100% + 6px);
  z-index: 25; gap: 2px; padding: 4px 6px; border-radius: var(--s-r-pill);
  background: var(--s-surface); border: 1px solid var(--s-line);
  box-shadow: var(--s-shadow);
}
.cmt-like-wrap:hover .cmt-react-picker,
.cmt-react-picker.show { display: flex; }
.cmt-react-picker button {
  border: 0; background: none; font-size: 1.15rem; line-height: 1;
  padding: 2px; cursor: pointer;
}
.cmt-react-picker button:hover { transform: scale(1.25); }
.comment-replies { margin-left: var(--s-sp-5); padding-left: var(--s-sp-2); border-left: 2px solid var(--s-line-soft); }
.comment-edit { margin-top: var(--s-sp-2); }
.cmt-edit-ta { width: 100%; }
.cmt-edit-actions { display: flex; gap: var(--s-sp-2); margin-top: var(--s-sp-2); }

/* ── 11s. Writing a comment ────────────────────────────────────────────── */
/* Compact until tapped: a feed of ten posts should not be ten open forms. */
.ajax-comment-form { margin-top: var(--s-sp-2); }
.comment-box {
  width: 100%; min-height: 40px; padding: 9px var(--s-sp-3);
  border: 1px solid var(--s-line); border-radius: var(--s-r-lg);
  background: var(--s-surface-2); font-family: inherit;
  font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink); resize: none;
}
.comment-box:focus {
  outline: none; border-color: var(--s-brand); background: var(--s-surface);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--s-brand) 14%, transparent);
}
.comment-submit-row { display: none; align-items: center; gap: var(--s-sp-2); margin-top: 6px; }
.ajax-comment-form.is-active .comment-submit-row { display: flex; }
.comment-photo-btn, .giphy-btn, .emoji-picker-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border: 0; border-radius: 50%;
  background: var(--s-surface-2); color: var(--s-ink-2);
  font-size: var(--s-fs-sm); font-weight: 800; cursor: pointer;
}
.comment-photo-btn.has-photo { background: var(--s-good-bg); color: var(--s-good); }
.comment-image-input { display: none; }
.comment-send { margin-left: auto; flex-shrink: 0; }
.comment-image-preview, .gif-preview { position: relative; display: none; }
.comment-image-preview img, .gif-preview img { max-height: 54px; border-radius: 8px; display: block; }
.comment-image-remove, .gif-preview-remove {
  position: absolute; top: -6px; right: -6px; width: 20px; height: 20px;
  border: 0; border-radius: 50%; background: var(--s-ink); color: #fff;
  font-size: .8rem; line-height: 1; cursor: pointer;
}
.reply-indicator { display: none; margin-bottom: 4px; font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-ink-2); }
.reply-to-name { color: var(--s-info); }
.cancel-reply {
  margin-left: 6px; border: 0; background: none;
  font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-bad); cursor: pointer;
}

/* ── 11t. Emoji picker & @mentions ─────────────────────────────────────── */
.emoji-picker-wrap { position: relative; display: inline-block; }
.emoji-picker-panel {
  display: none; position: absolute; right: 0; bottom: calc(100% + 6px);
  z-index: 30; width: 232px; max-height: 190px; overflow-y: auto;
  padding: 6px; border-radius: var(--s-r-sm); background: var(--s-surface);
  border: 1px solid var(--s-line); box-shadow: var(--s-shadow);
}
.emoji-picker-panel.show { display: flex; flex-wrap: wrap; gap: 2px; }
.emoji-picker-panel button {
  width: 32px; height: 32px; border: 0; background: none;
  font-size: 1.15rem; line-height: 1; cursor: pointer; border-radius: 6px;
}
.emoji-picker-panel button:hover { background: var(--s-surface-2); }

.mention-container { position: relative; }
.mention-suggestions {
  position: absolute; z-index: 40; left: 0; right: 0; max-height: 200px;
  overflow-y: auto; padding: 4px; border-radius: var(--s-r-sm);
  background: var(--s-surface); border: 1px solid var(--s-line);
  box-shadow: var(--s-shadow);
}
.mention-suggestions button {
  display: flex; align-items: center; gap: var(--s-sp-2); width: 100%;
  padding: 7px 9px; border: 0; border-radius: 7px; background: none;
  text-align: left; font-size: var(--s-fs-sm); font-weight: 700;
  color: var(--s-ink); cursor: pointer;
}
.mention-suggestions button.active { background: var(--s-info-bg); }
/* The avatar is a photo when there is one and the initial when there isn't. */
.mention-avatar {
  width: 34px; height: 34px; flex-shrink: 0; border-radius: 50%;
  object-fit: cover; display: inline-flex; align-items: center;
  justify-content: center; background: var(--s-brand); color: var(--s-brand-on);
  font-weight: 900;
}
img.mention-avatar { background: none; }
.mention-user { margin-left: auto; font-weight: 700; color: var(--s-muted); }

/* ── 11u. Share menu ───────────────────────────────────────────────────── */
.feed-share-menu {
  position: absolute; z-index: 60; min-width: 190px; padding: 6px;
  background: var(--s-surface); border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm); box-shadow: var(--s-shadow);
}
.feed-share-menu__item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 9px var(--s-sp-3); border: 0; border-radius: 8px;
  background: none; text-align: left; text-decoration: none;
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink); cursor: pointer;
}
.feed-share-menu__item:hover { background: var(--s-surface-2); }
.feed-share-menu__item i { width: 18px; }
.feed-share-menu__item .fa-whatsapp { color: #25d366; }
.feed-share-menu__item .fa-facebook { color: #1877f2; }
.feed-share-menu__item .fa-link { color: var(--s-muted); }
.feed-share-menu__item .fa-check { color: var(--s-good); }

/* ── 11v. GIPHY picker ─────────────────────────────────────────────────── */
.giphy-modal-overlay {
  display: none; position: fixed; inset: 0; z-index: 11000;
  background: rgba(15, 23, 42, .55); align-items: center; justify-content: center;
}
.giphy-modal-overlay.active { display: flex; }
.giphy-modal {
  display: flex; flex-direction: column; width: 100%; max-width: 520px;
  max-height: 84dvh; background: var(--s-surface);
  border-radius: var(--s-r-lg); overflow: hidden;
}
.giphy-modal-header { display: flex; gap: var(--s-sp-2); padding: var(--s-sp-3); border-bottom: 1px solid var(--s-line); }
.giphy-modal-header input {
  flex: 1; min-width: 0; padding: 10px var(--s-sp-3);
  border: 1px solid var(--s-line); border-radius: var(--s-r-pill);
  background: var(--s-surface-2); font-size: var(--s-fs-md); color: var(--s-ink);
}
.giphy-modal-close {
  width: 36px; height: 36px; flex-shrink: 0; border: 0; border-radius: 50%;
  background: var(--s-surface-2); color: var(--s-ink-2);
  font-size: 1.4rem; line-height: 1; cursor: pointer;
}
.giphy-grid {
  flex: 1; overflow-y: auto; padding: var(--s-sp-2);
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; align-content: start;
}
.giphy-grid img { width: 100%; border-radius: var(--s-r-sm); cursor: pointer; display: block; }
.giphy-empty {
  grid-column: 1 / -1; padding: var(--s-sp-6) var(--s-sp-4); text-align: center;
  font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-muted);
}
.giphy-modal-footer {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-sp-2); padding: 10px var(--s-sp-3); border-top: 1px solid var(--s-line);
  font-size: var(--s-fs-xs); color: var(--s-muted);
}
.giphy-attribution { font-weight: 900; letter-spacing: .03em; text-transform: uppercase; }
@media (min-width: 480px) { .giphy-grid { grid-template-columns: repeat(3, 1fr); } }

/* ── 11w. Poll builder ─────────────────────────────────────────────────── */
body.poll-modal-open { overflow: hidden; }
.poll-modal-overlay {
  display: none; position: fixed; inset: 0; z-index: 11500;
  background: rgba(15, 23, 42, .55);
}
.poll-modal-overlay.active { display: flex; align-items: stretch; justify-content: center; }
.poll-modal {
  display: flex; flex-direction: column; width: 100%; max-width: 560px;
  height: 100dvh; background: var(--s-surface);
}
.poll-modal-head {
  display: flex; align-items: center; gap: 10px; flex-shrink: 0;
  padding: var(--s-sp-4); border-bottom: 1px solid var(--s-line);
}
.poll-modal-head h3 { flex: 1; margin: 0; font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink); }
.poll-modal-head .fa-poll { color: var(--s-warn); font-size: 1.25rem; }
.pm-close {
  width: 36px; height: 36px; border: 0; border-radius: 50%;
  background: var(--s-surface-2); color: var(--s-ink-2);
  font-size: 1.4rem; line-height: 1; cursor: pointer;
}
.poll-modal-body { flex: 1; overflow-y: auto; padding: var(--s-sp-4); }
.pm-label {
  margin: 0 0 7px; font-size: var(--s-fs-xs); font-weight: 900;
  text-transform: uppercase; letter-spacing: .05em; color: var(--s-muted);
}
.pm-label--spaced { margin-top: var(--s-sp-6); }
.pm-q {
  width: 100%; margin-bottom: var(--s-sp-5); padding: 13px var(--s-sp-4);
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  background: var(--s-surface); font-size: var(--s-fs-md);
  font-weight: 600; color: var(--s-ink);
}
.pm-q:focus { outline: none; border-color: var(--s-brand); box-shadow: 0 0 0 3px color-mix(in srgb, var(--s-brand) 14%, transparent); }
.pm-when { margin-bottom: 4px; }
.pm-hint { font-size: var(--s-fs-xs); color: var(--s-muted); }
.pm-opt-row { display: flex; align-items: center; gap: var(--s-sp-2); margin-bottom: 11px; }
.pm-num {
  width: 27px; height: 27px; flex-shrink: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--s-warn-bg); color: var(--s-warn);
  font-size: var(--s-fs-xs); font-weight: 900;
}
.pm-opt-row input {
  flex: 1; min-width: 0; padding: 11px var(--s-sp-3);
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  background: var(--s-surface); font-size: var(--s-fs-md); color: var(--s-ink);
}
.pm-del {
  width: 36px; height: 36px; flex-shrink: 0; border: 0; background: none;
  color: var(--s-muted); font-size: 1.4rem; line-height: 1; cursor: pointer;
}
.pm-del:hover { color: var(--s-bad); }
.pm-add {
  width: 100%; margin-top: 6px; padding: 13px;
  border: 2px dashed var(--s-line); border-radius: var(--s-r-sm);
  background: var(--s-surface); color: var(--s-info);
  font-size: var(--s-fs-md); font-weight: 900; cursor: pointer;
}
.pm-add[disabled] { opacity: .45; cursor: not-allowed; }
.pm-notify {
  display: flex; gap: 11px; align-items: flex-start; margin-top: var(--s-sp-6);
  padding: 13px var(--s-sp-3); border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm); cursor: pointer;
}
.pm-notify input { width: 18px; height: 18px; flex-shrink: 0; margin-top: 2px; }
.pm-notify b { font-size: var(--s-fs-md); font-weight: 800; color: var(--s-ink); }
.pm-notify span { display: block; margin-top: 2px; font-size: var(--s-fs-xs); font-weight: 500; color: var(--s-muted); }
.poll-modal-foot {
  display: flex; align-items: center; gap: var(--s-sp-3); flex-shrink: 0;
  padding: var(--s-sp-3) var(--s-sp-4) calc(var(--s-sp-4) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--s-line);
}
.pm-save {
  flex: 1; padding: 15px; border: 0; border-radius: var(--s-r-sm);
  background: var(--s-good); color: #fff; font-size: var(--s-fs-md);
  font-weight: 900; cursor: pointer;
}
.pm-removepoll {
  border: 0; background: none; white-space: nowrap;
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-bad); cursor: pointer;
}
@media (min-width: 561px) {
  .poll-modal { margin: auto; height: auto; max-height: 88dvh; border-radius: var(--s-r-xl); overflow: hidden; }
}

/* ── 11x. Lightbox ─────────────────────────────────────────────────────── */
.feed-lightbox {
  display: none; position: fixed; inset: 0; z-index: 12000;
  align-items: center; justify-content: center; background: rgba(2, 6, 23, .92);
}
.feed-lightbox.active { display: flex; }
.feed-lightbox img { max-width: 94vw; max-height: 86dvh; border-radius: var(--s-r-sm); }
.lb-close {
  position: absolute; top: calc(12px + env(safe-area-inset-top, 0px)); right: 14px;
  width: 42px; height: 42px; border: 0; border-radius: 50%;
  background: rgba(255, 255, 255, .14); color: #fff;
  font-size: 1.6rem; line-height: 1; cursor: pointer;
}
.lb-nav {
  display: none; position: absolute; top: 50%; transform: translateY(-50%);
  width: 44px; height: 60px; border: 0; border-radius: var(--s-r-sm);
  background: rgba(255, 255, 255, .12); color: #fff; font-size: 1.5rem; cursor: pointer;
}
.feed-lightbox.has-nav .lb-nav { display: block; }
.lb-prev { left: 10px; }
.lb-next { right: 10px; }
/* Top-left, opposite the close button. At the bottom it landed squarely on
   the app's own bottom bar, which shows through the overlay. */
.lb-counter {
  position: absolute; top: calc(19px + env(safe-area-inset-top, 0px)); left: 14px;
  padding: 4px 12px;
  border-radius: var(--s-r-pill); background: rgba(255, 255, 255, .14);
  color: #fff; font-size: var(--s-fs-xs); font-weight: 800;
}

/* ── 11y. Sidebar ──────────────────────────────────────── */
/* Desktop only — .feed-layout collapses to one column below 992px and the
   sidebar goes with it, exactly as v1 did. */
.feed-side { padding: var(--s-sp-3); }
.feed-side__h {
  display: flex; align-items: center; gap: 6px; margin-bottom: var(--s-sp-2);
  font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-ink);
}
.feed-side__f { display: grid; gap: var(--s-sp-2); }
.feed-filter-lbl {
  display: block; margin-bottom: 3px; font-size: var(--s-fs-xs);
  font-weight: 800; color: var(--s-muted);
}
.feed-bcast {
  display: flex; align-items: flex-start; gap: var(--s-sp-2);
  padding: var(--s-sp-2) 0; text-decoration: none;
  border-bottom: 1px solid var(--s-line-soft);
}
.feed-bcast:last-child { border-bottom: 0; }
.feed-bcast__e { font-size: 1.1rem; line-height: 1.2; }
.feed-bcast__t { min-width: 0; }
.feed-bcast__t b {
  display: block; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.feed-bcast__t span { font-size: var(--s-fs-xs); color: var(--s-muted); }
/* The "reset what I've seen" link is a developer affordance, so it is
   deliberately quiet rather than promoted to a button. */
.feed-reset-missed {
  display: block; margin-top: var(--s-sp-3); text-align: center;
  font-size: var(--s-fs-xs); color: var(--s-muted); text-decoration: none; opacity: .55;
}

/* ══════════════════════════════════════════════════════════════════════════
   12. MATCH DAY

   One fixture, opened. The v1 page carried 45 lines of its own CSS and 131
   inline style attributes, and it opened on a page title — so the thing a
   coach actually came for (who are we playing, when, where) was assembled out
   of a badge row and a stack of small facts.

   Two ideas drive what is below:

     • A match has a scoreboard. .s-md is that: two teams, the kick-off or the
       result between them, and everything else underneath. It is the same
       block whether the match is next Saturday or three seasons ago — the
       middle switches from a time to a score and the card stops asking for
       anything.
     • Readiness is a summary, not five separate pills. .s-ready puts venue,
       register, weather, officials and plan in one strip at the top, so "what
       still needs me" is answered before a single card is opened. Each tile
       jumps to the card it stands for.

   Nothing here is fixture-specific by nature: a training session and a
   tournament fixture want the same scoreboard and the same readiness strip,
   which is why they live here rather than on the page.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── The scoreboard ─────────────────────────────────────────────────────
   Brand ground, because this is the one card on the page that is the match
   itself rather than a fact about it. Everything inside is set against
   --s-brand-on so a club with a pale primary still reads. */
.s-md {
  border-radius: var(--s-r-lg);
  background: linear-gradient(160deg,
              var(--s-brand) 0%,
              color-mix(in srgb, var(--s-brand) 82%, #000) 100%);
  color: var(--s-brand-on);
  padding: var(--s-sp-4);
  box-shadow: var(--s-shadow-sm);
}
.s-md__comp {
  font-size: 10px; font-weight: 900; letter-spacing: .07em; text-transform: uppercase;
  color: color-mix(in srgb, var(--s-brand-on) 70%, transparent);
  text-align: center;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.s-md__board {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: start;
  gap: var(--s-sp-2);
  margin-top: var(--s-sp-3);
}
.s-md__side { min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 6px; }
.s-md__crest {
  width: 46px; height: 46px; flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--s-fs-md); font-weight: 900; letter-spacing: .02em;
  background: color-mix(in srgb, var(--s-brand-on) 16%, transparent);
  color: var(--s-brand-on);
  border: 1px solid color-mix(in srgb, var(--s-brand-on) 26%, transparent);
  overflow: hidden;
}
.s-md__crest img { width: 100%; height: 100%; object-fit: cover; }
/* A club's own mark, on the one dark ground in the app. Logos are drawn for
   letterheads and kit, so dark ink on a transparent background is common and
   light ink on dark is rare — an image crest therefore gets a white plate and
   is fitted rather than cropped, the way a real crest sits on a scoreboard.
   A club whose mark is a wide wordmark still reads instead of being centre-
   cropped to two letters. */
.s-md__crest--logo { background: #fff; padding: 3px; border-color: color-mix(in srgb, var(--s-brand-on) 40%, transparent); }
.s-md__crest--logo img { object-fit: contain; }
/* Two lines, then ellipsis. "Riverside Youth FC Ravens" has to fit a 130px
   column on a 375px phone without pushing the score off-centre. */
.s-md__nm {
  font-size: var(--s-fs-sm); font-weight: 900; line-height: 1.25; text-align: center;
  color: var(--s-brand-on);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}
.s-md__ha {
  font-size: 9px; font-weight: 900; letter-spacing: .06em; text-transform: uppercase;
  color: color-mix(in srgb, var(--s-brand-on) 62%, transparent);
}
.s-md__mid { flex-shrink: 0; text-align: center; padding: 0 var(--s-sp-1); }
.s-md__score {
  font-size: var(--s-fs-2xl); font-weight: 900; line-height: 1.05;
  letter-spacing: -.02em; font-variant-numeric: tabular-nums;
  color: var(--s-brand-on);
}
.s-md__v {
  font-size: var(--s-fs-lg); font-weight: 900; line-height: 1.1;
  color: color-mix(in srgb, var(--s-brand-on) 55%, transparent);
}
.s-md__ko {
  margin-top: 3px;
  font-size: var(--s-fs-lg); font-weight: 900; line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--s-brand-on);
}
.s-md__kolb {
  margin-top: 2px;
  font-size: 9px; font-weight: 900; letter-spacing: .06em; text-transform: uppercase;
  color: color-mix(in srgb, var(--s-brand-on) 62%, transparent);
}
/* The facts under the board: date, meet time, address. A hairline above them
   rather than a card of their own — they belong to the match. */
.s-md__foot {
  margin-top: var(--s-sp-3); padding-top: var(--s-sp-3);
  border-top: 1px solid color-mix(in srgb, var(--s-brand-on) 16%, transparent);
  display: grid; gap: 5px;
}
.s-md__f { display: flex; align-items: flex-start; gap: 8px; font-size: var(--s-fs-sm); font-weight: 700; }
.s-md__f i { width: 14px; flex-shrink: 0; margin-top: 2px; text-align: center; color: color-mix(in srgb, var(--s-brand-on) 62%, transparent); }
.s-md__f span { min-width: 0; overflow-wrap: anywhere; color: color-mix(in srgb, var(--s-brand-on) 92%, transparent); }
.s-md__f b { font-weight: 900; color: var(--s-brand-on); }
.s-md__f a { color: var(--s-brand-on); font-weight: 900; }
.s-md__chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 5px; margin-top: var(--s-sp-3); }
/* Chips on the brand ground need their own tinting — .s-chip's surface
   colours vanish against it. */
.s-md__chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 9px; border-radius: 999px;
  font-size: var(--s-fs-xs); font-weight: 900; letter-spacing: .02em;
  background: color-mix(in srgb, var(--s-brand-on) 15%, transparent);
  color: var(--s-brand-on);
  border: 1px solid color-mix(in srgb, var(--s-brand-on) 22%, transparent);
}
.s-md__chip--win  { background: color-mix(in srgb, #4ade80 30%, transparent); border-color: color-mix(in srgb, #4ade80 45%, transparent); }
.s-md__chip--loss { background: color-mix(in srgb, #fb7185 28%, transparent); border-color: color-mix(in srgb, #fb7185 42%, transparent); }
.s-md__chip--draft{ background: color-mix(in srgb, #fbbf24 30%, transparent); border-color: color-mix(in srgb, #fbbf24 45%, transparent); }

/* ── Readiness strip ────────────────────────────────────────────────────
   Five tiles, one per card below. Scrolls sideways on a phone rather than
   wrapping to two ragged rows; the fifth tile peeking is the affordance. */
.s-ready {
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(84px, 1fr);
  gap: var(--s-sp-2);
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding-bottom: 2px;
}
.s-ready::-webkit-scrollbar { display: none; }
.s-ready__i {
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 10px 6px;
  border-radius: var(--s-r);
  background: var(--s-surface); border: 1px solid var(--s-line);
  text-decoration: none; color: var(--s-ink);
  text-align: center;
}
.s-ready__ic {
  width: 28px; height: 28px; flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 12px;
  background: var(--s-slate-bg); color: var(--s-slate);
}
.s-ready__lb { font-size: 10px; font-weight: 900; letter-spacing: .03em; text-transform: uppercase; color: var(--s-muted); }
.s-ready__i--done { background: var(--s-good-bg); border-color: var(--s-good-line); }
.s-ready__i--done .s-ready__ic { background: var(--s-good); color: #fff; }
.s-ready__i--done .s-ready__lb { color: var(--s-good); }
.s-ready__i--todo { background: var(--s-warn-bg); border-color: var(--s-warn-line); }
.s-ready__i--todo .s-ready__ic { background: var(--s-warn); color: #fff; }
.s-ready__i--todo .s-ready__lb { color: var(--s-warn); }

/* ── Availability ring ──────────────────────────────────────────────────
   Four numbers that have to add up, so they are drawn as one circle rather
   than four bars: the gap at the end IS the players who have not replied. */
.s-ringrow { display: flex; align-items: center; gap: var(--s-sp-4); }
.s-ring { position: relative; width: 92px; height: 92px; flex-shrink: 0; }
.s-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.s-ring circle { fill: none; stroke-width: 6; stroke-linecap: butt; }
.s-ring__c {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  line-height: 1;
}
.s-ring__n { font-size: var(--s-fs-xl); font-weight: 900; color: var(--s-ink); font-variant-numeric: tabular-nums; }
.s-ring__of { margin-top: 3px; font-size: 10px; font-weight: 900; letter-spacing: .04em; text-transform: uppercase; color: var(--s-muted); }
.s-legend { display: grid; gap: 5px; min-width: 0; }
.s-legend__i { display: flex; align-items: center; gap: 7px; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink-2); }
.s-legend__d { width: 9px; height: 9px; flex-shrink: 0; border-radius: 50%; }
.s-legend__i b { font-weight: 900; color: var(--s-ink); font-variant-numeric: tabular-nums; }
/* The ring's arcs and the legend's dots are the same four readings twice over,
   so they are named rather than coloured at the call site — a template writing
   stroke="#10b981" is how a design system starts leaking. */
.s-ring__bg { stroke: var(--s-line); }
.s-ring__a--good { stroke: var(--s-good); }
.s-ring__a--bad  { stroke: var(--s-bad); }
.s-ring__a--warn { stroke: var(--s-warn); }
.s-legend__d--good { background: var(--s-good); }
.s-legend__d--bad  { background: var(--s-bad); }
.s-legend__d--warn { background: var(--s-warn); }
.s-legend__d--none { background: var(--s-line); }

/* ── A named person: coach, referee ─────────────────────────────────────── */
.s-person { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-top: 1px solid var(--s-line-soft); text-decoration: none; color: inherit; }
.s-person:first-of-type { border-top: none; }
/* ...and a wrapper, for a card whose people are not its first children: the
   rule above is :first-of-type, which a card title above them would win. */
.s-people { display: flex; flex-direction: column; }
.s-person__av {
  width: 32px; height: 32px; flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: var(--s-fs-xs); font-weight: 900;
  background: var(--s-info-bg); color: var(--s-info);
}
.s-person__av--ref { background: var(--s-good-bg); color: var(--s-good); }
.s-person__av--award { background: var(--s-warn-bg); color: var(--s-warn); }
a.s-person { text-decoration: none; color: var(--s-ink); }
.s-person__b { min-width: 0; flex: 1; }
.s-person__nm { display: block; font-size: var(--s-fs-md); font-weight: 800; color: var(--s-ink); }
.s-person__rl { display: block; margin-top: 1px; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }
.s-person__when { flex-shrink: 0; font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-muted); }

/* ── Trophy cabinet ──────────────────────────────────────────────────────
   A team's silverware, on the team page and on the child profiles of the
   players tagged on each win. The metal colours are literal hexes, the same
   in dark mode on purpose: gold is gold whatever the theme, and the three
   metals ARE the information — league outranks cup outranks tournament in a
   glance before you read a word. */
.s-cab { display: flex; flex-direction: column; }
.s-cab__item { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-top: 1px solid var(--s-line-soft); }
.s-cab__item:first-child { border-top: none; }
.s-cab__ic {
  width: 40px; height: 40px; flex-shrink: 0;
  border-radius: var(--s-r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 17px;
}
.s-cab__ic--league     { background: rgba(212, 160, 23, .16); color: #b8860b; }
.s-cab__ic--cup        { background: rgba(148, 163, 184, .2);  color: #64748b; }
.s-cab__ic--tournament { background: rgba(205, 127, 50, .16);  color: #b06c2a; }
.s-cab__b { min-width: 0; flex: 1; }
.s-cab__t { display: block; font-size: var(--s-fs-md); font-weight: 800; color: var(--s-ink); }
.s-cab__s { display: block; margin-top: 1px; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }

/* ── A contact you can tap ──────────────────────────────────────────────
   An email address or a phone number rendered as a target rather than as a
   link. Bare inline it is a 13px sliver you aim at twice, and it is one
   unbreakable word, so on a 375px screen it takes the whole page sideways
   with it. Anything else you would do to a person from a list of people —
   open their profile, check their badges — belongs in the same strip, so
   there is one row of things to tap and not three. */
.s-contacts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 7px; }
.s-contact {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: 40px; max-width: 100%;
  padding: 6px 12px;
  border: 1px solid var(--s-line); border-radius: var(--s-r-pill);
  background: var(--s-surface-2); color: var(--s-ink);
  font-size: var(--s-fs-xs); font-weight: 700;
  text-decoration: none; overflow-wrap: anywhere;
}
.s-contact i { color: var(--s-muted); flex: 0 0 auto; }
.s-contact:hover { background: var(--s-surface); }

/* ── A list of named facts ──────────────────────────────────────
   Setup facts — squad size, age range, kick-off numbers, fees. Label above
   value doubles the height of a ten-row card; a bold run-in label inside a
   paragraph, which is what v1 did, gives a ragged left edge you have to read
   every line of to scan. Label left, value right, one rule between. */
.s-kv { display: flex; flex-direction: column; }
.s-kv__i {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s-sp-3);
  padding: 8px 0;
  border-bottom: 1px solid var(--s-line-soft);
}
.s-kv__i:first-child { padding-top: 0; }
.s-kv__i:last-child { padding-bottom: 0; border-bottom: none; }
.s-kv__k { flex-shrink: 0; font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink-2); }
.s-kv__v {
  min-width: 0; text-align: right;
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink);
}
/* A value that is a sentence rather than a figure gets the whole width and
   the normal reading direction. */
.s-kv__i--long { flex-direction: column; align-items: stretch; gap: 3px; }
.s-kv__i--long .s-kv__v { text-align: left; font-weight: 600; color: var(--s-ink-2); }

/* ── What has happened to one record, in order ─────────────────────────────
   An audit trail: the entries the app wrote itself and the notes people
   typed, on one ladder. v1 drew this with a stylesheet in the head of the
   incident page, which is how a shape every record in the app could use ended
   up belonging to one template.

   Two kinds of entry share the row. What the app recorded is a short
   sentence; what a person wrote can be a paragraph, so the text is set at
   reading weight rather than as a heading. The rail behind the glyphs is what
   makes eight of these read as one sequence instead of eight stacked rows,
   and it stops at the last glyph rather than running on into whatever the
   card puts underneath. */
.s-tl { display: flex; flex-direction: column; gap: 14px; }
.s-tl__i { display: flex; gap: 11px; position: relative; }
.s-tl__i::before {
  content: ""; position: absolute; left: 13px; top: 30px;
  width: 2px; height: calc(100% - 16px);
  background: var(--s-line);
}
.s-tl__i:last-child::before { display: none; }
.s-tl__ic {
  width: 28px; height: 28px; flex-shrink: 0;
  border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 11px;
  background: var(--s-slate-bg); color: var(--s-slate);
}
.s-tl__ic--good   { background: var(--s-good-bg);   color: var(--s-good); }
.s-tl__ic--violet { background: var(--s-violet-bg); color: var(--s-violet); }
.s-tl__b { min-width: 0; flex: 1; padding-top: 3px; }
.s-tl__t { font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink); overflow-wrap: anywhere; }
.s-tl__m { margin-top: 3px; font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }

/* ── Two columns inside a card, on a screen wide enough ─────────────────── */
.s-cols { display: grid; gap: var(--s-sp-4); }
.s-cols__t {
  font-size: 10px; font-weight: 900; letter-spacing: .05em; text-transform: uppercase;
  color: var(--s-muted); margin-bottom: 2px;
}
.s-cols__t i { margin-right: 5px; }

/* ── The reminder ladder ────────────────────────────────────────────────
   Who gets an email, at which stage. A stage is a position on a ladder, so
   the chip carries the ladder's colour rather than a per-stage invention. */
.s-stage {
  display: inline-flex; align-items: center;
  padding: 2px 8px; border-radius: 999px;
  font-size: 10px; font-weight: 900; letter-spacing: .03em; text-transform: uppercase;
  background: var(--s-info-bg); color: var(--s-info);
}
.s-stage--soon   { background: var(--s-warn-bg); color: var(--s-warn); }
.s-stage--set    { background: var(--s-good-bg); color: var(--s-good); }
.s-stage--urgent { background: var(--s-bad-bg);  color: var(--s-bad); }

/* ══════════════════════════════════════════════════════════════════════════
   13. THE REGISTER

   Marking off who turned up. It happens on a touchline, in the rain, on a
   phone held in one hand — which is the whole design brief. v1 built it twice
   (once for training, once inside match day) out of 90 lines of page CSS and
   42px squares that were still under the 44px Apple and Google both ask for.

   Three rules:

     • Present and absent are the same control the rest of the app uses for
       yes/no — .s-toggle. A coach who has answered an RSVP has already been
       taught this button. Only the shape changes: --sq makes it a thumb-sized
       square instead of a pill, because here it carries an icon, not a word,
       and it repeats down a list where a ragged right edge is noise.
     • Everything a coach does NOT need mid-register — the status dropdown,
       the note field, who recorded the reply — goes in a drawer. Taking a
       register is two taps per player; anything else is a different job.
     • The row never moves. The name column flexes and the actions are fixed
       width, so the button under your thumb on player 3 is in the same place
       on player 30.
   ══════════════════════════════════════════════════════════════════════════ */

/* The bar above the list: where you are, and the one shortcut worth having.
   "All present" is the honest common case — a coach marks the exceptions. */
.s-regbar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px var(--s-sp-2); margin-bottom: 4px; }
/* Both halves stay on one line each. On a 375px phone they do not fit side by
   side, so the button drops beneath and sits right — which is tidier than a
   count broken over two lines next to a button broken over two lines. */
.s-regbar__c { white-space: nowrap; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-muted); }
.s-regbar .s-btn { margin-left: auto; white-space: nowrap; }
.s-regbar__c b { color: var(--s-ink); font-weight: 900; font-variant-numeric: tabular-nums; }

.s-reg { border-bottom: 1px solid var(--s-line-soft); }
.s-reg:last-child { border-bottom: none; }
.s-reg__top { display: flex; align-items: center; gap: 9px; padding: 8px 0; }
.s-reg__b { flex: 1; min-width: 0; }
.s-reg__nm { display: block; font-size: var(--s-fs-md); font-weight: 800; color: var(--s-ink); overflow-wrap: anywhere; }
.s-reg__nm .s-chip { margin-left: 5px; vertical-align: 1px; }
.s-reg__sub { display: block; margin-top: 1px; font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-muted); }
.s-reg__sub--good { color: var(--s-good); }
.s-reg__sub--bad  { color: var(--s-bad); }
/* A parent's own words. Quoted and italic so it never reads as a system
   status line — "back at 6" is a person talking, not a field. */
.s-reg__note { display: block; margin-top: 1px; font-size: var(--s-fs-sm); font-style: italic; color: var(--s-muted); overflow-wrap: anywhere; }
.s-reg__acts { flex-shrink: 0; display: flex; gap: 5px; }
.s-reg__more {
  flex-shrink: 0; width: 26px; height: 42px;
  border: none; background: none; cursor: pointer;
  color: var(--s-muted); font-size: 13px;
}
.s-reg.is-open .s-reg__more { color: var(--s-brand); }
.s-reg__dr { display: none; padding: 0 0 11px 39px; }
.s-reg.is-open .s-reg__dr { display: block; }
.s-reg__dr .s-field { margin-bottom: 8px; }
.s-reg__rec { margin-top: 6px; font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }
@media (max-width: 380px) {
  /* No room to indent the drawer under the avatar as well as fit the row. */
  .s-reg__dr { padding-left: 0; }
}

/* Three-way rows: the choice moves off the name line and onto its own.

   Two icon squares fit beside a name at 375px; three do not — they leave the
   name about 130px, which wraps "Charlie Thompson" mid-word. And a third
   state cannot be an icon anyway: a tick reads as yes and a cross as no, but
   nothing reads as "maybe" without a word next to it. So the picker gets a
   full line and the buttons get their labels back.

   It costs a row about 34px of height. That is the right trade here and not on
   the training register: taking a register is two taps per player down a squad
   of thirty, where density is the whole point, while availability is usually a
   handful of replies being written down. */
.s-reg__pick { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; max-width: 460px; padding-bottom: 10px; }
.s-reg__pick .s-toggle { min-height: 44px; justify-content: center; padding: 6px 4px; }
/* A stacked row is a grid rather than a stack of blocks, because the stacking
   is only true while the screen is narrow. Given room, the picker folds back
   up beside the name and the row becomes the same one-line shape the training
   register has — same component, same reading, one breakpoint apart. */
.s-reg--stack { display: grid; grid-template-columns: minmax(0, 1fr) 26px; }
.s-reg--stack .s-reg__top  { grid-column: 1; grid-row: 1; }
/* Beside the name, not centred against it: the note under a name can run to
   two lines, and a button that moves row to row is the one thing the register
   promises not to do. */
.s-reg--stack .s-reg__more { grid-column: 2; grid-row: 1; align-self: start; }
.s-reg--stack .s-reg__pick { grid-column: 1 / -1; }
.s-reg--stack .s-reg__dr   { grid-column: 1 / -1; padding-left: 0; }
@media (min-width: 640px) {
  .s-reg--stack { grid-template-columns: minmax(0, 1fr) 336px 26px; align-items: center; }
  .s-reg--stack .s-reg__pick { grid-column: 2; grid-row: 1; max-width: none; padding-bottom: 0; }
  .s-reg--stack .s-reg__more { grid-column: 3; align-self: center; }
  .s-reg--stack .s-reg__dr   { grid-row: 2; }
}

/* ── 14. THE PLAYER PROFILE ────────────────────────────────────────────
   One child, seen by their parent or by a coach. The page is a scroll, so
   the job of these pieces is to make each section answerable at a glance
   and to keep the numbers kind: this is an eight-year-old's page, and a
   red "0" is not information a parent needs.
   ══════════════════════════════════════════════════════════════════════════ */

/* Who the page is about. A portrait belongs at 64px — big enough to be a
   face rather than a token, small enough that the name stays the headline. */
.s-avatar--lg { width: 64px; height: 64px; border-width: 2px; font-size: var(--s-fs-xl); }
.s-idrow { display: flex; align-items: center; gap: var(--s-sp-3); min-width: 0; }
.s-idrow__b { min-width: 0; }
/* A chip sitting directly on the row is a status for the whole person, not
   a label on their name, so it belongs on the far edge. Chips that describe
   the name itself live inside .s-idrow__b and are untouched by this. */
.s-idrow > .s-chip { margin-left: auto; flex: none; }

/* Stat tiles gain a unit and a sub-line here. The unit is set quiet so the
   number is what you read; the sub-line carries the working ("12 of 15") so
   the tile above it never has to. */
.s-stat__u { font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-muted); }
.s-stat__s {
  margin-top: 4px;
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted);
  font-variant-numeric: tabular-nums;
}
/* Earned tiles: goals and assists appear only once a child is off the mark,
   so when they do appear they are allowed to look like an occasion. */
.s-stat--earned { background: var(--s-warn-bg); }
.s-stat--earned .s-stat__v { color: var(--s-warn); }
.s-stat--earned .s-stat__k, .s-stat--earned .s-stat__s { color: var(--s-warn); }
a.s-stat { display: block; text-decoration: none; }
/* Tiles that wrap to a second row have to line up with the first. Sub-lines
   are optional per tile, so without this a row of three sits ragged. */
.s-stats--even { grid-auto-rows: 1fr; }
.s-stats--even .s-stat { display: flex; flex-direction: column; }
.s-stats--even .s-stat__s { margin-top: auto; }

/* A labelled bar. Minutes against the team average, and which positions a
   child has played, are the same shape of answer, so they are one component.
   The track is .s-progress — the width is live data and arrives as data-bar
   for the hydration script, the way every other bar in this app does. */
/* A bar sitting under the sentence it illustrates -- "3 still to reply",
   then the bar, then what it is out of -- needs a beat either side, or
   the three read as one block. */
.s-meta + .s-progress { margin-top: 8px; }
.s-progress + .s-meta { margin-top: 6px; }
.s-bars { display: flex; flex-direction: column; gap: 9px; }
.s-bar { display: flex; align-items: center; gap: 10px; }
.s-bar__k {
  width: 94px; flex-shrink: 0;
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink-2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.s-bar__t { flex: 1; min-width: 0; }
.s-bar__v {
  width: 54px; flex-shrink: 0; text-align: right; white-space: nowrap;
  font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-ink);
  font-variant-numeric: tabular-nums;
}
.s-progress__fill--info { background: var(--s-info); }
.s-progress__fill--mute { background: var(--s-muted); }
.s-progress__fill--warn { background: var(--s-warn); }
.s-progress__fill--bad  { background: var(--s-bad); }

/* A capacity bar sized to sit under a number in a table cell -- squad places
   filled, seats taken. The number is the fact and the bar is not a second copy
   of it: it is there so a column of thirty reads at a glance without being
   read. */
.s-cap { width: 78px; margin-top: 5px; }

/* Form: the last six results, most recent first. Tinted rather than filled,
   for the same reason .s-score is — at this age a draw is a result, not a
   near-miss, and a row of solid red blocks is a scoreline nobody asked for. */
.s-form { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
.s-fp {
  width: 26px; height: 26px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--s-r-sm);
  font-size: var(--s-fs-sm); font-weight: 900;
  background: var(--s-surface-2); color: var(--s-muted);
}
.s-fp--w { background: var(--s-good-bg); color: var(--s-good); }
.s-fp--l { background: var(--s-bad-bg);  color: var(--s-bad); }
.s-fp--d { background: var(--s-slate-bg); color: var(--s-slate); }
.s-fp--lg { width: 34px; height: 34px; border-radius: var(--s-r); font-size: var(--s-fs-md); }
.s-form__rec { margin-left: auto; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-muted); font-variant-numeric: tabular-nums; }

/* One game the child played in. The scoreline sits in the title, where a
   person would say it ("vs Ravens 2–1"), so the row needs no right-hand
   column and stays readable at 375px. */
.s-game { display: flex; align-items: flex-start; gap: 10px; padding: 11px 0; border-top: 1px solid var(--s-line-soft); text-decoration: none; color: inherit; }
.s-game:first-of-type { border-top: none; }
.s-game__b { flex: 1; min-width: 0; }
.s-game__t { font-size: var(--s-fs-md); font-weight: 800; color: var(--s-ink); overflow-wrap: anywhere; }
.s-game__t b { font-weight: 900; font-variant-numeric: tabular-nums; }
.s-game__t .s-chip { margin-left: 5px; vertical-align: 1px; }
.s-game__ln { display: flex; flex-wrap: wrap; gap: 2px 10px; margin-top: 4px; font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }
.s-game__ln b { color: var(--s-ink); font-weight: 900; font-variant-numeric: tabular-nums; }

/* A count inside a chip: "3 goals" reads as one thing, not a number next to
   a word. The disc is mixed from the chip's own colour so it works in every
   chip variant without a second set of rules. */
.s-chip__n {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 17px; height: 17px; padding: 0 5px; margin-right: 5px;
  border-radius: var(--s-r-pill);
  background: color-mix(in srgb, currentColor 22%, transparent);
  font-variant-numeric: tabular-nums;
}

/* A setting that takes effect on the spot. The whole row is the label, so
   the tap target is the sentence and not a 46px sliver at the end of it. */
.s-switch { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-sp-3); cursor: pointer; margin: 0; }
.s-switch__b { min-width: 0; }
.s-switch__t { display: block; font-size: var(--s-fs-md); font-weight: 800; color: var(--s-ink); }
.s-switch__s { display: block; margin-top: 2px; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }
.s-switch input {
  appearance: none; -webkit-appearance: none;
  flex-shrink: 0; position: relative; margin: 0;
  width: 46px; height: 26px; border-radius: var(--s-r-pill);
  background: var(--s-line); cursor: pointer;
  transition: background .18s ease;
}
.s-switch input::after {
  content: ""; position: absolute; top: 3px; left: 3px;
  width: 20px; height: 20px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(2,6,23,.3);
  transition: transform .18s ease;
}
.s-switch input:checked { background: var(--s-good); }
.s-switch input:checked::after { transform: translateX(20px); }
.s-calscope { justify-content: flex-start; align-items: center; gap: var(--s-sp-2); }
.s-calscope .s-switch__t { font-size: var(--s-fs-sm); white-space: nowrap; }

/* ── 15. UTILITIES ─────────────────────────────────────────────────────── */
/* Deliberately tiny. Utilities are where design systems go to die — if you
   need a fifth one, you probably need a component instead. */
.s-stack { display: flex; flex-direction: column; gap: var(--s-sp-3); }
/* .s-more carries its own top margin for the pages that drop one into
   flowing content. Inside a stack the gap is already there, and the two
   add up to a hole. */
.s-stack > .s-more { margin-top: 0; }
/* Same reasoning, same fix, for the other two components that carry a top
   margin of their own so they can be dropped into flowing content. Both
   margins are var(--s-sp-3), which is also the stack's gap, so inside a stack
   the space between was coming out at exactly double everywhere else on the
   page. .s-note already zeroed itself as a first child, which hid this: it
   only showed on the second note down. */
.s-stack > .s-note,
.s-stack > .s-stats { margin-top: 0; }

/* Single file on a phone, two abreast once there is width for it. For stacks
   of many short cards, where a laptop otherwise scrolls through the same card
   forty times over. align-items: start so a card holding an open drawer does
   not stretch the one beside it. */
@media (min-width: 992px) {
  .s-stack--2up:has(> :nth-child(2)) {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
  }
}
.s-inline { display: flex; align-items: center; gap: var(--s-sp-2); flex-wrap: wrap; }
.s-between { display: flex; align-items: center; justify-content: space-between; gap: var(--s-sp-2); }
/* ...and one that may wrap, for a bar holding a sentence on one side and a
   control on the other: at 375px those do not share a line. */
.s-between--wrap { flex-wrap: wrap; }
/* ...and one that pins both sides to the top. A card header is a title and
   a state chip; when the title takes two lines, centring floats the chip
   down beside the second one, level with nothing. The chip belongs against
   the first line of the thing it is describing. Same reasoning, and the
   same name, as .s-row--top. */
.s-between--top { align-items: flex-start; }
.s-scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }
/* A strip of more than four tabs does not fit one line of the reading
   column. On a phone the swipe is the right answer; on a screen with room
   underneath there is no reason to hide the last tab behind a gesture. */
@media (min-width: 640px) {
  .s-tabs--wrap { flex-wrap: wrap; overflow-x: visible; }
}

/* Which of a list's two presentations you get. Every list page in this app is
   a table on a desktop and a stack of cards on a phone, so this pair earns
   its place. 992px is the switch — the same point the v1 pages used
   d-none/d-lg-block, so nothing moves for anyone at cutover. */
.s-desk-only { display: none; }
.s-mob-only  { display: block; }
/* These two set `display`, so pairing one with a layout utility silently
   demotes it: `.s-stack.s-mob-only` loses to `.s-mob-only { display: block }`
   further down the file, block layout ignores `gap`, and a stack of cards
   butts together into one slab. Restore the layout inside each range — and
   only inside it, because a media query adds no specificity and an unscoped
   rule here would beat the hiding one. */
@media (max-width: 991.98px) { .s-stack.s-mob-only  { display: flex; } }
@media (min-width: 992px)    { .s-stack.s-desk-only { display: flex; } }
.s-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;
}

/* The one utility JS is allowed to reach for. Scripts that hide things by
   writing el.style.display slip past the house-style gate (it only reads
   templates) and quietly reintroduce the bespoke-styling problem v2 exists to
   end — worse, they have to guess the right display value to put back. Toggle
   this class instead and the component keeps its own layout. */
.is-hidden { display: none !important; }

/* Wraps a label and the list it names. A template that filters a list down to
   nothing would otherwise leave the heading behind on its own, promising rows
   that aren't there; this takes the heading with it. */
/* .s-fx is in the list because a group of event cards is filtered the same
   way a group of rows is -- the lineups hub splits one list into played
   and still to come, and either half can be empty. Anything that counts
   as content for one of these groups belongs in this selector; note that
   a heading carrying its own .s-chip count can never hide. */
.s-labelled:not(:has(.s-chip, .s-row, .s-person, .s-fx)) { display: none; }

/* An empty last child on a page carrying .s-bulk. The bar is fixed 74px off
   the floor and about 54px tall, while .s-main's own bottom padding only
   clears the nav beneath it — so without this the last thing on the page sits
   under the bar and cannot be tapped. A spacer rather than padding on that
   last element, because the element in question is usually .s-acts, whose grey
   footer strip would then be 66px of empty grey. */
.s-bulkfoot { height: 54px; }

/* The other one JS owns: a row on its way out. Fading before removal is the
   difference between "that worked" and "did I mis-tap?", and it saves four
   copies of the same three lines of inline-style choreography. */
.is-leaving { opacity: 0; transition: opacity .2s ease; }

/* ── 14. RESPONSIVE ────────────────────────────────────────────────────── */
@media (min-width: 768px) {
  .s-main { padding-left: var(--s-sp-6); padding-right: var(--s-sp-6); }
  .s-cols { grid-template-columns: 1fr 1fr; }
  .s-md__nm { font-size: var(--s-fs-md); }
  .s-tiles:not(.s-tiles--list) { grid-template-columns: repeat(6, 1fr); }
  .s-tiles--list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .s-h1 { font-size: 2rem; }
}
@media (min-width: 992px) {
  .s-drawer__panel { max-width: 720px; margin: 0 auto; border-radius: 0 0 var(--s-r-xl) var(--s-r-xl); }
  .s-tiles:not(.s-tiles--list) { grid-template-columns: repeat(8, 1fr); }
  .s-desk-only { display: block; }
  .s-mob-only  { display: none; }
}

/* ── 16. MOTION ────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  .s-drawer, .s-backdrop, .s-fab span,
  .s-ev__panel, .s-ev__panel-in, .s-ev__chev,
  .s-slides__i, .s-poll__bar, .s-quick,
  .s-navcust, .s-navcust__panel,
  .s-switch input, .s-switch input::after { transition: none; }
}

/* Focus: never remove it, always make it obvious. */
.s-nav__item:focus-visible,
.s-hdr__btn:focus-visible,
.s-fab:focus-visible,
.s-tile:focus-visible,
.s-btn:focus-visible,
.s-row:focus-visible,
.s-toggle:focus-visible,
.s-quick:focus-visible,
.s-poll__opt:focus-visible,
.s-iconbtn:focus-visible,
.s-navcust__mv:focus-visible,
.s-navcust__tile:focus-visible,
.s-ev__head:focus-visible,
.s-switch input:focus-visible,
a.s-card:focus-visible {
  outline: 3px solid var(--s-info);
  outline-offset: 2px;
  border-radius: var(--s-r-sm);
}


/* ── 15. THE CHAT SURFACE ──────────────────────────────────────────────────
   Chat is the one screen people sit in rather than pass through, so it takes
   over the viewport: the app header, bottom bar and drawer button all step
   aside and the page owns the full height. That is what v1 did too — but v1
   did it by hard-coding a slate-900 world of its own, which is precisely the
   fragmentation this sheet exists to end. Here the surface is the same
   tokens as everywhere else, so a club that re-themes gets a re-themed chat,
   and dark mode arrives for free instead of being the only mode.

   The shell is hidden with :has() rather than a body class, because a page
   cannot reach the <body> tag from inside a content block — and a class the
   page's own JS adds would flash the header for a frame first. */
.s-v2:has(.s-chat),
.s-v2:has(.s-chat) body { height: 100%; overflow: hidden; overscroll-behavior: none; }
.s-v2:has(.s-chat) .s-hdr,
.s-v2:has(.s-chat) .s-nav,
.s-v2:has(.s-chat) .s-fab { display: none; }
.s-v2:has(.s-chat) .s-sheet,
.s-v2:has(.s-chat) .s-main { padding: 0; margin: 0; max-width: none; border-radius: 0; }

.s-chat {
  position: fixed; inset: 0; z-index: 40;
  display: flex; flex-direction: column;
  background: var(--s-bg);
}

/* Header. Ink-dark like the app header it is standing in for, so leaving the
   rest of the app and arriving here is a continuation and not a jump. */
.s-chat__hdr {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: var(--s-sp-3);
  padding: calc(env(safe-area-inset-top, 0px) + 10px) var(--s-sp-4) 10px;
  background: var(--s-brand); color: var(--s-brand-on);
}
.s-chat__back, .s-chat__hbtn {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 36px; height: 36px; border-radius: var(--s-r-pill);
  border: 0; cursor: pointer;
  background: rgba(255, 255, 255, .14);
  color: var(--s-brand-on); font-size: var(--s-fs-md);
  text-decoration: none;
}
.s-chat__back:hover, .s-chat__hbtn:hover { background: rgba(255, 255, 255, .24); }
.s-chat__id { flex: 1 1 auto; min-width: 0; }
/* The person you are talking to, in the header. A one-to-one thread has a face
   where a group chat has a team name, so the DM header carries an avatar the
   group one does not. Ringed in the header's own foreground, like the back
   button, so it reads on the brand ground rather than floating on it. */
.s-chat__pic {
  flex: 0 0 auto;
  width: 38px; height: 38px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  object-fit: cover;
  background: rgba(255, 255, 255, .18); color: var(--s-brand-on);
  border: 2px solid rgba(255, 255, 255, .28);
  font-size: var(--s-fs-sm); font-weight: 900;
}
.s-chat__t {
  margin: 0; font-size: var(--s-fs-md); font-weight: 800; color: inherit;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.s-chat__s {
  margin: 0; font-size: var(--s-fs-xs); font-weight: 600;
  color: rgba(255, 255, 255, .68);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* "Encrypted" is a promise, so it is stated on the header and not buried in a
   menu. Small, quiet, always there. */
.s-chat__enc {
  display: inline-flex; align-items: center; gap: 4px;
  margin-right: 5px; padding: 1px 7px; border-radius: var(--s-r-pill);
  background: rgba(255, 255, 255, .16); color: #fff;
  font-size: var(--s-fs-xs); font-weight: 800;
}
.s-chat__enc i { font-size: .6em; }

/* Header overflow menu */
.s-chat__menuwrap { position: relative; flex: 0 0 auto; }
.s-chat__menu {
  position: absolute; right: 0; top: 42px; z-index: 5;
  min-width: 220px; padding: var(--s-sp-1); overflow: hidden;
  background: var(--s-surface); border: 1px solid var(--s-line);
  border-radius: var(--s-r); box-shadow: var(--s-shadow-lg);
}
.s-chat__menu[hidden] { display: none; }
.s-chat__mi {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 10px 12px; border: 0; border-radius: var(--s-r-sm);
  background: none; color: var(--s-ink); text-align: left;
  font-size: var(--s-fs-sm); font-weight: 700; cursor: pointer;
  text-decoration: none; /* a menu row can be a link -- it still reads as a row */
}
.s-chat__mi:hover { background: var(--s-surface-2); }
.s-chat__mi i { color: var(--s-muted); width: 16px; }

/* The room switcher's rows. A match chat can hop to any other room open in the
   same week, and a fixture name without its date is not enough to pick by — so
   these rows carry two lines where a plain menu item carries one. */
.s-chat__menu--scroll { max-height: min(60vh, 340px); overflow-y: auto; }
.s-chat__mlb {
  padding: 6px 12px 2px;
  font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-muted);
  text-transform: uppercase; letter-spacing: .04em;
}
.s-chat__mi--2 { align-items: flex-start; }
.s-chat__mi--2 i { margin-top: 3px; }
.s-chat__mi--2 b { display: block; }
.s-chat__mi--2 small {
  display: block; margin-top: 1px;
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted);
}

/* Presence strip — who else is in the room right now. A horizontal scroller so
   a busy team never pushes the messages down the screen. */
.s-chat__strip {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: var(--s-sp-2);
  padding: 6px var(--s-sp-4);
  background: var(--s-surface); border-bottom: 1px solid var(--s-line);
  overflow-x: auto; scrollbar-width: none;
}
.s-chat__strip::-webkit-scrollbar { display: none; }
.s-chat__striplb {
  flex: 0 0 auto;
  font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-muted);
  text-transform: uppercase; letter-spacing: .04em; white-space: nowrap;
}
.s-chat__avs { display: flex; align-items: center; gap: 6px; }
.s-chat__av {
  position: relative; flex: 0 0 auto;
  width: 28px; height: 28px; border-radius: var(--s-r-pill);
  display: flex; align-items: center; justify-content: center;
  background: var(--s-brand); color: var(--s-brand-on);
  font-size: var(--s-fs-xs); font-weight: 800;
  box-shadow: 0 0 0 2px var(--s-good);
}
.s-chat__av::after {
  content: ''; position: absolute; right: -1px; bottom: -1px;
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--s-good); border: 1.5px solid var(--s-surface);
}
.s-chat__av--off { box-shadow: 0 0 0 2px var(--s-line); }
.s-chat__av--off::after { background: var(--s-muted); }
/* When nobody else is here, say so in words rather than showing an empty rail. */
.s-chat__alone { font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-muted); }

/* Messages. The scroller, and the only part of the page that moves. */
.s-chat__body {
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column; gap: var(--s-sp-3);
  padding: var(--s-sp-4);
  overflow-y: auto; -webkit-overflow-scrolling: touch;
}
/* A short thread sits on the composer rather than hanging from the header,
   the way every chat app behaves. Once the thread is long enough to scroll the
   auto margin resolves to zero, so this costs nothing on a full room — and it
   avoids justify-content:flex-end, which makes the top of an overflowing
   scroller unreachable. Scoped to .s-msg so the empty state keeps its own
   placement. */
.s-chat__body > .s-msg:first-child { margin-top: auto; }
.s-msg {
  display: flex; align-items: flex-start; gap: var(--s-sp-2);
  max-width: 82%;
  animation: s-msg-in .18s ease-out;
}
@keyframes s-msg-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) { .s-msg { animation: none; } }
.s-msg--them { align-self: flex-start; }
.s-msg--me   { align-self: flex-end; flex-direction: row-reverse; }
.s-msg__av {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: var(--s-r-pill);
  display: flex; align-items: center; justify-content: center;
  background: var(--s-slate-bg); color: var(--s-slate);
  font-size: var(--s-fs-xs); font-weight: 800;
}
.s-msg__b { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* pre-wrap, so a message typed across three lines arrives as three lines. It
   also means the template must emit the bubble on ONE source line — any
   indentation inside it renders as leading whitespace. */
.s-msg__bub {
  padding: 8px 12px; border-radius: var(--s-r-lg);
  font-size: var(--s-fs-sm); line-height: 1.45;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.s-msg--them .s-msg__bub {
  background: var(--s-surface); color: var(--s-ink);
  border: 1px solid var(--s-line); border-bottom-left-radius: 5px;
}
.s-msg--me .s-msg__bub {
  background: var(--s-brand); color: var(--s-brand-on);
  border-bottom-right-radius: 5px;
}
.s-msg__bub img { display: block; max-width: 100%; border-radius: var(--s-r-sm); }
.s-msg__bub img + img,
.s-msg__bub img:not(:first-child) { margin-top: 6px; }
.s-msg__sending { font-size: var(--s-fs-xs); opacity: .7; }
.s-msg__bub--failed { opacity: .5; }
.s-msg__meta {
  display: flex; align-items: center; gap: 6px; padding: 0 4px;
  font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted);
}
.s-msg--me .s-msg__meta { flex-direction: row-reverse; }
.s-msg__act {
  border: 0; background: none; cursor: pointer; padding: 2px;
  color: var(--s-muted); font-size: var(--s-fs-sm);
  opacity: 0; transition: opacity .15s;
}
.s-msg__b:hover .s-msg__act, .s-msg__act:focus-visible { opacity: 1; }
.s-msg__act:hover { color: var(--s-brand); }
.s-msg__act--bad:hover { color: var(--s-bad); }
/* Touch has no hover, so on a phone the actions are simply always there —
   an invisible delete button is worse than a slightly busier row. */
@media (hover: none) { .s-msg__act { opacity: 1; } }
/* A send that failed says so in the meta row, and says it whether or not a
   pointer is hovering: the actions beside it are affordances you go looking
   for, this is news. */
.s-msg__fail {
  border: 0; background: none; padding: 0; cursor: pointer;
  color: var(--s-bad); font-size: var(--s-fs-xs); font-weight: 800;
}

/* Reactions */
.s-msg__rx { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 4px; }
/* Your own bubble hangs off the right edge, so its reactions have to as
   well — otherwise a single chip sits under the middle of the bubble and
   reads as belonging to nothing. The meta row already flips this way. */
.s-msg--me .s-msg__rx { justify-content: flex-end; }
.s-rx {
  display: inline-flex; align-items: center; gap: 3px;
  padding: 1px 7px; border-radius: var(--s-r-pill);
  background: var(--s-surface-2); border: 1px solid var(--s-line);
  font-size: var(--s-fs-sm); cursor: pointer;
}
.s-rx:hover { background: var(--s-line-soft); }
.s-rx--mine { background: var(--s-good-bg); border-color: var(--s-good-line); }
.s-rxpick {
  position: fixed; z-index: 60; display: none; gap: 2px;
  padding: 6px; border-radius: var(--s-r);
  background: var(--s-surface); border: 1px solid var(--s-line);
  box-shadow: var(--s-shadow-lg);
}
.s-rxpick--open { display: flex; }
.s-rxpick button {
  border: 0; background: none; cursor: pointer;
  font-size: 1.35rem; line-height: 1; padding: 4px; border-radius: var(--s-r-sm);
}
.s-rxpick button:hover { background: var(--s-surface-2); }

/* @mention inside a bubble. Painted by the client from already-escaped text,
   never from server HTML -- the note in the event chat template says why. */
/* A tinted chip rather than a colour alone: --s-brand follows the club's own
   primary, which on a dark bubble can land on the bubble's own colour and
   disappear. The chip reads on both grounds whatever the club has picked. */
.s-mention {
  padding: 1px 5px;
  border-radius: 5px;
  font-weight: 800;
  color: var(--s-info);
  background: var(--s-info-bg);
}

/* The @mention autocomplete. Anchored to the composer rather than to the
   caret: at 375px a caret-anchored menu is half off-screen half the time, and
   the composer edge is where the thumb already is. */
.s-mentions {
  position: absolute; left: var(--s-sp-3); right: var(--s-sp-3); bottom: 100%;
  z-index: 12; max-height: 190px; overflow-y: auto;
  margin-bottom: 6px; padding: var(--s-sp-1);
  background: var(--s-surface); border: 1px solid var(--s-line);
  border-radius: var(--s-r); box-shadow: var(--s-shadow-lg);
}
.s-mentions[hidden] { display: none; }
.s-mentions__i {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 10px; border: 0; border-radius: var(--s-r-sm);
  background: none; text-align: left; cursor: pointer;
}
.s-mentions__i:hover, .s-mentions__i.is-on { background: var(--s-surface-2); }
.s-mentions__av {
  flex: 0 0 auto; width: 28px; height: 28px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--s-slate-bg); color: var(--s-slate);
  font-size: var(--s-fs-xs); font-weight: 900;
}
.s-mentions__n {
  flex: 1 1 auto; min-width: 0;
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.s-mentions__r { flex: 0 0 auto; font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }

/* Polls rail. Sits under the presence strip because a live poll is the one
   thing in a chat that expires — it has to be seen without scrolling back. */
.s-chat__polls {
  flex: 0 0 auto;
  display: flex; align-items: stretch; gap: var(--s-sp-3);
  padding: var(--s-sp-2) var(--s-sp-4);
  background: var(--s-surface-2); border-bottom: 1px solid var(--s-line);
  overflow-x: auto; scrollbar-width: none;
}
.s-chat__polls::-webkit-scrollbar { display: none; }
.s-poll {
  flex: 0 0 auto; width: 250px;
  padding: 10px 12px; border-radius: var(--s-r);
  background: var(--s-surface); border: 1px solid var(--s-line);
}
.s-poll--closed { opacity: .7; }
.s-poll__q {
  margin-bottom: var(--s-sp-2);
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink); line-height: 1.3;
}
.s-poll__o {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 6px;
  width: 100%; margin-bottom: 6px; padding: 6px 10px;
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  background: var(--s-surface-2); color: var(--s-ink);
  font-size: var(--s-fs-sm); font-weight: 600; text-align: left; cursor: pointer;
}
.s-poll__o:disabled { cursor: default; }
/* Only the text sits above the fill. Lifting *every* child span out of the
   stack took the fill with it — no longer absolute, it became a flex item as
   wide as its own percentage and shoved the label across the row. */
.s-poll__tick, .s-poll__lb, .s-poll__n { position: relative; z-index: 1; }
/* The fill is a width the server computes, so JS writes it from data-bar. */
.s-poll__bar {
  position: absolute; left: 0; top: 0; bottom: 0; width: 0;
  background: var(--s-good-bg); transition: width .3s;
}
.s-poll__lb { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-poll__n { margin-left: auto; font-weight: 800; color: var(--s-ink-2); }
.s-poll__tick { display: none; color: var(--s-good); }
.s-poll__o--mine { border-color: var(--s-good); }
.s-poll__o--mine .s-poll__tick { display: inline; }
.s-poll__foot {
  display: flex; align-items: center; flex-wrap: wrap; gap: 5px;
  font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted);
}
.s-poll__end {
  margin-left: auto; padding: 2px 4px; border: 0; background: none;
  color: var(--s-bad); font-size: var(--s-fs-xs); font-weight: 800; cursor: pointer;
}
.s-poll__new {
  flex: 0 0 auto; align-self: center;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px; border-radius: var(--s-r-pill);
  border: 1px dashed var(--s-brand); background: transparent;
  color: var(--s-brand); font-size: var(--s-fs-sm); font-weight: 800;
  white-space: nowrap; cursor: pointer;
}
.s-poll__new:hover { background: var(--s-surface); }

/* Side panel — the people in this chat. Slides from the right on a phone,
   which is where a "who's here" list belongs: adjacent to the room, not
   replacing it. */
.s-side {
  position: fixed; top: 0; right: 0; bottom: 0; z-index: 70;
  width: min(360px, 100%);
  display: flex; flex-direction: column;
  background: var(--s-surface);
  box-shadow: var(--s-shadow-lg);
  transform: translateX(100%); transition: transform .28s cubic-bezier(.4, 0, .2, 1);
}
.s-side--open { transform: none; }
@media (prefers-reduced-motion: reduce) { .s-side { transition: none; } }
.s-side__hdr {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: var(--s-sp-2);
  padding: calc(env(safe-area-inset-top, 0px) + 12px) var(--s-sp-4) 12px;
  background: var(--s-brand); color: var(--s-brand-on);
}
.s-side__t { margin: 0; font-size: var(--s-fs-md); font-weight: 800; }
.s-side__x {
  margin-left: auto; width: 28px; height: 28px; border: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255, 255, 255, .16); color: var(--s-brand-on); cursor: pointer;
}
.s-side__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: var(--s-sp-2) var(--s-sp-4); }
.s-side__scrim {
  position: fixed; inset: 0; z-index: 69;
  background: rgba(16, 24, 40, .45);
}
.s-side__scrim[hidden] { display: none; }

/* Notification opt-in. A prompt, not a banner: it is asking for something.
   It sits in the flow rather than floating, because the only place it could
   float is on top of the presence strip — covering who is online to ask about
   notifications is the wrong trade. */
.s-chat__notif {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: var(--s-sp-3);
  padding: 8px var(--s-sp-4);
  background: var(--s-good-bg); border-bottom: 1px solid var(--s-line);
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink-2);
}
.s-chat__notif[hidden] { display: none; }
.s-chat__notif i { flex: 0 0 auto; color: var(--s-good); }
.s-chat__notif span { flex: 1 1 auto; min-width: 0; }
/* The button holds its label. A flex item shrinks in proportion to its own
   width, so this row was taking a few pixels off Enable as well as off the
   sentence, and a few pixels is all it takes: the word broke across two lines
   and the prompt got 15px taller for no reason. The sentence is the part with
   room to give. */
.s-chat__notif .s-btn { flex: 0 0 auto; }
.s-chat__notifx {
  flex: 0 0 auto; width: 26px; height: 26px; border: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; color: var(--s-muted); cursor: pointer;
}
.s-chat__notifx:hover { background: var(--s-line-soft); }

/* Archived. A room that has closed says so once, at the top, and then gets out
   of the way -- v1 dimmed the composer to 40% opacity and left it sitting
   there, which reads as broken rather than finished. Here the composer is not
   rendered at all and this strip is what explains the gap. */
.s-chat__arch {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: var(--s-sp-3);
  padding: 8px var(--s-sp-4);
  background: var(--s-warn-bg); border-bottom: 1px solid var(--s-warn-line);
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-warn);
  line-height: 1.45;
}
.s-chat__arch i { flex: 0 0 auto; }

/* Pushes what follows it to the far end of a flex row. The presence strip
   needs it: the People button belongs on the right whether or not there are
   avatars to its left. */
.s-chat__spacer { flex: 1 1 auto; }

/* Typing. A row between the thread and the composer that takes no height at
   all when nobody is typing. The group chat has no equivalent on purpose — a
   room of twelve people typing is noise; one-to-one is the only place where
   knowing the other person is mid-reply changes what you do next. */
.s-typing {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 7px;
  padding: 2px var(--s-sp-4) 6px;
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted);
}
.s-typing[hidden] { display: none; }
.s-typing__d { display: inline-flex; gap: 3px; }
.s-typing__d span {
  width: 5px; height: 5px; border-radius: 50%;
  background: var(--s-muted);
  animation: s-typing-b 1.2s infinite ease-in-out;
}
.s-typing__d span:nth-child(2) { animation-delay: .18s; }
.s-typing__d span:nth-child(3) { animation-delay: .36s; }
@keyframes s-typing-b {
  0%, 60%, 100% { opacity: .25; transform: translateY(0); }
  30%           { opacity: 1;   transform: translateY(-3px); }
}
@media (prefers-reduced-motion: reduce) {
  .s-typing__d span { animation: none; opacity: .55; }
}

/* "Seen" rides in the meta line beside the time. Green because it is the one
   part of that line that is good news, and still tiny because it is not the
   message. */
.s-msg__seen { color: var(--s-good); font-weight: 800; }

/* Composer. Pinned to the bottom, above the home indicator. */
.s-chat__foot {
  flex: 0 0 auto; position: relative;
  padding: var(--s-sp-2) var(--s-sp-3) calc(env(safe-area-inset-bottom, 0px) + var(--s-sp-2));
  background: var(--s-surface); border-top: 1px solid var(--s-line);
}
/* The composer slot when there is nothing to compose yet. A direct message
   needs the other person's consent first, so before that this strip holds
   either the two-button gate or the one-line request form instead. Accept and
   Decline get equal halves on purpose: they are two available choices, not a
   primary action and an escape hatch. */
.s-chat__gate { display: flex; align-items: center; gap: var(--s-sp-2); }
.s-chat__gate > .s-btn { flex: 1 1 0; justify-content: center; }
.s-chat__gate > .s-input { flex: 1 1 auto; min-width: 0; }
.s-chat__gate > .s-comp__send { flex: 0 0 auto; }
/* A line above the gate saying what the button will actually do. */
.s-chat__gatenote {
  display: block; margin-bottom: 6px;
  font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted); line-height: 1.45;
}

.s-comp { display: flex; flex-direction: column; }
.s-comp__row { display: flex; align-items: flex-end; gap: 6px; }
.s-comp__ta {
  flex: 1 1 auto; min-width: 0; resize: none;
  max-height: 120px; padding: 9px 12px;
  border: 1px solid var(--s-line); border-radius: var(--s-r-lg);
  background: var(--s-surface-2); color: var(--s-ink);
  font: inherit; font-size: 16px; /* 16px or iOS zooms the page on focus */
  line-height: 1.4;
}
.s-comp__ta:focus { outline: none; border-color: var(--s-brand); }
.s-comp__ic {
  flex: 0 0 auto; width: 36px; height: 36px; border-radius: var(--s-r-pill);
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--s-line); background: var(--s-surface-2);
  color: var(--s-ink-2); font-size: var(--s-fs-md); cursor: pointer;
}
.s-comp__ic:hover { background: var(--s-line-soft); }
.s-comp__ic--gif { font-size: var(--s-fs-xs); font-weight: 900; letter-spacing: -.02em; }
.s-comp__ic--img i { color: var(--s-good); }
.s-comp__ic input[type="file"] { display: none; }
/* Accent, not brand, and for a reason worth writing down: the brand colour is
   a near-black navy that dark mode does not restate, so on a dark composer the
   button was brand-on-surface -- two shades apart and effectively invisible.
   It is also the one primary action on the page, and every other primary
   action in the system is accent. Both arguments point the same way. */
.s-comp__send {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: var(--s-r-pill);
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: var(--s-accent); color: var(--s-accent-on);
  font-size: var(--s-fs-md); cursor: pointer;
}
.s-comp__send:disabled { opacity: .55; cursor: default; }

/* Emoji tray + GIF preview live above the composer row and push it up rather
   than float over it, so the thing you are about to send is never covered. */
.s-comp__tray {
  display: none; flex-wrap: wrap; gap: 2px;
  max-height: 168px; margin-bottom: var(--s-sp-2); padding: var(--s-sp-2);
  border: 1px solid var(--s-line); border-radius: var(--s-r);
  background: var(--s-surface-2); overflow-y: auto;
}
.s-comp__tray--open { display: flex; }
.s-comp__tray button {
  border: 0; background: none; cursor: pointer;
  font-size: 1.35rem; line-height: 1; padding: 4px; border-radius: var(--s-r-sm);
}
.s-comp__tray button:hover { background: var(--s-line-soft); }
.s-comp__gifprev {
  position: relative; display: none;
  margin-bottom: var(--s-sp-2);
}
.s-comp__gifprev--open { display: block; }
.s-comp__gifprev img { max-height: 120px; border-radius: var(--s-r-sm); display: block; }
.s-comp__gifx {
  position: absolute; top: 4px; left: 4px;
  width: 24px; height: 24px; border: 0; border-radius: 50%;
  background: rgba(16, 24, 40, .7); color: #fff; cursor: pointer; line-height: 1;
}
.s-comp__quick {
  display: flex; gap: 6px; margin-bottom: var(--s-sp-2);
  overflow-x: auto; scrollbar-width: none;
}
.s-comp__quick::-webkit-scrollbar { display: none; }
.s-comp__quick button {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; gap: 5px;
  padding: 5px 11px; border-radius: var(--s-r-pill);
  border: 1px solid var(--s-line); background: var(--s-surface-2);
  color: var(--s-ink-2); font-size: var(--s-fs-sm); font-weight: 700;
  white-space: nowrap; cursor: pointer;
}
.s-comp__quick button:hover { background: var(--s-line-soft); }

/* GIF picker — a bottom sheet, built by JS, styled here. */
.s-gifpick { position: fixed; inset: 0; z-index: 80; display: flex; align-items: flex-end; justify-content: center; background: rgba(16, 24, 40, .6); }
.s-gifpick__panel {
  display: flex; flex-direction: column;
  width: 100%; max-width: 480px; max-height: 70vh;
  background: var(--s-surface); border-radius: var(--s-r-lg) var(--s-r-lg) 0 0;
}
.s-gifpick__hdr { display: flex; align-items: center; gap: var(--s-sp-2); padding: var(--s-sp-3) var(--s-sp-4); border-bottom: 1px solid var(--s-line); }
.s-gifpick__hdr input {
  flex: 1 1 auto; min-width: 0; padding: 8px 14px;
  border: 1px solid var(--s-line); border-radius: var(--s-r-pill);
  background: var(--s-surface-2); color: var(--s-ink); font: inherit; font-size: 16px;
}
.s-gifpick__hdr input:focus { outline: none; border-color: var(--s-brand); }
.s-gifpick__x { border: 0; background: none; color: var(--s-ink-2); font-size: 1.2rem; cursor: pointer; }
.s-gifpick__grid { flex: 1 1 auto; display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s-sp-2); padding: var(--s-sp-3); overflow-y: auto; }
.s-gifpick__grid img { width: 100%; border-radius: var(--s-r-sm); cursor: pointer; display: block; }
/* A result you click is a button. v1 hung the handler on the <img>, which put
   the whole picker out of reach of a keyboard. */
.s-gifpick__grid button {
  padding: 0; border: 0; background: none; cursor: pointer;
  border-radius: var(--s-r-sm); overflow: hidden;
}
.s-gifpick__grid button:focus-visible { outline: 2px solid var(--s-accent); outline-offset: 2px; }
.s-gifpick__msg { grid-column: 1 / -1; padding: var(--s-sp-5); text-align: center; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-muted); }
.s-gifpick__msg--bad { color: var(--s-bad); }
.s-gifpick__by { padding: var(--s-sp-2) var(--s-sp-4); text-align: center; font-size: var(--s-fs-xs); color: var(--s-muted); }

/* New-poll sheet. Bottom sheet on a phone, centred dialog from tablet up. */
.s-sheetm { position: fixed; left: 0; right: 0; bottom: 0; z-index: 81; max-height: 85vh; overflow-y: auto; padding: var(--s-sp-4) var(--s-sp-4) calc(var(--s-sp-4) + env(safe-area-inset-bottom, 0px)); background: var(--s-surface); border-top: 1px solid var(--s-line); border-radius: var(--s-r-lg) var(--s-r-lg) 0 0; box-shadow: var(--s-shadow-lg); }
.s-sheetm[hidden], .s-sheetm__scrim[hidden] { display: none; }
.s-sheetm__scrim { position: fixed; inset: 0; z-index: 80; background: rgba(16, 24, 40, .5); }
.s-sheetm__t { margin: 0 0 4px; font-size: var(--s-fs-lg); font-weight: 800; color: var(--s-ink); }
/* A sheet long enough to scroll needs its exit at the top, not after 20 rows. */
.s-sheetm__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--s-sp-3); }
.s-sheetm__x {
  flex: 0 0 auto; width: 32px; height: 32px;
  border: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--s-surface-2); color: var(--s-muted);
  font-size: 15px; cursor: pointer;
}
.s-sheetm__x:hover { color: var(--s-ink); }
.s-sheetm__s { margin: 0 0 var(--s-sp-3); font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }
@media (min-width: 768px) {
  .s-sheetm { left: 50%; right: auto; bottom: auto; top: 50%; width: 420px; transform: translate(-50%, -50%); border: 1px solid var(--s-line); border-radius: var(--s-r-lg); }
}

/* Install prompt — the "add to your phone" sheet. Markup in
   templates/includes/v2_install_prompt.html, behaviour in
   static/js/sprout-pwa-install.js. Bottom sheet on a phone, centred dialog
   from tablet up, same split as .s-sheetm. It outranks every other overlay
   because it is the only one that invites itself: nothing may sit on top of
   a prompt the person did not ask for while they try to dismiss it. */
.s-install[hidden], .s-install [hidden] { display: none !important; }
.s-install__scrim { position: fixed; inset: 0; z-index: 94; background: rgba(16, 24, 40, .55); }
.s-install__sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 95;
  max-height: 85vh; overflow-y: auto;
  /* Side + bottom insets: on a notched iPhone the sheet reaches the screen
     edge, so its padding has to clear the home indicator and, in landscape,
     the cutout. */
  padding: var(--s-sp-5)
           calc(var(--s-sp-4) + env(safe-area-inset-right, 0px))
           calc(var(--s-sp-4) + env(safe-area-inset-bottom, 0px))
           calc(var(--s-sp-4) + env(safe-area-inset-left, 0px));
  background: var(--s-surface);
  border-top: 1px solid var(--s-line);
  border-radius: var(--s-r-lg) var(--s-r-lg) 0 0;
  box-shadow: var(--s-shadow-lg);
  animation: s-install-up .3s ease-out;
}
@keyframes s-install-up {
  from { transform: translateY(24px); opacity: 0; }
  to { transform: none; opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .s-install__sheet { animation: none; }
}
.s-install__x {
  position: absolute; top: var(--s-sp-3); right: calc(var(--s-sp-3) + env(safe-area-inset-right, 0px));
  width: 32px; height: 32px;
  border: 0; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--s-surface-2); color: var(--s-muted);
  font-size: 15px; cursor: pointer;
}
.s-install__x:hover { color: var(--s-ink); }
.s-install__head { text-align: center; padding: 0 var(--s-sp-4); }
.s-install__crest { width: 56px; height: 56px; object-fit: contain; margin-bottom: var(--s-sp-2); }
.s-install__title { margin: 0 0 4px; font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink); }
.s-install__sub { margin: 0; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }
.s-install__benefits { list-style: none; margin: var(--s-sp-4) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-sp-3); }
.s-install__benefits li { display: flex; align-items: flex-start; gap: var(--s-sp-3); }
.s-install__ico {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: 12px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--s-surface-2); color: var(--s-ink);
  font-size: 15px;
}
.s-install__btext { min-width: 0; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }
/* Benefit rows are a heading over a line; step text is a sentence with the
   button name emphasised mid-flow. Only the first gets block strongs. */
.s-install__benefits .s-install__btext strong { display: block; font-weight: 800; color: var(--s-ink); }
.s-install__steps .s-install__btext strong { font-weight: 800; color: var(--s-ink); }
/* Numbered by counter, not hand-typed digits: the "open Safari first" step is
   hidden for people already in Safari, and a hidden li generates no box so it
   does not increment — the visible sequence is always 1, 2, 3. */
.s-install__steps { list-style: none; counter-reset: s-install-step; margin: var(--s-sp-4) 0 0; padding: 0; display: flex; flex-direction: column; gap: var(--s-sp-3); }
.s-install__steps li { display: flex; align-items: flex-start; gap: var(--s-sp-3); counter-increment: s-install-step; }
.s-install__steps li::before {
  content: counter(s-install-step);
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--s-accent); color: var(--s-accent-on);
  font-size: var(--s-fs-xs); font-weight: 900;
}
.s-install__cta { display: flex; flex-direction: column; gap: var(--s-sp-2); margin-top: var(--s-sp-4); }
.s-install__never {
  display: block; width: 100%; margin-top: var(--s-sp-3); padding: 6px;
  border: 0; background: none;
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted);
  text-decoration: underline; text-align: center; cursor: pointer;
}
.s-install__never:hover { color: var(--s-ink); }
.s-install__x:focus-visible, .s-install__never:focus-visible { outline: 2px solid var(--s-accent); outline-offset: 2px; }
@media (min-width: 768px) {
  .s-install__sheet { left: 50%; right: auto; bottom: auto; top: 50%; width: 440px; transform: translate(-50%, -50%); border: 1px solid var(--s-line); border-radius: var(--s-r-lg); animation: none; }
  .s-install__x { right: var(--s-sp-3); }
}

/* People rows in the side panel. Built by JS from the presence poll, so the
   markup lives in a string — every value here, none of it inline. */
.s-prow { display: flex; align-items: center; gap: var(--s-sp-3); padding: 10px 0; border-bottom: 1px solid var(--s-line-soft); }
.s-prow:last-child { border-bottom: 0; }
/* A row whose body runs to several lines -- a coach's name, the job they hold
   and then a wrapped run of compliance chips. Centred, the avatar and the
   action float halfway down a five-line row and stop reading as that row's.
   Same modifier, same job, as .s-row--top. */
.s-prow--top { align-items: flex-start; }
.s-prow__av {
  position: relative; flex: 0 0 auto;
  width: 38px; height: 38px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--s-slate-bg); color: var(--s-slate);
  font-size: var(--s-fs-sm); font-weight: 900;
}
.s-prow__dot {
  position: absolute; right: -1px; bottom: -1px;
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--s-good); border: 2px solid var(--s-surface);
}
.s-prow__dot--off { background: var(--s-muted); }
.s-prow__txt { flex: 1 1 auto; min-width: 0; }
.s-prow__n { font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink); }
.s-prow__s { font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted); }
/* A people row can carry an action. In an event chat that is the door to a
   private thread with that person; in the group chat there is none. */
.s-prow__act { flex: 0 0 auto; }
.s-side__empty { padding: var(--s-sp-5) 0; text-align: center; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-muted); }

/* ==========================================================================
   16. MESSAGE LIST
   The chat lobby is a list, not a conversation, so it reuses .s-row rather
   than inventing a row of its own. These are the two things a list of
   conversations needs that a settings list does not: a right-hand column
   holding when it last moved, and a count of what you have not read.
   ========================================================================== */
/* flex: 0 1 auto, not 0 0 auto. This column holds a time or a chip, and
   holding it rigid is right up until the thing inside is wider than the
   screen: then the row cannot shrink, so the card cannot, so the page grows
   and the whole app sits further right than the phone. A chip carrying a
   club's own season name did exactly that, 456px of document in a 375px
   screen, and it was the last box the wrap rules above could not reach,
   because no amount of overflow-wrap helps a box that has been told never to
   give up width. Shrinking stops at 44px and only starts once the row has run
   out of room, so every row holding a short time or a two-word chip, which is
   all of them today, is unchanged. */
.s-row__end {
  flex: 0 1 auto;
  display: flex; flex-direction: column; align-items: flex-end; gap: 5px;
  min-width: 44px;
}
.s-row__when { font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); white-space: nowrap; }
.s-row__n {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 20px; height: 20px; padding: 0 6px;
  border-radius: var(--s-r-pill);
  background: var(--s-brand); color: #fff;
  font-size: var(--s-fs-xs); font-weight: 900;
  font-variant-numeric: tabular-nums;
}
.s-row__n--bad { background: var(--s-bad); }

/* An amount at the end of a row. Money is the one thing on a page people read
   DOWN rather than across, so it is set in tabular figures -- with
   proportional digits £9.00 and £11.00 come out nearly the same width and two
   different amounts look alike in a scan. The --due variant is a figure nobody
   has collected yet: it is what is owed, not what arrived, and it should not
   carry the same weight as money that is in the bank. */
.s-amt {
  font-size: var(--s-fs-md); font-weight: 900; color: var(--s-ink);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.s-amt--due { color: var(--s-muted); }
/* Unread reads as weight, not colour — colour is already carrying the count. */
.s-row--unread .s-row__title { font-weight: 900; }
.s-row--unread .s-row__sub { color: var(--s-ink); font-weight: 700; }
.s-row.is-hidden { display: none; }

/* A conversation preview is two lines at most. The view hands over 60 characters
   of the last message, which at 375px is two lines with the sender's name in
   front of it — but a long name plus a wide screen's worth of words could make
   one row twice the height of its neighbours, and a list you scan should have a
   steady rhythm. Only rows carrying a real message get this; a .s-row__sub that
   is a fixed description elsewhere is allowed to run as long as it needs to. */
.s-row--msg .s-row__sub {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}


/* ── 17. CHARTS ────────────────────────────────────────────────────────────
   One page in this app draws a graph, and to draw ten points on a phone it
   pulled 200KB of charting library off a public CDN — inside a product that
   offers to install itself and claims to work on a bad touchline signal.
   Everything below draws the same three graphs from one <svg> and about
   ninety lines of script, offline, in the app's own colours.

   Geometry is written by the page as attributes, because a line's shape is
   data and changes per player. What lives here is only what a stylesheet
   should own: colour, weight, and type. */
.s-chart { width: 100%; min-width: 0; }
.s-chart svg { display: block; width: 100%; height: auto; }
.s-chart__grid { stroke: var(--s-line-soft); stroke-width: 1; }
.s-chart__axis { stroke: var(--s-line); stroke-width: 1; }
/* The dashed reference line is the season's average. Muted on purpose: it is
   the thing the real line is being read against, not a second result. */
.s-chart__avg  { fill: none; stroke: var(--s-muted); stroke-width: 1.5; stroke-dasharray: 6 5; }
.s-chart__area { fill: var(--s-info-bg); }
.s-chart__line { fill: none; stroke: var(--s-info); stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
.s-chart__pt   { fill: var(--s-surface); stroke: var(--s-info); stroke-width: 2.5; }
.s-chart__bar  { fill: var(--s-info); }
.s-chart__bar--good { fill: var(--s-good); }
.s-chart__bar--warn { fill: var(--s-warn); }
/* 10px rather than a token, and set on the SVG rather than inherited: the
   chart scales with the card, and axis text that scaled with it would be a
   different size on a phone and a laptop. */
.s-chart__t {
  font-size: 10px; font-weight: 800; fill: var(--s-muted);
  font-variant-numeric: tabular-nums;
}
.s-chart__t--y { text-anchor: end; }
.s-chart__t--x { text-anchor: middle; }

/* ── 18. THE PHOTO GALLERY ─────────────────────────────────────────────────
   Photos are the one screen where the content is the interface. v1 wrapped
   every picture in a card with a caption, a team, an uploader, a timestamp
   and two icon buttons — five lines of chrome around a 220px thumbnail, and
   on a phone that meant one photo per screenful with the picture squeezed
   between its own metadata. Here the grid is only pictures, two-up on a
   phone, and everything that was printed under each tile is shown once, in
   the lightbox, for the photo you actually opened. */

/* A dialog with more than a confirmation in it: the head and the submit bar
   stay put and the middle scrolls, so the button that finishes the job is
   never below the fold on a phone. [open] is on the selector because a bare
   display:flex would defeat the element's own hidden state. */
.s-dialog--tall { padding: 0; max-height: calc(100dvh - 20px); }
.s-dialog--tall[open] { display: flex; flex-direction: column; }
.s-dialog--tall .s-dialog__head { flex: 0 0 auto; margin: 0; padding: 16px 18px 8px; }
/* The form is usually the thing that has to scroll, so it inherits the
   column layout rather than sitting between it and the body as a blocker. */
.s-dialog--tall > form { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.s-dialog__body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  -webkit-overflow-scrolling: touch; padding: 0 18px 4px;
  /* The sheet's own rhythm, so the sections in it read as sections rather than
     as one continuous column of labels. */
  display: flex; flex-direction: column; gap: var(--s-sp-4);
}
.s-dialog__foot {
  flex: 0 0 auto; display: flex; gap: 8px; justify-content: flex-end;
  padding: 12px 18px calc(12px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--s-line); background: var(--s-surface);
}

/* ── 18a. The photo grid ───────────────────────────────────────────────── */
.gal-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 8px; }
.gal-item {
  position: relative; display: block; width: 100%; padding: 0;
  aspect-ratio: 1; overflow: hidden; cursor: pointer;
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  background: var(--s-surface-2);
}
/* Crop biased upwards. A centre crop of a portrait phone photo takes the top
   off everybody's head, which is where the faces are. */
.gal-item img { width: 100%; height: 100%; object-fit: cover; object-position: center 32%; display: block; }
.gal-item__cap {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 18px 8px 6px;
  background: linear-gradient(to top, rgba(2, 6, 23, .78), transparent);
  color: #fff; font-size: var(--s-fs-xs); font-weight: 700; text-align: left;
  line-height: 1.3; overflow: hidden; display: -webkit-box;
  -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.gal-item__tag {
  position: absolute; top: 6px; left: 6px; padding: 2px 7px;
  border-radius: var(--s-r-pill); background: rgba(2, 6, 23, .62);
  color: #fff; font-size: 10px; font-weight: 800;
}
.gal-item__cam { position: absolute; top: 6px; right: 6px; color: var(--s-warn); font-size: 11px; }

/* ── 18b. Album tiles ──────────────────────────────────────────────────── */
/* An album is a button, not a div with an onclick: it is reachable by tab,
   answers the space bar, and tells a screen reader whether it is open. */
.gal-albs { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; }
.gal-alb {
  position: relative; display: block; width: 100%; padding: 0;
  aspect-ratio: 4 / 3; overflow: hidden; cursor: pointer; text-align: left;
  border: 1px solid var(--s-line); border-radius: var(--s-r); background: var(--s-surface-2);
}
.gal-alb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gal-alb__ov {
  position: absolute; inset: 0; display: flex; flex-direction: column; justify-content: flex-end;
  padding: 10px; background: linear-gradient(to top, rgba(2, 6, 23, .8) 0%, rgba(2, 6, 23, .1) 58%, transparent 100%);
}
.gal-alb__n { color: #fff; font-size: var(--s-fs-sm); font-weight: 800; line-height: 1.2; }
.gal-alb__m { margin-top: 2px; color: rgba(255, 255, 255, .8); font-size: var(--s-fs-xs); font-weight: 600; }
.gal-alb__c {
  position: absolute; top: 7px; right: 7px; padding: 2px 8px;
  border-radius: var(--s-r-pill); background: rgba(2, 6, 23, .65);
  color: #fff; font-size: var(--s-fs-xs); font-weight: 800;
}
.gal-alb__x {
  position: absolute; right: 10px; bottom: 11px; color: rgba(255, 255, 255, .8);
  font-size: 13px; transition: transform .2s ease;
}
.gal-alb[aria-expanded="true"] { border-color: var(--s-accent); box-shadow: 0 0 0 2px var(--s-accent); }
.gal-alb[aria-expanded="true"] .gal-alb__x { transform: rotate(90deg); }

/* The opened album. One is open at a time, and it sits directly under the row
   of tiles rather than being moved through the DOM to get there. */
.gal-panel[hidden] { display: none; }
.gal-panel__h {
  display: flex; align-items: flex-start; gap: var(--s-sp-3);
  padding: var(--s-sp-3) var(--s-sp-4); border-bottom: 1px solid var(--s-line);
}
.gal-panel__h .s-btn { margin-left: auto; flex: none; }
.gal-panel__t { font-size: var(--s-fs-md); font-weight: 800; color: var(--s-ink); }
.gal-panel__s { margin-top: 2px; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }
.gal-panel__b { padding: var(--s-sp-3); }

/* ── 18c. The lightbox, with the detail in it ──────────────────────────── */
/* Reuses the feed's overlay, close button, arrows and counter. What is new is
   that the photo carries its own caption block: v1 printed team, fixture,
   photographer and time under every tile in the grid, where it competed with
   the picture, and then showed none of it once you opened the photo. */
.lb-fig { display: flex; flex-direction: column; align-items: center; gap: 10px; max-width: 94vw; margin: 0; }
.feed-lightbox .lb-fig img { max-width: 94vw; max-height: 66dvh; border-radius: var(--s-r-sm); object-fit: contain; }
.lb-cap { max-width: 620px; text-align: center; color: rgba(255, 255, 255, .82); font-size: var(--s-fs-sm); font-weight: 600; line-height: 1.45; }
.lb-cap b { display: block; margin-bottom: 3px; color: #fff; font-size: var(--s-fs-md); font-weight: 800; }
.lb-cap .lb-credit { display: block; margin-top: 3px; color: var(--s-warn); font-weight: 800; }
.lb-acts { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.lb-acts .s-btn {
  border-color: rgba(255, 255, 255, .3); background: rgba(255, 255, 255, .1); color: #fff;
}
.lb-acts .s-btn--bad { border-color: rgba(248, 113, 113, .55); color: #fca5a5; }

/* ── 18d. Choosing photos to upload ────────────────────────────────────── */
.up-drop {
  display: block; width: 100%; padding: 26px 16px; margin-bottom: 10px;
  border: 2px dashed var(--s-line); border-radius: var(--s-r);
  background: var(--s-surface-2); color: inherit; text-align: center; cursor: pointer;
}
.up-drop:hover, .up-drop.is-over { border-color: var(--s-accent); background: var(--s-info-bg); }
.up-drop i { font-size: 1.9rem; color: var(--s-accent); }
.up-drop b { display: block; margin: 7px 0 2px; font-size: var(--s-fs-md); font-weight: 800; color: var(--s-ink); }
.up-drop span { font-size: var(--s-fs-xs); font-weight: 600; color: var(--s-muted); }
.up-review[hidden] { display: none; }
.up-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 7px; }
.up-top b { font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink); }
.up-top span { font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-muted); font-variant-numeric: tabular-nums; }
.up-stage {
  position: relative; display: flex; align-items: center; justify-content: center;
  aspect-ratio: 4 / 3; overflow: hidden; border-radius: var(--s-r); background: #0b1220;
}
.up-stage img { width: 100%; height: 100%; object-fit: contain; }
.up-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 38px; height: 38px; border: 0; border-radius: 50%;
  background: rgba(15, 23, 42, .55); color: #fff; cursor: pointer;
}
.up-nav.is-prev { left: 9px; } .up-nav.is-next { right: 9px; }
.up-nav:disabled { opacity: 0; pointer-events: none; }
.up-x {
  position: absolute; top: 9px; right: 9px; width: 32px; height: 32px;
  border: 0; border-radius: 50%; background: rgba(15, 23, 42, .6); color: #fff; cursor: pointer;
}
.up-thumbs { display: flex; gap: 6px; overflow-x: auto; -webkit-overflow-scrolling: touch; padding: 9px 0 2px; }
.up-thumb {
  width: 48px; height: 48px; flex-shrink: 0; padding: 0; cursor: pointer;
  border: 2px solid transparent; border-radius: 9px; opacity: .6; background: none;
}
.up-thumb img { width: 100%; height: 100%; object-fit: cover; border-radius: 7px; display: block; }
.up-thumb.is-on { border-color: var(--s-accent); opacity: 1; }
.up-thumb.has-cap { box-shadow: inset 0 0 0 2px var(--s-warn); }
.up-add {
  width: 48px; height: 48px; flex-shrink: 0; cursor: pointer;
  border: 1px dashed var(--s-line); border-radius: 9px;
  background: var(--s-surface); color: var(--s-muted);
}

/* ── 18e. Typeahead combobox ───────────────────────────────────────────── */
/* "What are these photos from?" — a searchable list of real calendar items.
   Multi-pick: every choice becomes a removable chip carrying its own hidden
   input, so one batch of photos can sit under several matches or albums. */
.s-combo { position: relative; }
.s-combo__menu {
  position: absolute; left: 0; right: 0; top: calc(100% + 3px); z-index: 30;
  max-height: 240px; overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 4px;
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  background: var(--s-surface); box-shadow: var(--s-shadow-lg);
}
.s-combo__menu[hidden] { display: none; }
.s-combo__opt {
  padding: 8px 10px; border-radius: 7px; cursor: pointer;
  font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink);
}
.s-combo__opt[hidden] { display: none; }
.s-combo__opt:hover, .s-combo__opt.is-active { background: var(--s-info-bg); }
.s-combo__opt.is-new { color: var(--s-accent); font-weight: 800; }
.s-combo__opt.is-picked { background: var(--s-good-bg); color: var(--s-good); font-weight: 800; }
.s-combo__opt.is-picked::after { content: " ✓"; }
.s-combo__grp {
  padding: 7px 10px 2px; font-size: var(--s-fs-xs); font-weight: 800;
  text-transform: uppercase; letter-spacing: .05em; color: var(--s-muted);
}
.s-combo__grp[hidden] { display: none; }
.s-combo__empty { padding: 10px; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-muted); }
.s-combo__empty[hidden] { display: none; }

/* A chosen thing, with a way to un-choose it. */
.s-tags { display: flex; flex-wrap: wrap; gap: 7px; }
.s-tags:empty { display: none; }
.s-tag {
  display: inline-flex; align-items: center; gap: 5px; max-width: 100%;
  padding: 5px 6px 5px 11px; border-radius: var(--s-r-pill);
  border: 1px solid var(--s-info-line); background: var(--s-info-bg);
  color: var(--s-info); font-size: var(--s-fs-xs); font-weight: 800;
}
.s-tag__t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-tag__x {
  border: 0; background: none; padding: 0 3px; line-height: 1;
  font-size: 1.05rem; color: inherit; opacity: .7; cursor: pointer;
}
.s-tag__x:hover { opacity: 1; color: var(--s-bad); }

/* A chip that IS a checkbox. Lights from the input's own state, so it is
   right before any script runs and stays right if none ever does. */
.s-chkchips { display: flex; flex-wrap: wrap; gap: 8px; }
.s-chkchip {
  position: relative; display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 13px; margin: 0; cursor: pointer; user-select: none;
  border: 1.5px solid var(--s-line); border-radius: var(--s-r-pill);
  background: var(--s-surface); color: var(--s-ink-2);
  font-size: var(--s-fs-sm); font-weight: 700;
}
.s-chkchip input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.s-chkchip i { display: none; font-size: .7rem; }
.s-chkchip:has(input:checked), .s-chkchip.is-on {
  border-color: var(--s-good-line); background: var(--s-good-bg); color: var(--s-good);
}
.s-chkchip:has(input:checked) i, .s-chkchip.is-on i { display: inline; }
.s-chkchip:focus-within { outline: 2px solid var(--s-accent); outline-offset: 2px; }

/* ── The emergency dials ────────────────────────────────────────────────
   999 and 111. This is the one place in the app where a control is sized
   for somebody whose hands are shaking rather than for the layout, so the
   number is set large enough to hit without aiming and nothing around it
   competes. The pair sit side by side at every width: the muscle memory
   should be the same on a phone at a pitch and on a laptop in a clubhouse.

   The red is --s-bad rather than a club colour. 999 is not branding, and a
   club whose scheme happens to be red would otherwise lose the one visual
   distinction on the page that matters. */
.s-dials { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-sp-2); }
.s-dial {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 3px; min-height: 96px;
  padding: var(--s-sp-3) var(--s-sp-2);
  border: 2px solid transparent; border-radius: var(--s-r);
  text-align: center; text-decoration: none;
}
.s-dial__n { font-size: 2rem; font-weight: 900; line-height: 1; font-variant-numeric: tabular-nums; }
.s-dial__k { font-size: var(--s-fs-xs); font-weight: 800; line-height: 1.3; }
.s-dial--now { background: var(--s-bad); color: #fff; box-shadow: var(--s-shadow); }
.s-dial--now .s-dial__k { opacity: .92; }
.s-dial--ask { background: var(--s-surface); border-color: var(--s-line); color: var(--s-ink); }
.s-dial--ask .s-dial__k { color: var(--s-ink-2); }
.s-dial:active { transform: scale(.985); }

/* A value that exists to be read out loud to somebody else -- a postcode to
   an ambulance call handler. Set large, spaced, and in tabular figures,
   because it gets read a character at a time, under pressure, off a phone
   held at arm's length by someone who is also watching an injured child. */
.s-readout {
  margin-top: var(--s-sp-2);
  font-size: 1.7rem; font-weight: 900; line-height: 1.15;
  letter-spacing: .06em; color: var(--s-ink);
  font-variant-numeric: tabular-nums; overflow-wrap: anywhere;
}

/* -- A formset row you have changed your mind about ----------------------
   The emergency contacts editor is the first surface here that edits a
   Django formset, and a formset cannot have rows taken out of it: the
   indexes have to stay contiguous, so "remove" means ticking a hidden DELETE
   box and leaving the row where it is. v1 greyed it to opacity .35 and
   disabled the button, which made the tick a one-way door -- press it by
   mistake and the only way back was a reload that lost every other unsaved
   edit on the page. Here the row says what is about to happen to it and the
   button toggles, so changing your mind costs nothing. */
.s-rowset__r--off { opacity: .62; }
.s-rowset__r--off .s-input { text-decoration: line-through; }
.s-rowset__gone { display: none; }
.s-rowset__r--off .s-rowset__gone {
  display: block; grid-column: 1 / -1;
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-bad);
}
.s-rowset__gone i { margin-right: 5px; }

/* Equal columns, rather than the base rule's fixed 12rem lead. A kit is a
   name and then its colours, so a narrow first column is right there. A
   person is a name, a role and a number and none of the three is the one the
   others hang off -- giving the optional middle field the widest box would be
   backwards.

   Two, not three. These rows live inside .s-narrow, which is capped at 640px
   however wide the window gets, so thirds are ~145px at every size above the
   breakpoint -- narrow enough that "Pulford Rec groundsman" is still cut off
   on a laptop, which rather defeats going wide in the first place. Halves
   leave the third field on a second row and everything readable. */
.s-rowset__f--even { grid-template-columns: minmax(0, 1fr); }
@media (min-width: 560px) {
  .s-rowset__f--even { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ── A yes/no that has to be answered ───────────────────────────────
   Consent, written down by one person on behalf of another. Two radios and
   not one checkbox, because a checkbox has no way of saying no: an unticked
   box means "they refused" and "nobody got round to asking" at the same
   time, and a consent record that cannot tell those apart is not a record
   of anything. Both are marked required, so the page will not move on until
   each question has an answer one way or the other.

   The radio stays visible, where .s-chkchip hides its checkbox behind the
   chip. This is the one control on the page whose state somebody may have
   to stand behind a year later, so it is worth drawing in the form that
   everybody already knows how to read.

   Green for given and amber for not — amber rather than red, because
   refusing is an answer and not a mistake. What it changes is what the club
   is allowed to do, which is worth seeing at a glance, and that is all. */
.s-yn { display: flex; gap: 8px; }
.s-yn__o {
  flex: 1; display: inline-flex; align-items: center; gap: 8px;
  margin: 0; padding: 11px 12px; cursor: pointer;
  border: 1.5px solid var(--s-line); border-radius: var(--s-r);
  background: var(--s-surface); color: var(--s-ink-2);
  font-size: var(--s-fs-sm); font-weight: 800;
}
.s-yn__o input {
  width: 18px; height: 18px; margin: 0; flex-shrink: 0;
  accent-color: currentColor;
}
.s-yn__o:has(input:checked) {
  border-color: var(--s-good-line); background: var(--s-good-bg); color: var(--s-good);
}
.s-yn__o--no:has(input:checked) {
  border-color: var(--s-warn-line); background: var(--s-warn-bg); color: var(--s-warn);
}
.s-yn__o:focus-within { outline: 2px solid var(--s-accent); outline-offset: 2px; }

/* -- A photograph, picked on a form ---------------------------------------
   The old add-player page drew this as a <label> with the file input set to
   `display: none`. That does hide it, and it also takes it out of the tab
   order — so on a form where every other field is reachable by keyboard, the
   one field that opens a file dialog was not. .s-sr-only hides it and leaves
   it focusable, and the label rings when it holds focus.

   The circle is the camera until a picture is chosen and the picture after
   that, which is the same has-photo convention as .s-imp__pic on a bulk-import
   row. That one is 43px and sits in a grid of fifteen; this is the single
   photo on a page about one player, so it is big enough to see the face. */
.s-photo {
  display: flex; align-items: center; gap: var(--s-sp-3);
  margin: 0; cursor: pointer;
}
.s-photo__lab { display: inline-flex; flex-shrink: 0; border-radius: 50%; }
.s-photo__lab .s-avatar { width: 64px; height: 64px; font-size: 21px; color: var(--s-slate); }
.s-photo:focus-within .s-photo__lab { outline: 2px solid var(--s-accent); outline-offset: 3px; }
.s-photo__lab img { display: none; }
.s-photo.has-photo .s-photo__lab img { display: block; }
.s-photo.has-photo .s-photo__lab i { display: none; }
.s-photo.has-photo .s-avatar { border-color: var(--s-brand); }
.s-photo__body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }


/* ── 15. PRINT ───────────────────────────────────────────────── */
/* Some things on these pages end up on a wall: a QR for the clubhouse
   noticeboard, a squad list for a coach's clipboard. Printing the page as it
   stands wraps the header, the drawer and the fixed bottom bar around it, and
   the QR comes out at the 148px it is on screen -- unreadable at arm's length.

   So printing shows one element and nothing else. Anything meant for the
   sheet goes inside .s-poster; anything inside it that is a screen control
   (a Print button, most obviously) gets .s-noprint.

   visibility rather than display, because hiding the ancestors outright would
   take .s-poster down with them. Hidden ancestors keep their boxes, which is
   why .s-poster is lifted out of the flow to the top of the sheet. */
@media print {
  .s-v2 body * { visibility: hidden; }
  .s-poster, .s-poster * { visibility: visible; }
  .s-poster {
    position: absolute; top: 0; left: 0;
    width: 100%; padding-top: 40px;
    text-align: center;
  }
  .s-poster .s-qr { width: 320px; height: 320px; margin: 0 auto; border: none; }
  .s-poster .s-noprint, .s-noprint { display: none; }
}

/* A tall choice card: a title, a sentence explaining it, and one of them on.
   .s-opt is the chip row — a word each, laid out inline. This is for the
   handful of places where the choice needs a sentence to be honest about what
   it does, and it stacks rather than sitting in columns because at 375px two
   columns of prose is four words a line. Works two ways: a <button> the page
   turns on with .is-on (state lives in a hidden input), or a <label> wrapping
   a real radio, which lights itself. */
.s-pickcards { display: flex; flex-direction: column; gap: 8px; }
.s-pickcard {
  display: block; width: 100%; text-align: left; cursor: pointer;
  margin: 0; padding: 12px 13px;
  background: var(--s-surface); color: inherit;
  border: 2px solid var(--s-line); border-radius: var(--s-r);
  transition: border-color .15s, background .15s;
}
.s-pickcard:hover { border-color: var(--s-good-line); }
.s-pickcard.is-on,
.s-pickcard:has(input:checked) {
  border-color: var(--s-good-line); background: var(--s-good-bg);
}
.s-pickcard__t {
  display: block; font-size: var(--s-fs-md); font-weight: 800;
  color: var(--s-ink);
}
.s-pickcard__d {
  display: block; margin-top: 3px; font-size: var(--s-fs-sm);
  font-weight: 600; color: var(--s-ink-2); line-height: 1.4;
}
.s-pickcard input { position: absolute; opacity: 0; pointer-events: none; }
.s-pickcard:focus-visible,
.s-pickcard:has(input:focus-visible) {
  outline: 2px solid var(--s-good-line); outline-offset: 2px;
}

/* ---------------------------------------------------------------------------
   Stacked bars. .s-bar puts key, track and value on one line and clips the
   value at 54px, which is right for "12 min" and wrong for "60 min · 3
   matches". This modifier gives the sentence a line of its own above a
   full-width track — the same component, folded for a 375px screen.
--------------------------------------------------------------------------- */
.s-bars--stack .s-bar { display: block; }
.s-bars--stack .s-bar__hd {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 8px; margin-bottom: 5px;
}
.s-bars--stack .s-bar__k { width: auto; flex: 1; }
.s-bars--stack .s-bar__v { width: auto; }

/* ---------------------------------------------------------------------------
   Lineup grid — a player, the minutes that follow from the boxes ticked, and
   one small picker per period of the match. The pickers are ordinary
   .form-select/.s-select--sm so they inherit the central field styling; only
   the layout lives here. auto-fit keeps four quarters on a phone and lets two
   halves take the width they are given.
--------------------------------------------------------------------------- */
.s-lineup { display: flex; flex-direction: column; gap: 8px; }
.s-lineup__r {
  padding: 10px 11px;
  border: 1px solid var(--s-line); border-radius: var(--s-r);
  background: var(--s-surface);
}
.s-lineup__r--bench { background: var(--s-surface-2); }
.s-lineup__hd {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 8px; margin-bottom: 8px;
}
.s-lineup__nm {
  min-width: 0; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.s-lineup__tag {
  margin-left: 6px; font-size: 10px; font-weight: 900;
  letter-spacing: .06em; text-transform: uppercase; color: var(--s-muted);
}
.s-lineup__mins {
  flex-shrink: 0; white-space: nowrap;
  font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-muted);
  font-variant-numeric: tabular-nums;
}
.s-lineup__cells {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(62px, 1fr));
  gap: 6px;
}
.s-lineup__c { display: flex; flex-direction: column; gap: 3px; margin: 0; min-width: 0; }
.s-lineup__lb {
  font-size: 10px; font-weight: 900; letter-spacing: .06em;
  text-transform: uppercase; color: var(--s-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The pickers themselves: full width of their cell, and small enough that
   four of them fit a 375px screen. Written against `.s-v2 .s-lineup__c` so it
   outranks the sheet's own `.s-v2 .form-select` sizing rather than losing to
   it. A picker with someone in it is the signal you scan for down a column of
   twelve players, so it carries the colour and the empty one stays quiet. */
.s-v2 .s-lineup__c .form-select {
  width: 100%; max-width: 100%;
  padding: 6px 22px 6px 8px;
  font-size: var(--s-fs-sm); font-weight: 800;
  text-align: center;
  background-position: right 5px center; background-size: 13px;
}
.s-v2 .s-lineup__c .form-select.is-on {
  border-color: var(--s-good-line); background-color: var(--s-good-bg);
  color: var(--s-good); font-weight: 900;
}
/* The tally under each match: how many are on in that period, against how
   many should be. Same column rhythm as the pickers above it. */
.s-lineup__tots {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(62px, 1fr));
  gap: 6px; margin-top: 10px; padding-top: 10px;
  border-top: 1px solid var(--s-line);
  text-align: center;
}
.s-lineup__t {
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted);
  line-height: 1.35;
}
.s-lineup__t b {
  display: block; font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink);
  font-variant-numeric: tabular-nums;
}
.s-lineup__t b.is-off { color: var(--s-bad); }

/* ── Tournament page ────────────────────────────────────────────────────
   A twelve-tab page, so the tab strip has to say how much is behind each
   tab before you tap it. Same disc as .s-chip__n, mixed from the tab's own
   colour so it inverts for free when the tab is the current one. */
.s-tabs__n {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 17px; height: 17px; padding: 0 5px;
  border-radius: var(--s-r-pill);
  background: color-mix(in srgb, currentColor 20%, transparent);
  font-size: 10px; font-weight: 900;
  font-variant-numeric: tabular-nums;
}
/* A league table has to fit a phone. Drawn and lost are the two columns a
   parent never reads — points and goal difference already say it — so they
   are the two that go when there isn't room. */
@media (max-width: 559px) {
  .s-table th.s-table__opt,
  .s-table td.s-table__opt { display: none; }
}
/* Taking a player back out of the randomise pool. */
.s-chip__x {
  border: 0; background: none; padding: 0 3px; line-height: 1;
  font-size: 1.05rem; color: inherit; opacity: .7; cursor: pointer;
}
/* The programme tick is a real submit button, not a decoration, so it needs
   the font and the pointer a <button> doesn't inherit. */
.s-v2 button.s-check__ic { font: inherit; cursor: pointer; }
/* The only chevron in the kit that opens something in place rather than
   navigating, so it is the only one that turns. */
.s-row__chev.is-open { transform: rotate(180deg); }
/* Availability is a three-way answer. Yes and no are filled; maybe is tinted
   rather than filled, so a wall of faces still reads as decided or not. */
.s-face--maybe { background: var(--s-warn-bg); color: var(--s-warn); }

/* ==========================================================================
   THE TOURNAMENT SCHEDULER (page 99)

   A five-step wizard whose fourth step is a drag-and-drop Gantt board: pitches
   down the side, the day across the top, a block per match. It is the one
   screen in the app that genuinely wants a mouse and a wide window, so the
   board scrolls inside its own frame rather than reflowing, and step 4 says so
   out loud on a phone. Every other step is an ordinary form and works at 375px
   like the rest of the app.

   Age-group colour arrives as data, not as decoration: the script sets --ag on
   an element and the rules below read it. Same for the geometry the board
   computes - --l/--w/--t position a block, --ht-l/--ht-w draw the half-time
   band inside it. That is what keeps a 1,400-line script out of the inline
   styles this sheet exists to abolish.
   ========================================================================== */

/* -- The wizard's panels ------------------------------------------------- */
/* One panel is on at a time and the rest carry the `hidden` attribute, so a
   screen reader and a script-off browser both see exactly one step. */
.s-step-panel[hidden] { display: none; }

/* -- Step 2: age groups, and the clubs in them --------------------------- */
.s-agb { display: flex; flex-direction: column; gap: var(--s-sp-2); }
.s-agb__row {
  border: 1.5px solid var(--ag, var(--s-line));
  border-radius: var(--s-r-sm); overflow: hidden;
}
.s-agb__hd {
  display: flex; align-items: center; gap: var(--s-sp-2);
  padding: 7px 9px; flex-wrap: wrap;
  background: var(--s-surface-2); border-bottom: 1px solid var(--s-line-soft);
}
.s-agb__sel { width: 150px; color: var(--ag, var(--s-ink)); font-weight: 800; }
.s-agb__n { font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-muted); }
.s-agb__bd {
  padding: 8px 9px; min-height: 48px;
  display: flex; align-items: center; flex-wrap: wrap; gap: 5px;
}
/* A club that has been added to an age group. */
.s-tchip {
  display: inline-flex; align-items: center; gap: 5px;
  border-radius: var(--s-r-pill); padding: 3px 8px 3px 11px;
  font-size: var(--s-fs-sm); font-weight: 800;
  background: color-mix(in srgb, var(--ag, var(--s-slate)) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--ag, var(--s-slate)) 34%, transparent);
  color: var(--ag, var(--s-slate));
}
.s-tchip button {
  background: none; border: 0; padding: 0; line-height: 1;
  font-size: var(--s-fs-sm); color: inherit; opacity: .6; cursor: pointer;
}
.s-tchip button:hover { opacity: 1; color: var(--s-bad); }
.s-agb__add { display: inline-flex; align-items: center; gap: 5px; }
.s-agb__add input {
  width: 190px; max-width: 44vw;
  border: 1.5px solid var(--s-line); border-radius: var(--s-r-sm);
  padding: 5px 9px; font: inherit; font-size: var(--s-fs-sm); font-weight: 700;
  background: var(--s-surface); color: var(--s-ink);
}
.s-agb__add input:focus { outline: none; border-color: var(--s-info); }
.s-agb__add button {
  background: var(--s-accent); color: var(--s-accent-on);
  border: 0; border-radius: var(--s-r-sm);
  padding: 5px 11px; font-weight: 900; cursor: pointer;
}
.s-agb__sum { display: flex; flex-wrap: wrap; gap: 5px; margin-top: var(--s-sp-3); }
.s-agb__sum span {
  border-radius: 7px; padding: 2px 9px;
  font-size: var(--s-fs-sm); font-weight: 800;
  background: color-mix(in srgb, var(--ag, var(--s-slate)) 14%, transparent);
  border: 1px solid var(--ag, var(--s-slate));
  color: var(--ag, var(--s-slate));
}

/* -- Step 3: the day, split into windows --------------------------------- */
.s-per {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s-sp-2);
  padding: 9px 11px; margin-bottom: 7px;
  border: 1.5px solid var(--s-line); border-radius: var(--s-r-sm);
}
.s-per__dot {
  width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0;
  background: var(--ag, var(--s-slate));
}
.s-per__lbl {
  border: 0; background: none; padding: 0; width: 108px;
  font: inherit; font-weight: 900; color: var(--s-ink);
}
.s-per__lbl:focus { outline: none; border-bottom: 1.5px solid var(--s-info); }
.s-per__t {
  border: 1.5px solid var(--s-line); border-radius: 8px;
  padding: 4px 8px; font: inherit; font-size: var(--s-fs-sm); font-weight: 700;
  background: var(--s-surface); color: var(--s-ink);
}
.s-per__t:focus { outline: none; border-color: var(--s-info); }
.s-per__ags { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
/* An age group is either in this window or it is not; tapping it moves it here
   and out of wherever it was. */
.s-per__ag {
  border-radius: 6px; padding: 2px 9px; cursor: pointer;
  font-size: var(--s-fs-xs); font-weight: 900;
  background: color-mix(in srgb, var(--ag, var(--s-slate)) 9%, transparent);
  border: 1px solid color-mix(in srgb, var(--ag, var(--s-slate)) 20%, transparent);
  color: var(--ag, var(--s-slate)); opacity: .5;
}
.s-per__ag.is-on {
  background: color-mix(in srgb, var(--ag, var(--s-slate)) 20%, transparent);
  border-color: color-mix(in srgb, var(--ag, var(--s-slate)) 55%, transparent);
  opacity: 1;
}
.s-per__x { margin-left: auto; background: none; border: 0; color: var(--s-muted); cursor: pointer; }
.s-per__x:hover { color: var(--s-bad); }

/* Pitches, as chips you add and take away. */
.s-pchips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: var(--s-sp-3); min-height: 34px; }
.s-pchip {
  display: inline-flex; align-items: center; gap: 5px;
  background: var(--s-good-bg); border: 1px solid var(--s-good-line);
  border-radius: var(--s-r-pill); padding: 4px 9px 4px 12px;
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-good);
}
.s-pchip button { background: none; border: 0; padding: 0; color: inherit; opacity: .6; cursor: pointer; }
.s-pchip button:hover { opacity: 1; color: var(--s-bad); }
/* A pitch this club has used before. Dashed, because it is an offer and not
   yet a choice. */
.s-psugs { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
.s-psug {
  border: 1px dashed var(--s-line); border-radius: var(--s-r-pill);
  padding: 3px 11px; font: inherit; font-size: var(--s-fs-sm); font-weight: 700;
  color: var(--s-ink-2); background: none; cursor: pointer;
}
.s-psug:hover { border-color: var(--s-good); color: var(--s-good); background: var(--s-good-bg); }

/* The shape of one match: two halves, the break between them, the changeover
   after. Each segment is flexed by how many minutes it is, so the picture is
   the numbers above it. */
.s-tbar { display: flex; height: 30px; border-radius: 8px; overflow: hidden; }
.s-tbar__seg {
  flex: var(--f, 1) 1 0; min-width: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--s-fs-xs); font-weight: 900; white-space: nowrap; overflow: hidden;
}
.s-tbar__seg--play { background: var(--s-good); color: #fff; }
.s-tbar__seg--ht   { background: var(--s-warn); color: #fff; min-width: 8px; }
.s-tbar__seg--gap  { background: var(--s-line); color: var(--s-muted); }
.s-tbar__lbls { display: flex; margin-top: 4px; }
.s-tbar__lbls span {
  flex: var(--f, 1) 1 0; min-width: 0; text-align: center;
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-ink-2);
}

/* Two ways to end up with a tournament: a programme, or real fixtures. */
.s-outs { display: grid; gap: var(--s-sp-2); grid-template-columns: 1fr; }
@media (min-width: 640px) { .s-outs { grid-template-columns: 1fr 1fr; } }
.s-out {
  border: 2px solid var(--s-line); border-radius: var(--s-r-sm);
  padding: var(--s-sp-3); cursor: pointer; background: var(--s-surface);
  text-align: left; font: inherit; display: block; width: 100%;
}
.s-out.is-on { border-color: var(--s-good); background: var(--s-good-bg); }
.s-out__t { font-size: var(--s-fs-md); font-weight: 900; color: var(--s-ink); }
.s-out__s { margin-top: 2px; font-size: var(--s-fs-sm); font-weight: 600; color: var(--s-ink-2); }

/* -- Step 4: the board --------------------------------------------------- */
.s-board {
  --s-row-h: 84px; --s-lbl-w: 132px; --s-board-hdr: 46px;
  display: flex; flex-direction: column;
  /* Tall enough to drag in, never so tall that the four counters underneath
     it get pushed behind the fixed bottom nav. The upper bound also keeps a
     full-page screenshot honest: with a bare 100vh the board grows to the
     height of the whole capture. */
  height: auto;
  border: 1px solid var(--s-line); border-radius: var(--s-r);
  overflow: hidden; background: var(--s-surface-2);
}
/* On a phone the board stacks and grows: the toolbar alone is five rows, so
   a fixed height leaves the grid a sliver and hides the pitches entirely.
   Let the page scroll instead. Beside-each-other, with the sidebar scrolling
   on its own, is what needs the fixed height - and the upper bound keeps the
   four counters underneath from being pushed behind the fixed bottom nav. */
@media (min-width: 900px) {
  .s-board {
    flex-direction: row;
    height: clamp(420px, calc(100dvh - 430px), 820px);
  }
}

/* The pile of matches waiting to be placed. Above the board on a phone,
   beside it once there is room. */
#ts-sidebar {
  flex-shrink: 0; background: var(--s-surface);
  border-bottom: 1px solid var(--s-line);
  display: flex; flex-direction: column; overflow: hidden;
  max-height: 190px;
}
@media (min-width: 900px) {
  #ts-sidebar { width: 208px; max-height: none; border-bottom: 0; border-right: 1px solid var(--s-line); }
}
#ts-sidebar-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 7px 10px; background: var(--s-surface-2);
  border-bottom: 1px solid var(--s-line-soft);
  font-size: var(--s-fs-xs); font-weight: 900;
  letter-spacing: .07em; text-transform: uppercase; color: var(--s-ink-2);
}
#pending-badge {
  background: var(--s-line); border-radius: var(--s-r-pill);
  padding: 1px 9px; font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-ink-2);
}
#ts-sidebar-filters {
  display: flex; flex-wrap: wrap; gap: 4px;
  padding: 5px 7px; border-bottom: 1px solid var(--s-line-soft);
}
.s-fchip {
  border-radius: var(--s-r-pill); padding: 2px 9px; cursor: pointer;
  user-select: none; font: inherit;
  font-size: var(--s-fs-xs); font-weight: 900;
  background: var(--s-surface-2); border: 1px solid var(--s-line); color: var(--s-muted);
}
.s-fchip.is-on {
  background: color-mix(in srgb, var(--ag, var(--s-slate)) 14%, transparent);
  border-color: var(--ag, var(--s-slate)); color: var(--ag, var(--s-slate));
}
#ts-sidebar-list { flex: 1; overflow-y: auto; padding: 5px; }
#ts-sidebar-list.is-over { background: var(--s-good-bg); }
.s-mcard {
  background: var(--s-surface-2); border: 1.5px solid var(--s-line);
  border-radius: var(--s-r-sm); padding: 6px 9px; margin-bottom: 5px;
  cursor: grab; user-select: none; position: relative; font-size: var(--s-fs-sm);
}
.s-mcard:hover { border-color: var(--s-good); background: var(--s-good-bg); }
.s-mcard.is-dragging { opacity: .3; }
.s-mcard__ag {
  display: inline-block; border-radius: 5px; padding: 1px 7px; margin-bottom: 3px;
  font-size: var(--s-fs-xs); font-weight: 900;
  background: color-mix(in srgb, var(--ag, var(--s-slate)) 14%, transparent);
  color: var(--ag, var(--s-slate));
}
.s-mcard__t { font-weight: 900; color: var(--s-ink); line-height: 1.3; }
.s-mcard__t i { opacity: .45; font-style: normal; font-size: var(--s-fs-xs); }
.s-mcard__r { margin-top: 2px; font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }
/* The ceremony is not a match, so it does not sit in the match list - it gets
   its own card per window. */
.s-mcard--pres {
  background: var(--s-warn-bg); border: 1.5px dashed var(--s-warn-line);
  border-radius: var(--s-r-sm); padding: 6px 9px; margin-bottom: 5px;
  cursor: grab; user-select: none;
  display: flex; align-items: center; gap: 7px;
  font-size: var(--s-fs-sm); color: var(--s-warn);
}
.s-mcard--pres:hover { border-color: var(--s-warn); }
.s-mcard--pres.is-dragging { opacity: .3; }
.s-mcard--pres b { display: block; font-size: var(--s-fs-xs); font-weight: 900; }
.s-presrow { padding: 5px 7px; }
.s-presdone {
  display: flex; align-items: center; gap: 7px;
  background: var(--s-warn-bg); border: 1px solid var(--s-warn-line);
  border-radius: 8px; padding: 6px 9px;
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-warn);
}
.s-presdone span { flex: 1; min-width: 0; }
.s-presdone button { background: none; border: 0; color: inherit; cursor: pointer; padding: 0; }
#sidebar-empty {
  padding: var(--s-sp-4) var(--s-sp-3); text-align: center;
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-muted);
}

#ts-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }

/* The toolbar. Everything the whole grid obeys lives here, and it wraps
   rather than scrolls so nothing hides behind an edge. */
#ts-toolbar {
  display: flex; align-items: center; flex-wrap: wrap; gap: 7px;
  padding: 7px 9px; min-height: 48px;
  background: var(--s-surface); border-bottom: 1px solid var(--s-line);
}
.s-tb__set {
  display: flex; align-items: center; gap: 5px;
  border: 1px solid var(--s-line); border-radius: 9px; padding: 3px 8px;
}
.s-tb__set--good { background: var(--s-good-bg); border-color: var(--s-good-line); color: var(--s-good); }
.s-tb__set--warn { background: var(--s-warn-bg); border-color: var(--s-warn-line); color: var(--s-warn); }
.s-tb__g { font-size: var(--s-fs-xs); font-weight: 900; color: inherit; }
.s-tb__op { color: var(--s-muted); font-size: var(--s-fs-sm); font-weight: 900; }
.s-tb__n {
  width: 54px; padding: 3px 5px; text-align: center;
  border: 1.5px solid var(--s-line); border-radius: 7px;
  font: inherit; font-size: var(--s-fs-sm); font-weight: 800;
  background: var(--s-surface); color: var(--s-ink);
}
.s-tb__set--good .s-tb__n { border-color: var(--s-good-line); }
.s-tb__set--warn .s-tb__n { border-color: var(--s-warn-line); }
.s-tb__n:focus { outline: none; border-color: var(--s-info); }
.s-tb__tot { min-width: 34px; text-align: center; font-size: var(--s-fs-sm); font-weight: 900; }
.s-tb__sep { width: 1px; height: 22px; background: var(--s-line); flex-shrink: 0; }
.s-tbb {
  display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  background: var(--s-surface-2); border: 1px solid var(--s-line);
  border-radius: 8px; padding: 5px 11px; cursor: pointer;
  font: inherit; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink);
}
.s-tbb:hover { background: var(--s-line-soft); }
.s-tbb--good { background: var(--s-good-bg); border-color: var(--s-good-line); color: var(--s-good); }
.s-tbb--bad  { background: var(--s-bad-bg);  border-color: var(--s-bad-line);  color: var(--s-bad); }
.s-tbb--warn { background: var(--s-warn-bg); border-color: var(--s-warn-line); color: var(--s-warn); }
.s-tb__zoom { display: flex; align-items: center; gap: 4px; }
.s-tb__zoom button {
  width: 26px; height: 26px; border-radius: 6px; cursor: pointer;
  border: 1px solid var(--s-line); background: var(--s-surface-2); color: var(--s-ink);
  display: flex; align-items: center; justify-content: center; font-weight: 900;
}
.s-tb__zoom button:hover { background: var(--s-line-soft); }
#zoom-lbl { min-width: 26px; text-align: center; font-size: var(--s-fs-xs); font-weight: 900; color: var(--s-ink-2); }

/* The presets dropdown. */
.s-pmenu { position: relative; display: inline-block; }
#preset-menu {
  position: absolute; top: calc(100% + 5px); left: 0; z-index: 100;
  min-width: 232px; padding: 5px 0;
  background: var(--s-surface); border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm); box-shadow: var(--s-shadow);
}
.s-pmenu__i {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 8px 13px; cursor: pointer; text-align: left;
  background: none; border: 0; font: inherit;
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink);
}
.s-pmenu__i:hover { background: var(--s-good-bg); color: var(--s-good); }
.s-pmenu__i i { width: 17px; text-align: center; color: var(--s-muted); }
.s-pmenu__i:hover i { color: inherit; }

/* The grid: one scroller, a sticky ruler along the top and a sticky column of
   pitch names down the left. */
#ts-grid-scroll { flex: 1; overflow: auto; position: relative; }
#ts-grid-inner { position: relative; display: inline-block; min-width: 100%; }
#ts-ruler {
  display: flex; height: var(--s-board-hdr);
  position: sticky; top: 0; z-index: 20;
  background: var(--s-surface); border-bottom: 2px solid var(--s-line);
}
.s-ruler__pad {
  width: var(--s-lbl-w); flex-shrink: 0;
  position: sticky; left: 0; z-index: 21;
  background: var(--s-surface-2); border-right: 1px solid var(--s-line);
}
#ruler-ticks { position: relative; flex: 1; min-width: var(--w, 0); }
.s-rtick {
  position: absolute; top: 0; bottom: 0; left: var(--l, 0);
  border-left: 1px solid var(--s-line-soft);
  display: flex; align-items: flex-end; padding-bottom: 5px;
}
.s-rtick.is-major { border-left-color: var(--s-line); }
.s-rtick span { padding-left: 4px; white-space: nowrap; font-size: 10px; font-weight: 800; color: var(--s-muted); }
.s-rtick.is-major span { font-size: var(--s-fs-xs); color: var(--s-ink-2); }
/* A window of the day, drawn faintly behind the row it governs. */
.s-band {
  position: absolute; top: 0; bottom: 0; z-index: 1; pointer-events: none;
  left: var(--l, 0); width: var(--w, 0);
  background: var(--ag, var(--s-slate)); opacity: .07;
}
#ts-rows { position: relative; }
.s-prow { display: flex; position: relative; height: var(--s-row-h); border-bottom: 1px solid var(--s-line-soft); }
.s-prow__lbl {
  width: var(--s-lbl-w); flex-shrink: 0;
  position: sticky; left: 0; z-index: 5;
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 9px; background: var(--s-surface);
  border-right: 1px solid var(--s-line);
  font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-ink);
}
.s-prow__lbl span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-prow__del { background: none; border: 0; color: var(--s-line); cursor: pointer; padding: 3px 5px; border-radius: 5px; }
.s-prow__del:hover { color: var(--s-bad); background: var(--s-bad-bg); }
.s-prow__bg {
  position: absolute; left: var(--s-lbl-w); right: 0; top: 0; bottom: 0;
  cursor: crosshair; min-width: var(--w, 0);
}
.s-prow__bg.is-over {
  background: color-mix(in srgb, var(--s-good) 8%, transparent);
  outline: 2px dashed color-mix(in srgb, var(--s-good) 45%, transparent);
  outline-offset: -2px;
}
.s-prow--add { height: 42px; display: flex; align-items: center; padding: 0 9px; border-bottom: 1px solid var(--s-line-soft); }
.s-prow__add {
  background: none; border: 1.5px dashed var(--s-line); border-radius: 8px;
  padding: 5px 11px; cursor: pointer;
  font: inherit; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-muted);
}
.s-prow__add:hover { border-color: var(--s-good); color: var(--s-good); background: var(--s-good-bg); }

/* A match on the grid. --l and --w are minutes turned into pixels by the
   script; --ag is the age group's colour. */
.s-mb {
  position: absolute; top: 7px; left: var(--l, 0); width: var(--w, 60px);
  height: calc(var(--s-row-h) - 14px);
  display: flex; flex-direction: column; justify-content: center;
  padding: 0 22px 0 8px; overflow: hidden;
  border: 1.5px solid var(--ag, var(--s-slate)); border-radius: 9px;
  background: color-mix(in srgb, var(--ag, var(--s-slate)) 13%, var(--s-surface));
  color: var(--ag, var(--s-slate));
  cursor: grab; user-select: none; z-index: 10;
}
.s-mb:hover { box-shadow: var(--s-shadow); z-index: 12; }
.s-mb.is-dragging { opacity: .55; cursor: grabbing; z-index: 50; }
.s-mb.is-clash { outline: 2px solid var(--s-bad); outline-offset: -1px; }
.s-mb--pres { background: var(--s-warn-bg); border-color: var(--s-warn); color: var(--s-warn); text-align: center; }
.s-mb__ag, .s-mb__t, .s-mb__v, .s-mb__time { position: relative; z-index: 4; }
.s-mb__ag {
  font-size: 9px; font-weight: 900; text-transform: uppercase; letter-spacing: .06em;
  opacity: .7; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.s-mb__t {
  font-size: var(--s-fs-sm); font-weight: 900; line-height: 1.25;
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}
.s-mb__v { font-size: 10px; opacity: .55; line-height: 1; }
.s-mb__time { margin-top: 1px; font-size: 10px; font-weight: 700; opacity: .7; }
/* Half-time, drawn inside the block where it actually falls. */
.s-mb__ht {
  position: absolute; top: 0; bottom: 0; z-index: 2; pointer-events: none;
  left: var(--ht-l, 0); width: var(--ht-w, 0);
  background: color-mix(in srgb, var(--s-warn) 26%, transparent);
  border-left: 1.5px solid color-mix(in srgb, var(--s-warn) 55%, transparent);
  border-right: 1.5px solid color-mix(in srgb, var(--s-warn) 55%, transparent);
}
.s-mb__htl {
  position: absolute; top: 4px; z-index: 3; pointer-events: none;
  left: calc(var(--ht-l, 0px) + var(--ht-w, 0px) / 2); transform: translateX(-50%);
  font-size: 9px; font-weight: 900; white-space: nowrap; color: var(--s-warn);
}
.s-mb__rs {
  position: absolute; right: 0; top: 0; bottom: 0; width: 9px; z-index: 6;
  cursor: col-resize; background: rgba(0, 0, 0, .07);
  border-radius: 0 8px 8px 0; opacity: 0;
}
.s-mb:hover .s-mb__rs { opacity: 1; }
.s-mb__x {
  position: absolute; top: 4px; right: 8px; z-index: 15;
  width: 17px; height: 17px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, .28); border: 0; color: #fff;
  font-size: 10px; cursor: pointer; opacity: 0; pointer-events: none;
}
.s-mb:hover .s-mb__x { opacity: 1; pointer-events: auto; }
/* The minutes a team is asked to be at the pitch before kick-off: a dashed
   run-up to the block rather than part of it. */
.s-stripe {
  position: absolute; top: 12px; bottom: 12px; z-index: 8; pointer-events: none;
  left: var(--l, 0); width: var(--w, 0);
  border: 1.5px dashed var(--ag, var(--s-slate));
  border-radius: 6px 0 0 6px; opacity: .35;
}
/* Where the block you are dragging will land. */
#drop-ghost {
  position: absolute; display: none; pointer-events: none; z-index: 50;
  top: var(--t, 0); left: var(--l, 0); width: var(--w, 0); height: var(--h, 0);
  border: 2px dashed var(--s-good); border-radius: 9px;
  background: color-mix(in srgb, var(--s-good) 9%, transparent);
}
#drop-ghost.is-pres { border-color: var(--s-warn); background: color-mix(in srgb, var(--s-warn) 11%, transparent); }
/* The block's full story, following the cursor. Fixed, so it escapes the
   scroller's overflow. */
#mb-tooltip {
  position: fixed; z-index: 9999; display: none;
  top: var(--t, 0); left: var(--l, 0);
  max-width: 232px; padding: 9px 13px;
  border-radius: var(--s-r-sm); border: 1px solid rgba(255, 255, 255, .1);
  background: rgba(15, 23, 42, .92); color: #fff;
  font-size: var(--s-fs-sm); line-height: 1.55; pointer-events: none;
}
#mb-tooltip hr { border: 0; border-top: 1px solid rgba(255, 255, 255, .2); margin: 5px 0; }
/* Drag-and-drop is a mouse gesture and a touchscreen has no dragstart to give.
   Rather than pretend, step 4 says so on a phone and points at the two buttons
   that build the whole grid without dragging anything. */
.s-nomouse { display: block; }
@media (min-width: 900px) { .s-nomouse { display: none; } }

/* -- The preview ---------------------------------------------------------- */
.s-schtbl { width: 100%; border-collapse: collapse; font-size: var(--s-fs-sm); }
.s-schtbl th {
  padding: 6px 10px; text-align: left;
  background: var(--s-surface-2); border-bottom: 2px solid var(--s-line);
  font-size: var(--s-fs-xs); font-weight: 900;
  letter-spacing: .06em; text-transform: uppercase; color: var(--s-ink-2);
}
.s-schtbl td { padding: 6px 10px; border-bottom: 1px solid var(--s-line-soft); font-weight: 700; color: var(--s-ink); }
.s-schtbl td.s-schtbl__n { color: var(--s-muted); font-variant-numeric: tabular-nums; }
.s-schtbl td.s-schtbl__v { color: var(--s-muted); text-align: center; font-weight: 600; }
.s-schtbl tr.s-schtbl__grp td {
  background: var(--s-surface-2); padding: 5px 10px;
  font-weight: 900; color: var(--ag, var(--s-slate));
}
.s-schtbl__p {
  display: inline-block; border-radius: 6px; padding: 1px 8px;
  background: var(--s-good-bg); border: 1px solid var(--s-good-line);
  color: var(--s-good); font-size: var(--s-fs-xs); font-weight: 900;
}
.s-schtbl__a {
  display: inline-block; border-radius: 6px; padding: 1px 8px;
  background: color-mix(in srgb, var(--ag, var(--s-slate)) 14%, transparent);
  color: var(--ag, var(--s-slate)); font-size: var(--s-fs-xs); font-weight: 900;
}
/* A dialog the page owns rather than a page you navigate to: the schedule is
   still being built behind it.

   NOT .s-sheet: base_v2.html already uses that class for the rounded surface
   the whole page sits on, so redefining it as `display:none` blanked every
   v2 page in the tree. .s-sheetm is the bottom-sheet primitive and caps at
   420px on desktop, which a schedule table cannot live in. */
.s-modal {
  position: fixed; inset: 0; z-index: 8000; display: none;
  align-items: center; justify-content: center; padding: var(--s-sp-3);
  background: rgba(0, 0, 0, .5);
}
.s-modal.is-open { display: flex; }
.s-modal__in {
  display: flex; flex-direction: column;
  width: 100%; max-width: 880px; max-height: 88vh;
  background: var(--s-surface); border-radius: var(--s-r-lg); overflow: hidden;
}
.s-modal__hd {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s-sp-3);
  padding: var(--s-sp-3) var(--s-sp-4); border-bottom: 1px solid var(--s-line-soft);
  font-size: var(--s-fs-lg); font-weight: 900; color: var(--s-ink);
}
.s-modal__x { background: none; border: 0; font-size: 1.3rem; line-height: 1; cursor: pointer; color: var(--s-muted); }
.s-modal__bd { flex: 1; overflow: auto; padding: var(--s-sp-4); }


/* ══ The on-the-day score console ═══════════════════════════════════════════
   One person, one phone, a field with no signal and forty matches to keep up
   with. Everything here is sized for that: a cold thumb, a screen held at
   arm's length, and a save that might fail.

   Reused rather than reinvented: .s-tally is the touchline stepper the
   volunteer scoring page already uses, and .s-score is the read-only pill
   every other page shows a result in. The card below is the wrapper that puts
   the second in a list and the first behind a tap. */

/* The two questions - what order, and which matches - stay on screen while
   the list scrolls under them, because the answer to "where is pitch 3" is
   not worth scrolling back to the top for. Under the shell header, which
   measures itself, so this does not need a hard-coded offset the way v1's
   `top: 89px` did. */
.s-filters {
  position: sticky;
  top: calc(var(--s-hdr-h) + 8px);
  z-index: 20;
  display: grid; gap: var(--s-sp-2);
  padding: var(--s-sp-2);
  border: 1px solid var(--s-line);
  border-radius: var(--s-r-lg);
  background: var(--s-surface);
  box-shadow: 0 6px 18px rgba(15, 23, 42, .06);
}
.s-filters .s-seg { margin: 0; }
/* .s-seg leaves a gutter for the drawer's button. Inside a page there is no
   button to leave room for. */
.s-seg--full { margin-right: 0; }
/* The switch was written for the drawer, where each option is a link to
   another view. Here it changes what is already on screen, so it is a real
   button - and a <button> does not inherit the page's font. */
.s-v2 button.s-seg__opt { font-family: inherit; cursor: pointer; -webkit-appearance: none; appearance: none; }

.s-grouph {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s-sp-2);
  margin: var(--s-sp-4) 2px var(--s-sp-2);
  padding-left: 9px;
  border-left: 3px solid var(--s-accent);
}
.s-grouph__k {
  font-size: var(--s-fs-xs); font-weight: 900;
  text-transform: uppercase; letter-spacing: .09em; color: var(--s-ink-2);
}
.s-grouph__n { font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }

/* One match. Closed it is a result you can read at a glance; open it is the
   thing that changes the result. Both states are the same card, so tapping
   does not move the row you were looking at somewhere else on the screen. */
.s-match {
  border: 1px solid var(--s-line);
  border-radius: var(--s-r);
  background: var(--s-surface);
  overflow: hidden;
}
.s-match + .s-match { margin-top: var(--s-sp-2); }
.s-match--live { border-color: var(--s-warn-line); background: var(--s-warn-bg); }
.s-match--done { border-color: var(--s-good-line); background: var(--s-good-bg); }
.s-match--bad  { border-color: var(--s-bad-line); }
.s-match.is-open { border-color: var(--s-accent); }
.s-match__head {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  gap: var(--s-sp-2); align-items: center;
  width: 100%;
  padding: 11px var(--s-sp-3) 9px;
  border: 0; background: none; font: inherit; text-align: inherit;
  cursor: pointer;
}
.s-match__team {
  min-width: 0;
  display: flex; align-items: center; gap: 6px;
  font-size: var(--s-fs-sm); font-weight: 800; line-height: 1.2; color: var(--s-ink);
}
.s-match__team--a { justify-content: flex-end; text-align: right; }
.s-match__team span { min-width: 0; overflow-wrap: anywhere; }
/* --tc is set from the participant's colour by script, not by an inline
   style attribute: this stylesheet is the only place styling lives. */
.s-match__dot {
  flex-shrink: 0;
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--tc, var(--s-muted));
}
.s-match__box { display: flex; justify-content: center; }
.s-match__foot {
  display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: 5px;
  padding: 0 var(--s-sp-3) 10px;
}
.s-match__ed {
  display: none;
  padding: var(--s-sp-3);
  border-top: 1px solid var(--s-line-soft);
  background: var(--s-surface);
}
.s-match.is-open .s-match__ed { display: block; }

/* Whether the number on screen is the number on the server. The page saves
   by itself, so the only honest thing it can do is say where the save got
   to - and keep saying so while there is no signal. */
.s-led {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-muted);
}
.s-led--warn { color: var(--s-warn); }
.s-led--bad { color: var(--s-bad); }
.s-led--good { color: var(--s-good); }

/* The match clock. Big enough to read while you are watching the game rather
   than the phone, and it goes red the moment it is over time, because that is
   the one state somebody needs to notice without looking. */
.s-clock {
  margin-top: var(--s-sp-3);
  padding: var(--s-sp-3);
  border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm);
  background: var(--s-surface-2);
}
.s-clock__row { display: flex; align-items: center; justify-content: space-between; gap: var(--s-sp-3); }
.s-clock__t {
  font-size: 1.9rem; line-height: 1; font-weight: 900;
  font-variant-numeric: tabular-nums; color: var(--s-ink);
}
.s-clock__t.is-over { color: var(--s-bad); }
.s-clock__m { margin-top: 3px; font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }
.s-clock__acts { display: grid; grid-template-columns: 1.3fr 1fr 1fr; gap: 6px; margin-top: var(--s-sp-3); }

/* Said once, above the tab bar, and gone. Anything that has to persist is a
   .s-note in the card it belongs to instead. */
.s-toast {
  position: fixed; left: 50%; transform: translateX(-50%); z-index: 8500;
  bottom: calc(var(--s-nav-h) + 16px);
  max-width: min(92vw, 420px);
  padding: 11px var(--s-sp-4);
  border-radius: var(--s-r);
  background: var(--s-ink); color: var(--s-surface);
  font-size: var(--s-fs-sm); font-weight: 800; text-align: center;
  box-shadow: 0 12px 30px rgba(15, 23, 42, .28);
}

/* ══════════════════════════════════════════════════════════════════════════
   Season Wrapped — the one page in the app with its own visual world.

   It is a celebration artefact, not an admin screen: a team's year on one
   scrolling canvas that families are meant to enjoy and share. That justifies
   a look of its own, but not a look that lives in the page. Everything the
   wrapped page needs is here, so the template carries no <style> block and no
   inline style= — the same rule as everywhere else.

   The canvas is dark in both themes on purpose. It is a lit stage, and it
   spans the content column rather than the viewport, so it reads as a panel
   the page hands you rather than a page that has broken out of the shell.
   ══════════════════════════════════════════════════════════════════════════ */

.wrp {
  --wrp-ink:    #f1f5f9;
  --wrp-mute:   #8b98ad;
  --wrp-gold:   #fbbf24;
  --wrp-good:   #34d399;
  --wrp-warn:   #fbbf24;
  --wrp-bad:    #f87171;
  --wrp-info:   #60a5fa;
  --wrp-violet: #c084fc;
  --wrp-pink:   #f472b6;
  --wrp-orange: #fb923c;
  --wrp-glass:  rgba(255, 255, 255, .045);
  --wrp-line:   rgba(255, 255, 255, .09);

  position: relative;
  isolation: isolate;
  /* Cancels .s-main's 15px gutter so the stage spans the content column. */
  margin-inline: -15px;
  padding: var(--s-sp-6) 15px calc(var(--s-sp-6) * 1.6);
  border-radius: var(--s-r-xl);
  background: #0a0a12;
  color: var(--wrp-ink);
  overflow: hidden;
}

/* One soft mesh behind everything. v1 ran a hue-rotate on this forever; a
   still gradient does the same work without repainting the page for hours. */
.wrp::before {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  background:
    radial-gradient(ellipse 80% 55% at 12% 14%, rgba(168, 85, 247, .20), transparent 70%),
    radial-gradient(ellipse 65% 70% at 92% 82%, rgba(59, 130, 246, .16), transparent 70%),
    radial-gradient(ellipse 70% 45% at 50% 48%, rgba(251, 191, 36, .07), transparent 70%);
  pointer-events: none;
}

.wrp__stack {
  max-width: 520px;
  margin: 0 auto;
  display: flex; flex-direction: column;
  gap: var(--s-sp-5);
}

/* Confetti only ever falls on a champion, and only inside the stage. */
/* The stage is thousands of pixels tall, so the confetti layer is capped at
   the opening screenful -- a burst over the hero, not a shower down a page
   nobody can see the bottom of. */
.wrp__confetti {
  position: absolute; inset: 0 0 auto; height: 100vh;
  overflow: hidden; z-index: 0; pointer-events: none;
}
.wrp-conf {
  position: absolute; top: -14px;
  animation: wrpFall linear forwards;
}
@keyframes wrpFall {
  0%   { transform: translateY(0) rotate(0deg); opacity: 1; }
  100% { transform: translateY(100vh) rotate(680deg); opacity: 0; }
}

/* ── Reveal on scroll ───────────────────────────────────────────────────── */
/* Hidden only once the script has said it is driving. Without JavaScript the
   page is simply all there -- v1 hid every card and every number behind its
   script, so a page that never ran one showed nothing. */
.wrp.is-anim .wrp-reveal { opacity: 0; transform: translateY(42px) scale(.95); }
.wrp.is-anim .wrp-reveal.is-in {
  animation: wrpIn .7s cubic-bezier(.16, 1, .3, 1) forwards;
}
@keyframes wrpIn {
  to { opacity: 1; transform: none; }
}

/* ── The card ───────────────────────────────────────────────────────────── */
.wrp-card {
  position: relative;
  padding: var(--s-sp-6) var(--s-sp-5);
  border-radius: 24px;
  border: 1px solid var(--wrp-line);
  background: var(--wrp-glass);
  backdrop-filter: blur(30px) saturate(1.3);
  -webkit-backdrop-filter: blur(30px) saturate(1.3);
  box-shadow: 0 4px 24px rgba(0, 0, 0, .3);
  display: flex; flex-direction: column;
  gap: var(--s-sp-4);
}

/* Champion treatment. v1 shimmered the border of every card on the page;
   one card carrying it is what makes it mean something. */
.wrp-card--gold {
  background: linear-gradient(140deg, rgba(251, 191, 36, .10), rgba(249, 115, 22, .04), rgba(255, 255, 255, .02));
  border-color: rgba(251, 191, 36, .34);
  box-shadow: 0 4px 30px rgba(251, 191, 36, .12), 0 4px 24px rgba(0, 0, 0, .3);
}

.wrp-card--mid { text-align: center; }

/* ── Hero ───────────────────────────────────────────────────────────────── */
.wrp-hero { text-align: center; display: grid; gap: var(--s-sp-3); justify-items: center; }
.wrp-hero__ic {
  width: 74px; height: 74px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%;
  font-size: 30px;
  color: var(--wrp-gold);
  background: rgba(251, 191, 36, .12);
  border: 1px solid rgba(251, 191, 36, .28);
}
.wrp-hero__t {
  margin: 0;
  font-size: 2.3rem; font-weight: 900; line-height: 1.04;
  letter-spacing: -.035em;
  background: linear-gradient(135deg, #fff 0%, #e2e8f0 48%, var(--wrp-gold) 100%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
  text-wrap: balance;
}
.wrp-hero__t--sm { font-size: 1.7rem; }
.wrp-hero__sub { font-size: var(--s-fs-md); font-weight: 700; color: var(--wrp-mute); }

.wrp-pill {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 15px;
  border-radius: var(--s-r-pill);
  font-size: var(--s-fs-sm); font-weight: 800;
  color: var(--wrp-ink);
  background: rgba(255, 255, 255, .06);
  border: 1px solid var(--wrp-line);
}
.wrp-pill--gold {
  color: var(--wrp-gold);
  background: rgba(251, 191, 36, .12);
  border-color: rgba(251, 191, 36, .30);
}

/* ── Section eyebrow ────────────────────────────────────────────────────── */
.wrp-sect {
  display: flex; align-items: center; gap: 8px;
  font-size: var(--s-fs-xs); font-weight: 800;
  text-transform: uppercase; letter-spacing: .15em;
  color: var(--wrp-mute);
}

.wrp-story {
  font-size: var(--s-fs-lg); font-weight: 600; line-height: 1.62;
  color: var(--wrp-ink);
}
.wrp-note { font-size: var(--s-fs-sm); font-weight: 600; color: var(--wrp-mute); }

/* ── Big numbers ────────────────────────────────────────────────────────── */
.wrp-nums { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s-sp-3); }
.wrp-num {
  padding: var(--s-sp-4) var(--s-sp-2);
  border-radius: var(--s-r-lg);
  text-align: center;
  background: rgba(255, 255, 255, .035);
  border: 1px solid var(--wrp-line);
}
.wrp-num__v {
  font-size: 2.1rem; font-weight: 900; line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--wrp-ink);
}
.wrp-num__k {
  margin-top: 6px;
  font-size: var(--s-fs-xs); font-weight: 800;
  text-transform: uppercase; letter-spacing: .08em;
  color: var(--wrp-mute);
}
.wrp-num--good .wrp-num__v   { color: var(--wrp-good); }
.wrp-num--warn .wrp-num__v   { color: var(--wrp-warn); }
.wrp-num--bad .wrp-num__v    { color: var(--wrp-bad); }
.wrp-num--info .wrp-num__v   { color: var(--wrp-info); }
.wrp-num--violet .wrp-num__v { color: var(--wrp-violet); }

/* ── Biggest win ────────────────────────────────────────────────────────── */
.wrp-big {
  padding: var(--s-sp-4);
  border-radius: var(--s-r-lg);
  text-align: center;
  background: rgba(52, 211, 153, .08);
  border: 1px solid rgba(52, 211, 153, .22);
}
.wrp-big__k {
  font-size: var(--s-fs-xs); font-weight: 800;
  text-transform: uppercase; letter-spacing: .12em;
  color: var(--wrp-good);
}
.wrp-big__v {
  margin-top: 4px;
  font-size: 1.9rem; font-weight: 900; line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--wrp-ink);
}
.wrp-big__o { margin-top: 4px; font-size: var(--s-fs-sm); font-weight: 700; color: var(--wrp-mute); }

/* ── Result dots ────────────────────────────────────────────────────────── */
.wrp-dots { display: flex; flex-wrap: wrap; gap: 7px; }
.wrp-dot {
  width: 30px; height: 30px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 10px;
  font-size: var(--s-fs-sm); font-weight: 900;
  background: rgba(255, 255, 255, .06);
  color: var(--wrp-mute);
  border: 1px solid var(--wrp-line);
}
.wrp-dot--W { background: rgba(52, 211, 153, .16); color: var(--wrp-good); border-color: rgba(52, 211, 153, .3); }
.wrp-dot--D { background: rgba(251, 191, 36, .16); color: var(--wrp-warn); border-color: rgba(251, 191, 36, .3); }
.wrp-dot--L { background: rgba(248, 113, 113, .14); color: var(--wrp-bad); border-color: rgba(248, 113, 113, .28); }

.wrp-streaks { display: flex; flex-wrap: wrap; gap: var(--s-sp-2); }
.wrp-streak {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 6px 13px;
  border-radius: var(--s-r-pill);
  font-size: var(--s-fs-sm); font-weight: 800;
  background: rgba(255, 255, 255, .06);
  border: 1px solid var(--wrp-line);
  color: var(--wrp-ink);
}
.wrp-streak--win { color: var(--wrp-orange); background: rgba(251, 146, 60, .12); border-color: rgba(251, 146, 60, .28); }
.wrp-streak--unb { color: var(--wrp-info); background: rgba(96, 165, 250, .12); border-color: rgba(96, 165, 250, .28); }

/* ── Month by month ─────────────────────────────────────────────────────── */
.wrp-month { display: grid; grid-template-columns: 62px 1fr 26px; align-items: center; gap: var(--s-sp-2); }
.wrp-month + .wrp-month { margin-top: var(--s-sp-2); }
.wrp-month__k { font-size: var(--s-fs-sm); font-weight: 800; color: var(--wrp-mute); }
.wrp-month__n { font-size: var(--s-fs-sm); font-weight: 800; color: var(--wrp-mute); text-align: right; font-variant-numeric: tabular-nums; }
.wrp-month__bar { display: flex; height: 26px; border-radius: 8px; overflow: hidden; background: rgba(255, 255, 255, .05); }
.wrp-month__seg {
  display: flex; align-items: center; justify-content: center;
  font-size: var(--s-fs-xs); font-weight: 900;
  color: #0a0a12;
  min-width: 0;
}
.wrp-month__seg--w { background: var(--wrp-good); }
.wrp-month__seg--d { background: var(--wrp-warn); }
.wrp-month__seg--l { background: var(--wrp-bad); }

/* ── Leaderboard bars ───────────────────────────────────────────────────── */
.wrp-row { display: grid; grid-template-columns: 96px 1fr; align-items: center; gap: var(--s-sp-3); }
.wrp-row + .wrp-row { margin-top: var(--s-sp-3); }
.wrp-row__k {
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--wrp-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wrp-row__track { height: 28px; border-radius: 9px; background: rgba(255, 255, 255, .05); overflow: hidden; }
/* Width comes from the script. With no script the fill hugs its own number
   rather than collapsing to nothing and hiding it. */
.wrp-row__fill {
  height: 100%; width: auto;
  display: flex; align-items: center; justify-content: flex-end;
  padding-right: 9px;
  border-radius: 9px;
  font-size: var(--s-fs-sm); font-weight: 900;
  color: #0a0a12;
  background: linear-gradient(90deg, var(--wrp-gold), var(--wrp-orange));
  transition: width .8s cubic-bezier(.16, 1, .3, 1);
}
.wrp.is-anim .wrp-row__fill { width: 0; }
.wrp-row__fill--info   { background: linear-gradient(90deg, var(--wrp-info), var(--wrp-violet)); }
.wrp-row__fill--orange { background: linear-gradient(90deg, var(--wrp-orange), var(--wrp-pink)); }

/* ── Awards & shout-outs ────────────────────────────────────────────────── */
.wrp-award { display: grid; grid-template-columns: 42px 1fr; gap: var(--s-sp-3); align-items: start; }
.wrp-award + .wrp-award { margin-top: var(--s-sp-4); }
.wrp-award__ic {
  width: 42px; height: 42px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 13px;
  font-size: 17px;
  color: var(--wrp-gold);
  background: rgba(251, 191, 36, .12);
  border: 1px solid rgba(251, 191, 36, .26);
}
.wrp-award__t { font-size: var(--s-fs-xs); font-weight: 800; text-transform: uppercase; letter-spacing: .1em; color: var(--wrp-mute); }
.wrp-award__w { font-size: var(--s-fs-lg); font-weight: 900; color: var(--wrp-ink); }
.wrp-award__v { font-size: var(--s-fs-sm); font-weight: 700; color: var(--wrp-gold); }

.wrp-shouts { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s-sp-3); }
.wrp-shout {
  padding: var(--s-sp-4) var(--s-sp-3);
  border-radius: var(--s-r-lg);
  text-align: center;
  background: rgba(255, 255, 255, .035);
  border: 1px solid var(--wrp-line);
}
.wrp-shout__ic { font-size: 19px; color: var(--wrp-violet); }
.wrp-shout__n { margin-top: 7px; font-size: var(--s-fs-md); font-weight: 900; color: var(--wrp-ink); }
.wrp-shout__t { margin-top: 3px; font-size: var(--s-fs-xs); font-weight: 800; text-transform: uppercase; letter-spacing: .07em; color: var(--wrp-mute); }
.wrp-shout__v { margin-top: 3px; font-size: var(--s-fs-sm); font-weight: 800; color: var(--wrp-gold); }

/* ── Tables ─────────────────────────────────────────────────────────────── */
.wrp-scroll {
  overflow-x: auto;
  margin-inline: calc(var(--s-sp-2) * -1); padding-inline: var(--s-sp-2);
  /* The wide tables do not fit a phone. The fade at the right edge is the
     only thing saying so -- a cut-off column on its own reads as a bug. */
  -webkit-mask-image: linear-gradient(90deg, #000 88%, transparent 100%);
          mask-image: linear-gradient(90deg, #000 88%, transparent 100%);
}
@media (min-width: 560px) { .wrp-scroll { -webkit-mask-image: none; mask-image: none; } }
.wrp-table { width: 100%; border-collapse: collapse; font-size: var(--s-fs-sm); }
.wrp-table th {
  padding: 8px 9px;
  text-align: left;
  font-size: var(--s-fs-xs); font-weight: 800;
  text-transform: uppercase; letter-spacing: .08em;
  color: var(--wrp-mute);
  border-bottom: 1px solid var(--wrp-line);
  white-space: nowrap;
}
.wrp-table td {
  padding: 9px;
  font-weight: 700;
  color: var(--wrp-ink);
  border-bottom: 1px solid rgba(255, 255, 255, .05);
}
.wrp-table tr:last-child td { border-bottom: 0; }
.wrp-td--good { color: var(--wrp-good); }
.wrp-td--warn { color: var(--wrp-warn); }
.wrp-td--bad  { color: var(--wrp-bad); }
.wrp-td--gold { color: var(--wrp-gold); font-weight: 900; }
.wrp-td--orange { color: var(--wrp-orange); }

/* ── Fun facts ──────────────────────────────────────────────────────────── */
.wrp-fact { display: grid; grid-template-columns: 34px 1fr; gap: var(--s-sp-3); align-items: start; }
.wrp-fact + .wrp-fact { margin-top: var(--s-sp-3); }
.wrp-fact__ic {
  width: 34px; height: 34px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 11px;
  font-size: 14px;
  color: var(--wrp-info);
  background: rgba(96, 165, 250, .12);
  border: 1px solid rgba(96, 165, 250, .24);
}
.wrp-fact__t { font-size: var(--s-fs-md); font-weight: 700; line-height: 1.55; color: var(--wrp-ink); }

/* ── Buttons on the stage ───────────────────────────────────────────────── */
.wrp-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 9px;
  width: 100%;
  padding: 13px var(--s-sp-5);
  border-radius: var(--s-r-pill);
  border: 1px solid var(--wrp-line);
  background: rgba(255, 255, 255, .07);
  color: var(--wrp-ink);
  font-size: var(--s-fs-md); font-weight: 800;
  text-decoration: none;
  cursor: pointer;
}
.wrp-btn:hover { background: rgba(255, 255, 255, .12); color: var(--wrp-ink); }
.wrp-btn--gold {
  background: rgba(251, 191, 36, .16);
  border-color: rgba(251, 191, 36, .34);
  color: var(--wrp-gold);
}
.wrp-btn--gold:hover { background: rgba(251, 191, 36, .24); color: var(--wrp-gold); }
.wrp-btn--info {
  background: rgba(96, 165, 250, .16);
  border-color: rgba(96, 165, 250, .32);
  color: var(--wrp-info);
}
.wrp-btn--info:hover { background: rgba(96, 165, 250, .24); color: var(--wrp-info); }

/* The coach-only strip at the top of the stage. */
.wrp-banner {
  padding: var(--s-sp-5);
  border-radius: 22px;
  border: 1px dashed rgba(255, 255, 255, .18);
  background: rgba(255, 255, 255, .03);
  display: flex; flex-direction: column;
  gap: var(--s-sp-3);
}
.wrp-banner__t { font-size: var(--s-fs-lg); font-weight: 900; color: var(--wrp-ink); }
.wrp-banner__p { font-size: var(--s-fs-sm); font-weight: 600; line-height: 1.55; color: var(--wrp-mute); }
.wrp-banner__p strong { color: var(--wrp-ink); }
.wrp-link { color: var(--wrp-info); font-weight: 800; text-decoration: none; }
.wrp-link:hover { color: var(--wrp-info); text-decoration: underline; }

@media (min-width: 560px) {
  .wrp-nums { grid-template-columns: repeat(4, 1fr); }
  .wrp-shouts { grid-template-columns: repeat(3, 1fr); }
}

/* Nothing on this page needs to move for the information to arrive. */
@media (prefers-reduced-motion: reduce) {
  .wrp-reveal { opacity: 1; transform: none; animation: none; }
  .wrp-row__fill { transition: none; }
  .wrp-conf { display: none; }
}

/* -- The photo tray -------------------------------------------------------
   Editing a post means editing the photos on it: dropping one, promoting one
   to the cover, adding more. v1 drew that with 130 lines of `<style>` at the
   top of edit_post.html and nowhere else, so no other page could reuse it.
   Nothing here is post-specific -- it is a grid of square thumbs with three
   controls, and the next page that edits a gallery gets it for free. */
.s-tray {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(94px, 1fr));
  gap: var(--s-sp-2);
}
.s-tray__cell {
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm);
  background: var(--s-surface-2);
}
.s-tray__cell img {
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: filter .15s, opacity .15s;
}
.s-tray__cell--cover { border: 2px solid var(--s-accent); }

/* A removal is not final until the form is saved, so the thumb stays where it
   was, greyed, with the way back written on it. v1 did the same and it is the
   right call -- an undo you can see beats a confirm you have to answer. */
.s-tray__cell--gone img { filter: grayscale(1) brightness(.55); opacity: .5; }
.s-tray__cell--gone .s-tray__star,
.s-tray__cell--gone .s-tray__tag--next { display: none; }
.s-tray__undo {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center; gap: 5px;
  border: 0; background: none; cursor: pointer;
  color: #fff; font-family: inherit;
  font-size: var(--s-fs-xs); font-weight: 900;
  text-shadow: 0 1px 3px rgba(0, 0, 0, .65);
}

.s-tray__tag {
  position: absolute; top: 5px; left: 5px;
  padding: 2px 7px;
  border-radius: var(--s-r-pill);
  background: var(--s-accent); color: var(--s-accent-on);
  font-size: var(--s-fs-xs); font-weight: 900;
  letter-spacing: .04em; text-transform: uppercase;
}
.s-tray__tag--new { background: var(--s-good); color: #fff; }
.s-tray__tag--next {
  top: auto; left: 5px; bottom: 5px;
  background: var(--s-warn); color: #fff;
}

.s-tray__x,
.s-tray__star,
.s-gifshot__x {
  position: absolute;
  width: 27px; height: 27px;
  display: flex; align-items: center; justify-content: center;
  padding: 0;
  border: 0; border-radius: 50%;
  cursor: pointer;
  box-shadow: 0 1px 5px rgba(0, 0, 0, .28);
  transition: transform .12s, background .12s, color .12s;
}
.s-tray__x,
.s-gifshot__x {
  top: 5px; right: 5px;
  background: rgba(16, 24, 40, .72); color: #fff;
  font-size: 1.05rem; line-height: 1;
}
.s-tray__x:hover,
.s-gifshot__x:hover { background: var(--s-bad); transform: scale(1.08); }
.s-tray__star {
  right: 5px; bottom: 5px;
  background: rgba(255, 255, 255, .92); color: var(--s-warn);
  font-size: var(--s-fs-sm);
}
.s-tray__star:hover { background: #fff; transform: scale(1.08); }

/* The GIF sits beside the words rather than under them -- it is one image at
   a known size, not a gallery, so it does not need the grid. */
.s-gifshot {
  position: relative;
  display: inline-block;
  max-width: 220px;
  border-radius: var(--s-r-sm);
  overflow: hidden;
}
.s-gifshot img { display: block; width: 100%; }

/* A multiple select collapses to one visible row by default, which reads as a
   dropdown that will not open. Give it enough height to look like a list. */
.s-select[multiple] {
  height: auto;
  min-height: 108px;
  padding: 6px;
}
.s-select[multiple] option {
  padding: 5px 8px;
  border-radius: 6px;
}

@media (prefers-reduced-motion: reduce) {
  .s-tray__x, .s-tray__star, .s-gifshot__x { transition: none; }
  .s-tray__x:hover, .s-tray__star:hover, .s-gifshot__x:hover { transform: none; }
}

/* ── 31. COLOUR PICKING ────────────────────────────────────────────────────
   The club-colours step of onboarding, and the appearance settings page it
   shares markup with. Nothing else in the app lets a person choose a colour,
   which is why there was no primitive for it until now: v1's onboarding
   wizard carried its own copy in a <style> block, and the settings page
   carried a second, different copy.

   Three parts: a row of one-tap presets, a manual well-plus-hex-plus-RGB
   control, and a preview showing the pair on the two surfaces a club
   actually sees them on -- the crest plate and the app bar. */

.s-swatches { display: flex; flex-wrap: wrap; gap: var(--s-sp-2); }

/* A preset is a colour and nothing else, so the button is the colour. The
   ring is drawn outside the circle with box-shadow rather than a border, so
   selecting one does not change its size and the row does not reflow. */
.s-swatch {
  width: 34px; height: 34px; flex-shrink: 0;
  padding: 0; border: 0; border-radius: 50%;
  cursor: pointer;
  box-shadow: 0 0 0 1px rgba(16, 24, 40, .16);
  transition: box-shadow .15s, transform .15s;
}
.s-swatch:hover { transform: scale(1.1); }
.s-swatch.is-on {
  box-shadow: 0 0 0 2px var(--s-surface), 0 0 0 4px var(--s-ink);
  transform: scale(1.06);
}
.s-swatch:focus-visible {
  outline: 2px solid var(--s-info); outline-offset: 3px;
}

/* The well is a plain rounded box; the native colour input is scaled up
   inside it and clipped, because every browser draws its own inset swatch
   with its own padding and the only reliable way to fill the box is to
   overflow it. */
.s-well {
  position: relative;
  width: 46px; height: 46px; flex-shrink: 0;
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  overflow: hidden;
  background: var(--s-surface-2);
}
.s-well input[type="color"] {
  position: absolute;
  top: -8px; left: -8px;
  width: calc(100% + 16px); height: calc(100% + 16px);
  padding: 0; border: 0;
  background: none;
  cursor: pointer;
}
.s-well:focus-within {
  outline: 2px solid var(--s-info); outline-offset: 2px;
}

/* Hex and RGB sit beside the well. The hex box is monospaced and sized to
   its seven characters; the three RGB boxes are the smallest input in the
   system and get their own width rather than .s-input's full-bleed one. */
.s-colour { display: flex; align-items: center; gap: var(--s-sp-2); flex-wrap: wrap; }
.s-colour__hex {
  width: 92px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  text-transform: uppercase;
}
.s-rgb { display: flex; gap: var(--s-sp-1); }
.s-rgb input {
  width: 54px;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* The preview. A club's colours are chosen for a crest and a bar, so those
   are the two things drawn -- a strip of tinted panels would not tell anyone
   whether their secondary reads against their primary. */
.s-cpreview {
  display: flex; align-items: center; gap: var(--s-sp-3);
  padding: var(--s-sp-4);
  border: 1px solid var(--s-line); border-radius: var(--s-r);
  background: var(--s-surface-2);
}
.s-cpreview__badge {
  width: 74px; height: 74px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: var(--s-r-lg);
  font-size: var(--s-fs-xl); font-weight: 900; letter-spacing: -.03em;
  box-shadow: 0 0 0 1px rgba(16, 24, 40, .12);
  transition: background .25s, color .25s;
}
.s-cpreview__bar {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: var(--s-sp-2);
  padding: var(--s-sp-3);
  border-radius: var(--s-r-sm);
  font-size: var(--s-fs-md); font-weight: 800;
  box-shadow: 0 0 0 1px rgba(16, 24, 40, .12);
  transition: background .25s, color .25s;
}
.s-cpreview__bar span {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The accent sample, on its own line under the badge and the bar.

   The secondary colour is NOT the writing on the primary -- the app computes
   that for itself (accounts.context_processors._readable_on) and always uses
   black or white. The secondary is --pp-accent / --s-accent: the colour every
   button, badge, link and progress fill is painted with. So it has to be
   previewed against a card, which is where those things live, and not against
   the primary, where it never appears.

   Three samples because the accent is used three ways and behaves differently
   in each: as a fill with computed ink on it, as a pill, and as link text --
   which the app darkens when the accent is too pale to read (_readable_text).
   All three are painted by script; none of them can be a class, because the
   colour is whatever the club just picked. */
.s-cpreview { flex-wrap: wrap; }
.s-cpreview__pills {
  flex: 1 0 100%;
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s-sp-2);
}
.s-cpreview__btn {
  padding: 8px 14px;
  border-radius: var(--s-r-sm);
  font-size: var(--s-fs-sm); font-weight: 800;
  box-shadow: 0 0 0 1px rgba(16, 24, 40, .12);
  transition: background .25s, color .25s;
}
.s-cpreview__pill {
  padding: 3px 10px;
  border-radius: var(--s-r-pill);
  font-size: var(--s-fs-xs); font-weight: 900;
  transition: background .25s, color .25s;
}
.s-cpreview__lk {
  font-size: var(--s-fs-sm); font-weight: 800;
  transition: color .25s;
}

@media (prefers-reduced-motion: reduce) {
  .s-swatch, .s-cpreview__badge, .s-cpreview__bar,
  .s-cpreview__btn, .s-cpreview__pill, .s-cpreview__lk { transition: none; }
  .s-swatch:hover, .s-swatch.is-on { transform: none; }
}

/* The eight ready-made pairs. These are classes rather than inline style=
   attributes because a v2 template may not carry inline styles at all -- see
   teamplan/tests_ui_house_style.py. Swatches generated from an uploaded badge
   set the primary only, are painted by script at runtime, and so need no class.

   A preset is two colours, not one, and the second one is every button in the
   app -- so the swatch has to show it. The ground fills the circle and the
   accent sits in a dot on the corner, ringed in the card colour so it reads
   against a dark ground and a light one alike. */
.s-swatch--pair { position: relative; }
.s-swatch--pair::after {
  content: ""; position: absolute; right: -2px; bottom: -2px;
  width: 15px; height: 15px; border-radius: 50%;
  background: var(--sw-a);
  box-shadow: 0 0 0 2px var(--s-surface);
}
.s-swatch--forest { background: #2E7D32; --sw-a: #F2B705; }
.s-swatch--royal  { background: #1565C0; --sw-a: #7EC8F5; }
.s-swatch--purple { background: #7B1FA2; --sw-a: #F2B705; }
.s-swatch--amber  { background: #F57F17; --sw-a: #1F3A5F; }
.s-swatch--crimson{ background: #C62828; --sw-a: #1F3A5F; }
.s-swatch--navy   { background: #0D1B2A; --sw-a: #F7C500; }
.s-swatch--teal   { background: #00695C; --sw-a: #4DD0B1; }
.s-swatch--slate  { background: #37474F; --sw-a: #F59E0B; }

/* ---------------------------------------------------------------------------
   A rating scale: one word per option, the whole set readable in one glance.

   .s-pickcards stacks, which is right when an option needs a sentence to be
   honest about what it does and wrong when it needs a single word -- three
   stacked cards for "Poor / OK / Great", repeated for each thing being rated,
   is nine full-width boxes on a 375px screen for what is really three taps.
   Same card, same checked and focus behaviour, laid abreast.
--------------------------------------------------------------------------- */
.s-pickcards--row { flex-direction: row; }
.s-pickcards--row .s-pickcard {
  flex: 1 1 0; min-width: 0; text-align: center; padding: 11px 4px;
}
.s-pickcards--row .s-pickcard__t { font-size: var(--s-fs-sm); }

/* ---------------------------------------------------------------------------
   A group of radios that belong to one question.

   The right markup is a fieldset with a legend, and a browser gives both a
   border and an indent of their own -- so without a rule here, every page
   that does the accessible thing pays for it visually and reaches for a
   <div> instead. min-width:0 because a fieldset carries an intrinsic minimum
   width that stops it shrinking inside a flex or grid parent.

   Called .s-fieldset and not .s-group. It was .s-group, which collided with
   the card that opens further up this file: that one is only ever written as
   <details class="s-card s-card--flush s-group">, and a bare .s-group rule
   carrying border:0 sits later in the cascade than .s-card at the same
   specificity, so every one of those cards across the app was losing its
   border. Two different components cannot share a class name.
--------------------------------------------------------------------------- */
.s-fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
.s-fieldset > legend { padding: 0; }

/* ---------------------------------------------------------------------------
   A price, and the period it buys.

   The figure is the thing being compared, so it carries the weight; the period
   is qualifying information and sits at reading size beside it. Both pages
   that show a membership price were setting this inline -- 1.8rem, weight 900,
   a hand-typed letter-spacing -- so the two did not match each other.
   tabular-nums so a column of prices lines up on the decimal point.
--------------------------------------------------------------------------- */
.s-price {
  font-size: var(--s-fs-2xl); font-weight: 900;
  letter-spacing: -.02em; line-height: 1.05;
  color: var(--s-ink);
  font-variant-numeric: tabular-nums;
}
.s-price__per {
  font-size: var(--s-fs-sm); font-weight: 700;
  letter-spacing: 0; color: var(--s-ink-2);
  margin-left: 4px;
}

/* ---------------------------------------------------------------------------
   The organiser board — club:league_organise

   A club's leagues, their divisions, and the teams inside them, arranged so a
   team can be dragged from one division to another. The board is the only
   screen in the app that moves a record by dragging it, so it needs a small
   vocabulary of its own: a drop list, a team card, a lift state, a placeholder
   showing where the card will land.

   Everything else on the page is house furniture — .s-card--flush for a
   league, .s-sect for its heading, .s-row inside .s-sheetm for the "move to"
   picker. The picker is a bottom sheet rather than a floating menu because a
   floating menu taller than a phone screen has nothing to scroll.
--------------------------------------------------------------------------- */
.s-org { display: flex; flex-direction: column; gap: var(--s-sp-3); }
/* While a card is in the air, stop the page selecting text under the finger. */
.s-org.is-dragging { user-select: none; -webkit-user-select: none; }

.s-org__lghead { display: flex; align-items: center; gap: 10px; }
.s-org__lgid { flex: 1; min-width: 0; }
.s-org__lgn {
  display: block; font-size: var(--s-fs-md); font-weight: 900;
  color: var(--s-ink); text-decoration: none;
}
.s-org__lgn:hover { text-decoration: underline; }
.s-org__lgs {
  display: block; margin-top: 2px;
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink-2);
}

/* Up/down a step. Two one-line forms stacked, so the pair is as tall as the
   thing it sits beside and the arrows read as one control. */
.s-org__arrows { display: flex; flex-direction: column; gap: 3px; flex-shrink: 0; }
.s-org__arrow {
  width: 27px; height: 20px;
  display: flex; align-items: center; justify-content: center;
  padding: 0; border: 1px solid var(--s-line); border-radius: 6px;
  background: var(--s-surface); color: var(--s-ink-2);
  font-size: 10px; cursor: pointer;
}
.s-org__arrow:hover:not(:disabled) { border-color: var(--s-good); color: var(--s-good); }
.s-org__arrow:disabled { opacity: .35; cursor: default; }

.s-org__dv { border-top: 1px solid var(--s-line-soft); }
.s-org__dvhead {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 9px var(--s-sp-3);
  background: var(--s-surface-2);
}
.s-org__dvn { font-size: var(--s-fs-sm); font-weight: 900; color: var(--s-ink); }
.s-org__dvnone {
  padding: var(--s-sp-3); border-top: 1px solid var(--s-line-soft);
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink-2);
}

/* A division's teams, and the place a dragged team can be dropped. */
.s-org__list {
  list-style: none; margin: 0; padding: var(--s-sp-2);
  display: flex; flex-direction: column; gap: 6px;
}
.s-org__list.is-over {
  background: var(--s-good-bg);
  outline: 2px dashed var(--s-good); outline-offset: -3px;
}
.s-org__none {
  padding: 12px; text-align: center;
  border: 1.5px dashed var(--s-line); border-radius: var(--s-r-sm);
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-muted);
}

.s-org__t {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px;
  border: 1px solid var(--s-line); border-radius: var(--s-r-sm);
  background: var(--s-surface);
}
/* The grip is the only part a finger can drag from, so the rest of the card
   still scrolls the page. touch-action: none stops the browser claiming the
   gesture before the pointer handler sees it. */
.s-org__grip {
  flex-shrink: 0; padding: 2px 4px;
  color: var(--s-muted); font-size: 12px;
  cursor: grab; touch-action: none;
}
.s-org__tb { flex: 1; min-width: 0; }
.s-org__tn {
  display: block; font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.s-org__ta { display: block; font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted); }
.s-org__move {
  flex-shrink: 0; width: 32px; height: 32px;
  display: flex; align-items: center; justify-content: center;
  padding: 0; border: 1px solid var(--s-line); border-radius: 8px;
  background: var(--s-surface); color: var(--s-ink-2); cursor: pointer;
}
.s-org__move:hover { border-color: var(--s-good); color: var(--s-good); }

/* The card is hidden where it started, a copy follows the pointer, and a
   dashed slot shows where it lands. */
.s-org__t.is-lifted { display: none; }
.s-org__t.is-ghost {
  position: fixed; top: 0; left: 0; z-index: 9000;
  margin: 0; pointer-events: none; opacity: .95;
  box-shadow: var(--s-shadow-lg);
}
.s-org__ph {
  border: 1.5px dashed var(--s-good); border-radius: var(--s-r-sm);
  background: var(--s-good-bg);
}

/* A picker row that is already the answer. */
button.s-row:disabled { opacity: .5; cursor: default; }

/* A file drop zone: a label wrapping a hidden file input, so the whole area
   opens the picker on a tap and takes a dragged file on a desktop. Pair it
   with .s-card.s-card--dashed for the outline. */
.s-dropzone { display: block; cursor: pointer; }
.s-dropzone:hover { border-color: var(--s-brand-ink); }
.s-dropzone.is-over {
  border-color: var(--s-brand-ink);
  background: color-mix(in srgb, var(--s-brand-ink) 7%, var(--s-surface));
}

/* ── MATCH PLAN BOARD ──────────────────────────────────────────────────
   The tactics pitch, the rotation grid and the formation pickers only
   exist on one page: matchplans/interactive_match_plan. Nothing else in
   the app draws a football pitch, so these are scoped under .s-mp and
   never leak. The short child names are the ones that page's own script
   queries and writes — renaming them would mean rewriting 1,600 lines of
   working JavaScript, so they stay as they are and the scope keeps them
   contained. */

.s-mp { display: block; }

/* The board is dark on purpose: white markers and a green pitch need a
   dark ground to read, in either theme. */
.s-mp .tactics-board {
  background: linear-gradient(160deg, #16203a, #0d1526);
  border: 1px solid rgba(148,163,184,.22);
  border-radius: var(--s-r);
  overflow: hidden;
  box-shadow: var(--s-shadow);
}
.s-mp .tactics-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s-sp-2); flex-wrap: wrap;
  padding: var(--s-sp-3) var(--s-sp-4);
  border-bottom: 1px solid rgba(255,255,255,.1);
  color: #fff; font-weight: 800; font-size: var(--s-fs-md);
}
.s-mp .tactics-inner {
  display: grid; gap: var(--s-sp-4);
  padding: var(--s-sp-4);
  container-type: inline-size;
}
@container (min-width: 520px) {
  .s-mp .tactics-inner { grid-template-columns: 1fr minmax(180px, 240px); align-items: start; }
}
.s-mp .tactics-side { display: grid; gap: var(--s-sp-4); align-content: start; }
.s-mp .tactics-label {
  font-size: var(--s-fs-xs); font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase; color: rgba(226,232,240,.6);
  margin-bottom: var(--s-sp-1);
}
.s-mp .tactics-hint { font-size: var(--s-fs-xs); color: rgba(226,232,240,.72); line-height: 1.7; }
.s-mp .tactics-hint b { color: #fff; }
.s-mp .tactics-hint i { width: 14px; text-align: center; color: var(--s-accent); }

.s-mp .pitch-container {
  background: #14532d;
  border-radius: var(--s-r-sm);
  padding: 10px;
  position: relative;
  aspect-ratio: 2 / 3;
  width: 100%;
  max-width: 300px;
  margin: 0 auto;
}
@container (min-width: 520px) { .s-mp .pitch-container { max-width: 340px; } }
.s-mp .pitch {
  width: 100%; height: 100%;
  border: 3px solid #fff; border-radius: 6px;
  position: relative;
  box-shadow: inset 0 0 0 1px rgba(15,23,42,.5);
  transition: transform .15s;
}
.s-mp .pitch::before {
  content: ""; position: absolute; left: 0; top: 50%;
  width: 100%; height: 2px; background: rgba(255,255,255,.75);
}
.s-mp .pitch::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 38%; height: 38%; border: 2px solid rgba(255,255,255,.75);
  border-radius: var(--s-r-pill); transform: translate(-50%, -50%);
}
.s-mp .pitch.pitch-rotated { transform: rotate(180deg); }
.s-mp .pitch.pitch-rotated .player-marker { transform: translate(-50%, -50%) rotate(180deg); }

/* A player on the pitch: an avatar circle with a caption under it. */
.s-mp .player-marker {
  position: absolute; transform: translate(-50%, -50%);
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  line-height: 1; cursor: grab; user-select: none;
}
.s-mp .player-marker:active { cursor: grabbing; }
.s-mp .pm-av {
  width: 42px; height: 42px; border-radius: var(--s-r-pill); overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: linear-gradient(135deg, #60a5fa, #1d4ed8);
  color: #fff; font-weight: 800; font-size: var(--s-fs-sm);
  border: 2px solid #fff; box-shadow: 0 6px 16px rgba(15,23,42,.45);
}
.s-mp .pm-av img { width: 100%; height: 100%; object-fit: cover; }
.s-mp .pm-av.pm-empty {
  background: rgba(15,23,42,.35); border-style: dashed;
  color: rgba(248,250,252,.85); font-weight: 400;
}
.s-mp .pm-cap {
  font-size: var(--s-fs-xs); font-weight: 800; color: #fff;
  background: rgba(2,6,23,.72); padding: 1px 7px; border-radius: var(--s-r-pill);
  white-space: nowrap; max-width: 96px; overflow: hidden; text-overflow: ellipsis;
}
.s-mp .pm-rating {
  font-size: 10px; font-weight: 800; color: #fff;
  background: rgba(245,158,11,.95); padding: 0 5px;
  border-radius: var(--s-r-pill); margin-left: 4px;
}

/* Formation and period pickers. Both sit on the dark board, so they carry
   their own colours rather than borrowing .s-seg. */
.s-mp .formation-presets,
.s-mp .quarter-presets { display: flex; flex-wrap: wrap; gap: var(--s-sp-1); }
.s-mp .formation-btn,
.s-mp .quarter-btn {
  padding: 6px 14px; background: rgba(15,23,42,.7); color: #e5e7eb;
  border: 1px solid rgba(148,163,184,.7); border-radius: var(--s-r-pill);
  font-weight: 700; font-size: var(--s-fs-sm); cursor: pointer;
  transition: background .15s, color .15s, border-color .15s;
}
.s-mp .formation-btn:hover, .s-mp .formation-btn.active {
  background: #f97316; border-color: #fed7aa; color: #111827;
}
.s-mp .quarter-btn:hover, .s-mp .quarter-btn.active {
  background: var(--s-good); border-color: #bbf7d0; color: #052e16;
}
.s-mp .formation-info {
  margin-top: var(--s-sp-2); font-size: var(--s-fs-xs);
  line-height: 1.5; color: rgba(226,232,240,.9);
}
.s-mp .fi-kind {
  display: inline-block; font-weight: 800; font-size: 10px; text-transform: uppercase;
  letter-spacing: .05em; padding: 1px 8px; border-radius: var(--s-r-pill); margin-bottom: 5px;
}
.s-mp .fi-kind.k-Defensive { background: rgba(59,130,246,.22); color: #93c5fd; }
.s-mp .fi-kind.k-Attacking { background: rgba(239,68,68,.22); color: #fca5a5; }
.s-mp .fi-kind.k-Balanced  { background: rgba(34,197,94,.22); color: #86efac; }
.s-mp .fi-row { display: flex; gap: 6px; align-items: flex-start; margin-top: 3px; }
.s-mp .fi-row .fi-ic { width: 12px; flex: none; text-align: center; font-weight: 800; }
.s-mp .fi-row.pro .fi-ic { color: #4ade80; }
.s-mp .fi-row.con .fi-ic { color: #f87171; }

/* The rotation grid. One row per player, one square per period. */
.s-mp .rotation-table { width: 100%; border-collapse: separate; border-spacing: 0; }
.s-mp .rotation-table th {
  background: var(--s-surface-2); padding: var(--s-sp-2);
  font-weight: 800; font-size: var(--s-fs-xs); text-transform: uppercase;
  letter-spacing: .05em; color: var(--s-muted); text-align: center;
  white-space: nowrap;
}
.s-mp .rotation-table th:first-child { text-align: left; padding-left: var(--s-sp-4); }
.s-mp .rotation-table td {
  padding: var(--s-sp-2); border-bottom: 1px solid var(--s-line-soft);
  font-size: var(--s-fs-sm); text-align: center; color: var(--s-ink);
}
.s-mp .rotation-table td:first-child { text-align: left; padding-left: var(--s-sp-4); }
.s-mp .rotation-table tbody tr:last-child td { border-bottom: 0; }

.s-mp .rp-id { display: flex; align-items: center; gap: var(--s-sp-2); }
.s-mp .rp-avatar {
  width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
  font-weight: 800; font-size: var(--s-fs-sm); color: #fff;
  background: linear-gradient(135deg, #6366f1, #8b5cf6);
}
.s-mp .rp-avatar img { width: 100%; height: 100%; object-fit: cover; }
.s-mp .rp-name {
  font-weight: 800; font-size: var(--s-fs-sm); line-height: 1.2;
  color: var(--s-ink); white-space: nowrap;
}
.s-mp .rp-meta { display: flex; align-items: center; gap: 5px; margin-top: 3px; flex-wrap: wrap; }
.s-mp .rp-pos {
  font-size: 10px; font-weight: 800; letter-spacing: .03em;
  color: var(--s-violet); background: var(--s-violet-bg);
  border-radius: 5px; padding: 1px 5px;
}
.s-mp .rp-rating {
  font-size: 10px; font-weight: 800; color: var(--s-warn); background: var(--s-warn-bg);
  border-radius: 5px; padding: 1px 5px; display: inline-flex; align-items: center; gap: 3px;
}
.s-mp .rp-minbar {
  margin-top: var(--s-sp-1); height: 5px; width: 96px; max-width: 100%;
  border-radius: var(--s-r-pill); background: var(--s-line); overflow: hidden;
}
.s-mp .rp-body { min-width: 0; }
.s-mp .rp-minbar > i { display: block; width: 0; height: 100%; border-radius: var(--s-r-pill); background: var(--s-good); }
.s-mp .rp-minbar.hi > i { background: #d97706; }
.s-mp .rp-minbar.lo > i { background: var(--s-bad); }
.s-mp .rp-mins-label { font-size: 10px; font-weight: 700; color: var(--s-muted); margin-top: 2px; }
body.hide-ratings .s-mp .rp-rating,
body.hide-ratings .s-mp .pm-rating { display: none; }

.s-mp .quarter-cell {
  width: 42px; height: 42px; margin: 0 auto;
  border: 2px solid var(--s-line); border-radius: var(--s-r-sm);
  background: var(--s-surface); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background .15s, border-color .15s, color .15s;
}
.s-mp .quarter-cell:hover { border-color: var(--s-info); background: var(--s-info-bg); }
.s-mp .quarter-cell.selected {
  background: var(--s-good); border-color: var(--s-good); color: #fff;
  font-weight: 800; font-size: var(--s-fs-md);
}
.s-mp .quarter-cell.selected::after { content: "\2713"; }
.s-mp .quarter-cell.disabled {
  opacity: .35; cursor: not-allowed; pointer-events: none; background: var(--s-surface-2);
}
.s-mp .quarter-count {
  display: inline-block; min-width: 20px; padding: 0 5px;
  border-radius: var(--s-r-pill); background: var(--s-slate-bg); color: var(--s-slate);
  font-size: 10px; font-weight: 800;
}
.s-mp .quarter-count.is-full { background: var(--s-info-bg); color: var(--s-info); }
.s-mp .quarter-count.live-warning { background: var(--s-bad-bg); color: var(--s-bad); }
.s-mp .minutes-display { font-weight: 800; color: var(--s-info); }
.s-mp .total-display { font-weight: 700; color: var(--s-ink-2); }

/* The match-day register carries five columns - a name and four controls - and
   all five have to fit on a phone, so its gutters are tighter than the shared
   table's and a long name wraps rather than pushing Clear off the screen. */
.s-mp .mp-att th, .s-mp .mp-att td { padding: 10px 6px; text-align: center; }
.s-mp .mp-att th:first-child,
.s-mp .mp-att td:first-child { text-align: left; padding-left: var(--s-sp-3); }

/* The algorithm comparison: one small card per option, each a list of players
   with a bar for the minutes that option would give them. */
.s-mp .algo-grid {
  display: grid; gap: var(--s-sp-3); margin-top: var(--s-sp-3);
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
}
.s-mp .algo-card {
  background: var(--s-surface); border: 1px solid var(--s-line);
  border-radius: var(--s-r-sm); padding: 10px 12px;
}
.s-mp .algo-t { font-weight: 900; font-size: var(--s-fs-sm); color: var(--s-ink); }
.s-mp .algo-b { font-size: var(--s-fs-xs); color: var(--s-muted); margin-bottom: 8px; }
.s-mp .algo-row {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--s-fs-xs); margin: 2px 0;
}
.s-mp .algo-nm { flex: 0 0 74px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.s-mp .algo-bar {
  flex: 1; height: 7px; border-radius: var(--s-r-pill);
  background: var(--s-surface-2); overflow: hidden;
}
.s-mp .algo-bar > i { display: block; width: 0; height: 100%; background: var(--s-violet); }
.s-mp .algo-m { flex: 0 0 58px; text-align: right; font-weight: 800; font-variant-numeric: tabular-nums; }
.s-mp .algo-up { color: var(--s-good); }
.s-mp .algo-dn { color: var(--s-bad); }

@media (max-width: 420px) {
  .s-mp .quarter-cell { width: 36px; height: 36px; }
  .s-mp .rotation-table th, .s-mp .rotation-table td { padding: var(--s-sp-1); }
  .s-mp .rotation-table th:first-child, .s-mp .rotation-table td:first-child { padding-left: var(--s-sp-2); }
}

/* A save-state pip: the page says whether your typing has landed. */
.s-savestate {
  display: inline-flex; align-items: center; gap: var(--s-sp-1);
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted);
  white-space: nowrap;
}
.s-savestate i { font-size: 7px; }
.s-savestate.is-saving { color: var(--s-warn); }
.s-savestate.is-saved  { color: var(--s-good); }
.s-savestate.is-error  { color: var(--s-bad); }

/* GUIDED FLOW — below 992px the plan collapses to one long column, so each
   stage gets a "Done - next" button that ticks it off and scrolls on. The
   switch at the top of the page turns it off and the choice is remembered on
   that device. Sections are marked with data-flowsec so the script never has
   to know how a card is built. */
.s-mp .flow-next { display: none; }
.s-mp [data-flowsec] { position: relative; }
.s-mp [data-flowsec].flow-done::before {
  content: "\2713";
  position: absolute; top: 10px; right: 10px; z-index: 2;
  width: 22px; height: 22px; border-radius: var(--s-r-pill);
  display: flex; align-items: center; justify-content: center;
  background: var(--s-good-bg); color: var(--s-good);
  font-size: 12px; font-weight: 900;
}
@media (max-width: 991.98px) {
  body.flow-on .s-mp .flow-next {
    display: flex; align-items: center; justify-content: center;
    gap: var(--s-sp-2);
    width: 100%; margin: var(--s-sp-3) 0 0;
    padding: 10px var(--s-sp-3);
    border-radius: var(--s-r-sm);
    border: 1px dashed var(--s-line);
    background: transparent; color: var(--s-muted);
    font-weight: 700; font-size: var(--s-fs-xs);
  }
  body.flow-on .s-mp .tactics-board .flow-next {
    color: rgba(255,255,255,.75); border-color: rgba(255,255,255,.3);
  }
  .s-mp [data-flowsec] { scroll-margin-top: 78px; }
}
.s-savebar.flow-pulse { animation: sFlowPulse 1s ease 2; }
@keyframes sFlowPulse {
  50% { box-shadow: 0 -6px 24px color-mix(in srgb, var(--s-good) 50%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .s-savebar.flow-pulse { animation: none; }
}

/* ── How to: the live walkthrough ──────────────────────────────────────────
   A spotlight ring over the real control, and a card that says what to do
   with it. Both are fixed: the ring is positioned from JS on every frame
   that matters, so it carries no size of its own here.

   Sits above everything, including the drawer (60) and the picker (1201),
   because it has to survive being started from either.
--------------------------------------------------------------------------- */
.s-howto {
  position: fixed; inset: 0; z-index: 1300;
  pointer-events: none;
}
.s-howto__ring {
  position: fixed; top: 0; left: 0; width: 0; height: 0;
  border-radius: var(--s-r-sm);
  border: 2px solid var(--s-violet);
  box-shadow: 0 0 0 9999px rgba(2, 6, 23, .58);
  pointer-events: none;
  transition: top .2s ease, left .2s ease, width .2s ease, height .2s ease;
}
/* No control to point at: dim the lot rather than ring an empty box. */
.s-howto__ring.is-flat {
  top: 0; left: 0; width: 100%; height: 100%;
  border: 0; border-radius: 0;
  box-shadow: none; background: rgba(2, 6, 23, .58);
}

.s-howto__card {
  position: fixed; left: 12px; right: 12px;
  bottom: calc(var(--s-nav-h) + 12px);
  max-width: 560px; margin: 0 auto;
  padding: var(--s-sp-3);
  border-radius: var(--s-r);
  border: 1px solid var(--s-violet-line);
  background: var(--s-surface);
  box-shadow: var(--s-shadow-lg);
  pointer-events: auto;
}
/* The control is low on the screen, so the card moves out of its way. */
.s-howto__card.is-top {
  bottom: auto;
  top: calc(12px + env(safe-area-inset-top, 0px));
}

.s-howto__bar {
  height: 3px; border-radius: var(--s-r-pill);
  background: var(--s-violet-bg);
  margin: 0 0 var(--s-sp-3);
  overflow: hidden;
}
.s-howto__bar > i {
  display: block; height: 100%;
  background: var(--s-violet);
  transition: width .2s ease;
}

.s-howto__top {
  display: flex; align-items: center; gap: var(--s-sp-2);
  margin-bottom: var(--s-sp-2);
}
.s-howto__n {
  flex: 0 0 auto;
  padding: 2px 8px; border-radius: var(--s-r-pill);
  background: var(--s-violet-bg); color: var(--s-violet);
  font-size: var(--s-fs-xs); font-weight: 800;
}
.s-howto__ttl {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--s-ink-2); font-size: var(--s-fs-xs); font-weight: 700;
}
.s-howto__ic {
  flex: 0 0 auto;
  width: 30px; height: 30px; border-radius: var(--s-r-sm);
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--s-line);
  background: var(--s-surface); color: var(--s-ink-2);
  cursor: pointer;
}
.s-howto__ic:hover { background: var(--s-bg); color: var(--s-ink); }

.s-howto__txt {
  color: var(--s-ink); font-size: var(--s-fs-md); font-weight: 700;
  line-height: 1.35;
}
.s-howto__note {
  margin-top: var(--s-sp-1);
  color: var(--s-ink-2); font-size: var(--s-fs-sm); line-height: 1.4;
}
.s-howto__acts {
  display: flex; gap: var(--s-sp-2); margin-top: var(--s-sp-3);
}
.s-howto__acts .s-btn { flex: 1 1 0; justify-content: center; }

/* You wandered off the page this step is on. */
.s-howto__warn {
  display: flex; align-items: center; gap: var(--s-sp-2); flex-wrap: wrap;
  margin-top: var(--s-sp-3); padding: var(--s-sp-2) var(--s-sp-3);
  border-radius: var(--s-r-sm);
  background: var(--s-warn-bg); color: var(--s-warn);
  font-size: var(--s-fs-sm); font-weight: 600;
}
.s-howto__warn > span { flex: 1 1 140px; }

/* Paused: everything gets out of the way except the way back in. */
.s-howto__pill {
  position: fixed; left: 12px;
  bottom: calc(var(--s-nav-h) + 12px);
  z-index: 1301;
  display: flex; align-items: center; gap: var(--s-sp-2);
  padding: 6px 6px 6px var(--s-sp-3);
  border-radius: var(--s-r-pill);
  border: 1px solid var(--s-violet-line);
  background: var(--s-surface);
  box-shadow: var(--s-shadow-lg);
  color: var(--s-ink-2); font-size: var(--s-fs-xs); font-weight: 700;
}

@media (min-width: 992px) {
  .s-howto__card,
  .s-howto__pill { bottom: 16px; }
}
@media (prefers-reduced-motion: reduce) {
  .s-howto__ring,
  .s-howto__bar > i { transition: none; }
}

/* -- The training grid, one row per session -------------------------------
   Every session the club has, with the eight fields anyone actually edits
   sitting in the row itself. It is the same problem .s-sched solves for a
   fixture list with a different set of columns, so it gets its own grid:
   borrowing one whose areas are called "opp" and "ha" would leave the next
   reader holding two meanings for one name.

   Every track is minmax(0, ...), never a bare fr. A grid item's floor is its
   own content, so one long venue name inside a <select> is enough to push the
   whole row off the right edge of a 375px screen.

   The micro-labels stay on at every width. A heading strip would be a second
   copy of the column order to keep in step, and this row rearranges at three
   breakpoints; the labels travel with the field instead.

   Date, time and team are not fields here. Both open the full edit form,
   because moving a session emails the parents and offers to move the rest of
   the run with it, and that decision needs the dates on screen. */
.s-tg__r {
  display: grid;
  grid-template-columns: 22px minmax(0, 1fr) minmax(0, 1fr) 34px;
  grid-template-areas:
    "pick  when   when   open"
    ".     team   team   team"
    ".     venue  venue  venue"
    ".     stat   stat   stat"
    ".     term   term   term"
    ".     plan   plan   plan"
    ".     mins   meet   meet"
    ".     acc    acc    acc"
    ".     note   note   note"
    ".     err    err    err";
  gap: 7px 8px;
  padding: var(--s-sp-3);
  border-bottom: 1px solid var(--s-line);
  /* On every row, not only the changed ones: a border that appears on edit
     would shift the whole row 3px sideways as you type into it. */
  border-left: 3px solid transparent;
}
.s-tg__r:last-child { border-bottom: none; }
.s-tg__r > * { min-width: 0; }

.s-tg__pick  { grid-area: pick; align-self: end; height: 43px; display: inline-flex; align-items: center; }
.s-tg__when  { grid-area: when; align-self: end; }
.s-tg__team  { grid-area: team; }
.s-tg__venue { grid-area: venue; }
.s-tg__stat  { grid-area: stat; }
.s-tg__term  { grid-area: term; }
.s-tg__plan  { grid-area: plan; }
.s-tg__mins  { grid-area: mins; }
.s-tg__meet  { grid-area: meet; }
.s-tg__acc   { grid-area: acc; }
.s-tg__note  { grid-area: note; }
.s-tg__open  { grid-area: open; align-self: end; }
.s-tg__err   { grid-area: err; }

/* A row with unsaved edits, and a row the server refused. Amber and red rather
   than a dot, because the point of the marker is to be findable while you
   scroll a phone through forty rows looking for what you changed. */
.s-tg__r.is-dirty { border-left-color: var(--s-warn); background: var(--s-warn-bg); }
.s-tg__r.is-bad   { border-left-color: var(--s-bad);  background: var(--s-bad-bg); }
.s-tg__r.is-saved { border-left-color: var(--s-good); }

.s-tg__err {
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-bad);
}
.s-tg__err:empty { display: none; }

/* Date, time and team read as text and link to the full form. The date is the
   one thing on the row you navigate by, so it takes the row's largest type. */
.s-tg__when a, .s-tg__team a { color: inherit; text-decoration: none; }
.s-tg__when a:hover, .s-tg__team a:hover { text-decoration: underline; }
.s-tg__d { font-weight: 900; font-size: var(--s-fs-sm); color: var(--s-ink); }
.s-tg__t { font-weight: 800; font-size: var(--s-fs-sm); color: var(--s-ink-2); }
.s-tg__nm { font-weight: 800; font-size: var(--s-fs-sm); color: var(--s-ink); }
.s-tg__sub {
  display: block; margin-top: 2px;
  font-size: var(--s-fs-xs); font-weight: 700; color: var(--s-muted);
}

/* A finished or cancelled session still has to be editable (a register gets
   taken late, a venue was wrong all along), so it is dimmed rather than
   locked. */
.s-tg__r.is-past .s-tg__d, .s-tg__r.is-past .s-tg__t { color: var(--s-muted); }

/* Facility access is one checkbox, and a checkbox with its label above it in
   a 10px eyebrow reads as a field with no value. It gets its label inline. */
.s-tg__acc label {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink-2);
  cursor: pointer;
}

/* Laptop: the row's first line becomes the columns of a sheet, and the fields
   nobody sorts by fall to the lines below it. */
@media (min-width: 992px) {
  .s-tg__r {
    grid-template-columns:
      22px minmax(0, 128px) minmax(0, 1fr) minmax(0, 1.1fr) minmax(0, 112px) minmax(0, 1fr) 34px;
    grid-template-areas:
      "pick when team  venue stat term open"
      ".    plan plan  mins  meet acc  ."
      ".    note note  note  note note ."
      ".    err  err   err   err  err  .";
  }
  .s-tg__pick, .s-tg__open { height: 38px; }
}

/* The heading strip, wide screens only, because below that the row is several
   lines and each field carries its own micro-label instead. */
.s-tg__hd { display: none; }

/* Wide: one line, which is what a spreadsheet is. Below this the eleven
   fields cannot each hold a readable select. */
@media (min-width: 1400px) {
  .s-tg__r, .s-tg__hd {
    grid-template-columns:
      22px minmax(0, 122px) minmax(0, 1fr) minmax(0, 1.05fr) minmax(0, 104px)
      minmax(0, 0.9fr) minmax(0, 0.9fr) 64px 64px 58px minmax(0, 1.1fr) 34px;
  }
  .s-tg__r {
    grid-template-areas:
      "pick when team venue stat term plan mins meet acc note open"
      ".    err  err  err   err  err  err  err  err  err  err  .";
  }

  .s-tg__hd {
    display: grid;
    gap: 7px 8px;
    padding: 9px var(--s-sp-3);
    /* The rows carry a 3px edge marker, so the strip carries the same width of
       nothing at all and its headings line up with the fields under them. */
    border-left: 3px solid transparent;
    border-bottom: 1px solid var(--s-line);
    background: var(--s-surface-2);
    font-size: var(--s-fs-xs); font-weight: 900;
    letter-spacing: .07em; text-transform: uppercase;
    color: var(--s-slate);
  }

  /* Clipped, not display: none. The strip above is decorative to a screen
     reader, so these are still the only name each select has. */
  .s-tg__lab, .s-tg__acct {
    position: absolute; width: 1px; height: 1px;
    margin: 0; padding: 0; overflow: hidden;
    clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }
  .s-tg__acc label { gap: 0; }
}

/* The sheet needs more than a reading column: eleven fields across a 1120px
   cap left every one of them about 97px, which is four characters of a term
   name. This is the only page that lifts it, and it is tied to the same
   breakpoint as the one-line row above. Raising that breakpoint instead would
   change nothing, because the cap does not grow with the screen. */
@media (min-width: 1400px) {
  .s-main:has(.s-tg__r) { max-width: 1560px; }
}

/* Laptop and up the fields are columns of a sheet rather than a form, so they
   take the sheet's own type size and the padding a column can afford. */
@media (min-width: 992px) {
  .s-tg__r .s-input, .s-tg__r .s-select {
    padding: 8px 10px; font-size: var(--s-fs-sm);
  }
  .s-tg__r .s-select { padding-right: 30px; }
}

/* The count of unsaved rows, sitting in the save bar next to Save. The bar's
   ground is var(--s-ink), so its text takes var(--s-surface) the way
   .s-bulk__n does; without that these two inherited body ink and were black
   on black. --s-bad-line rather than --s-bad for the error, because it is the
   red that flips with the theme and so stays readable on both bars. */
.s-tg__n {
  font-weight: 900; font-size: var(--s-fs-sm);
  color: var(--s-surface); font-variant-numeric: tabular-nums;
}
.s-tg__msg { font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-surface); }
.s-tg__msg.is-bad { color: var(--s-bad-line); }

/* ==========================================================================
   KIT HAND-OUT GRID (.s-kg)

   Hand-out day as a sheet: one row per player, grouped under their team,
   nothing written until the one button at the bottom. A row carries three
   kinds of pending change (give, return, swap-return) and the row is the
   unit of success or failure, so it borrows the training grid's marker
   scheme wholesale: amber edge while dirty, red edge when the server said
   no, green edge once landed. The border-left is on every row so the edge
   appearing does not shift the row sideways.
   ========================================================================== */
.s-kg { border: 1px solid var(--s-line); border-radius: var(--s-r-lg); overflow: hidden; background: var(--s-surface); }

/* One team's stretch of the sheet. The header IS the switch that folds the
   team away (a coach handing out to one squad does not want to scroll past
   four others), so it leans on .s-switch for its layout and only paints the
   band. */
.s-kg__team.s-switch {
  align-items: center;
  padding: 9px var(--s-sp-3);
  background: var(--s-surface-2);
  border-bottom: 1px solid var(--s-line);
}
.s-kg [data-sec] + [data-sec] .s-kg__team { border-top: 1px solid var(--s-line); }

.s-kg__r {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas:
    "who"
    "held"
    "give"
    "err";
  gap: 7px 10px;
  padding: var(--s-sp-3);
  border-bottom: 1px solid var(--s-line);
  border-left: 3px solid transparent;
}
.s-kg__r:last-child { border-bottom: none; }
.s-kg__r > * { min-width: 0; }
.s-kg__who  { grid-area: who; }
.s-kg__held { grid-area: held; }
.s-kg__give { grid-area: give; }
.s-kg__err  { grid-area: err; }

.s-kg__r.is-dirty { border-left-color: var(--s-warn); background: var(--s-warn-bg); }
.s-kg__r.is-bad   { border-left-color: var(--s-bad);  background: var(--s-bad-bg); }
.s-kg__r.is-saved { border-left-color: var(--s-good); }

.s-kg__err { font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-bad); }
.s-kg__err:empty { display: none; }

.s-kg__nm { font-weight: 900; font-size: var(--s-fs-sm); color: var(--s-ink); }

/* What the family asked for, worn as chips under the name. A size matched
   only by a typed name on the public form is a guess and dresses like one. */
.s-kg__ask { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
.s-kg__askc {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 1px 8px; border-radius: 999px;
  border: 1px solid var(--s-line);
  font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-ink-2);
  background: var(--s-surface);
}
.s-kg__askc.is-guess { border-style: dashed; color: var(--s-muted); }

/* One held item: what the record says they have, and the three-way answer
   (keeping it, handed it back, handed back a different size). The size
   picker only exists while "different size" is chosen. */
.s-kg__h {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
  padding: 3px 0;
}
.s-kg__hwhat { font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-ink-2); }
.s-kg__h .s-select, .s-kg__give .s-select { width: auto; max-width: 100%; }

/* The give control: the stock picker, the one-press fill from the asked-for
   sizes, and the queue of picks waiting for Apply, each with its own x. */
.s-kg__pick { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.s-kg__tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 5px; }
.s-kg__tag {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 2px 6px 2px 9px; border-radius: 999px;
  background: var(--s-warn-bg); border: 1px solid var(--s-warn);
  font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-ink);
}
.s-kg__tag.is-done { background: transparent; border-color: var(--s-good); }
.s-kg__tag button {
  border: 0; background: none; padding: 0 2px; cursor: pointer;
  font-weight: 900; color: var(--s-ink-2); line-height: 1;
}

/* Laptop: the row becomes the three columns of a sheet. */
@media (min-width: 992px) {
  .s-kg__r {
    grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr) minmax(0, 1.2fr);
    grid-template-areas:
      "who held give"
      "err err  err";
  }
}

/* The bar's ground is var(--s-ink); its text takes var(--s-surface) and its
   errors the theme-flipping red, exactly as the training grid learned. */
.s-kg__n { font-weight: 900; font-size: var(--s-fs-sm); color: var(--s-surface); font-variant-numeric: tabular-nums; }
.s-kg__msg { font-size: var(--s-fs-sm); font-weight: 700; color: var(--s-surface); }
.s-kg__msg.is-bad { color: var(--s-bad-line); }

/* The register's autosave note, sat in the bar between the counts and "All
   present". Muted while a tap is in flight, green once it has landed, red
   with the words "tap again" when it has not: the tick on the row stays put
   either way, so the coach never has to guess which state the row is in. */
.s-saved { font-size: var(--s-fs-xs); font-weight: 800; color: var(--s-muted); white-space: nowrap; min-height: 1em; }
.s-saved.is-ok { color: var(--s-good); }
.s-saved.is-bad { color: var(--s-bad); }

/* ── Pick one, with words ───────────────────────────────────────────────
   House rows carrying a native radio: the notification page's "How much
   email". The whole row is the label; the radio is the visible state, sized
   for a thumb and coloured like the switches. */
.s-radios .s-row { cursor: pointer; }
.s-radios input[type="radio"] {
  flex: none; width: 22px; height: 22px; margin: 1px 0 0;
  accent-color: var(--s-info);
}

/* The Publish form on a fixture: one checkbox beside the button, so the
   coach decides in the same tap whether parents hear about it now. */
.s-publish { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
.s-publish__opt {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: var(--s-fs-sm); font-weight: 800; color: var(--s-ink); cursor: pointer;
}
.s-publish__opt input { width: 18px; height: 18px; margin: 0; accent-color: var(--s-good); }


/* ── Trial bar ─────────────────────────────────────────────────────────────
   The strip above every page inside a 15 minute trial on try.sprout-club.
   Amber because it is a deadline, not an error. Everything stays on one row
   on a phone: the clock, the role you are in, the other roles, and the way
   out to a club of your own. */
.s-trial {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px 12px;
  margin: 0 0 var(--s-sp-3);
  padding: 8px var(--s-sp-3);
  border: 1px solid var(--s-warn-line);
  border-radius: var(--s-r);
  background: var(--s-warn-bg);
  color: var(--s-ink);
  font-size: var(--s-fs-sm); font-weight: 700;
}
.s-trial__clock { color: var(--s-warn); white-space: nowrap; font-variant-numeric: tabular-nums; }
.s-trial__roles { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px 10px; }
.s-trial__you { font-weight: 800; }
.s-trial__you::after { content: ':'; }
.s-trial__roles a { color: var(--s-ink); text-decoration: underline; text-underline-offset: 2px; min-height: 28px; display: inline-flex; align-items: center; }
.s-trial__keep {
  margin-left: auto; white-space: nowrap;
  padding: 4px 10px; border-radius: 999px;
  background: var(--s-warn); color: #fff; text-decoration: none; font-weight: 800;
}
