/* ==========================================================================
   PickSportPlus. static/app.css
   Hand authored stylesheet. No framework, no build step.

   PALETTE INTENT
   A warm collegiate throwback. Cream paper stock, deep collegiate green as the
   only primary action colour, maroon as the secondary voice, antique gold used
   the way a varsity club uses it: thin rules under labels, badge outlines,
   winner ribbons and the focus ring. Nothing neon, nothing purple, no gradient
   hero, no oversized radii. Elevation is one soft shadow used sparingly.
   Light theme only for v1. Tokens are split into a raw palette layer and a
   semantic layer so a dark theme can later remap the semantic layer alone.

   AA CONTRAST AUDIT (computed, WCAG 2.1 relative luminance)
   Body text needs 4.5:1, large text (>= 24px, or >= 18.7px bold) needs 3:1,
   focus and state indicators need 3:1 as non text contrast.

     ink        on paper        14.60:1   pass
     ink        on surface      16.88:1   pass
     ink        on surface-2    15.69:1   pass
     ink        on green-tint   14.39:1   pass
     ink        on gold-soft    11.46:1   pass  (gold surfaces carry ink, per 3b)
     ink        on gold          6.15:1   pass  (gold surfaces carry ink, per 3b)
     ink-soft   on paper         6.95:1   pass
     ink-soft   on surface       8.04:1   pass
     ink-soft   on surface-2     7.47:1   pass
     ink-soft   on green-tint    6.85:1   pass
     ink-soft   on gold-soft     5.46:1   pass
     green      on paper         9.54:1   pass
     green      on surface      11.03:1   pass
     green      on green-tint    9.40:1   pass
     green-str  on green-tint   11.08:1   pass
     maroon     on paper         9.49:1   pass
     maroon     on surface      10.97:1   pass
     brick      on paper         5.84:1   pass
     brick      on surface       6.76:1   pass
     brick      on brick-tint    5.72:1   pass
     surface    on green        11.03:1   pass  (cream text on primary buttons)
     surface    on green-strong 13.00:1   pass  (pressed primary)
     surface    on maroon       10.97:1   pass
     surface    on brick         6.76:1   pass  (danger button, cream on brick)
     nav-ink    on green         7.64:1   pass  (muted cream nav links)
     gold-soft  on green         7.49:1   pass  (wordmark Plus, nav underline)
     ink        on rank-2        11.40:1   pass
     ink        on rank-3         8.33:1   pass

   SECOND PASS AUDIT (the component styles added for the finished templates)
     green-str  on surface-2    12.08:1   pass  (your own name on a grid head)
     green      on surface-2    10.25:1   pass  (join code, week chips)
     maroon     on surface-2    10.19:1   pass
     brick      on surface-2     6.28:1   pass  (a voided row in an admin table)
     brick      on green-tint    5.76:1   pass
     ink-soft   on brick-tint    6.81:1   pass  (a losing side's score)
     surface    on brick         6.76:1   pass  (locked lock bar link)
     ink        on gold-soft    11.46:1   pass  (live badge, commissioner badge)
   Non text pairings, which need 3:1, on the results legend keys:
     green edge on green-tint    9.40:1   pass  (correct)
     brick edge on brick-tint    5.72:1   pass  (wrong)
     ink-soft edge on surface    8.04:1   pass  (not final)
     ink-soft edge on surface-2  7.47:1   pass  (void)

   FAILURES FOUND AND FIXED
   1. --focus at the specified #C0942F measured 2.38:1 on --paper and 2.75:1
      on --surface. A focus indicator must reach 3:1, so --focus is darkened to
      #A67C1E, which measures 3.23:1 on paper, 3.74:1 on surface, 3.47:1 on
      surface-2, 3.19:1 on green-tint and 3.17:1 on brick-tint. It still reads
      as antique gold. The decorative --gold token is left verbatim at #C0942F.
   2. #A67C1E measures only 2.95:1 against --green, so controls on the green
      top bar use --focus-invert (--gold-soft) at 7.49:1 on green.
   3. #A67C1E measures only 2.54:1 against --gold-soft, so controls on a gold
      surface (the lock bar) use --green-strong at 8.83:1. The strong lock bar
      is brick rather than gold and flips back to --focus-invert at 4.59:1.
      Every focus ring in the app therefore clears 3:1 on its own background.

   DECORATIVE GOLD IS NEVER TEXT
   --gold on paper is 2.38:1 and on surface 2.75:1. It is used only for 1px to
   2px rules, badge outlines and ribbon edges, never for a text colour, which
   matches the Section 3b rule "gold never carries small body text".

   TOKEN NOTE
   Every Section 3b token is declared verbatim except --focus, which 3b itself
   asks to be darkened if a pairing falls short. Nothing else was altered.

   WHAT A LATER DARK THEME HAS TO TOUCH
   Component rules read the semantic layer, so redeclaring that block under a
   [data-theme="dark"] selector covers almost everything. Four deliberate
   exceptions reach past it, and all four are correct as they stand:
     --ink       on gold-soft and gold fills. Section 3b fixes ink as the text
                 colour on any gold surface, in either theme.
     --nav-ink   the muted cream on the green top bar, which stays green.
     --rank-2    warm silver and bronze on the standings badges. They are
     --rank-3    medal colours, not surfaces, so they do not invert.

   CONTENTS
     01  Tokens
     02  Reset and base elements
     03  Accessibility, focus and motion
     04  Layout shell, top bar, tab bar, footer
     05  Buttons
     06  Surfaces: card, panel, divider
     07  Forms, auth pages
     08  Game row, confidence controls, picks bar, saved state
     09  Banners: lock bar, flash, toast, notice, empty state
     10  Tables, leaderboard, badges and stats
     11  Results: week switcher, pick legend, the picks grid
     12  Admin: sub nav, budgets, join code, slate tables
     13  SortableJS drag states
     14  Utilities
     15  Print
     16  Payouts: pot figures, dollar/percent mode toggle

   TEMPLATE HOOKS THIS FILE OWNS
   Names that app/templates and app/static/app.js already emit, so they are
   fixed contract, not invention:
     .skip-link  .footer .footer-inner  .flash-stack  .user-name .inline-form
     .btn-label  .icon  .brand-sport  .auth-wrap .auth-card .auth-intro
     .auth-alt  .input-code  .error-list  .error-state  .page-auth
     .save-state with .is-dirty / .is-saving / .is-saved / .is-error
     [data-pick-meter] filled from a --pct custom property
     .picks-bar .picks-progress .picks-summary .picks-meter .picks-meter-fill
     .picks-actions .picks-status  (picks.html, the save bar)
     .conf-controls .conf-input (Stage 1 typed confidence, .has-error is the existing
     global input state, Section 07, reused rather than invented)
     .game-list-divider [data-divider]  (the "Not picked" group boundary, app.js
     repositions it, it is never itself draggable, see regroupDivider)
     .lock-confirm .lock-confirm-list  ([data-lock-panel] starts hidden via the HTML
     hidden attribute, [data-reorder-btn] [data-lock-open] [data-lock-cancel] are plain
     .btn buttons, no dedicated classes of their own)
     .picks-grid with .pick-game .pick-player .pick-cell and the
     .is-correct / .is-wrong / .is-pending / .is-void / .is-missing states
     .subnav .subnav-link  (the four admin screens carry it inline)
     badge-{{ week.status }} resolves to .badge-draft / -open / -locked /
     -scored, and badge-{{ game.league }} to the existing .badge-nfl / -ncaaf
   ========================================================================== */


/* ==========================================================================
   01  TOKENS
   Raw palette is Section 3b verbatim. Semantic aliases sit on top so a future
   dark theme only has to redeclare the semantic block under a
   [data-theme="dark"] selector. Do not add a dark theme in v1.
   ========================================================================== */

:root {
  /* -- Raw palette (Section 3b, exact values) ---------------------------- */
  --paper:        #F4ECDA;  /* page background, warm cream */
  --surface:      #FFFDF7;  /* cards and panels */
  --surface-2:    #FBF4E6;  /* subtle alternate surface, table stripes */
  --border:       #E3D7BE;  /* warm hairline borders */
  --ink:          #1E1B15;  /* primary text */
  --ink-soft:     #574E40;  /* secondary text, 6.95:1 on paper */
  --green:        #16432B;  /* primary brand, deep collegiate green */
  --green-strong: #10371F;  /* hover and pressed */
  --green-tint:   #E6EDE4;  /* selected pick background, correct fill */
  --maroon:       #6E1E2A;  /* secondary accent */
  --gold:         #C0942F;  /* antique gold, decorative and accents only */
  --gold-soft:    #E8D28A;  /* ribbons, badge backgrounds */
  --brick:        #9B3B2C;  /* loss or incorrect */
  --shadow:       0 1px 2px rgba(30, 27, 21, .06), 0 10px 26px rgba(30, 27, 21, .05);
  --radius:       10px;
  --radius-sm:    8px;

  /* -- Derived tokens ---------------------------------------------------- */
  /* --focus is the one darkened token. See the audit note in the header.
     Specified #C0942F measured 2.38:1 on paper, below the 3:1 floor for a
     focus indicator. #A67C1E measures 3.23:1 on paper, 3.74:1 on surface. */
  --focus:        #A67C1E;
  /* Ring for controls sitting on the deep green top bar. 7.49:1 on green. */
  --focus-invert: var(--gold-soft);
  --brick-tint:   #F7E7E2;  /* incorrect pick fill, ink on it is 14.30:1 */
  --accent-line:  #C9D8C6;  /* hairline that reads on a green-tint fill */
  --negative-line:   #E2C3BB;  /* hairline that reads on a brick-tint fill */
  --negative-strong: #85311F;  /* danger hover, cream on it is 8.40:1 */
  --nav-ink:      #DCD5BE;  /* muted cream nav text, 7.64:1 on green */
  --rank-2:       #D8D2C4;  /* warm silver, ink on it is 11.40:1 */
  --rank-3:       #CDB08C;  /* warm bronze, ink on it is 8.33:1 */
  --line-strong:  #CDBE9E;  /* input border on hover, one step off --border */
  --hairline:     1px solid var(--border);
  --shadow-sm:    0 1px 2px rgba(30, 27, 21, .07);
  --radius-pill:  999px;

  /* -- Semantic aliases (remap these for a future dark theme) ------------ */
  --bg-page:        var(--paper);
  --bg-surface:     var(--surface);
  --bg-surface-alt: var(--surface-2);
  --line:           var(--border);
  --text:           var(--ink);
  --text-muted:     var(--ink-soft);
  --text-invert:    var(--surface);
  --accent:         var(--green);
  --accent-strong:  var(--green-strong);
  --accent-tint:    var(--green-tint);
  --accent-2:       var(--maroon);
  --decor:          var(--gold);
  --decor-soft:     var(--gold-soft);
  --positive:       var(--green);
  --negative:       var(--brick);
  --negative-tint:  var(--brick-tint);

  /* -- Typography (Section 3c) ------------------------------------------- */
  --font-display: "Oswald", "Arial Narrow", system-ui, sans-serif;
  --font-body:    "Libre Franklin", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  --fs-xs:   .75rem;     /* 12px, eyebrows, badges, records */
  --fs-sm:   .875rem;    /* 14px, meta text, hints */
  --fs-base: 1rem;       /* 16px, body floor on mobile */
  --fs-md:   1.125rem;   /* 18px, team abbreviations, lead text */
  --fs-lg:   1.375rem;   /* 22px, section titles */
  --fs-xl:   1.75rem;    /* 28px, page title on mobile */
  --fs-2xl:  2.125rem;   /* 34px, page title on desktop */
  --fs-3xl:  2.625rem;   /* 42px, rare display numbers */

  --lh-tight: 1.15;
  --lh-snug:  1.3;
  --lh-base:  1.55;
  --track-eyebrow: .06em;   /* Section 3c, eyebrow letter spacing */
  --track-abbr:    .04em;

  /* -- 4px spacing scale (Section 3d) ------------------------------------ */
  --space-1:  4px;
  --space-2:  8px;
  --space-3:  12px;
  --space-4:  16px;
  --space-5:  20px;
  --space-6:  24px;
  --space-8:  32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;

  /* -- Layout ------------------------------------------------------------ */
  --measure:    1080px;   /* content column max width */
  --gutter:     var(--space-4);
  /* 60px of content plus the 2px gold rule, so the sticky lock bar can sit
     flush under the top bar at every breakpoint. */
  --topbar-h:   60px;
  --tabbar-h:   60px;
  --tap:        44px;     /* minimum tap target */
  --tap-sm:     36px;

  --z-tabbar: 60;
  --z-topbar: 50;
  --z-lockbar: 40;
  --z-menu: 70;

  --ease: cubic-bezier(.2, .6, .3, 1);
  --dur: 140ms;
}


/* ==========================================================================
   02  RESET AND BASE ELEMENTS
   Small and deliberate. Only what this app needs.
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  /* The page column itself. The top bar, the sub bar, main and the footer are
     direct children, so the footer settles at the bottom of short pages
     without main having to claim a full viewport of its own. */
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  background: var(--bg-page);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-base);
  line-height: var(--lh-base);
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  /* Room for the fixed mobile tab bar. Removed at the 768px breakpoint. */
  padding-bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom, 0px));
}

h1, h2, h3, h4, h5, h6 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: .01em;
  color: var(--text);
}

p, ul, ol, dl, figure, pre, table { margin: 0; }

p { max-width: 68ch; }

a {
  color: var(--accent);
  text-decoration: none;
  text-underline-offset: 3px;
}

a:hover { text-decoration: underline; }

img, svg { display: block; max-width: 100%; }

svg { flex: none; }

button, input, select, textarea {
  font: inherit;
  color: inherit;
}

button { cursor: pointer; }

ul, ol { padding-left: 0; list-style: none; }

hr { border: 0; }

strong, b { font-weight: 600; }

small { font-size: var(--fs-sm); }

code, kbd, samp {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .9em;
}

::selection {
  background: var(--decor-soft);
  color: var(--ink);
}

