/* ==========================================================================
   NFL Frenzy design system — professional LIGHT theme, phone-first.
   Redesigned July 2026 after user review killed the dark green-everything
   look (murky, hid dark logos like NYJ, tiny helmets). Direction now:
   crisp white/off-white surfaces, near-black text, field green + gold as
   ACCENTS ONLY (dark-green top nav + table heads, green buttons, gold
   winner elements). Team imagery ALWAYS sits on white/light neutrals so
   every logo pops, and it renders LARGE.

   DESIGN-CONSISTENCY SWEEP (July 29 2026) — the canonical patterns live HERE
   and page modules must not re-declare them:
     page heading ........ .page-head (+ .page-head-pool/.page-head-links)
     section heading ..... .section-head  (between stacked cards/tables)
     card ................ .card (+ .card-title, which `.card > h2` inherits;
                           .-accent = the one call-to-action card, .-flat)
     card grid ........... .cardgrid  (auto-fill card columns: open pools,
                           admin action cards)
     list row ............ .listrow (+ .-spread) — one bordered row of a short
                           list (pending requests, entries, payments)
     tag pill ............ .chip  (+ .-done / .-now; grid markers .-bye/.-bb/
                           .-miss/.-none/.-hidden reset to square cell markers)
     status badge ........ .badge (winner/cowinner/out/alive/pending/live) —
                           NEVER rendered for a default state
     outcome marking ..... .oc-won / .oc-lost / .oc-tie (+ .pos/.neg) = a LIGHT
                           BACKGROUND FILL in the legacy colours #b8eb9b /
                           #de9087. Rewritten July 29 2026 — this REVERSES the
                           July 28 "no fills, underline only" rule, which only
                           existed because helmets were opaque. See "THE
                           outcome rule" before changing it back.
     button .............. .btn (+ .-primary/.-danger/.-ghost/.-sm/.-block);
                           work surfaces may use a bare <button>, which the
                           admin block maps onto the same geometry.
     empty state ......... .empty (+ .empty-title/.empty-note) — every "there
                           is nothing here yet" block, on every page
     notice .............. .notice (+ .-ok/.-error/.-warn/.-info) via flash()
     form ................ .field / .field-row / .field-help / .field-error
     table ............... .tablewrap > table
     pick board .......... .pickcols-wrap > .pickcols > .pickcol > .pickcell
                           (one column per ENTRY on the picks page; the week's
                           picks AND the season history are rows of the SAME
                           grid; density is container queries on the column,
                           never JS) — the ONE horizontal scroller in the
                           design system
     spacing ............. --sp-1..6 + the .mt-N and .mb-N utilities (never an
                           inline style attribute)

   NEVER write an asterisk immediately followed by a slash inside a comment
   here. The previous version of this header listed the spacing utilities as
   ".mt-<star>/.mb-<star>", which CLOSED THIS COMMENT 140 characters early: the
   leftover words parsed as a selector, and that selector ate the whole
   `:root{}` token block below it. Every var() in this file — every colour,
   radius, tap target and spacing step — silently resolved to nothing, on every
   page. Verified with the browser CSSOM (`:root` absent from cssRules) and
   fixed July 29 2026.
   ========================================================================== */

/* ----- tokens ----- */
:root {
  /* surfaces: light neutrals with a whisper of green */
  --bg: #f2f4f0;            /* page */
  --surface: #ffffff;       /* cards / primary surface */
  --surface-2: #f1f4f0;     /* hover / raised */
  --head-band: #e4f0e2;     /* LIGHT-GREEN sticky-header band: week bar + page/board title bar, one colour across every page (user, Aug 2026: "the light green header is sticky ... the week header bar background needs to match the light green background") */
  --surface-3: #e6ece6;     /* active / highest */
  --line: #dfe5dd;          /* hairlines */
  --line-strong: #c0cabe;
  /* THE PICKS-BOARD ENTRY DIVIDER, AND NOTHING ELSE USES IT. The vertical rule
     between entry columns ran at --line-strong (#c0cabe) and the user said twice
     it was still not dark enough — "there needs to be very clear division between
     the picks columns", then "the vertical lines dividing the entrys on the picks
     page are still not dark enough". This is its own token because --line-strong
     is a hairline colour shared by ~40 rules across every page; darkening THAT to
     fix one grid would have put a heavy edge on every card, table and button on
     the site. One owner, one job. */
  --pc-divider: #46564b;

  /* ink */
  --text: #161b18;          /* near-black */
  --text-muted: #4f5d55;    /* 6.9:1 on white */
  --text-faint: #5f6c65;    /* 5.5:1 on white, 5.0:1 on --bg */

  /* brand accents */
  --accent: #0b6b32;        /* field green: links, buttons, active states */
  --accent-deep: #085226;
  --accent-soft: rgba(11, 107, 50, 0.08);
  --nav-bg: #0a3d20;        /* dark-green chrome (top nav ONLY — no logos on it) */
  --nav-bg-2: #0d4a27;

  /* status */
  --gold: #8a6508;          /* text-safe old gold on light surfaces (5.3:1) */
  --gold-deep: #6f5106;
  --gold-bright: #f7c948;   /* gold as a BACKGROUND (badges, winner hero) */
  --gold-bright-deep: #d4a017;
  --red: #c62828;           /* eliminations / urgency (5.6:1 on white) */
  --red-soft: rgba(198, 40, 40, 0.10);
  --green-live: #0b7a37;    /* alive / live / positive text (5.5:1 on white) */
  --green-soft: rgba(11, 122, 55, 0.12);
  --brand-red: #a6192e;     /* legacy swoosh red — sparing accent */

  /* type: fluid scale */
  --font: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --fs-xs: clamp(0.72rem, 0.68rem + 0.2vw, 0.8rem);
  --fs-sm: clamp(0.82rem, 0.78rem + 0.25vw, 0.92rem);
  --fs-base: clamp(0.95rem, 0.9rem + 0.3vw, 1.05rem);
  --fs-lg: clamp(1.1rem, 1rem + 0.5vw, 1.3rem);
  --fs-xl: clamp(1.35rem, 1.2rem + 0.9vw, 1.8rem);
  --fs-2xl: clamp(1.7rem, 1.4rem + 1.6vw, 2.5rem);

  --radius: 10px;
  --radius-lg: 16px;
  --shadow: 0 1px 2px rgba(15, 23, 18, 0.05), 0 2px 10px rgba(15, 23, 18, 0.06);
  --shadow-lg: 0 12px 32px rgba(15, 23, 18, 0.14);
  --tap: 44px;              /* minimum touch target */
  --tabbar-h: calc(58px + env(safe-area-inset-bottom, 0px));
  --topnav-h: 56px;
  /* Height of the sticky WEEK BAR (the second header, .week-nav). A fixed height
     so everything that has to sit below it — the desktop scores sidebar's own
     sticky top — can do the arithmetic. Only pages that pass `weeks` render it,
     hence the :has() guards rather than a blanket offset. */
  --weekbar-h: 46px;
  /* The shell's own horizontal padding, named so the picks board can cancel
     exactly it and run edge to edge. Kept in step with .shell below. */
  --shell-pad-x: 14px;
  --sidebar-w: 360px;

  /* ONE spacing scale (July 2026 sweep). Every gap/margin/padding picks a step
     from this ladder — no more one-off `style="margin-top:12px"` leftovers.
     4 / 8 / 12 / 16 / 24 / 32. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --gap-block: var(--sp-4);  /* vertical rhythm between stacked blocks */

  /* NO ROW STRIPING AND NO ROW HOVER ON DATA GRIDS (user, July 29 2026): "the
     background is either red, white, or green". A grid cell's background is its
     OUTCOME and nothing else. There is deliberately no --row-hover token: any
     row-level background wins over the cell's outcome fill, which is how
     hovering the picks list made the green disappear. Do not add one back. */

  /* OUTCOME FILLS — four states, all rgba so they sit lighter over the white
     cell and composite with hover instead of fighting it (user asked for
     "lighter, more transparent"). Hue identity is the legacy site's
     #b8eb9b / #de9087 from standingsknock.php's <td bgcolor>, kept as the
     saturated core so the colours still read as the same green and red.
       final  = the game is over: this pick won or lost
       live   = the game is being played and the pick is currently ahead/behind,
                deliberately much fainter so "not settled yet" is obvious at a
                glance without reading the clock. */
  --oc-win-bg: rgba(118, 196, 74, 0.34);
  --oc-win-ink: #17420a;
  --oc-loss-bg: rgba(206, 84, 68, 0.32);
  --oc-loss-ink: #5a140c;
  --oc-live-win-bg: rgba(118, 196, 74, 0.15);
  --oc-live-loss-bg: rgba(206, 84, 68, 0.14);
}

/* ----- reset-ish ----- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--fs-base);
  line-height: 1.5;
  color: var(--text);
  background:
    linear-gradient(180deg, rgba(11, 107, 50, 0.05) 0%, rgba(11, 107, 50, 0) 260px),
    var(--bg);
  min-height: 100dvh;
}
img { max-width: 100%; }
/* [hidden] must ALWAYS win. Any component rule that sets display (.btn is
   inline-flex, .pbar-links is flex) out-ranks the UA's [hidden]{display:none},
   so a hidden element renders anyway — that is how the "Add to home screen"
   button showed up on browsers that never offered an install, and the same bug
   rendered the Scores link twice in the top bar (ui.ts kept a local patch for
   its own case). One rule, once, for the whole site. */
[hidden] { display: none !important; }
h1, h2, h3 { line-height: 1.2; margin: 0 0 0.5em; }
h1 { font-size: var(--fs-2xl); letter-spacing: 0.01em; }
h2 { font-size: var(--fs-xl); }
h3 { font-size: var(--fs-lg); }
p { margin: 0 0 1em; }
a { color: var(--accent); text-underline-offset: 3px; }
a:hover { color: var(--accent-deep); }
small { font-size: var(--fs-sm); color: var(--text-muted); }
hr { border: 0; border-top: 1px solid var(--line); margin: 1.25rem 0; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }

/* ==========================================================================
   Shell layout
   ========================================================================== */

.shell {
  max-width: 1400px;
  margin: 0 auto;
  padding: 18px 14px calc(var(--tabbar-h) + 22px);
  /* Named so full-bleed children can undo it at any width (standings grid). */
  --shell-pad-x: 14px;
}
.shell-main { min-width: 0; }
.shell-sidebar { display: none; }

/* Desktop >=1100px: persistent live-scores sidebar, tab bar goes away. */
@media (min-width: 1100px) {
  .shell { padding: 28px 24px 44px; --shell-pad-x: 24px; }
  .shell.has-sidebar {
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--sidebar-w);
    gap: 24px;
    /* MAIN COLUMN STRETCHES to the full row height — THE one-place fix for
       the sticky title bar creeping up at the bottom of EVERY rail page
       (user, Aug 11 2026: "fix it once instead of one page at a time"). A
       sticky element can only travel inside its containing block; with
       align-items:start the main column ended at its own content, so on any
       page whose content is shorter than the rail the last rail-height of
       scroll pushed the stuck bar up and out. Stretched, the bar's travel
       covers the whole scroll range on every page that uses this shell.
       The rail itself keeps align-self:start below — ITS jump fix stays. */
    align-items: stretch;
  }
  .shell-main { min-height: 0; }
  .shell-sidebar {
    display: block;
    position: sticky;
    /* align-self: start IS THE FIX FOR THE JUMP. A grid item defaults to
       stretch, so the rail was as tall as the whole main column; a sticky box
       can only travel inside its own containing block, and a stretched one has
       already used all of it — so it held position until the very bottom of the
       page and then lurched upward as the block ran out (user, July 31 2026:
       "the scores sidebar can't move... it jumps up when you scroll all the way
       to the bottom of the page").
       Starting it at the top of the track gives it the whole column to travel
       through, so it stays put the entire way down. */
    align-self: start;
    position: sticky;
    top: calc(var(--topnav-h) + 16px);
    /* FIXED full height, not content height (user, Aug 11 2026: "the overlay
       scoreboard needs to be at max height even if there aren't games to fill
       it") — a sparse week must not render a shorter window, because a week
       switch swaps the games INSIDE the box and nothing re-sizes the box. */
    height: calc(100dvh - var(--topnav-h) - 32px);
    overflow-y: auto;
    background: var(--surface);
    /* SQUARE, HEAVY, FLOATING (user, Aug 8 2026): the scrollbar cut off the
       right-side rounding so rounded corners read broken — square them. The
       thicker border + deep shadow make the window sit ABOVE the page. */
    border: 2px solid var(--line-strong);
    border-radius: 0;
    box-shadow: 0 12px 30px rgba(15, 23, 18, 0.28), 0 3px 8px rgba(15, 23, 18, 0.16);
  }
  .shell-sidebar-inner { padding: 14px; }
  .shell { --tabbar-h: 0px; }
  /* The scores sidebar is sticky too. On a page that has the week bar it must
     stop UNDER it, not behind it. */
  body:has(.week-nav) .shell-sidebar {
    top: calc(var(--topnav-h) + var(--weekbar-h) + 16px);
    height: calc(100dvh - var(--topnav-h) - var(--weekbar-h) - 32px);
  }
}

/* Phone: Scores tab toggles the sidebar as a full overlay (app.js adds .scores-open). */
@media (max-width: 1099.98px) {
  body.scores-open .shell-sidebar {
    display: block;
    position: fixed;
    inset: var(--topnav-h) 0 var(--tabbar-h) 0;
    z-index: 40;
    overflow-y: auto;
    background: var(--bg);
    border-top: 1px solid var(--line-strong);
  }
  body.scores-open .shell-sidebar-inner { padding: 12px; }
  body.scores-open { overflow: hidden; }
}

/* ----- top nav: the ONE dark-green surface (chalk mark needs it; no team
       imagery ever renders here) ----- */
.topnav {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: 10px;
  height: var(--topnav-h);
  padding: 0 12px;
  padding-left: max(12px, env(safe-area-inset-left));
  padding-right: max(12px, env(safe-area-inset-right));
  background: linear-gradient(180deg, var(--nav-bg-2), var(--nav-bg));
  border-bottom: 1px solid rgba(0, 0, 0, 0.25);
  box-shadow: 0 1px 6px rgba(10, 61, 32, 0.35);
}
.topnav-brand {
  display: flex;
  align-items: center;
  gap: 8px;
  text-decoration: none;
  flex-shrink: 0;
  min-height: var(--tap);
}
.topnav-mark { display: block; }
.topnav-word {
  font-size: 1.05rem;
  letter-spacing: 0.04em;
  color: #f4f8f4;
  white-space: nowrap;
}
.topnav-word b { color: var(--gold-bright); font-weight: 800; }
.topnav-pool {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: var(--fs-sm);
  color: rgba(244, 248, 244, 0.8);
}
.topnav-user { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.topnav a { color: #f4f8f4; }
.topnav a:hover { color: #fff; }
.topnav .btn.-ghost {
  color: #f4f8f4;
  border-color: rgba(244, 248, 244, 0.4);
  background: transparent;
}
.topnav .btn.-ghost:hover { background: rgba(244, 248, 244, 0.12); color: #fff; }
@media (max-width: 480px) {
  .topnav-word { display: none; }  /* mark only on small phones */
}
/* THE TOP ROW'S WIDTH BUDGET AT 375px (measured July 30 2026).
   375 - 24 padding - 79 mark - 10 gap - 46 account button - 10 gap = 206px for
   the whole pool bar, which then had to seat the pool name, the Year select and
   an Admin link. The pool name lost, every time: it rendered in a 27.6px box.
   The mark is the one item in that row that costs 79px and carries no
   information the wordmark on / does not already carry. Regenerated Aug 14
   2026 from the source emblem with the WHOLE football on the canvas (user:
   "the fucking football cutoff on the right side") — 38x26 undistorted at
   every width, so the old phone-shrink override is gone. */

/* pool switcher slot: style a bare <select> dropped into it (sits on dark nav) */
.topnav-pool select {
  max-width: 100%;
  background: rgba(255, 255, 255, 0.14);
  color: #fff;
  border: 1px solid rgba(244, 248, 244, 0.35);
  border-radius: var(--radius);
  min-height: 36px;
  padding: 4px 8px;
  font-size: var(--fs-sm);
}
.topnav-pool select option { color: var(--text); background: #fff; }

/* ----- user menu (details/summary popover) ----- */
.usermenu { position: relative; }
.usermenu-btn {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: var(--tap);
  padding: 4px 6px;
  border-radius: var(--radius);
  color: #f4f8f4;
}
.usermenu-btn::-webkit-details-marker { display: none; }
.usermenu-btn:hover { background: rgba(244, 248, 244, 0.12); }
.usermenu-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--gold-bright);
  color: #221a02;
  font-weight: 800;
  flex-shrink: 0;
}
/* PROFILE PICTURES (039). The initial stays in the DOM underneath every
   avatar and simply shows through when there is no picture — so a missing or
   still-loading image degrades to the old gold circle rather than a gap. */
.usermenu-avatar { position: relative; overflow: hidden; }
.avatar-img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
}
/* The settings-page picker */
.avatarrow { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; }
.avatarprev {
  position: relative;
  width: 96px;
  height: 96px;
  border-radius: 50%;
  overflow: hidden;
  background: var(--gold-bright);
  color: #221a02;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.avatarprev img { width: 100%; height: 100%; object-fit: cover; }
.avatarprev-initial { font-size: 2.4rem; font-weight: 800; }
.avatarcta { display: flex; flex-direction: column; gap: var(--sp-2); align-items: flex-start; }
/* CHOOSE WHICH PART OF THE PICTURE SHOWS (user, Aug 25 2026). A fixed circular
   window with the picture moving behind it — a portrait no longer gets its
   middle taken automatically. touch-action:none so a drag pans the picture
   instead of scrolling the page on a phone. */
.croppanel { margin-top: var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-3); align-items: flex-start; }
.cropstage {
  position: relative;
  width: 260px;
  max-width: 100%;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius);
  overflow: hidden;
  background: #10140f;
  touch-action: none;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
}
.cropstage:active { cursor: grabbing; }
.cropstage canvas { display: block; width: 100%; height: 100%; }
.cropmask { position: absolute; inset: 0; border-radius: 50%; box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.55); pointer-events: none; }
.cropzoom { display: flex; align-items: center; gap: var(--sp-2); width: 260px; max-width: 100%; font-size: var(--fs-sm); color: var(--text-muted); }
.cropzoom input { flex: 1; min-width: 0; }
.cropbtns { display: flex; gap: var(--sp-2); flex-wrap: wrap; }
/* On the standings board, in front of the entry name */
.ava-sm {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Sized in the ROW's em — hence no font-size here; the letters get their own
     size on .ava-ini below. Was two lines of name tall; up 50% on request. */
  width: 3.75em;
  height: 3.75em;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
  background: var(--gold-bright);
  color: #221a02;
}
.ava-ini { font-size: 1.58em; font-weight: 800; letter-spacing: -0.03em; line-height: 1; }
/* Beside the My Pools heading. Only rendered when a real picture exists — no
   initials stand-in here, the heading already says whose page it is. */
.home-head { display: flex; align-items: center; gap: 10px; }
.home-ava { width: 1.5em; height: 1.5em; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
.ava-sm img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.usermenu-name { font-size: var(--fs-sm); color: #f4f8f4; }
@media (max-width: 640px) { .usermenu-name { display: none; } }
.usermenu-pop {
  position: absolute;
  right: 0;
  top: calc(100% + 6px);
  min-width: 220px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: 8px;
  z-index: 60;
  color: var(--text);
}
.usermenu-who { padding: 6px 8px 10px; border-bottom: 1px solid var(--line); margin-bottom: 6px; }
.usermenu-who small { color: var(--text-muted); }
.usermenu-link {
  display: flex;
  align-items: center;
  min-height: var(--tap);
  padding: 0 8px;
  border-radius: 8px;
  text-decoration: none;
}
.topnav .usermenu-pop a { color: var(--text); }
.usermenu-link:hover { background: var(--surface-2); color: var(--text); }
.usermenu-logout { margin: 6px 0 0; }
.topnav .usermenu-pop .btn.-ghost { color: var(--text); border-color: var(--line-strong); }
.topnav .usermenu-pop .btn.-ghost:hover { background: var(--surface-2); color: var(--text); }

/* ----- bottom tab bar (phone): clean white, green active ----- */
.tabbar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 50;
  display: flex;
  /* The SAME light green as the sticky header band (user, Aug 12 2026: "make
     the background on the bottom menu the same light green as the header
     background so it doesn't blend as much with the detail") — white-on-white
     made the bar disappear into the page. */
  background: var(--head-band);
  border-top: 1px solid var(--line-strong);
  box-shadow: 0 -2px 12px rgba(15, 23, 18, 0.07);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.tabbar-tab {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 56px;
  font-size: var(--fs-xs);
  font-weight: 600;
  text-decoration: none;
  color: var(--text-muted);
  border-top: 3px solid transparent;
  margin-top: -1px;
}
/* THE ACTIVE TAB IS A SELECTED CHIP. It used to be a coloured word with a thin
   rule over it, which reads as "slightly emphasised" rather than "this is the
   page you are on" — and it looked nothing like the week chips directly above
   it, which is the site's existing vocabulary for a chosen thing (user, Aug 1
   2026: "the buttons on the bottom on mobile should look like a selected week
   chip when selected... if picks is tapped, it should turn green with white
   text"). Same fill, same white text, same shape as W2 in the week bar. */
.tabbar-tab.-active {
  color: #fff;
  background: var(--accent-deep);
  border-top-color: var(--accent-deep);
  font-weight: 700;
  border-radius: 10px;
  margin: 4px 6px;
  min-height: 48px;
}
.tabbar-tab.-active svg { color: #fff; }
/* The surname sits under the first name: the rows are already tall enough for
   it, and the width it frees goes to the week column. */
.entry-surname { display: block; }

/* Grid-view toggles that act on WEEK COLUMNS are desktop-only: a phone shows one
   week at a time, so reordering or hiding weeks there is either meaningless or
   hides the week on screen. */
.gsz-wideonly { display: contents; }
@media (max-width: 700px) { .gsz-wideonly { display: none; } }
.tabbar-tab:hover { color: var(--text); }
/* Desktop: tab bar retires; sidebar is persistent (must follow the base rule). */
@media (min-width: 1100px) { .tabbar { display: none; } }

/* (No .shell-footer block: pageShell renders no footer element — 10 lines of
   unreachable CSS, deleted in the July 29 2026 sweep along with .winner-banner
   [superseded by .winner-hero], .empty-cell, .hide-phone/.only-phone and the
   .install-btn print rule [app.js keys off [data-install-btn]].) */

/* ==========================================================================
   Components
   ========================================================================== */

/* ----- cards ----- */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--sp-4);
  box-shadow: var(--shadow);
  margin-bottom: var(--gap-block);
}
/* A card's title is `.card-title` OR a plain <h2> inside the card — both render
   identically. Before the sweep pools.ts (FORM_CSS) gave `.card h2` a hairline
   underline, payments.ts (PAY_HEAD) gave it none, and rules.ts used
   `.card-title`: three looks for one thing. */
.card-title,
.card > h2 {
  font-size: var(--fs-lg);
  font-weight: 700;
  margin: 0 0 var(--sp-3);
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  line-height: 1.25;
}
.card-title small, .card > h2 small { font-weight: 400; color: var(--text-muted); }
.card > :last-child { margin-bottom: 0; }
.card.-flat { box-shadow: none; }
/* The ONE call-to-action card (buy back in, "one more entry?"): same card,
   green edge + tint so it reads as the thing to do without inventing a colour. */
.card.-accent {
  border-color: var(--accent);
  border-left-width: 4px;
  background: linear-gradient(0deg, var(--accent-soft), var(--accent-soft)), var(--surface);
}

/* Card columns: open pools on Home, action cards in admin. One grid, one gap. */
.cardgrid {
  display: grid;
  gap: var(--sp-3);
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  margin-bottom: var(--gap-block);
}
.cardgrid > .card { margin-bottom: 0; }

/* One row of a short list (pending join requests, your entries, payments
   awaiting review). Was .pendrow (pools settings), .entryrow (join page) and
   .payrow (payments admin) — three near-identical declarations. */
.listrow {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 10px var(--sp-3);
  min-height: 48px;
}
.listrow.-spread { justify-content: space-between; }
.listrow + .listrow { margin-top: var(--sp-2); }
.listrow-main { min-width: 0; }
.listrow:hover { border-color: var(--line-strong); }

/* hero panel (login/home): the field photo keeps its dark wash — tokens are
   re-scoped so page-module styles inside it stay chalk-on-green. */
.field-hero {
  position: relative;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background:
    linear-gradient(180deg, rgba(7, 31, 18, 0.82), rgba(11, 61, 31, 0.88)),
    url('/brand/field-hero.jpg') center/cover no-repeat;
  border: 1px solid rgba(10, 61, 32, 0.4);
  padding: clamp(20px, 5vw, 48px);
  box-shadow: var(--shadow-lg);
  margin-bottom: var(--gap-block);
  --text: #f4f8f4;
  --text-muted: #cfe0d2;
  --text-faint: #a9c0ae;
  --gold: var(--gold-bright);
  color: #f4f8f4;
}
.field-hero a { color: #dff0e2; }
.field-hero a:hover { color: var(--gold-bright); }

/* chalk yard-line texture for section headers/banners (light version) */
.field-lines {
  background-image: repeating-linear-gradient(
    90deg,
    transparent 0 56px,
    rgba(11, 61, 31, 0.06) 56px 58px
  );
}

/* ----- buttons ----- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: var(--tap);
  padding: 8px 18px;
  border-radius: var(--radius);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--text);
  font-size: var(--fs-base);
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  touch-action: manipulation;
  user-select: none;
  transition: background 0.15s, border-color 0.15s, transform 0.05s;
}
.btn:hover { background: var(--surface-2); color: var(--text); border-color: var(--text-faint); }
.btn:active { transform: translateY(1px); }
.btn.-primary {
  background: linear-gradient(180deg, var(--accent), var(--accent-deep));
  border-color: var(--accent-deep);
  color: #fff;
}
.btn.-primary:hover { filter: brightness(1.08); color: #fff; }
.btn.-danger { background: var(--brand-red); border-color: #7c1322; color: #fff; }
.btn.-danger:hover { background: #c02036; color: #fff; }
.btn.-ghost { background: transparent; border-color: var(--line-strong); }
.btn.-ghost:hover { background: var(--surface-2); }
.btn.-block { display: flex; width: 100%; }
.btn.-sm { min-height: 36px; padding: 4px 12px; font-size: var(--fs-sm); }
.btn[disabled], .btn.-disabled { opacity: 0.45; pointer-events: none; }

/* ----- forms ----- */
.field { margin-bottom: var(--sp-3); }
.field label {
  display: block;
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--text-muted);
  margin-bottom: 4px;
}
.field input[type="text"],
.field input[type="email"],
.field input[type="password"],
.field input[type="number"],
.field input[type="url"],
.field input[type="tel"],
.field select,
.field textarea,
input.input, select.input, textarea.input {
  width: 100%;
  min-height: var(--tap);
  padding: 8px 12px;
  font-size: 16px; /* >=16px stops iOS zoom-on-focus */
  font-family: var(--font);
  color: var(--text);
  background: #fff;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
}
.field input:focus, .field select:focus, .field textarea:focus,
.input:focus { outline: 2px solid var(--accent); outline-offset: 0; border-color: var(--accent); }
.field input::placeholder, .field textarea::placeholder { color: var(--text-faint); }
.field .field-help { font-size: var(--fs-xs); color: var(--text-faint); margin-top: 3px; }
.field.-error input, .field.-error select { border-color: var(--red); }
.field .field-error { font-size: var(--fs-xs); color: var(--red); margin-top: 3px; }
.field-row { display: flex; gap: var(--sp-3); flex-wrap: wrap; }
.field-row > .field { flex: 1 1 180px; }
input[type="checkbox"], input[type="radio"] { width: 20px; height: 20px; accent-color: var(--accent); }

/* ----- notices / flash -----
   ONE notice component for every page (auth errors, admin warnings, join
   states). Page modules must never re-declare `.notice ul` — it lives here. */
.notice {
  padding: 10px 14px;
  border-radius: var(--radius);
  border: 1px solid;
  margin-bottom: var(--sp-3);
  font-size: var(--fs-sm);
}
/* ACTION CONFIRMATIONS ARE CORNER TOASTS, NOT LAYOUT (user, Aug 2 2026: "I
   TOLD YOU TO GET RID OF ALL THESE LAYOUT BREAKING SAVES.. INFO LIKE THIS
   SHOULD APPEAR AS AN OVERLAY IN A CORNER THEN DISAPPEAR"). The -toast
   modifier is what ui.ts toast() emits, and it is used ONLY for the
   ?ok=/?err= echoes a save redirects back with — never for structural page
   content like "You have no entries in this pool", which must stay inline and
   stay put. position:fixed lifts the toast out of flow so nothing below it
   ever moves. Top-right, under the nav.
     -ok / -info    slide in, hold ~4s, fade out on pure CSS — no JS involved,
                    so a page whose script failed behaves the same.
     -error / -warn slide in, hold ~8s, fade out — AND dismiss early on tap
                    (app.js). They used to stay until clicked; overruled
                    Aug 11 2026: "ALL messages like this... should clear
                    after 5-10 sec".
   A second toast in the same response (err + ok emitted adjacent) steps down
   so the two never overlap. */
/* ADMIN PAGE CARDS (user, Aug 11 2026: "no organization, no formatting,
   everything is just run on sentences of options") — one bordered section per
   job on /:slug/:season/admin. Square corners per the site chrome rules. */
.admsec {
  background: var(--surface);
  border: 1px solid var(--line-strong);
  padding: 14px 18px 16px;
  margin: 16px 0;
  box-shadow: var(--shadow);
}
.admsec .section-head { margin-top: 0; }
.admsec > :last-child { margin-bottom: 0; }
/* A folded section must LOOK openable (user, Aug 11 2026: "just a bar with no
   indication that you click on it... looks like an empty setting"): chevron,
   an underlined show/hide tag, pointer, hover surface. */
.admsec details > summary {
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 10px;
  list-style: none;
  padding: 4px 6px;
  margin: -4px -6px;
}
.admsec details > summary::-webkit-details-marker { display: none; }
.admsec details > summary::before { content: '\25B8'; color: var(--text-faint); font-size: 0.85em; }
.admsec details[open] > summary::before { content: '\25BE'; }
.admsec details > summary:hover { background: var(--surface-2); }
.admsec details > summary .admfold-hint::after {
  content: 'show';
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--accent);
  text-decoration: underline;
}
.admsec details[open] > summary .admfold-hint::after { content: 'hide'; }
/* THE TWO ADMIN ZONES (user, Aug 11 2026: "two distinct sections, superadmin
   and admin... separate borders around each section, different colored ones").
   Green = pool-admin work; amber = superadmin-only (matches the view-as bar's
   warning family — amber is the one chrome color not spoken for). The amber
   zone is ABSENT from the HTML for non-superadmins, not merely hidden. */
