/* Ticket Window — see docs/DESIGN.md. */

@font-face {
  font-family: 'Oswald';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/oswald-700.cedd65736210.woff2") format('woff2');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/inter-400.cef6d3a6737e.woff2") format('woff2');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/inter-600.ca03b7ee20d3.woff2") format('woff2');
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/inter-700.64d12f9f7d82.woff2") format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/jetbrains-mono-400.3fb54ef45e30.woff2") format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/jetbrains-mono-600.dba84cb55691.woff2") format('woff2');
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/jetbrains-mono-700.b9c90042b2e0.woff2") format('woff2');
}

:root {
  --ground: #E7EBEA;
  --surface: #FFFFFF;
  --surface-2: #DCE2DF;
  /* Paper is its own stock, not a generic surface: a card can sit
     quietly against the ground, but a ticket has to read as a physical
     piece laid on it. The dark value is a compromise between two pulls
     -- lighter separates the paper from the ground, but closes on
     --line (#303838), which is what the perforation and the band rules
     are drawn in. At #2B3236 those rules were 1.08:1 against the paper
     and washed out; #232A2C sits equidistant, 1.22:1 either way. */
  --paper: #FFFFFF;
  --paper-lift: 0 3px 7px rgba(0, 0, 0, .16);
  --paper-lift-hover: 0 6px 13px rgba(0, 0, 0, .22);
  --ink: #1A2422;
  --ink-mute: #57635F;
  --accent: #1D3557;
  --accent-2: #E8A33D;
  --accent-text: #ffffff;
  --win: #00966B;
  --loss: #D2551A;
  --line: #CBD3CE;
  --marker: #C23B2E;
  --marker-dried: #8B7355;
  --medal-gold: #9A7209;
  --medal-silver: #6B7681;
  --medal-bronze: #8A5A33;

  --f-display: 'Oswald', ui-rounded, -apple-system, "Segoe UI", system-ui, sans-serif;
  --f-body: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --f-mono: 'JetBrains Mono', ui-monospace, "SF Mono", Menlo, monospace;

  /* Tells the browser this page supports both themes, so natively-rendered
     form controls (checkbox tick, select's dropdown popup, scrollbars) that
     have no author background/color of their own follow the OS/manual theme
     too, instead of always rendering with light-mode UA styling. */
  color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
  :root {
    --ground: #14181A;
    --surface: #1D2224;
    --surface-2: #262C2E;
  --paper: #232A2C;
    --ink: #ECEFEC;
    --ink-mute: #92A19B;
    --accent: #6E93C9;
    --accent-2: #E0A857;
    --accent-text: #000000;
    --win: #2FBF8F;
    --loss: #E37142;
    --line: #303838;
    --marker: #E2574A;
    --marker-dried: #9C8564;
    --medal-gold: #E3B341;
    --medal-silver: #B9C2C9;
    --medal-bronze: #C68A5B;
    color-scheme: dark;
  }
}
:root[data-theme="dark"] {
  --ground: #14181A;
  --surface: #1D2224;
  --surface-2: #262C2E;
  --paper: #232A2C;
  --ink: #ECEFEC;
  --ink-mute: #92A19B;
  --accent: #6E93C9;
  --accent-2: #E0A857;
  --accent-text: #000000;
  --win: #2FBF8F;
  --loss: #E37142;
  --line: #303838;
  --marker: #E2574A;
  --marker-dried: #9C8564;
  --medal-gold: #E3B341;
  --medal-silver: #B9C2C9;
  --medal-bronze: #C68A5B;
  color-scheme: dark;
}
:root[data-theme="light"] {
  --ground: #E7EBEA;
  --surface: #FFFFFF;
  --surface-2: #DCE2DF;
  --paper: #FFFFFF;
  --paper-lift: 0 3px 7px rgba(0, 0, 0, .16);
  --paper-lift-hover: 0 6px 13px rgba(0, 0, 0, .22);
  --ink: #1A2422;
  --ink-mute: #57635F;
  --accent: #1D3557;
  --accent-2: #E8A33D;
  --accent-text: #ffffff;
  --win: #00966B;
  --loss: #D2551A;
  --line: #CBD3CE;
  --marker: #C23B2E;
  --marker-dried: #8B7355;
  --medal-gold: #9A7209;
  --medal-silver: #6B7681;
  --medal-bronze: #8A5A33;
  color-scheme: light;
}

* { box-sizing: border-box; }

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

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--f-body);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); }

/* A name is tappable everywhere it appears, and looks the same tappable
   as it did before.

   Not accent-coloured and underlined, which is what these got from the
   bare `a` rule above when only two sites linked them. A standings board
   is thirty names; a pick matrix header is sixteen; the record book puts
   three in a row inside one trophy. Styled as links, a page becomes a
   wall of blue, and the colour stops meaning "go here" because
   everything is. So the name keeps whatever colour its own row gives it
   -- .board-who, .roster-who, .award-who and the rest all set their own
   weight and colour, and none of them wanted to be overridden.

   The affordance moves to hover and focus. That leaves a phone with no
   resting hint, which is the honest trade: the alternative is colouring
   every name on the page, and a name that reads as a name is worth more
   than a hint that a tap will work. Focus-visible still shows a ring, so
   this stays reachable by keyboard. */
a.username { color: inherit; text-decoration: none; }
@media (hover: hover) {
  a.username:hover { text-decoration: underline; text-underline-offset: 2px; }
}
a.username:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; border-radius: 3px; }

.page {
  max-width: 480px;
  margin: 0 auto;
  padding: 0 16px 48px;
}

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 16px 14px;
  max-width: 480px;
  margin: 0 auto;
}
.site-header .brand {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 19px;
  text-decoration: none;
  color: var(--ink);
}
.site-header .account { font-size: 13px; color: var(--ink-mute); }
.site-header .account a { margin-left: 10px; }
.site-header .logout-form { display: inline; margin-left: 10px; }
.icon-link {
  display: inline-flex;
  align-items: center;
  position: relative;
  vertical-align: middle;
  color: var(--ink-mute);
}
.icon-link:hover { color: var(--accent); }
.notification-badge {
  position: absolute;
  top: -6px;
  right: -8px;
  display: inline-block;
  min-width: 15px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--accent-2);
  color: #000000;
  font-size: 11px;
  font-weight: 700;
  text-align: center;
  line-height: 15px;
}
.site-header .logout-button {
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; cursor: pointer;
}

.league-indicator {
  margin-right: 10px;
  color: var(--ink-mute);
}
.league-switcher {
  display: inline-block;
  margin-right: 10px;
  position: relative;
}
.league-switcher summary {
  cursor: pointer;
  list-style: none;
  color: var(--ink-mute);
}
.league-switcher summary::-webkit-details-marker { display: none; }
.league-switcher summary::after {
  content: "▾";
  display: inline-block;
  margin-left: 4px;
  font-size: 10px;
  transition: transform .15s ease;
}
.league-switcher[open] summary::after { transform: rotate(180deg); }
.league-switcher ul {
  list-style: none;
  margin: 4px 0 0;
  padding: 6px;
  position: absolute;
  right: 0;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(27, 35, 30, .1);
  min-width: 140px;
  z-index: 10;
}
.league-switcher li form { margin: 0; }
.league-switcher li button {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  padding: 6px 8px;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
  border-radius: 6px;
}
.league-switcher li button:hover {
  background: var(--surface-2);
}

.messages { list-style: none; margin: 0 0 16px; padding: 0; }
.messages li {
  background: var(--surface);
  border: 1.5px solid var(--win);
  color: var(--win);
  border-radius: 12px;
  padding: 10px 14px;
  font-size: 13.5px;
  font-weight: 600;
  margin-bottom: 8px;
}
.messages li.error { border-color: var(--loss); color: var(--loss); }

@media (prefers-reduced-motion: no-preference) {
  .messages li { animation: message-in .35s ease-out; }
  @keyframes message-in {
    from { opacity: 0; transform: translateY(-6px); }
    to { opacity: 1; transform: translateY(0); }
  }
}

.form-errors {
  background: var(--surface);
  border: 1.5px solid var(--loss);
  color: var(--loss);
  border-radius: 12px;
  padding: 12px 14px;
  font-size: 13.5px;
  margin-bottom: 16px;
}
.form-errors p { margin: 0 0 4px; }
.form-errors p:last-child { margin-bottom: 0; }

.screen-head { padding: 4px 0 16px; }
.week-label,
.screen-eyebrow {
  font-family: var(--f-mono);
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 4px;
}
.screen-head h1 { font-family: var(--f-display); font-size: 26px; font-weight: 700; margin: 0 0 14px; }
.leagues-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.leagues-head h1 { margin-bottom: 0; }
.leagues-head-action {
  font-family: var(--f-display); font-weight: 700; font-size: 14px;
  text-decoration: none; white-space: nowrap; margin-bottom: 14px;
}
/* Season pass -- the leagues landing page's artifact. Shares the ticket's
   paper stock, printed border and perforation, but has no torn edge and
   no stub: a pass is what you hold for a whole season, a ticket is per
   game, and making them identical would flatten a real distinction. */
.pass-rack { list-style: none; margin: 0 0 20px; padding: 0; }
.pass-rack li {
  margin-bottom: 14px;
  /* drop-shadow belongs on the wrapper, not the pass. Masking is applied
     after filters on the same element, so a shadow set on the pass would
     be cut through by its own perforation; set here it traces the
     punched silhouette. (The game ticket has the matching bug in its
     other form -- box-shadow on the same element as clip-path, so its
     shadow is clipped away entirely. Worth fixing there too.) */
  filter: drop-shadow(var(--paper-lift));
}

/* Shape, paper and frame all come from the shared ticket chrome further
   down; this is only what makes a pass a pass rather than a ticket. */
.season-pass {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 15px 18px 15px;
  color: var(--ink);
  text-decoration: none;
  /* Mono, like the ticket's own body: the two artifacts are printed on
     the same machine, so they should be set in the same face. */
  font-family: var(--f-mono);
}
.season-pass:focus-visible {
  /* Negative offset keeps the ring inside the mask, which would
     otherwise punch notches out of the focus indicator. */
  outline: 3px solid var(--accent);
  outline-offset: -3px;
}

.pass-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.pass-kind {
  font-family: var(--f-mono);
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.pass-flag { width: 9px; height: 9px; border-radius: 50%; background: var(--marker); flex: none; }
/* Matches .ticket-matchup: same face, weight, tracking and uppercase, so
   a league name on a pass reads like a matchup on a ticket. */
.pass-name {
  font-size: 15px;
  font-weight: 600;
  letter-spacing: .01em;
  line-height: 1.55;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Leaders, plus your own line when you're not among them. Three columns
   so ranks, names and records align down the card the way a printed
   standings column would. */
/* The band between the punch strip and the status line. Carries the
   standings once there are any, and who's in / when it opens before
   then, so the pass keeps its shape all season instead of collapsing to
   a thin strip through the whole preseason. */
.pass-band {
  padding: 9px 0;
  border-top: 1px dashed var(--line);
  border-bottom: 1px dashed var(--line);
  font-size: 12px;
  color: var(--ink-mute);
}
.pass-standings {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 3px 10px;
  align-items: baseline;
}
.pass-preseason { display: flex; justify-content: space-between; gap: 12px; }
.pass-standing { display: contents; }
.pass-standing b { font-weight: 700; color: var(--ink-mute); }
.pass-standing-who { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pass-standing-line { font-variant-numeric: tabular-nums; }
.pass-standing.is-you .pass-standing-who,
.pass-standing.is-you .pass-standing-line,
.pass-standing.is-you b { color: var(--ink); font-weight: 700; }

/* How this league is set up, at a glance. Bordered mono tags rather
   than .mode-chip's body-font pill: the pass is set in mono throughout,
   and a pill would read as a control rather than something printed on
   it. Same vocabulary as the ATS tag on the pick form.

   Confidence appears only when it's on, matching how week_picks already
   shows it -- across a rack of passes its absence is legible, because
   the pass next to it has one. */
.pass-tags { display: flex; flex-wrap: wrap; gap: 5px; }
.pass-tag {
  font-size: 9.5px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-mute);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 2px 5px;
  line-height: 1.4;
}

/* One mark per week, filled once every game that week is picked. Uses
   the pick form's punch vocabulary: unpunched is the same dashed circle
   the pick control shows before you mark it. */
.pass-punches { display: flex; flex-wrap: wrap; gap: 4px; }
/* An unpunched week is an empty hole waiting to be marked. Solid, not
   dashed: the pick form's punch circle is dashed at 26px, where the
   dashes read as a dashed circle. At 9px in a row of twenty-two there
   is no room for them to say anything -- the only question here is
   punched or not, and a clean ring answers it where a broken one just
   looked like grit. */
.pass-punch {
  width: 9px; height: 9px; border-radius: 50%;
  border: 1.5px solid var(--ink-mute);
  opacity: .5;
}
.pass-punch.is-punched {
  background: var(--marker);
  border-color: var(--marker);
  opacity: 1;
}

.pass-status { font-family: var(--f-mono); font-size: 11.5px; color: var(--ink-mute); }
/* --marker, not the marigold accent: this is the one line on the page
   worth acting on, and marigold on white is far too low-contrast to
   carry it. Red is also the colour the pick form marks with, so "due"
   and "your mark goes here" read as the same idea. */
.pass-status b { color: var(--marker); font-weight: 700; }
.season-pass.needs-you::before { border-color: var(--marker); opacity: .55; }

/* An empty rack is an invitation, not an error: same object, visibly
   blank -- no paper, no shadow, dashed outline. */
.season-pass.is-unissued { background: transparent; color: var(--ink-mute); }
.season-pass.is-unissued::before { border-style: dashed; opacity: .4; }
.season-pass.is-unissued::after { display: none; }
.season-pass.is-unissued .pass-name { color: var(--ink-mute); }
.season-pass.is-unissued .pass-status { font-family: var(--f-body); font-size: 13px; }

@media (hover: hover) {
  .pass-rack li:hover { filter: drop-shadow(var(--paper-lift-hover)); }
  .season-pass:hover .pass-name { color: var(--accent); }
}
@media (prefers-reduced-motion: no-preference) {
  .pass-rack li { transition: filter .18s ease; }
  .season-pass .pass-name { transition: color .18s ease; }
}

.league-head { display: flex; align-items: center; gap: 10px; }
.settings-gear {
  text-decoration: none;
  font-size: 20px;
  line-height: 1;
  color: var(--ink-mute);
  margin-bottom: 14px;
}
.settings-gear:hover { color: var(--accent); }

/* Was 11px/muted/uppercase -- the same visual weight as body text.
   Now sized and weighted to read as a real section break; the forced
   uppercase transform is dropped in favor of each heading's own natural
   case (see docs/superpowers/specs/2026-07-21-season-dashboard-design.md). */
.section-label {
  font-family: var(--f-mono); font-size: 15px; font-weight: 700; letter-spacing: .04em;
  color: var(--ink); margin: 32px 0 12px;
}
.section-label:first-child { margin-top: 8px; }

/* A matrix's section label shares a line with its orientation toggle. */
.matrix-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin: 32px 0 10px; }
.matrix-head .section-label { margin: 0; }
.matrix-orient { display: inline-flex; border: 1.5px solid var(--line); border-radius: 999px; padding: 2px; flex-shrink: 0; }
.stats-mode-toggle { margin-top: 4px; }
/* Which edition is showing is one attribute on the page, not a `hidden`
   flag on each of the seventy figures. Writing seventy attributes forced
   a full relayout of a several-thousand-pixel page on every switch, and
   doing that in the middle of the turn is what made the animation
   stutter. One attribute, one relayout, and the browser can do it inside
   the frame it already has.

   !important because these targets set their own `display` (.program-line
   is flex, table cells are table-cell), and an author rule setting
   display always beats the user-agent's [hidden] rule -- which is the
   bug the old `hidden` version had to work around too. */
.program-page[data-edition="su"] [data-stat-value="ats"],
.program-page[data-edition="ats"] [data-stat-value="su"] { display: none !important; }
.matrix-orient-btn {
  font-family: var(--f-body); font-size: 11px; font-weight: 700; color: var(--ink-mute);
  background: none; border: 0; border-radius: 999px; padding: 4px 10px; cursor: pointer;
}
.matrix-orient-btn.is-active { background: var(--accent); color: var(--accent-text); }
.matrix-dash { color: var(--ink-mute); }

.section-subhead {
  display: flex; align-items: center; gap: 8px;
  font-weight: 700; font-size: 12.5px; color: var(--ink-mute);
  margin: 18px 0 8px;
}
.section-subhead:first-child { margin-top: 0; }
.trophy-tag {
  font-family: var(--f-mono); font-size: 9px; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--accent); border: 1.5px solid var(--accent);
  border-radius: 999px; padding: 2px 7px;
}
.trophy-list { list-style: none; margin: 0 0 8px; padding: 0; display: flex; flex-direction: column; gap: 7px; }
.trophy-list li { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font-size: 13px; }
.trophy-rank { font-family: var(--f-mono); color: var(--ink-mute); width: 1.4em; flex-shrink: 0; }
.trophy-who { flex: 1; font-weight: 700; }
.trophy-count { font-family: var(--f-mono); color: var(--ink-mute); font-variant-numeric: tabular-nums; }
.trophy-cat { color: var(--ink-mute); }
.notification-unread { border-left: 3px solid var(--accent-2); padding-left: 8px; }
.trophy-note { font-size: 11px; color: var(--ink-mute); }
.trophy-link { font-size: 13px; font-weight: 600; text-decoration: none; }
.trophy-link::after { content: " \2192"; }