/* Anchors should not slide under the sticky top bar. */
:target { scroll-margin-top: calc(var(--topbar-h) + var(--space-6)); }


/* ==========================================================================
   03  ACCESSIBILITY, FOCUS AND MOTION
   Focus outlines are never removed anywhere in this file.
   ========================================================================== */

:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Explicit belt and braces for every interactive element type. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible,
[role="button"]:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Controls on the deep green top bar need the lighter ring. --focus measures
   only 2.95:1 on --green, --gold-soft measures 7.49:1. */
.topbar a:focus-visible,
.topbar button:focus-visible,
.topbar summary:focus-visible {
  outline-color: var(--focus-invert);
}

/* Controls sitting on a gold surface take the deep green ring. --focus is
   2.54:1 on --gold-soft, --green-strong is 8.83:1. */
.lockbar a:focus-visible,
.lockbar button:focus-visible {
  outline-color: var(--accent-strong);
}

/* The strong lock bar is brick rather than gold, so it flips back to the
   light ring. --gold-soft on --brick measures 4.59:1. */
.lockbar-strong a:focus-visible,
.lockbar-strong button:focus-visible {
  outline-color: var(--focus-invert);
}

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

/* Skip link. The template ships <a class="skip-link" href="#main"> as the
   first thing in the body, so it needs its own rule. It stays in the tab
   order and in the accessibility tree, it is only pushed off screen, and it
   parks itself over the top bar when focused. */
.skip-link {
  position: fixed;
  top: var(--space-2);
  left: -9999px;
  z-index: calc(var(--z-menu) + 10);
  padding: var(--space-2) var(--space-4);
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--accent);
}

.skip-link:focus,
.skip-link:focus-visible {
  left: var(--space-2);
  text-decoration: none;
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Second supported form. A link carrying .visually-hidden reveals itself on
   focus. Scoped to anchors so a visually hidden label or caption stays
   hidden. */
a.visually-hidden:focus,
a.visually-hidden:focus-visible {
  position: static !important;
  width: auto;
  height: auto;
  margin: 0;
  padding: var(--space-2) var(--space-3);
  overflow: visible;
  clip: auto;
  clip-path: none;
  white-space: normal;
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius-sm);
  color: var(--accent);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
}


/* ==========================================================================
   04  LAYOUT SHELL, TOP BAR, TAB BAR
   ========================================================================== */

/* The main content region. It grows to fill whatever the top bar and the
   footer leave behind, so a one line page still puts the footer at the
   bottom and never invents an extra viewport of scroll. */
.shell {
  display: flex;
  flex-direction: column;
  flex: 1 0 auto;
  min-width: 0;
}

.container {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* -- Top bar ------------------------------------------------------------- */

.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-topbar);
  background: var(--accent);
  color: var(--text-invert);
  border-bottom: 2px solid var(--decor);
  box-shadow: var(--shadow-sm);
}

.topbar-inner {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  max-width: var(--measure);
  min-height: var(--topbar-h);
  margin-inline: auto;
  padding: var(--space-2) var(--gutter);
}

.topbar a { color: var(--text-invert); }
.topbar a:hover { text-decoration: none; }

/* -- Brand --------------------------------------------------------------- */

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-right: auto;
  min-height: var(--tap);
  color: var(--text-invert);
}

/* The shield is a 60 by 70 viewBox, so it is sized by height and lets its
   own aspect ratio pick the width. Works whether .brand-mark is put on the
   <svg> itself (what components/brand.html does) or on a wrapping span. */
.brand-mark {
  flex: none;
  width: auto;
  height: 32px;
}

.brand-mark svg {
  width: auto;
  height: 100%;
}

.brand-word {
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: .01em;
  line-height: 1;
  white-space: nowrap;
  color: var(--text-invert);
}

/* Only the word Plus is tinted, per Section 3a. .brand-sport exists so the
   wordmark can be split into three spans without the middle one drifting. */
.brand-sport { color: inherit; }

.brand-plus {
  color: var(--decor-soft);   /* 7.49:1 on green */
}

/* -- Desktop nav. Hidden below 768px, the tab bar covers mobile. --------- */

.nav {
  display: none;
  align-items: center;
  gap: var(--space-1);
}

.nav-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--nav-ink);          /* 7.64:1 on green */
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

.nav-link svg {
  width: 18px;
  height: 18px;
  stroke-width: 1.75;
}

.nav-link:hover {
  color: var(--text-invert);
  background: rgba(255, 253, 247, .08);
}

.nav-link.is-active {
  color: var(--text-invert);
  font-weight: 600;
}

.nav-link.is-active::after {
  content: "";
  position: absolute;
  left: var(--space-3);
  right: var(--space-3);
  bottom: 2px;
  height: 2px;
  background: var(--decor-soft);
  border-radius: 2px;
}

/* -- User menu ----------------------------------------------------------- */

.user-menu {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: none;
}

/* The signed in display name. Hidden below 640px, where the brand plus the
   sign out button already fill a 360px top bar. Restored at 640px. */
.user-name {
  display: none;
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--nav-ink);      /* 7.64:1 on green */
  white-space: nowrap;
  max-width: 18ch;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A form that only exists to POST a single button, for example sign out. */
.inline-form {
  display: flex;
  align-items: center;
  margin: 0;
}

.user-menu > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap);
  padding: var(--space-2) var(--space-3);
  border: 1px solid rgba(232, 210, 138, .45);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-invert);
  list-style: none;
  cursor: pointer;
}

.user-menu > summary::-webkit-details-marker { display: none; }
.user-menu > summary::marker { content: ""; }

.user-menu > summary:hover { background: rgba(255, 253, 247, .08); }

.user-menu[open] > summary {
  background: var(--accent-strong);
  border-color: var(--decor);
}

/* The dropdown panel. Scoped to details so plain markup is unaffected. */
details.user-menu[open] > :not(summary) {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-2));
  z-index: var(--z-menu);
  min-width: 200px;
  padding: var(--space-2);
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  color: var(--text);
}

details.user-menu[open] > :not(summary) a,
details.user-menu[open] > :not(summary) button {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: var(--tap);
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  text-align: left;
  font-size: var(--fs-sm);
  color: var(--text);
}

details.user-menu[open] > :not(summary) a:hover,
details.user-menu[open] > :not(summary) button:hover {
  background: var(--bg-surface-alt);
  text-decoration: none;
}

/* -- Mobile bottom tab bar ----------------------------------------------- */

.tabbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: var(--z-tabbar);
  display: flex;
  align-items: stretch;
  background: var(--bg-surface);
  border-top: var(--hairline);
  box-shadow: 0 -1px 2px rgba(30, 27, 21, .05);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

.tabbar-link {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: var(--tabbar-h);
  padding: var(--space-2) var(--space-1);
  color: var(--text-muted);
  font-family: var(--font-display);
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: .02em;
  text-align: center;
  border-top: 2px solid transparent;
}

.tabbar-link:hover { text-decoration: none; }

.tabbar-link svg {
  width: 20px;
  height: 20px;
  stroke-width: 1.75;
}

.tabbar-link.is-active {
  color: var(--accent);
  border-top-color: var(--decor);
  background: var(--bg-surface-alt);
  font-weight: 600;
}

/* -- Mobile site menu (hamburger) -----------------------------------------
   Phone width only (.mobile-only hides it at 768px, where the full row of
   Pricing / How it works / Contact / Login, or the signed-in Home link,
   already fits in .nav / .user-menu instead). Toggled by app.js
   (initMenuToggle): [hidden] removed on open, .is-open added a frame later
   so the fade-and-drop actually animates; both happen in the same paint
   when prefers-reduced-motion is on, see app.js's reduceMotion branch. The
   global reduced-motion rule in Section 03 collapses the transition itself
   to near zero either way, this only avoids animating a menu that opens
   pre-collapsed. */
.site-menu {
  position: fixed;
  top: var(--topbar-h);
  left: 0;
  right: 0;
  z-index: var(--z-menu);
  background: var(--bg-surface);
  border-bottom: var(--hairline);
  box-shadow: var(--shadow);
  padding: var(--space-2);
  opacity: 0;
  transform: translateY(-6px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}

.site-menu.is-open {
  opacity: 1;
  transform: translateY(0);
}

.site-menu-nav {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.site-menu-nav a {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--fs-sm);
  font-weight: 500;
}

.site-menu-nav a:not(.btn):hover {
  background: var(--bg-surface-alt);
  text-decoration: none;
}

.site-menu-nav .btn { margin-top: var(--space-1); justify-content: flex-start; }

/* -- Footer -------------------------------------------------------------- */

.footer {
  margin-top: auto;
  padding-block: var(--space-5);
  background: var(--bg-surface-alt);
  border-top: var(--hairline);
  font-size: var(--fs-sm);
}

.footer-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-1) var(--space-4);
}

/* Second footer line: legal links on the left, the maker credit on the right. Set a step
   smaller than the line above it so it reads as fine print without dropping below the
   12px floor. --text-muted on --bg-surface-alt measures 7.47:1. */
.footer-legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-1) var(--space-4);
  margin-top: var(--space-2);
  padding-top: var(--space-2);
  border-top: var(--hairline);
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

.footer-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-4);
}

.footer-legal a {
  color: var(--text-muted);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.footer-legal a:hover,
.footer-legal a:focus-visible {
  color: var(--accent);
}

.footer-credit {
  margin: 0;
}

.footer-credit a {
  font-weight: 600;
}

@media (max-width: 479px) {
  /* On a narrow phone the two halves stack, so left align them rather than leaving the
     credit stranded against the right edge. */
  .footer-legal {
    justify-content: flex-start;
  }
}

/* -- Long form prose (terms, privacy) ------------------------------------- */

/* These are the only pages with real running text, so they get a reading measure and
   generous leading rather than the tight rhythm the rest of the app uses. */
.prose {
  max-width: 74ch;
}

.prose .card-body > * + * {
  margin-top: var(--space-4);
}

.prose h2 {
  margin-top: var(--space-8);
  padding-top: var(--space-4);
  border-top: var(--hairline);
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 500;
  line-height: var(--lh-snug);
  color: var(--text);
}

.prose .card-body > h2:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

.prose p {
  margin: 0;
  line-height: 1.65;
}

.prose ul {
  margin: 0;
  padding-left: var(--space-5);
  list-style: disc;
}

.prose li + li {
  margin-top: var(--space-2);
}

.prose li {
  line-height: 1.6;
}

.prose code {
  padding: 1px 5px;
  border: var(--hairline);
  border-radius: 4px;
  background: var(--bg-surface-alt);
  font-size: .92em;
}

.prose .table {
  margin-top: var(--space-4);
}

.legal-note {
  margin-top: var(--space-8);
  padding-top: var(--space-4);
  border-top: var(--hairline);
  font-size: var(--fs-sm);
}

/* -- Page head and sections ---------------------------------------------- */

.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-6) 0 var(--space-5);
  border-bottom: var(--hairline);
  margin-bottom: var(--space-6);
}

.page-title {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: .01em;
  margin: 0;
}

.eyebrow {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-2);
}

/* The thin gold rule under a label. This is the main decorative use of gold. */
.eyebrow::after {
  content: "";
  display: block;
  width: 28px;
  height: 2px;
  margin-top: var(--space-1);
  background: var(--decor);
  border-radius: 1px;
}

.section {
  margin-bottom: var(--space-8);
}

.section:last-child { margin-bottom: var(--space-10); }

.section-title {
  position: relative;
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 600;
  padding-bottom: var(--space-2);
  margin-bottom: var(--space-4);
  border-bottom: var(--hairline);
}

.section-title::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 48px;
  height: 2px;
  background: var(--decor);
}

/* A section title with a small action beside it. The title stays the flexible
   child so its hairline and its gold rule still run the width of the row. */
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.section-head .section-title {
  flex: 1 1 12ch;
  margin-bottom: 0;
}

/* -- Page hooks ----------------------------------------------------------
   base.html writes the page name onto <body>. Only the picks page needs one:
   it stacks a sticky lock bar under the sticky top bar, so its "Back to save"
   anchor has to clear both, not just the top bar.
   ------------------------------------------------------------------------ */

.page-picks :target {
  scroll-margin-top: calc(var(--topbar-h) + var(--space-12));
}


/* ==========================================================================
   05  BUTTONS
   Minimum height 44px on .btn. .btn-sm relaxes to 36px for dense controls.
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--tap);
  padding: var(--space-2) var(--space-5);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: var(--bg-surface);
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              color var(--dur) var(--ease),
              transform var(--dur) var(--ease);
}

.btn:hover { text-decoration: none; }

.btn:active { transform: translateY(1px); }

.btn svg {
  width: 18px;
  height: 18px;
  stroke-width: 1.75;
}

/* A disabled button drops its own colour rather than fading out. app.js
   disables the primary Save action until every winner is chosen, and a washed
   out green still reads as "press me". A flat neutral fill does not, and the
   label stays at 7.47:1 instead of dropping to about 3:1 behind an opacity. */
.btn[disabled],
.btn[aria-disabled="true"],
.btn.is-disabled {
  background: var(--bg-surface-alt);
  border-color: var(--line);
  color: var(--text-muted);    /* 7.47:1 on surface-2 */
  box-shadow: none;
  cursor: not-allowed;
  transform: none;
}

/* And it never reacts to the pointer. Without this the variant hover rules
   below still repaint a disabled button. */
.btn[disabled]:hover,
.btn[disabled]:active,
.btn[aria-disabled="true"]:hover,
.btn[aria-disabled="true"]:active,
.btn.is-disabled:hover,
.btn.is-disabled:active {
  background: var(--bg-surface-alt);
  border-color: var(--line);
  color: var(--text-muted);
  transform: none;
}

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--text-invert);   /* 11.03:1 */
}

.btn-primary:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}

.btn-primary:active { background: var(--accent-strong); }

.btn-secondary {
  background: var(--bg-surface);
  border-color: var(--accent);
  color: var(--accent);        /* 11.03:1 */
}