.admzone {
  border: 2px solid var(--line-strong);
  padding: 4px 16px 16px;
  margin: 20px 0;
  background: var(--bg);
}
.admzone-head {
  display: inline-block;
  font-size: var(--fs-sm);
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 3px 10px;
  margin: -4px 0 4px -16px;
  color: #fff;
}
.admzone.-admin { border-color: var(--accent-deep, #0a3d20); }
.admzone.-admin > .admzone-head { background: var(--accent-deep, #0a3d20); }
.admzone.-super { border-color: #b98600; background: #fffdf5; }
.admzone.-super > .admzone-head { background: #b98600; }
.admzone .admsec { margin: 12px 0; }
.admswitch { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; padding: 10px 0; }
/* THE MEMBER ROSTER TABLE (user, Aug 11 2026: "put it back as a table/grid")
   — three columns that fit: email under the name, and each entry on its own
   grid line inside the Entries cell so every button shares a column. */
.mtable td { vertical-align: top; }
.mtable .mt-who { min-width: 180px; }
.mtable .mt-rename { white-space: nowrap; }
.mtable .mt-entries { width: 55%; min-width: 320px; }
.mrow-entry {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 6px 14px;
  padding: 3px 0;
}
.mrow-entry + .mrow-entry { border-top: 1px solid var(--line); }
.mrow-entry .btn { justify-self: end; }
/* FULL-BLEED TABLES ON PHONES (user, Aug 12 2026: "on mobile i can barely see
   more than one column.. meanwhile, there is a ton of wasted space on either
   side"). A .tablewrap escapes EVERY ancestor's padding — shell, card, admin
   zone — with the 50vw breakout, so the scrollable area is the whole screen.
   The standings grid (.gridscroll) is excluded: its mobile layout is its own
   measured system (task #59) and must not move. */
@media (max-width: 700px) {
  /* The hpage arrow-pager DISSOLVES around plain data tables on phones: its
     flex row (30px arrow buttons each side) was itself part of the wasted
     side space, and a finger scrolls a table natively. display:contents
     removes the flex context so the breakout below measures against the
     page, not the pager. The standings grid keeps its pager. */
  .hpage:has(> .tablewrap.hpage-box:not(.gridscroll)) { display: contents; }
  .hpage:has(> .tablewrap.hpage-box:not(.gridscroll)) > .hpage-btn { display: none; }
  .tablewrap:not(.gridscroll) {
    width: 100vw;
    max-width: 100vw;
    margin-left: calc(50% - 50vw);
    margin-right: calc(50% - 50vw);
    border-left: 0;
    border-right: 0;
    border-radius: 0;
  }
  .tablewrap:not(.gridscroll) table { font-size: 13px; }
  .tablewrap:not(.gridscroll) th,
  .tablewrap:not(.gridscroll) td { padding: 6px 8px; }
}
/* Sortable tables: pointer headers with an arrow on the sorted column. */
table.sortable thead th { cursor: pointer; user-select: none; white-space: nowrap; }
table.sortable thead th[data-dir='asc']::after { content: ' \25B2'; font-size: 0.8em; }
table.sortable thead th[data-dir='desc']::after { content: ' \25BC'; font-size: 0.8em; }
/* The copy-paste email list on the pool admin page. */
.emaillist {
  width: 100%;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 12px;
  line-height: 1.5;
  padding: 8px 10px;
  border: 1px solid var(--line-strong);
  background: var(--surface-2);
  resize: vertical;
}
/* Unsubscribed members — red, kept apart from the main list so pasting them
   is a deliberate override, never an accident. */
.emaillist.-unsub { color: var(--red); border-color: var(--red); background: #fbeaea; }
/* The forensics filter bar: one wrapping row of labeled controls. */
.fx-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 8px 14px;
  margin: 10px 0 14px;
}
.fx-filter label { display: flex; flex-direction: column; gap: 2px; font-size: var(--fs-sm); font-weight: 600; }
/* The declare-winners picker: chosen entries as removable rows. */
.winlist { list-style: none; margin: 0 0 var(--sp-3); padding: 0; }
.winlist li {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 10px;
  border: 1px solid var(--line);
  background: var(--surface-2);
  margin-bottom: 6px;
  max-width: 480px;
}
.winlist-name { flex: 1 1 auto; font-weight: 700; }
.notice.-toast {
  position: fixed;
  top: calc(var(--topnav-h, 56px) + 10px);
  right: 10px;
  z-index: 300;
  max-width: min(420px, calc(100vw - 20px));
  box-shadow: var(--shadow);
  margin-bottom: 0;
  animation: notice-in 0.22s ease;
}
.notice.-toast + .notice.-toast { top: calc(var(--topnav-h, 56px) + 72px); }
.notice.-toast.-ok, .notice.-toast.-info {
  animation: notice-in 0.22s ease, notice-out 0.45s ease 4s forwards;
}
.notice.-toast.-error, .notice.-toast.-warn {
  cursor: pointer;
  animation: notice-in 0.22s ease, notice-out 0.45s ease 8s forwards;
}
@keyframes notice-in {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: none; }
}
@keyframes notice-out {
  to { opacity: 0; visibility: hidden; }
}
@media print { .notice.-toast { display: none; } }
.notice.-ok { background: #e7f4ea; border-color: var(--green-live); color: #0a5228; }
.notice.-error { background: #fbeaea; border-color: var(--red); color: #8f1d1d; }
.notice.-warn { background: #fdf3d7; border-color: var(--gold-bright-deep); color: #6f5106; }
.notice.-info { background: var(--surface-2); border-color: var(--line-strong); color: var(--text); }
.notice > :last-child { margin-bottom: 0; }
.notice ul { margin: 0; padding-left: 18px; }
.notice li + li { margin-top: 3px; }
.notice a { color: inherit; text-decoration: underline; }

/* ----- badges ----- */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 9px;
  border-radius: 999px;
  font-size: var(--fs-xs);
  font-weight: 800;
  letter-spacing: 0.06em;
  white-space: nowrap;
  vertical-align: middle;
}
.badge.-winner, .badge.-cowinner {
  background: linear-gradient(180deg, var(--gold-bright), var(--gold-bright-deep));
  color: #221a02;
  box-shadow: 0 1px 6px rgba(212, 160, 23, 0.45);
}
.badge.-out { background: #fbeaea; color: #a02020; border: 1px solid rgba(198, 40, 40, 0.55); }
.badge.-alive { background: #e7f4ea; color: var(--green-live); border: 1px solid rgba(11, 122, 55, 0.45); }
.badge.-pending { background: var(--surface-2); color: var(--text-muted); border: 1px solid var(--line); }
.badge.-live { background: var(--red); color: #fff; }
.badge.-live::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #fff;
  animation: livepulse 1.6s ease-in-out infinite;
}

/* subtle legacy buyback star after names */
.buyback-star {
  color: var(--gold);
  font-weight: 700;
  margin-left: 1px;
  cursor: help;
}

/* ----- countdown ----- */
.countdown {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  white-space: nowrap;
}
.countdown.cd-soon { color: var(--red); animation: cdpulse 1.2s ease-in-out infinite; }
.countdown.cd-past { color: var(--text-faint); animation: none; }
@keyframes cdpulse { 50% { opacity: 0.45; } }
@keyframes livepulse { 50% { opacity: 0.3; } }

/* ----- tables: standings grids -----
   White body cells so every helmet pops; the dark-green head is chrome
   (no imagery ever renders in the head row). */
.tablewrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  margin-bottom: var(--gap-block);
  scrollbar-width: thin;
  box-shadow: var(--shadow);
}
.tablewrap table { border-collapse: separate; border-spacing: 0; width: 100%; }
.tablewrap th, .tablewrap td {
  padding: 7px 10px;
  font-size: var(--fs-sm);
  text-align: center;
  white-space: nowrap;
  /* One notch darker than the old --line / 0.05 alpha (user, Aug 8 2026:
     "make the dividing lines slightly darker"). */
  border-bottom: 1px solid #c6d0c4;
  border-right: 1px solid rgba(22, 27, 24, 0.18);
}
/* Dark-green grid lines on the standings grids (user, Aug 18 2026: "make the
   dividing lines on the standings grid the same dark green that's in the
   header"). Scoped to .sgrid so the picks board keeps its lighter rules. */
.tablewrap .sgrid tbody td { border-bottom-color: var(--nav-bg); border-right-color: var(--nav-bg); }
.tablewrap thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--nav-bg);
  color: #fff;
  font-weight: 700;
  letter-spacing: 0.04em;
  border-bottom: 2px solid var(--accent-deep);
  border-right-color: rgba(255, 255, 255, 0.08);
}
/* ===== Row striping is STRICTLY NEUTRAL — no green, ever ==================
   GREEN MEANS "CORRECT PICK" ON THESE GRIDS. The stripe used to be #f7f9f6
   and the hover --surface-2 (#f1f4f0), both green-biased, because the whole
   neutral ramp in :root carries "a whisper of green" for brand warmth. That
   whisper is fine on page chrome and actively confusing on a results grid:
   every other row read as a faint success state.
   These two values are deliberately R=G=B. Do not swap them for a --surface-*
   token, and do not "harmonise" them with the palette — the neutrality IS the
   point. Same reasoning bans red-tinted stripes (red = eliminated). */
/* NO STRIPING AND NO HOVER BACKGROUND ON DATA GRIDS (user, July 29 2026).
   Two separate bugs lived in the two lines that used to be here:
     1. the zebra stripe competed with the outcome fill — "the background is
        either red, white, or green", nothing else;
     2. the row-hover background REPLACED the outcome fill, so moving the mouse
        across the picks list made the green disappear.
   A cell's background is its outcome. Full stop. Pointer feedback is not worth
   overwriting the data. Keyboard :focus-visible outlines stay — those are
   accessibility, not mouse decoration. */
.tablewrap td.name, .tablewrap th.name {
  position: sticky;
  left: 0;
  z-index: 1;
  text-align: left;
  background: var(--surface);
  border-right: 1px solid var(--line-strong);
  max-width: 46vw;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tablewrap thead th.name { z-index: 3; background: var(--nav-bg); }
/* Name column: no stripe, no hover fill — same reasoning as above. It keeps the
   plain --surface background from the sticky rule. */

/* ===== THE outcome rule (user's law — REWRITTEN July 29 2026) ==============
   OUTCOME IS A LIGHT BACKGROUND FILL. Correct pick / positive margin = light
   green. Incorrect pick / negative margin = light red. The cell is filled and
   the team image sits ON the fill.

   THIS REVERSES THE JULY 28 RULE, on the user's explicit instruction. The old
   rule was a 3px inset edge bar with NO fill, and it existed for one reason:
   helmets were opaque GIFs with a white matte, so a filled cell put a white
   box on a colored plate. Team imagery is now transparent PNG logos, so that
   reason is gone — and the underline treatment read, in the user's words, like
   "a fucking abortion": a full-width rule under a left-aligned score with no
   visual tie to the pick above it.

   THE COLOURS ARE THE LEGACY SITE'S OWN, not new inventions — lifted from
   standingsknock.php / makeselectionknock.php, which set them as <td bgcolor>
   directly behind the helmet:
       #b8eb9b  correct   (legacy 'bgcolor' for a winning pick)
       #de9087  incorrect (legacy 'bgcolor' for a losing pick)
   Keep them. They are what every participant has looked at for a decade.

   `.oc-won / .oc-lost / .oc-tie` is the ONE set of outcome classes — page
   modules must NOT re-declare them (standings.ts did, which is how eliminated
   rows kept a green zebra after styles.css was fixed). Neutral markers (BYE /
   buyback / missed / hidden) stay unfilled: their chip carries the shape.
   Row striping stays strictly neutral — see --zebra. */
/* --oc-fill IS THE FILL, NAMED ONCE. Every fill below is TRANSLUCENT on purpose
   (the legacy colours are alpha over white), which means a surface that is NOT
   white underneath changes them. The pick board is exactly that case: its grid
   paints the column-divider colour, so a translucent cell would show grey through
   and the decade-old green/red would read muted. Rather than restate a colour
   there — the .sb-midlogo habit — that block READS --oc-fill and composites it
   over an opaque --surface. One owner, one value, two readers. */
td.oc-won, .oc-won { --oc-fill: var(--oc-win-bg); background: var(--oc-fill); color: var(--oc-win-ink); }
td.oc-lost, td.oc-tie, .oc-lost, .oc-tie { --oc-fill: var(--oc-loss-bg); background: var(--oc-fill); color: var(--oc-loss-ink); }
/* IN PROGRESS: fainter green while ahead, fainter red while behind. These come
   AFTER the settled rules so a live cell reads as unsettled even though the
   engine has not scored it yet. Text stays normal ink — the fill is too pale to
   need its own. */
td.oc-live-won, .oc-live-won { --oc-fill: var(--oc-live-win-bg); background: var(--oc-fill); color: var(--text); }
td.oc-live-lost, .oc-live-lost { --oc-fill: var(--oc-live-loss-bg); background: var(--oc-fill); color: var(--text); }
/* ONLY THE CELL IS COLOURED. NOTHING INSIDE IT GETS ITS OWN FILL.
   These two rules used to read `.pos { background: var(--oc-win-bg) }` and the
   same for .neg, and that was a real bug with two heads:
     1. .pos/.neg sit on the result block INSIDE the cell, so every cell had two
        stacked fills — measured in a browser, a live cell was 0.34 green over
        0.15 green;
     2. .pos means "positive number", NOT "this pick won". It is used for signed
        numbers all over the site, so a .pos block painted itself GREEN even
        inside a red losing cell — a green background on an incorrect pick.
   The rule is now the user's, exactly: green cell = correct, red cell =
   incorrect, and the text inside just inherits the cell's ink. */
td.oc-won .kres, td.oc-won .kres *, td.oc-won .kpts { color: var(--oc-win-ink); }
td.oc-lost .kres, td.oc-lost .kres *, td.oc-lost .kpts,
td.oc-tie .kres, td.oc-tie .kres *, td.oc-tie .kpts { color: var(--oc-loss-ink); }
/* Live cells are pale enough to carry normal ink. */
td.oc-live-won .kres, td.oc-live-won .kres *,
td.oc-live-lost .kres, td.oc-live-lost .kres * { color: var(--text); }
/* No element inside a grid cell may paint a background — belt and braces so a
   future .pos/.neg/.kres tweak cannot reintroduce the stacked-fill bug. */
.sgrid td .kres, .sgrid td .kres *, .sgrid td .kpts { background: none; }
/* Chips (BYE / buyback) inside a filled cell take the cell's ink too — a
   knockout BYE cell is green, and --text-muted grey on green read as washed. */
td.oc-won .chip { color: var(--oc-win-ink); }
td.oc-lost .chip, td.oc-tie .chip { color: var(--oc-loss-ink); }
td.bye, td.buyback { color: var(--text-muted); }
td.missed { color: #7f1d1d; }
td.hid { color: var(--text-faint); }
td .helmet, td .teamlogo { vertical-align: middle; }

/* ----- team imagery -----
   max-width:none: the global img{max-width:100%} lets auto-layout table cells
   crush helmets well below their authored size (the "unreadably tiny" bug) —
   these images carry explicit width/height attributes, never fluid-size them. */
.helmet, .teamlogo { object-fit: contain; max-width: none; flex-shrink: 0; }

/* ----- skeleton / loading shimmer ----- */
.skel {
  position: relative;
  overflow: hidden;
  background: rgba(22, 27, 24, 0.07);
  border-radius: 6px;
}
.skel::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.65), transparent);
  animation: shimmer 1.4s infinite;
}
.skel-line { height: 0.95em; margin: 0.5em 0; }
.skel-block .skel-line:last-child { width: 62%; }
@keyframes shimmer { 100% { transform: translateX(100%); } }

/* ----- section heading -----
   The ONE heading between stacked cards/tables (Home "Open pools", admin
   "Actions"/"Participants", settings groups). Bare <h2>s used to set their own
   ad-hoc margins per page; this is the shared rhythm. */
.section-head {
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin: var(--sp-5) 2px var(--sp-3);
  font-size: var(--fs-lg);
  font-weight: 700;
  letter-spacing: 0.01em;
}
.section-head:first-child { margin-top: 0; }
.section-head small, .section-head .muted { font-weight: 400; font-size: var(--fs-sm); }
.section-head .chip { align-self: center; }

/* ----- empty state -----
   ONE look for "there is nothing here yet": quiet dashed card, a plain
   sentence, and (only when the viewer can actually DO something about it) one
   action. Never a badge, never a scary color. */
.empty {
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: var(--sp-5) var(--sp-4);
  margin-bottom: var(--gap-block);
  text-align: center;
  color: var(--text-muted);
}
.empty-title { display: block; font-size: var(--fs-lg); font-weight: 700; color: var(--text); margin-bottom: var(--sp-1); }
.empty-note { margin: 0 0 var(--sp-3); font-size: var(--fs-sm); }
.empty > :last-child { margin-bottom: 0; }
.empty .row { justify-content: center; }
/* An empty state INSIDE a card (admin sections, payment queues) drops the
   dashed frame — the card already draws one. */
.card > .empty { border: 0; padding: var(--sp-4) 0 var(--sp-2); margin-bottom: 0; }

/* ----- utility ----- */
.muted { color: var(--text-muted); }
.center { text-align: center; }
.stack-sm > * + * { margin-top: var(--sp-2); }
.row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.spread { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }

/* Spacing utilities on the --sp ladder. These exist so page modules stop
   shipping one-off inline `style="margin-top:12px"` (the sweep removed 29 of
   them). Pick a step; never invent a value. */
.m-0 { margin: 0; }
.mt-0 { margin-top: 0; }
.mb-0 { margin-bottom: 0; }
.mt-1 { margin-top: var(--sp-1); }
.mt-2 { margin-top: var(--sp-2); }
.mt-3 { margin-top: var(--sp-3); }
.mt-4 { margin-top: var(--sp-4); }
.mt-5 { margin-top: var(--sp-5); }
.mb-1 { margin-bottom: var(--sp-1); }
.mb-2 { margin-bottom: var(--sp-2); }
.mb-3 { margin-bottom: var(--sp-3); }
.mb-4 { margin-bottom: var(--sp-4); }
.mb-5 { margin-bottom: var(--sp-5); }
.inline-form { display: inline; }

/* ==========================================================================
   Page heads + stat strips (standings/picks pages)
   ========================================================================== */

.topnav-poolname {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
  color: rgba(244, 248, 244, 0.85);
}
/* CONSISTENT STICKY TITLE BAR (task #88, user Aug 2026: "the headers across all
   pages need a consistent sticky bar ... the light green header is sticky, the
   white scrollable page is the detail"). The page title sticks under the top nav
   (and the week bar where there is one), light-green to match that bar, so the
   white content scrolls beneath it. Picks overrides this with its own board-title
   band (body.picks-sticky, later in this file); scoreboard mirrors it on .sb-hero. */
.page-head {
  position: sticky;
  top: var(--topnav-h);
  z-index: 25;
  margin: 0 0 var(--gap-block);
  padding: 8px 12px;
  background: var(--head-band);
  /* FULL-BLEED BAND, EDGE TO EDGE (user, Aug 2026: the title bar must read as ONE
     bar with the week bar above it, not a floating box inside the content column).
     The element sits inside centered/padded containers, so its own background
     stops short of the viewport; a spread box-shadow paints the rest of the band —
     ink-only overflow, so it can NEVER create sideways scroll — and the second
     shadow, 1px lower, is the bottom hairline running the full width. clip-path
     confines both to the band's own horizontal strip. */
  border-bottom: 0;
  box-shadow: 0 0 0 100vmax var(--head-band), 0 1px 0 100vmax var(--line-strong);
  clip-path: inset(0 -100vmax -1px -100vmax);
}
body:has(.week-nav) .page-head { top: calc(var(--topnav-h) + var(--weekbar-h)); }
/* THE STICKY-JUMP KILLER (user, Aug 13 2026: "still didn't fix the 'sticky
   jump'"). A sticky band that RESTS below its pinned offset slides up and
   snaps the moment the page starts to scroll. So the band must rest EXACTLY
   where it pins, on every page that has one. Week-bar pages already zero the
   shell's top padding (further down); these cover the band pages WITHOUT a
   week bar — Home above all — and the scoreboard's mirror band. The breathing
   room the padding used to give lives BELOW the band (its margin-bottom). */
body:has(.page-head) .shell { padding-top: 0; }
body:has(.sb-hero) .shell { padding-top: 0; }
.page-head-pool {
  color: var(--text-muted);
  font-size: var(--fs-sm);
  margin: 0 0 2px;
  letter-spacing: 0.02em;
}
.page-head h1 { margin: 0; }
.page-head-count { margin: 6px 0 0; color: var(--text-muted); font-size: var(--fs-sm); }

.stat-strip { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--gap-block); }
.stat {
  flex: 1 1 100px;
  min-width: 96px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  box-shadow: var(--shadow);
}
.stat-value {
  font-size: var(--fs-xl);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
  color: var(--accent-deep);
}
.stat-sub { font-size: var(--fs-sm); color: var(--text-muted); font-weight: 600; }
.stat-label {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* ONE line, always: seasons scroll sideways behind arrow buttons rather than
   wrapping into a block of chips (user July 28). */
.season-nav {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: nowrap;
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-sm);
  min-width: 0;
}
.season-nav-track {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
  scroll-behavior: smooth;
  padding: 1px;
}
.season-nav-track::-webkit-scrollbar { display: none; }
.season-nav-arrow {
  flex: 0 0 auto;
  min-height: 32px;
  min-width: 30px;
  border-radius: 8px;
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--text-muted);
  cursor: pointer;
  font-size: 12px;
  line-height: 1;
}
.season-nav-arrow:hover { background: var(--surface-2); color: var(--text); }
.season-nav-arrow[hidden] { display: none; }
.season-nav-label {
  color: var(--text-faint);
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-right: 2px;
}
.season-chip {
  padding: 4px 12px;
  min-height: 32px;
  flex: 0 0 auto;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  text-decoration: none;
  color: var(--text-muted);
  background: var(--surface);
}
.season-chip:hover { background: var(--surface-2); color: var(--text); }
.season-chip.-active {
  background: var(--accent);
  color: #fff;
  border-color: var(--accent-deep);
  font-weight: 700;
}

/* ---- WEEK CHIPS (the shell's POOL/YEAR/WEEK/PAGE row, src/ui.ts weekChips) --
   These rules live HERE and not in the shell's inline <style> because that
   <style> is emitted by masterMenu(), which pageShell renders ONLY for a
   logged-in user — while the chip row itself is rendered unconditionally. A
   logged-out viewer of /scores or /schedule (the only indexable pages on the
   site) got the chips as a row of bare underlined links. */
/* THE WEEK BAR IS A SECOND HEADER (user, July 29 2026: "THE WEEKS 'BAR' IS A
   SECOND HEADER, IT NEVER LEAVES THE SCREEN"). It sticks directly under .topnav
   and never scrolls away, so the week you are in — and the way to change it — is
   always on screen.
     top      = var(--topnav-h), the height of the bar above it.
     z-index  = 30, deliberately BELOW .topnav (50): the pool switcher panel and
                the Year select live inside the top nav and must overlap this bar
                when they open. It is also below the phone scores overlay (40).
     background = required. It is a sticky bar over live content; without its own
                opaque surface the page scrolls THROUGH it.
   FULL-WIDTH BOX, centred TRACK: the bar's surface must span the whole viewport
   or content shows past it on wide screens, so the 1400px content measure moved
   onto the track inside it (aligned with .shell's own padding). */
.week-nav {
  position: sticky;
  top: var(--topnav-h);
  z-index: 30;
  display: flex;
  align-items: center;
  width: 100%;
  height: var(--weekbar-h);
  margin: 0;
  padding: 0;
  background: var(--head-band);
  border-bottom: 1px solid var(--line-strong);
  box-shadow: 0 1px 4px rgba(15, 23, 18, 0.06);
  /* NEVER let this bar become a grid COLUMN. It used to render inside .shell (a
     2-column grid when a sidebar is present), where it consumed the 1fr track
     and pushed <main> into the 360px sidebar column. It sits outside .shell
     now; this is the guard if it is ever moved back in. */
  grid-column: 1 / -1;
}
/* Only the deltas from .season-nav-track, which is already a scrolling flex row. */
.week-nav .season-nav-track {
  /* 4px, not 5: chip 44 + gap 4 are both divisible by 4, so at 125%, 150% and
     175% Windows display scaling every chip edge lands on a whole device pixel.
     A 5px gap is 6.25 device px at 125% and the browser snaps it, which drifts
     the pitch and puts a sliver of the next chip back on screen (user, Aug 25
     2026, after the geometry measured exact at 100%). */
  gap: 4px;
  scrollbar-width: none;
  -ms-overflow-style: none;
  max-width: 1400px;
  margin: 0 auto;
  /* 2px vertical so a chip's :focus-visible ring is not clipped by overflow.
     NO horizontal padding: the rounding above measures this element, so any
     side padding would shift the chips out of alignment with its edges. The
     bar's own padding provides the gutter instead. */
  padding: 2px 0;
}
.week-nav { padding-left: 14px; padding-right: 14px; }
.week-chip {
  /* UNIFORM WIDTH is what makes "no half chips" possible: with W1 at 38px and
     W12 at 45px there is no pitch to align the strip's edge to. Every chip is
     the same size, so the viewport below can be rounded to a whole number of
     them — flex shares any shortfall equally, so they stay uniform while they
     shrink.
     THE WHOLE SEASON BEFORE ANY ARROWS (user, Aug 26 2026: "why are there even
     arrows when there is plenty of width"). Eighteen 44px chips need 860px; a
     bar even slightly under that used to drop a chip and switch the arrows on.
     They now shrink to 38px first, which fits the season in 752px, and only a
     bar narrower than that scrolls. */
  flex: 1 1 auto;
  width: var(--chip-w);
  min-width: 38px;
  max-width: var(--chip-w);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 30px;
  padding: 0;
  border-radius: 8px;
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
  font-weight: 700;
  font-size: 13px;
}
.week-chip:hover { background: var(--surface-2); }
.week-chip.-active { background: var(--accent); border-color: var(--accent-deep); color: #fff; }
/* Track padding follows .shell's own so the chips line up with the page content
   at both breakpoints (14px phone, 24px desktop). */
@media (min-width: 1100px) {
  .week-nav { padding-left: 24px; padding-right: 24px; }
}
/* Anything the page may jump to (a #week2-slot1 card) must clear BOTH headers. */
.pickslot, .card[id] { scroll-margin-top: calc(var(--topnav-h) + var(--weekbar-h) + 12px); }
@media print {
  .week-nav { display: none !important; }
}

/* winner hero: gold, celebratory but classy */
.winner-hero {
  display: flex;
  align-items: center;
  gap: 14px;
  background: linear-gradient(135deg, var(--gold-bright), var(--gold-bright-deep));
  color: #221a02;
  border-radius: var(--radius-lg);
  padding: var(--sp-4) 18px;
  margin-bottom: var(--sp-3);
  box-shadow: 0 6px 22px rgba(212, 160, 23, 0.35);
}
.winner-hero-trophy { flex-shrink: 0; }
.winner-hero-label {
  display: block;
  font-size: var(--fs-xs);
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.75;
}
.winner-hero-week { text-transform: none; letter-spacing: 0.02em; }
.winner-hero-names { font-size: var(--fs-xl); font-weight: 800; line-height: 1.25; }
.winner-hero .buyback-star { color: #221a02; }
/* ONE BOX PER WINNER (Aug 17 2026): boxes sit side by side on desktop and wrap
   to a stack on phones. Each box owns its own margin only when standalone. */
.winner-heroes { display: flex; flex-wrap: wrap; gap: var(--sp-3); margin-bottom: var(--sp-3); }
.winner-heroes .winner-hero { margin-bottom: 0; flex: 1 1 260px; }
@media (max-width: 700px) {
  .winner-heroes { gap: 6px; }
  .winner-heroes .winner-hero { flex: 0 1 calc(50% - 3px); min-width: 0; }
  .winner-hero { gap: 7px; padding: 4px 9px; border-radius: var(--radius); box-shadow: 0 2px 8px rgba(212, 160, 23, 0.3); }
  .winner-hero-trophy { width: 15px; height: 15px; }
  .winner-hero-label { font-size: 0.56rem; letter-spacing: 0.06em; }
  .winner-hero-names { font-size: var(--fs-sm); line-height: 1.15; }
}

/* koob standings sort toggle (Most Points / Least Points), user Aug 17 2026 */
/* Compact chips (user, Aug 18 2026: "way too big taking way too much screen
   real estate") — no tap-target min-height, tight padding, small type. */
.kbsort { display: flex; align-items: center; gap: 6px; margin: 0 0 3px; flex-wrap: wrap; }
.kbsort-lbl { font-size: var(--fs-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); }
.kbsort-btn {
  font: inherit; font-size: var(--fs-xs); font-weight: 700; line-height: 1;
  padding: 4px 10px; border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface); color: var(--text-muted); cursor: pointer;
}
.kbsort-btn.-on { background: var(--accent); border-color: var(--accent); color: #fff; }

/* ==========================================================================
   Standings grids (knockout helmets + differential ranked list)
   ========================================================================== */

.cellpick {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: 44px;
  vertical-align: middle;
}
.cellpick.-row { flex-direction: row; gap: 6px; min-width: 0; }
.cellpick-abv { font-size: 0.68rem; font-weight: 700; letter-spacing: 0.05em; opacity: 0.9; }

/* ----- chip: THE tag pill -----
   One component for every inline tag: TEST · Preseason, PRESEASON, the season
   number, "Complete", "Archived", "This week". Before the sweep this pill was
   redeclared inside pools.ts's HOME_CSS and JOIN_CSS only, so the SAME tag
   rendered as a bordered pill on Home/Join and as unstyled bold text on
   Standings / Picks / Scores / Rules. It lives here now, once. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  min-height: 24px;
  padding: 2px 10px;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--text-muted);
  font-size: var(--fs-xs);
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  white-space: nowrap;
  vertical-align: middle;
}
.chip[title] { cursor: help; }
a.chip { text-decoration: none; cursor: pointer; }
a.chip:hover { background: var(--surface-2); color: var(--text); border-color: var(--text-faint); }
.chip.-done { background: #fdf6e2; border-color: var(--gold-bright-deep); color: var(--gold); }
.chip.-now { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-deep); }

/* Grid CELL markers reuse the name but are a different shape: square, quiet,
   sized by the standings density vars. Reset the pill treatment for them. */
.chip.-hidden, .chip.-bye, .chip.-bb, .chip.-miss, .chip.-none {
  justify-content: center;
  min-width: 36px;
  min-height: 28px;
  padding: 0 8px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: none;
}
.chip.-hidden {
  background: #eef1ed;
  color: var(--text-faint);
  border: 1px dashed var(--line-strong);
  cursor: help;
}
.chip.-bye, .chip.-bb { color: var(--text-muted); }
/* NO QUESTION-MARK CURSOR ON THE STANDINGS BOARD (user, Aug 10 2026: "the
   mouseover turns to a pointer with a question mark... it should never do
   this"). Tooltips still appear on hover; the cursor just stays normal.
   Links inside the grid keep their pointer — this touches only the
   help-cursor carriers. */
.sgrid .chip[title], .sgrid .chip.-hidden, .sgrid .buyback-star { cursor: default; }
/* THE STANDINGS CELL MENU (user, Aug 10 2026): a small fixed popover at the
   tap. Above the frozen header bar (z34) and the rail (z40). */
.cellmenu {
  position: fixed;
  z-index: 60;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  box-shadow: 0 12px 30px rgba(15, 23, 18, 0.28);
  padding: 8px;
  min-width: 170px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.cellmenu-x {
  position: absolute;
  top: 2px;
  right: 2px;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--text-muted);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.cellmenu-x:hover { background: var(--surface-2); color: var(--text); }
.cellmenu-item {
  display: block;
  min-height: var(--tap);
  padding: 9px 36px 9px 12px;
  border: 0;
  border-radius: 7px;
  background: none;
  text-align: left;
  text-decoration: none;
  color: var(--text);
  font-size: 14.5px;
  font-weight: 650;
  cursor: pointer;
  white-space: nowrap;
}
.cellmenu-item:hover { background: #eef2ee; color: var(--text); }
.cellmenu-item.-danger { color: var(--red); }
/* The palette's used-week marker: this entry already has the team in W-N. */
.adm-teambtn .adm-used { color: var(--red); font-size: 0.6rem; font-weight: 800; margin-left: 4px; white-space: nowrap; }
/* The cell the open menu applies to (user, Aug 10 2026): an outline, not a
   border, so nothing shifts — cleared the moment the menu closes. */
.cellmenu-on { outline: 3px solid var(--accent-deep); outline-offset: -3px; }
/* Menu-clickable squares advertise it (user, Aug 10 2026: "the mouseover
   cursor should show the cells are clickable"): picked slots for everyone;
   every addressed cell for edit-capable admins — data-slot is only stamped
   when the server granted pick-editing. */
.sgrid .kslot:has(a.tlink), .sgrid td.pick, .sgrid [data-slot] { cursor: pointer; }
/* NO PICK marker: the text "NO PICK" in red, wrapping to two lines in the narrow
   grid cell (user, Aug 6 2026 — it had regressed to a bare ✕). */
/* NO PICK fills the WHOLE cell red (user, Aug 6 2026: "fill the whole box"). The
   cell carries the red fill; the chip is just the centered label spanning it, no
   pill. */
.kgrid td.missed, .koobgrid .kslot.-miss { background: var(--oc-loss-bg); padding: 0; }
.chip.-miss {
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 100%; margin: 0; border: 0; border-radius: 0;
  background: none; color: var(--oc-loss-ink);
  font-weight: 800; font-size: 0.56rem; line-height: 1.05; letter-spacing: 0.02em;
  white-space: normal; padding: 2px;
}
.chip.-none { color: var(--text-faint); border: 1px solid var(--line); font-weight: 600; }
/* Standings squares: the anchors are HIT-TESTABLE but never navigate on a plain
   click — app.js's cell menu intercepts every square tap (preventDefault) and
   offers Team page / Game page instead. pointer-events:none here (the Aug 6
   "links removed" state) silently killed that menu for every non-editing
   viewer: real clicks fell through the anchor to the bare td, which the menu
   handler had no reason to recognise — while admins' cells carried data-slot
   and kept working, which is why only they ever saw the menu (Aug 11 2026). */
.sgrid a.tlink, .sgrid a.glink {
  text-decoration: none;
  color: inherit;
}

/* Row states — appended after the base zebra/hover rules so they win.
   Same law as cells: NO wash across the week columns (those hold helmets).
   The state lives on the NAME cell, as an inset edge bar + a tint on that one
   image-free cell. Sticky name cells need SOLID backgrounds (weeks scroll
   beneath them). */
.tablewrap tbody tr.-out td.name {
  background: #faf0f0;
  box-shadow: inset 3px 0 0 0 rgba(198, 40, 40, 0.55);
}
.tablewrap tbody tr.-winner td.name {
  background: #faf3dd;
  box-shadow: inset 3px 0 0 0 var(--gold-bright-deep);
}
.entry-name { display: block; font-weight: 600; }
.tablewrap td.name .badge { margin-top: 2px; }

/* .pos / .neg = "this NUMBER is positive/negative". Nothing more. They colour a
   signed number where there is no outcome cell around it (a koob total, a
   payment delta). Inside a filled outcome cell the ink rules in "THE outcome
   rule" win — declared later there with higher specificity — because bright
   --green-live / --red text on a green or red fill is what produced red text on
   a green background. These must never carry a background. */
.pos { color: var(--green-live); font-weight: 700; }
.neg { color: var(--red); font-weight: 700; }

/* (The .dif-* differential "ranked list" component was removed in the July 28
   2026 consistency sweep — standings.ts renders the koob board as a real grid
   (.koobgrid) and nothing had referenced .dif-list/.dif-row/.dif-rank/.dif-who/
   .dif-pts/.dif-chev/.dif-detail/.dif-sep for months. ~70 lines of dead CSS.) */

/* ==========================================================================
   Pick pages (helmet-button pick forms)
   ========================================================================== */

.entry-tabs {
  display: flex;
  gap: var(--sp-2);
  overflow-x: auto;
  margin: 0 0 var(--sp-3);
  padding-bottom: 2px;
}
.entry-tab {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  padding: 4px 16px;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  text-decoration: none;
  color: var(--text-muted);
  background: var(--surface);
  font-size: var(--fs-sm);
  font-weight: 600;
}
.entry-tab:hover { background: var(--surface-2); color: var(--text); }
.entry-tab.-active { background: var(--accent); border-color: var(--accent-deep); color: #fff; }

.pickslot { padding: var(--sp-3); }
.pickslot .card-title { justify-content: space-between; }
.pickslot-now {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-sm);
  color: var(--text-muted);
  font-weight: 600;
}
.gamecards { display: flex; flex-direction: column; gap: 10px; margin-bottom: 10px; }
.gamecard {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: #f8faf7;
  padding: 8px;
}
.gamecard.-locked { background: #eff2ee; }
.gamecard-teams {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: stretch;
  gap: 6px;
}
.gamecard-at {
  align-self: center;
  color: var(--text-faint);
  font-weight: 700;
  font-size: var(--fs-xs);
}
.gamecard-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  padding: 6px 4px 0;
  font-size: var(--fs-xs);
  color: var(--text-muted);
  flex-wrap: wrap;
}
.gamechip { font-weight: 700; letter-spacing: 0.04em; }
.gamechip.-final, .gamechip.-locked { color: var(--text-faint); }
.gamechip.-open .countdown { color: var(--text); }
.gamechip.-open .countdown.cd-soon { color: var(--red); }

.teampick { position: relative; display: block; min-width: 0; }
.teampick input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}
.teampick-body {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 64px;
  height: 100%;
  padding: 8px 10px;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  transition: border-color 0.12s, background 0.12s, box-shadow 0.12s;
}
/* NO HOVER BACKGROUND ON A PICK CELL, EVER (user, July 30 2026: "oh look, a
   stupid mouseover turning the background white"). A pick cell's background is
   its OUTCOME - green won, red lost, the paler pair live, white unplayed. A
   hover background REPLACES that, so pointing at a finished game erased whether
   it was won or lost. This is the third time a hover rule has overwritten
   outcome colour here (the standings zebra hover, the koob sticky columns, now
   this). Hover changes the BORDER only; the fill is data. */
.teampick-body:hover { border-color: var(--text-faint); }
.teampick input:checked + .teampick-body {
  border-color: var(--accent);
  background: var(--accent-soft);
  box-shadow: 0 0 0 1px var(--accent) inset;
}
.teampick input:focus-visible + .teampick-body { outline: 2px solid var(--accent); outline-offset: 2px; }
/* THE SELECTED AND LOCKED LOOKS CANNOT RIDE ON THE INPUT (task #47). A card whose
   game has kicked off carries NO input at all now — that absence IS the lock — so
   `input:checked + …` and `input:disabled + …` have nothing to hang on for exactly
   the card that matters most: the pick that can never change again. Both facts are
   already on the CARD as server-rendered classes, and those are what paint here.
   The two input rules stay because they are the INSTANT feedback between a click
   and the save landing, before app.js moves the -current class. */
.teampick.-current > .teampick-body {
  border-color: var(--accent);
  background: var(--accent-soft);
  box-shadow: 0 0 0 1px var(--accent) inset;
}
.teampick.-locked > .teampick-body,
.teampick.-usedlock > .teampick-body { cursor: default; }
/* Dim the locked card, but never the one that is the viewer's own pick — a
   settled pick has to stay legible, which is the whole point of showing it. */
.teampick.-locked:not(.-current) > .teampick-body { opacity: 0.55; }
/* A WHOLE COLUMN THAT RULE (b) HAS CLOSED (task #47): this entry's own pick has
   kicked off, so its week is finished even though the other games have not
   started — and those cards would otherwise look identical to the live ones in
   the next column and invite a tap that does nothing. Same treatment as a locked
   card, and the same exception for the pick itself. */
/* MATRIX EQUIVALENT (Aug 4 2026 — the two rules this replaces targeted
   .teampick, a class the matrix board no longer renders, so a closed column
   had NO dim at all and its inert squares looked tappable). The entry's own
   pick (.-on, drawn as the check mark) stays vivid; every other square in the
   closed column fades. */
.pickcol[data-collock] .pickcell.-pkbox { cursor: default; }
/* Dim the BOX VISUAL, never the cell: each cell carries a segment of the 3px
   entry divider on its left edge, so cell-level opacity faded those segments
   to a lighter grey wherever a locked column met an unchecked row (user,
   Aug 10 2026: "do you see the grey change shades on the vertical lines...
   it shouldn't"). The divider stays one solid colour top to bottom. */
.pickcol[data-collock] .pickcell.-pkbox:not(.-on) .pkbox-box { opacity: 0.55; }
.teampick-text { min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
.teampick-abv {
  font-weight: 800;
  font-size: var(--fs-base);
  letter-spacing: 0.01em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.teampick-name {
  font-size: var(--fs-xs);
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.teampick-res { margin-left: auto; font-weight: 800; font-size: var(--fs-sm); color: var(--text-faint); }
.teampick-res.-w { color: var(--green-live); }
.teampick-res.-l { color: var(--red); }
/* INSIDE the chosen card, pinned to its right edge — never floated over a card
   corner, where it visually landed on the OTHER team (user July 28). */
.teampick-you {
  margin-left: auto;
  flex: 0 0 auto;
  align-self: center;
  /* NOT nowrap. .teampick only ever renders inside a pick column, and one column
     of a 10-entry board is 104px wide — "Your pick" on one line pushed the pill
     past the cell edge and into the next entry's column (measured, task #41). */
  white-space: normal;
  background: var(--gold-bright);
  color: #221a02;
  font-size: 0.6rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 1px 7px;
  border-radius: 999px;
  box-shadow: 0 1px 3px rgba(15, 23, 18, 0.2);
}
.teampick.-bye { margin-bottom: 10px; }
.teampick.-bye .teampick-body { border-style: dashed; }
.bye-mark {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--surface-3);
  color: var(--text);
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  flex-shrink: 0;
}
.pickslot-save { margin: 4px 0 0; }

/* ==========================================================================
   Light-theme compatibility shims for module-scoped legacy styles.

   These exist ONLY where the offending rule lives in a file this sweep does
   not own. Every shim for admin.ts / payments.ts was deleted in the July 28
   2026 sweep and fixed at source instead (.admin th, .admin input/select,
   .refcode) — a shim that out-specifies a module is a leftover, not a fix.
   ========================================================================== */

/* NO ROW TINT ON THE VIEWER'S OWN ROWS (user, Aug 8 2026: "no color backgrounds
   other than green or red behind the teams" — the green wash also striped
   randomly because the even-row background reset wiped it on alternate rows).
   The gold inset notch on the name cell remains the "your row" marker. */
/* standings.ts koob demoted-section separator: the module's rgba(0,0,0,.22)
   wash renders a mid-gray on light surfaces (muted text falls under 4.5:1) —
   use a solid light neutral instead. REMOVE once standings.ts is fixed. */
.shell-body .tablewrap .koobgrid tbody tr.grid-sep td {
  background: var(--surface-3);
}
.shell-body .tablewrap .koobgrid tbody tr.grid-sep td.kn {
  background-color: var(--surface-3);
  background-image: none;
}

/* ==========================================================================
   Motion + print
   ========================================================================== */

/* NO global reduced-motion animation nuke (user, Aug 15 2026: "they are not
   fucking flashing"). Windows "animation effects off" maps to
   prefers-reduced-motion in Chrome, and the old *-rule here froze the pulsing
   win/lose edge bars and live dots — which are GAME-STATE INFORMATION, not
   decoration. Only genuinely decorative loops stay suppressed. */
@media (prefers-reduced-motion: reduce) {
  .countdown.cd-soon, .skel::after { animation: none !important; }
}

@media print {
  body { background: #fff; color: #000; }
  .topnav, .tabbar, .shell-sidebar, [data-install-btn], .noprint { display: none !important; }
  .shell { padding: 0; display: block; }
  .card, .tablewrap { box-shadow: none; border-color: #999; background: #fff; }
  .tablewrap { overflow: visible; }
  .tablewrap th, .tablewrap td { color: #000; background: #fff !important; border-color: #bbb; }
  .tablewrap td.name { position: static; max-width: none; }
  /* PRINT is the one place fills are right: box-shadow edge bars do not print,
     so outcome falls back to a light wash. Screen keeps the edge-bar law. */
  td.oc-won { background: #d7f5d7 !important; color: #000; }
  td.bye, td.buyback { background: #f0f0f0 !important; color: #000; }
  td.oc-lost, td.oc-tie, td.missed { background: #f5d7d7 !important; color: #000; }
  .empty { border-style: solid; }
  a { text-decoration: none; color: #000; }
}

/* ==========================================================================
   Task #26 interaction layer: shared confirm dialog, pick auto-save
   affordances, admin board mode. (app.js owns the behavior; standings.ts /
   picks.ts render the admin-only hooks.)
   ========================================================================== */

/* ----- page-head quick links (Rules / Full schedule) ----- */
.page-head-links {
  margin: 4px 0 0;
  font-size: var(--fs-sm);
}
.page-head-links a { font-weight: 600; }

/* ----- shared confirmation dialog (frzDialog) ----- */
.frz-dialog {
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-lg);
  padding: 18px;
  width: min(440px, calc(100vw - 32px));
}
.frz-dialog::backdrop { background: rgba(10, 25, 15, 0.55); }
.frz-dialog h3 { margin: 0 0 10px; font-size: var(--fs-lg); line-height: 1.3; }
.frz-dialog-list {
  margin: 0 0 12px;
  padding: 10px 12px 10px 30px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  max-height: 40vh;
  overflow-y: auto;
}
.frz-dialog-list li { margin: 2px 0; font-size: var(--fs-sm); }
.frz-dialog-note { color: var(--text-muted); font-size: var(--fs-sm); margin: 0 0 14px; }
.frz-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  flex-wrap: wrap;
  margin: 4px 0 0;
}

/* ----- pick auto-save (form[data-pickform], app.js) ----- */
/* JS on = auto-save: the save button retires.
   ON THE BODY, NOT ON THE FORM (task #48). This was form.js-auto, a class app.js
   wrote onto each column at load — and the picks page now REPLACES a column with
   the server's own re-render after every write, which threw the class away and
   blinked the Save buttons back into a page that saves on selection. The body
   outlives every swap. */
body.js-picksave .pickslot-save { display: none; }
/* THE PICK ERROR FLOATS, exactly like every other picks-page notice (user,
   Aug 10 2026: "how many times do i have to tell you to get rid of these
   fucking inline messages"). It used to flow INSIDE the 44px pick column and
   rendered as a one-word-per-line red smear. Same fixed overlay spot as
   body.picks-sticky .notice: zero layout shift, readable width, on top. */
.pickslot-msg {
  position: fixed;
  top: calc(var(--topnav-h) + var(--weekbar-h, 0px) + 10px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 46;
  max-width: min(92vw, 560px);
  box-shadow: var(--shadow-lg, 0 8px 24px rgba(15, 23, 18, 0.18));
  padding: 10px 14px;
  border-radius: var(--radius);
  border: 1px solid;
  margin: 0;
  font-size: var(--fs-sm);
  font-weight: 600;
}
body:not(:has(.week-nav)) .pickslot-msg { top: calc(var(--topnav-h) + 10px); }
.pickslot-msg.-ok { background: #e7f4ea; border-color: var(--green-live); color: #0a5228; }
.pickslot-msg.-error { background: #fbeaea; border-color: var(--red); color: #8f1d1d; }
/* The "saved" flash on the chosen card is GONE with the hand-patching that lit
   it (app.js flashSaved, task #48). A save now re-renders the column from the
   server, so a class the client had just written to the old node could not have
   survived to animate anything — and the card arrives already wearing .-current
   and the gold "Your pick" tag, which is the same news said permanently. */
/* clear affordances: quiet text buttons, never competing with the picks */
.pickclear-row { margin: 8px 0 0; }
.pickclear {
  background: none;
  border: 0;
  padding: 6px 2px;
  min-height: var(--tap);
  color: var(--red);
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.pickclear:hover { color: #8f1d1d; }

/* ----- admin board mode (standings grid; hooks render for admins only) ----- */
.admin-bar {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  margin: 0 0 12px;
}
.admin-bar-classic { font-size: var(--fs-xs); color: var(--text-faint); }
/* edit mode ON: pick cells read as tappable */
body.admin-edit .sgrid td[data-slot] { cursor: pointer; }
body.admin-edit .sgrid td[data-slot]:hover { outline: 2px dashed var(--accent); outline-offset: -2px; }
body.admin-edit .sgrid .kslot[data-slot] { cursor: pointer; border-radius: 6px; }
body.admin-edit .sgrid .kslot[data-slot]:hover { outline: 2px dashed var(--accent); outline-offset: 0; }
/* row-select checkboxes (winner stamping) */
.adm-ck {
  display: inline-flex;
  align-items: center;
  margin-right: 7px;
  flex-shrink: 0;
  cursor: pointer;
}
.adm-ck input { width: 20px; height: 20px; }
body:not(.admin-edit) .adm-ck { display: none; }
/* sticky action bar (appears above the phone tab bar / bottom on desktop) */
.admbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: var(--tabbar-h);
  z-index: 45;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
  padding: 10px max(14px, env(safe-area-inset-left)) 10px;
  background: var(--surface);
  border-top: 2px solid var(--accent);
  box-shadow: 0 -4px 18px rgba(15, 23, 18, 0.18);
}
@media (min-width: 1100px) { .admbar { bottom: 0; } }
.admbar-count { font-weight: 700; font-size: var(--fs-sm); color: var(--text-muted); }
/* helmet palette dialog (admin pick editing) */
.adm-palette { width: min(560px, calc(100vw - 24px)); }
.adm-palette-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(86px, 1fr));
  gap: 6px;
  margin: 0 0 14px;
  max-height: 52vh;
  overflow-y: auto;
  padding: 2px;
}
.adm-teambtn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  min-height: 62px;
  padding: 7px 4px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  font-weight: 800;
  font-size: var(--fs-xs);
  letter-spacing: 0.04em;
  color: var(--text);
}
.adm-teambtn:hover { background: var(--surface-2); border-color: var(--accent); }
.adm-teambtn img { width: 32px; height: 32px; object-fit: contain; max-width: none; }
.adm-teambtn.-current { border-color: var(--accent); background: var(--accent-soft); box-shadow: 0 0 0 1px var(--accent) inset; }
.adm-teambtn.-special { justify-content: center; background: var(--surface-2); }

/* admin classic no-JS fallback blocks read as secondary */
.admin details.classic-fallback { margin: 10px 0 0; }
.admin details.classic-fallback summary {
  cursor: pointer;
  color: var(--text-muted);
  font-size: var(--fs-sm);
  min-height: var(--tap);
  display: flex;
  align-items: center;
}

@media print {
  .admbar, .admin-bar, .pickclear-row { display: none !important; }
}

/* Betting line on pick rows — pre-kickoff only, muted (user July 28). */
.gamecard-odds {
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--red);
  white-space: nowrap;
}

/* Forensics timeline: five columns that fit any screen — secondary facts ride
   under their primary value instead of adding columns (user July 28). */
.fx-sub { display: block; font-size: 0.72em; color: var(--text-faint); font-weight: 500; }
.fx-when { white-space: nowrap; }
.fx-change { font-variant-numeric: tabular-nums; white-space: nowrap; }
@media (max-width: 720px) {
  .fx-sub { font-size: 0.68em; }
  .fx-change { white-space: normal; }
}

/* Standings: use every available pixel before resorting to the scrollbar
   (user July 28). The 1400px cap stays for reading-width pages. */
body.wide-grid .shell { max-width: none; }
@media (min-width: 1600px) {
  body.wide-grid .shell { padding-left: 20px; padding-right: 20px; }
}

/* ==========================================================================
   RESULTS ON THE PICK PAGE — THE UNIVERSAL RULE (user, July 29 2026)
   "ANYWHERE a pick or a game is shown, the game's RESULT is shown with it."
   Owned by src/picks.ts. NOTHING here re-declares an outcome colour or edge:
   the canonical .oc-won / .oc-lost / .oc-tie rule (see "THE outcome rule"
   above) supplies every underline, and .pos / .neg every signed number. These
   rules only lay the new pieces out.
   ========================================================================== */

/* ----- the score on a team's own pick card ----- */
.teampick-out {
  margin-left: auto;
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  flex: 0 0 auto;
}
/* The "Your pick" badge also carries margin-left:auto; once .teampick-out has
   taken the free space they must sit together at the right edge, not split it. */
.teampick-out + .teampick-you { margin-left: 6px; }
.teampick-out .teampick-res { margin-left: 0; }
/* THE CARD MUST FIT THE PHONE. Three cards across 375px leave each team half
   ~127px of content: helmet + name + score + "Your pick" cannot share one line
   there, and the badge was already running past the card's right edge before
   the score existed (measured: 15px over without a score, 48px with one). The
   row wraps instead — the card grows a few pixels taller and NOTHING leaves the
   screen. Wrapping is inert wherever the line already fits. */
.teampick-body { flex-wrap: wrap; row-gap: 4px; }
.teampick-score {
  font-size: 1.32rem;
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}
.teampick-score.-w { color: var(--green-live); }
.teampick-score.-l { color: var(--red); }

/* Outcome on the pick card is the same LIGHT FILL as everywhere else (see "THE
   outcome rule"). The logo is a transparent PNG and sits on the fill.
   The checked state keeps its 1px accent ring — that ring is a SEPARATE signal
   ("this is your pick") from the fill ("it won/lost"), so both must show at
   once. It is now a border-style inset ring only, which no longer collides with
   the fill the way two box-shadows on one property did. */
.teampick.oc-won .teampick-body { background: var(--oc-win-bg); color: var(--oc-win-ink); }
.teampick.oc-lost .teampick-body,
.teampick.oc-tie .teampick-body { background: var(--oc-loss-bg); color: var(--oc-loss-ink); }
.teampick.oc-won input:checked + .teampick-body,
.teampick.oc-lost input:checked + .teampick-body,
.teampick.oc-tie input:checked + .teampick-body,
/* ...and by class, for the settled pick that has no input left to be :checked
   (task #47). A played game is precisely where this ring has to survive. */
.teampick.oc-won.-current > .teampick-body,
.teampick.oc-lost.-current > .teampick-body,
.teampick.oc-tie.-current > .teampick-body {
  box-shadow: 0 0 0 2px var(--accent) inset;
}
/* The score inside a filled card inherits the outcome ink — the standalone
   green/red score colours would be near-invisible on their own fill. */
.teampick.oc-won .teampick-score.-w, .teampick.oc-won .teampick-score { color: var(--oc-win-ink); }
.teampick.oc-lost .teampick-score.-l, .teampick.oc-lost .teampick-score,
.teampick.oc-tie .teampick-score { color: var(--oc-loss-ink); }
/* A finished game is dimmed as "locked", but a settled pick has to stay
   legible — that is the whole point of the rule. */
.teampick.oc-won input:disabled + .teampick-body,
.teampick.oc-lost input:disabled + .teampick-body,
.teampick.oc-tie input:disabled + .teampick-body,
.teampick.oc-won.-locked > .teampick-body,
.teampick.oc-lost.-locked > .teampick-body,
.teampick.oc-tie.-locked > .teampick-body { opacity: 1; }

/* ----- live clock next to the LIVE badge on the game card ----- */
.gamechip.-live { display: inline-flex; align-items: center; gap: 6px; }
.gamecard-clock {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  color: var(--text);
  letter-spacing: 0.02em;
}
.gamecard-state { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; }
/* PHONES: the live clock must never be sliced in half. The spine cell is ~74px
   wide there, and "Q3 · 07:21" beside a LIVE badge on one inline-flex line
   overflowed it — the clock was cut down the middle on every phone (round-2
   adversarial audit, Aug 3 2026, measured at 360 and 375). Letting the badge
   row wrap and the clock keep its own line costs one line of height on live
   games only, and nothing is ever unreadable. */
@media (max-width: 700px) {
  .gamechip.-live { flex-wrap: wrap; row-gap: 2px; max-width: 100%; }
  /* NOT nowrap. The first attempt at this pinned the clock to one unbreakable
     line, which in a ~74px spine cell simply overflowed and was CLIPPED
     mid-digit ("Q3 · 07:2") — worse than the wrap it replaced (user, Aug 3
     2026: "its all getting cuttoff on the left"). Let it wrap, and let the
     cell be the thing that decides the width. */
  .gamecard-clock { flex: 1 1 100%; min-width: 0; white-space: normal; overflow-wrap: anywhere; font-size: var(--fs-xs); }
  /* Nothing in the spine may paint outside its cell. */
  .pickcell .gamecard, .pickcell .gamecard-state, .pickcell .gamechip { min-width: 0; max-width: 100%; }
  .gspine-play { overflow-wrap: anywhere; }
}

/* ----- .pickout: THE result element, identical on the slot header and in the
   season grid. Muted chrome, coloured numbers, no fills. ----- */
.pickout {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--text-muted);
  white-space: nowrap;
  padding-bottom: 2px;
}
.pickout[title] { cursor: help; }
.pickout-score, .pickout-w { font-variant-numeric: tabular-nums; }
.pickout-w { font-size: var(--fs-sm); }
.pickout-score { font-size: var(--fs-sm); color: var(--text); }
.pickout-final, .pickout-clock, .pickout-odds, .pickout-ppd {
  font-weight: 700;
  color: var(--text-faint);
  letter-spacing: 0.04em;
}
.pickout-ppd { color: var(--red); }
.pickout-odds { color: var(--red); } /* betting lines in red everywhere (user, Aug 6 2026) */
/* Inside a filled outcome cell the numbers inherit the outcome INK — standalone
   green/red text on its own green/red fill is close to invisible. */
.pickout.-w, .pickout.-w .pickout-w, .pickout.-w .pickout-score { color: var(--oc-win-ink); }
.pickout.-l, .pickout.-l .pickout-w, .pickout.-l .pickout-score { color: var(--oc-loss-ink); }
.pickout.-t, .pickout.-t .pickout-w { color: var(--oc-loss-ink); }
/* "VS LAR" / "AT PIT" — the opponent rides with the score, one step quieter so
   the score still reads first. */
.pickout-opp { font-weight: 700; letter-spacing: 0.03em; opacity: 0.8; }
.pickout.-live .pickout-score { color: var(--text); }
.pickout.-none, .pickout.-pre { color: var(--text-faint); font-weight: 600; }
.pickout.-bye { color: var(--text-muted); font-weight: 600; }
.pickout.-miss { color: var(--red); }
.pickout .badge { font-size: 0.58rem; }
.pickmarg { font-variant-numeric: tabular-nums; font-size: var(--fs-sm); }

/* Slot header: the viewer's own outcome sits beside the current-pick chip.
   It is a SIBLING of .pickslot-now (app.js rebuilds that one after every
   auto-save) and wraps to its own line on a phone rather than squeezing. */
.pickslot-res { display: inline-flex; align-items: center; }
.pickslot-res:empty { display: none; }

/* NO .pick-grid RULES HERE ANY MORE. That <table> was the season/archive grid; as
   of task #43 the season history is rows of .pickcols (the ONE pick board), so
   these cells are divs and their layout lives with the rest of the board — see
   "THE SEASON HALF" in the pick-board block below. Leaving td-scoped copies here
   is exactly how a stale duplicate ends up winning a cascade later. */

@media (max-width: 560px) {
  .teampick-score { font-size: 1.15rem; }
  .teampick-out { gap: 4px; }
  /* Phones: the outcome drops under the pick chip instead of off the edge. */
  .pickslot-res { width: 100%; }
  .pickout { white-space: normal; }
}

/* ==========================================================================
   THE 375px TOUCH PASS (measured July 30 2026, iPhone-mini viewport 375x812)
   ==========================================================================
   Rule: every CONTROL is at least var(--tap) (44px) on BOTH axes on a phone.
   A "control" is a chip, a button, a nav link or a form field. Table DATA
   cells are not controls and are exempt — the density ladder (S/M/L) exists
   precisely so the grid can be dense on purpose.

   Why this block is one place and not sprinkled: the same four controls
   (.week-chip, .season-chip, .btn.-sm and the pool bar) were each declared at
   whatever size their own page needed, and every one of them measured under
   44px. Sizes now come from the token, so raising --tap raises all of them.

   THE WEEK BAR GROWS WITH ITS CHIPS. --weekbar-h was 46px, which left the
   track 42px of content box — a 44px chip physically could not fit inside it.
   The bar is re-declared at 52px HERE, inside the same media query, so the two
   move together. Everything that positions off the chrome (every
   scroll-margin-top, the sidebar's sticky top, the phone overlay inset) reads
   the token and follows automatically — do NOT hard-code 46 or 52 anywhere. */
@media (max-width: 700px) {
  :root { --weekbar-h: 52px; }

  .week-chip { min-height: var(--tap); min-width: var(--tap); }
  .season-chip { min-height: var(--tap); padding-left: 14px; padding-right: 14px; }
  .season-nav-arrow { min-height: var(--tap); min-width: var(--tap); }

  /* .btn.-sm is a DENSITY variant, not a touch variant. It keeps its small
     type and padding; only the hit box grows. */
  .btn.-sm { min-height: var(--tap); }

  /* The under-title link row ("Pool rules · Full schedule") and the admin
     bar's escape hatch measured 17.0px and 17.4px tall. They read as prose, so
     the TEXT keeps its size and only the hit box grows — inline-flex keeps the
     box the link's own rather than the whole line's. */
  .page-head-links a,
  .admin-bar-classic { display: inline-flex; align-items: center; min-height: var(--tap); }

  /* NOTHING CLIPPED. .teampick-abv holds the team's FULL name (the class name
     predates that), and at 375px its box is 118px while the widest name wants
     201px — measured 28 spans clipped, the worst losing 83px, so "Washington
     Commanders" read as "Washington Comm…". nowrap + ellipsis was the wrong
     valve on a phone: there is a whole second line available inside the card.
     The name wraps instead. overflow-wrap:anywhere is the guard for a single
     token wider than the column (nothing in team_seasons is, today). */
  .teampick-abv {
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    overflow-wrap: anywhere;
    font-size: var(--fs-sm);
    line-height: 1.15;
  }
}

/* ----- THE TWO DELIBERATE EXCEPTIONS, and why they are not oversights -------
   Both were measured under 44px and both are LEFT that way on purpose. Anyone
   re-running a touch audit will find them again; read this before "fixing" them.

   1. .teampick-more ("Team page"), 55.9 x 15.5 on the pick page.
      It is an <a> nested INSIDE the <label> that is the pick control (measured:
      the label is 140 x 100.9). Growing the link to 44px would hand 44% of the
      pick button's height to a link that navigates AWAY from the pick page —
      the accidental tap would cost a user their pick action, which is strictly
      worse than a small target. It stays a small secondary affordance.

   2. The standings-grid cell links. Measured at 375px:
        knockout 2025 (113 entries x 18 weeks) — .kslot 47.8 x 40.5 and
          .glink.kresl 47.8 x 22.5, 1554 links.
        koob 2025 (183 entries, 2 picks/week) — .tlink 20.6 x 12.2, .glink
          17.1 x 13.2, .tlink.kimg 109 x 21, .glink.kopl 109 x 9.5, 22476 links.
      These are DATA cells that happen to link. The grid already ships an
      explicit density ladder (.gsz-s / default / .gsz-l, the S/M/L control) so
      the reader chooses the trade; four 44px targets per koob cell would be a
      5619-cell board roughly 4x taller than the 8500px it already is, and would
      defeat the reason the ladder exists. The CONTROLS that drive the ladder
      ARE 44px (see the touch block in src/standings.ts). */

/* ==========================================================================
   THE PICK BOARD — one column per ENTRY (task #39, owned by src/picks.ts), and
   ONE grid for the week's picks AND the season's history (task #43).

   User, July 29 2026: "i want the pick page to show columns for each entry ...
   a column with the header being the entry name, then each column shows the
   picks for that entry (if they exist) plus has the schedule where the user can
   click on the picks, clear the picks, change the picks ... the tricky part is
   you have to account for potentially 5, 6, who knows how many entries ...
   there becomes a point where you probably lose the full teams names and drop
   down to three letter abbreviations with the logos, then if things get really
   crazy, like 10 entries or something, maybe it even drops the logos ... this is
   dependent on screen size/resolution".

   NOTHING HERE RE-DECLARES A CANONICAL PATTERN. The buttons are still
   .teampick / .teampick-body, the outcome fills are still .oc-won / .oc-lost /
   .oc-tie, the chips are .chip, the results are .pickout, the buttons are .btn,
   the notice is flash(). This block only LAYS THEM OUT in a grid and chooses
   between the two name widths the server always renders. Re-declaring an
   outcome colour or a chip here is the exact habit that let a duplicated
   .sb-midlogo rule silently win the cascade.

   HOW ROWS STAY ALIGNED. One grid owns the row axis for every column. A column
   is a form with display:contents, so its cells are grid items of the OUTER
   grid: cell N of every column lands on row N (the nth-child rules below), and
   the tallest cell in a row grows that row everywhere. No fixed row heights, no
   subgrid, no JS measuring. The form keeps working exactly as a form —
   display:contents removes its BOX, not its behaviour, and app.js still finds
   every field with form.querySelector.
     CONSEQUENCE, and it is load-bearing: a column's DIRECT children must be
     nothing but its cells, in the same count and order in every column. The
     hidden entry/week/slot inputs therefore live INSIDE the head cell (picks.ts
     says so too), and app.js must never insert a direct child into the form.

   THE SEASON HISTORY IS PART OF THIS GRID (task #43, user July 30 2026: "the
   'season picks' thing under the picks selections needs to be in the same column
   ... it looks like shit right now because of course you didn't make the column
   widths match"). It was a <table class="pick-grid"> in its own card, so TWO
   layouts sized the same columns from their own content and drifted by
   construction — the .sb-midlogo / standings-zebra / koob-box-metrics habit,
   again. There is now ONE .pickcols: the week's head, game, BYE and foot rows,
   then a season head row, then one row per week, all on the SAME tracks.
     THERE ARE NO WIDTHS TO MATCH. Not two grids reading one custom property —
     ONE grid. grid-template-columns is declared exactly once, on .pickcols, and
     both halves are rows of it. Alignment is not maintained, it is structural:
     verified by measurement, not by eye. NOTE, July 31 2026: the season half is
     GONE from the picks page ("this eliminates the need for the picks summary
     thing at the bottom"), and the suite that measured the two halves against
     each other retired with it. The one-grid rule below still holds for the
     header, the game rows and the foot.

   THE COLUMN DIVIDER IS THE COLUMN GAP (task #41, user: "there needs to be very
   clear division between the picks columns"). With 6-10 columns of near-identical
   logo cells, misreading a column means misreading WHOSE pick it is.
     * column-gap is 2px and .pickcols paints var(--line-strong) behind it, so
       every boundary is a solid 2px rule that runs the FULL HEIGHT of the grid —
       through the header, through every game row, through the foot, and on
       through every season row. Cells never extend into the gap, so nothing can
       paint over it and it cannot be interrupted. A divider that stopped between
       the two halves would reintroduce the ambiguity it exists to remove.
     * row-gap is 0 and the horizontal hairlines are each cell's own
       border-bottom. That is the whole reason the vertical rule is unbroken: a
       1px row GAP would have crossed it once per row.
     * NOT hover (user: "stop fucking so much shit up with mousover bullshit", and
       hover does nothing on a phone) and NOT alternating column tints —
       backgrounds on this site mean OUTCOME (green correct / red incorrect /
       white neutral), so a tinted column would read as a result. Same reason the
       standings zebra was removed.

   THE DENSITY LADDER IS A CONTAINER QUERY ON THE COLUMN ITSELF, which is the
   only thing that knows about entry count AND viewport at the same time: the
   track width is (available width minus the spine) divided by the column count.
   So six entries on a 27 inch monitor get full names and six on a laptop do not,
   with no media query pretending to know how many entries there are and no JS
   resize handler.
     tightest  (default, under 100px of content) .. abbreviation only, no logo
     tighter   (100px and up) .................... abbreviation + logo
     roomy     (216px and up) .................... full team names
   The DEFAULT is the tightest tier on purpose: a browser without container
   query support still fits the page and stays usable, it is only terse.

   BOTH NUMBERS ARE MEASUREMENTS, not taste (tools/test-pick-density.mjs, real
   headless Chrome, July 30 2026 — it re-derives them from the browser on every
   run, so a font change cannot quietly invalidate them):

     216px for FULL NAMES. The widest UNBREAKABLE token in the league is
     "Commanders" at 96.8px in this tier's own font (--fs-sm at its 0.92rem cap);
     the pick button adds 6px of padding, and a cell holds TWO buttons plus the
     4px gap: 2 x (96.8 + 6) + 4 = 209.6. 216 leaves ~3px a button. Below that
     "Commanders" and "Jacksonville" break MID-WORD (overflow-wrap: anywhere
     keeps it from overflowing, but a hyphenless break reads as broken), so the
     abbreviation is the better answer and the ladder steps down.

     100px for THE LOGO, and it is deliberately close to the 104px column floor.
     The logo does NOT cost width here — .pickcols .teampick-body is a COLUMN
     flexbox, so the 30px helmet sits ABOVE the abbreviation and a 45px button
     seats it with room to spare. It costs HEIGHT: 33px x 16 games x every
     column. So the logo survives right down to the point where the tracks are
     PINNED to their 104px tap-target floor (94px of content on desktop, 96px on
     a phone, i.e. the grid is now scrolling and you are skimming) — and only
     there does it go. This threshold used to be 130px, which dropped the logos
     at FIVE entries on a 1280 laptop (128.8px of content: it missed by 1.2px)
     and left the middle tier rendering in 2 of 24 measured combinations. The
     user asked for the opposite ladder: "you probably lose the full teams names
     and drop down to three letter abbreviations WITH THE LOGOS, then if things
     get really crazy, like 10 entries or something, maybe it even drops the
     logos."

   THE PAGE NEVER SCROLLS SIDEWAYS (user: "i fucking hate scrollbars"). Only
   .pickcols-wrap scrolls, and only when the columns' own minimum widths cannot
   fit — everything else on the page keeps the normal reading measure.
   ========================================================================== */

.pickcols-wrap {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch;
  /* NO CARD AT ALL. The board is not a panel sitting on a page — it IS the page
     (user, July 31 2026: "the 'rounded div' or whatever you have going on there
     needs to go away... the games/checkboxes are the page... everything else is
     headers"). Border, radius and shadow all go; the filler track above is what
     stops the removed card leaving white space to the right. */
  width: 100%;
  max-width: 100%;
  margin: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}
.pickcols-wrap:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* THE LAST ENTRY'S CLOSING LINE RUNS THE WHOLE HEIGHT OF THE BOARD.
   Every divider between entries is a cell border, so it stops where the cells
   stop — and the line closing the RIGHT of the final entry stopped with them,
   leaving that column open-ended while every column to its left was ruled top
   to bottom (reported twice: "that last entry doesn't have a closing vertical
   green line", then "you have failed to make the green line run all the way
   down the page on the right side of pick 2 on the last entry").
   A border cannot do it: there are no cells below the last row to carry one.
   So the wrapper draws it once, down its full height, at the inside edge of the
   3px filler track — which is exactly where the last entry ends. */
.pickcols-wrap {
  position: relative;
  /* THE WRAPPER ENDS WHERE THE BOARD ENDS. The board is width:fit-content on a
     desktop, so a wrapper stretched to the page put its right edge far past the
     last entry — and the closing line below, positioned from that edge, drew
     itself out in the margin instead of against Pick 2 (user, Aug 1 2026: "why
     can't you line up that green fucking line on the right"). Shrinking the
     wrapper to its content makes the two edges the same edge. max-width keeps
     it inside the page, and the phone board is width:100% either way. */
  width: fit-content;
  max-width: 100%;
}
.pickcols-wrap::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  /* app.js measures where the LAST visible pick column actually ends and writes
     it here. Positioning from the wrapper's right edge only worked when the
     wrapper and the board were the same width, which is why this line kept
     sitting beside the board rather than closing it. Falls back to the old
     behaviour if the script has not run. */
  left: var(--pc-lastright, auto);
  right: var(--pc-lastright, 3px);
  width: 3px;
  background: var(--pc-divider);
  pointer-events: none;
}
.pickcols-wrap[style*="--pc-lastright"]::after { right: auto; }

.pickcols {
  --pc-spine: 132px;
  /* THE TEAM COLUMN HAS A FLOOR, AND IT IS NOT NEGOTIABLE. On a phone this
     track is flexible so it can absorb slack instead of leaving a dead gutter
     — but written as minmax(0, 1fr) it could absorb slack all the way to ZERO,
     and it did: at 375px the pager fitted THREE entry columns by shrinking the
     teams column out of existence, so the board rendered kickoff times, spreads
     and checkboxes with no team names anywhere on it. The no-sideways-scroll
     check passed the whole time, because nothing overflowed — the content had
     simply been squeezed away. A pick board without team names is not a denser
     pick board, it is a broken one, so the track cannot go below a helmet plus
     a nickname. */
  --pc-teams-min: 124px;
  /* THE FLOOR IS A TAP TARGET, not a guess. A column holds TWO pick buttons side
     by side, so the narrowest a column may ever get is 2 x var(--tap) plus the
     gap and the cell padding: 44 + 44 + 4 + 10 = 102, rounded to 104. Measured at
     375px with 6 columns and a 96px floor the buttons came out 42px wide, which
     breaks the phone rule that every CONTROL is at least 44px on BOTH axes (see
     THE 375px TOUCH PASS above). Below this the grid scrolls inside its wrapper
     instead of shrinking the buttons — the scroller exists for exactly this. */
  /* THE MATRIX (user, July 30 2026). A column is now ONE CHECKBOX wide, not two
     team buttons, because the teams are listed once in their own column instead
     of being repeated per entry. 44px is the touch floor and the whole column,
     which is what makes ten entries fit where six used to overflow. */
  --pc-colmin: 44px;
  --pc-teams: 232px;
  /* The rule between two slots of the SAME entry. Softer than --pc-divider on
     purpose: an entry's own picks are a group, and a board of 2-pick entries
     read as 4 unrelated columns when every rule was equally black. */
  --pc-divider-soft: #b9c4bc;
  --pc-cols: 1;
  display: grid;
  /* DECLARED ONCE, FOR BOTH HALVES. Every row of this grid — pick rows and season
     rows alike — is on these tracks, so there is no second set of widths that
     could drift from them (task #43). */
  /* ...plus a trailing 1fr FILLER track. The entry columns are fixed widths, so
     without it the grid ends at the last entry and any rule drawn on the header
     ends with it. The filler carries the header's dividing line out to the edge
     of the page. */
  grid-template-columns:
    var(--pc-spine) var(--pc-teams)
    repeat(var(--pc-cols), minmax(var(--pc-colmin), var(--pc-colmin)))
    minmax(0, 1fr);
  width: 100%;
  /* THE COLUMN GAP IS THE DIVIDER (task #41): a 2px stripe of --line-strong that
     runs the full height of the grid, unbroken through the header row, every game
     row, the foot and every season row. Cells are opaque and never extend into
     the gap, so nothing paints over it.
     ROW GAP IS 0 ON PURPOSE. A 1px row gap would have cut that vertical rule once
     per row; the horizontal hairlines are each cell's own border-bottom instead
     (see .pickcols > * > * below), which stops inside the cell and never crosses
     the column gap. */
  /* NO COLUMN GAP. The divider used to be a 3px gap with the grid's background
     showing through, and an entry's second slot painted over its own 3px to hide
     the line inside a group. That over-paint could not cover the cells'
     horizontal borders, so every horizontal rule had a 3px hole in it at the
     slot seam — visible as a faint white vertical line, "basically invisible
     except when they cross horizontal lines".
     The divider is now a LEFT BORDER on the column that starts a group. Borders
     are part of the cell, so the horizontal rules meet them cleanly and there is
     nothing to paint over. */
  column-gap: 0;
  row-gap: 0;
  background: var(--surface);
  /* THE BOX HAS TO BE AS WIDE AS ITS TRACKS, and this is a bug fix, not a tidy-up.
     A grid container is a block-level box: its width came from .pickcols-wrap, so at
     375px with 10 entries the BOX was 345px while the tracks ran to 1150px — the
     tracks overflowed their own container. Everything inside still laid out
     correctly, which is why it went unnoticed, but the container's BACKGROUND only
     paints its own box: scroll past 345px and the column-gap stripes had nothing
     behind them, so the divider simply stopped. Measured in headless Chrome
     (by the board suite that preceded tools/test-pick-matrix.mjs):
     elementFromPoint down boundary 3 of 10 at 375px
     answered .pickcols-wrap, not .pickcols. min-content here is the sum of the
     track minimums, so the box grows to hold every track and the wrapper scrolls;
     where there IS room, width stays auto and the 1fr tracks share it exactly as
     before (track widths are unchanged in both cases — also measured). */
  min-width: min-content;
  /* AND NO WIDER THAN ITS TRACKS. The entry columns are a fixed 44px now, so a
     block-level grid box stretched to the wrapper leaves dead space to the right
     of the last column — and this box's background IS the divider colour, so
     that dead space painted as a slab of dark green beside the board (measured,
     1280px x 6 entries). fit-content makes the box exactly its tracks; the
     wrapper still scrolls when the tracks are wider than it. */
  width: fit-content;
  max-width: 100%;
  align-items: stretch;
}

/* Column count comes from the server (data-cols = entries x picks_per_week).
   Past the end of this list the explicit grid-column assignments still place
   every column — they just land in auto-sized implicit tracks, which degrades
   to uneven widths inside the scroller instead of breaking. */
.pickcols[data-cols="1"] { --pc-cols: 1; }
.pickcols[data-cols="2"] { --pc-cols: 2; }
.pickcols[data-cols="3"] { --pc-cols: 3; }
.pickcols[data-cols="4"] { --pc-cols: 4; }
.pickcols[data-cols="5"] { --pc-cols: 5; }
.pickcols[data-cols="6"] { --pc-cols: 6; }
.pickcols[data-cols="7"] { --pc-cols: 7; }
.pickcols[data-cols="8"] { --pc-cols: 8; }
.pickcols[data-cols="9"] { --pc-cols: 9; }
.pickcols[data-cols="10"] { --pc-cols: 10; }
.pickcols[data-cols="11"] { --pc-cols: 11; }
.pickcols[data-cols="12"] { --pc-cols: 12; }
.pickcols[data-cols="13"] { --pc-cols: 13; }
.pickcols[data-cols="14"] { --pc-cols: 14; }
.pickcols[data-cols="15"] { --pc-cols: 15; }
.pickcols[data-cols="16"] { --pc-cols: 16; }
.pickcols[data-cols="17"] { --pc-cols: 17; }
.pickcols[data-cols="18"] { --pc-cols: 18; }
.pickcols[data-cols="19"] { --pc-cols: 19; }
.pickcols[data-cols="20"] { --pc-cols: 20; }
.pickcols[data-cols="21"] { --pc-cols: 21; }
.pickcols[data-cols="22"] { --pc-cols: 22; }
.pickcols[data-cols="23"] { --pc-cols: 23; }
.pickcols[data-cols="24"] { --pc-cols: 24; }

/* A column contributes no box of its own — its CELLS are the grid items. */
.pickcols > .pickcol { display: contents; }

/* WHICH COLUMN. Child 1 of the grid is the schedule spine, then one column per
   (entry, slot) in server order. */
.pickcols > :nth-child(1) > * { grid-column: 1; }
/* A CONTINUING SLOT OF THE SAME ENTRY gets a softer rule to its left. The
   divider is the grid's own column gap, which is one colour for the whole grid,
   so a continuing column paints over its own 3px of that gap. One declaration,
   no second grid, nothing measured. */
/* NO RULE between two picks of the SAME entry — they are one group and the
   entry name spans them both (user: "for 2 pick per entry pools, i don't want a
   line between the pick columns"). The continuing column paints its own 3px of
   the grid gap in the cell colour, which erases the divider there and nowhere
   else. */
/* The over-paint has to match the cell it sits beside, or it reads as a line of
   its own — which is exactly what happened in the header, where the cells are
   --surface-2 and this painted --surface: a pale stripe between Pick 1 and
   Pick 2 (user: "get ride of the ugly white line between pick 1 and pick 2"). */
/* THE ENTRY DIVIDER: a left border on the first column of each entry. A
   continuing slot of the same entry draws none, which is what groups an entry's
   picks without any over-painting. */
.pickcols > .pickcol[data-colkey] > * { border-left: 3px solid var(--pc-divider); }
.pickcols > .pickcol[data-slotcont] > * { border-left: 0; }
.pickcols > .pickcol.-teams > * { border-left: 3px solid var(--pc-divider); }
/* AND THE LAST ENTRY CLOSES. Each entry column opens with a left border, so the
   final one had nothing on its right and the block of entries ran open-ended
   into the filler. The filler draws the closing rule — one line, in the same
   colour, so the group of entries reads as bounded on both sides. */
.pickcols > * > .pickcell.-fillhead,
.pickcols > * > .pickcell.-fillbody { border-left: 3px solid var(--pc-divider); }

.pickcols > :nth-child(2) > * { grid-column: 2; }
.pickcols > :nth-child(3) > * { grid-column: 3; }
.pickcols > :nth-child(4) > * { grid-column: 4; }
.pickcols > :nth-child(5) > * { grid-column: 5; }
.pickcols > :nth-child(6) > * { grid-column: 6; }
.pickcols > :nth-child(7) > * { grid-column: 7; }
.pickcols > :nth-child(8) > * { grid-column: 8; }
.pickcols > :nth-child(9) > * { grid-column: 9; }
.pickcols > :nth-child(10) > * { grid-column: 10; }
.pickcols > :nth-child(11) > * { grid-column: 11; }
.pickcols > :nth-child(12) > * { grid-column: 12; }
.pickcols > :nth-child(13) > * { grid-column: 13; }
.pickcols > :nth-child(14) > * { grid-column: 14; }
.pickcols > :nth-child(15) > * { grid-column: 15; }
.pickcols > :nth-child(16) > * { grid-column: 16; }
.pickcols > :nth-child(17) > * { grid-column: 17; }
.pickcols > :nth-child(18) > * { grid-column: 18; }
.pickcols > :nth-child(19) > * { grid-column: 19; }
.pickcols > :nth-child(20) > * { grid-column: 20; }
.pickcols > :nth-child(21) > * { grid-column: 21; }
.pickcols > :nth-child(22) > * { grid-column: 22; }
.pickcols > :nth-child(23) > * { grid-column: 23; }
.pickcols > :nth-child(24) > * { grid-column: 24; }
.pickcols > :nth-child(25) > * { grid-column: 25; }

/* WHICH ROW. Row 1 is the header, then one row per game of the week, then the
   optional BYE row, then the footer — and then (task #43) the season head row and
   one row per week of the season history, on these same tracks. Every column emits
   the SAME cells in the SAME order, spine included, so cell N of one column is
   always on the same row as cell N of the next.

   WHY THE LADDER RUNS TO 48, and it is arithmetic, not a guess. The worst case
   this page can build is:
       1 header
    + 16 game rows      (32 NFL teams = at most 16 games in a week)
    +  1 BYE row
    +  1 foot row
    +  1 season head row
    + 22 season week rows (18 regular + 4 playoff — the widest season that exists)
    = 42
   48 leaves six rows of headroom over a maximum that cannot grow without the NFL
   changing shape. Past the end of the ladder a cell would land in an implicit row
   and the two halves would stop lining up, which is precisely what task #43 was
   about — so the bound is stated here rather than left to be discovered. */
.pickcols > * > :nth-child(1) { grid-row: 1; }
.pickcols > * > :nth-child(2) { grid-row: 2; }
.pickcols > * > :nth-child(3) { grid-row: 3; }
.pickcols > * > :nth-child(4) { grid-row: 4; }
.pickcols > * > :nth-child(5) { grid-row: 5; }
.pickcols > * > :nth-child(6) { grid-row: 6; }
.pickcols > * > :nth-child(7) { grid-row: 7; }
.pickcols > * > :nth-child(8) { grid-row: 8; }
.pickcols > * > :nth-child(9) { grid-row: 9; }
.pickcols > * > :nth-child(10) { grid-row: 10; }
.pickcols > * > :nth-child(11) { grid-row: 11; }
.pickcols > * > :nth-child(12) { grid-row: 12; }
.pickcols > * > :nth-child(13) { grid-row: 13; }
.pickcols > * > :nth-child(14) { grid-row: 14; }
.pickcols > * > :nth-child(15) { grid-row: 15; }
.pickcols > * > :nth-child(16) { grid-row: 16; }
.pickcols > * > :nth-child(17) { grid-row: 17; }
.pickcols > * > :nth-child(18) { grid-row: 18; }
.pickcols > * > :nth-child(19) { grid-row: 19; }
.pickcols > * > :nth-child(20) { grid-row: 20; }
.pickcols > * > :nth-child(21) { grid-row: 21; }
.pickcols > * > :nth-child(22) { grid-row: 22; }
.pickcols > * > :nth-child(23) { grid-row: 23; }
.pickcols > * > :nth-child(24) { grid-row: 24; }
.pickcols > * > :nth-child(25) { grid-row: 25; }
.pickcols > * > :nth-child(26) { grid-row: 26; }
.pickcols > * > :nth-child(27) { grid-row: 27; }
.pickcols > * > :nth-child(28) { grid-row: 28; }
.pickcols > * > :nth-child(29) { grid-row: 29; }
.pickcols > * > :nth-child(30) { grid-row: 30; }
.pickcols > * > :nth-child(31) { grid-row: 31; }
.pickcols > * > :nth-child(32) { grid-row: 32; }
.pickcols > * > :nth-child(33) { grid-row: 33; }
.pickcols > * > :nth-child(34) { grid-row: 34; }
.pickcols > * > :nth-child(35) { grid-row: 35; }
.pickcols > * > :nth-child(36) { grid-row: 36; }
.pickcols > * > :nth-child(37) { grid-row: 37; }
.pickcols > * > :nth-child(38) { grid-row: 38; }
.pickcols > * > :nth-child(39) { grid-row: 39; }
.pickcols > * > :nth-child(40) { grid-row: 40; }
.pickcols > * > :nth-child(41) { grid-row: 41; }
.pickcols > * > :nth-child(42) { grid-row: 42; }
.pickcols > * > :nth-child(43) { grid-row: 43; }
.pickcols > * > :nth-child(44) { grid-row: 44; }
.pickcols > * > :nth-child(45) { grid-row: 45; }
.pickcols > * > :nth-child(46) { grid-row: 46; }
.pickcols > * > :nth-child(47) { grid-row: 47; }
.pickcols > * > :nth-child(48) { grid-row: 48; }

/* Every cell: opaque, its own density container, no inherited card edge, and its
   OWN horizontal hairline. The border-bottom (not a row gap) is what leaves the
   2px column gap free to run as one unbroken vertical rule — see the divider note
   in this block's header. */
.pickcols > * > * {
  min-width: 0;
  padding: 5px;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  /* AN OPAQUE BASE, AND THE OUTCOME FILL ON TOP OF IT. background-color is always
     the page surface; the outcome arrives as a background-IMAGE built from
     --oc-fill, which the canonical .oc-* rule set on this cell (see "THE outcome
     rule" — no colour is named here, only read). Two reasons it has to be this way
     round:
       1. the fills are translucent, and this grid's own background is the
          divider colour — a bare translucent fill showed --line-strong grey
          through it and the decade-old green/red read muted;
       2. background-image paints ABOVE background-color, so the outcome lands on
          white exactly as it does in every table on the site.
     A cell with no outcome resolves --oc-fill to transparent and is plain surface.
     Measured July 30 2026: before this, a settled season cell rendered WHITE with
     a small tinted chip inside it, because .pickcols > * > *'s background came
     later in the file than .oc-lost and simply won. */
  background-color: var(--surface);
  background-image: linear-gradient(var(--oc-fill, transparent), var(--oc-fill, transparent));
  /* NOTHING LEAVES ITS CELL. Measured July 30 2026 (by the board suite that
     preceded tools/test-pick-matrix.mjs):
     the shared nowrap elements — the slot result's kickoff + spread, "Your pick",
     the team-once mark — spilled SIDEWAYS out of a narrow cell, landed in the NEXT
     entry's column and painted over the 2px divider. Both halves of that are the
     whose-pick-is-this ambiguity task #41 exists to remove. A cell IS a column
     here, so a cell clips; the few offenders are also allowed to wrap (below) so
     clipping is the backstop and not the mechanism. */
  overflow: hidden;
  container-type: inline-size;
  container-name: pcol;
}
/* No hairline under the last row of a column: the wrapper's own border is there. */
.pickcols > * > :last-child { border-bottom: 0; }

/* THE SPINE STAYS PUT, IN BOTH HALVES. When the grid scrolls sideways the
   schedule must not leave with it — a column of helmets with no matchup beside it
   is unreadable, and the same is true of a season row with no week number.
   THE SHADOW IS THE DIVIDER, TRAVELLING. The grid's own background paints the
   spine/column-2 rule, but that stripe belongs to the grid and scrolls away under
   the sticky cells; this 2px shadow sits in exactly the same 2px, in exactly the
   same colour, so unscrolled it IS that rule (one line, not two) and scrolled it
   keeps the spine's edge where the eye expects it. */
.pickcols > :nth-child(1) > * {
  position: sticky;
  left: 0;
  z-index: 3;
}
/* THE TEAM COLUMN STICKS TOO, immediately right of the game info. Scrolling ten
   entry columns sideways with the team names gone would leave a field of
   anonymous checkboxes — the one thing this layout must never do. */
/* A CONTINUING SLOT OF THE SAME ENTRY gets a softer rule to its left. The
   divider is the grid's own column gap, which is one colour for the whole grid,
   so a continuing column paints over its own 3px of that gap. One declaration,
   no second grid, nothing measured. */
/* NO RULE between two picks of the SAME entry — they are one group and the
   entry name spans them both (user: "for 2 pick per entry pools, i don't want a
   line between the pick columns"). The continuing column paints its own 3px of
   the grid gap in the cell colour, which erases the divider there and nowhere
   else. */
/* The over-paint has to match the cell it sits beside, or it reads as a line of
   its own — which is exactly what happened in the header, where the cells are
   --surface-2 and this painted --surface: a pale stripe between Pick 1 and
   Pick 2 (user: "get ride of the ugly white line between pick 1 and pick 2"). */
/* THE ENTRY DIVIDER: a left border on the first column of each entry. A
   continuing slot of the same entry draws none, which is what groups an entry's
   picks without any over-painting. */
.pickcols > .pickcol[data-colkey] > * { border-left: 3px solid var(--pc-divider); }
.pickcols > .pickcol[data-slotcont] > * { border-left: 0; }
.pickcols > .pickcol.-teams > * { border-left: 3px solid var(--pc-divider); }
/* AND THE LAST ENTRY CLOSES. Each entry column opens with a left border, so the
   final one had nothing on its right and the block of entries ran open-ended
   into the filler. The filler draws the closing rule — one line, in the same
   colour, so the group of entries reads as bounded on both sides. */
.pickcols > * > .pickcell.-fillhead,
.pickcols > * > .pickcell.-fillbody { border-left: 3px solid var(--pc-divider); }

/* STICKY ONLY — NO SHADOW. This painted a 3px rule on the team column's RIGHT
   edge, left over from when the divider was a grid gap. Now that each entry
   column draws its own 3px LEFT border, the two sat side by side and read as one
   6px double line. The border owns the divider; this rule only pins the column. */
.pickcols > :nth-child(2) > * {
  position: sticky;
  left: var(--pc-spine);
  z-index: 2;
}

/* ONE ALIGNMENT PER COLUMN (task #42, user July 30 2026: the "Team page" link "is
   left-justified while everything else in the cell is centred").
   THE SPINE IS LEFT-ALIGNED — it is the label column, and a ragged-right list of
   matchups and week numbers reads as a schedule. EVERY ENTRY COLUMN IS CENTRED,
   top to bottom: header, game cells, BYE row, foot. child 1 is the spine, so
   :nth-child(n+2) is exactly "every entry column" and the rule is stated ONCE, at
   the column, instead of per element — which is how a link and a button drifted
   left in the first place. The offending per-element declarations
   (.teampick-more{align-self:flex-start} in picks.ts, .pickclear{text-align:left}
   below) are DELETED rather than overridden. */
.pickcols > :nth-child(n+2) > * { text-align: center; }
.gspine-teams {
  display: flex;
  align-items: center;
  gap: 4px;
  font-weight: 800;
  font-size: var(--fs-sm);
  letter-spacing: 0.02em;
}
.gspine-meta {
  display: flex;
  flex-direction: column;
  gap: 1px;
  margin-top: 2px;
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

/* HEADER + FOOTER CELLS, and the header is what ANCHORS a column (task #41). The
   2px column rule already runs up through it — it is the grid's gap, so it cannot
   do otherwise — and the head cell is distinctly styled so the eye can find the
   top of the column it is reading: a raised surface, a heavier accent underline,
   and a 3px accent cap along its top edge. No tint on the column body, ever:
   background means OUTCOME on this site. */
.pickcols .pickcell.-head,
/* The foot band mirrors the header band — same --head-band, or the board would
   open green and close grey. */
.pickcols .pickcell.-foot { background: var(--head-band); }
/* NO ACCENT LINES. These two green rules were the "green lines at the top": a
   3px accent above every header cell and a 2px accent below. The header band is
   defined by its background and by the single rule under it, not by a colour
   that means something else everywhere on this site. */
.pickcols .pickcell.-head {
  border-top: 0;
  border-bottom: 0;
}
.pickcol-name {
  display: block;
  font-weight: 800;
  font-size: var(--fs-sm);
  line-height: 1.2;
  overflow-wrap: anywhere;
}
.pickcol-sub {
  display: block;
  font-size: var(--fs-xs);
  color: var(--text-muted);
  font-weight: 600;
}
.pickcol-sub.-mine { font-style: italic; overflow-wrap: anywhere; }
.pickcol-rowlab {
  font-size: var(--fs-xs);
  font-weight: 700;
  color: var(--text-muted);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
/* justify-content, NOT text-align: these are flex rows, and the column rule below
   (text-align on the cell) cannot move a flex item. Both of them only ever render
   inside an ENTRY column's header cell, so no spine cell is affected. */
.pickcols .pickslot-now { display: flex; flex-wrap: wrap; justify-content: center; margin-top: 3px; }
.pickcols .pickslot-res { display: flex; flex-wrap: wrap; justify-content: center; }
/* The shared .pickout is nowrap because everywhere else it owns a whole row. In a
   header cell one entry wide it has to wrap instead of running into the next
   column (see the clip note on the cell rule above). */
.pickcols .pickcell.-head .pickout { flex-wrap: wrap; white-space: normal; justify-content: center; }
.pickcol-acts { display: flex; flex-direction: column; gap: 4px; }
.pickcols .pickslot-save { margin: 0; }
/* REMOVE, INSIDE THE PICK'S OWN CELL (task #50). It is the last child of the
   .gamecard that holds this slot's pick, under the two team cards, so it reads
   as belonging to that pick instead of to the column. Centred to match the cell,
   and it keeps the full tap target. */
.pickcols .pickclear-row { margin: 2px 0 0; text-align: center; }
/* NO text-align HERE. It said `left`, which made the Remove button the second thing
   in a centred column sitting hard left (the "Team page" link was the first — task
   #42). The column's own alignment owns it now; see the ONE ALIGNMENT PER COLUMN
   rule above. */
.pickcols .pickclear {
  white-space: normal;
  overflow-wrap: anywhere;
  font-size: var(--fs-xs);
}
.pickslot-lockednote {
  margin: 0;
  font-size: var(--fs-xs);
  color: var(--text-faint);
  font-weight: 600;
}

/* THE GAME CELLS. .gamecard keeps every hook the live patcher writes to
   (data-gid / data-status / -locked / .gamecard-state); only its card box is
   neutralised, because the cell IS the box now. The .-locked wash is
   deliberately NOT touched: a locked game must keep reading as locked. */
.pickcols .gamecard { display: block; }
.pickcell-teams {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 4px;
  height: 100%;
}
/* NO text-align HERE EITHER, and for the same reason as .pickclear below: it said
   `center`, which was right, but it was a SECOND owner of the entry column's
   alignment — and while it happened to agree, the "Team page" link two levels down
   did not. Alignment is the column's, once (see ONE ALIGNMENT PER COLUMN above);
   this rule keeps only the flex geometry, which is genuinely its own. */
.pickcols .teampick-body {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  min-height: 54px;
  padding: 5px 3px;
}
.pickcols .teampick-text { align-items: center; width: 100%; }
.pickcols .teampick-you,
.pickcols .teampick-out { margin-left: 0; }
.pickcols .teampick-out { justify-content: center; }
.pickcols .teampick-score { font-size: var(--fs-base); }
.tp-code {
  font-weight: 800;
  font-size: var(--fs-sm);
  letter-spacing: 0.05em;
}
/* ==========================================================================
   USED TEAMS LOOK USED — tasks #42 and #44, and this is the ONLY place it is
   styled. The user, July 30 2026: "used picks should be 'greyed out' somehow",
   then "get rid of the word 'freese' or whatever its supposed to be saying, if its
   already used in another week, put the week number in red and that's it.. and
   GREY THE MOTHER FUCKER OUT".

   IT USED TO BE SPLIT IN TWO FILES: .teampick-used / .teampick-usedmark were
   declared in picks.ts's page <style> while the density ladder that showed and hid
   them lived here. A page <style> OUTRANKS this file, so any greying rule written
   here would have lost to that copy without saying so — the same duplicate-owner
   trap as the .sb-midlogo rule whose copy silently won. Both of those classes are
   gone, with the two wordings they carried ("releases it", "frees").

   WHAT THE CELL SAYS: the week number, in red, and nothing else. No words, no
   separator. The explanation is in the title attribute, which is where an
   explanation belongs on a 104px-wide column.
   WHY filter AND NOT A BACKGROUND: on this site a background means an OUTCOME
   (green correct / red incorrect). "Used" is not an outcome, so it may not touch
   the cell's fill — it desaturates the ART and mutes the INK instead.
   ========================================================================== */
.teampick-wk {
  display: block;
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  color: var(--red);
  /* Wraps, for the same measured reason as .teampick-you above. */
  white-space: normal;
  overflow-wrap: anywhere;
}
.teampick.-used .helmet { filter: grayscale(1); opacity: 0.4; }
/* NO STRIKE-THROUGH. There was a line through the team code here and it was
   never asked for — user, July 30 2026: "did i asked for a fucking line through
   teams you cannot use? no i fucking did not... i just asked for greyed out, and
   that doesn't mean literally grey, it means... like 'unavailable button' type of
   greyed out... 'faded' maybe is the word?"
   So this is the DISABLED-BUTTON look and nothing else: the ink fades, the fill
   goes flat, the edge softens. Faded, not crossed out and not painted grey.
   Every property is applied to the CARD'S OWN ELEMENTS one at a time rather than
   as an opacity on the body, because opacity on a parent cannot be undone by a
   child — and .teampick-wk, the red week number that is the entire point of the
   mark, has to stay at full strength. */
.teampick.-used .tp-name,
.teampick.-used .tp-code { color: var(--text-faint); }
.teampick.-used .teampick-more { color: var(--text-faint); opacity: 0.65; }
.teampick.-used .teampick-body {
  background: var(--surface-2);
  border-style: solid;
  border-color: var(--line);
}
/* NOT the blanket locked dim, which fades the whole card to 0.55 and would fade
   the red week number with it. The greying above is deliberate and targeted, so a
   used card keeps full opacity — EXCEPT when the GAME is locked, which is a
   different fact and keeps its own dim. (A hard-used card has carried no input
   since task #47, so this is by class now; the input form is kept for the soft
   used card, whose radio is still real.) */
.teampick.-used:not(.-locked) input:disabled + .teampick-body,
.teampick.-used.-usedlock:not(.-locked) > .teampick-body { opacity: 1; }

/* HELD BY A LOCKED SLOT = NOT A CONTROL AT ALL (never show an action the user
   cannot take). teamPickHtml renders that card with NO INPUT and as a <div>
   (task #47), which is what keeps it unclickable and out of the tab order; this
   takes the whole card out of hit-testing as well, so a tap on the logo or the
   abbreviation does not even land on it. NOT hover-based and nothing here needs a
   pointer to work.
   The drill-down link is put back on purpose: "Team page" is information, not an
   action the server will refuse.
   A team held by a slot that has NOT kicked off is deliberately NOT in here — it
   stays a real control, because clicking it is how the user asked to move a future
   pick ("the website would have to warn them that its going to do it and do it for
   them", July 29 2026), and it opens the one confirm. */
.teampick.-usedlock { pointer-events: none; }
.teampick.-usedlock .teampick-more { pointer-events: auto; }
.teampick.-usedlock .teampick-body { cursor: not-allowed; }
.pickcols .teampick.-bye { margin: 0; }
.pickcols .teampick.-bye .teampick-body { min-height: 44px; }
.pickcols .bye-mark { width: 34px; height: 34px; font-size: 0.58rem; }

/* THE DENSITY LADDER IS THE PICK CARD'S, AND ONLY THE PICK CARD'S. Every selector
   in the three tiers below is scoped to .teampick — the tappable card — because
   since task #43 the same grid also holds the SEASON rows, whose helmets are
   .cellpick's 36px standings helmets. `.pickcols .helmet` would have hidden those
   at the tightest tier and resized them at the other two, which is a different
   surface answering to a threshold that was measured for this one. */

/* ----- TIER 3, THE DEFAULT: tightest. Abbreviation only, no logo. -----
   .teampick-wk IS NOT IN THIS LADDER AT ALL. It was two elements (a long sentence
   and a short marker) that the tiers swapped between; "W12" fits the narrowest
   column this ladder builds, so there is one element, shown at every tier, and no
   tier can accidentally hide the only thing that says the team is spent. */
.pickcols .tp-name,
.pickcols .teampick-more,
.pickcols .teampick-out,
.pickcols .teampick-you,
.pickcols .teampick.-bye .teampick-name,
.pickcols .teampick .helmet { display: none; }
.pickcols .tp-code { display: block; }

/* ----- TIER 2: abbreviation + logo, and the score comes back. ----- */
@container pcol (min-width: 100px) {
  .pickcols .teampick .helmet { display: block; width: 30px; height: 30px; }
  .pickcols .teampick-out { display: flex; }
  .pickcols .teampick-you { display: inline-block; }
}

/* ----- TIER 1: roomy. Full team names, bigger logo, the drill-down link and
   the long team-once sentence. ----- */
@container pcol (min-width: 216px) {
  .pickcols .tp-name {
    display: block;
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    overflow-wrap: anywhere;
    font-size: var(--fs-sm);
    line-height: 1.15;
  }
  .pickcols .tp-code { display: none; }
  .pickcols .teampick .helmet { width: 40px; height: 40px; }
  .pickcols .teampick-more { display: inline-block; }
  .pickcols .teampick.-bye .teampick-name { display: block; }
  .pickcols .teampick-body { min-height: 64px; }
}

/* ==========================================================================
   THE SEASON HALF — rows of THIS grid, not a table of its own (task #43).
   Same tracks, same spine, same 2px column rule running down through them.
   ========================================================================== */

/* THE ONE HORIZONTAL DIVISION THAT MATTERS: where the week's picks end and the
   season's history begins. It is a CELL border, so it stops inside the cell and
   never crosses the column gap — the vertical rules stay unbroken through it.
   :not(:first-child) because on an archived pool with no week selected the season
   head IS row 1, and a divider above the first row divides nothing. */
.pickcols .pickcell.-shead:not(:first-child) { border-top: 3px solid var(--pc-divider); }

/* A season cell: the helmet, the abbreviation and the result, stacked and centred.
   The fill (green correct / red incorrect) comes from the canonical .oc-won /
   .oc-lost / .oc-tie rules on the cell itself — NOT redeclared here. */
/* text-align is NOT declared here — third and last of the duplicate owners removed
   in task #42. It said `center`, which is right for an entry column and wrong for
   the spine, and the very next rule already has to say "the spine is different".
   One statement of that difference is enough: ONE ALIGNMENT PER COLUMN above
   centres every entry column and leaves the spine alone, and align-items below is
   the flex half of the same fact. */
.pickcols .pickcell.-srow {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-height: 52px;
}
.pickcols .pickcell.-srow.-spine { align-items: flex-start; }
/* The shared .pickout is nowrap because on the slot header it owns a whole row.
   In a 104px track it has to wrap, and it has to centre under the helmet. */
.pickcols .pickcell.-srow .pickout {
  justify-content: center;
  flex-wrap: wrap;
  white-space: normal;
  /* ONLY THE CELL IS COLOURED. pickResultHtml puts the outcome class on this span
     too (it is the outcome indicator on surfaces that have no cell of their own,
     like the slot header above), and the fills are translucent — left alone it
     painted a SECOND 0.32 layer over the cell's, a visibly darker patch behind the
     score. Same law, and the same fix, as .sgrid td .kres. */
  background: none;
}
/* THE UNFILLED STATES. These used to come from td.bye / td.missed / td.hid, which
   could only ever match a <table>; the fills above are shared and stay shared. */
.pickcols .pickcell.-srow.bye,
.pickcols .pickcell.-srow.buyback { color: var(--text-muted); }
.pickcols .pickcell.-srow.missed { color: #7f1d1d; }
.pickcols .pickcell.-srow.hid { color: var(--text-faint); }
.pickcols .pickcell.-srow.missed .cellout { font-size: var(--fs-xs); font-weight: 700; }
/* Inside a FILLED cell a chip takes the cell's ink — muted grey on green read as
   washed out (the <td> twins of these live with the canonical outcome rules). */
.pickcols .pickcell.-srow.oc-won .chip { color: var(--oc-win-ink); }
.pickcols .pickcell.-srow.oc-lost .chip,
.pickcols .pickcell.-srow.oc-tie .chip { color: var(--oc-loss-ink); }

/* A read-only grid presents no controls at all (picks.ts emits no radio there),
   so nothing in it may look tappable. */
.pickcols.-view .teampick-body { cursor: default; }
.pickcols.-view .teampick-body:hover { border-color: var(--line-strong); }

@media (max-width: 700px) {
  .pickcols { --pc-spine: 88px; }
  .pickcols > * > * { padding: 4px; }
}
@media (max-width: 560px) {
  .pickcols .pickcell.-srow .pickout { font-size: 0.68rem; }
}

/* Printing a pick sheet: no scroller, no footer controls. The outcome fills print
   as a light wash here for the same reason the table cells do — an edge bar does
   not print. These are the <div> twins of the td.oc-* print rules. */
@media print {
  .pickcols-wrap { overflow: visible; border: 0; box-shadow: none; }
  .pickcols .pickcell.-foot { display: none; }
  .pickcols .pickcell.-srow.oc-won { background: #d7f5d7 !important; color: #000; }
  .pickcols .pickcell.-srow.oc-lost,
  .pickcols .pickcell.-srow.oc-tie,
  .pickcols .pickcell.-srow.missed { background: #f5d7d7 !important; color: #000; }
  .pickcols .pickcell.-srow.bye,
  .pickcols .pickcell.-srow.buyback { background: #f0f0f0 !important; color: #000; }
}

/* ==========================================================================
   THE PICKS MATRIX (task #55, user July 30 2026)
   "each team is on a new line ... column 1 is the game time info like you have
    it now, the next columns are a column for each entry/pick (the header text
    would be turned vertically to save space) ... then the user just checks the
    boxes next to the teams they want ... that layout means you only have to
    list the nfl teams once instead of list them for each entry"
   ========================================================================== */

/* --- the rotated entry header ------------------------------------------- */
/* writing-mode, not a transform. A transform would leave the element's BOX
   horizontal, so the grid would still reserve the untransformed width and the
   whole point (a 44px column) would be lost. */
/* THE HEADER IS A FLEX COLUMN, AND SAYS SO. It set align-items and
   justify-content without ever setting display:flex, so BOTH were inert and the
   rotated label just sat wherever a block lands — hard against the left edge of
   a 44px track (user, July 31 2026: "i fucking told you to center pick 1 and
   pick 2 in the column"). align-items is the cross axis in a column flex, which
   is what actually centres a rotated label horizontally. */
.pickcell.-vhead {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  padding: 6px 2px;
  gap: 4px;
}
.vhead-rot {
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  align-self: center;
  flex: 0 0 auto;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-height: 190px;
  font-weight: 700;
  font-size: var(--fs-sm);
  line-height: 1.1;
  display: flex;
  align-items: center;
  gap: 6px;
}
/* THE CHIP IS NOT SHOWN HERE. .pickslot-now prints "VS DEN / Mon Aug 3 3:55 PM
   / DAL -3.5" — a whole matchup card. Rotated 90 degrees in a 44px column that
   was an unreadable wall that made the header ~300px tall (measured). The
   CHECKED BOX two rows below already says what this column picked, which is the
   whole idea of the matrix, so the chip is redundant here rather than merely
   ugly. It stays in the DOM because the feed swaps it by signature after a save;
   it is simply not drawn. */
.pickcell.-vhead .pickslot-now { display: none; }
/* THE SLOT RESULT IS NOT DRAWN HERE EITHER. Pre-kickoff it prints the matchup
   and the kickoff time ("VS DEN, Mon Aug 3, 3:55 PM"); sideways in a 44px column
   that is a smear. Post-kickoff it prints the result — which the board already
   shows twice over, as the outcome colour on this column's checked cell and as
   the score on the team row. Kept in the DOM for the feed's signature swap. */
.pickcell.-vhead .pickslot-res { display: none; }
/* A hard ceiling on the header, so one long entry name cannot set the height of
   every column on the board. */
.pickcell.-vhead { max-height: 210px; overflow: hidden; }

/* --- column 2: the team, once ------------------------------------------- */
/* THE PAIR HUGS, THE GAMES SEPARATE. With equal padding on both rows the space
   inside a game equalled the space between games, so the home team read as
   belonging to the NEXT game (user, July 31 2026: "the fucking home team of the
   first game is closer to the road team of the second game than it is to its
   actual opponent"). The away row loses its bottom padding, the home row loses
   its top padding and gains the separation instead — so a matchup is one block
   with real air after it. Applied to the checkbox cells too, or the boxes would
   drift out of line with the names beside them. */
/* THE ROW IS ONE LINE: helmet, "at", name, then the score hard right. The score
   used to be a SIBLING of a full-width flex link, so it wrapped underneath and
   every played game took two lines. */
.pickcell.-tmrow { padding: 3px 8px; display: flex; align-items: center; gap: 6px; }
/* min-width:0 on EVERY flex ancestor of the name, or the name refuses to shrink
   below its content width and the score — a later sibling — is painted straight
   over the top of it (measured: "Cowbo" with "L29-31" across it). A flex item's
   default min-width is auto, which is exactly "never smaller than my content". */
.pickcell.-tmrow .tmrow-link { flex: 1 1 auto; min-width: 0; overflow: hidden; }
.pickcols > * > .pickcell.-tmrow.-away,
.pickcols > * > .pickcell.-pkbox.-away { padding-bottom: 0; }
.pickcols > * > .pickcell.-tmrow.-home,
.pickcols > * > .pickcell.-pkbox.-home { padding-top: 0; padding-bottom: 9px; }
.pickcols > * > .pickcell.-tmrow.-home,
.pickcols > * > .pickcell.-pkbox.-home { border-bottom: 1px solid var(--line-strong); }
.tmrow-link {
  display: flex;
  align-items: center;
  gap: 7px;
  min-height: 30px;
  text-decoration: none;
  color: var(--text);
  min-width: 0;
}
/* "at" HAS ITS OWN FIXED GUTTER, before the logo. It is rendered on every row —
   empty on an away row — so the column reserves the same 15px either way and
   every helmet still starts at the same x. */
.tmrow-at {
  flex: 0 0 15px;
  color: var(--text-faint);
  font-size: 0.72rem;
  font-weight: 700;
  text-align: right;
}
.tmrow-text { min-width: 0; overflow: hidden; display: flex; align-items: baseline; gap: 4px; }
/* The ellipsis is a backstop that should never fire: the column is sized for the
   longest nickname plus a full score. It exists so that if it ever does not fit,
   the name clips cleanly instead of being overprinted. */
.tmrow-name { font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* EVERY LOGO ON ONE LEFT EDGE. The helmet is the first child of every row, home
   and away alike, and a fixed box keeps a wide logo from nudging the name. */
.tmrow-link > .helmet { flex: 0 0 26px; }
.tmrow-code { display: none; color: var(--text-faint); font-size: 0.72rem; font-weight: 700; }
/* GREEN WON, RED LOST — the same two colours the outcome scale uses, on the
   number rather than behind it. */
.tmrow-out {
  flex: 0 0 auto;
  display: flex;
  align-items: baseline;
  gap: 5px;
  font-size: 0.76rem;
  font-weight: 800;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.tmrow-out.-w { color: var(--oc-win-ink, #17420a); }
.tmrow-out.-l { color: var(--oc-loss-ink, #5a140c); }
.tmrow-out .teampick-res { font-weight: 900; }
/* The margin, for a pool that scores the margin. */
.tmrow-diff { font-size: 0.7rem; font-weight: 800; opacity: 0.85; }
.tmrow-bye { font-weight: 800; letter-spacing: 0.04em; color: var(--text-muted); }
/* A started game dims its own two rows in the team column. The ENTRY cells lose
   their inputs (rule a, in the markup); this is only the visual echo. */
.pickcell.-tmrow.-locked .tmrow-link { opacity: 0.62; }
/* THE SPREAD, RED, ON THE FAVORITE'S NAME (user, Aug 10 2026: "should say
   Titans -3 with the -3 in red"). Emitted in both the name and the code span —
   whichever the density ladder shows carries it. */
.tmrow-spread { color: var(--red); font-weight: 800; margin-left: 5px; font-size: 0.85em; white-space: nowrap; }
/* A GAME IS ONE BLOCK. The away row draws no bottom rule, so the hairlines fall
   between GAMES and never between a road team and its host. Applied to the
   whole row — the team cell and every entry cell on it — or the line would
   simply reappear one column to the right. */
.pickcols > * > .pickcell.-tmrow.-away,
.pickcols > * > .pickcell.-pkbox.-away { border-bottom: 0; }
/* The game-info cell spans both rows, so it already has only the outer edge. */
/* Narrow viewports drop the full name for the code — the same density idea the
   old board had, now needed in ONE column instead of every column. */
@media (max-width: 720px) {
  .pickcols { --pc-teams: 96px; }
  .tmrow-name { display: none; }
  .tmrow-code { display: inline; font-size: 0.82rem; color: var(--text); }
}

/* --- the checkbox cell --------------------------------------------------- */
/* The whole cell is the label, so the tap target IS the column width and there
   is no small square to hit. */
.pickcell.-pkbox {
  /* GRID, NOT FLEX-COLUMN. The cell holds a box and an optional red week number,
     and with flex-column the box's position depended on whether that number was
     there — so a used cell's box sat higher than its neighbours and the row of
     boxes stopped being a row (user, July 31 2026: "align the boxes
     motherfucker"). Two fixed rows put the box on the SAME baseline in every
     cell, present or absent, marked or not. */
  /* ONE ROW, because the red week number now lives INSIDE the box (user, July
     31 2026: "why no just put the red w1 inside the box"). It used to sit on a
     second row underneath, which made every row on the board taller to reserve
     space that most cells never used — and made "is the box aligned" a question
     in the first place. Inside the box there is nothing left to align. */
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 3px 2px;
  /* NOT var(--tap). 44px here set the height of every ROW on the board, which
     is why the two teams of one game could never be pulled together: the
     padding changed but the row did not. The cell IS the label, so the tap
     target is the full column width by the full row height; a coarse pointer
     gets the 44px back below. */
  min-height: 30px;
  cursor: pointer;
}
@media (pointer: coarse) { .pickcell.-pkbox { min-height: var(--tap); } }
.pickcell.-pkbox > input { position: absolute; opacity: 0; width: 0; height: 0; }
.pkbox-box {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  border: 2px solid var(--line-strong);
  border-radius: 5px;
  background: var(--surface);
  box-sizing: border-box;
}
/* CHECKED = SOLID. It was a two-gradient tick that rendered as a diagonal wedge
   the user read as a pick axe; drawing a checkmark out of linear-gradients at
   20px was never going to be legible. A filled square is unambiguous at any size
   and needs no glyph, no font and no icon file.
   The fill is INK, not --accent: --accent is green and on this board green means
   a CORRECT PICK, so an accent-filled box claimed a result before kickoff. */
/* GREEN WHEN SELECTED, on the user's explicit instruction (July 31 2026: "make
   the fill color green instead of black when a pick is selected"). Noted for
   whoever reads this later: green also means a CORRECT PICK on the outcome
   scale, so a ticked box and a won cell now share a colour. That was raised and
   overruled, which is the right call to be the owner's — the outcome lives on
   the CELL behind the box, so the two still differ in shape and position. */
.pickcell.-pkbox > input:checked + .pkbox-box {
  background: var(--accent);
  border-color: var(--accent);
}
.pickcell.-pkbox > input:focus-visible + .pkbox-box {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}
/* A locked-in pick with no input left still has to read as chosen: same solid
   square, muted, so a locked column looks settled rather than empty. */
.pkbox-mark {
  width: 24px;
  height: 24px;
  border-radius: 5px;
  background: var(--text-faint);
  font-size: 0;
}
/* INSIDE the box: it is the box's own label, so it cannot move the box. */
.pkbox-wk {
  font-size: 0.58rem;
  font-weight: 800;
  letter-spacing: 0.02em;
  color: var(--red);
  line-height: 1;
}
/* USED = FADED, the disabled-button look — never a line through it and never a
   grey fill (a fill on this site means an OUTCOME). */
.pickcell.-pkbox.-used .pkbox-box { opacity: 0.4; border-style: dashed; }

/* ONE COUNTDOWN, IN THE TOP BAR (user, July 31 2026: "get rid of the 'locks in'
   shit... everywhere... put it in one place.. the very top header, if there is
   room right justified but left of the person's name... Next game kicks in:").
   It replaced a per-game chip on all 16 rows, which restated the same fact
   sixteen times when only the soonest of them ever mattered. */
.topnav-kick {
  margin-left: auto;
  display: flex;
  align-items: baseline;
  gap: 6px;
  color: #dfeee2;
  font-size: 13px;
  white-space: nowrap;
  padding-right: 6px;
}
.topnav-kick-val { font-weight: 800; color: #fff; font-variant-numeric: tabular-nums; }
.topnav-kick-val.cd-soon { color: var(--gold-bright, #ffc53d); }
/* The phone has no room for it beside the pool switcher and the account menu,
   and the board still says which games are open by having a box in them. */
@media (max-width: 860px) { .topnav-kick { display: none; } }

/* THE COLUMN'S PICK, AT THE TOP OF THE COLUMN. Upright, not rotated: a helmet
   turned on its side is unreadable, and "VS DEN" is three tokens the eye should
   take in at once. The entry name above it stays rotated because it is prose;
   this is data. */
/* A FIXED BOX, PICKED OR NOT. The height is stated rather than derived so the
   header cannot change size when a pick is made or removed — every column's
   boxes stay on the same rows through any amount of clicking. */
.vhead-pick {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-end;
  gap: 1px;
  padding-top: 4px;
  border-top: 1px solid var(--line);
  width: 100%;
  /* 72px: the differential chip is five short lines now — helmet, team, opp,
     margin, SCORE (user, Aug 10 2026: "the score is cutoff") — and 64 clipped
     the last one. Same box for every column, so nothing shears. */
  height: 72px;
  box-sizing: content-box;
}
.vhead-pick.-empty { visibility: hidden; }
/* A MISSED week's header slot says so (user, Aug 10 2026: "'NO PICK' needs to
   show on the picks page when applicable") — the standings' red marker in the
   exact .vhead-pick footprint, so the header geometry never moves. */
.vhead-pick.-miss { justify-content: center; }
.vhead-pick.-miss .chip.-miss { background: var(--oc-loss-bg); font-size: 0.62rem; }
.vhead-pick .helmet { width: 22px; height: 22px; }
.vhead-abv { font-size: 0.68rem; font-weight: 800; letter-spacing: 0.02em; }
.vhead-opp { font-size: 0.56rem; font-weight: 700; color: var(--text-faint); white-space: nowrap; }
/* HOW THIS PICK IS DOING. Live shows the running score; final shows the pool's
   own currency — the margin in a differential pool, W/L in a knockout. Same two
   colours as everywhere else, on the text rather than behind it. */
.vhead-res {
  font-size: 0.64rem;
  font-weight: 900;
  line-height: 1;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* The differential header's score: its OWN line under the margin, one step
   quieter. Inline beside the margin it was wider than the 44px column and
   clipped (user, Aug 10 2026: "the score is cutoff"). */
.vhead-res .vhead-sc { display: block; font-size: 0.56rem; font-weight: 700; opacity: 0.8; line-height: 1.1; margin-top: 1px; }
.vhead-res.-w { color: var(--oc-win-ink, #17420a); }
.vhead-res.-l { color: var(--oc-loss-ink, #5a140c); }
.vhead-res.-live { color: var(--text); }
/* The header is a column now: the rotated name on top, the pick underneath. */
.pickcell.-vhead { flex-direction: column; justify-content: flex-end; }
/* AN ELIMINATED ENTRY'S HEADER (task #86). Same 64px footprint as .vhead-pick, so
   the header row never changes height, but it shows a red OUT badge in place of a
   pick chip and the rotated name reads struck-through. The stale future pick is
   gone with it — an out entry advertises nothing to pick. */
.vhead-out {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding-top: 4px;
  border-top: 1px solid var(--line);
  width: 100%;
  min-height: 34px;
  box-sizing: content-box;
  color: var(--red, #c62828);
  font-size: 0.72rem;
  font-weight: 900;
  letter-spacing: 0.05em;
}
.vhead-out-wk { font-size: 0.54rem; font-weight: 800; letter-spacing: 0.02em; }
.pickcell.-vhead.-elim .vhead-rot {
  color: var(--red, #c62828);
  text-decoration: line-through;
  opacity: 0.85;
}
/* The dead column's LOST pick (its elimination week) must stay full-strength —
   it is the record of what knocked the entry out (user: "you can't make the week
   one pick disappear"). Future picks are cleared server-side, so there is nothing
   left to dim; data-elim stays as the hook for the red header treatment. */

/* THE BUYBACK, IN THE DEAD COLUMN ITSELF — NOT a banner (user, repeatedly: "I
   DON'T WANT FUCKING LAYOUT BREAKING BANNERS"). A compact red button under the OUT
   badge. The column is ~44px, so the label wraps and the button fills it. */
/* EXACTLY the .vhead-pick footprint (64px + the same border-top), because this
   REPLACES the chip in a dead column's header — same slot, same height, so every
   column's header stays identical and the rotated names align. */
.vhead-bb {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding-top: 4px;
  border-top: 1px solid var(--line);
  width: 100%;
  /* Tracks .vhead-pick's 72px — the two slots must stay the same box. */
  height: 72px;
  box-sizing: content-box;
}
/* FILLS ITS WHOLE SLOT AND READS AT ARM'S LENGTH. The first cut was a 37x30px
   pill with 9.6px text (user: "that buy back button is ugly, almost unreadable").
   The slot is 72px tall — use all of it: one bold uppercase word per line. */
.vhead-bb-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  padding: 4px 2px;
  border: 0;
  border-radius: 8px;
  background: var(--red, #c62828);
  color: #fff;
  font-size: 12px;
  font-weight: 800;
  line-height: 1.25;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  white-space: normal;
  box-sizing: border-box;
}
.vhead-bb-btn:hover { background: #a81f1a; color: #fff; }
/* THE ENTRY NAME, HORIZONTAL, ACROSS ITS SLOTS. */
/* THE TITLE CELL. One cell across the game-info and team columns, so there is
   no vertical rule inside it, and no rule above it either — it is the top of
   the board, not a row within it. */
/* THE FILLER CELLS carry the header/body split out to the page edge. They hold
   no content — they exist so the rule under the header is one line, not one that
   stops at the last entry column. */
.pickcell.-fillhead { background: var(--head-band); border-bottom: 2px solid var(--line-strong); }
/* THE WHOLE PICKS HEADER BAND IS THE LIGHT-GREEN STICKY BAR (user, Aug 2026: "the
   light green header is sticky ... the white scrollable page is the detail"). Every
   band cell — the title, the entry-name headers, their group row — carries the same
   --head-band as the week bar above it, so header and week bar read as one bar. */
.pickcols > * > .pickcell.-vhead,
.pickcols > * > .pickcell.-egroup,
.pickcols > * > .pickcell.-head { background: var(--head-band); }
.pickcell.-fillbody { background: var(--bg); border-bottom: 0; }
/* THE RULE ITSELF: the last header row, all the way across. */
.pickcols > * > .pickcell.-vhead,
.pickcols > * > .pickcell.-boardtitle { border-bottom: 2px solid var(--line-strong); }

.pickcell.-boardtitle {
  display: flex;
  /* TOP of the cell, not the bottom (user, July 31 2026: "week 2 picks should be
     at the top of the cell not the bottom"). */
  align-items: flex-start;
  padding: 6px 10px 8px;
  border-top: 0;
  background: var(--head-band);
}
.boardtitle-h {
  margin: 0;
  font-size: clamp(20px, 2.4vw, 30px);
  line-height: 1.1;
  font-weight: 800;
  white-space: nowrap;
}
.pickcell.-egroup {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 5px 4px;
  border-bottom: 1px solid var(--line);
}
.egroup-name {
  font-weight: 800;
  font-size: var(--fs-sm);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* CENTRED, not left-justified: the rotated text is a flex item in a column, so
   align-items is what centres it across the 44px track. */
.pickcell.-vhead .vhead-rot { align-self: center; }
/* An empty foot row must not draw a band across the board. */
.pickcols > * > .pickcell.-foot:empty { padding: 0; min-height: 0; border-bottom: 0; }

/* THE STANDINGS BOARD ON A PHONE: NAME, POINTS, WEEK — and the week column has
   to actually FIT. The name column is capped at 46vw for wide screens; on a
   phone that plus the points column left the week squeezed against the right
   edge with its content clipped and one arrow off the screen entirely (user,
   Aug 1 2026: "standings are still a fuck show"). The week is the column you
   came to read, so it gets the remaining width, and the name gives some back —
   it still ellipsises rather than wrapping, exactly as before. */
@media (max-width: 700px) {
  /* THE WEEK COLUMN NEEDS ROOM FOR WHAT IS IN IT. A differential cell carries a
     helmet, a three-letter code, the opponent ("VS CAR") and a score — about
     96px — and on a phone it was getting whatever the name and points columns
     left over, which was not enough, so every cell was clipped mid-word at the
     right edge (user, Aug 1 2026: "standings still look like shit for
     differential on mobile"). The week is the column you came to read, so it
     states its floor and the name yields. */
  .tablewrap td.name, .tablewrap th.name { max-width: 30vw; }
  .sgrid td.kt, .sgrid th.kt { width: 46px; min-width: 46px; }
  /* THE WEEK COLUMN TAKES WHAT IS LEFT. With the name and points columns at
     fixed widths and only one week on screen, the table's columns added up to
     less than the card and the remainder sat there as a white strip running the
     full height down the right (user, Aug 1 2026: "there should not be the
     fucking white gap on the right side running down the standings").
     table-layout:fixed makes the declared widths authoritative and hands every
     remaining pixel to the one column that did not declare one — the week. */
  .sgrid { width: 100%; table-layout: fixed; }
  .sgrid th.wk, .sgrid td[data-wk] { min-width: 96px; width: auto; }
  /* NO SCROLLBAR AT THE BOTTOM. The board pages by WEEK on a phone — one week on
     screen, arrows in its header — so there is nothing left to scroll to, and a
     horizontal scrollbar under the grid is both useless and a second way to move
     the same board (user, Aug 1 2026: "i told you to get rid of that fucking
     scroll bar at the bottom of the standing page on mobile"). */
  .tablewrap.gridscroll { overflow-x: hidden; }
  .sgrid th.wk.-paged {
    min-width: 0;
    width: auto;
    white-space: nowrap;
  }
  /* The arrows keep their tap target but stop stealing the header's width. */
  .sgrid th.wk .wkpage { width: 38px; min-width: 38px; }
}

/* THE WEEK-COLUMN ARROWS on the standings board. Same idea as the entry-header
   arrows below, same tap-target floor: they sit INSIDE the week's own <th>, so
   the control is on the column it moves and costs no extra row. The generic
   sideways pager is skipped for this grid, so these are the only arrows on it. */
.sgrid th.wk .wkpage {
  width: var(--tap);
  min-width: var(--tap);
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.sgrid th.wk .wkpage:disabled { opacity: 0.3; cursor: default; }
.sgrid th.wk .wkpage:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
/* The paged header holds its arrows either side of the week number. */
.sgrid th.wk.-paged {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
  min-width: 140px;
}

/* THE ENTRY-HEADER ARROWS. They page through ENTRIES from inside the entry's
   own header cell, so the control sits on the thing it controls and costs no
   extra row above the board. Rendered only when there is more than one entry.
   A REAL TAP TARGET: 44px on both axes, the floor every control on this site
   keeps. The first version was 22px wide — fine for a mouse, useless for a
   thumb (user, Aug 1 2026: "THOSE ARROWS ARE WAY TO TINY FOR A FINGER TRYING TO
   TAP THEM ON MOBILE") — and it had no surface, so it read as punctuation
   rather than as a button. */
.pickcell.-boardtitle .colpage,
.pickcell.-egroup .colpage,
.pickcell.-head .colpage {
  flex: 0 0 auto;
  width: var(--tap);
  min-width: var(--tap);
  height: var(--tap);
  min-height: var(--tap);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  /* NEITHER WHITE NOR GREEN. White read as an unselected week chip, green as a
     selected one (user, Aug 1 2026: "make the arrow buttons not white because
     they look like the chips... and don't make them green either"). A filled
     slate button is obviously a control and obviously not a week. */
  border: 1px solid #7b8a80;
  border-radius: 10px;
  background: #64736a;
  color: #fff;
  font-size: 26px;
  line-height: 1;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.pickcell.-egroup .colpage:active,
.pickcell.-boardtitle .colpage:active,
.pickcell.-head .colpage:active { background: #55635b; }
.pickcell.-egroup .colpage:disabled,
.pickcell.-boardtitle .colpage:disabled,
.pickcell.-head .colpage:disabled { opacity: 0.28; cursor: default; }
.pickcell.-egroup .colpage:focus-visible,
.pickcell.-boardtitle .colpage:focus-visible,
.pickcell.-head .colpage:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
/* The header cell holds the arrows either side of whatever it already showed.
   -egroup is the entry-level cell that spans an entry's slot columns; -head
   covers the single-slot case where there is no egroup row. */
/* The title cell stacks: its heading on top, the entry carousel beneath it. */
.pickcell.-boardtitle:has(.colpage-bar) {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  gap: 6px;
}
.colpage-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  width: 100%;
}
.pickcell.-egroup:has(.colpage),
.pickcell.-head:has(.colpage) {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2px;
}

/* The entry the arrows point at, named between them. Only on a phone, where
   the arrows sit in the title cell and there is room to say it. */
.colpage-say {
  flex: 1 1 auto;
  min-width: 0;
  text-align: center;
  font-weight: 700;
  font-size: 0.95rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* THE PHONE HAS NO SLACK TO SPEND ON A FILLER TRACK. On a wide screen the
   trailing 1fr is empty page and carrying the header's rule across it is free.
   At 375px it was eating ~100px while the team column, held to its fixed width,
   clipped "Texans" mid-word and the board sat in a corner with a dead white
   band beside it (user, July 31 2026: "what a fucking shit show on mobile").
   So on a phone the TEAM column takes the slack and the filler shrinks to the
   width of its own closing rule — it still draws the line that closes the last
   entry, it just no longer reserves a column of nothing to do it in. */
@media (max-width: 700px) {
  /* THE NAME COMES BACK ON A PHONE. The rule above swaps the nickname for the
     three-letter code below 720px, which was right when six entry columns were
     fighting over the width. A phone now shows ONE entry column and the team
     track takes all the slack — about 240px of a 375px screen — so there is
     room for the word, and "Seahawks" beats "SEA" when it costs nothing.
     Source order matters: this block sits after that one, so it wins. */
  /* "Commanders" is the longest nickname in the league and it is what sets this
     size: with an entry's BOTH picks on screen (paging is by entry, never by
     column) there is no room for it at the desktop size, and an ellipsis on a
     pick board is worse than a slightly smaller word. */
  .tmrow-name { display: inline; font-size: 0.88rem; }
  .tmrow-code { display: none; }
  /* EVERY PIXEL OF THE WIDTH (user, Aug 1 2026: "THERE SHOULD BE ALMOST NO
     MARGIN/PADDING ON THE SIDES FOR MOBILE.. YOU NEED ALL THE FUCKING SCREEN
     SPACE YOU CAN GET"). The board IS the page here; side padding is width
     taken from team names and checkboxes in order to draw nothing. */
  .pickcols {
    /* min-width:min-content is what the desktop board uses so its tracks never
       overflow their own box. On a phone that floor fights width:100%: it is
       computed from every column's content, so the box would not shrink into
       the space freed by trimming the side padding and the page scrolled
       sideways by exactly that much. The team track keeps its own floor, so
       nothing can collapse to nothing. */
    min-width: 0;
    /* FILL THE PHONE. The board is width:fit-content everywhere else, because
       its own background is the divider colour and a stretched grid would paint
       a dark slab past the last entry. That reasoning does not hold here: this
       layout has a FLEXIBLE team track, so any width the grid is given goes to
       the team names rather than to empty background.
       Left at fit-content the 1fr resolved to its 124px minimum, so the board
       stopped ~95px short of the right edge AND clipped every name to
       "Rave...", "Pant...", "Cow...". Stretching it hands that 95px straight to
       the team column, which is the one column on this screen that wants it. */
    width: 100%;
    grid-template-columns:
      var(--pc-spine)
      minmax(var(--pc-teams-min), 1fr)
      repeat(var(--pc-cols), minmax(var(--pc-colmin), var(--pc-colmin)))
      3px;
  }
}

/* "+ Add entry" — ONE control, in the top bar, on the pool you are in. It used
   to sit on the picks page AND on every pool card on the home page. */
.topnav-addentry {
  flex: 0 0 auto;
  margin-left: auto;
  padding: 6px 12px;
  border: 1px solid rgba(255, 255, 255, 0.3);
  border-radius: 8px;
  color: #fff;
  text-decoration: none;
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
  min-height: var(--tap);
  display: inline-flex;
  align-items: center;
}
/* Only ONE of the two right-hand items may claim the free space, or they fight
   over it and the countdown jumps as the button appears. */
.topnav-addentry ~ .topnav-kick { margin-left: 10px; }
/* THE PHONE STILL NEEDS IT. Hiding the control outright left a phone with no
   way to add an entry at all (user, July 31 2026: "where is the 'add entry' on
   mobile?") — the word is what does not fit, not the button. Keep the button,
   drop the label to a plus. */
@media (max-width: 860px) {
  .topnav-addentry {
    padding: 0;
    width: var(--tap);
    justify-content: center;
    font-size: 0;
  }
  .topnav-addentry::before { content: '+'; font-size: 24px; font-weight: 700; line-height: 1; }
}

/* ==========================================================================
   STANDINGS DENSITY (user, July 31 2026: "there is so god damn much wasted
   space... why is the points column so wide, why isn't the number centered,
   since the column is so god damn tall, it would be easy to put the person's
   last name on a second line so the first column didn't need to be so fucking
   wide")
   ========================================================================== */

/* THE NAME WRAPS TO TWO LINES. The rows are tall enough for two already, so
   letting "Mike Rohde" break after the first name buys back most of the first
   column's width for free. The name was nowrap + ellipsis, which forced the
   column to be as wide as the longest full name on the board. */
.sgrid td.kn .kb-name {
  white-space: normal;
  overflow-wrap: break-word;
  min-width: min-content;
  max-width: none;
  overflow: visible;
  text-overflow: clip;
  line-height: 1.15;
  display: block;
}
/* The column that holds it must be willing to be as wide as the name needs. */
.sgrid td.kn { width: 1%; }
/* ==========================================================================
   A NAME IS NEVER CUT OFF AND NEVER BROKEN MID-WORD — EITHER BOARD, EVERY
   WIDTH, PHONE AND DESKTOP (user, Aug 26 2026, after three partial fixes:
   "FIX IT FOR GOD DAMN EVERYTHING, PHONE AND DESKTOP").
   The name cell carries overflow:hidden for its sticky-column behaviour, so
   the ONLY way to guarantee nothing is cut is to stop the column from being
   squeezed below what it holds. min-content is that width exactly — rank chip,
   picture, and the longest single word of the name — and it is measured rather
   than guessed, so it holds for "Rohde" and for "Rocha-Johnson" alike.
   min-width beats max-width in CSS, so the vw caps above cannot re-crush it.
   ========================================================================== */
.sgrid:not(.koobgrid) td.name .namewrap { min-width: min-content; }
.sgrid:not(.koobgrid) td.name, .sgrid:not(.koobgrid) th.name { min-width: min-content; }
@media (max-width: 700px) {
  /* Under table-layout:fixed a cell's min-width is ignored — the declared
     column width is authoritative — so the phone board states a share instead
     of relying on the content floor above. */
  .sgrid td.kn, .sgrid th.kn { width: 42%; }
  .sgrid td.name, .sgrid th.name { width: 42%; max-width: none; }
}
.sgrid td.kn .kb-cell { gap: 8px; }
/* THE POINTS COLUMN IS A NUMBER, NOT A PARAGRAPH. It was sized to fit the word
   "Points" plus an "N back" note; both fit in far less. Centred, because a
   lone integer under a centred heading reads as a column of digits rather than
   a ragged left edge. */
.sgrid th.kt, .sgrid td.kt {
  width: 1%;
  white-space: nowrap;
  text-align: center;
}
.sgrid td.kt .kb-pts { display: block; text-align: center; }
.sgrid td.kt .kb-back { display: block; text-align: center; font-size: 0.68rem; }

/* MOBILE GETS NO S/M/L. A phone has one sensible density, and offering three
   was offering the reader a way to make it worse (user: "mobile doesn't get
   s/m/l size selectors, they get a layout optimized it fit on the screen with
   no side scrolling"). The view toggles beside it stay — those change WHAT is
   shown, not how big it is. */
@media (max-width: 720px) {
  .gsz-ctl .gsz-ctl-label,
  .gsz-ctl .gsz-btn:not(.-wide) { display: none; }
}

/* WEEK-CHIP PAGING ARROWS. The chip strip used to be a bare horizontal
   scroller; it now pages a full screenful at a time (user, July 31 2026: "they
   should not scroll one week at a time, they should jump a 'page'"). The
   arrows are hidden by the script unless the track really overflows, so a
   season that fits shows none. */
/* THE CHIP ROW STAYS ON TOP AND STAYS HIT-TESTABLE. It sits ABOVE the page
   content in the document, so a sticky element further down with a higher
   stacking order can be painted over it and swallow the clicks — which is what
   a chip that "does nothing" looks like. A stacking context of its own, above
   the sticky sub-header, means nothing can cover it. */
/* ONLY THE DELTAS FOR THE PAGING ARROWS. This block used to restate
   `position: relative`, which lands AFTER the .week-nav rule above and
   therefore overrode its `position: sticky` — the bar stopped sticking under
   the top nav and dropped into normal flow, leaving a gap where its sticky slot
   had been. Never re-declare position here: the stickiness belongs to the rule
   that owns the bar, and this one only adds the arrow layout.
   The z-index is raised to sit above the picks sub-header, which is what made a
   week chip unclickable when that header was painted over it. */
.week-nav { gap: 4px; z-index: 40; }
.week-pg {
  flex: 0 0 auto;
  width: 30px;
  min-height: var(--tap);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
}
.week-pg[hidden] { display: none !important; }
.week-pg:disabled { opacity: 0.32; cursor: default; }
.week-pg:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
/* The track keeps scrolling programmatically, but never shows a scrollbar and
   is not draggable: the arrows are the interface. */
.week-nav .season-nav-track {
  flex: 0 1 auto;
  overflow-x: auto;
  scroll-behavior: smooth;
  /* NEVER HALF A CHIP (user, Aug 1 2026 and again Aug 25 2026 — it never
     worked). Snapping alone could not fix it: scroll-snap-align:start aligns
     the LEFT edge, so whichever chip straddled the RIGHT edge stayed sliced,
     measured at 560px as "W12: 18px of 45". The cure is the VIEWPORT: round its
     width down to a whole number of chip pitches, so the strip physically
     cannot show a fraction of one. Snapping then keeps the left edge honest
     while scrolling. */
  --chip-w: 44px;
  margin-right: auto;
  scroll-snap-type: x mandatory;
}
.week-nav .season-nav-track > * { scroll-snap-align: start; }
.week-nav .season-nav-track::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* GENERIC HORIZONTAL PAGING (public/app.js hpage()). Every side-scroller on the
   site — the standings grid, the picks board, the entry tabs — is wrapped in
   this and moved a screenful at a time by the arrows. Nothing scrolls sideways
   by drag any more, which is the point: a hidden overflow you have to discover
   is not navigation. */
/* THE WRAPPER MUST NOT STRETCH ITS BOX. flex:1 1 auto here overrode the
   width:fit-content on .pickcols-wrap and blew the board's white card back out
   to the full page width, with nothing in the right-hand half — the exact thing
   that was already fixed once (user, July 31 2026: "DIDN'T I FUCKING SAY ONCE
   ALREADY TO GET RID OF THE WHITE SPACE ON THE RIGHT"). 0 1 auto lets the box
   be its content's width and still shrink when the content is wider than the
   page; justify-content keeps it and its arrows packed left. */
.hpage { display: flex; align-items: stretch; justify-content: flex-start; gap: 4px; min-width: 0; }
.hpage > .hpage-box {
  flex: 0 1 auto;
  min-width: 0;
  max-width: 100%;
  /* Still a scroll container — the arrows drive it — but with no visible bar
     and no drag affordance. */
  overflow-x: auto;
  scroll-behavior: smooth;
  scrollbar-width: none;
}
.hpage > .hpage-box::-webkit-scrollbar { display: none; }
.hpage-btn {
  flex: 0 0 auto;
  align-self: center;
  width: 30px;
  min-height: 64px;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 19px;
  line-height: 1;
  cursor: pointer;
}
.hpage-btn[hidden] { display: none !important; }
.hpage-btn:disabled { opacity: 0.32; cursor: default; }
.hpage-btn:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
@media print { .hpage-btn { display: none !important; } }

/* ==========================================================================
   THE PICKS SUB-HEADER STICKS (user, July 31 2026: "Week 2 picks and the table
   header become kind of a 3rd sticky sub header").
   The board is 30-odd rows tall, so without this you scroll to row 20 and can
   no longer see which entry a column belongs to — which is the one thing you
   need to know to tick the right box.
   Two layers, in order: the page title, then the grid's own header row beneath
   it. --pickhead-h is measured and set by app.js, because the title's height
   depends on the pool name and the font, and a guessed constant would either
   overlap the first row or leave a gap.
   ========================================================================== */
/* IT STICKS BELOW THE BARS ABOVE IT, NOT AT THE VIEWPORT TOP. top:0 put it
   underneath the sticky top nav (and the week bar under that), so the title
   slid behind them instead of coming to rest below. The offset is the height of
   whatever is actually above it: the nav always, plus the week bar on the pages
   that render one. */
body.picks-sticky .page-head {
  position: sticky;
  top: var(--topnav-h);
  z-index: 30;
  background: var(--bg);
  padding-top: 6px;
  margin-bottom: 6px;
}
body:has(.week-nav).picks-sticky .page-head {
  top: calc(var(--topnav-h) + var(--weekbar-h));
}
/* FLUSH. The board's header is the next band under the week bar, so there is no
   page padding, no page-head margin and no card gap between them. */
/* FLUSH, and every contributor to that gap named explicitly rather than hoping
   one of them was the culprit: the shell's top padding, the page header's box,
   the week bar's own bottom margin, and the wrapper's. */
/* THE PAGE UNDER THE HEADER IS WHITE, not the site's grey field: on this page
   the games ARE the document. */
body.picks-sticky { background: var(--surface); }
/* THE BOARD'S BODY IS WHITE — ONLY THE HEADER BAND IS TINTED. The spine cells
   and the pick cells below the header were painting the same --surface-2 as the
   band, so column 1 and column 3 read as if the header ran all the way down the
   page (user, Aug 1 2026: "why is the background in column 1 the same fucking
   color as the header? it should be white... same with the BG in column 3").
   The band keeps its tint; everything under it is the page. */
.pickcols > * > .gamecard.-spine,
.pickcols > * > .pickcell.-spine,
.pickcols > * > .pickcell.-tmrow,
.pickcols > * > .pickcell.-pkbox,
.pickcols > * > .pickcell.-byerow,
.pickcols > * > .pickcell.-fillbody { background: var(--surface); }
/* The spine cell is a .gamecard, not a .pickcell — the first version of this
   rule named the wrong element and matched nothing, which is why column 1 kept
   the header's tint all the way down the page. */
.pickcols > * > .gamecard { background: var(--surface); }
body.picks-sticky .shell { padding-top: 0 !important; }
/* THE SIDE GUTTER STAYS FOR NOW, AND HERE IS WHY.
   Asked twice to take the mobile side padding to zero. Tried twice, measured
   both times: the page then scrolls sideways — 368px of content in a 360px
   viewport — and no-sideways-scroll is a hard rule that outranks reclaiming
   6px. The second attempt also ruled out the suspected cause (the wrapper being
   wider than the board; it now shrinks to fit and the overflow is unchanged at
   exactly 8px). Something else inside is sized against the viewport rather than
   its container. When that is found, this goes to zero for free.
   ORIGINAL NOTE:
   Asked for "almost no margin/padding on the sides for mobile", I took it to
   zero and the page began scrolling sideways: 368px of content in a 360px
   viewport, and 364 at 2px. Something inside the board is sized against the
   VIEWPORT rather than the padded box, so the shell's gutter is what has been
   holding it in. No-sideways-scroll is a hard rule and it wins over reclaiming
   6px, so the gutter stays until the real offender is found and fixed — at
   which point this can go to zero for free. Measured, not assumed. */
/* ROOM UNDER THE BOARD. Ending hard against the viewport bottom reads as the
   page being cut off; this is deliberate white space after the closing rule. */
body.picks-sticky .pickcols-wrap { padding-bottom: 0; margin-bottom: 72px; }

/* THE BAND ITSELF, EDGE TO EDGE — same reasoning as the rule below it, and the
   same mechanism. The board is fit-content on purpose: its own background is
   the divider colour, so stretching the grid to the window paints a slab of
   dark green beside the last entry. That left the header painted only as far as
   the board reached and white from there to the edge — a header that stopped
   two thirds of the way across the screen (user, July 31 2026: "header should
   be green all the way across", measured: paint ended at 661 of 1905).
   A fixed bar the height of the band carries the header's surface the rest of
   the way. It sits BELOW the cells (they are z-index 20) so every cell still
   paints its own background over it, and it is the same --pickband-h app.js
   already measures for the rule. */
body.picks-sticky::before {
  content: '';
  position: fixed;
  left: 0;
  right: 0;
  top: var(--topnav-h);
  height: var(--pickband-h, 0px);
  background: var(--head-band);
  z-index: 19;
  pointer-events: none;
}
body:has(.week-nav).picks-sticky::before {
  top: calc(var(--topnav-h) + var(--weekbar-h));
}
/* ONE LINE, ONE THICKNESS. The fixed rule below runs the width of the window,
   and the bottom row of header cells drew its own border-bottom on top of it —
   so the line was 2px past the board and 3px across it, and visibly stepped at
   the boundary (user: "the horiz dividing line changes thickness"). The band's
   bottom edge is the fixed rule's job alone. */
body.picks-sticky .pickcols > * > .pickcell.-vhead,
body.picks-sticky .pickcols > * > .pickcell.-boardtitle,
body.picks-sticky .pickcols > * > .pickcell.-egroup,
body.picks-sticky .pickcols > * > .pickcell.-head,
body.picks-sticky .pickcols > * > .pickcell.-fillhead {
  /* ONE LINE, ONE THICKNESS, ALL THE WAY ACROSS. The fixed rule below spans the
     window; every cell in the band was ALSO drawing its own bottom border on
     top of it, so the line came out 2px where the band was empty and 3px over
     each cell that had one — visibly stepping as it crossed the board (user,
     Aug 1 2026: "THE LINE ISN'T A CONSISTENT THICKNESS DIVIDING THE HEADER FROM
     THE REST"). Listing every band cell, not just two of them: the title and
     the entry-name row are in that band too. */
  border-bottom: 0;
}

/* THE DIVIDING RULE, EDGE TO EDGE ACROSS THE WHOLE VIEWPORT — under the scores
   rail included. It cannot be drawn by the board: the board lives in a grid
   track beside the rail, so any border it draws stops at the rail's edge. This
   is a fixed 2px bar at the bottom of the header band instead, which is the only
   way a single line can cross both. --pickband-h is measured by app.js because
   the band is one row in a single-pick pool and two in a multi-pick one. */
body.picks-sticky::after {
  content: '';
  position: fixed;
  left: 0;
  right: 0;
  top: calc(var(--topnav-h) + var(--pickband-h, 0px));
  height: 2px;
  background: var(--line-strong);
  z-index: 26;
  pointer-events: none;
}
body:has(.week-nav).picks-sticky::after {
  top: calc(var(--topnav-h) + var(--weekbar-h) + var(--pickband-h, 0px));
}
body.picks-sticky .page-head { margin: 0; padding: 0; border: 0; }
body.picks-sticky .page-head:empty { display: none; }
/* NOTICES ON THE PICK PAGE FLOAT — they never push the board (user, Aug 2026: the
   header "always pushed down and the line goes right through the picks"). Anything
   in-flow above the board shifts it below the FIXED band/divider, which is exactly
   how the divider ended up slicing the pick chips. A floating notice overlays the
   header instead: zero layout shift, message still readable, gone on next nav. */
/* ...and on the STANDINGS board too (Aug 11 2026): its header band is a FIXED
   overlay, so an in-flow notice rendered UNDERNEATH it — the /grid redirect's
   error came back and nobody ever saw it (user: the edit "goes through the
   motions but never changes it" — the refusal was invisible). Same float. */
body.picks-sticky .notice,
body:has(.tablewrap.gridscroll) .notice {
  position: fixed;
  top: calc(var(--topnav-h) + var(--weekbar-h) + 10px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 46;
  max-width: min(92vw, 560px);
  box-shadow: var(--shadow-lg, 0 8px 24px rgba(15, 23, 18, 0.18));
}
body.picks-sticky:not(:has(.week-nav)) .notice,
body:has(.tablewrap.gridscroll):not(:has(.week-nav)) .notice { top: calc(var(--topnav-h) + 10px); }
body.picks-sticky .week-nav { margin-bottom: 0; }
body.picks-sticky .shell-main > :first-child { margin-top: 0; }
body.picks-sticky .pickcols-wrap { margin-top: 0; }
/* AND THE DEAD SPACE UNDER THE WEEK BAR GOES — ALL of it. 28px became 10px, and
   the 10px still read as a white slit between the week bar and the sticky title
   band (user, Aug 2026: one flush green header). Zero: the title bands carry
   their own inner padding. */
body:has(.week-nav) .shell { padding-top: 0; }

/* THE ENTRIES HEADER STICKS — AND THIS IS WHAT MAKES IT POSSIBLE.
   A sticky element resolves against its nearest SCROLL CONTAINER, and
   .pickcols-wrap is one (it sets overflow-x for the paging arrows, which makes
   overflow-y compute to auto as well). So a header inside it could never stick
   to the page no matter what offset it was given — it could only stick inside a
   box that was never taller than its own content, i.e. never.
   Giving the board a real height makes it a genuine vertical scroller: the
   board scrolls under its own pinned header, the page itself barely moves, and
   the header is fixed exactly where it needs to be. The height is the viewport
   minus the two bars above it and a little breathing room. */
/* THE BOARD IS NOT A SCROLL CONTAINER, and that is the whole fix.
   A sticky element resolves against its nearest scroll container. .pickcols-wrap
   was one — it set overflow-x for the paging arrows, and CSS then computes
   overflow-y to auto as well — so the header could only ever stick INSIDE the
   board. Capping the board's height to make that work put a scrollbar in the
   middle of the page and still failed wherever the cap did not resolve.
   It does not need to scroll any more: the entry columns are a fixed 44px, the
   filler track absorbs the leftover width, and a screen too narrow for the
   columns switches to one-entry-at-a-time instead. With no scroll container in
   the way, the header sticks to the PAGE, under the two bars above it, exactly
   like every other sticky thing on the site. */
body.picks-sticky .pickcols-wrap {
  overflow: visible;
  max-height: none;
}
/* FULL BLEED — the board runs to the edge of the page, so the rule under its
   header does too. It cancels the shell's horizontal padding rather than
   guessing a width, so it lands exactly on the viewport edge.
   WITH THE SCORES RAIL OPEN it stops where main stops, which is what was asked
   for ("it can stop at the scores sidebar if it is not hidden") — main is a grid
   track beside the rail, so cancelling the padding cannot reach past it. With
   the rail closed the shell is one column and the same rule reaches the edge. */
/* NO FULL-BLEED NEGATIVE MARGINS. Pulling the board out past the shell's padding
   made the PAGE scroll sideways and clipped its own left edge — the title and the
   kickoff column were cut off. It was also unnecessary: the edge-to-edge rule the
   user asked for is drawn by the fixed bar below, which crosses the whole
   viewport including the scores rail. The board stays inside its container; only
   the LINE spans the page. */
body.picks-sticky .pickcols-wrap {
  margin-left: 0;
  margin-right: 0;
  width: 100%;
  max-width: 100%;
}
/* The header cells are grid items, so each one sticks on its own — the row
   travels as a unit because they all share the same offset. */
/* top:0, NOT the page-head's height.
   .pickcols-wrap sets overflow-x for the arrow paging, which makes IT the
   scroll container these sticky cells resolve against — so an offset measured
   from the top of the PAGE pushed the header row that far down INSIDE the
   board, leaving its own row empty and showing the dark grid background through
   it while covering the first game. Measured, not reasoned: the 1280px render
   came back with a checkbox inside the header band in every column.
   Pinned to the top of its own scroll box it is correct by construction. Making
   this stick to the PAGE would mean the wrapper could not be an overflow
   container, which is what the horizontal paging drives — a bigger change than
   this, and not one to make halfway. */
/* ============ THE HEADER BAND, AND WHY IT LIVES IN ONE PLACE ============
   Everything above the dividing rule is header: the title, the cells beside it,
   the entry-group row, the slot row and the filler that carries the rule to the
   page edge. All of it pins together under the top nav and the week bar.
   THIS BLOCK WAS CORRUPTED. An earlier edit spliced a comment into the middle of
   the selector list ("...pickcols > * > /* THE TITLE CELL..."), which killed the
   rule outright — the browser threw away everything after the malformed
   selector, `top` computed to auto, and the band never stuck. That is why only
   the entry-group row appeared to pin: its own rule further down still parsed.
   One block, one selector list, no comments inside it. */
body.picks-sticky .pickcols > * > .pickcell.-head,
body.picks-sticky .pickcols > * > .pickcell.-boardtitle,
body.picks-sticky .pickcols > * > .pickcell.-egroup,
body.picks-sticky .pickcols > * > .pickcell.-fillhead {
  position: sticky;
  top: var(--topnav-h);
  z-index: 20;
  /* --head-band, matching the week bar and the fixed fill band behind these
     cells. This block outranks the base cell rules, so leaving --surface-2 here
     painted the title/header cells a DIFFERENT green than the band around them
     (user, Aug 2026: "i want the lighter green under week 2 picks to match the
     green all around it"). ONE band colour: --head-band, everywhere. */
  background: var(--head-band);
}
body:has(.week-nav).picks-sticky .pickcols > * > .pickcell.-head,
body:has(.week-nav).picks-sticky .pickcols > * > .pickcell.-boardtitle,
body:has(.week-nav).picks-sticky .pickcols > * > .pickcell.-egroup,
body:has(.week-nav).picks-sticky .pickcols > * > .pickcell.-fillhead {
  top: calc(var(--topnav-h) + var(--weekbar-h));
}
/* THE SECOND HEADER ROW PINS BELOW THE FIRST, NOT ON TOP OF IT.
   In a multi-pick pool the entry name and the slot number are two separate rows,
   and giving both the same offset made the slot row jump up under the entry row
   the moment the page moved — measured as 6 of 11 header cells drifting.
   --egroup-h is the entry row's real height, set by app.js; it is 0 in a
   one-pick pool, where there is no entry row and this term disappears. */
body.picks-sticky .pickcols > * > .pickcell.-vhead {
  top: calc(var(--topnav-h) + var(--egroup-h, 0px));
}
body:has(.week-nav).picks-sticky .pickcols > * > .pickcell.-vhead {
  top: calc(var(--topnav-h) + var(--weekbar-h) + var(--egroup-h, 0px));
}

/* The filler cells carry the header/body split out to the page edge: they hold
   no content and exist so the rule under the header is ONE line, not one that
   stops at the last entry column. */
/* BELOW THE LINE IS WHITE — that is the scrolling page. Above it is header. */
.pickcell.-fillhead { background: var(--head-band); }
.pickcell.-fillbody { background: var(--surface); }
/* THE BOARD CLOSES THE SAME WAY IT OPENS. The games used to just stop, with the
   foot row's odds and ends trailing off into white — a beginning with no end.
   One rule across the bottom, matching the one under the header, then real
   breathing room beneath it. It spans the board including the filler, so it runs
   to the same edge the header rule does. */
.pickcols > * > .pickcell.-foot,
.pickcols > * > .pickcell.-fillbody { border-bottom: 2px solid var(--line-strong); }
/* AN EMPTY FOOT ROW COLLAPSES. With auto-save on, the Save button is hidden and
   the foot holds nothing — but it still occupied a row, so the entry dividers
   ran on past the last game into a band of blank space and the board looked like
   it had been cut off mid-stride. Zero height when there is nothing in it; the
   closing rule sits on it either way, so the board ends cleanly right under the
   last row. A column that IS showing something there (a lock marker, a save
   message) keeps its height. */
.pickcols > * > .pickcell.-foot { background: var(--surface); }
body.js-picksave .pickcols > * > .pickcell.-foot:not(:has(.pickslot-lockednote)):not(:has([data-pickmsg]:not([hidden]))) {
  padding: 0;
  min-height: 0;
  line-height: 0;
  font-size: 0;
}
/* THE RULE ITSELF — the bottom of the header band, all the way across. */
.pickcols > * > .pickcell.-vhead,
.pickcols > * > .pickcell.-boardtitle,
.pickcols > * > .pickcell.-fillhead { border-bottom: 2px solid var(--line-strong); }

/* THE TITLE CELL. One cell across the game-info and team columns, so there is no
   vertical rule inside it and no rule above it: it is the top of the board, not
   a row within it. */
.pickcell.-boardtitle {
  display: flex;
  align-items: flex-start;
  padding: 6px 10px 8px;
  border-top: 0;
  background: var(--head-band);
}
.boardtitle-h {
  margin: 0;
  font-size: clamp(20px, 2.4vw, 30px);
  line-height: 1.1;
  font-weight: 800;
  white-space: nowrap;
}
/* The two fixed left columns already stick sideways; where the two stickies
   cross, they must sit above the row that only sticks downwards. */
body.picks-sticky .pickcols > :nth-child(1) > .pickcell.-head,
body.picks-sticky .pickcols > :nth-child(1) > .pickcell.-boardtitle,
body.picks-sticky .pickcols > :nth-child(2) > .pickcell.-head { z-index: 24; }

/* MOBILE ONE-COLUMN NAVIGATION (public/app.js). One entry on picks, one week on
   standings, with the arrows that replace the sideways scroll. Hidden on
   desktop and hidden whenever there is only one column to look at — a single
   entry needs no paging. */
.onecol-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin: 0 0 8px;
  padding: 6px 8px;
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  background: var(--surface);
}
.onecol-nav[hidden] { display: none !important; }
.onecol-label {
  flex: 1 1 auto;
  text-align: center;
  font-weight: 800;
  font-size: var(--fs-sm);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.onecol-btn {
  flex: 0 0 auto;
  width: 44px;
  min-height: var(--tap);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--surface);
  color: var(--text);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}
.onecol-btn:disabled { opacity: 0.3; cursor: default; }
.onecol-btn:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
/* With one entry column showing, the board has no reason to be wider than the
   phone — so the paging arrows on the wrapper stand down too. */
@media (max-width: 720px) {
  .pickcols-wrap, .tablewrap { max-width: 100%; }
}

/* Screen-reader-only text: visually gone, still announced. Used where a 44px
   column can only show a glyph but the meaning has to survive for anyone not
   looking at it. */
.u-sr {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
/* The closed-column marker: a lock, centred, with the reason in its tooltip. */
.pickcols .pickslot-lockednote {
  margin: 0;
  text-align: center;
  font-size: 15px;
  line-height: 1.4;
  opacity: 0.55;
}

/* ----- checkbox row with a bold label and a muted sub-line ------------------
   ONE canonical copy (design law, task #30). This lived only in autopick.ts's
   and pools.ts's private page styles, so the Settings page rendered the label
   and the sub-text JAMMED TOGETHER on one line (user, Aug 2 2026: "HAVE YOU
   EVER HEARD OF A SPACE OR LINE BREAK?"). Global now; the module copies are
   deleted. */
.checkrow { display: flex; align-items: flex-start; gap: 10px; margin: 0 0 14px; cursor: pointer; }
.checkrow input { margin-top: 2px; flex-shrink: 0; }
.checkrow b { font-weight: 600; }
.checkrow small { display: block; color: var(--text-muted); }
.formfoot { display: flex; gap: 12px; align-items: center; margin: 4px 0 20px; }

/* ----- form pages: one readable column ------------------------------------
   .formwrap was defined ONLY inside pools.ts (720px) and autopick.ts (640px)
   private stylesheets, so any page that did not carry one of those — the new
   My settings page — rendered its forms edge to edge across a 1900px monitor.
   Global here; the module copies may stay, they simply override with their own
   width. Same defect class as .checkrow. */
.formwrap { max-width: 720px; margin: 0 auto; }

/* EQUAL-WIDTH ACTION STACK (user, Aug 13 2026, buyback page: "these two buttons
   should always be the same width (the wider of the two, no wrapping of
   text)"). The column is sized by its widest button and every other button
   stretches to match; labels never wrap. A form in the stack dissolves
   (display:contents) so its button is sized by the same column. */
.btncol { display: inline-flex; flex-direction: column; align-items: stretch; gap: 10px; max-width: 100%; }
.btncol form { display: contents; }
.btncol .btn { justify-content: center; white-space: nowrap; }

/* Localized date/time: each part stays whole so a narrow cell breaks BETWEEN the
   date and the time, never mid-date, and no separator is left dangling. */
.lt-part { white-space: nowrap; }

/* ----- picks board: the live "last play" line -----------------------------
   Had NO rule at all, so it inherited body type and wrapped to several lines,
   ballooning a live game's row ~2.6x taller than its neighbours (adversarial
   audit, Aug 3 2026). It is a supporting detail in a narrow spine cell: small,
   muted, and capped at two lines. */
.gspine-play {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  font-size: var(--fs-xs);
  line-height: 1.25;
  color: var(--text-muted);
}

/* ===== Buyback fee mode (022): the shared fee fragment (pools.ts
   buybackFeeFieldsHtml). The fixed/escalating blocks swap on the mode select
   via :has(option:checked) — no script. Engines without :has simply show both
   blocks; the POST parser saves only what the selected mode uses. ===== */
.bbwrap .bbesc { display: none; }
.bbwrap:has(.bbmode option[value="escalating"]:checked) .bbesc { display: block; }
.bbwrap:has(.bbmode option[value="escalating"]:checked) .bbfixed { display: none; }
.bbtiers {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 8px 10px;
  margin: 10px 0 6px;
}
.bbtiers .field { margin: 0; }
.bbtiers input { width: 100%; min-width: 0; }
@media (max-width: 700px) {
  .bbtiers { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Tier labels carry a live unit marker — the .bbwrap inline script flips
   -pct/-amt (and the placeholders) when the unit select changes. */
.bbtiers.-pct .field label::after { content: " (%)"; color: var(--text-muted); }
.bbtiers.-amt .field label::after { content: " ($)"; color: var(--text-muted); }

/* ===== Admin console tables: the "inner table" fix (user, Aug 13 2026:
   "tired of this 'inner' table layout everywhere that cuts off data (and
   even worse it can't even be scrolled)"). The wrap used to grow as tall as
   its table — /admin/payments measured 9,643px — so the horizontal scrollbar
   sat thousands of pixels below the rows being read and the pager arrows
   centered off-screen: scrollable in theory, never in practice. Capping the
   wrap at viewport height turns every long admin table into a panel whose
   scrollbars are always on screen; thead is already sticky. Desktop only —
   phones keep the full-bleed treatment above. ===== */
@media (min-width: 701px) {
  .admin .tablewrap:not(.gridscroll) {
    max-height: calc(100dvh - 210px);
    overflow: auto;
  }
}
.admin .tablewrap:not(.gridscroll) { scrollbar-width: thin; }
.admin .tablewrap:not(.gridscroll)::-webkit-scrollbar { height: 12px; width: 12px; }
.admin .tablewrap:not(.gridscroll)::-webkit-scrollbar-thumb {
  background: rgba(22, 64, 35, 0.45);
  border-radius: 6px;
}
.admin .tablewrap:not(.gridscroll)::-webkit-scrollbar-track { background: rgba(22, 64, 35, 0.08); }

/* Long free-text cells (emails, PayPal txn ids) clip with a tooltip instead of
   inflating the table past its container. */
.clipcell {
  display: inline-block;
  max-width: 24ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
}

/* Stacked action rows (button + note per line): form.row is the ONE pattern —
   never <p><form>, which the parser splits and the spacing goes ragged. Give
   form/div rows the same 1em rhythm p already has. */
.admin form.row, .admin div.row { margin: 0 0 1em; }
.admin .card form.row:last-child, .admin .card div.row:last-child { margin-bottom: 0; }

/* ===== Admin tables use the WHOLE screen (user, Aug 13 2026: "i didn't mean
   just add scrollbars ... use the whole god damn screen, make the text
   smaller, make the columns more efficient"). Top-level console tables (the
   .hpage-wrapped ones directly in the page flow — /admin/payments, /admin/
   users, forensics) break out of the 1400px shell to the viewport edges on
   desktop, exactly the mobile full-bleed trick at the other end. Tables
   inside cards keep their card, but ALL admin tables drop to 13px with
   tighter cells, so columns actually fit before any scrolling starts. ===== */
@media (min-width: 701px) {
  /* THE WHOLE SCREEN, properly (user, Aug 13 2026: "are these your attempts
     at using the whole screen width?" — the per-wrapper breakout margins
     missed half the tables and left dead gutters). The admin shell itself
     drops its 1400px cap, so every card and table simply spans the window;
     form fields cap at a sane line length instead. */
  .admin .shell { max-width: none; }
  .admin .field { max-width: 520px; }
  .admin .tablewrap:not(.gridscroll) table { font-size: 13px; }
  .admin .tablewrap:not(.gridscroll) th,
  .admin .tablewrap:not(.gridscroll) td { padding: 5px 8px; }
  .admin .tablewrap:not(.gridscroll) .btn { padding: 2px 10px; font-size: 12px; }
  .admin .tablewrap:not(.gridscroll) input:not([type="hidden"]):not([type="checkbox"]) { padding: 3px 6px; font-size: 12px; }
}

/* Roster (one row per entry): people and entries read left-aligned; the
   rowspanned member cell hugs the top of its entry group. */
.rostertbl td { text-align: left; vertical-align: top; }

/* Roster density (user, Aug 13 2026: "the roster table looks exactly the
   fucking same") — spreadsheet rhythm: short rows, control-sized-to-text
   buttons and inputs, zebra striping so entry rows read as ROWS instead of
   the old tall member blocks. */
.rostertbl td { padding: 4px 8px; }
.rostertbl .btn { padding: 2px 10px; font-size: 12px; }
.rostertbl input:not([type="hidden"]) { padding: 3px 6px; font-size: 12px; }
.rostertbl form { margin: 0; }
.rostertbl td form + form { margin-top: 4px; }
.rostertbl tbody tr:nth-child(even) td { background: #f4f8f3; }
/* The hpage arrows wrapper is a FLEX row, so its table box sized to content
   and left a dead gutter at full width — make it fill the row (admin only;
   the standings grid's tuned geometry stays untouched). */
.admin .hpage > .hpage-box { flex: 1 1 auto; min-width: 0; }

/* ===== THE FIFTH TAB (user, Aug 13 2026): "4 button mode which is the
   default ... and 5 button mode where you reduce the margins left and right
   of each button to fit a fifth button". app.js appends .tabbar-extra +
   .-five from /api/tabextra.json. The fifth button is the RED action —
   New entry while the pool takes entries, Buy back while a knockout entry
   can rejoin. ===== */
.tabbar.-five .tabbar-tab { min-width: 0; padding-left: 2px; padding-right: 2px; }
.tabbar.-five .tabbar-tab span { letter-spacing: 0; }
/* RED TEXT, not a red fill (user, Aug 13 2026: "red text, not a red
   background, unless selected (on the buyback page)") — a plain tab in brand
   red; ON the buyback page it renders as the selected chip, same shape the
   green -active tab uses, filled red. */
.tabbar-tab.tabbar-extra { color: var(--brand-red); font-weight: 700; }
.tabbar-tab.tabbar-extra svg { color: var(--brand-red); }
.tabbar-tab.tabbar-extra:hover { color: var(--brand-red); }
.tabbar-tab.tabbar-extra.-active { color: #fff; background: var(--brand-red); }
.tabbar-tab.tabbar-extra.-active svg { color: #fff; }

/* The top-bar "+" made no sense on phones ("you didn't know what + meant") —
   below the tab-bar breakpoint the fifth tab IS the add-entry path, so the
   top control goes entirely. On desktop it stays, in red. */
@media (max-width: 1099px) {
  .topnav-addentry { display: none; }
}
@media (min-width: 1100px) {
  .topnav-addentry { background: var(--brand-red); border-color: #7c1322; }
  .topnav-addentry:hover { background: #c02036; }
}

/* Desktop New Entry / Buy Back — the same decision as the phone's fifth tab,
   rendered by the same middleware as the LAST items of the green header menu
   (user, Aug 13 2026: "... Admin (If applicable), New Entry (if applicable),
   Buy Back (if applicable)"). Hidden below the tab-bar breakpoint: phones use
   the fifth tab. */
.pbar-action { display: none; }
@media (min-width: 1100px) {
  .pbar-action {
    display: inline-flex;
    align-items: center;
    background: var(--brand-red);
    border: 1px solid #7c1322;
    color: #fff;
    font-weight: 700;
    font-size: 13px;
    padding: 6px 14px;
    border-radius: 8px;
    text-decoration: none;
    white-space: nowrap;
  }
  .pbar-action:hover { background: #c02036; color: #fff; }
}

/* The account circle stays pinned to the RIGHT edge on every header variant
   (user, Aug 13 2026). On normal pages .pbar{flex:1} absorbs all free space so
   this is a no-op; on the bare My Pools header (no pool menu) it is the only
   thing keeping the circle off the brand's shoulder. */
.topnav-user { margin-left: auto; }

/* PICK-NEEDED BADGE (Aug 17 2026): red circle, white count, top-right corner of
   the Picks menu item on both the desktop pool bar and the mobile tab bar. */
[data-pb-picks], .tabbar-tab[data-tab="picks"] { position: relative; overflow: visible; }
.navbadge {
  position: absolute;
  min-width: 16px; height: 16px; padding: 0 4px;
  box-sizing: border-box;
  background: var(--red, #c62828); color: #fff;
  border-radius: 999px;
  font-size: 10px; font-weight: 800; line-height: 16px; text-align: center;
  font-variant-numeric: tabular-nums;
  pointer-events: none; z-index: 3;
}
.navbadge[hidden] { display: none; }
/* desktop: lower + more left so it sits ON the "Picks" text, slightly bigger
   (user, Aug 17 2026). Stays clear of the gap toward Standings. */
[data-pb-picks] .navbadge {
  top: -3px; right: -2px;
  min-width: 19px; height: 19px; padding: 0 5px;
  font-size: 11px; line-height: 19px;
}
/* mobile: over the top-right of the tab's icon. 50% BIGGER than the desktop
   badge (user, Aug 17 2026) — the tab bar only exists below 1100px, so this
   size override never touches the desktop pool-bar badge. */
.tabbar-tab[data-tab="picks"] .navbadge {
  top: 3px; right: calc(50% - 27px);
  min-width: 24px; height: 24px; padding: 0 6px;
  font-size: 15px; line-height: 24px;
}


/* Money columns on the payment logs: right-aligned, tabular figures so the
   totals line up under the rows they add. */
td.num, th.num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }

/* ===========================================================================
   PLAYER PROFILE LIGHTBOX (user, Aug 25 2026: "opens a lighbox full screen with
   a larger version of the players name and profile pic ... this will be the
   sticky header of the lightbox, then put that persons pool history info").
   =========================================================================== */
.proflb {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: rgba(10, 16, 12, 0.62);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: max(12px, 3vh) 12px;
  overflow: hidden;
  overscroll-behavior: contain;
}
body.proflb-open { overflow: hidden; }
.proflb-card {
  position: relative;
  width: min(760px, 100%);
  max-height: calc(100vh - max(24px, 6vh));
  max-height: calc(100dvh - max(24px, 6vh));
  overflow: auto;
  overscroll-behavior: contain;
  background: var(--surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
/* THE HEADER STAYS PUT while the history scrolls under it. */
.prof-head {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  padding: var(--sp-4);
  background: var(--nav-bg);
  color: #f4f8f4;
  border-radius: var(--radius) var(--radius) 0 0;
}
.prof-pic { width: 96px; height: 96px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
.prof-id { min-width: 0; }
.prof-name { margin: 0; font-size: clamp(1.4rem, 1.1rem + 1.6vw, 2.1rem); line-height: 1.1; }
.prof-sub { margin: 4px 0 0; font-size: var(--fs-sm); color: #cfe0d2; }
.prof-x {
  position: absolute;
  top: 8px;
  right: 10px;
  background: transparent;
  border: 0;
  color: #f4f8f4;
  font-size: 1.7rem;
  line-height: 1;
  cursor: pointer;
  padding: 4px 8px;
}
.prof-body { padding: var(--sp-4); }
/* The superlatives, as tiles that wrap on a phone. */
.prof-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--sp-2);
  margin-bottom: var(--sp-4);
}
.prof-tile {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 10px 12px;
  background: var(--surface);
}
.prof-tile.-win { border-color: var(--gold-bright); background: rgba(247, 201, 72, 0.14); }
.prof-tile.-muted { opacity: 0.72; }
.prof-tile-v { display: block; font-size: 1.35rem; font-weight: 800; line-height: 1.15; }
.prof-tile-l { display: block; font-size: var(--fs-xs); color: var(--text-muted); margin-top: 2px; }
.prof-hist { width: 100%; border-collapse: collapse; }
.prof-hist th, .prof-hist td { padding: 7px 8px; border-bottom: 1px solid var(--line); text-align: left; }
.prof-hist th { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-muted); }
.prof-hist tr.-win td { background: rgba(247, 201, 72, 0.16); font-weight: 700; }
.prof-yr { font-weight: 700; white-space: nowrap; }
.prof-seq { font-weight: 400; color: var(--text-muted); }
.prof-of { font-weight: 400; color: var(--text-muted); font-size: var(--fs-xs); }
.prof-badges { text-align: right; }
.prof-badge {
  display: inline-block;
  font-size: var(--fs-xs);
  font-weight: 700;
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  color: var(--text-muted);
  white-space: nowrap;
}
.prof-badge.-win { background: var(--gold-bright); color: #221a02; border-color: transparent; }
.prof-badge.-live { border-color: var(--accent); color: var(--accent); }
.prof-badge.-none { opacity: 0.7; }
@media (max-width: 560px) {
  .prof-pic { width: 64px; height: 64px; }
  .prof-badges { text-align: left; }
  .prof-hist th, .prof-hist td { padding: 6px 5px; }
  .prof-badge { white-space: normal; line-height: 1.3; }
  .proflb-card { overflow-x: hidden; }
}
/* A name cell now opens a menu, so it should look like it does. */
.sgrid td.name, .sgrid td.kn { cursor: pointer; }
/* The winner count inside a CO-WON badge: present but quieter than the word. */
.prof-badge-n { font-weight: 600; opacity: 0.72; font-size: 0.92em; }
/* A winner bar is a button now: one tap opens that person's profile. */
.winner-hero[data-uid] { cursor: pointer; }
.winner-hero[data-uid]:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; }

/* PER-TEAM BREAKDOWN on the profile (user, Aug 26 2026). As many columns as the
   lightbox can hold: three on a desktop card, one on a phone. */
.prof-h3 { margin: var(--sp-4) 0 var(--sp-2); font-size: 1rem; }
.tmgrid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  font-size: var(--fs-sm);
  grid-template-columns: repeat(auto-fill, minmax(15.2em, 1fr));
  gap: 6px;
}
.tm {
  display: grid;
  grid-template-columns: 26px 2.9em 4.6em auto;
  justify-content: start;
  align-items: center;
  gap: 0 8px;
  padding: 5px 9px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface);
  font-size: var(--fs-sm);
}
.tm.-none { opacity: 0.55; background: transparent; }
.tm-logo { width: 26px; height: 26px; object-fit: contain; }
.tm-abv { font-weight: 800; letter-spacing: 0.02em; }
.tm-picks { color: var(--text-muted); font-variant-numeric: tabular-nums; text-align: right; }
.tm-unit { font-size: var(--fs-xs); }
.tm-rec { font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right; }
.tm-w { color: var(--accent); }
.tm-l { color: #a3241f; }
.tm-sep { color: var(--text-muted); padding: 0 1px; }

/* The payments log is NOT a boxed pager: it scrolls sideways in place (the
   table is far wider than any screen) while the PAGE scrolls vertically, and
   the translated thead needs to paint over the rows it slides across. */
/* PAYMENTS LOG: the PAGE scrolls in both directions (user's explicit call for
   this page — the table is wider than any screen even at small type). No inner
   scroll region exists; the topnav and the title band are FIXED so sideways
   panning cannot move them; the green thead is REAL position:sticky against the
   viewport, so it pins below the band vertically while travelling with its
   columns horizontally — which it must, to stay aligned. */
.payxwrap { max-height: none !important; overflow: visible !important; }
.paytable { font-size: var(--fs-xs); }
.paytable th, .paytable td { padding: 5px 7px; font-size: var(--fs-xs); }
.paytable thead th {
  position: sticky;
  top: calc(var(--topnav-h, 56px) + var(--payhead-h, 76px));
  z-index: 6;
  background: var(--nav-bg);
}
body.paywide { padding-top: var(--topnav-h, 56px); }
body.paywide .topnav { position: fixed; top: 0; left: 0; width: 100vw; box-sizing: border-box; }
body.paywide .payhead {
  position: fixed;
  top: var(--topnav-h, 56px);
  left: 0;
  width: 100vw;
  box-sizing: border-box;
  z-index: 30;
  background: var(--bg);
  padding: 8px 16px 10px;
  box-shadow: 0 6px 8px -6px rgba(15, 23, 18, 0.18);
}
body.paywide .payxwrap { margin-top: calc(var(--payhead-h, 76px) + 8px); }