.mode-banner {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding-bottom: 12px; margin-bottom: 4px; border-bottom: 2px dashed var(--line);
}
.mode-chip {
  font-family: var(--f-body); font-weight: 700; font-size: 11.5px;
  padding: 5px 10px; border-radius: 999px;
  background: var(--surface-2); color: var(--ink-mute);
}
.mode-chip.is-on { background: var(--accent); color: var(--accent-text); }
a.mode-progress { text-decoration: none; font-weight: 700; }
.mode-progress {
  font-family: var(--f-mono); font-size: 11px; color: var(--ink-mute);
  margin-left: auto; font-variant-numeric: tabular-nums;
}


.confidence-chip {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line);
}
.confidence-chip > span:first-child { font-size: 11.5px; font-weight: 600; color: var(--ink-mute); }

.confidence-stepper { display: flex; align-items: center; gap: 10px; }
.stepper-btn {
  width: 26px; height: 26px; flex: 0 0 auto;
  font-family: var(--f-display); font-size: 14px; line-height: 1;
  border: 2px solid var(--line); border-radius: 8px;
  background: var(--surface); color: var(--ink); cursor: pointer;
}
.stepper-btn:hover { border-color: var(--accent); color: var(--accent); }
.stepper-btn:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }

.confidence-input {
  width: 34px; text-align: center;
  font-family: var(--f-display); font-weight: 700; font-size: 16px;
  border: 2px solid var(--line); border-radius: 8px; padding: 4px 2px;
  background: var(--surface); color: var(--ink);
  /* stepper buttons drive the value; hide the browser's own spin controls */
  -moz-appearance: textfield;
  appearance: textfield;
}
.confidence-input::-webkit-outer-spin-button,
.confidence-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* Pick sheet (pick_sheet.html) -- the whole week as rows
   after picks. The tray answers "which numbers are left?" at a glance,
   sticky so it stays answerable while scrolling the game list. */
.confidence-help,
.sheet-help { font-size: 13.5px; color: var(--ink-mute); margin: 0 0 12px; }

/* Sticky for the whole scroll, so the numbers still free are always in
   view -- which means it has to be a sibling of the games, not nested in
   the header. A sticky element only sticks while its own containing
   block is on screen. z-index clears the tickets below it and stays
   under the toast (60) and the confirm stamp (50). */
.confidence-tray {
  position: sticky; top: 0; z-index: 30;
  display: flex; flex-wrap: wrap; gap: 6px;
  padding: 10px 0; margin-bottom: 12px;
  background: var(--ground);
  border-bottom: 2px dashed var(--line);
}
.tray-chip {
  min-width: 30px; height: 30px; padding: 0 4px;
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--f-display); font-weight: 700; font-size: 14px;
  font-variant-numeric: tabular-nums;
  border: 2px solid var(--line); border-radius: 8px;
  background: var(--surface); color: var(--ink);
}
/* Spoken for: dimmed, never struck. One treatment, both pages -- the
   sheet used to strike used numbers while the ticket page only dimmed
   them, so the same state looked like two different things depending on
   where you were standing.

   Dimming is the one that survives. A strike means "this is gone", and
   on a fully-ranked sheet that printed all sixteen numbers cancelled,
   which is the opposite of what a completed ranking is. It also
   collided with the result colours below, where a strike already means
   the pick lost -- so a struck winner read as negated. Strike now means
   exactly one thing, everywhere: lost. */
.tray-chip.is-used {
  background: var(--surface-2); color: var(--ink-mute);
  border-color: transparent;
}
.tray-chip.is-locked { opacity: .55; }
.tray-chip.is-dup {
  background: var(--surface); color: var(--loss);
  border-color: var(--loss); text-decoration: none;
}

.confidence-row {
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 8px;
  padding: 12px 0; border-bottom: 1px solid var(--line);
}
.confidence-row.has-error { border-bottom-color: var(--loss); }
.confidence-row-kickoff {
  font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .08em;
  color: var(--ink-mute); margin-bottom: 4px;
}
.confidence-row-matchup { display: flex; align-items: center; gap: 6px; font-size: 13.5px; }
.confidence-row-matchup .at { color: var(--ink-mute); }
.confidence-your-pick {
  display: inline-flex; align-items: center; gap: 5px;
  margin-left: 8px; font-size: 11.5px; font-weight: 600; color: var(--ink-mute);
}

/* Tapping a badge switches the pick. The unpicked side is dimmed rather
   than hidden, so both remain readable as the matchup while it's still
   obvious which one you're on -- the row has to answer "who's playing"
   and "who did I take" at the same time. */
.confidence-teams { display: inline-flex; align-items: center; gap: 6px; }
.confidence-team { cursor: pointer; display: inline-flex; opacity: .42; transition: opacity .12s ease; }
.confidence-team.is-picked { opacity: 1; }
/* :has keeps it live without JS -- the page saves by fetch and never
   reloads, so the server-rendered is-picked class would otherwise stay
   on whichever team you started with. That class is still the fallback
   where :has isn't supported: correct on load, just not until save. */
.confidence-team:has(input:checked) { opacity: 1; }
.confidence-team:not(:has(input:checked)) { opacity: .42; }
/* The radio drives state but never shows: the badge is the control. It
   stays in the DOM, focusable, so keyboard and screen readers get a real
   radio group rather than a pair of unlabelled decorations. */