.btn-secondary:hover {
  background: var(--accent-tint);
  border-color: var(--accent-strong);
  color: var(--accent-strong);
}

.btn-secondary:active { background: var(--accent-tint); }

.btn-ghost {
  background: transparent;
  border-color: transparent;
  color: var(--text-muted);    /* 6.95:1 on paper */
  font-weight: 500;
}

.btn-ghost:hover {
  background: var(--bg-surface-alt);
  color: var(--text);
}

.topbar .btn-ghost { color: var(--nav-ink); }
.topbar .btn-ghost:hover {
  background: rgba(255, 253, 247, .08);
  color: var(--text-invert);
}

/* The signed-out bar carries a secondary button. Two reasons this needs its own rule.
   First, ".topbar a" sets cream text and outranks ".btn-secondary", so the default
   green-on-cream button rendered as cream on cream: a blank slab with an invisible
   label. Second, a filled cream button is too heavy against the green bar. An outline
   in gold-soft matches the bar and the ghost button beside it.
   Cream on green measures 11.03:1, the gold-soft border 7.49:1. */
.topbar .btn-secondary {
  background: transparent;
  border-color: var(--gold-soft);
  color: var(--text-invert);
}

.topbar .btn-secondary:hover,
.topbar .btn-secondary:active {
  background: rgba(255, 253, 247, .10);
  border-color: var(--text-invert);
  color: var(--text-invert);
}

/* The signed-out header's Login button. Deliberately a different treatment from the plain
   text Pricing / How it works / Contact links beside it, filled antique gold against the
   green top bar, so it reads as the one action that actually leaves the marketing pages.
   --ink on --gold measures 6.15:1. See DECISIONS.md, Post-launch fixes, for why gold (not a
   second green fill or the maroon accent) was chosen. */
.btn-gold {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--ink);
}

.btn-gold:hover {
  background: var(--gold-soft);
  border-color: var(--gold-soft);
  color: var(--ink);
}

.btn-gold:active { background: var(--gold-soft); }

/* .topbar a sets cream text at the same specificity .btn-gold's own color rule carries, so
   without this override every Login label on the green bar would render invisible cream on
   gold. Two classes here (0,2,0) outrank .topbar a's class+element selector (0,1,1). */
.topbar .btn-gold,
.topbar .btn-gold:hover {
  color: var(--ink);
}

/* Breathing room between the plain nav links and the Login button that follows them. */
.nav-cta { margin-left: var(--space-2); }

.btn-danger {
  background: var(--negative);
  border-color: var(--negative);
  color: var(--text-invert);   /* 6.76:1 */
}

.btn-danger:hover {
  background: var(--negative-strong);         /* darker brick, cream on it is 8.42:1 */
  border-color: var(--negative-strong);
}

/* A dense button. It still honours the 44px tap floor from Sections 3f and
   3g on phone widths and only relaxes to 36px at 768px, where the pointer is
   normally a mouse. */
.btn-sm {
  min-height: var(--tap);
  padding: var(--space-1) var(--space-3);
  font-size: var(--fs-xs);
  gap: var(--space-1);
}

.btn-sm svg {
  width: 16px;
  height: 16px;
}

/* The text half of an icon plus text button. Kept as its own hook so a
   template can hide the words in a very tight bar without losing the label
   for screen readers. */
.btn-label { white-space: nowrap; }

/* Every inline Lucide icon carries .icon. Component rules below set the size
   that matches their text. */
.icon { flex: none; }


/* ==========================================================================
   06  SURFACES: CARD, PANEL, DIVIDER
   ========================================================================== */

.card {
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  overflow: hidden;
}

.card + .card { margin-top: var(--space-4); }

.card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-4) var(--space-4) var(--space-3);
  border-bottom: var(--hairline);
}

.card-head h2,
.card-head h3 {
  font-size: var(--fs-lg);
  margin: 0;
}

.card-body {
  padding: var(--space-4);
}

.card-body > * + * { margin-top: var(--space-4); }

.card-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  border-top: var(--hairline);
  background: var(--bg-surface-alt);
}

/* A panel is a quieter container. No shadow, alternate fill. */
.panel {
  background: var(--bg-surface-alt);
  border: var(--hairline);
  border-radius: var(--radius);
  padding: var(--space-4);
}

.panel > * + * { margin-top: var(--space-3); }

.divider {
  display: block;
  height: 1px;
  margin: var(--space-6) 0;
  background: var(--line);
  border: 0;
}


/* ==========================================================================
   07  FORMS
   Every control is labelled in the template. These styles assume
   <div class="field"><label class="field-label">..</label><input class="input">..
   ========================================================================== */

.field {
  display: block;
  margin-bottom: var(--space-4);
}

.field:last-child { margin-bottom: 0; }

.field-label {
  display: block;
  margin-bottom: var(--space-2);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text);
}

.input,
.select,
textarea {
  display: block;
  width: 100%;
  min-height: var(--tap);
  padding: var(--space-2) var(--space-3);
  background: var(--bg-surface);
  color: var(--text);
  border: var(--hairline);
  border-radius: var(--radius-sm);
  font-family: var(--font-body);
  font-size: var(--fs-base);   /* 16px keeps iOS from zooming on focus */
  line-height: 1.4;
  transition: border-color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

.input::placeholder,
textarea::placeholder { color: var(--text-muted); opacity: .75; }

.input:hover,
.select:hover { border-color: var(--line-strong); }

/* background-color, not the background shorthand, so the .select chevron
   drawn below is never wiped out by a state rule. */
.input:focus,
.select:focus,
textarea:focus {
  border-color: var(--accent);
  background-color: var(--bg-surface);
}

.select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: var(--space-8);
  /* Inline chevron drawn as a data URI so nothing loads from a CDN. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23574E40' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  cursor: pointer;
}

.input[disabled],
.select[disabled],
textarea[disabled] {
  background-color: var(--bg-surface-alt);
  color: var(--text-muted);
  cursor: not-allowed;
}

.input[aria-invalid="true"],
.select[aria-invalid="true"],
.input.has-error {
  border-color: var(--negative);
  background-color: var(--negative-tint);
}

.checkbox {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: var(--tap);
  font-size: var(--fs-sm);
  cursor: pointer;
}

.checkbox input[type="checkbox"],
.checkbox input[type="radio"] {
  width: 20px;
  height: 20px;
  flex: none;
  accent-color: var(--accent);
  cursor: pointer;
}

.form-error {
  display: block;
  margin-top: var(--space-2);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--negative);       /* 6.76:1 on surface, 5.84:1 on paper */
}

.form-hint {
  display: block;
  margin-top: var(--space-2);
  font-size: var(--fs-sm);
  color: var(--text-muted);     /* 6.95:1 on paper */
}

.form-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

.form-row .field { margin-bottom: 0; }

/* A group of related controls, for example the two league checkboxes. The
   legend uses .field-label, so it only needs the gap in the hairline. */
.field-group {
  margin: 0 0 var(--space-4);
  padding: var(--space-4);
  background: var(--bg-surface-alt);
  border: var(--hairline);
  border-radius: var(--radius-sm);
}

.field-group > legend {
  padding-inline: var(--space-2);
  margin-bottom: 0;
}

.field-group .form-hint { margin-top: var(--space-3); }

/* The row a form ends on. The button first, then any note about what saving
   actually does. */
.form-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-4);
}

.form-actions .btn { flex: none; }

.form-actions p {
  margin: 0;
  font-size: var(--fs-sm);
}

/* A whole page of cards that submit together. */
.settings-form { margin-bottom: var(--space-10); }

/* A tiny form living inside a table cell, an input plus its button. */
.action-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin: 0;
}

/* Dense controls to match .btn-sm. Like .btn-sm they hold the 44px tap floor
   on phone widths and only relax to 36px at 768px, where the pointer is
   normally a mouse. */
.input-sm,
.select-sm {
  width: auto;
  min-height: var(--tap);
  padding: var(--space-1) var(--space-3);
  font-size: var(--fs-sm);
}

.input-sm { max-width: 8ch; }

.select-sm {
  max-width: 24ch;
  padding-right: var(--space-6);
  background-position: right var(--space-2) center;
}

/* A list of validation messages inside .form-error. The global list reset
   strips markers, so put them back here where they carry meaning. */
.error-list {
  margin: 0;
  padding-left: var(--space-5);
  list-style: disc;
}

.error-list li + li { margin-top: var(--space-1); }

/* A join code. Short, uppercase, read aloud over the phone, so it gets the
   display face and generous tracking. */
.input-code {
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: .14em;
  text-transform: uppercase;
}

.input-code::placeholder { letter-spacing: normal; }

/* -- Sign in, register and join ------------------------------------------ */

.auth-wrap {
  width: 100%;
  max-width: 460px;
  margin-inline: auto;
  padding-block: var(--space-8);
}

/* Sign in is the one page with no nav to anchor it, so centre the card in
   whatever height is left. Body text inside it stays left aligned. */
.page-auth .shell { justify-content: center; }

.auth-card .page-title { margin-bottom: var(--space-2); }

.auth-intro {
  margin: 0;
  font-size: var(--fs-sm);
}

.auth-alt {
  margin: 0;
  padding-top: var(--space-4);
  border-top: var(--hairline);
  font-size: var(--fs-sm);
}


/* ==========================================================================
   08  GAME ROW AND CONFIDENCE CONTROLS
   The core pick control. Expected markup, so the drag handle, the two team
   buttons, the meta strip and the keyboard fallback all land in the right
   grid areas:

     <ol class="game-list" id="slate">
       <li class="game-row" data-game-id="12">
         <span class="game-grip" aria-hidden="true">(grip icon)</span>
         <div class="game-teams">
           <button class="team-btn is-picked">
             <span class="team-abbr">GB</span>
             <span class="team-name">Green Bay Packers</span>
             <span class="team-record num">3-2</span>
             <span class="team-check">(check icon)</span>
           </button>
           <button class="team-btn"> ... home side ... </button>
         </div>
         <div class="game-meta">
           <span class="badge badge-nfl">NFL</span>
           <span class="game-line">Line GB -2.5</span>
           <span class="game-kick">Sun 1:00 PM ET</span>
           <span class="conf-chip num">14</span>
         </div>
         <div class="rank-controls">
           <button class="rank-btn">(up)</button>
           <button class="rank-btn">(down)</button>
         </div>
       </li>
     </ol>
   ========================================================================== */

.game-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.game-row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas:
    "grip teams teams"
    "meta meta  rank";
  align-items: center;
  gap: var(--space-1) var(--space-2);
  padding: var(--space-2);
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

/* A scored or locked row. No handle, no rank buttons, so the teams and the
   meta strip take the full width. */
.game-row.is-complete {
  grid-template-areas:
    "teams teams teams"
    "meta  meta  meta";
  background: var(--bg-surface-alt);
  box-shadow: none;
}

.game-row.is-complete .game-grip,
.game-row.is-complete .rank-controls {
  display: none;
}

/* A voided game, or an NFL tie. It stays perfectly legible, it just stops
   looking like a game that pays. */
.game-row.is-void {
  border-style: dashed;
  background: var(--bg-surface-alt);
}

.game-row.is-void .team-abbr,
.game-row.is-void .team-name,
.game-row.is-void .game-score { color: var(--text-muted); }   /* 7.47:1 */

/* -- Drag handle --------------------------------------------------------- */

.game-grip {
  grid-area: grip;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap);
  height: var(--tap);
  flex: none;
  color: var(--text-muted);
  cursor: grab;
  border-radius: var(--radius-sm);
  touch-action: none;          /* SortableJS touch dragging */
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

.game-grip:hover {
  color: var(--text);
  background: var(--bg-surface-alt);
}

.game-grip:active { cursor: grabbing; }

.game-grip svg {
  width: 20px;
  height: 20px;
  stroke-width: 1.75;
}

/* -- The two team buttons ------------------------------------------------ */

.game-teams {
  grid-area: teams;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-2);
  min-width: 0;
}

/* picks.html prints a small "at" between the two sides, so the grid grows a
   third track. .game-teams goes to 1fr 1fr at 640px, so this variant has to
   be restated there as 1fr auto 1fr or the wide layout loses a column. */
.game-teams-at { grid-template-columns: 1fr; }

.game-at {
  align-self: center;
  justify-self: center;
  font-family: var(--font-display);
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: var(--track-abbr);
  line-height: 1;
  color: var(--text-muted);   /* 8.04:1 on surface */
}

.team-btn {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  /* A grid item defaults to min-width:auto, which is min-content, so a long team name
     ("Notre Dame Fighting Irish") pushed the button 25px past its track at 360px and
     scrolled the whole page sideways. Zero lets it shrink and .team-name ellipsis. */
  min-width: 0;
  /* 44px is the accessibility floor for a tap target (Spec Section 3f), not a starting
     point: do not shrink this further even to fit more rows on a page. */
  min-height: 44px;
  padding: var(--space-1) var(--space-3);
  background: var(--bg-surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  text-align: left;
  color: var(--text);
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}

/* Hover is a promise of interaction, so it is scoped to a live button. The
   locked picks page renders the same control disabled and the results page
   renders it as a plain div, and neither should light up under the pointer.
   Excluding .is-picked keeps a chosen side on its green tint while hovered. */
button.team-btn:not([disabled]):not(.is-picked):hover {
  border-color: var(--accent);
  background: var(--bg-surface-alt);
}

.team-btn[disabled] { cursor: default; }

.team-abbr {
  flex: none;
  min-width: 3.5ch;
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: var(--track-abbr);
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--text);
}

.team-name {
  flex: 1 1 6ch;
  min-width: 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.team-record {
  flex: none;
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);   /* 6.95:1 */
}

/* Final score on the results page. Display face and tabular figures so the
   two sides of a matchup line up, per Section 3c. */
.team-score {
  flex: none;
  margin-left: auto;
  min-width: 2ch;
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  text-align: right;
  color: var(--text);
}

