/* 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;

  --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;
    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;
  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;
  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); }

.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); }
.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;
}

.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)); }

.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;
}
.detail-hero-main { flex: 1; min-width: 0; padding: 14px 14px 12px; font-family: var(--f-mono); }
.ticket-eyebrow { font-size: 10.5px; letter-spacing: .18em; color: var(--ink-mute); display: flex; justify-content: space-between; }

/* 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: 15px; font-weight: 600; letter-spacing: .01em; line-height: 1.55; text-transform: uppercase; }
.ticket-matchup div { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ticket-matchup .at { color: var(--ink-mute); font-weight: 400; padding: 1px 0; }
.ticket-matchup .fav { position: relative; }
.ticket-matchup .fav::after { content: "\2605"; font-size: 9px; margin-left: 6px; color: var(--accent-2); vertical-align: middle; }

/* 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);
}
.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: 3px; column-gap: 8px;
  font-size: 11px; letter-spacing: .08em;
}
.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; }

/* Narrowed from the original 148px, then widened back up a bit to
   128px -- reclaims width for the main ticket area (long team names)
   while still leaving enough room for the stub's own content. */
.detail-hero-stub {
  width: 128px; 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; }
/* 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; }

.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-list { list-style: none; margin: 0; padding: 4px 0 24px; display: flex; flex-direction: column; gap: 10px; }
.standings-row {
  display: flex; align-items: center; gap: 12px;
  background: var(--surface); border-radius: 14px; padding: 12px 14px;
  box-shadow: 0 1px 0 var(--line);
}
.standings-row.rank-1 { position: relative; overflow: hidden; box-shadow: 0 0 0 2px var(--accent-2); }
.standings-row .rank { font-family: var(--f-display); font-weight: 700; font-size: 16px; color: var(--ink-mute); width: 18px; }
.standings-row.rank-1 .rank { color: var(--accent-2); }
.standings-row .who { flex: 1; min-width: 0; font-weight: 700; font-size: 14.5px; }
.standings-row .record-cell { text-align: right; }
.standings-row .pct { display: block; font-family: var(--f-mono); font-weight: 700; font-size: 14px; font-variant-numeric: tabular-nums; }
.standings-row .wl { font-size: 10.5px; color: var(--ink-mute); font-variant-numeric: tabular-nums; }

.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);
}
.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; }

.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;
}

.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-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; }

.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 {
  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);
}
.trophy-description {
  font-size: 13px;
  color: var(--ink-mute);
  margin: 0;
}
.trophy-chip.is-unearned .trophy-description {
  margin-top: 6px;
}

.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;
  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; }

/* Past-seasons dropdown -- same <details> disclosure pattern as the
   header's league-switcher, just with plain <a> links instead of
   POST-switch buttons, since viewing a season isn't a state change. */
.season-dropdown { margin: 4px 0 8px; position: relative; }
.season-dropdown summary {
  cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--f-body); font-weight: 600; font-size: 13.5px; color: var(--ink);
  padding: 8px 12px; border: 1.5px solid var(--line); border-radius: 10px;
}
.season-dropdown summary::-webkit-details-marker { display: none; }
.season-dropdown summary::after { content: "\25BE"; font-size: 10px; color: var(--ink-mute); }
.season-dropdown[open] summary::after { transform: rotate(180deg); }
.season-dropdown ul {
  list-style: none; margin: 4px 0 0; padding: 6px;
  position: absolute; left: 0; background: var(--surface); border: 1px solid var(--line);
  border-radius: 10px; box-shadow: 0 4px 12px rgba(27,35,30,.12); min-width: 200px; z-index: 5;
}
.season-dropdown li a { display: block; padding: 7px 10px; border-radius: 7px; color: var(--ink); text-decoration: none; font-size: 13.5px; }
.season-dropdown li a:hover { background: var(--surface-2); }

.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 pick stub is a fixed 128px, so its share of the row grows as the
     screen shrinks -- 28.6% at the design width but 44.4% on an SE, taking
     it straight out of the team names next to it. */
  .detail-hero-stub { width: 108px; 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; }
/* One team's rest figure. Grouped in its own inline-flex so the badge,
   the day count and the record can't be split across a wrap -- the two
   sides wrap as units instead of interleaving. */
.insight-rest-side { display: inline-flex; align-items: center; gap: 5px; }
.insight-rest-side + .insight-rest-side::before { content: '\00b7'; color: var(--ink-mute); }
.insight-rest-side 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; }