.confidence-team input {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.confidence-team:has(input:focus-visible) .team-badge {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
@media (hover: hover) {
  .confidence-team:hover { opacity: .75; }
  .confidence-team.is-picked:hover { opacity: 1; }
}
.confidence-locked,
.sheet-locked { font-size: 12px; font-weight: 600; color: var(--ink-mute); }
.confidence-pick-first { font-size: 12.5px; font-weight: 600; }
.confidence-row-errors {
  flex-basis: 100%; font-size: 12.5px; color: var(--loss); font-weight: 600;
}
.confidence-row-errors p { margin: 0; }

/* Two ends: up one level on the left, across on the right. */
.page-nav { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.page-nav-side {
  margin: 4px 0 14px; font-weight: 600; font-size: 13px;
  color: var(--ink-mute); text-decoration: none;
}
.page-nav-side:hover, .page-nav .back:hover { color: var(--accent); }

.back { display: inline-block; margin: 4px 0 14px; font-weight: 600; font-size: 13px; color: var(--ink-mute); text-decoration: none; }

/* Game detail hero -- a literal event-ticket metaphor. See
   docs/superpowers/specs/2026-07-21-game-detail-ticket-redesign-design.md.
   Sharp corners (no border-radius) with a hand-jittered clip-path outline
   instead of a perfect rectangle, so it reads as a physically imperfect
   printed card rather than a clean UI panel. */
/* Separates one game's whole card (ticket + insights) from the next in
   week_picks.html's list -- without it, an insights panel's muted text
   ran right into the next game's eyebrow row with nothing to mark where
   one game ends and the next begins. The dashed rule matches the
   ticket-tear perforation look used elsewhere in this design. Applied
   unconditionally (including after the last game) rather than trying to
   detect "last card" -- the form's own .submit-row follows the loop as a
   sibling, so a :last-child rule wouldn't actually match the last
   .ticket-card-wrap anyway, and the extra breathing room before the
   Save button reads fine regardless. */
/* ---- Shared ticket chrome -------------------------------------------
   One paper vocabulary for every artifact in the app: the game ticket
   and the season pass. Common to both -- chamfered corners, scalloped
   vertical edges, a printed frame and an inner dashed rule. Different --
   only a game gets torn, so only the ticket has a stub.

   Every measurement here is px, never %. The previous hand-authored
   percentage polygons scaled with the element, so the same "torn edge"
   was a couple of px on a phone and a visible bite on desktop -- which
   is why the printed border kept having to be thickened to survive its
   own dips. A 9px chamfer is 9px everywhere. */
.ticket-card-wrap { margin-bottom: 28px; padding-bottom: 22px; border-bottom: 2px dashed var(--line); }

/* drop-shadow lives on the wrapper because masking is applied after
   filters on the same element: a shadow set on the ticket would be cut
   through by its own scallops. This also replaces the old box-shadow on
   .is-intact, which sat on the same element as its clip-path and so was
   being clipped away entirely -- the tickets have never actually had a
   shadow.

   One shadow for both themes. A light "reverse" shadow was tried for
   dark mode, on the theory that a black shadow on a near-black ground
   does nothing -- it reads as fog, not as lift, and the scallops make
   it worse: drop-shadow follows the masked silhouette, so every notch
   blurs into its own blob and the edge looks smudged rather than lit.
   A shadow only reads as a shadow when it darkens. Dark mode gets less
   out of this than light mode does, and that's the better trade. */
.ticket-card-wrap,
.detail-hero-wrap { filter: drop-shadow(var(--paper-lift)); }

/* The ticket sizes itself off its own width, not the viewport's, and it
   is the same ticket at every width -- no breakpoint reshapes it.

   The problem this solves: the longest name and record ("WASHINGTON
   COMMANDERS (12-5)", 28 characters) does not fit beside a score at
   15px, and not only on a phone -- at the desktop ticket's own width the
   name column holds about 26 characters. So it truncated there too, just
   less often. A breakpoint can't fix that; it only moves where the
   truncation starts.

   Scaling the type off the container does fix it, because the thing that
   has to fit is measured in characters and the space is measured in
   container width.

   The container is .detail-hero-main, not the whole ticket, so the
   measurement is of the space the name actually gets. That matters at
   the narrow end: the stub stops shrinking at its min-width, and past
   that point the main column's share of the ticket is no longer a fixed
   proportion. Measured off the ticket, the type stayed 4px too big at
   320px for exactly that reason.

   The upshot is one layout. A phone gets the same ticket a laptop does,
   smaller, with every element in the same place -- which is the point:
   there is one design to maintain, not two. */
.detail-hero { position: relative; display: flex; margin-bottom: 14px; }

.detail-hero.is-intact,
.season-pass {
  --chamfer: 9px;
  --scallop-r: 4px;
  --scallop-pitch: 14px;
  position: relative;
  background: var(--paper);
  /* Corners cut at an angle, as on a printed admission ticket. */
  clip-path: polygon(
    var(--chamfer) 0, calc(100% - var(--chamfer)) 0,
    100% var(--chamfer), 100% calc(100% - var(--chamfer)),
    calc(100% - var(--chamfer)) 100%, var(--chamfer) 100%,
    0 calc(100% - var(--chamfer)), 0 var(--chamfer)
  );
  /* Scallops punched out of both vertical edges. Two mask layers, one
     per edge, intersected so a pixel shows only where both agree --
     otherwise the default union makes every hole opaque again. Where
     mask-composite isn't supported the declaration drops and you lose
     the scallops, not the ticket. */
  -webkit-mask-image:
    radial-gradient(circle var(--scallop-r) at left center, transparent calc(var(--scallop-r) - .5px), #000 var(--scallop-r)),
    radial-gradient(circle var(--scallop-r) at right center, transparent calc(var(--scallop-r) - .5px), #000 var(--scallop-r));
  mask-image:
    radial-gradient(circle var(--scallop-r) at left center, transparent calc(var(--scallop-r) - .5px), #000 var(--scallop-r)),
    radial-gradient(circle var(--scallop-r) at right center, transparent calc(var(--scallop-r) - .5px), #000 var(--scallop-r));
  -webkit-mask-size: 100% var(--scallop-pitch);
  mask-size: 100% var(--scallop-pitch);
  -webkit-mask-repeat: repeat-y;
  mask-repeat: repeat-y;
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* The printed frame: a solid rule, then a dashed one just inside it,
   the way the reference ticket prints a panel edge and a dashed guide.
   Inset past the chamfers and scallops so the cuts read as cut paper
   rather than cut ink. */
.detail-hero.is-intact::before,
.season-pass::before {
  content: '';
  position: absolute;
  inset: 5px;
  border: 2px solid var(--ink);
  opacity: .3;
  pointer-events: none;
}
.detail-hero.is-intact::after,
.season-pass::after {
  content: '';
  position: absolute;
  inset: 9px;
  border: 1px dashed var(--ink);
  opacity: .2;
  pointer-events: none;
}

.is-intact .detail-hero-stub { border-left: 2px dashed var(--line); }

/* Once a game locks the stub tears off: two pieces, each with its own
   paper and rotation and a real gap between them. Each piece keeps the
   chamfered corners and scallops on its *outer* edge -- that edge was
   always the ticket's edge -- while the edge it tore along stays raw. */
.detail-hero.is-torn { gap: 5px; }
.is-torn .detail-hero-main,
.is-torn .detail-hero-stub {
  --chamfer: 9px;
  --scallop-r: 4px;
  --scallop-pitch: 14px;
  position: relative;
  background: var(--paper);
  -webkit-mask-size: 100% var(--scallop-pitch);
  mask-size: 100% var(--scallop-pitch);
  -webkit-mask-repeat: repeat-y;
  mask-repeat: repeat-y;
}
.is-torn .detail-hero-main {
  transform: rotate(-0.5deg);
  /* Chamfers on the left; the right is the ragged edge it tore along. */
  clip-path: polygon(
    var(--chamfer) 0, 100% 0,
    calc(100% - 3px) 12%, 100% 24%, calc(100% - 3px) 37%, 100% 50%,
    calc(100% - 3px) 63%, 100% 76%, calc(100% - 3px) 88%, 100% 100%,
    var(--chamfer) 100%, 0 calc(100% - var(--chamfer)), 0 var(--chamfer)
  );
  -webkit-mask-image: radial-gradient(circle var(--scallop-r) at left center, transparent calc(var(--scallop-r) - .5px), #000 var(--scallop-r));
  mask-image: radial-gradient(circle var(--scallop-r) at left center, transparent calc(var(--scallop-r) - .5px), #000 var(--scallop-r));
}
.is-torn .detail-hero-stub {
  transform: rotate(0.8deg);
  clip-path: polygon(
    0 0, calc(100% - var(--chamfer)) 0, 100% var(--chamfer),
    100% calc(100% - var(--chamfer)), calc(100% - var(--chamfer)) 100%, 0 100%,
    3px 88%, 0 76%, 3px 63%, 0 50%, 3px 37%, 0 24%, 3px 12%
  );
  -webkit-mask-image: radial-gradient(circle var(--scallop-r) at right center, transparent calc(var(--scallop-r) - .5px), #000 var(--scallop-r));
  mask-image: radial-gradient(circle var(--scallop-r) at right center, transparent calc(var(--scallop-r) - .5px), #000 var(--scallop-r));
}
/* Printed frame on the torn pieces too, but only on the sides that are
   still a real ticket edge -- the torn side gets no rule, because a rule
   there would look printed rather than ripped. */
.is-torn .detail-hero-main::before {
  content: ''; position: absolute; inset: 5px 0 5px 5px;
  border: 2px solid var(--ink); border-right: 0;
  opacity: .3; pointer-events: none;
}
.is-torn .detail-hero-stub::before {
  content: ''; position: absolute; inset: 5px 5px 5px 0;
  border: 2px solid var(--ink); border-left: 0;
  opacity: .3; pointer-events: none;
}
/* A column, so the odds row can be pinned to the bottom of it. The
   stub is often the taller of the two and stretches the card past what
   the main side needs; measured, that left 7.3px of dead space under
   the odds on a ticket with no odds row of its own to fill it, and the
   numbers sat visibly high in the space they had. margin-top:auto on
   .ticket-details puts that slack above the dashed rule instead, where
   it reads as the divider sitting lower rather than the odds floating. */
.detail-hero-main {
  /* 19px at the bottom, not 12: the ticket's printed frame is inset 5px
     with a 2px rule, so it sits 7px inside the card and the last line of
     content was clearing *it* by 5px while clearing the dashed rule above
     by 12. That is what read as the odds being pinned to the bottom --
     they were, to the frame, which is the edge the eye actually uses.
     12 + 7 puts the same 12px on both sides of them. */
  flex: 1; min-width: 0; padding: 14px 14px 19px; font-family: var(--f-mono);
  display: flex; flex-direction: column;
  container-type: inline-size;
}
.ticket-eyebrow { font-size: 10.5px; letter-spacing: .18em; color: var(--ink-mute); display: flex; justify-content: space-between; }
/* On the week page the eyebrow is the link into the game's own page --
   it already names the game, so a separate "game detail" control under
   it said the same thing twice. Still a plain <div> on game_detail
   itself, which has nowhere to go. */
a.ticket-eyebrow {
  text-decoration: none; gap: 10px;
  /* Given the shape of a control rather than left as text that happens
     to be tappable: its own tinted strip, ruled off from the ticket body
     below it. The eyebrow was already the top line of the ticket, so
     without this it read as a heading and nobody would try tapping it. */
  margin: -2px -4px 2px; padding: 5px 8px 6px;
  border-radius: 6px;
  /* Tighter and a touch smaller than the unlinked eyebrow's 10.5px/.18em
     so the date and the status fit one line inside the strip. The wrap
     was always there, but a tinted box makes a two-line control look
     broken rather than merely loose. */
  letter-spacing: .04em; font-size: 10px;
  flex-wrap: nowrap;
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  border-bottom: 1.5px solid color-mix(in srgb, var(--accent) 22%, transparent);
  color: var(--accent);
}
@media (hover: hover) {
  a.ticket-eyebrow:hover {
    background: color-mix(in srgb, var(--accent) 14%, transparent);
  }
  a.ticket-eyebrow:hover .ticket-eyebrow-arrow { transform: translateX(2px); }
}
a.ticket-eyebrow:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }
/* The status keeps its own colour -- LIVE is about the game, not about
   this being a link. */
a.ticket-eyebrow .ticket-eyebrow-end { color: var(--ink-mute); }
a.ticket-eyebrow .ticket-eyebrow-end.ticket-live { color: var(--accent-2); }
/* The date yields first if a ticket is ever narrower than its content:
   truncating "SAT, AUG 8 · 10:15 AM" still leaves the day and date
   readable, where dropping the status loses whether the game is live. */
a.ticket-eyebrow > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ticket-eyebrow-end { display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap; flex: none; }
.ticket-eyebrow-arrow {
  font-size: 14px; line-height: 1; font-weight: 700; color: var(--accent);
  transition: transform .12s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  .ticket-eyebrow-arrow { transition: none; }
}

/* Font sizes here (and in .ticket-details/.ticket-odds-row below)
   restored to their pre-mobile-mockup values -- the mockup used a
   390px frame that turned out narrower than the real ticket actually
   renders at, so the aggressive shrinking wasn't needed. The
   overflow/ellipsis safety net stays regardless. */
/* Venue sits between the eyebrow and the matchup -- above the team
   names, not down with the odds -- since it reads more like part of
   "when/where" alongside the date/time than like a betting detail. */
.ticket-venue { margin-top: 6px; font-size: 10.5px; letter-spacing: .08em; color: var(--ink-mute); text-transform: uppercase; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Accented so the label reads at a glance rather than blending into the
   muted venue line -- the point of it is noticing the game is abroad. */
.venue-international { color: var(--accent); font-weight: 700; }

.ticket-matchup { margin-top: 14px; font-size: min(15px, calc(5.16cqw - 0.6px)); font-weight: 600; letter-spacing: .01em; line-height: 1.55; text-transform: uppercase; }
/* Name and score are separate tracks, so a long club name truncates
   against the score rather than pushing it off the ticket. Adding the
   score into the same run of text clipped it exactly when it mattered
   most -- a game in progress. */
.ticket-matchup div { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: baseline; }
.ticket-matchup .side { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Never split the record. Left as bare text the browser treats the hyphen
   in "(12-5)" as a break opportunity and prints "(12-" then "5)".

   It lives *inside* .side deliberately. Put beside it, as a direct child
   of the .ticket-matchup grid row, it becomes a second grid item: the
   name takes column 1, the record takes column 2, and the score is
   pushed onto an implicit second row under them. That is exactly the bug
   .fav's star hit before it (see .fav .side::after below), so it is
   worth stating twice: nothing new goes in this grid row except a
   column. */
.ticket-matchup .side-record { white-space: nowrap; }
.ticket-matchup .at { display: block; }
.ticket-matchup .score {
  font-family: var(--f-display); font-size: 1.2em; line-height: 1;
  font-variant-numeric: tabular-nums; color: var(--ink-mute);
}
.ticket-matchup .score.is-ahead { color: var(--ink); }
.ticket-matchup .at { color: var(--ink-mute); font-weight: 400; padding: 1px 0; }
.ticket-matchup .fav { position: relative; }
/* On .side, not on .fav: .fav is the grid row now, so an ::after there
   became its own grid item and the star dropped onto a line of its own. */
/* Sized in em, not pixels: the ticket's own type is fluid
   (min(15px, 5.16cqw)), so a star fixed at 9px was a different fraction
   of the name at every ticket width, and at all of them small enough to
   read as a stray gold dot rather than a star. At ~1em the five points
   are actually resolvable. */
.ticket-matchup .fav .side::after { content: "\2605"; font-size: 1.25em; margin-left: 5px; color: var(--accent-2); vertical-align: baseline; }

/* Only ever wraps the odds row now (venue moved above the matchup) --
   omitted from the template entirely when there's no odds line, so
   this dashed divider never renders over an empty gap.

   The actual bug in the last two attempts: comparing the raw numbers,
   the gap *above* the odds text (this block's old margin-top:14px +
   padding-top:12px = 26px) was simply larger than the gap *below* it
   (.detail-hero-main's padding-bottom:12px) -- an asymmetry baked
   directly into the authored values, nothing to do with flex/leftover-
   space behavior (which is genuinely unpredictable here, since main's
   own content is often taller than the stub's, leaving no leftover
   space at all for flex-grow/auto-margin tricks to redistribute).
   padding-top here now matches .detail-hero-main's padding-bottom
   exactly (12px each), so the odds row sits with equal, fixed gaps to
   the dashed line above and the card's bottom edge below, regardless
   of stretch behavior. */
.ticket-details {
  margin-top: 10px; padding-top: 12px; border-top: 2px dashed var(--line);
  font-size: 11px; color: var(--ink-mute);
  /* Takes whatever height is going spare and centres the numbers in it,
     rather than the block being pushed to the bottom as a unit.

     margin-top:auto did the pushing and was worse than the dead space it
     replaced: the rule went down with the numbers, so a ticket whose stub
     was the taller side opened a visible hole between the team names and
     the rule. The rule belongs under the matchup it divides. Growing this
     box instead puts the slack inside it, split evenly above and below
     the numbers, and the rule stays put. */
  flex: 1;
  display: flex; flex-direction: column; justify-content: center;
}
.ticket-details b { color: var(--ink); font-weight: 700; }

/* Odds condensed to one left-justified line, grouped by team (spread +
   ML) with O/U between. min-width:0 isn't needed here since this is a
   flex row (not an earlier grid version, which was the actual source
   of overflow: 1fr grid tracks default to min-width:auto and refuse to
   shrink below their text's intrinsic width). */
/* Evenly spaced across 3 columns (was left-justified flex, a holdover
   from the narrow 390px mockup -- the real ticket has plenty of width
   for this to sit centered/evenly spaced instead). minmax(min-content,
   1fr), not plain 1fr, so a long spread/moneyline combination can't
   compress a column below what its own text needs. */
/* No margin-top here -- .ticket-details' own padding-top already
   provides the (symmetric, matched to padding-bottom below) gap from
   the dashed line. This row's margin-top:8px was a leftover from when
   the venue line used to sit directly above it in the same wrapper
   (before venue moved up next to the matchup); with venue gone, it was
   just adding an extra, unaccounted-for 8px above the text that broke
   the intended symmetry. */
/* Three columns, two rows: labels on the first, values on the second, so a
   team's number sits directly under its own abbreviation.

       CHI        O/U       CAR
     -2.5/-135    44.5    +2.5/+114

   Previously each team was one blob ("CHI -2.5/-135") and wrapped on its
   own, which put the numbers at different heights. minmax(0, ...) rather
   than a min-content floor -- a floor means the grid can't shrink below its
   text, so a narrow ticket overflowed the card and ran through the
   perforation into the stub instead of adapting. */
.ticket-odds-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, auto) minmax(0, 1fr);
  row-gap: 5px; column-gap: 8px;
  font-size: 11px; letter-spacing: .08em;
  /* Boxes that hug their glyphs. On `normal` the label row's caps sat
     well below the top of its line box while the value row's digits --
     which have no descenders -- left the bottom of its box empty, so
     12px of padding above and 12px below measured out as 10px and 18px
     of visible space. row-gap picks up what the leading used to give. */
  line-height: 1;
}
.ticket-odds-label { text-align: center; color: var(--ink-mute); }
/* Every number contrasts, not just the over/under -- they're all the
   figures you're actually reading. */
.ticket-odds-value { text-align: center; color: var(--ink); font-weight: 600; overflow-wrap: anywhere; }

/* 148px, then 128px, and now a share rather than a number: 28.5% is
   what 128px was of the ticket's own 449px, so a desktop ticket is
   unchanged and every narrower one keeps the same proportions instead of
   handing the stub a growing share of the row. The @media override that
   used to cut it to 108px below 430px was doing this arithmetic by hand
   -- 28.5% of a 369px phone ticket is 105px, which is where that number
   came from. One rule now, and no width where the ticket changes shape.

   min-width keeps it honest on a very narrow screen, where a percentage
   alone would squeeze the badge and the punch circle out. */
.detail-hero-stub {
  width: 28.5%; min-width: 96px; flex-shrink: 0; padding: 12px 8px;
  display: flex; flex-direction: column; align-items: stretch; justify-content: center; gap: 4px;
}
.stub-label {
  font-family: var(--f-body); font-size: 8.5px; font-weight: 600; color: var(--ink-mute);
  text-transform: uppercase; letter-spacing: .06em; text-align: center; margin-bottom: 5px;
}
.stub-final-label { color: var(--ink-mute); font-style: italic; }
/* Names the state the two stacked numbers below are in. This heading is
   the only place on the stub wide enough to say it in words. */
.stub-moved-label { color: var(--accent-2); }
/* minmax(min-content, 1fr), not plain 1fr -- guarantees neither outer
   track shrinks below its content's natural size, while still sharing
   any leftover space 1fr-style so the badge stays centered. (Turned
   out not to be the cause of the punch-circle rendering as a sliver --
   that was .punch-circle needing display:inline-block, see below --
   but this is still a more robust pattern than a bare 1fr on its own.) */
.stub-team-row { display: grid; grid-template-columns: minmax(min-content, 1fr) auto minmax(min-content, 1fr); align-items: center; }
/* No spread this game -- collapses to two centered columns (badge,
   circle) instead of three with a dead empty spread cell. */
.stub-team-row.no-spread { display: flex; justify-content: center; align-items: center; gap: 8px; }
.stub-spread { font-family: var(--f-mono); font-size: 9px; color: var(--ink-mute); justify-self: center; }
/* Fixed width (not min-width) so every badge is the same size
   regardless of a 2- vs 3-letter abbreviation ("NE" vs "SEA") --
   min-width let 3-letter abbreviations render wider than 2-letter
   ones. */
.stub-team-row .team-badge {
  width: 2.6em; padding: 3px 2px; font-size: 10px; border-radius: 5px; justify-self: center;
}

/* "@" shares the exact same 3-column grid as the team rows above/below
   it, placed explicitly in the middle column -- so it lines up with
   the badge specifically, not with the center of the whole stub's
   width (which drifts depending on how wide the spread text is on
   either side, which is what centering it standalone actually did). */
.stub-at-row { display: grid; grid-template-columns: minmax(min-content, 1fr) auto minmax(min-content, 1fr); align-items: center; margin: 1px 0; }
.stub-at { grid-column: 2; justify-self: center; font-family: var(--f-display); font-weight: 700; font-size: 12px; color: var(--ink-mute); }

/* The circle is the real pick control now (wraps a radio input) --
   padding+negative margin gives a comfortably large tap target without
   visually growing the circle itself. */
.punch-circle-label { justify-self: center; cursor: pointer; padding: 6px; margin: -6px; position: relative; }
.punch-circle-label input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.punch-circle-label.is-disabled { cursor: default; }

/* A plain dashed-outline circle by default (both teams); the picked
   team's circle additionally gets a hand-drawn "X" (see .marker-x
   below) -- deliberately not a punched-hole cutout (that relied on
   var(--ground) contrasting with var(--surface), which doesn't hold up
   in every theme). Back to its original 20px -- that was always the
   right size once it actually rendered at its declared size.

   display: inline-block is the actual fix for a "circle renders as a
   thin sliver" bug this used to have: it's a <span>, which defaults to
   display:inline, and width/height are silently ignored on inline
   elements -- the circle was never rendering at its declared size at
   all, it was collapsing to its inline content's natural size
   (near-zero, since its only child is the absolutely-positioned,
   out-of-flow marker-x SVG) with just the two side borders rendering
   close together. */
.punch-circle {
  display: inline-block;
  width: 20px; height: 20px; border-radius: 50%;
  border: 2px dashed var(--ink-mute);
  position: relative; flex-shrink: 0; justify-self: center;
}
.punch-circle-label input:focus-visible + .punch-circle { outline: 2px solid var(--accent-2); outline-offset: 2px; }

/* Mark visibility and the "drawn in" animation are both driven entirely
   by JS-added classes (is-picked / is-drawing -- see ticket-pick.js),
   not by :checked. An earlier version gated opacity on :checked (which
   flips instantly, natively) while the animation's undrawn starting
   point only got applied a tick later once the change-event JS ran --
   for one paint, the mark showed fully drawn before JS reset it and
   animated it back in, reading as a flash of "leftover" ink. Routing
   visibility through the same JS-owned classes eliminates that race.
   A server-rendered existing pick gets `is-picked` directly in the
   template (no `is-drawing`), so there's no animation on page load. */
/* Explicitly sized and centred rather than `inset: -3px`. The SVG has a
   viewBox but no width/height attributes, which makes it a replaced element
   with no intrinsic size -- and for those, an over-constrained inset does
   NOT stretch the box. The browser keeps the intrinsic size and honours
   top/left while dropping right/bottom, so the X rendered ~3px up and left
   of the circle. 26px = the 20px circle plus the 3px overhang each side
   that the old inset was reaching for. */
.marker-x {
  position: absolute;
  top: 50%; left: 50%;
  width: 26px; height: 26px;
  transform: translate(-50%, -50%) rotate(-8deg);
  opacity: 0;
}
.punch-circle.is-picked .marker-x { opacity: 1; }
/* stroke-dasharray is 1 2 (dash, gap), not just 1 -- with a bare `1`
   (dash=1, gap=1, since a single value repeats), the "hidden" resting
   state below (dashoffset:1) lands exactly ON the dash/gap boundary at
   the path's start point. Combined with stroke-linecap:round, that
   boundary is precision-sensitive enough that a stray dot could render
   right at the tip of each stroke even though it's meant to be fully
   hidden -- seen as a brief flash of "the start of stroke 1 and the
   end of stroke 2" the instant the animation begins. Widening the gap
   and resting at dashoffset:2 (solidly mid-gap, not at its edge) gives
   real margin against that. */
.marker-x path { stroke: var(--marker); stroke-width: 3; stroke-linecap: round; fill: none; stroke-dasharray: 1 2; stroke-dashoffset: 0; }
.punch-circle.is-drawing .marker-x path { stroke-dashoffset: 2; animation: draw-stroke .32s ease-out forwards; }
.punch-circle.is-drawing .marker-x path:last-child { animation-delay: .22s; }
@keyframes draw-stroke { to { stroke-dashoffset: 0; } }
@media (prefers-reduced-motion: reduce) {
  .punch-circle.is-drawing .marker-x path { animation: none; stroke-dashoffset: 0; }
}
/* Locked: the circle isn't a real tap target anymore (see is-disabled
   above) -- dried ink IS the disabled look, no separate greyed-out
   treatment. */
.is-torn .marker-x path { stroke: var(--marker-dried); opacity: .8; }

/* ---- Line marks on the stub -----------------------------------------
   When the board moves off the line a pick is holding, the stub shows
   both numbers: the one you have, crossed out, and the one on offer,
   ringed. Same pen as the punch X -- these are marks made on your own
   ticket, not a restyling of the number into a different colour.

   Markup: _stub_line.html. Both marks use the punch's draw technique
   (pathLength=1 + dasharray/dashoffset), so read the .marker-x notes
   above for why the dasharray is `1 2` rather than a bare `1`. */
.stub-lines {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  /* Room for the ring, which is drawn wider than the number it encircles
     and would otherwise reach the badge beside it. */
  justify-self: center; padding: 2px 5px; line-height: 1.15;
}
.stub-line-old,
.stub-line-new {
  position: relative;
  display: flex; align-items: baseline; gap: 3px;
  font-family: var(--f-mono); font-size: 9px;
  font-variant-numeric: tabular-nums;
}
/* The stub heading carries the "line moved" label -- see
   .stub-moved-label. Per-number tags in this column were tried and are
   illegible at the width available. */
/* The line you're letting go of: still legible under its own strike,
   because what you had matters until you've decided. */
.stub-line-old { color: var(--ink-mute); opacity: .75; }
.stub-line-new { color: var(--ink); font-weight: 700; cursor: pointer; }
.stub-line-new input { position: absolute; opacity: 0; width: 1px; height: 1px; }

/* Settled: what you locked, over what the game started at. No per-number
   tags for the same reason the moved pair has none -- 40px of column. The
   stub's heading reads "Yours / Kickoff" instead, and the order and weight
   below carry it: yours in ink because it's the one you're graded on, the
   close muted underneath as the thing to measure it against. */
.stub-lines.is-settled { gap: 1px; padding: 2px 3px; }
.stub-line-mine,
.stub-line-close {
  font-family: var(--f-mono); font-size: 9px; font-variant-numeric: tabular-nums;
}
.stub-line-mine { color: var(--ink); font-weight: 700; }
.stub-line-close { color: var(--ink-mute); font-size: 8px; }

.mark-strike,
.mark-ring {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  pointer-events: none; overflow: visible;
}
.mark-strike { width: 108%; height: 14px; }
/* Wider than the number it rings so the stroke sits outside the glyphs
   rather than through them. */
.mark-ring { width: 118%; height: 19px; }

.mark-strike path,
.mark-ring path {
  stroke: var(--marker); stroke-width: 2; stroke-linecap: round; fill: none;
  stroke-dasharray: 1 2; stroke-dashoffset: 0;
}
.mark-ring path { stroke: var(--win); }

/* Unmarked until you act. A moved line at rest is an offer, not a
   decision: both numbers are shown plainly, and only taking the new one
   crosses out the old. Drawing the marks on page load would say you'd
   already agreed to something you haven't.
   Visibility is class-driven rather than :checked-driven for the reason
   the .marker-x notes give: :checked flips instantly while the
   animation's undrawn start only lands a tick later, which shows one
   frame of finished ink before it animates back in. */
.mark-strike,
.mark-ring { opacity: 0; }
.stub-lines.is-taken .mark-strike,
.stub-lines.is-taken .mark-ring { opacity: 1; }

.stub-lines.is-drawing .mark-strike path,
.stub-lines.is-drawing .mark-ring path {
  stroke-dashoffset: 2; animation: draw-stroke .34s ease-out forwards;
}
/* The ring lands after the strike: you cross a line out, then circle the
   one you took. Reading order, not decoration. */
.stub-lines.is-drawing .mark-ring path { animation-delay: .2s; }

@media (prefers-reduced-motion: reduce) {
  .stub-lines.is-drawing .mark-strike path,
  .stub-lines.is-drawing .mark-ring path { animation: none; stroke-dashoffset: 0; }
}

/* Your pick has moved to the other side, so this side's line is no
   longer a choice you're making. Dulled rather than removed: taking the
   number away as you tap would make the stub jump under your finger. */
.stub-lines.is-superseded { opacity: .4; }
.stub-lines.is-superseded .stub-line-new { cursor: default; font-weight: 400; }

.stub-line-new input:focus-visible + .stub-line-new-value {
  outline: 2px solid var(--accent-2); outline-offset: 2px; border-radius: 3px;
}
/* Once torn, nothing on the stub is a control and the ink has dried. */
.is-torn .mark-strike path { stroke: var(--marker-dried); opacity: .8; }
.is-torn .mark-ring path { opacity: .8; }

.stat-line-group { margin-bottom: 16px; display: flex; flex-direction: column; gap: 11px; }
.stat-line { display: flex; justify-content: space-between; font-size: 13.5px; }
.stat-label { color: var(--ink-mute); }
.stat-value { font-family: var(--f-mono); font-weight: 600; font-variant-numeric: tabular-nums; }

.pick-block { background: var(--surface); border-radius: 16px; padding: 16px; margin-bottom: 14px; }
.pick-block-label { font-weight: 700; font-size: 12px; color: var(--ink-mute); margin: 0 0 10px; }
.pick-block > input[type="text"],
.pick-block > input[type="email"],
.pick-block > select,
.pick-block > textarea {
  width: 100%; font: inherit; font-size: 14px; padding: 9px 10px;
  border: 1.5px solid var(--line); border-radius: 8px; background: var(--surface); color: var(--ink);
}

/* ---- Standings board ------------------------------------------------
   The clubhouse board: each row is a name-card in the same paper stock as the
   tickets and passes, seated in a printed rail. Reuses the existing
   paper vocabulary rather than introducing a fifth artifact -- what's
   new here is the rail and the deficit bar, and the bar is there
   because it carries information the raw numbers make you work out.

   Markup lives in _standings_board.html, shared by the season standings
   and the weekly results page. */
.board { list-style: none; margin: 0; padding: 4px 0 24px; display: flex; flex-direction: column; gap: 7px; }

/* The rail. A single hairline under each card, inset past the card's
   own edges so the card reads as seated in it rather than bordered by
   it. Last one is dropped -- a rail below the final card would read as
   a slot with nothing in it. */
.board-slot { position: relative; padding-bottom: 7px; }
.board-slot::after {
  content: '';
  position: absolute; left: 10px; right: 10px; bottom: 0;
  border-bottom: 1px solid var(--line);
}
.board-slot:last-child { padding-bottom: 0; }
.board-slot:last-child::after { display: none; }

.board-card {
  position: relative;
  display: grid;
  grid-template-columns: 24px minmax(0, 1fr) auto;
  align-items: center;
  /* Wide enough that the gap figure and the W-L beside it stay two
     readings -- they're both small mono numerals and run together at 10px. */
  column-gap: 15px;
  padding: 10px 12px;
  background: var(--paper);
  box-shadow: var(--paper-lift);
  /* Rounded, where this used to carry the tickets' chamfered corner.
     The chamfer was a clip-path, and a clip-path clips everything drawn
     on the element -- including the leader's inset ring, whose square
     corners were sliced off at each of the eight cuts. That read as a
     broken border rather than a cut corner.
     A radius is the one shape a box-shadow ring follows exactly, so the
     ring closes. The chamfer stays on the tickets and passes, which have
     no outline to lose to it. */
  border-radius: 8px;
}

.board-rank {
  font-family: var(--f-display); font-weight: 700; font-size: 19px;
  line-height: 1; text-align: right;
  color: var(--ink-mute); font-variant-numeric: tabular-nums;
}

.board-body { min-width: 0; }
.board-who {
  margin: 0 0 5px;
  font-weight: 700; font-size: 14.5px; line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.board-you {
  margin-left: 6px; padding: 1px 5px;
  font-family: var(--f-mono); font-size: 9.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em;
  color: var(--accent); border: 1px solid var(--accent); border-radius: 3px;
  vertical-align: 1px;
}

/* Deficit bar. The track runs from the season's baseline (zero points, or
   .500) to the leader; the fill is where this player sits on it. See
   views._attach_board_extras for why that scale and not an absolute one. */
/* The bar and the number it describes share a row, so the gap reads as
   the bar's label rather than as a fourth column competing with the
   record for the right edge. */
.board-gauge { display: flex; align-items: center; gap: 8px; }
.board-bar { flex: 1; min-width: 0; height: 4px; background: var(--line); border-radius: 2px; overflow: hidden; }
.board-bar span { display: block; height: 100%; background: var(--ink-mute); border-radius: 2px; }

.board-figures { text-align: right; line-height: 1.15; }
.board-stat {
  display: block;
  font-family: var(--f-mono); font-weight: 700; font-size: 14px;
  font-variant-numeric: tabular-nums;
}
.board-stat small { font-size: 9.5px; font-weight: 400; color: var(--ink-mute); margin-left: 2px; }
.board-sub { font-family: var(--f-mono); font-size: 10.5px; color: var(--ink-mute); font-variant-numeric: tabular-nums; }

.board-gap {
  flex: none; min-width: 46px; text-align: right;
  font-family: var(--f-mono); font-size: 10.5px; color: var(--ink-mute);
  font-variant-numeric: tabular-nums;
}
/* The GB/pts unit rides with the number rather than sitting in a column
   header, since the board has no header row to put one in. */
.board-gap small { font-size: 8.5px; letter-spacing: .06em; margin-left: 2px; }
.board-lead {
  font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .07em;
  color: var(--accent-2);
}

/* Leader. Keeps the outline and confetti the design doc specifies. The
   ring follows .board-card's radius now, so it closes at the corners
   instead of being clipped square by the old chamfer. */
.board-card.is-leader { overflow: hidden; box-shadow: var(--paper-lift), inset 0 0 0 2px var(--accent-2); }
.board-card.is-leader .board-rank { color: var(--accent-2); }
.board-card.is-leader .board-bar span { background: var(--accent-2); }

/* The reader's own card. Standings had no way to show this at all, so
   finding yourself in a 16-person league meant reading every name. */
.board-card.is-you .board-bar span { background: var(--accent); }
.board-card.is-you.is-leader .board-bar span { background: var(--accent-2); }

/* Pick sheet outcomes. Same two registers as the week page's stub -- a
   stamp for something settled, quiet text while it's in play -- at the
   density a sheet row allows. */
.sheet-verdict {
  display: inline-block; padding: 2px 7px;
  font-family: var(--f-display); font-size: 11px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
  border: 2px solid currentColor; border-radius: 4px;
  transform: rotate(-2deg);
}
.sheet-verdict.is-hit { color: var(--win); }
.sheet-verdict.is-miss { color: var(--loss); }
.sheet-verdict.is-push { color: var(--accent-2); }

.sheet-standing {
  font-family: var(--f-mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .06em; text-transform: uppercase;
}
.sheet-standing.is-hit { color: var(--win); }
.sheet-standing.is-miss { color: var(--loss); }
.sheet-standing.is-push { color: var(--accent-2); }

/* Tray chips carrying a result.

   A pick has five states, and the tray draws all five. Two independent
   axes make them: which way it is going (hit / miss / push, from
   pick_standing) and whether that can still change (is_settled). Colour
   is the first axis; weight is the second.

     not yet played   dim fill, no colour        .is-used alone
     winning          outlined green             .is-running.is-hit
     losing           outlined red               .is-running.is-miss
     won              solid green                .is-settled.is-hit
     lost             solid red, struck          .is-settled.is-miss

   Push is the sixth, in accent-2, running and settled alike -- it is a
   real outcome rather than a shade of the other two.

   The weight distinction is the whole point of drawing two axes rather
   than one: a settled pick is filled because that number is banked or
   gone, one still in play is only outlined because it is a guess about a
   game being played. Same thing the stub says in words with "Won" versus
   "Winning", and losing/lost must never collapse into each other just
   because they share a colour.

   Both pick surfaces render this now. The sheet used to emit only
   is-used, so the same numbers were a scoreboard on one page and a list
   of taken digits on the other -- see views._decorate_tray. */
a.tray-chip { text-decoration: none; }
/* The same chip, worn beside a game rather than in the tray -- so the
   rank on a row and the rank in the tray are visibly one fact. Only the
   size changes; every state rule below applies unaltered, which is the
   point of reusing the class instead of restyling a second element. */
.tray-chip.is-inline {
  min-width: 22px; height: 22px; padding: 0 3px;
  font-size: 12px; border-width: 1.5px; border-radius: 6px;
}
/* The only strike in the tray, and it needs is-settled to earn it: it
   means the number is gone, which is not true of a pick that is merely
   behind. See live.is_settled for how the tray came to strike those. */
.tray-chip.is-settled.is-miss { text-decoration: line-through; }

.tray-chip.is-settled.is-hit {
  background: var(--win); border-color: var(--win); color: var(--surface);
}
.tray-chip.is-settled.is-miss {
  background: var(--loss); border-color: var(--loss); color: var(--surface);
}
.tray-chip.is-settled.is-push { background: var(--accent-2); border-color: var(--accent-2); color: var(--surface); }
.tray-chip.is-running.is-hit { border-color: var(--win); color: var(--win); }
.tray-chip.is-running.is-miss { border-color: var(--loss); color: var(--loss); }
.tray-chip.is-running.is-push { border-color: var(--accent-2); color: var(--accent-2); }
@media (hover: hover) {
  a.tray-chip:hover { border-color: var(--accent); }
}
a.tray-chip:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }

/* The chips jump here, and the sticky tray would otherwise land on top
   of the row they jumped to. Both pages, since both trays link out. */
.ticket-card-wrap, .sheet-row { scroll-margin-top: 84px; }

/* ---- Week rail --------------------------------------------------------
   The season's weeks, navigable. Same punch vocabulary as the season
   pass's strip -- a filled mark is a week that happened -- except every
   mark here is a link, which is what the app was missing: there was no
   week index at all, only a column header inside a scrolling matrix.

   Markup: _week_rail.html. */
.week-rail { margin: 0 0 18px; }
.rail-strip {
  display: flex; gap: 4px; list-style: none; margin: 0; padding: 2px 0 8px;
  overflow-x: auto;
  /* clip, not hidden: overflow-x:hidden forces overflow-y to compute as
     auto, which would make this a vertical scroll container too. */
  overflow-y: clip;
}
.rail-strip li { flex: none; }
.rail-week {
  display: block; width: 34px; text-align: center;
  text-decoration: none; color: var(--ink-mute);
  font-family: var(--f-mono); font-size: 10px; font-weight: 700;
  padding: 2px 0;
}
.rail-mark {
  display: block; width: 18px; height: 18px; margin: 0 auto 4px;
  border-radius: 50%; border: 2px solid var(--line);
}
.rail-num { display: block; }

/* The playoffs are the strip's one structural division: eighteen numbered
   weeks, then four named rounds. A name needs room a number doesn't --
   "Divisional" is a single unbreakable word wider than a 34px cell, so it
   overflowed onto its neighbours and the four rounds ran together into
   one line of overlapping text. Wider cells, a size down, and wrapping
   allowed between words so "Conference Championship" takes two lines
   instead of one impossible one. */
.rail-week.is-playoff {
  width: 66px; font-size: 8.5px; line-height: 1.25;
  overflow-wrap: break-word;  /* a longer round name than we've seen wraps rather than spills */
}
.rail-split {
  align-self: stretch; width: 0; margin: 0 6px;
  border-left: 1px dashed var(--line);
}

/* Four independent facts, four channels, so a mark can say all of them at
   once instead of one winning and hiding the rest (see week_rail.py).

     inner fill   when the week is -- grey past, primary current, empty future
     ring         your picks -- dashed red due, solid green in
     pulse        in play
     outer ring   the week you're viewing

   Order matters: fill rules first, then ring rules, so the ring's colour
   wins the shared border-color property. */

/* --- inner fill: when the week is ------------------------------------ */
/* Future weeks keep the plain .rail-mark outline: no fill, no claim. */
.rail-week.is-past .rail-mark { background: var(--ink-mute); border-color: var(--ink-mute); }
.rail-week.is-past { color: var(--ink); }
.rail-week.is-current .rail-mark { background: var(--accent); border-color: var(--accent); }
.rail-week.is-current { color: var(--accent); }

/* --- in play: the fill pulses amber ---------------------------------- */
/* Amber is the app's live colour, and it goes on the fill rather than the
   ring because the ring is spoken for by your picks. Static amber is the
   base so the state still reads with motion turned off. */
.rail-week.is-live .rail-mark { background: var(--accent-2); border-color: var(--accent-2); }
.rail-week.is-live { color: var(--accent-2); }
/* The pulse itself lives with the other live indicators below, so they
   all share one timing. */

/* --- ring: your picks ------------------------------------------------- */
/* Dashed red: this week wants something from you. Only ever the current
   week -- see week_rail.picks_due. */
.rail-week.picks-due .rail-mark {
  border-color: var(--marker); border-style: dashed; border-width: 3px;
}
/* Solid green: everything chosen and, where it matters, ranked. Allowed to
   run ahead of the current week -- picking week 5 early earns the green --
   but never on a past week, whose grey fill already tells that story. */
.rail-week.picks-in .rail-mark {
  border-color: var(--win); border-style: solid; border-width: 3px;
}

/* --- outer ring: navigation ------------------------------------------ */
/* Hover is a ring too, not a change to the mark itself. It used to recolour
   the border, which now means your picks -- so pointing at a week
   momentarily claimed you had finished it.

   The two rings differ by weight rather than hue, because hue is spent:
   amber is live, green is picks in, red is picks due. So hover is a muted
   version of the same ring the viewed week wears -- literally a preview of
   the mark you'd be given by going there -- and the viewed one is the
   full-strength version.

   Hover is written first on purpose. The two rules have equal specificity
   and set the same property, so the later one wins: the week you are
   already viewing keeps its own ring while you point at it rather than
   fading to the hover weight. */
@media (hover: hover) {
  .rail-week:hover .rail-mark {
    box-shadow: 0 0 0 2px var(--ground), 0 0 0 4px var(--ink-mute);
  }
}
.rail-week.is-viewed .rail-mark { box-shadow: 0 0 0 2px var(--ground), 0 0 0 4px var(--ink); }

.rail-week:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; border-radius: 4px; }

/* ---- Live week --------------------------------------------------------
   A ticket carries its own score once its game starts, so the week page
   works in every phase instead of redirecting to a separate board. */
.mode-chip.is-live {
  color: var(--accent-2); border-color: var(--accent-2); font-weight: 700;
}
.ticket-live { color: var(--accent-2); font-weight: 700; }

/* Everything that says "this is happening right now" pulses, and they all
   pulse together: same duration, same easing, so the week chip, a
   ticket's LIVE, a sheet row's LIVE and the week strip's mark read as one
   signal rather than four things that happen to be amber.

   Two keyframes because the mark and the words need different properties.
   Text fades on opacity. The strip's mark can't -- its ring carries a
   separate meaning (your picks) and would fade along with it -- so that
   one animates background-color and leaves the ring alone.

   Amber is the resting state in both, so the indicator still reads with
   motion turned off; the animation is the emphasis, not the message. */
@keyframes live-pulse { 50% { opacity: .45; } }
@keyframes rail-live-pulse {
  50% { background-color: color-mix(in srgb, var(--accent-2) 30%, transparent); }
}
@media (prefers-reduced-motion: no-preference) {
  .mode-chip.is-live,
  .ticket-live,
  .sheet-live { animation: live-pulse 1.9s ease-in-out infinite; }
  .rail-week.is-live .rail-mark { animation: rail-live-pulse 1.9s ease-in-out infinite; }
}

/* Settled beside provisional. The running cell is tinted rather than
   merely labelled, so the two don't read as one number that moved. */
.week-score { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 0; }
.week-score-cell {
  flex: 1 1 0; min-width: 128px;
  padding: 8px 11px; border-radius: 10px;
  background: var(--surface); box-shadow: 0 1px 0 var(--line);
}
.week-score-cell.is-running {
  background: color-mix(in srgb, var(--accent-2) 10%, var(--surface));
  box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--accent-2) 30%, transparent);
}
.week-score-cell dt {
  font-family: var(--f-mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--ink-mute);
  margin-bottom: 3px;
}
.week-score-cell.is-running dt { color: var(--accent-2); }
.week-score-cell dd {
  margin: 0; display: flex; align-items: baseline; gap: 2px;
  font-variant-numeric: tabular-nums;
}
.week-score-cell dd b {
  font-family: var(--f-display); font-size: 19px; line-height: 1; color: var(--ink);
}
.week-score-of { font-family: var(--f-mono); font-size: 13px; color: var(--ink-mute); }
.week-score-rec { font-family: var(--f-mono); font-size: 11.5px; color: var(--ink-mute); margin-left: 5px; }

/* Where a pick stands, in two registers. A game still being played gets
   quiet text -- it's a running commentary and shouldn't shout. A graded
   result gets a stamp, because that one is final and is the thing you
   came to the page to find out. Same reason the wording changes tense:
   "Winning" and "Won" are different claims. */
.stub-standing {
  display: block; margin-top: 8px; text-align: center;
  font-family: var(--f-mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .07em; text-transform: uppercase;
  color: var(--ink-mute);
}
.stub-standing.is-hit { color: var(--win); }
.stub-standing.is-miss { color: var(--loss); }
.stub-standing.is-push { color: var(--accent-2); }
.stub-standing b { font-weight: 700; }

/* The settled verdict. Rotated and outlined like the confirm stamp the
   design doc specifies for a locked-in pick -- same gesture, applied to
   the outcome rather than the act. */
.stub-verdict {
  display: block; width: fit-content; margin: 10px auto 2px;
  padding: 3px 10px;
  font-family: var(--f-display); font-size: 14px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
  border: 2.5px solid currentColor; border-radius: 5px;
  transform: rotate(-3deg);
}
.stub-verdict.is-hit { color: var(--win); }
.stub-verdict.is-miss { color: var(--loss); }
.stub-verdict.is-push { color: var(--accent-2); }
/* What it earned, or what it cost. In a confidence season the number is
   half the result -- losing your 16 is not losing your 1. */
.stub-verdict-pts {
  display: inline-block; margin-left: 5px; padding-left: 6px;
  border-left: 1.5px solid currentColor;
  font-family: var(--f-mono); font-size: 12px;
}
.stub-verdict.is-miss .stub-verdict-pts { text-decoration: line-through; }

/* ---- Public league directory ----------------------------------------
   The same pass as the landing page's, minus the parts that only mean
   something once you're in: no punch strip, no standings band. What's
   left is what you'd choose a league on. */
.season-pass.is-directory { cursor: default; }
.season-pass.is-directory .pass-name {
  display: block; text-decoration: none; color: inherit;
}
.season-pass.is-directory .pass-name:hover { color: var(--accent); }
.pass-enroll { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.pass-enroll form { margin: 0; }
.pass-join {
  font-family: var(--f-display); font-weight: 700; font-size: 12px;
  letter-spacing: .04em; text-transform: uppercase;
  color: var(--accent); background: transparent;
  border: 1.5px solid var(--accent); border-radius: 999px;
  padding: 4px 14px; cursor: pointer;
}
.pass-join:hover { background: var(--accent); color: var(--surface); }

/* ---- Box scores -----------------------------------------------------
   A week's results, two rows to a game. The winner's line stays in ink
   and the loser's drops back, so the outcome reads before either number
   does -- the same way a printed box score is scanned. */
.boxscores { list-style: none; margin: 0 0 24px; padding: 4px 0 0; display: flex; flex-direction: column; gap: 8px; }
.boxscore {
  background: var(--surface); border-radius: 12px; padding: 10px 12px;
  box-shadow: 0 1px 0 var(--line);
}
.boxscore-link { display: block; text-decoration: none; color: var(--ink); }
.boxscore-side {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: 9px;
  padding: 3px 0;
  color: var(--ink-mute);
}
.boxscore-side.is-winner { color: var(--ink); }
.boxscore-name {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 13px;
}
.boxscore-side.is-winner .boxscore-name { font-weight: 700; }
.boxscore-score {
  font-family: var(--f-display); font-size: 17px; font-weight: 700;
  font-variant-numeric: tabular-nums; min-width: 26px; text-align: right;
}
/* A game still to come has no result to rank its two sides by, so
   neither is dropped back. */
.boxscore.is-pending .boxscore-side { color: var(--ink-mute); }
.boxscore-note {
  margin: 5px 0 0;
  font-family: var(--f-mono); font-size: 10.5px; color: var(--ink-mute);
}
.boxscore-note:empty { display: none; }

/* ---- Award list -----------------------------------------------------
   Trophies handed out, on the week's results and the season's own page.
   The name leads and the criterion sits under it, because a trophy here
   is being announced rather than looked up. */
.award-list { list-style: none; margin: 0 0 24px; padding: 4px 0 0; display: flex; flex-direction: column; gap: 10px; }
.award {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 10px 12px;
  background: var(--surface); border-radius: 12px;
  border-left: 3px solid var(--accent-2);
}
.award-body { flex: 1; min-width: 0; }
.award-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
.award-name { font-family: var(--f-display); font-weight: 700; font-size: 14px; }
.award-winners { font-size: 13px; font-weight: 700; color: var(--accent); }
.award-note { font-family: var(--f-mono); font-size: 10.5px; font-weight: 400; color: var(--ink-mute); }
/* How many times a weekly trophy went out this season. Quiet and on the
   name's line -- unstyled it inherited the name's size and wrapped, which
   read as a second title. */
.award-count {
  font-family: var(--f-mono); font-size: 11px; font-weight: 700;
  color: var(--ink-mute); font-variant-numeric: tabular-nums;
}
.award-desc { margin: 4px 0 0; font-size: 12px; color: var(--ink-mute); line-height: 1.35; }

/* A weekly trophy is a log, not a single result: the same award recurs
   all season, so its winners list itself rather than sitting on the
   heading line the way a season trophy's one-off winner does. */
.award.is-weekly { border-left-color: var(--line); }
.award-runs { list-style: none; margin: 8px 0 0; padding: 0; }
.award-runs li {
  display: flex; align-items: baseline; gap: 9px;
  padding: 4px 0; border-top: 1px solid var(--line);
  font-size: 12.5px;
}
.award-when {
  font-family: var(--f-mono); font-size: 10.5px; color: var(--ink-mute);
  min-width: 58px; flex: none;
}
.award-who { font-weight: 700; }
.award-runs .award-note { margin-left: auto; text-align: right; }

/* ---- The record book ------------------------------------------------
   A league across all its seasons. Set denser and flatter than the season
   board on purpose: that one is a live scoreboard you check, this is a
   reference you look something up in. Rules rather than cards. */
.book-totals { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 22px; padding: 0; }
.book-total {
  flex: 1 1 0; min-width: 74px;
  padding: 8px 10px; border-radius: 10px; background: var(--surface);
  box-shadow: 0 1px 0 var(--line);
}
.book-total dt {
  font-family: var(--f-mono); font-size: 9px; font-weight: 700;
  letter-spacing: .07em; text-transform: uppercase; color: var(--ink-mute);
  margin: 0 0 3px;
}
.book-total dd {
  margin: 0; font-family: var(--f-display); font-size: 18px; line-height: 1;
  font-variant-numeric: tabular-nums;
}

.book-head {
  font-family: var(--f-display); font-weight: 700; font-size: 14px;
  letter-spacing: .06em; text-transform: uppercase;
  margin: 26px 0 6px; padding-bottom: 5px;
  border-bottom: 2px solid var(--ink);
}

.book-standings, .book-seasons, .book-trophies { list-style: none; margin: 0; padding: 0; }
.book-row, .book-season {
  display: flex; align-items: baseline; gap: 10px;
  padding: 8px 2px; border-bottom: 1px solid var(--line);
  font-size: 13px;
}
.book-trophy {
  padding: 8px 2px; border-bottom: 1px solid var(--line); font-size: 13px;
}
.book-row:last-child, .book-season:last-child, .book-trophy:last-child { border-bottom: 0; }

.book-rank {
  font-family: var(--f-display); font-size: 13px; color: var(--ink-mute);
  min-width: 18px; font-variant-numeric: tabular-nums;
}
.book-who { flex: 1; min-width: 0; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The dash is a fixed column: wins right-align into it, losses left-align
   out of it, so every row's hyphen sits on the same vertical line no matter
   how many digits either side carries. 4ch each covers a five-figure career
   without the columns going baggy on a young league. */
.book-record {
  font-family: var(--f-mono); font-weight: 700; font-variant-numeric: tabular-nums;
  display: inline-flex; align-items: baseline; flex: none;
}
.book-w { min-width: 4ch; text-align: right; }
.book-l { min-width: 4ch; text-align: left; }
.book-dash { padding: 0 1px; color: var(--ink-mute); }
.book-pct { font-family: var(--f-mono); font-size: 11.5px; color: var(--ink-mute); font-variant-numeric: tabular-nums; }
/* The context that stops a short, hot run from reading like a dynasty. */
.book-span {
  font-family: var(--f-mono); font-size: 10px; color: var(--ink-mute);
  min-width: 62px; text-align: right; flex: none;
}

/* Nothing but the year on the left. It carried a win-condition label for
   a moment, which on a league that has never changed its rules is the same
   word printed once per season. */
.book-season-year {
  font-family: var(--f-mono); font-weight: 700; font-size: 12px;
  flex: 1; color: var(--ink);
}
.book-champion { font-weight: 700; text-align: right; }
.book-inprogress {
  font-family: var(--f-mono); font-size: 10px; font-weight: 400;
  letter-spacing: .06em; text-transform: uppercase; color: var(--accent-2);
}

.book-subhead {
  font-family: var(--f-mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-mute);
  margin: 14px 0 2px;
}
.book-subhead:first-of-type { margin-top: 8px; }
/* The name yields, the holders don't. Both used to shrink, so a long
   trophy name and three holders each got clipped -- and a holder clipped
   mid-name ("mikehollowed 6...") is unreadable in a way a clipped trophy
   name isn't, since the trophy is recognizable from its first words. */
.book-trophy-id { margin: 0 0 4px; display: flex; align-items: baseline; gap: 6px; }
/* Wraps rather than clips: the name has the full width to itself now, and
   the two-line layout exists precisely so nothing has to be cut. */
.book-trophy-name { font-weight: 700; }
/* Uncolored on purpose -- the medals below carry the only color in this
   row, and an accented total would read as one of them. */
.book-trophy-count { font-family: var(--f-mono); font-size: 11.5px; color: var(--ink-mute); font-weight: 700; flex: none; }
/* Gold, silver, bronze left to right, on their own columns so the eye can
   read down a place rather than hunting along a sentence. */
.book-trophy-holders {
  margin: 0;
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 0 8px;
  font-family: var(--f-mono); font-size: 10.5px; color: var(--ink-mute);
}
.book-trophy-holders .book-holder {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.book-trophy-holders b { color: inherit; font-weight: 700; }
/* Text-legible metals. The .trophy-chip tiers above are borders and can sit
   at full saturation; at 10.5px on paper they'd be unreadable, so these are
   darkened for light and lifted for dark. */
.book-holder { color: var(--ink-mute); }
.book-holder.medal-gold   { color: var(--medal-gold); font-weight: 700; }
.book-holder.medal-silver { color: var(--medal-silver); font-weight: 700; }
.book-holder.medal-bronze { color: var(--medal-bronze); font-weight: 700; }

/* Profile career line -- the back of a player's card: the few numbers that
   describe a whole career, set in the data face and given room, rather
   than another list. */
.career {
  display: flex; gap: 8px; margin: 0 0 8px; padding: 0;
}
.career-cell {
  flex: 1;
  padding: 9px 10px;
  background: var(--surface); border-radius: 10px;
}
.career-cell dt {
  font-family: var(--f-mono); font-size: 9.5px; text-transform: uppercase;
  letter-spacing: .07em; color: var(--ink-mute); margin: 0 0 3px;
}
.career-cell dd {
  margin: 0;
  font-family: var(--f-mono); font-size: 15px; font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.season-history { list-style: none; margin: 0 0 24px; padding: 4px 0 0; }
.season-row {
  display: flex; align-items: baseline; gap: 10px;
  padding: 9px 2px; border-bottom: 1px solid var(--line);
}
.season-row:last-child { border-bottom: 0; }
.season-row.is-empty { color: var(--ink-mute); font-size: 13.5px; }
.season-row-who { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 8px; }
.season-row-year { font-family: var(--f-mono); font-weight: 700; font-size: 13px; }
.season-row-league {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 13px; color: var(--ink-mute);
}
.season-row-finish { font-family: var(--f-display); font-size: 14px; color: var(--ink-mute); }
.season-row-finish b { font-size: 16px; color: var(--ink); }
.season-row-finish small { font-size: 10px; margin-left: 3px; }
.season-row-record {
  min-width: 52px; text-align: right;
  font-family: var(--f-mono); font-size: 12px; font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Account links: one per row, matching the season rows directly above
   them rather than sitting as a paragraph of links under a heading that
   introduces a list. Stacked rather than wrapped -- see user_profile.html
   for why the middot-separated run had to go. */
.account-links { list-style: none; margin: 0 0 24px; padding: 4px 0 0; }
.account-links li { padding: 9px 2px; border-bottom: 1px solid var(--line); }
.account-links li:last-child { border-bottom: 0; }
.account-links a { font-size: 14px; }

/* League detail. The tag row reuses .pass-tag so a league's facts read the
   same here as on the pass for it on the landing page. */
.league-tags { display: flex; flex-wrap: wrap; gap: 5px; margin: 0 0 4px; }

.roster { list-style: none; margin: 0 0 24px; padding: 4px 0 0; }
.roster-row {
  display: flex; align-items: baseline; gap: 8px;
  padding: 9px 2px;
  border-bottom: 1px solid var(--line);
}
.roster-row:last-child { border-bottom: 0; }
.roster-who { font-weight: 700; font-size: 14px; }
.roster-role {
  font-family: var(--f-mono); font-size: 9.5px; text-transform: uppercase; letter-spacing: .06em;
  color: var(--ink-mute);
}
/* Pushed right on its own so a member with no graded picks simply has no
   record, rather than the column collapsing the name into it. */
.roster-record {
  margin-left: auto;
  font-family: var(--f-mono); font-size: 12.5px; font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.board-scale {
  margin: -16px 0 24px; padding-left: 2px;
  font-family: var(--f-mono); font-size: 10.5px; color: var(--ink-mute);
}

.board-empty {
  margin: 4px 0 24px; padding: 18px 14px;
  border: 1.5px dashed var(--line); border-radius: 12px;
  color: var(--ink-mute); font-size: 13.5px; text-align: center;
}

.pick-matrix-wrap {
  overflow-x: auto;
  margin: 0 0 20px;
}
.pick-matrix {
  border-collapse: collapse;
  font-size: 12px;
  font-family: var(--f-mono);
  font-variant-numeric: tabular-nums;
}
.pick-matrix th, .pick-matrix td {
  padding: 6px 8px;
  text-align: center;
  white-space: nowrap;
}
.pick-matrix thead th {
  color: var(--ink-mute);
  font-weight: 700;
  font-family: var(--f-body);
  border-bottom: 1.5px solid var(--line);
}
/* Where the numbered weeks end and the named rounds begin. The matrix
   inverts, so the same division is a vertical rule in the users view and
   a horizontal one in the weeks view -- both driven by matrix.split_at,
   since a cell and its column share an index either way round.
   Dashed, matching the week rail's divider: this is a seam in the season,
   not a table border. */
.pick-matrix th.matrix-split,
.pick-matrix td.matrix-split { border-left: 2px dashed var(--line); }
.pick-matrix tr.matrix-split-row th,
.pick-matrix tr.matrix-split-row td { border-top: 2px dashed var(--line); }
/* A little air either side so the rule reads as a seam rather than as a
   cell border crowding the round's name. */
.pick-matrix th.matrix-split,
.pick-matrix td.matrix-split { padding-left: 12px; }
.pick-matrix thead th a {
  color: inherit;
  text-decoration: none;
}
.pick-matrix tbody th {
  text-align: left;
  font-family: var(--f-body);
  font-weight: 700;
  color: var(--ink);
}
.pick-matrix tbody tr:nth-child(even) {
  background: var(--surface-2);
}
/* Freeze the row-label column (the header corner and every row's username
   cell are both this row's first <th>) so it stays visible while the rest
   of a wide matrix scrolls horizontally under it. Needs its own background
   (matching the row-stripe it sits on) since sticky positioning doesn't
   otherwise stop scrolled cells from showing through underneath it. */
.pick-matrix th:first-child {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--surface);
  box-shadow: 1px 0 0 var(--line);
}
.pick-matrix tbody tr:nth-child(even) th:first-child {
  background: var(--surface-2);
}
.matrix-pick {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 2px 8px;
  border-radius: 4px;
  border: 2px solid;
  font-family: var(--f-body);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .02em;
}
.matrix-pick-tie {
  background: var(--surface-2);
  border-color: var(--line);
  color: var(--ink-mute);
}

/* Confetti -- see docs/DESIGN.md's Motion section (pattern 2). The one
   ambient/looping animation in the system, reserved for the #1 standings
   spot. Unlike the diamond glint below, this pattern has no static-but-
   visible fallback: the rank-1 row's existing 2px accent-2 outline (above)
   is already the complete reduced-motion experience, so outside the query
   these squares stay invisible (opacity: 0) rather than sitting frozen
   mid-fall.

   Position, size, color, fall distance, rotation, and delay are randomized
   per piece at render time (see picks/confetti.py) and set as inline
   styles/custom properties on each <span> -- not fixed CSS classes -- so
   every celebratory burst actually looks different, rather than the same
   hand-picked arrangement every time. --rot/--fall feed the one shared
   keyframe below so each piece can have its own arc without a separate
   @keyframes per piece. */
.confetti {
  position: absolute;
  border-radius: 1px;
  opacity: 0;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .confetti { animation: confetti-fall 3.6s ease-in infinite; }
}

@keyframes confetti-fall {
  0%   { transform: translateY(-4px) rotate(0deg); opacity: 0; }
  12%  { opacity: 1; }
  65%  { opacity: 1; }
  100% { transform: translateY(var(--fall, 24px)) rotate(var(--rot, 50deg)); opacity: 0; }
}

.submit-row { padding-top: 4px; }
/* For a row holding more than one button-in-its-own-form (e.g. Accept/
   Decline) -- .button's width:100% otherwise makes each one fill the
   whole row on its own line, stacking them directly on top of each
   other instead of sitting side by side. */
.submit-row-multi { display: flex; gap: 10px; padding-top: 4px; }
.submit-row-multi form { flex: 1; }
.button {
  display: inline-block;
  font-family: var(--f-display); font-weight: 700; font-size: 15px;
  background: var(--accent); color: var(--accent-text); border: 0;
  padding: 13px 20px; border-radius: 12px; cursor: pointer; width: 100%;
  text-align: center; text-decoration: none;
  box-sizing: border-box;
}
.button:hover { filter: brightness(1.05); }
.button.is-submitting {
  opacity: .6;
  pointer-events: none;
  cursor: not-allowed;
}

/* Destructive/leave-type actions -- var(--loss) is already the app's
   established "negative" semantic color (see docs/DESIGN.md), reused
   here rather than introducing a new red. Outlined, not filled, so it
   doesn't read as loud/urgent as the primary accent button next to it. */
.button-danger {
  background: transparent; color: var(--loss); border: 2px solid var(--loss);
  padding: 11px 20px;
}
.button-danger:hover { background: var(--loss); color: var(--accent-text); filter: none; }

/* Understated sibling to a primary action -- outlined and muted, so the
   week-clear reads as secondary to the filled Autofill above it, and to
   Save. */
.button.is-quiet {
  background: transparent; color: var(--ink-mute); border: 1.5px solid var(--line);
}
.button.is-quiet:hover { color: var(--loss); border-color: var(--loss); filter: none; }

/* Autofill toolbar at the top of both pick forms. A vertical stack of
   labelled controls -- "Autofill picks" and, in a straight-up confidence
   league, "Autorank picks" -- then the button that applies both, in its
   own boxed group since it is a compound control. The week-clear below is
   a bare full-width button, not part of this box. See _pick_tools.html. */
.pick-tools {
  display: flex; flex-direction: column; gap: 12px;
  margin: 0 0 14px; padding: 12px 14px;
  border: 1px solid var(--line); border-radius: 12px; background: var(--surface);
}
/* One labelled control: its label sits tight above its select. */
.pick-tool { display: flex; flex-direction: column; gap: 6px; }
.pick-tools-label {
  font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--ink-mute);
}
.pick-tools-select {
  width: 100%; box-sizing: border-box;
  font-family: var(--f-body); font-size: 14px; color: var(--ink);
  background: var(--surface-2); border: 1.5px solid var(--line);
  border-radius: 9px; padding: 10px 12px;
}
.pick-tools-select:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 1px; }

/* Full-width week clear. Sits below the copy box when there is one, or on
   its own otherwise; either way it's a button in its own right, spaced off
   the tickets below it. */
.pick-clear-week { margin: 0 0 18px; }

/* Per-game / per-row "clear this pick": a quiet text action, matching the
   roster's inline actions rather than the loud form buttons. The same
   control on both surfaces -- the ticket wraps it in its own spaced row,
   the sheet drops it straight into the row's control cell. */
.pick-clear {
  background: none; border: 0; padding: 0; cursor: pointer;
  font-family: var(--f-body); font-size: 12px; color: var(--ink-mute);
  text-decoration: underline; text-underline-offset: 2px;
}
.pick-clear:hover { color: var(--loss); }
.pick-clear:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; border-radius: 2px; }
.pick-clear-row { margin-top: 12px; text-align: right; }

.member-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.member-row-actions { display: flex; align-items: center; gap: 10px; }
.member-row-action { background: none; border: 0; padding: 0; font-size: 12px; color: var(--ink-mute); text-decoration: underline; cursor: pointer; }
.member-row-action:hover { color: var(--accent); }
.member-row-action-remove:hover { color: var(--loss); }

.season-block h2 a { color: inherit; text-decoration: none; }

.login-form { background: var(--surface); border-radius: 16px; padding: 20px; margin-top: 16px; }
.login-form p { margin: 0 0 12px; }
.login-form label { display: block; font-size: 12.5px; font-weight: 600; color: var(--ink-mute); margin-bottom: 4px; }
.login-form input[type="text"],
.login-form input[type="email"],
.login-form input[type="password"] {
  width: 100%; padding: 10px 12px; border-radius: 10px; border: 2px solid var(--line);
  font-size: 15px; background: var(--ground); color: var(--ink);
}

.auth-links { margin-top: 14px; font-size: 13px; color: var(--ink-mute); }

/* Confirm stamp -- see docs/DESIGN.md's Components section. Fixed to the
   viewport (not a specific card) so the same element/JS works from any
   page: a rubber-stamp-style confirmation that lands when a pick is
   submitted, reinforcing the "physical ticket" metaphor the rest of the
   design system leans on, rather than a generic toast. */
.confirm-stamp {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) rotate(-8deg);
  opacity: 0;
  pointer-events: none;
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 26px;
  letter-spacing: .1em;
  color: var(--win);
  border: 4px solid var(--win);
  border-radius: 12px;
  padding: 12px 26px;
  background: var(--surface);
  text-transform: uppercase;
  box-shadow: 0 16px 40px -12px rgba(30, 40, 25, .45);
  z-index: 50;
  transition: opacity .2s ease;
}
.confirm-stamp.is-visible { opacity: 1; }

/* Transient error toast -- the error counterpart to .confirm-stamp, for
   the fetch-based save paths (see error-toast.js). Anchored to the bottom
   of the viewport rather than the top of the document: a validation error
   like two games sharing a confidence number is about the form you are
   looking at, and the old .messages block rendered above the header, which
   on a full week of games is several screens away from the save button you
   just pressed. Bottom rather than dead centre so it never covers the rows
   being corrected. */
.error-toast {
  position: fixed;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%) translateY(8px);
  max-width: min(520px, calc(100vw - 32px));
  opacity: 0;
  pointer-events: none;
  background: var(--surface);
  border: 1.5px solid var(--loss);
  color: var(--loss);
  border-radius: 12px;
  padding: 11px 16px;
  font-size: 13.5px;
  font-weight: 600;
  line-height: 1.35;
  text-align: center;
  box-shadow: 0 16px 40px -12px rgba(30, 40, 25, .45);
  z-index: 60;
}
.error-toast.is-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
  pointer-events: auto;
  cursor: pointer;
}

@media (prefers-reduced-motion: no-preference) {
  .error-toast { transition: opacity .2s ease, transform .2s ease; }
  /* The slow dissolve the dwell ends with, distinct from the quick fade in. */
  .error-toast.is-leaving { transition: opacity .9s ease, transform .9s ease; }
}

@media (prefers-reduced-motion: no-preference) {
  .confirm-stamp.is-visible { animation: stamp-in .4s cubic-bezier(.34, 1.56, .64, 1); }
  @keyframes stamp-in {
    0% { transform: translate(-50%, -50%) scale(2.4) rotate(-14deg); opacity: 0; }
    100% { transform: translate(-50%, -50%) scale(1) rotate(-8deg); opacity: 1; }
  }
}

/* Trophy case -- see docs/DESIGN.md's Components section and the
   Trophy Chip mockup linked there. Tier is set via a class
   (.tier-bronze etc.) rather than an inline style, matching how the rest
   of this stylesheet avoids inline styles in templates. */
.trophy-subhead {
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 15px;
  margin: 24px 0 10px;
}

/* Most Backed / Most Faded (see picks/team_affinity.py) -- one card, two
   columns, rather than two full-width stacked lists: three rows a side
   is small enough to sit next to itself, and side by side is the more
   useful shape besides -- the two columns are a comparison, which reads
   faster across a divider than scrolled past.

   The divider is a border on the first column, not a third grid track:
   a track only for a 1px rule would need its own column-gap tuned
   separately from the real one between badge/bar/count, for a line doing
   nothing but sit there. */
.affinity-mode { margin: 0 0 6px; }
.affinity-card {
  display: grid; grid-template-columns: 1fr 1fr;
  background: var(--surface); border-radius: 14px;
  padding: 14px 0; margin: 0 0 14px;
}
.affinity-col { padding: 0 14px; min-width: 0; }
.affinity-col:first-child { border-right: 1px solid var(--line); }
.affinity-col-head {
  font-family: var(--f-mono); font-size: 10px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-mute); margin: 0 0 10px;
}
/* Two lines per row, not three columns on one -- badge and count share a
   line, and the bar gets one to itself below them. On one line the bar
   only ever got whatever badge and count didn't need, which in a column
   half a phone wide wasn't much: a full-size badge alone is most of that
   width. Given its own line, the bar spans the column, and a full-size
   .team-badge (its normal 13px, not shrunk to squeeze in beside a bar)
   fits it easily. */
.affinity-list {
  display: flex; flex-direction: column; gap: 12px;
  list-style: none; margin: 0; padding: 0; font-size: 13px;
}
.affinity-row-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 5px;
}
.affinity-bar { height: 9px; background: var(--surface-2); border-radius: 4px; overflow: hidden; }
.affinity-bar span { display: block; height: 100%; border-radius: 4px; }
/* Backed is --win and Faded is --marker -- not --loss, which the rest of
   this sheet already uses for an actual bad outcome (a missed pick, a
   losing grade). Fading a team isn't losing with it; --marker is the
   ticket's own colour for "not the one you picked" (the punch-mark X),
   which is exactly what this list counts. */
.affinity-bar.is-backed span { background: var(--win); }
.affinity-bar.is-faded span { background: var(--marker); }
.affinity-count {
  font-family: var(--f-mono); font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* Banners, one per row. Squares were tried and reverted: a card narrow
   enough to sit two or three across can hold a name or a description, not
   both, and the expanded half -- award list, tier holes -- had nowhere to
   go.

   The alignment complaint that prompted the experiment was a real bug and
   is fixed below rather than designed around: .trophy-summary-text had no
   flex basis, so with justify-content: space-between the spare width went
   wherever the description was short, and every banner started its text
   at a different x. */
.trophy-grid {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.trophy-chip {
  position: relative;
  background: var(--surface);
  border-radius: 14px;
  padding: 14px 16px;
  box-shadow: 0 1px 2px rgba(27, 35, 30, .06), 0 4px 14px rgba(27, 35, 30, .05);
  border: 2.5px solid var(--tier-color, var(--line));
  cursor: pointer;
}
.trophy-summary-row {
  display: flex;
  align-items: center;
  gap: 14px;
}
/* flex: 1 is the fix. Without it this block sized to its content, and
   space-between handed the leftover width to whichever banner had the
   shorter description -- so the names started at a different x on every
   row. It now takes the width, and every name begins in the same place. */
.trophy-summary-text { flex: 1; min-width: 0; }
.trophy-chip.is-unearned {
  cursor: default;
  opacity: .4;
  border-color: var(--line);
}

.trophy-chip.tier-bronze   { --tier-color: #9C7A54; }
.trophy-chip.tier-silver   { --tier-color: #9CA3AA; }
.trophy-chip.tier-gold     { --tier-color: #D4A62A; }
.trophy-chip.tier-platinum { --tier-color: #5D6FA3; }
.trophy-chip.tier-diamond  { --tier-color: #2FA8E0; overflow: visible; }

/* ---- The trophy object ------------------------------------------------
   A medal for a weekly award, a plaque for a season one -- see
   _trophy_object.html for why they're two shapes rather than one.

   Both hold everything inside themselves: art above, rarity stars below.
   The stars sat underneath the object at first, which read as a badge
   with a caption rather than as one struck or engraved thing.

   The art plate is a slot for a raster. It's already a fixed box with its
   own clip, so dropping the commissioned art in later changes one rule
   and nothing else has to know an image arrived. */
.trophy-object { position: relative; flex: none; display: block; }
.trophy-body {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  box-sizing: border-box;
}
.trophy-plate { display: grid; place-items: center; }
.trophy-monogram {
  font-family: var(--f-display); font-weight: 700;
  line-height: 1; color: var(--tier-color, var(--ink-mute));
}
/* Placeholder art -- see templatetags/trophy_art.py.

   Sized to fill the room the object actually has, the way a phone blows
   up a lone emoji and shrinks a row of them: a trophy case face gets the
   biggest, a 13px flair stamp the smallest. It was set at 26px in a 78px
   medal, which left the art looking like a caption on its own frame.

   line-height 1 because emoji carry generous metrics of their own and
   would otherwise push the plate open. */
.trophy-art { font-size: 44px; line-height: 1; }
.trophy-object.is-plaque .trophy-art { font-size: 36px; }
.trophy-object.is-stamp .trophy-art { font-size: 0.78em; }

/* Grayscale, then tinted to the tier. A full-colour emoji inside a
   bronze or diamond frame reads as two unrelated objects sharing a box --
   the frame says what you've earned and the art says what it's for, and
   they have to look struck from the same thing.

   Done with a filter chain rather than a blend mode: mix-blend-mode
   needs an opaque backdrop to blend against, and these plates are
   transparent or nearly so, which paints the overlay flat over the
   emoji. sepia() lands every glyph on one hue (~35deg) whatever colour
   it started, and hue-rotate carries that to the tier.

   Only tiered objects are tinted. The tint means "how often" -- that is
   the whole content of a tier -- so an object with no tier has nothing
   to say with colour and keeps the emoji's own. That covers every award
   summary, where the row is the trophy and one showing of it: a week's
   winners, a game's, a season's. Draining those to grey read as a
   disabled state for something nobody had failed to win.

   Unearned is the one untiered case that stays grey, because there the
   grey is the message. */
.trophy-object.is-unearned .trophy-art { filter: grayscale(1) contrast(1.05); }
.tier-bronze   .trophy-art { filter: grayscale(1) sepia(1) hue-rotate(-6deg)  saturate(0.85) brightness(0.95); }
.tier-silver   .trophy-art { filter: grayscale(1) sepia(1) hue-rotate(170deg) saturate(0.3)  brightness(1.05); }
.tier-gold     .trophy-art { filter: grayscale(1) sepia(1) hue-rotate(8deg)   saturate(1.7)  brightness(1.05); }
.tier-platinum .trophy-art { filter: grayscale(1) sepia(1) hue-rotate(188deg) saturate(0.8); }
.tier-diamond  .trophy-art { filter: grayscale(1) sepia(1) hue-rotate(162deg) saturate(1.9)  brightness(1.05); }

/* ---- The game-detail line chart --------------------------------------
   Drawn as a chart in a printed programme rather than a dashboard
   sparkline: hairline rules, mono figures in the margin, one inked line
   doing the work. Everything is in points from the home team's side, so
   the rules and the line share an axis -- see picks/charts.py.

   No fills and no gradients. The one thing worth seeing is where the
   moving line sits against the flat ones, and a filled area under it
   would put weight on the space rather than the crossing. */
.line-chart { margin: 18px 0 0; }
.line-chart figcaption {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 10px; flex-wrap: wrap; margin-bottom: 6px;
}
.line-chart-title {
  font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-mute);
}
.line-chart-key {
  display: flex; gap: 10px; flex-wrap: wrap;
  font-family: var(--f-mono); font-size: 9.5px; color: var(--ink-mute);
}
/* A swatch each, so the key needs no colour words. */
.line-chart-key span::before {
  content: ''; display: inline-block; width: 12px; height: 0;
  border-top-width: 2px; margin-right: 4px; vertical-align: middle;
}
.key-margin::before  { border-top-style: solid;  border-top-color: var(--ink); }
.key-closing::before { border-top-style: dashed; border-top-color: var(--accent-2); }
.key-locked::before  { border-top-style: dashed; border-top-color: var(--accent); }
.key-live::before    { border-top-style: dotted; border-top-color: var(--ink-mute); }

/* preserveAspectRatio is none, so the drawing stretches to whatever width
   the ticket gives it. Heights are fixed in px rather than an aspect
   ratio: a chart that grows taller on a wide screen turns a close game
   into a mountain range for no reason. */
.line-chart svg { display: block; width: 100%; height: 140px; }

.chart-grid   { stroke: var(--line); stroke-width: .5; }
.chart-zero   { stroke: var(--line); stroke-width: 1.2; }
.chart-tick {
  font-family: var(--f-mono); font-size: 7px; fill: var(--ink-mute);
  font-variant-numeric: tabular-nums;
}
.chart-closing { stroke: var(--accent-2); stroke-width: 1.2; stroke-dasharray: 5 3; }
.chart-locked  { stroke: var(--accent);   stroke-width: 1.2; stroke-dasharray: 3 3; }
.chart-live    { stroke: var(--ink-mute); stroke-width: 1.2; stroke-dasharray: 1 2.5; opacity: .8; }
.chart-margin  { stroke: var(--ink); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.chart-final   { fill: var(--ink); }
/* Quarter boundaries. Hairline and full-height so they read as the ruled
   divisions of a programme's scoring grid rather than as another series
   -- they carry no value, only a place. */
.chart-period {
  stroke: var(--line); stroke-width: .5; stroke-dasharray: 2 3; opacity: .9;
}
.chart-period-label {
  font-family: var(--f-mono); font-size: 6.5px; fill: var(--ink-mute);
  letter-spacing: .06em;
}
.line-chart-foot {
  margin: 6px 0 0; font-family: var(--f-mono); font-size: 9.5px;
  color: var(--ink-mute); line-height: 1.5;
}

/* ---- Medal: struck metal, one surface, no plates ---------------------- */
/* Sized for art, not for a letter. A banner has the width to spare and
   the face is the whole point of the object -- the monogram is a
   placeholder for a commissioned drawing, and a drawing needs to be
   looked at. The art area is ~58px across inside the rim. */
.trophy-object.is-medal .trophy-body {
  width: 78px; height: 78px; padding: 9px 0 8px;
  justify-content: center; gap: 3px;
  border: 2.5px solid var(--tier-color, var(--line));
  border-radius: 50%;
  background: var(--surface-2);
}
.trophy-object.is-medal .trophy-monogram { font-size: 30px; }

/* ---- Plaque: stock, so the ticket chamfer, and two engraving plates ----
   The outline is two clipped layers rather than a border under a
   clip-path: a clip-path clips everything drawn on the element including
   its border, so a bordered version had its top corners sliced open
   instead of followed. (The standings leader's ring had the same bug for
   the same reason.) Outer is filled with the tier colour and clipped to
   the chamfer; ::before is the same shape inset by the border width and
   filled with the body colour, leaving a 2px outline that traces the cut. */
.trophy-object.is-plaque .trophy-body {
  width: 72px; height: 88px; padding: 7px;
  justify-content: space-between; gap: 5px;
  background: var(--tier-color, var(--line));
  clip-path: polygon(
    10px 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%, 0 10px
  );
}
.trophy-object.is-plaque .trophy-body::before {
  content: '';
  position: absolute; inset: 2.5px;
  background: var(--surface-2);
  clip-path: polygon(
    8.5px 0, calc(100% - 8.5px) 0, 100% 8.5px, 100% 100%, 0 100%, 0 8.5px
  );
}
/* Above the inset fill, which would otherwise cover them. */
.trophy-object.is-plaque .trophy-plate { position: relative; }
/* Screwed-on pieces: the art and the lettering are separate plates on a
   real plaque, and reading as two is what makes it a plaque rather than a
   rounded-off medal. */
.trophy-object.is-plaque .trophy-plate {
  width: 100%; border: 1px solid var(--tier-color, var(--line));
  background: color-mix(in srgb, var(--tier-color, var(--line)) 12%, transparent);
}
.trophy-object.is-plaque .trophy-plate.is-art { flex: 1; }
.trophy-object.is-plaque .trophy-plate.is-rarity { padding: 3px 0; }
.trophy-object.is-plaque .trophy-monogram { font-size: 26px; }

/* Not yet earned: the shape stays, so the case reads as a set with gaps
   in it rather than a shorter list. */
.trophy-object.is-unearned .trophy-body { border-color: var(--line); background: transparent; }
.trophy-object.is-plaque.is-unearned .trophy-body { background: var(--line); }
.trophy-object.is-plaque.is-unearned .trophy-body::before { background: var(--ground); }
.trophy-object.is-unearned .trophy-plate { border-color: var(--line); background: transparent; }
.trophy-object.is-unearned .trophy-monogram { color: var(--line); }

/* The tier scale, matching the chip borders above. Set on the object
   rather than inherited, because it also renders outside a chip. */
.trophy-object.tier-bronze   { --tier-color: #9C7A54; }
.trophy-object.tier-silver   { --tier-color: #9CA3AA; }
.trophy-object.tier-gold     { --tier-color: #D4A62A; }
.trophy-object.tier-platinum { --tier-color: #5D6FA3; }
.trophy-object.tier-diamond  { --tier-color: #2FA8E0; }

/* Choosing flair happens on the trophy, not in a list somewhere else.
   A submit button dressed as a checkbox: one click rather than
   tick-then-save, and no JS in the path. */
.flair-pick { margin: 10px 0 0; }
.flair-toggle {
  display: flex; align-items: center; gap: 7px;
  padding: 0; border: 0; background: none; cursor: pointer;
  font-family: var(--f-body); font-size: 12px; color: var(--ink-mute);
}
.flair-box {
  flex: none; width: 14px; height: 14px;
  border: 1.5px solid var(--line); border-radius: 4px;
  background: var(--surface);
}
.flair-toggle.is-on { color: var(--ink); font-weight: 600; }
/* Only on the worn one, and quieter than the label it follows. */
.flair-hint { color: var(--ink-mute); font-weight: 400; }
.flair-toggle.is-on .flair-box {
  border-color: var(--tier-color, var(--accent));
  background: var(--tier-color, var(--accent));
  /* The same drawn tick the tickets use, cut from the box. */
  clip-path: polygon(
    14% 46%, 0 60%, 38% 100%, 100% 26%, 86% 12%, 38% 72%
  );
  border-radius: 0;
}
@media (hover: hover) {
  .flair-toggle:hover .flair-box { border-color: var(--accent); }
}

/* ---- Stamp: the inline variant worn beside a username ----------------
   Small enough that the plates and the stars can't survive, so it keeps
   the two things that still read: the shape (medal or plaque, meaning
   weekly or season) and the tier colour.

   Everything here is in em, and that is the whole point. A name is set at
   26px in a profile heading, 13px in a record-book row and 10.5px in a
   trophy-case holder list, and a stamp fixed at 13px was small and low in
   the first, right in the second and oversized in the third. In em it is
   the same size relative to the text wherever it lands, and one alignment
   value works for all of them.

   It sits *on* the baseline rather than offset from it: an inline-block's
   bottom edge aligns there by default, so the mark rests on the line the
   name rests on and grows upward from it, the way an ascender does. That
   is what keeps it aligned at every size -- earlier attempts used a
   negative offset, which hung the box below the baseline and did it by a
   different-looking amount in each of the four sizes a name is set at.

   Sized just past cap height so it reads at the weight of the word. Cap
   height exactly (0.72em) was correctly aligned and too timid; this
   reaches roughly the ascenders, which is as tall as anything else in a
   lowercase name. */
.trophy-object.is-stamp {
  display: inline-block; vertical-align: baseline; margin-right: 0.28em;
}
.trophy-object.is-stamp .trophy-body {
  width: 0.92em; height: 0.92em; padding: 0; gap: 0;
  border-width: 0.13em;
}
.trophy-object.is-stamp.is-plaque .trophy-body {
  width: 0.78em; height: 0.98em;
  /* Percentages, not pixels, so the chamfer scales with the box. */
  clip-path: polygon(22% 0, 78% 0, 100% 17%, 100% 100%, 0 100%, 0 17%);
}
.trophy-object.is-stamp.is-plaque .trophy-body::before {
  inset: 0.11em;
  clip-path: polygon(20% 0, 80% 0, 100% 15%, 100% 100%, 0 100%, 0 15%);
}
/* Nothing inside survives at this size. */
.trophy-object.is-stamp .trophy-plate { border: 0; background: none; width: auto; }
.trophy-object.is-stamp .trophy-plate.is-rarity,
.trophy-object.is-stamp .trophy-stars { display: none; }
.trophy-object.is-stamp .trophy-monogram { font-size: 0.62em; }

/* Rarity: how hard the thing is to earn at all, which is a different
   question from the tier colour (how many times you have). Two rows,
   because the formations are exactly that -- 1 / 2 / 1+2 / 2+2 / 2+3 --
   and each silhouette is distinguishable without counting.

   Deliberately not five stars with some dimmed: an empty star is still a
   star at this size and the shape stops being readable. */
.trophy-stars {
  display: flex; flex-direction: column; align-items: center; gap: 1px;
}
.star-row { display: flex; gap: 1.5px; line-height: 0; }
.star-row i {
  /* 7px: a ten-point polygon reads as a dot below about six. */
  display: block; width: 7px; height: 7px;
  background: var(--tier-color, var(--ink-mute));
  clip-path: polygon(
    50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%,
    50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%
  );
}
/* Unearned: the difficulty still reads, because knowing what you're
   chasing is the point of listing it at all. */
.trophy-object.is-unearned .star-row i { background: var(--line); }

.trophy-chip summary {
  list-style: none;
}
.trophy-chip summary::-webkit-details-marker { display: none; }

.trophy-summary-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

.trophy-summary-text {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.trophy-name {
  display: block;
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 15px;
}
.trophy-count {
  font-family: var(--f-mono);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  color: var(--ink-mute);
  align-self: flex-start;
}
.trophy-description {
  font-size: 13px;
  color: var(--ink-mute);
  margin: 2px 0 0;
}

.trophy-holes {
  display: flex;
  gap: 7px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1.5px dashed var(--line);
  align-items: center;
}
.trophy-holes .hole {
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 1.5px solid var(--line);
  background: var(--ground);
}
.trophy-holes .hole.filled {
  border-color: var(--tier-color);
  background: var(--tier-color);
  box-shadow: inset 0 0 0 2px var(--surface);
}
.trophy-holes .tier-word {
  margin-left: auto;
  font-family: var(--f-display);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--tier-color);
}

.trophy-award-list {
  list-style: none;
  margin: 12px 0 0;
  padding: 12px 0 0;
  border-top: 1.5px dashed var(--line);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.trophy-award-list li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  font-size: 13px;
}
.trophy-award-list .when { color: var(--ink); }
.trophy-award-list .when a,
.trophy-award-list .note a {
  color: inherit;
  text-decoration: none;
}
.trophy-award-list .note {
  font-family: var(--f-mono);
  font-variant-numeric: tabular-nums;
  color: var(--ink-mute);
  font-size: 12px;
}

.season-links {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.season-links li {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  font-size: 13.5px;
}

.standings-link { font-size: 13px; font-weight: 600; text-decoration: none; }
.standings-link::after { content: " \2192"; }

.empty-season {
  background: var(--surface); border-radius: 14px; padding: 16px; margin: 10px 0 4px;
  font-size: 13.5px; color: var(--ink-mute);
}
.empty-season p { margin: 0 0 8px; }
.empty-season p:last-child { margin-bottom: 0; }

/* The way into a season, on pages where you're looking at one you haven't
   joined. Same card as .empty-season -- it is one, in effect -- but
   marker-edged rather than quiet grey: the tickets below it are fully
   rendered and read as pickable, so this has to be the thing the eye
   lands on first or it reads as a footnote to a page that isn't working.
   The button inside is the page's only action, so it keeps its normal
   full-width .button/.submit-row treatment. */
.join-prompt {
  background: var(--surface); border-radius: 14px; padding: 16px; margin: 10px 0 14px;
  border-left: 3px solid var(--marker);
}
.join-prompt-head { margin: 0 0 6px; font-size: 14.5px; font-weight: 700; color: var(--ink); }
.join-prompt-body { margin: 0; font-size: 13px; color: var(--ink-mute); }
.join-prompt .submit-row { margin-bottom: 0; }


.field-row { margin-bottom: 12px; }
.field-row label { display: block; font-size: 12.5px; font-weight: 600; color: var(--ink-mute); margin-bottom: 4px; }
.field-row input, .field-row select {
  width: 100%; font: inherit; font-size: 14px; padding: 9px 10px;
  border: 1.5px solid var(--line); border-radius: 8px; background: var(--surface); color: var(--ink);
}
.field-row-check { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
.field-row-check label { font-size: 13.5px; font-weight: 600; margin: 0; }
.pick-block-help { font-size: 12.5px; color: var(--ink-mute); margin: -4px 0 12px; }


/* ---------------------------------------------------------------------
   Phones.

   Everything above is tuned at the .page max-width of 480px, which the
   layout never adapted away from -- there were no width-based breakpoints
   at all, so a 390px iPhone got the 480px design squeezed into 358px of
   content and a 320px SE into 288px. Nothing overflowed; it just got
   progressively tighter in the places that could least afford it.

   430px covers every current iPhone and most Androids. Kept to the few
   things that are measurably worst rather than a wholesale re-layout.
   --------------------------------------------------------------------- */
@media (max-width: 430px) {
  /* 8px back for content, on both the page and the header that has to
     line up with it. */
  .page { padding-left: 12px; padding-right: 12px; }
  .site-header { padding-left: 12px; padding-right: 12px; }

  /* The stub's width override lived here and is gone: it is a percentage
     now, so it is already the same share at every width. See
     .detail-hero-stub. */
  .detail-hero-stub { padding-left: 6px; padding-right: 6px; }

  /* Was nowrap + ellipsis, which truncates from the right -- and the
     "(International)" label lives on the right, so exactly the thing worth
     seeing is what got cut. Wrapping to a second line costs nothing here. */
  .ticket-venue { white-space: normal; overflow: visible; text-overflow: clip; }

  /* A long league name would otherwise push the notification and profile
     icons toward the edge. Truncate the name instead -- it's the least
     important thing in that row, and it's on screen elsewhere. */
  .league-switcher summary,
  .league-indicator {
    display: inline-block;
    max-width: 38vw;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: middle;
  }

  /* Big enough to read at 480px, unnecessarily large once the column is
     100px narrower. */
  .screen-head h1 { font-size: 23px; }
}

/* Diamond glint -- see docs/DESIGN.md's Motion section (pattern 3). A
   sharp, brief flash catching a facet, not an ambient twinkle -- gated
   behind prefers-reduced-motion, with a static-but-visible default. */
.glint {
  position: absolute;
  width: 30px;
  height: 30px;
  opacity: .6;
  pointer-events: none;
  transform: rotate(var(--rot, 0deg));
  filter: drop-shadow(0 0 5px var(--tier-diamond, #2FA8E0));
}
.glint::before,
.glint::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  background: linear-gradient(90deg, transparent, #2FA8E0 30%, #FFFFFF 50%, #2FA8E0 70%, transparent);
}
.glint::before { width: 30px; height: 3.5px; transform: translate(-50%, -50%); }
.glint::after  { width: 3.5px; height: 30px; transform: translate(-50%, -50%); }

.glint.g1 { top: -12px; right: 26px; --rot: 6deg; }
.glint.g2 { top: 4px; right: -12px; width: 22px; height: 22px; --rot: -12deg; }
.glint.g2::before { width: 22px; }
.glint.g2::after  { height: 22px; }
.glint.g3 { bottom: -11px; right: 58px; width: 20px; height: 20px; --rot: 20deg; }
.glint.g3::before { width: 20px; }
.glint.g3::after  { height: 20px; }
.glint.g4 { top: 32%; left: -12px; --rot: -8deg; }

@media (prefers-reduced-motion: no-preference) {
  .glint { animation: glint-flash 4.8s ease-in-out infinite; }
  .glint.g1 { animation-delay: 0s; }
  .glint.g2 { animation-delay: 1.2s; }
  .glint.g3 { animation-delay: 2.4s; }
  .glint.g4 { animation-delay: 3.6s; }
}

@keyframes glint-flash {
  0%   { opacity: .15; transform: rotate(var(--rot, 0deg)) scale(.5); }
  18%  { opacity: 1;   transform: rotate(var(--rot, 0deg)) scale(1.15); }
  30%  { opacity: 1;   transform: rotate(var(--rot, 0deg)) scale(1.05); }
  45%  { opacity: .2;  transform: rotate(var(--rot, 0deg)) scale(.6); }
  100% { opacity: .15; transform: rotate(var(--rot, 0deg)) scale(.5); }
}

.theme-toggle {
  background: none;
  border: 1.5px solid var(--line);
  border-radius: 999px;
  padding: 4px 10px;
  font: inherit;
  font-family: var(--f-body);
  font-size: 11.5px;
  font-weight: 600;
  color: var(--ink-mute);
  cursor: pointer;
  margin-left: 10px;
}
.theme-toggle:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.team-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 3px 10px;
  border-radius: 6px;
  border: 2px solid;
  font-family: var(--f-display);
  min-width: 4.2em;
}

.line-lock-note {
  font-family: var(--f-body);
  font-size: 12px;
  color: var(--ink-mute);
  margin: 0 0 10px;
}

/* Deliberately no box/background/divider here -- bordering this like a
   card made it read as another pickable game rather than commentary on
   the one above it, and a rule between the ticket and its own stats
   turned out to be one divider too many next to the between-games
   divider on .ticket-card-wrap. Just spacing. */
.insights-panel {
  padding: 10px 0 0;
  margin: 10px 0 0;
}
.insights-heading {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--f-mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-mute); margin: 0 0 8px;
}
.insights-heading:not(:first-child) { margin-top: 12px; }
.insight-line {
  font-size: 13px; margin: 0 0 6px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
}
.insight-line:last-child { margin-bottom: 0; }
.insight-line-muted { color: var(--ink-mute); font-size: 12px; }
/* The rest stat, as a heading over one row per team rather than a
   sentence with both teams inline.

   As a sentence it failed twice over. The two sides wrapped, so the
   second team began a line flush left and looked like the separate
   insight below it rather than the other half of this one. And the
   separator between them was a CSS-generated middot on the second
   .insight-rest-side -- an inline-flex box, so an atomic inline that
   cannot break, which meant a wrap carried the middot down and opened
   the new line with a period floating beside the badge. Nothing can hide
   a generated character only when it starts a line.

   The grid answers both: the sides can't interleave because they are
   rows, they can't be confused with a neighbour because the heading owns
   them, and no separator glyph is needed because the columns do the
   separating. Same shape as .team-form directly below, which had already
   solved this for the same reason. */
/* Extra room underneath, because the insight that follows also opens
   with a team badge at the same left edge -- without it, a fourth badge
   in the same column reads as a third row of this block. */
.insight-rest { margin: 0 0 12px; }
.insight-rest-head {
  font-size: 13px; margin: 0 0 4px;
  display: flex; align-items: center; gap: 6px;
}
/* auto auto 1fr: badge, then the rest condition, then the record. The
   first two columns are shared, so both teams' figures line up under
   each other and the two rows can be read as a comparison. */
.insight-rest-rows {
  display: grid; grid-template-columns: auto auto 1fr;
  gap: 5px 10px; align-items: center;
  /* Indented under its heading, which is the other half of telling these
     rows apart from the flush-left insight below them. */
  padding-left: 10px;
  font-size: 12.5px; color: var(--ink-mute);
}
.insight-rest-side { display: contents; }
.insight-rest-days { font-family: var(--f-mono); font-size: 11.5px; letter-spacing: .04em; }
.insight-rest-rows b {
  color: var(--ink); font-weight: 700;
  font-family: var(--f-mono); font-variant-numeric: tabular-nums;
}


/* One row per team: badge in a fixed column, stats in the next, so the
   two teams' numbers line up vertically for comparison. Previously both
   teams shared one wrapping sentence, which stranded the second badge at
   the end of a line with its own text orphaned onto the next. */
/* font-size matches .insight-line so the badge in column 1 comes out the
   same size as the one in the head-to-head line above it -- .team-badge
   sizes itself off the inherited font (including its 4.2em min-width), so
   without this it rendered at the panel's larger base size and read as a
   different, bigger badge than every other one on the page. */
.team-form { display: grid; grid-template-columns: auto 1fr; gap: 6px 10px; align-items: center; margin-top: 10px; font-size: 13px; }
.team-form-row { display: contents; }
.team-form-stats { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; font-size: 12.5px; color: var(--ink-mute); }
.team-form-stat b { color: var(--ink); font-weight: 700; font-family: var(--f-mono); font-variant-numeric: tabular-nums; margin-left: 3px; }
/* Qualifies the whole row's numbers as against-the-spread. Bordered
   rather than plain muted text so it doesn't read as a fourth stat label
   sitting next to "Streak" and "Last 10". Straight-up leagues show
   nothing -- unqualified figures are straight up by default. */
.team-form-mode {
  font-family: var(--f-mono); font-size: 9.5px; letter-spacing: .08em;
  color: var(--ink-mute); border: 1px solid var(--line); border-radius: 4px;
  padding: 1px 4px; line-height: 1.4;
}

/* ---- Stats program --------------------------------------------------
   The stats dashboard set as a printed game program -- the artifact
   DESIGN.md's concept section already names, and the one screen that
   never got the app's direction. It was nine section labels over eight
   pick-matrix tables, borrowing the pick matrix's styling because
   nothing else existed when it was built.

   Everything here is scoped under .program on purpose: .section-label,
   .stat-line-group and .pick-matrix are shared with eight other
   templates, so this restyles the page without touching them.

   No section numbering. Records, Home Favorites, Travel, Rest,
   Divisional, Momentum and the rest are independent -- there is no
   reading order for a number to encode, and numbering them anyway is
   decoration pretending to be structure. */
.program-masthead {
  padding: 2px 0 14px;
  border-bottom: 4px double var(--line);
  margin-bottom: 6px;
}
.program-eyebrow {
  font-family: var(--f-mono); font-size: 10px; letter-spacing: .22em;
  text-transform: uppercase; color: var(--ink-mute); margin: 0 0 6px;
}
.program-masthead h1 {
  font-family: var(--f-display); font-weight: 700; font-size: 30px;
  letter-spacing: .01em; line-height: 1.05; margin: 0;
}
.program-edition {
  font-family: var(--f-mono); font-size: 11px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-mute); margin: 8px 0 0;
  font-variant-numeric: tabular-nums;
}

/* Which edition you're reading, not a control that changes a table --
   the same two sets of figures printed on two sides of the same sheet. */
.program-reading {
  display: flex; align-items: baseline; flex-wrap: wrap; gap: 3px 12px;
  margin: 12px 0 4px;
  font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .12em;
  text-transform: uppercase;
}
.program-reading-label { color: var(--ink-mute); }
.program-edition-btn {
  font: inherit; letter-spacing: inherit; text-transform: inherit;
  background: none; border: 0; padding: 2px 0; cursor: pointer;
  color: var(--ink-mute); border-bottom: 2px solid transparent;
}
.program-edition-btn.is-active { color: var(--ink); border-bottom-color: var(--accent-2); }
.program-edition-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* Standing head: label, then a rule running out to the margin. */
.program-head {
  display: flex; align-items: center; gap: 10px;
  font-family: var(--f-display); font-weight: 700; font-size: 14px;
  letter-spacing: .09em; text-transform: uppercase; color: var(--ink);
  margin: 30px 0 11px;
}
.program-head::after {
  content: ""; flex: 1 1 auto;
  border-top: 2px solid var(--ink); opacity: .22;
}
.program-note {
  font-size: 12px; color: var(--ink-mute); margin: -4px 0 12px; line-height: 1.45;
}

/* Dot leaders, as a program's roster or bill of fare sets them. The
   leader is the container's own ::after reordered between the two
   spans -- a pseudo-element on the label can't flex, and a third span
   in the markup would be a spacer pretending to be content. */
.program-lines { display: flex; flex-direction: column; gap: 9px; margin: 0 0 20px; font-size: 13px; }
.program-line { display: flex; align-items: baseline; }
.program-line .k { order: 1; flex: none; color: var(--ink-mute); }
.program-line::after {
  content: ""; order: 2; flex: 1 1 auto; min-width: 14px;
  margin: 0 7px 4px; border-bottom: 1px dotted var(--line);
}
.program-line .v {
  order: 3; flex: none;
  font-family: var(--f-mono); font-weight: 600; font-variant-numeric: tabular-nums;
}

/* Figures right-aligned under their heads, row labels left, hairline
   rules between rows and nothing vertical -- a results table, not a
   grid of boxes. */
.program-table-wrap { overflow-x: auto; margin: 0 0 22px; }
.program-table {
  border-collapse: collapse; width: 100%;
  font-family: var(--f-mono); font-size: 12px; font-variant-numeric: tabular-nums;
}
.program-table thead th {
  font-size: 9.5px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-mute); font-weight: 700; white-space: nowrap;
  text-align: right; padding: 0 0 7px 14px;
  border-bottom: 1.5px solid var(--line);
}
.program-table thead th:first-child { text-align: left; padding-left: 0; }
.program-table tbody th {
  text-align: left; font-weight: 400; color: var(--ink-mute);
  padding: 7px 14px 7px 0; white-space: nowrap;
}
.program-table tbody td { text-align: right; padding: 7px 0 7px 14px; white-space: nowrap; }
.program-table tbody tr + tr th,
.program-table tbody tr + tr td { border-top: 1px dotted var(--line); }

/* Streak leaders read as a ranked list, so they get numbers -- here the
   order is the information. */
.program-ranks { list-style: none; margin: 0 0 20px; padding: 0; font-size: 12.5px; }
.program-ranks li {
  display: flex; align-items: baseline; gap: 10px;
  padding: 6px 0; border-bottom: 1px dotted var(--line);
}
.program-ranks li:last-child { border-bottom: 0; }
.program-rank {
  font-family: var(--f-mono); font-size: 10px; letter-spacing: .08em;
  color: var(--ink-mute); flex: none; min-width: 2.4em;
}
.program-ranks .who { flex: 1 1 auto; }
.program-ranks .len {
  font-family: var(--f-mono); font-weight: 700; font-variant-numeric: tabular-nums; flex: none;
}
.program-subhead {
  font-family: var(--f-mono); font-size: 10px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-mute); margin: 16px 0 6px;
}

.program-colophon {
  margin-top: 30px; padding-top: 12px; border-top: 1px solid var(--line);
  font-family: var(--f-mono); font-size: 10px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--ink-mute); line-height: 1.7;
}

/* The program as an actual page of paper, in the ticket's vein but not
   its shape: a ticket is cut and torn, a page is bound and turned. So it
   shares the paper stock, the lift and the printed frame, and skips the
   chamfers and scallops -- those say "admit one", which a page doesn't.

   The bound edge on the left is what a page turns around, and what the
   turn animation pivots from. */
.program-page {
  position: relative;
  background: var(--paper);
  box-shadow: var(--paper-lift);
  padding: 20px 18px 22px 22px;
  margin: 4px 0 8px;
}
.program-page::before {
  content: '';
  position: absolute; inset: 6px 6px 6px 10px;
  border: 2px solid var(--ink);
  opacity: .22;
  pointer-events: none;
}
/* Bound edge -- a double rule down the spine side. */
.program-page::after {
  content: '';
  position: absolute; top: 0; bottom: 0; left: 4px;
  border-left: 1px solid var(--line);
  border-right: 1px solid var(--line);
  width: 3px;
  pointer-events: none;
}
.program-page > :first-child { margin-top: 0; }
.program-page > :last-child { margin-bottom: 0; }

/* The eyebrow carries the masthead now, so it's set to be read rather
   than to sit quietly above the title. */
.program-eyebrow {
  font-size: 12px;
  letter-spacing: .32em;
  font-weight: 700;
  color: var(--accent);
  margin: 0 0 8px;
}

/* Editions read as two tabs, not as a labelled control -- which is what
   "Reading:" made it look like. */
.program-reading { gap: 3px 18px; margin: 14px 0 2px; font-size: 11px; }

/* Crossing between the two ways to pick a week. Deliberately a wide,
   obvious target rather than a text link tucked in a corner: it's a
   different view of the same week, not a secondary action, and the
   ticket page gives no other clue that the sheet exists. */
.view-switch {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  margin: 12px 0 4px;
  padding: 10px 14px;
  border: 1.5px solid var(--line);
  border-radius: 12px;
  text-decoration: none;
  color: var(--ink);
}
.view-switch:hover { border-color: var(--accent); }
.view-switch-label { font-family: var(--f-display); font-weight: 700; font-size: 14px; }
.view-switch-label::after { content: " \2192"; color: var(--accent); }
/* Going back the other way, from the sheet to the tickets. The arrow leads
   rather than trails, so the direction reads before the label does. */
.view-switch.is-back .view-switch-label::after { content: none; }
/* \00a0, not a plain space: a trailing space in `content` collapses, so
   the arrow ends up jammed against the label. */
.view-switch.is-back .view-switch-label::before { content: "\2190\00a0"; color: var(--accent); }
.view-switch-note {
  font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .04em;
  color: var(--ink-mute); text-align: right;
}

/* ---- Pick sheet rows ------------------------------------------------
   The week as a table you can pick from. Seven columns:

     [ yours | live | AWAY | @ | HOME | live | yours ]

   The live line is always shown and is the number that gets marked when
   you select a side -- it's never reprinted in an outer column. The
   outer columns carry the line your pick is holding, which is only ever
   on the side you picked, struck through once you give it up.

   Fixed column widths rather than flex: the badges have to sit in the
   same place on every row, and a spread that gains a digit mustn't shove
   them sideways. */
.sheet-row {
  padding: 11px 0;
  border-bottom: 1px solid var(--line);
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px;
}
.sheet-row.has-error { border-bottom-color: var(--loss); }
/* The row's top line is its link into the game, the same way the ticket's
   eyebrow is -- see a.ticket-eyebrow, whose reasoning this follows. It
   gets the same tinted strip so it reads as a control rather than as a
   caption that happens to be tappable.

   Lighter than the ticket's version on purpose: a sheet row is a third
   the height of a ticket, and the strip that gives a ticket its heading a
   sense of being pressable would dominate a row that is mostly this
   line. */
.sheet-row-kickoff {
  flex-basis: 100%;
  font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .08em;
  color: var(--ink-mute);
  display: flex; justify-content: space-between; align-items: baseline; gap: 10px;
  text-decoration: none;
  margin: -1px -6px 1px; padding: 3px 6px 4px;
  border-radius: 5px;
}
/* The date yields first if the row is ever narrower than its content:
   truncating the time still leaves the day readable, where dropping the
   status loses whether the game is live. Same call as the ticket. */
.sheet-row-kickoff > span:first-child {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.sheet-row-end { flex: none; display: inline-flex; align-items: baseline; gap: 2px; }
/* LIVE is about the game, not about this being a link. */
.sheet-live { color: var(--accent-2); }
.sheet-row-arrow { transition: transform .12s ease; }
@media (hover: hover) {
  a.sheet-row-kickoff:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }
  a.sheet-row-kickoff:hover .sheet-row-arrow { transform: translateX(2px); }
}
a.sheet-row-kickoff:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) {
  .sheet-row-arrow { transition: none; }
}
.sheet-matchup {
  flex: 1 1 auto;
  display: grid;
  /* max-content, not auto, on the middle three: an auto column soaks up
     the row's spare width, which pushed the badges apart from the @
     between them. Sized to content and centred, the middle three sit
     together and the spare width falls outside the grid where it
     belongs. (Safe here because justify-content is center, so the grid
     never stretches its tracks to fill.)

     The outer pair -- .sheet-yours, the line a pick is holding after the
     board has moved off it -- is content-sized rather than a fixed
     3.4em, because it is empty on almost every row. Held at 3.4em the
     seven columns wanted 327px against the 369px a phone has, so the
     confidence stepper was pushed onto a second line under every single
     game and each row cost twice the height it needed. Content-sized it
     comes to 200px and the stepper sits back on the line.

     One rule, not a phone-only override: a row that reflows at one width
     and not another is two designs. The cost is that a row which *is*
     holding a moved line sits its badges slightly off-centre against its
     neighbours -- at every width now, which at least makes it a
     consistent quirk rather than a surprise on one device.

     gap is 4px, not 5: on the row with the widest content -- two
     three-letter abbreviations plus a two-digit spread on both sides
     (e.g. ARI +10.5 @ LAC -10.5) -- the six internal gaps were the
     difference between fitting the stepper beside the matchup at 393px
     and pushing it to a second line under just that one row. 1px was
     the whole margin; measured on a picked row (stepper + Clear button,
     the widest .sheet-row-control gets), the two together came to
     372px against a 369px phone, 3px over. */
  grid-template-columns: auto auto max-content max-content max-content auto auto;
  justify-content: center;
  align-items: center;
  gap: 4px;
  font-size: 13px;
}
.sheet-at { color: var(--ink-mute); font-size: 12px; text-align: center; }

.sheet-line,
.sheet-yours {
  font-family: var(--f-mono); font-size: 11.5px; font-variant-numeric: tabular-nums;
  color: var(--ink-mute);
}
/* Outer columns hug the badges; live columns sit just outside them. */
.sheet-yours:first-child, .sheet-line:nth-child(2) { text-align: right; }
.sheet-yours:first-child { align-items: flex-end; }
.sheet-yours:last-child, .sheet-line:nth-child(6) { text-align: left; }
.sheet-yours:last-child { align-items: flex-start; }

/* Selected: the live line you're on. */
.sheet-line.is-selected { color: var(--ink); font-weight: 700; }
/* Taking it: the line you'd lock if you saved now. Affirmative, not
   alarming -- you're choosing this, and --win is the only colour in the
   palette that means "good outcome". Red would read as a warning about
   a decision you just made on purpose. */
.sheet-line.is-taking { color: var(--win); font-weight: 700; }
/* Offered but not yet agreed to: tappable, and says so on hover. */
.sheet-line.is-takeable { cursor: pointer; text-decoration: underline dotted; text-underline-offset: 3px; }
/* Scoped to .sheet-line rather than .sheet-line.is-takeable: once a line
   is taken, the pick-sheet script drops the is-takeable class from this
   same label in place (no reload -- see the fetch success handler) but
   leaves the checkbox itself in the DOM. A rule gated on is-takeable
   stops matching at that exact moment and the checkbox reverts to its
   native, unstyled appearance -- on iOS Safari specifically, which
   paints a checkbox's native control inside this clip despite it being
   sized to 1px (unlike .sheet-team's radio just below, which the same
   clip hides cleanly on the same device). Confirmed by screenshot: a
   solid ~20px square appeared between the "yours" value and the live
   one on a just-relocked row, wide enough to push .sheet-row-control
   onto its own line under the badges. This input has no other visual
   job at any point in its life, taken or not, so hiding it doesn't need
   is-takeable's help. */
.sheet-line input {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
  -webkit-appearance: none; appearance: none; background: transparent;
}
.sheet-line.is-takeable:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The line you hold, and the moment you give it up. Struck and dimmed
   is enough on its own -- nothing here is an error, so no colour needs
   to say so.

   Captioned "yours" rather than "locked": this page already says locked
   about a game that has kicked off, and the two would read as the same
   thing. A lock glyph has the same problem without the word to
   disambiguate it. */
.sheet-yours { display: inline-flex; flex-direction: column; line-height: 1.15; }
.sheet-yours-value { font-weight: 600; color: var(--ink); }
.sheet-yours-tag {
  font-size: 8.5px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-mute); opacity: .8;
}
.sheet-yours.is-dropped .sheet-yours-value {
  text-decoration: line-through;
  color: var(--ink-mute);
  opacity: .7;
  font-weight: 400;
}
.sheet-yours.is-dropped .sheet-yours-tag { opacity: .45; }

.sheet-team { display: inline-flex; cursor: pointer; opacity: .42; transition: opacity .12s ease; }
.sheet-team.is-picked { opacity: 1; }
.sheet-team input {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.sheet-team:has(input:focus-visible) .team-badge {
  outline: 2px solid var(--accent); outline-offset: 2px;
}
@media (hover: hover) {
  .sheet-team:hover { opacity: .75; }
  .sheet-team.is-picked:hover { opacity: 1; }
}

/* Holds either the stepper or, once the game has started, the rank chip
   beside how the pick is doing. Baseline-aligned rather than centred so
   the word sits on the same line as the numbers in the row beside it. */
.sheet-row-control {
  flex: none; display: flex; align-items: center; gap: 7px;
}
/* A confidence number that no longer matches what's saved, marked the
   same way a taken line is -- one visual language for "not saved yet". */
.confidence-stepper.is-changed .confidence-input {
  color: var(--win); font-weight: 700; border-color: var(--win);
}
.sheet-row-errors {
  flex-basis: 100%; font-size: 12.5px; color: var(--loss); font-weight: 600;
}
.sheet-row-errors p { margin: 0; }

/* Ranking on the ticket, under the game's own stats -- where the number
   is actually decided. The tray at the top of the page says which are
   still free. */
.confidence-rank {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px 10px;
  margin-top: 10px;
}
.confidence-rank-label {
  font-family: var(--f-mono); font-size: 10.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-mute);
}
.confidence-rank.has-error .confidence-input { border-color: var(--loss); }
.confidence-rank-errors {
  flex-basis: 100%; font-size: 12.5px; color: var(--loss); font-weight: 600;
}
.confidence-rank-errors p { margin: 0; }