.team-check {
  flex: none;
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  color: var(--accent);
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

.team-check svg {
  width: 18px;
  height: 18px;
  stroke-width: 2.25;
}

/* When a score is present it takes the auto margin, so the check only needs
   a gap to sit in. */
.team-score + .team-check { margin-left: var(--space-2); }

/* The results scoreboard renders the same row as plain divs rather than
   buttons, so it carries .game-score. The admin slate table reuses the name
   for a quiet "GB 24, CHI 20" line under the status badge, which is why the
   base rule is the small meta form and the team row restates it. */
.game-score {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-xs);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  letter-spacing: .01em;
  color: var(--text-muted);   /* 8.04:1 on surface */
}

.team-btn .game-score {
  flex: none;
  display: inline-block;
  margin-left: auto;
  min-width: 2ch;
  font-size: var(--fs-md);
  font-weight: 600;
  line-height: 1;
  text-align: right;
  color: var(--text);
}

.team-btn.is-winner .game-score { color: var(--accent-strong); }  /* 11.08:1 */
.team-btn.is-loser .game-score { color: var(--text-muted); }      /* 6.81:1 */

/* The winner tick on the results scoreboard. It sits before the score, so it
   takes the auto margin and the score follows on a fixed gap. Two auto
   margins in one flex row would otherwise share the free space between them. */
.win-check {
  flex: none;
  display: inline-flex;
  align-items: center;
  margin-left: auto;
  color: var(--positive);     /* 9.40:1 on the green tint fill */
}

.win-check svg {
  width: 16px;
  height: 16px;
  stroke-width: 2.25;
}

.win-check + .game-score { margin-left: var(--space-2); }

.team-btn.is-winner .team-score { color: var(--accent-strong); }

.team-btn.is-loser .team-score { color: var(--text-muted); }

/* Selected winner. Green tint fill plus the check, per Section 8. */
.team-btn.is-picked {
  background: var(--accent-tint);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.team-btn.is-picked .team-abbr,
.team-btn.is-picked .team-name { color: var(--accent-strong); }

.team-btn.is-picked .team-name { font-weight: 600; }

.team-btn.is-picked .team-check { opacity: 1; }

/* Result states, used on the results page after a game goes final. */
.team-btn.is-winner {
  background: var(--accent-tint);
  border-color: var(--positive);
  border-left: 4px solid var(--positive);
}

.team-btn.is-winner .team-abbr { color: var(--accent-strong); }

.team-btn.is-loser {
  background: var(--negative-tint);
  border-color: var(--negative-line);
  border-left: 4px solid var(--negative);
}

.team-btn.is-loser .team-abbr,
.team-btn.is-loser .team-name { color: var(--text-muted); }

.team-btn.is-loser .team-check { opacity: 0; }

/* A picked side that lost reads clearly as the mistake. */
.team-btn.is-picked.is-loser {
  background: var(--negative-tint);
  border-color: var(--negative);
  box-shadow: inset 0 0 0 1px var(--negative);
}

.team-btn.is-picked.is-loser .team-abbr,
.team-btn.is-picked.is-loser .team-name { color: var(--negative); }

/* -- Meta strip ---------------------------------------------------------- */

.game-meta {
  grid-area: meta;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  min-width: 0;
  padding-left: var(--space-1);
}

.game-line,
.game-kick {
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);   /* 6.95:1 on paper, 8.04:1 on surface */
}

.game-line { font-weight: 500; }

/* A short aside in the meta strip, for example "No points on this game." */
.game-note {
  flex: none;
  font-size: var(--fs-xs);
  color: var(--text-muted);   /* 7.47:1 on the void row fill */
}

.game-kick::before {
  content: "";
  display: inline-block;
  width: 3px;
  height: 3px;
  margin-right: var(--space-2);
  vertical-align: middle;
  border-radius: 50%;
  background: var(--line);
}

/* -- Confidence chip ----------------------------------------------------- */

.conf-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  min-height: 26px;
  margin-left: auto;
  padding: 0 var(--space-2);
  background: var(--decor-soft);
  border: 1px solid var(--decor);
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  letter-spacing: .02em;
  line-height: 1;
  color: var(--ink);          /* 11.46:1, gold surfaces carry ink */
}

/* When a row shows more than one chip, only the first one pushes right. */
.conf-chip + .conf-chip { margin-left: var(--space-2); }

/* An unpicked row, or a picked row with no confidence assigned yet (Phase 4: confidence is
   scoped to the picked rows only, never the whole slate). Quiet hollow pill instead of a
   filled gold one with nothing in it. */
.conf-chip.is-empty {
  background: var(--bg-surface-alt);
  border-color: var(--line);
  color: var(--text-muted);   /* 7.47:1 on surface-2 */
}

/* -- Stage 1: the typed confidence input ---------------------------------
   Sits next to the chip in .game-meta. It IS what the player types into, the hidden
   input[data-conf] app.js reads for the actual POST is separate and driven from this one.
   .has-error is the existing global input state (Section 07), reused rather than invented,
   so a colliding or out of range value reads exactly like any other bad form field. */
.conf-controls {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
}

.conf-field {
  display: inline-flex;
}

.conf-input {
  width: 4.5rem;
  min-height: var(--tap-sm);
  flex: none;
  padding: var(--space-1) var(--space-2);
  text-align: center;
}

/* -- "Not picked" divider, inside the same sortable list ------------------
   A real <li> so it travels with the list rather than floating outside it, but it carries
   no .game-grip, and SortableJS is scoped to that handle, so it can never be dragged itself
   (see app.js regroupDivider). Purely cosmetic: which side of it a row visually sits on
   never decides that row's picked state, only whether its team button is picked does. */
/* :not([hidden]), not a bare .game-list-divider rule: the HTML hidden attribute (app.js
   toggles it in regroupDivider) has to actually hide this element. A plain class selector
   setting display:flex sits at the same specificity as the UA stylesheet's [hidden] rule
   but comes later in the cascade, so it would win and the divider would stay visible even
   while hidden. */
.game-list-divider:not([hidden]) {
  display: flex;
  align-items: center;
  padding-top: var(--space-2);
  border-top: var(--hairline);
}

.game-list-divider .eyebrow { margin: 0; }

/* -- Keyboard fallback for ranking --------------------------------------- */

.rank-controls {
  grid-area: rank;
  display: flex;
  align-items: center;
  gap: var(--space-1);
  flex: none;
}

.rank-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap);
  height: var(--tap);
  padding: 0;
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  transition: color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              background-color var(--dur) var(--ease);
}

.rank-btn:hover {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-tint);
}

/* The row is already at the top or the bottom of the ranking. Flat neutral
   fill plus a faded arrow, and no hover at all. WCAG 1.4.3 exempts an
   inactive control from the text contrast floor, which is what lets the
   arrow itself fade. */
.rank-btn[disabled] {
  background: var(--bg-surface-alt);
  border-color: var(--line);
  opacity: .5;
  cursor: not-allowed;
}

.rank-btn[disabled]:hover {
  color: var(--text-muted);
  background: var(--bg-surface-alt);
  border-color: var(--line);
}

.rank-btn svg {
  width: 18px;
  height: 18px;
  stroke-width: 2;
}

/* -- Per row expand toggle and detail panel (post launch, desktop only) --
   Phone and tablet widths never see either element: .row-expand stays
   display:none below 1024px (see the 1024px block for where it turns back
   on) and .game-detail is server rendered with the hidden attribute and
   only ever loses it from app.js's onClick, which the invisible, unfocusable
   toggle can never trigger below that width. Below 1024px every field this
   panel duplicates (badge, line, kickoff, each team's record) is already
   sitting inline in .game-meta and .team-btn exactly as it always has. */
.row-expand { display: none; }

/* -- Picks summary bar ---------------------------------------------------
   Section 8 asks for a progress summary plus a Save action and a clear saved
   indicator. app.js drives these exact hooks, so the names are fixed:
     [data-pick-summary]  the "12 of 14 winners chosen" text
     [data-pick-meter]    the bar, filled from a --pct custom property
     [data-save-btn]      the save button, disabled until the slate is full
     [data-save-state]    the badge, class set to save-state is-dirty,
                          is-saving or is-saved
   Add .is-sticky to keep the bar in reach on a long slate.
   ------------------------------------------------------------------------ */

.summary-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

/* Sticky offsets are measured from the bottom of the viewport, not from the
   body padding, so on phone widths this has to clear the fixed tab bar. */
.summary-bar.is-sticky {
  position: sticky;
  bottom: calc(var(--tabbar-h) + var(--space-3) + env(safe-area-inset-bottom, 0px));
  z-index: var(--z-lockbar);
  box-shadow: var(--shadow);
}

.summary-text {
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: .01em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.summary-bar .btn { margin-left: auto; }

/* The fill is drawn from --pct, which app.js writes as a percentage. */
.pick-meter,
[data-pick-meter] {
  position: relative;
  flex: 1 1 120px;
  max-width: 220px;
  height: 6px;
  background: var(--bg-surface-alt);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.pick-meter::after,
[data-pick-meter]::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--pct, 0%);
  background: var(--accent);
  transition: width var(--dur) var(--ease);
}

/* -- The picks page save bar ---------------------------------------------
   picks.html builds this out of .spread plus the hooks below. It is
   deliberately not sticky. The lock bar above it is itself sticky and wraps
   to two or three lines at 360px, so any fixed top offset would either
   overlap it or leave a gap; the template ships a "Back to save" anchor to
   #save instead, which costs nothing and works with JS switched off.
   ------------------------------------------------------------------------ */

.picks-bar {
  padding: var(--space-3) var(--space-4);
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

.picks-progress {
  flex: 1 1 220px;
  min-width: 0;
}

.picks-progress > * + * { margin-top: var(--space-2); }

.picks-summary {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: .01em;
  font-variant-numeric: tabular-nums;
  line-height: var(--lh-tight);
  color: var(--text);
}

/* The track comes from the [data-pick-meter] rule above, which this same
   element carries. app.js writes --pct onto it, and the template also sets
   --pct inline so the first paint is correct before any script runs. */
.picks-meter {
  width: 100%;
  max-width: 280px;
}

/* This variant nests a real element for the fill, so the shared ::after
   would draw a second identical bar on top of it. */
.picks-meter::after { content: none; }

.picks-meter-fill {
  display: block;
  height: 100%;
  width: var(--pct, 0%);
  background: var(--accent);
  transition: width var(--dur) var(--ease);
}

.picks-actions {
  flex: none;
  justify-content: flex-end;
}

/* The HTMX swap target. It is empty on first paint, and an empty box inside
   a .stack would otherwise spend a 16px gap on nothing. */
.picks-status:empty { display: none; }

/* -- Saved indicator ----------------------------------------------------- */

.save-state {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  background: var(--bg-surface-alt);
  font-size: var(--fs-xs);
  font-weight: 600;
  color: var(--text-muted);    /* 7.47:1 on surface-2 */
  white-space: nowrap;
}

.save-state.is-dirty {
  background: var(--decor-soft);
  border-color: var(--decor);
  color: var(--ink);           /* 11.46:1, gold surfaces carry ink */
}

.save-state.is-saving {
  background: var(--bg-surface-alt);
  border-color: var(--line);
  color: var(--text-muted);
}

.save-state.is-saved {
  background: var(--accent-tint);
  border-color: var(--accent-line);
  color: var(--accent-strong);  /* 11.08:1 */
}

/* The save came back 4xx. app.js only knows the success path, so picks.html
   sets this from hx-on::after-request. */
.save-state.is-error {
  background: var(--negative-tint);
  border-color: var(--negative-line);
  color: var(--negative);       /* 5.72:1 on brick-tint */
}

/* -- Stage 3: the lock step ------------------------------------------------
   .lock-panel is a plain .panel. The confirmation itself, [data-lock-panel], starts
   hidden (the HTML hidden attribute, no CSS needed to show or hide it, app.js flips it)
   and is the deliberate extra step between tapping "Lock picks" and the picks actually
   locking, per Section 8: locking has to be hard to trigger by accident, Save does not.
   ------------------------------------------------------------------------ */

.lock-confirm {
  margin-top: var(--space-4);
  padding-top: var(--space-4);
  border-top: var(--hairline);
}

.lock-confirm-list {
  margin: var(--space-3) 0;
  padding-left: var(--space-5);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.lock-confirm-list li + li { margin-top: var(--space-1); }

/* -- HTMX request states -------------------------------------------------
   HTMX toggles .htmx-request on the element issuing the request. It ships no
   stylesheet of its own, so without these an indicator would sit visible all
   the time.
   ------------------------------------------------------------------------ */

.htmx-indicator {
  opacity: 0;
  transition: opacity var(--dur) var(--ease);
}

.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { opacity: 1; }

.htmx-request .btn,
.btn.htmx-request { cursor: progress; }


/* ==========================================================================
   09  BANNERS: LOCK BAR, FLASH, EMPTY STATE
   ========================================================================== */

.lockbar {
  position: sticky;
  top: calc(var(--topbar-h) + 2px);   /* clears the top bar plus its gold rule */
  z-index: var(--z-lockbar);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  /* Full bleed fill, but the text starts on the same line as the 1080px
     content column instead of hugging the window edge on a wide screen. */
  padding-block: var(--space-2);
  padding-inline: max(
    var(--gutter),
    calc((100% - var(--measure)) / 2 + var(--gutter))
  );
  background: var(--decor-soft);
  border-bottom: 1px solid var(--decor);
  color: var(--ink);            /* 11.46:1, gold surface carries ink */
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

.lockbar svg {
  width: 16px;
  height: 16px;
  stroke-width: 2;
  color: var(--ink);
}

.lockbar strong {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: .01em;
}

/* The bar is full bleed, so an inner element pulls its text back onto the
   same line as the 1080px content column. It carries .row, so it is already
   a wrapping flex row. This mirrors .topbar-inner. */
.lockbar-inner {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  min-width: 0;
}

/* The base link colour is --accent, which is unreadable on both lock bar
   fills. Ink on gold-soft is 11.46:1, cream on brick is 6.76:1. */
.lockbar-link {
  color: var(--ink);
  text-decoration: underline;
}

.lockbar-strong .lockbar-link { color: var(--text-invert); }

/* Imminent lock, or the week is already locked. */
.lockbar-strong {
  background: var(--negative);
  border-bottom-color: #7E2F23;
  color: var(--text-invert);    /* 6.76:1 */
}

.lockbar-strong svg { color: var(--text-invert); }

/* -- Co-commissioner invite banner ---------------------------------------
   Shown near the top of a member's own pages (picks, standings, results)
   while they have a pending co-commissioner invite (Post-launch fixes:
   co-commissioner self-service invites with confirmation). Deliberately not
   .lockbar above: that is the site admin borrowing commissioner power for a
   league they do not run, drawn full bleed and sticky in gold to read as
   "you are here on borrowed authority". This is the opposite message, an
   invite into the viewer's own account that they can accept or decline, so
   it sits in normal document flow, in the same understated maroon accent
   .flash already uses for its border-left treatment, with its own
   Accept/Decline actions instead of a single exit link.
   ------------------------------------------------------------------------ */

.invite-banner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-5);
  background: var(--bg-surface);
  border: var(--hairline);
  border-left: 4px solid var(--accent-2);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  color: var(--text);
}

.invite-banner svg {
  width: 18px;
  height: 18px;
  stroke-width: 2;
  color: var(--accent-2);
  flex-shrink: 0;
}

.invite-banner span { flex: 1 1 260px; }

.invite-banner-actions {
  display: flex;
  gap: var(--space-2);
  flex-shrink: 0;
}

/* -- Flash messages ------------------------------------------------------ */

/* The container the base template renders flashes into. */
.flash-stack { margin-top: var(--space-5); }

.flash-stack .flash:last-child { margin-bottom: 0; }

.flash {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  margin-bottom: var(--space-4);
  background: var(--bg-surface);
  border: var(--hairline);
  border-left: 4px solid var(--line);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  color: var(--text);
}

.flash svg {
  width: 18px;
  height: 18px;
  margin-top: 2px;
  stroke-width: 2;
}

.flash p { margin: 0; }

.flash-ok {
  background: var(--accent-tint);
  border-color: var(--accent-line);
  border-left-color: var(--positive);
  color: var(--ink);            /* 14.39:1 */
}

.flash-ok svg { color: var(--positive); }

.flash-error {
  background: var(--negative-tint);
  border-color: var(--negative-line);
  border-left-color: var(--negative);
  color: var(--ink);            /* 14.30:1 */
}

.flash-error svg { color: var(--negative); }

.flash-info {
  background: var(--bg-surface-alt);
  border-color: var(--line);
  border-left-color: var(--decor);
  color: var(--ink);            /* 15.69:1 */
}

.flash-info svg { color: var(--text-muted); }

/* -- Quiet notice --------------------------------------------------------
   A .panel with an icon and a small heading, for the places a flash would be
   too loud. Results uses it for "picks stay private until the week locks".
   ------------------------------------------------------------------------ */

.notice {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
}

/* .panel stacks its children. This one is a row, so the stacking is undone
   here and re-applied inside the body. */
.notice > * + * { margin-top: 0; }

.notice > svg {
  width: 20px;
  height: 20px;
  margin-top: 2px;
  stroke-width: 2;
  color: var(--text-muted);
}

.notice-body { min-width: 0; }

.notice-body > * + * { margin-top: var(--space-2); }

.notice-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 600;
  line-height: var(--lh-snug);
  color: var(--text);
}

/* -- Toasts --------------------------------------------------------------
   Same voice as a flash, but transient and floating. On phone widths it
   clears the fixed tab bar. On desktop it settles bottom right.
   ------------------------------------------------------------------------ */

.toast-stack {
  position: fixed;
  left: var(--space-4);
  right: var(--space-4);
  bottom: calc(var(--tabbar-h) + var(--space-4) + env(safe-area-inset-bottom, 0px));
  z-index: var(--z-menu);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  pointer-events: none;
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--bg-surface);
  border: var(--hairline);
  border-left: 4px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  font-size: var(--fs-sm);
  color: var(--text);
  pointer-events: auto;
}

.toast svg {
  width: 18px;
  height: 18px;
  margin-top: 2px;
  stroke-width: 2;
}

.toast p { margin: 0; }

.toast-ok {
  background: var(--accent-tint);
  border-color: var(--accent-line);
  border-left-color: var(--positive);
  color: var(--ink);            /* 14.39:1 */
}

.toast-ok svg { color: var(--positive); }

.toast-error {
  background: var(--negative-tint);
  border-color: var(--negative-line);
  border-left-color: var(--negative);
  color: var(--ink);            /* 14.30:1 */
}

.toast-error svg { color: var(--negative); }

.toast-close {
  margin-left: auto;
  padding: 0 var(--space-1);
  background: none;
  border: 0;
  color: var(--text-muted);
  line-height: 1;
}

/* -- Empty and pending states -------------------------------------------- */

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-10) var(--space-5);
  background: var(--bg-surface-alt);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  text-align: center;
  color: var(--text-muted);
}

.empty-state p {
  max-width: 46ch;
  margin: 0;
  font-size: var(--fs-sm);
}

.empty-state svg {
  width: 28px;
  height: 28px;
  stroke-width: 1.5;
  /* The darkened gold, not --gold. Decorative --gold is only 2.55:1 on this
     surface, which is too faint to read as a drawn mark. */
  color: var(--focus);
}

/* The block is centred, so the gold rule under the eyebrow centres with it. */
.empty-state .eyebrow::after { margin-inline: auto; }

.empty-state-title {
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--text);
}

.empty-state .btn { margin-top: var(--space-2); }

/* An error page is an empty state with more room around it. */
.error-state {
  margin-block: var(--space-8);
}


/* ==========================================================================
   10  TABLES, LEADERBOARD, BADGES AND STATS

   RESPONSIVE TABLE PATTERN, READ THIS BEFORE WRITING A TEMPLATE.
   Below 768px every .table reflows to stacked cards. That works only if each
   <td> carries a data-label attribute matching its column heading, because the
   label is drawn from attr(data-label). The <thead> is visually hidden on
   mobile and restored at 768px.

     <div class="table-wrap">
       <table class="table">
         <thead>
           <tr><th>Rank</th><th>Player</th><th>Points</th><th>Correct</th><th>Weekly wins</th></tr>
         </thead>
         <tbody>
           <tr class="lb-row lb-leader">
             <td data-label="Rank"><span class="rank-badge is-1 num">1</span></td>
             <td data-label="Player">Dana Whitfield</td>
             <td data-label="Points" class="num">92</td>
             <td data-label="Correct" class="num">10 of 14</td>
             <td data-label="Weekly wins" class="num">2</td>
           </tr>
         </tbody>
       </table>
     </div>
   ========================================================================== */

.table-wrap {
  /* Positioned so it is the containing block for everything inside it. Every
     table in this app carries a visually hidden caption, and the results grid
     puts a visually hidden sentence in every cell. .visually-hidden is
     position: absolute, so without this their containing block is the page
     itself: on a grid wider than the window they land past the right edge,
     escape the overflow below and scroll the whole page sideways. Measured at
     768px on the results page before this line went in: the document scrolled
     53px. */
  position: relative;
  width: 100%;
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--fs-sm);
}

/* Mobile first: stacked cards. */
.table,
.table tbody,
.table tr,
.table td {
  display: block;
}

.table thead {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

.table tr {
  margin-bottom: var(--space-3);
  padding: var(--space-3) var(--space-4);
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

.table tr:last-child { margin-bottom: 0; }

/* A row header (<th scope="row">, the semantically right markup for a player
   name) reflows exactly like a cell. Without this it would stay a table cell
   inside a block row and break the stack. */
.table td,
.table tbody th {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-2) 0;
  border-bottom: 1px dashed var(--line);
  text-align: right;
}

.table tbody th { font-weight: 400; }

.table td:last-child,
.table tbody th:last-child { border-bottom: 0; }

.table td::before,
.table tbody th::before {
  content: attr(data-label);
  flex: none;
  font-family: var(--font-display);
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);    /* 8.04:1 on surface */
  text-align: left;
}

/* -- Leaderboard rows ---------------------------------------------------- */

.lb-row td.num,
.lb-row .num { font-variant-numeric: tabular-nums; }

/* Leader row. A thin gold ribbon, not a loud fill. */
.lb-leader {
  border-color: var(--decor);
  box-shadow: inset 3px 0 0 var(--decor), var(--shadow-sm);
  background: var(--bg-surface);
}

/* The signed in player's own row. Every row is a card below 768px, so the
   marker is the card edge. The desktop form is in the 768px block, where
   rows are real table rows again. */
.lb-row.is-you {
  border-color: var(--accent);
  box-shadow: inset 3px 0 0 var(--accent), var(--shadow-sm);
}

/* The leader ribbon still outranks it. */
.lb-row.lb-leader.is-you {
  border-color: var(--decor);
  box-shadow: inset 3px 0 0 var(--decor), var(--shadow-sm);
}

/* The player cell: a name plus whatever pills it has earned. */
.lb-name {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.lb-player { font-weight: 500; }

/* A note under a leaderboard heading, for example "this week is still being
   scored". */
.lb-note {
  margin: 0 0 var(--space-4);
  font-size: var(--fs-sm);
}

/* A player with no entry that week. Quiet. It is not an error, they simply
   did not play. */
.no-entry {
  font-size: var(--fs-xs);
  color: var(--text-muted);    /* 8.04:1 on surface */
}

/* -- Cells that hold controls or notes ----------------------------------- */

/* Several small forms in one cell. Below 768px the cell is already a flex row
   with its column name drawn from data-label, so the label takes a line of
   its own and the controls wrap underneath it. Above 768px the cell goes back
   to being a real table cell and the forms inside do the flexing. See the
   note in the 768px block. */
.table td.cell-actions {
  flex-wrap: wrap;
  justify-content: flex-start;
  gap: var(--space-2);
  text-align: left;
}

.table td.cell-actions::before { flex: 1 0 100%; }

.cell-note {
  flex: 1 0 100%;
  font-size: var(--fs-xs);
}

/* A value, a kickoff time or a line should not break in the middle of
   itself just because a column is tight. The table scrolls inside
   .table-wrap instead. */
.table .num,
.table .game-line,
.table .line-gap { white-space: nowrap; }

/* A voided game in an admin table. */
.table tr.is-void { border-style: dashed; }

.table tr.is-void td,
.table tr.is-void th { color: var(--text-muted); }   /* 7.47:1 */

/* -- Rank badge ---------------------------------------------------------- */

.rank-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--bg-surface-alt);
  border: var(--hairline);
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--text);
}

.rank-badge.is-1 {
  background: var(--decor-soft);
  border-color: var(--decor);
  color: var(--ink);           /* 11.46:1 */
}

.rank-badge.is-2 {
  background: var(--rank-2);
  border-color: #C4BCA9;
  color: var(--ink);           /* 11.40:1 */
}

.rank-badge.is-3 {
  background: var(--rank-3);
  border-color: #B8996F;
  color: var(--ink);           /* 8.33:1 */
}

/* -- Sortable tables ------------------------------------------------------
   app.js's sortTable() reads table[data-sortable] and th[data-sortable-col],
   sorts tbody rows client side by each cell's data-sort-value (falling back
   to the cell's own text), and keeps aria-sort and a caret in sync on the
   active header. Below 768px there is no header row to click (see the
   data-label reflow at the top of this section), so a .sort-select-wrap
   <select> next to the table drives the exact same function; see results.html
   and leaderboard.html for the markup. Reduced motion is respected in JS by
   reusing the module level reduceMotion flag, not a second query here. */

.sort-select-wrap { margin-bottom: var(--space-4); }

.table th[data-sortable-col] {
  cursor: pointer;
  user-select: none;
}

.table th[data-sortable-col]:hover { color: var(--text); }

.table th[data-sortable-col]:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}

.sort-caret {
  display: inline-block;
  margin-left: var(--space-1);
  vertical-align: -2px;
  color: var(--accent);
}

/* -- By player / by game toggle -------------------------------------------
   Both pick grids are rendered server side (see results.html); this only
   ever hides one of them. Segmented control, matching the week chip's pill
   shape rather than the tab underline used elsewhere, since these two are
   equally weighted alternatives, not a hierarchy. */

.view-toggle {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  margin-bottom: var(--space-4);
  background: var(--bg-surface-alt);
  border: var(--hairline);
  border-radius: var(--radius-pill);
}

.toggle-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap-sm);
  padding: var(--space-1) var(--space-4);
  border: 0;
  border-radius: var(--radius-pill);
  background: none;
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  font-weight: 500;
  letter-spacing: .02em;
  color: var(--text-muted);    /* 6.95:1 on paper, this control sits on surface too */
  cursor: pointer;
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

.toggle-btn:hover { color: var(--text); }

.toggle-btn.is-active {
  background: var(--accent);
  color: var(--text-invert);   /* 11.03:1 */
}

.toggle-btn:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

a.toggle-btn { text-decoration: none; }

/* -- Scenarios panel (Phase 8) -------------------------------------------- */

.scenario-toolbar {
  align-items: flex-start;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.scenario-leverage { margin-top: var(--space-4); }

.leverage-list,
.representative-list {
  margin: 0;
  padding-left: var(--space-5);
}

.leverage-list li,
.representative-list li { margin-top: var(--space-2); }

.scenario-representative {
  margin-top: var(--space-4);
}

.scenario-representative summary {
  cursor: pointer;
  font-weight: 500;
  color: var(--accent);
}

/* Closed by default (Spec Section 8): a short rules refresher a returning player does not
   have to scroll past to get to the games. Native <details>, browser marker removed in favor
   of a rotating caret so the eyebrow rule (a small underline, see .eyebrow::after) stays
   visually intact instead of colliding with a default disclosure triangle. */
.how-it-works summary {
  cursor: pointer;
  list-style: none;
}

.how-it-works summary::-webkit-details-marker { display: none; }

.how-it-works-caret {
  display: inline-block;
  margin-left: var(--space-2);
  vertical-align: -3px;
  transition: transform var(--dur) var(--ease);
}

.how-it-works[open] .how-it-works-caret { transform: rotate(180deg); }

.how-it-works > p,
.how-it-works > .form-hint { margin-top: var(--space-3); }

.custom-scenario-games {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: var(--space-3);
  margin: var(--space-4) 0;
}

.custom-scenario-game {
  border: var(--hairline);
  border-radius: var(--radius);
  padding: var(--space-3);
}

.custom-scenario-game .checkbox { margin-top: var(--space-1); }

/* -- Winner pennant ------------------------------------------------------ */

.ribbon {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 3px var(--space-5) 3px var(--space-3);
  background: var(--decor-soft);
  border-radius: 3px 0 0 3px;
  font-family: var(--font-display);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  line-height: 1;
  color: var(--ink);           /* 11.46:1 */
  /* The notch on the right edge is what makes it read as a pennant. */
  clip-path: polygon(0 0, 100% 0, calc(100% - 9px) 50%, 100% 100%, 0 100%);
}

.ribbon svg {
  width: 13px;
  height: 13px;
  stroke-width: 2;
}

/* -- Stats --------------------------------------------------------------- */

/* Two names for the same idea, because the standings page reads as a row of
   season facts and the admin overview as a grid of pool facts. auto-fit
   means neither has to know how many .stat blocks it holds. */
.stat-row,
.stat-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}

.stat {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-4);
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius-sm);
  min-width: 0;
}

.stat-label {
  font-family: var(--font-display);
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);    /* 8.04:1 on surface */
}

.stat-value {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  color: var(--text);
}

/* A person's name in a stat slot. It has to be able to wrap, and 28px of
   condensed display face is too big for "Marguerite Okonkwo". */
.stat-value-name {
  font-size: var(--fs-lg);
  line-height: var(--lh-snug);
  overflow-wrap: break-word;
}

/* Season: Points / Season: Wins award panels, side by side on desktop, stacking
   on mobile. auto-fit means this still looks right when only one of the two
   scopes has awards to show. Overrides .card + .card's own margin-top, which
   would otherwise misalign the second panel in a grid instead of a stack. */
.season-awards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-4);
  margin-bottom: var(--space-4);
}

.season-awards-grid .card { margin-top: 0; }

/* -- Badges and pills ---------------------------------------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 2px var(--space-2);
  border: 1px solid transparent;
  border-radius: 4px;
  background: var(--bg-surface-alt);
  font-family: var(--font-display);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  line-height: 1.5;
  color: var(--text);
  white-space: nowrap;
}

.badge-nfl {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--text-invert);   /* 11.03:1 */
}

.badge-ncaaf {
  background: var(--accent-2);
  border-color: var(--accent-2);
  color: var(--text-invert);   /* 10.97:1 */
}

.badge-void {
  background: var(--bg-surface-alt);
  border-color: var(--line);
  color: var(--text-muted);    /* 7.47:1 */
  text-decoration: none;
}

.badge-final {
  background: var(--accent-tint);
  border-color: var(--accent-line);
  color: var(--accent-strong);  /* 11.08:1 */
}

/* A game in progress. Gold is the "right now" voice everywhere in this app,
   the same as the lock bar, and a gold surface carries ink per Section 3b. */
.badge-live {
  background: var(--decor-soft);
  border-color: var(--decor);
  color: var(--ink);            /* 11.46:1 */
}

/* A game pinned onto the slate, by a commissioner or by rivalry auto-pin
   (Phase 5). Gold is already this app's "pay attention to this" voice
   (badge-live, the lock bar), which is exactly the point of a pin: it
   overrides the ordinary closeness ranking on purpose. */
.badge-pinned {
  background: var(--decor-soft);
  border-color: var(--decor);
  color: var(--ink);            /* 11.46:1 */
}

/* -- Week status. badge-{{ week.status }} resolves to one of these four ---
   The palette tells the story of a week: neutral while it is a draft, green
   once picks are open, brick once it locks (the same brick .lockbar-strong
   turns), then solid green when it is scored and done.
   ------------------------------------------------------------------------ */

.badge-draft {
  background: var(--bg-surface-alt);
  border-color: var(--line);
  color: var(--text-muted);     /* 7.47:1 */
}

.badge-open {
  background: var(--accent-tint);
  border-color: var(--accent-line);
  color: var(--accent-strong);  /* 11.08:1 */
}

.badge-locked {
  background: var(--negative-tint);
  border-color: var(--negative-line);
  color: var(--negative);       /* 5.72:1 */
}

.badge-scored {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--text-invert);    /* 11.03:1 */
}

/* -- Pool roles ---------------------------------------------------------- */

.badge-commissioner {
  background: var(--decor-soft);
  border-color: var(--decor);
  color: var(--ink);            /* 11.46:1 */
}

.badge-member {
  background: var(--bg-surface-alt);
  border-color: var(--line);
  color: var(--text-muted);     /* 7.47:1 */
}

.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  background: var(--bg-surface-alt);
  border: var(--hairline);
  border-radius: var(--radius-pill);
  font-size: var(--fs-xs);
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  line-height: 1.4;
  color: var(--text-muted);    /* 7.47:1 */
  white-space: nowrap;
}

/* "You". Marks the signed in player's own row or column, on the leaderboard
   and in the results grid. Small, green, and never the loudest thing there. */
.pill-you {
  padding: 1px var(--space-2);
  background: var(--accent-tint);
  border-color: var(--accent-line);
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--accent-strong);  /* 11.08:1 */
}


/* ==========================================================================
   11  RESULTS: WEEK SWITCHER, PICK LEGEND, THE PICKS GRID

   The picks grid is the widest thing in the app: one column per player, one
   row per game. It is a .table, so it inherits the stacked card reflow below
   768px, and above 768px it becomes a real grid that scrolls sideways inside
   .table-wrap and never on the page body.

     <div class="table-wrap">
       <table class="table picks-grid">
         <thead><tr>
           <th class="pick-game is-sticky">Game</th>
           <th class="pick-player">..</th>
         </tr></thead>
         <tbody><tr>
           <th class="pick-game is-sticky">..</th>
           <td class="pick-cell is-correct" data-label="Dana Whitfield">..</td>
         </tr></tbody>
       </table>
     </div>

   Below 768px .pick-game stops being a cell and becomes the card heading, so
   it is the one cell in the app that does not draw a data-label.
   ========================================================================== */

/* -- Week switcher -------------------------------------------------------
   A single scrolling strip of weeks. Results nests a <ul class="week-switch
   -list"> inside the nav, the admin slate editor puts its links straight in,
   so the strip is the nav and the list is transparent to it.
   ------------------------------------------------------------------------ */

.week-switch {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-2);
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.week-switch-list {
  display: flex;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
}

.week-switch > *,
.week-switch-list > li { flex: none; }

.week-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  padding: var(--space-1) var(--space-4);
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius-pill);
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: .02em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--text);
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease),
              color var(--dur) var(--ease);
}

.week-chip:hover {
  background: var(--accent-tint);
  border-color: var(--accent);
  color: var(--accent-strong);   /* 11.08:1 */
  text-decoration: none;
}

.week-chip.is-active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--text-invert);     /* 11.03:1 */
}

/* -- Pick legend ---------------------------------------------------------
   The keys repeat the cell fills exactly, so the legend stays honest. Each
   key border clears the 3:1 floor for meaningful non text content.
   ------------------------------------------------------------------------ */

.pick-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
  margin: 0 0 var(--space-4);
  padding: 0;
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

.pick-legend li {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.pick-key {
  flex: none;
  width: 14px;
  height: 14px;
  border: 1px solid var(--text-muted);
  border-radius: 3px;
  background: var(--bg-surface);
}

.pick-key.is-correct {
  background: var(--accent-tint);
  border-color: var(--positive);     /* 9.40:1 on its own fill */
}

.pick-key.is-wrong {
  background: var(--negative-tint);
  border-color: var(--negative);     /* 5.72:1 */
}

.pick-key.is-pending {
  background: var(--bg-surface);
  border-color: var(--text-muted);   /* 8.04:1 */
}

.pick-key.is-void {
  background: var(--bg-surface-alt);
  border-style: dashed;
  border-color: var(--text-muted);   /* 7.47:1 */
}

/* -- Per player summary --------------------------------------------------
   The grid head carries every player's total, and the head is visually
   hidden below 768px, so this list carries the same totals on a phone. It is
   .mobile-only, which owns its display property.
   ------------------------------------------------------------------------ */

.pick-summary {
  margin: 0 0 var(--space-4);
  padding: 0;
}

.pick-summary-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
  padding: var(--space-2) var(--space-3);
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius-sm);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

.pick-summary-row + .pick-summary-row { margin-top: var(--space-2); }

.pick-summary-row.is-you {
  border-color: var(--accent);
  box-shadow: inset 3px 0 0 var(--accent);
}

/* -- The grid itself ----------------------------------------------------- */

/* Below 768px the first cell of every row is the card heading, not a cell,
   so it drops the flex row and the data-label the reflow normally draws.
   .pick-rowhead is the same idea for the by-player grid, where the row
   heading is a player rather than a game. */
.picks-grid .pick-game,
.picks-grid .pick-rowhead {
  display: block;
  padding: 0 0 var(--space-2);
  margin-bottom: var(--space-1);
  border-bottom: 1px solid var(--line);
  text-align: left;
}

.picks-grid .pick-game::before,
.picks-grid .pick-rowhead::before {
  content: none;
  display: none;
}

.pick-game-name {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 600;
  letter-spacing: .01em;
  line-height: var(--lh-snug);
  color: var(--text);
}

.pick-game-note {
  display: block;
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);    /* 8.04:1 on surface */
}

/* The player column head. .table th is nowrap, and this one is two lines. */
.pick-player { white-space: normal; }

.pick-player .ribbon { margin-bottom: var(--space-1); }

.pick-player-name {
  display: block;
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: normal;
  text-transform: none;
  color: var(--text);
}

.pick-player.is-you .pick-player-name { color: var(--accent-strong); }  /* 12.08:1 */

.pick-player-stats {
  display: block;
  font-family: var(--font-body);
  font-size: var(--fs-xs);
  font-weight: 400;
  letter-spacing: normal;
  text-transform: none;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);    /* 7.47:1 on the head fill */
}

/* One player's pick on one game. The state fill is the whole cell at every
   width, so the tint reads the same on a phone card and in the desktop grid.
   The small inline padding keeps it off the card edge while stacked. */
.picks-grid .pick-cell { padding-inline: var(--space-2); }

.picks-grid .pick-cell.is-correct {
  background: var(--accent-tint);
  color: var(--accent-strong);   /* 11.08:1 */
}

.picks-grid .pick-cell.is-wrong {
  background: var(--negative-tint);
  color: var(--negative);        /* 5.72:1 */
}

.picks-grid .pick-cell.is-pending { color: var(--text-muted); }  /* 8.04:1 */

.picks-grid .pick-cell.is-void,
.picks-grid .pick-cell.is-missing {
  background: var(--bg-surface-alt);
  color: var(--text-muted);      /* 7.47:1 */
}

/* Every cell wraps its content in exactly one element, which is what keeps
   the stacked card layout a clean two child flex row. */
.pick-mark {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.pick-side {
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  font-weight: 600;
  letter-spacing: var(--track-abbr);
  text-transform: uppercase;
  line-height: 1;
}

/* Points staked. Colour is inherited from the cell state, which is already
   audited against that state's fill. */
.pick-points {
  font-family: var(--font-display);
  font-size: var(--fs-xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.pick-none {
  font-family: var(--font-display);
  font-weight: 600;
  color: var(--text-muted);
}

/* -- The by-player grid ---------------------------------------------------
   Rows are players, columns are confidence values. A cell shows the matchup
   staked at that confidence, "GB over CHI", colour coded exactly like the
   by-game grid's cells. A confidence value nobody on the slate maps to for a
   player (an incomplete card) is .is-empty: no fill, no dash, nothing to
   read as an error. */

.picks-grid .pick-conf {
  text-align: center;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.pick-matchup {
  font-family: var(--font-body);
  font-size: var(--fs-sm);
}

.pick-matchup strong {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: var(--track-abbr);
  text-transform: uppercase;
}

.pick-void-badge {
  display: inline-block;
  margin-left: var(--space-1);
  font-family: var(--font-display);
  font-size: var(--fs-xs);
  font-weight: 600;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);    /* 7.47:1 on the void tint */
}

.picks-grid .pick-cell.is-empty { background: none; }


/* ==========================================================================
   12  ADMIN: SUB NAV, BUDGETS, JOIN CODE, SLATE TABLES
   The four commissioner screens. They reuse .card, .panel, .table and the
   form controls; everything below is what those screens add.
   ========================================================================== */

/* -- Sub navigation ------------------------------------------------------
   Repeated inline in all four admin templates. Classic tabs: the active link
   lays its 2px gold rule over the nav hairline. The hairline is an inset
   shadow rather than a border, because the strip scrolls sideways and a link
   overlapping a real border by -1px would trip a 1px vertical scrollbar.
   ------------------------------------------------------------------------ */

.subnav {
  display: flex;
  gap: var(--space-1);
  margin-bottom: var(--space-6);
  box-shadow: inset 0 -1px 0 var(--line);
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.subnav-link {
  flex: none;
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding: var(--space-2) var(--space-3);
  border-bottom: 2px solid transparent;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  font-weight: 500;
  letter-spacing: .02em;
  white-space: nowrap;
  color: var(--text-muted);      /* 6.95:1 on paper */
  transition: color var(--dur) var(--ease), background-color var(--dur) var(--ease);
}

.subnav-link:hover {
  background: var(--bg-surface-alt);
  color: var(--text);
  text-decoration: none;
}

.subnav-link.is-active {
  border-bottom-color: var(--decor);
  font-weight: 600;
  color: var(--accent);          /* 9.54:1 on paper */
}

/* -- Week switcher, admin form -------------------------------------------
   Same strip as the results page (Section 11), but each link stacks the week
   number over its status.
   ------------------------------------------------------------------------ */

.week-switch-link {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 64px;
  min-height: var(--tap);
  padding: var(--space-2) var(--space-3);
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius-sm);
  color: var(--text);
  transition: background-color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}

.week-switch-link:hover {
  border-color: var(--accent);
  background: var(--accent-tint);
  text-decoration: none;
}

.week-switch-num {
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.week-switch-status {
  font-family: var(--font-display);
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  line-height: 1;
  color: var(--text-muted);      /* 8.04:1 on surface */
}

.week-switch-link.is-active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--text-invert);     /* 11.03:1 */
}

.week-switch-link.is-active .week-switch-status {
  color: var(--decor-soft);      /* 7.49:1 on green */
}

/* -- Week summary panel -------------------------------------------------- */

.week-summary { margin-bottom: var(--space-4); }

.target-line {
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}

/* -- Provider budgets ---------------------------------------------------- */

.budget-intro {
  margin-bottom: var(--space-4);
  font-size: var(--fs-sm);
  color: var(--text-muted);      /* 6.95:1 on paper */
}

.budget-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
}

.budget-card { min-width: 0; }

/* Budget spent. The card says so in a flash of its own, this is the edge. */
.budget-card.is-exhausted { border-color: var(--negative-line); }

.budget-card.is-exhausted .meter-fill { background: var(--negative); }

.budget-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
}

.budget-name {
  margin: 0;
  font-size: var(--fs-md);
}

.budget-figure {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
  letter-spacing: .01em;
}

.budget-facts {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-1) var(--space-3);
  margin: 0;
  font-size: var(--fs-sm);
}

.budget-facts dt { color: var(--text-muted); }   /* 7.47:1 on the panel */

.budget-facts dd {
  margin: 0;
  font-weight: 500;
  text-align: right;
  font-variant-numeric: tabular-nums;
}

/* -- Usage meter ---------------------------------------------------------
   Width comes from an inline style, clamped in the template. This is not the
   picks meter: that one is driven by a --pct custom property from app.js.
   ------------------------------------------------------------------------ */

.meter {
  height: 8px;
  background: var(--bg-surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.meter-fill {
  display: block;
  height: 100%;
  background: var(--accent);
}

/* -- Join code ----------------------------------------------------------- */

.join-code {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-2xl);
  font-weight: 600;
  letter-spacing: .16em;
  line-height: var(--lh-tight);
  text-transform: uppercase;
  overflow-wrap: break-word;
  color: var(--accent);          /* 11.03:1 on surface */
}

.join-code-actions {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-5);
}

/* Both are stacks of a control and the sentence explaining it, so .inline-form
   on the rotate form is overridden here. */
.join-code-actions > form {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
  margin: 0;
}

.join-code-set .field {
  width: 100%;
  max-width: 34ch;
  margin-bottom: 0;
}

/* -- Invite links (commissioner and player) ------------------------------
   Native <details>, same pattern .how-it-works and .scenario-representative
   already use for a closed-by-default disclosure: no JS needed to open it. */

.invite-details summary {
  cursor: pointer;
  color: var(--accent);
  font-weight: 500;
  list-style: none;
}

.invite-details summary::-webkit-details-marker { display: none; }

.invite-panel {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-3);
  margin-top: var(--space-3);
  width: 100%;
  max-width: 34rem;
}

.invite-panel .input { width: 100%; }

.invite-message {
  width: 100%;
  min-height: 7rem;
  resize: vertical;
  font-family: var(--font-body);
}

/* -- League switcher (multi-league commissioner) -------------------------
   Native <details>, same disclosure pattern as .invite-details above. Only
   rendered when a real commissioner (never the site admin) runs more than
   one league, so the common single-league case stays the plain text it
   always was. A separate, deliberately understated concept from .lockbar,
   the site admin's "viewing as commissioner" banner: this is a commissioner
   picking between leagues they actually run, not an admin borrowing powers
   for a league they don't. */

.league-switcher { position: relative; }

.league-switcher summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  list-style: none;
  font-size: var(--fs-sm);
  color: var(--text);
}

.league-switcher summary::-webkit-details-marker { display: none; }

.league-switcher summary svg {
  width: 14px;
  height: 14px;
  stroke-width: 2;
  color: var(--text-muted);
}

.league-switcher[open] summary svg { transform: rotate(180deg); }

.league-switcher-panel {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-2));
  z-index: var(--z-menu);
  min-width: 220px;
  padding: var(--space-2);
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.league-switcher-label {
  padding: var(--space-1) var(--space-3);
  font-size: var(--fs-xs);
  font-weight: 500;
  letter-spacing: var(--track-eyebrow);
  text-transform: uppercase;
  color: var(--text-muted);
}

.league-switcher-option {
  display: block;
  width: 100%;
  min-height: var(--tap);
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  text-align: left;
  font-size: var(--fs-sm);
  color: var(--text);
  cursor: pointer;
}

.league-switcher-option:hover {
  background: var(--bg-surface-alt);
  text-decoration: none;
}

/* -- Slate tables -------------------------------------------------------- */

.matchup {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
}

.matchup-at {
  font-size: var(--fs-xs);
  color: var(--text-muted);      /* 8.04:1 on surface */
}

.matchup-full {
  display: block;
  font-size: var(--fs-xs);
}

.line-gap {
  display: block;
  font-size: var(--fs-xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

/* -- Members ------------------------------------------------------------- */

.member-name { font-weight: 600; }

.member-email {
  font-size: var(--fs-sm);
  overflow-wrap: anywhere;
}


/* ==========================================================================
   13  SORTABLEJS DRAG STATES
   Class names are the SortableJS defaults. Styled in theme so a drag looks
   like the rest of the app rather than a browser default.
   ========================================================================== */

.sortable-ghost {
  background: var(--accent-tint);
  border: 1px dashed var(--accent);
  box-shadow: none;
  opacity: .7;
}

.sortable-ghost .team-btn,
.sortable-ghost .game-meta,
.sortable-ghost .rank-controls {
  visibility: hidden;
}

.sortable-chosen {
  border-color: var(--accent);
  background: var(--bg-surface);
}

.sortable-drag {
  box-shadow: var(--shadow);
  border-color: var(--decor);
  opacity: 1;
  cursor: grabbing;
}

.sortable-drag .game-grip { cursor: grabbing; }

/* While a list is being reordered, suppress hover noise on the other rows. */
.game-list.is-dragging .team-btn:hover {
  border-color: var(--line);
  background: var(--bg-surface);
}


/* ==========================================================================
   14  UTILITIES
   ========================================================================== */

/* Tabular figures. Apply to any score, point total or standings number. */
.num {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

.muted { color: var(--text-muted); }

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.spread {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

.desktop-only { display: none; }

.mobile-only { display: block; }


/* ==========================================================================
   16  PUBLIC PAGES: HERO, PRICING, MARKETING
   The landing page, pricing, how to use, and contact. Everything here is
   built from the same cards, buttons and spacing scale the signed in app
   already uses, no new component system.
   ========================================================================== */

/* -- Landing page hero ---------------------------------------------------- */

.hero {
  padding: var(--space-8) 0 var(--space-6);
  text-align: center;
}

.hero-eyebrow {
  justify-content: center;
}

.hero-eyebrow::after {
  margin-inline: auto;
}

.hero-title {
  max-width: 20ch;
  margin: 0 auto;
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 600;
  line-height: var(--lh-tight);
  letter-spacing: .01em;
}

.hero-lede {
  max-width: 56ch;
  margin: var(--space-4) auto 0;
  font-size: var(--fs-md);
  line-height: var(--lh-base);
  color: var(--text-muted);
}

.hero-actions {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-3);
  max-width: 320px;
  margin: var(--space-6) auto 0;
}

/* -- Feature list, three short "how it works" facts on the landing page -- */

.feature-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
  margin-bottom: var(--space-8);
}

.feature-card {
  padding: var(--space-4);
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.feature-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  margin-bottom: var(--space-3);
  background: var(--accent-tint);
  border-radius: var(--radius-sm);
  color: var(--accent);
}

.feature-card h3 {
  margin: 0 0 var(--space-1);
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 600;
}

.feature-card p {
  margin: 0;
  color: var(--text-muted);
  line-height: var(--lh-base);
}

.landing-cta {
  padding: var(--space-6);
  text-align: center;
  background: var(--bg-surface-alt);
  border: var(--hairline);
  border-radius: var(--radius);
}

.landing-cta h2 {
  margin: 0 0 var(--space-2);
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 600;
}

.landing-cta p {
  margin: 0 0 var(--space-4);
  color: var(--text-muted);
}

.landing-cta .btn { min-width: 200px; }

/* -- Pricing ---------------------------------------------------------------- */

.pricing-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}

.pricing-card {
  display: flex;
  flex-direction: column;
  padding: var(--space-5);
  background: var(--bg-surface);
  border: var(--hairline);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.pricing-card.is-featured {
  border-color: var(--decor);
  border-width: 2px;
}

.pricing-name {
  font-family: var(--font-display);
  font-size: var(--fs-md);
  font-weight: 600;
  margin: 0;
}

.pricing-price {
  display: flex;
  align-items: baseline;
  gap: var(--space-1);
  margin: var(--space-3) 0;
  font-family: var(--font-display);
  font-size: var(--fs-3xl);
  font-weight: 600;
  color: var(--accent);
}

.pricing-price .unit {
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  font-weight: 500;
  color: var(--text-muted);
}

.pricing-was {
  font-size: var(--fs-md);
  font-weight: 500;
  color: var(--text-muted);
  text-decoration: line-through;
  text-decoration-color: var(--negative);
}

.pricing-save {
  margin: calc(-1 * var(--space-2)) 0 var(--space-3);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--accent-strong);
}

.pricing-list {
  margin: 0 0 var(--space-5);
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  flex-grow: 1;
}

.pricing-list li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
}

.pricing-list svg {
  flex: none;
  margin-top: 2px;
  color: var(--accent);
}

.refer-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5);
  background: var(--bg-surface-alt);
  border: var(--hairline);
  border-radius: var(--radius);
}

.refer-card h2 {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 600;
}

.refer-card p { margin: 0; color: var(--text-muted); }


/* ==========================================================================
   RESPONSIVE. Breakpoints at 640px, 768px and 1024px.
   Base styles above target 360px.
   ========================================================================== */

/* -- 640px: the two team buttons sit side by side ------------------------ */

@media (min-width: 640px) {
  :root {
    --gutter: var(--space-5);
  }

  .game-teams {
    grid-template-columns: 1fr 1fr;
  }

  /* Away, the "at", then home. This has to follow .game-teams. */
  .game-teams-at {
    grid-template-columns: 1fr auto 1fr;
  }

  .team-name { font-size: var(--fs-base); }

  .form-row {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .card-head,
  .card-body { padding: var(--space-5); }

  .card-head { padding-bottom: var(--space-4); }

  .card-foot { padding: var(--space-4) var(--space-5); }

  .panel { padding: var(--space-5); }

  .game-row { padding: var(--space-3); }

  /* There is room for the signed in name once the bar is wider than a phone. */
  .user-name { display: block; }
}

/* -- 768px: desktop nav appears, tab bar goes away, tables become tables -- */

@media (min-width: 768px) {
  body { padding-bottom: 0; }

  .nav { display: flex; }

  /* The mobile tab bar is a phone control only. */
  .tabbar { display: none; }

  .desktop-only { display: block; }

  .mobile-only { display: none; }

  .page-title { font-size: var(--fs-2xl); }

  .page-head { padding: var(--space-8) 0 var(--space-5); }

  .brand-word { font-size: var(--fs-lg); }

  .brand-mark { height: 36px; }

  /* A mouse can hit 36px comfortably. Phones keep the 44px floor. */
  .btn-sm { min-height: var(--tap-sm); }

  /* No tab bar to clear once the desktop nav takes over. */
  .summary-bar.is-sticky { bottom: var(--space-3); }

  /* Toasts settle in the bottom right once the tab bar is gone. */
  .toast-stack {
    left: auto;
    right: var(--space-5);
    bottom: var(--space-5);
    width: min(380px, calc(100vw - var(--space-10)));
  }

  /* Real table layout returns. */
  .table-wrap {
    background: var(--bg-surface);
    border: var(--hairline);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    overflow-x: auto;
  }

  .table { display: table; }
  .table tbody { display: table-row-group; }

  .table thead {
    display: table-header-group;
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip: auto;
    clip-path: none;
    white-space: normal;
  }

  .table tr {
    display: table-row;
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }

  .table th,
  .table td {
    display: table-cell;
    padding: var(--space-3) var(--space-4);
    border-bottom: var(--hairline);
    text-align: left;
    vertical-align: middle;
  }

  .table thead th {
    font-family: var(--font-display);
    font-size: var(--fs-xs);
    font-weight: 500;
    letter-spacing: var(--track-eyebrow);
    text-transform: uppercase;
    color: var(--text-muted);
    background: var(--bg-surface-alt);
    border-bottom: 1px solid var(--decor);
    white-space: nowrap;
  }

  /* A row header is body copy, not a column heading. */
  .table tbody th {
    font-family: var(--font-body);
    font-size: var(--fs-sm);
    font-weight: 500;
    letter-spacing: normal;
    text-transform: none;
    color: var(--text);
    background: none;
  }

  .table td::before,
  .table tbody th::before { content: none; display: none; }

  .table tbody tr:nth-child(even) { background: var(--bg-surface-alt); }

  .table tbody tr:last-child td { border-bottom: 0; }

  /* Numeric columns right align once there is a real grid to align to. */
  .table th.num,
  .table td.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
  }

  .lb-leader {
    background: var(--bg-surface-alt);
    box-shadow: none;
  }

  .lb-leader td:first-child {
    box-shadow: inset 3px 0 0 var(--decor);
  }

  .lb-leader td { font-weight: 600; }

  /* Rows are rows again, not cards, so your own row takes the same inset
     rule .lb-leader uses. The weekly winner still outranks it. */
  .table tbody tr.is-you,
  .table tbody tr.lb-leader.is-you {
    border: 0;
    box-shadow: none;
  }

  .table tbody tr.is-you td:first-child,
  .table tbody tr.is-you th:first-child {
    box-shadow: inset 3px 0 0 var(--accent);
  }

  .table tbody tr.lb-leader.is-you td:first-child {
    box-shadow: inset 3px 0 0 var(--decor);
  }

  /* An actions cell stays a real table cell. A td set to display: flex is
     pulled out of the table layout and paints outside its own row, so the
     forms inside are what flex and wrap. */
  .table td.cell-actions {
    display: table-cell;
    min-width: 240px;
  }

  .cell-actions > form {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
    margin: 0 var(--space-2) var(--space-2) 0;
    vertical-align: middle;
  }

  .cell-note {
    display: inline-block;
    vertical-align: middle;
  }

  /* A mouse can hit 36px comfortably, the same allowance .btn-sm takes. */
  .input-sm,
  .select-sm,
  .subnav-link,
  .week-chip,
  .week-switch-link { min-height: var(--tap-sm); }

  .stat-row,
  .stat-grid { gap: var(--space-4); }

  .budget-grid { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }

  .join-code-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* -- The results picks grid --------------------------------------------
     One column per player, so it is allowed to outgrow the page. .table-wrap
     already carries overflow-x: auto, which is what keeps the sideways
     scroll inside the panel and off the body.
     --------------------------------------------------------------------- */

  .picks-grid {
    width: auto;
    min-width: 100%;
  }

  /* Colour coded cells read better without a stripe underneath them, and it
     keeps the sticky first column to one opaque fill. */
  .picks-grid tbody tr:nth-child(even) { background: none; }

  .picks-grid .pick-game,
  .picks-grid .pick-rowhead {
    display: table-cell;
    min-width: 180px;
    margin: 0;
    padding: var(--space-3) var(--space-4);
    border-bottom: var(--hairline);
    vertical-align: middle;
  }

  .picks-grid .pick-game-name { font-size: var(--fs-sm); }

  /* The frozen first column. The right hand hairline is a box shadow, not a
     border: with border-collapse the table paints collapsed borders and they
     do not travel with a sticky cell. */
  .picks-grid .is-sticky {
    position: sticky;
    left: 0;
    z-index: 2;
    background: var(--bg-surface);
    box-shadow: 1px 0 0 var(--line);
  }

  /* The head corner has to sit above the body cells it scrolls over. */
  .picks-grid thead .is-sticky {
    z-index: 3;
    background: var(--bg-surface-alt);
  }

  .picks-grid .pick-player {
    min-width: 120px;
    text-align: center;
    vertical-align: bottom;
  }

  .picks-grid .pick-cell {
    padding-inline: var(--space-3);
    text-align: center;
    white-space: nowrap;
  }

  /* -- The by-player grid's sticky header row -----------------------------
     Confidence 15 down to 1 across the top, pinned the same way .is-sticky
     pins the player column on the left; the corner cell needs both. */
  .picks-grid-player thead th {
    position: sticky;
    top: 0;
    z-index: 3;
    background: var(--bg-surface-alt);
  }

  .picks-grid-player thead th.is-sticky { z-index: 4; }

  .picks-grid-player .pick-conf { min-width: 64px; }

  /* Denser meta strip once there is room for it. */
  .game-meta { gap: var(--space-2) var(--space-4); }

  .rank-badge { width: 32px; height: 32px; font-size: var(--fs-base); }

  /* -- Public pages: hero, feature grid, pricing -------------------------- */

  .hero { padding: var(--space-12) 0 var(--space-8); }

  .hero-title { font-size: var(--fs-2xl); max-width: 26ch; }

  .hero-actions {
    flex-direction: row;
    justify-content: center;
    max-width: none;
  }

  .hero-actions .btn { min-width: 180px; }

  .feature-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }

  .pricing-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* -- 1024px: the game row collapses to a single line --------------------- */

@media (min-width: 1024px) {
  :root {
    --gutter: var(--space-6);
  }

  .game-row {
    grid-template-columns: auto minmax(0, 1fr) auto auto;
    grid-template-areas: "grip teams meta rank";
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
  }

  .game-row.is-complete {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas: "teams meta";
  }

  .game-meta {
    flex-wrap: nowrap;
    justify-content: flex-end;
    padding-left: 0;
    min-width: 260px;
  }

  .conf-chip {
    margin-left: var(--space-2);
    min-width: 40px;
    min-height: 30px;
    font-size: var(--fs-base);
  }

  /* -- Compact expandable rows, the open week pick list only ---------------
     Post launch (see DECISIONS.md). Scoped to .game-list[data-sortable]
     .game-row, never plain .game-row, so the read only slate (readonly_list,
     rendered as .game-row.is-complete, both here in state 4 and on
     results.html) keeps showing everything inline exactly as before; this is
     the interactive, still-open pick list only.

     The one thing that cannot get any shorter is .team-btn's 44px minimum
     tap target (Spec 3f), so that is the floor: row height here is that
     44px plus a trimmed var(--space-1) of padding on each edge, 52px total,
     down from about 60px. The badge, the line, kickoff and each team's
     record move out into .game-detail (identical markup, rendered a second
     time, see DECISIONS.md for why a second render was chosen over
     reflowing the matchup/team_button macros) since none of them were ever
     load bearing for the row's height, only its width; team abbreviations,
     full names (already ellipsis truncated, Section 3) and the confidence
     controls stay put for the same reason, moving them would not buy back
     a single pixel of height. The grip and the rank buttons shrink from
     44px to 32px, since only team-btn carries the tap target requirement;
     that is real width back, not height, but it also means a shorter,
     denser looking row. */
  .game-list[data-sortable] { gap: var(--space-1); }

  .game-list[data-sortable] .game-row {
    grid-template-columns: auto minmax(0, 1fr) auto auto auto;
    padding-block: var(--space-1);
  }

  .game-list[data-sortable] .game-row .game-meta { min-width: 0; }

  /* The badge, line and kickoff move into .game-detail below; only the
     confidence controls stay inline. Scoped to direct children of
     .game-meta so .game-detail's own copies, rendered lower in the same
     row, are never touched by this rule. */
  .game-list[data-sortable] .game-row .game-meta > .badge,
  .game-list[data-sortable] .game-row .game-meta > .game-line,
  .game-list[data-sortable] .game-row .game-meta > .game-kick {
    display: none;
  }

  /* Scoped to inside .team-btn specifically, same reasoning: .game-detail
     prints each team's record again, lower in the row, and that copy must
     stay visible. */
  .game-list[data-sortable] .game-row .team-btn .team-record {
    display: none;
  }

  .game-list[data-sortable] .game-row .game-grip {
    width: 32px;
    height: 32px;
  }

  .game-list[data-sortable] .game-row .rank-btn {
    width: 32px;
    height: 32px;
  }

  .game-list[data-sortable] .game-row .rank-btn svg {
    width: 15px;
    height: 15px;
  }

  /* Fifth grid column, not named in .game-row's grid-template-areas (that
     stays "grip teams meta rank" for every other page that shares it), so
     this is placed by explicit line number instead of joining the area
     system. */
  .row-expand {
    display: inline-flex;
    grid-column: 5;
    grid-row: 1;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    flex: none;
    background: var(--bg-surface);
    border: var(--hairline);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease),
                background-color var(--dur) var(--ease);
  }

  .row-expand:hover {
    color: var(--accent);
    border-color: var(--accent);
    background: var(--bg-surface-alt);
  }

  .row-expand-caret { transition: transform var(--dur) var(--ease); }

  .row-expand[aria-expanded="true"] .row-expand-caret { transform: rotate(180deg); }

  /* :not([hidden]), not a bare .game-detail rule, for the same reason
     .game-list-divider uses it (see the note above regroupDivider in
     app.js): a plain class rule sits at the same specificity as the UA
     stylesheet's [hidden] rule but wins on source order, which would make
     this panel visible even while hidden. app.js's onClick toggle removes
     the hidden attribute, waits a frame (skipped under reduceMotion, the
     same flag initMenuToggle reuses), then adds .is-open for the fade in;
     closing does both in the same tick, same as initMenuToggle's close(). */
  .game-detail:not([hidden]) {
    grid-column: 1 / -1;
    grid-row: 2;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-5);
    padding-top: var(--space-2);
    border-top: var(--hairline);
    opacity: 0;
    transition: opacity var(--dur) var(--ease);
  }

  .game-detail.is-open { opacity: 1; }

  .game-detail-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2) var(--space-3);
  }

  .game-detail-team {
    font-size: var(--fs-xs);
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
  }

  .section { margin-bottom: var(--space-10); }

  .stat { padding: var(--space-4) var(--space-5); }
}

/* -- 1280px and up: nothing widens past the 1080px measure --------------- */

@media (min-width: 1280px) {
  .page-head { padding-top: var(--space-10); }
}


/* ==========================================================================
   15  PRINT
   A standings page should print cleanly on a club noticeboard.
   ========================================================================== */

@media print {
  body {
    background: #FFFFFF;
    color: #000000;
    padding-bottom: 0;
  }

  .topbar,
  .tabbar,
  .lockbar,
  .rank-controls,
  .game-grip,
  .skip-link,
  .toast-stack,
  .summary-bar,
  .save-state,
  .picks-bar,
  .picks-status,
  .subnav,
  .week-switch,
  .cell-actions,
  .view-toggle,
  .sort-select-wrap,
  .btn { display: none !important; }

  .card,
  .panel,
  .table-wrap {
    box-shadow: none;
    border-color: #999999;
  }

  .table { display: table; }
  .table thead { display: table-header-group; position: static; clip: auto; clip-path: none; }
  .table tr { display: table-row; }
  .table th, .table td { display: table-cell; }
  .table td::before { content: none; }

  /* On paper the whole grid is visible at once, so nothing has to freeze and
     the phone only summary would be a duplicate of the column heads. */
  .picks-grid .is-sticky { position: static; box-shadow: none; }
  .picks-grid .pick-game { display: table-cell; }
  .pick-summary { display: none !important; }
}


/* ==========================================================================
   16  PAYOUTS: POT FIGURES, DOLLAR/PERCENT MODE TOGGLE
   The Set Payouts editor (Payout system rebuild, Phase 4, /admin/payouts). No existing
   component covered a two-way segmented toggle, so .mode-toggle is new here; everything
   else reuses .card / .table / .field / .btn-secondary etc, unchanged.
   ========================================================================== */

.pot-figures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  padding: var(--space-4);
  background: var(--bg-surface-alt);
  border: var(--hairline);
  border-radius: var(--radius-sm);
}

.pot-figure {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.pot-figure-label {
  font-size: var(--fs-xs);
  color: var(--text-muted);        /* 6.95:1 on paper, 7.47:1 on surface-2 */
  text-transform: uppercase;
  letter-spacing: var(--track-eyebrow);
}

.pot-figure-value {
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.pot-figure-big {
  font-size: var(--fs-xl);
  color: var(--accent);            /* 9.54:1 on paper, 10.25:1 on surface-2 */
}

/* Dollar / percent segmented toggle, styled off two native radio inputs so it stays fully
   keyboard and screen reader operable (a real radiogroup, not a div faking one). Each radio
   is visually hidden and swapped for its wrapping label, which is what makes the whole
   segment, not just a tiny dot, the click target.

   :has() drives the pressed state live, with zero JavaScript, the instant either radio is
   clicked (Baseline widely available since 2023). The server rendered .is-active class,
   from whichever mode the row was actually saved in, is the fallback for a browser without
   :has() support: the toggle still shows which mode is saved even there, it just will not
   repaint again until the next save, which matches this screen's own "server rendered on
   every save, not live JS math" design (see app/routers/payouts.py's module docstring). */
.mode-toggle {
  display: inline-flex;
  border: 1px solid var(--accent);
  border-radius: var(--radius-pill);
  overflow: hidden;
}

.mode-toggle-option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.75rem;
  min-height: var(--tap-sm);
  padding: var(--space-1) var(--space-3);
  font-family: var(--font-body);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--accent);            /* 11.03:1 on surface */
  cursor: pointer;
  transition: background-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

.mode-toggle-option + .mode-toggle-option { border-left: 1px solid var(--accent); }

/* Visually hidden, not display: none, so the input stays focusable and its :checked state
   stays readable by :has() and by assistive tech. */
.mode-toggle-option input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.mode-toggle-option.is-active,
.mode-toggle-option:has(input:checked) {
  background: var(--accent);
  color: var(--text-invert);       /* 11.03:1, same pairing as .btn-primary */
}

.mode-toggle-option:has(input:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 1px;
}

/* The muted "other representation" beside a rule's Value input: the equivalent percent of
   pot when the row is in dollar mode, the resolved dollar figure when it is in percent
   mode. Always visible, per the brief, so nobody has to guess which figure is really in
   play; recomputed server side on every page render against the real effective pot. */
.payout-other-repr {
  display: inline-block;
  margin-left: var(--space-2);
  font-size: var(--fs-xs);
  color: var(--text-muted);        /* 6.95:1 on paper */
  white-space: nowrap;
}

/* The "Add place" row under each scope table: place / label / mode / value / submit, on one
   line from tablet width up. A dedicated class rather than reusing .form-row, since .form-row
   locks in a 2 column grid at 768px (Section 7) which does not fit five unevenly sized
   fields. */
.payout-add-row {
  display: grid;
  grid-template-columns: 1fr;
  align-items: end;
  gap: var(--space-3);
  margin-top: var(--space-4);
}

@media (min-width: 640px) {
  .payout-add-row {
    grid-template-columns: 5rem 1fr 6.5rem 7rem auto;
  }
}

.payout-add-row .field { margin-bottom: 0; }

.payout-summary-strong {
  font-weight: 700;
  border-top: 2px solid var(--line);
}
