/* PUNT -- the sticker album.
 *
 * Every colour and font in the app is a variable defined here, and nothing else
 * declares a literal colour. That is not tidiness for its own sake: the design
 * lookbook offered six directions and the templates are written against
 * semantic names, so a second theme is a second variables block rather than a
 * second set of templates.
 *
 * Palette and type are Direction 06 from the lookbook: Panini foil, Anton for
 * names and scores, Roboto Mono for the stat block.
 */

:root {
  color-scheme: dark;

  /* Ground */
  --bg: #0d1017;
  --bg-raised: #151a24;
  --bg-sunken: #090b10;
  --border: #232b3a;
  --border-bright: #33405a;

  /* Ink */
  --ink: #eef1f7;
  --ink-dim: #9aa6bd;
  /* Lightened from #64708a, which measured 3.51:1 on the raised panel ground
   * and is used for every sub-line, table header and caption -- including the
   * Cheer tab's reasons, which are that tab's entire content. Same hue and
   * saturation, lifted until it clears WCAG AA on all three grounds
   * (4.52, 4.93 and 5.10 to one). tools/a11y.py checks it. */
  --ink-faint: #76829c;

  /* Brand. Blue is marcdeller.com's; the rest are the foil. */
  --brand: #1e73be;
  --brand-bright: #4a9fd4;
  --violet: #9b51e0;
  --magenta: #ff2fd0;
  --green: #00d084;
  --amber: #fcb900;
  --red: #ff4d5e;

  /* Rarity. Named for the tier, not the colour, so the tiers can be restyled. */
  --tier-common: #4a5568;
  --tier-rare: #4a9fd4;
  --tier-epic: #9b51e0;
  --tier-legendary: #fcb900;
  --tier-cursed: #6b6b6b;

  /* Type */
  --font-display: Anton, 'Arial Narrow', Impact, sans-serif;
  --font-body: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
  --font-mono: 'Roboto Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  /* Chrome. Both are read from JS as well, so they live here rather than in a
   * magic number in two places. */
  --tabbar-height: 56px;
  --header-height: 52px;
  --radius: 10px;
  --radius-lg: 16px;
}

/* TV mode is a scale factor and a chrome removal, not a separate stylesheet.
 * `?tv=1` sets this attribute and everything below keys off it.
 *
 * The scale is `zoom` rather than a root font size, because every size in this
 * sheet is in px: a 22px root changes nothing at all, which is exactly what the
 * first Big Board capture showed -- a 1280x720 bar screen rendering phone-sized
 * type. Phase 5 owns making this genuinely readable from twelve feet; this makes
 * it honest in the meantime. Safe here because TV mode has no fixed chrome.
 *
 * Back to the 2.2 the plan asks for, now that the Big Board shows one matchup at
 * a time: it ran at 1.6 while all five were on screen at once, because at 2.2 a
 * 720p screen fits two and a half of them. */
:root[data-tv='1'] {
  --tabbar-height: 0px;
  --header-height: 34px;
  zoom: 2.2;
}

/* At 2.2x the chrome is 2.2x too, and a 720p screen does not have the room. The
 * banners in particular are written for a phone held at arm's length, not for a
 * television across a bar: full width and three lines of explanation there is a
 * third of the screen spent on something nobody is reading. */
:root[data-tv='1'] .banner {
  margin: 6px 14px 0;
  padding: 5px 10px;
  font-size: 10px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
:root[data-tv='1'] main { padding: 8px 14px 14px; }
:root[data-tv='1'] .stack > * + * { margin-top: 8px; }
:root[data-tv='1'] .panel { padding: 10px 14px; }
:root[data-tv='1'] .panel-title { margin-bottom: 6px; }
:root[data-tv='1'] .wordmark { font-size: 20px; }

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

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 15px;
  line-height: 1.45;
  /* Chrome's pull-to-refresh reloads the whole app when somebody swipes down on
   * the album, which on a phone is most of the gestures anybody makes. */
  overscroll-behavior: none;
  -webkit-tap-highlight-color: transparent;
  -webkit-text-size-adjust: 100%;
}

body {
  /* 100dvh, never 100vh: the address bar collapsing mid-scroll changes vh and
   * produces a visible jump, which is the most obvious tell that an installed
   * app is really a web page. */
  min-height: 100dvh;
  padding-bottom: calc(var(--tabbar-height) + env(safe-area-inset-bottom));
}

/* Nothing in the app chrome is selectable text; the scores are. */
header, nav, .chrome { user-select: none; }
a { color: var(--brand-bright); text-decoration: none; }

/* Emphasis is semibold, not bold. `<b>` and `<strong>` default to 700, and the
 * design never asks for 700 anywhere -- so an unstyled `<b>` in the commentary
 * feed was pulling a whole extra 48 kB font face that nothing else used.
 * Measured: the browser fetched inter-700-latin.woff2 on every page load. */
b, strong { font-weight: 600; }

/* A focus ring that can actually be seen on this ground. The browser default is
 * a 1px `auto` outline, which on a near-black page is close to invisible --
 * measured, not assumed: tools/a11y.py reported `focus=auto 1px` on every route.
 * `:focus-visible` rather than `:focus`, so a tap does not leave a ring behind. */
:focus-visible {
  outline: 2px solid var(--magenta);
  outline-offset: 2px;
  border-radius: 4px;
}

/* Announced to a screen reader, drawn for nobody. The scores update silently
 * under htmx, so without this a reader hears nothing happen all afternoon. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- header ---------- */

.app-header {
  position: sticky;
  top: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  gap: 10px;
  height: calc(var(--header-height) + env(safe-area-inset-top));
  padding: env(safe-area-inset-top) 14px 0;
  background: linear-gradient(180deg, #131826 0%, #0f131d 100%);
  border-bottom: 1px solid var(--border);
}

.wordmark {
  /* 44px tall even though the glyphs are 26: it is a link in the top corner and
   * the minimum applies to it like everything else. */
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-right: 4px;
  font-family: var(--font-display);
  font-size: 26px;
  letter-spacing: 0.06em;
  line-height: 1;
  background: linear-gradient(92deg, var(--brand-bright), var(--violet) 45%, var(--magenta));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.header-meta {
  flex: 1;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--ink-faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.mute-toggle {
  flex-shrink: 0;
  min-width: 44px;
  min-height: 44px;
  display: grid;
  place-items: center;
  background: none;
  border: 1px solid var(--border-bright);
  border-radius: 999px;
  color: var(--ink-dim);
  font-size: 15px;
  cursor: pointer;
}

/* ---------- banners ---------- */

.banner {
  margin: 10px 14px 0;
  padding: 9px 12px;
  border-radius: var(--radius);
  border: 1px solid;
  font-size: 13px;
}
.banner--stale { border-color: #6b5a1a; background: #2a2410; color: #f3d78a; }
.banner--demo  { border-color: #3a2a5e; background: #1c1430; color: #d0b6ff; }
.banner--error { border-color: #6b2028; background: #2a1114; color: #ffb3bb; }
.banner code { font-family: var(--font-mono); font-size: 12px; }
.banner-action {
  display: inline-flex;
  align-items: center;
  margin-left: 4px;
  min-height: 44px;
  padding: 5px 14px;
  font: inherit; font-weight: 600;
  color: inherit;
  background: rgba(255, 255, 255, .08);
  border: 1px solid currentColor;
  border-radius: 999px;
  cursor: pointer;
  touch-action: manipulation;
}

/* ---------- layout ---------- */

main { padding: 12px 14px 24px; }
.stack > * + * { margin-top: 12px; }

.panel {
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 14px;
}

.panel-title {
  font-family: var(--font-display);
  font-size: 15px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin: 0 0 10px;
}

/* Every panel has one. "Never a blank screen" is enforced by there being a
 * styled component for the empty case, not by each template remembering. */
.empty {
  padding: 22px 14px;
  text-align: center;
  color: var(--ink-faint);
  font-size: 13px;
  border: 1px dashed var(--border-bright);
  border-radius: var(--radius);
}
.empty strong { display: block; color: var(--ink-dim); font-size: 14px; margin-bottom: 4px; }

/* ---------- matchup ---------- */

/* Two stacked rows, not two columns. At 390 px a side-by-side pair gives each
 * team about 130 px, which is not enough for a name a manager actually chose. */
.matchup + .matchup { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }

.mside {
  display: grid;
  grid-template-columns: 34px minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  padding: 5px 0;
}

.crest {
  width: 34px; height: 34px;
  border-radius: 9px;
  background: linear-gradient(135deg, hsl(var(--hue) 62% 46%), hsl(var(--hue) 58% 28%));
  display: grid; place-items: center;
  font-family: var(--font-display); font-size: 12px; letter-spacing: .04em;
  color: rgba(255,255,255,.94);
  overflow: hidden;
}
.crest img { width: 100%; height: 100%; object-fit: cover; }

/* minmax(0, 1fr) on the track plus this is what actually lets the name ellipsis:
 * a grid item's automatic minimum size is its content, so without both it
 * refuses to shrink and pushes the score off the row instead. */
.mside-id { min-width: 0; }
.mside-id b {
  display: block;
  font-size: 14.5px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mside-id span {
  display: block;
  font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.mside-score { text-align: right; }
.mside-score b {
  display: block;
  font-family: var(--font-display); font-size: 25px; line-height: 1;
  font-variant-numeric: tabular-nums;
}
.mside-score span {
  display: block;
  font-family: var(--font-mono); font-size: 9.5px; color: var(--ink-faint);
}
/* Leading is marked twice: in colour and in a shape. Colour alone excludes
 * roughly one man in twelve from the single most important fact on the screen,
 * and this is an app for a room full of them. */
.mside--leading .mside-score b { color: var(--green); }
.mside--leading .mside-score b::before {
  content: '\25B2';
  font-size: 9px;
  vertical-align: 4px;
  margin-right: 4px;
  color: var(--green);
}

/* ---------- album grid ---------- */

/* Ten teams is 2x5 on a phone and 5x2 on the TV, and both fit without
 * scrolling. That is worth protecting: it means no pagination and no
 * virtualisation anywhere in the album. */
.album-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }

/* While the pack is sealed the cards exist and are face-down, but ten invisible
 * cards leave a screen-high void under the packet. Collapsed rather than
 * `display: none` so they keep their layout for the reveal animation. */
.album-grid--sealed { max-height: 0; overflow: hidden; }
:root[data-tv='1'] .album-grid { grid-template-columns: repeat(5, 1fr); }

/* A card is a stack of four layers, all of which move only on `transform` and
 * `opacity`. No filters, no blend modes: both are frame-rate killers on mobile
 * and the budget is 60 fps on an iPhone 12 with ten of these on screen.
 *
 *   .card          the plate, tinted in the team's own deterministic hue
 *   ::before       the foil sheen, one rotated gradient translated across
 *   ::after        the tier treatment (holographic, cracked, nothing)
 *   > children     the content
 *
 * `--tx` and `--ty` are written by the tilt handler in card.js, in the range
 * -1 to 1. With no tilt they stay at 0 and the card is simply flat, so every
 * treatment below degrades to something deliberate rather than to nothing.
 */
.card {
  --tx: 0;
  --ty: 0;
  --tier-colour: var(--tier-common);
  position: relative;
  aspect-ratio: 5 / 7;
  border-radius: 12px;
  padding: 10px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  overflow: hidden;
  background: linear-gradient(160deg, hsl(var(--hue) 42% 24%), hsl(var(--hue) 46% 13%));
  border: 1px solid var(--tier-colour);
  /* A shared 3D context so the tilt reads as a tilt rather than a skew, and so
   * the two faces can sit back to back for the flip. `--flip` is 0 face-up and
   * 1 face-down; the pack rip animates it. */
  --flip: 0;
  transform-style: preserve-3d;
  transform: perspective(700px)
             rotateY(calc(var(--tx) * 9deg + var(--flip) * 180deg))
             rotateX(calc(var(--ty) * -9deg))
             scale(var(--pop, 1));
  transition: transform 120ms ease-out;
  will-change: transform;
}

/* Two faces, back to back. `backface-visibility` is what makes the flip a flip
 * rather than a mirrored front. */
.card-face {
  position: absolute;
  inset: 0;
  padding: 10px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  border-radius: 12px;
  overflow: hidden;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.card-face--back {
  transform: rotateY(180deg);
  background:
    repeating-linear-gradient(48deg, rgba(255,255,255,.028) 0 2px, transparent 2px 7px),
    linear-gradient(150deg, #16203a 0%, #0d1220 100%);
  display: grid;
  place-items: center;
}
.card-back-mark {
  font-family: var(--font-display);
  font-size: 26px;
  letter-spacing: .07em;
  background: linear-gradient(92deg, var(--brand-bright), var(--violet) 45%, var(--magenta));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  opacity: .82;
}

/* The foil. One rotated linear gradient, translated on `transform` only: the
 * cheap trick that sells the whole conceit. It tracks the tilt, so on a phone
 * the light moves when the phone does. */
.card-face--front::before {
  content: '';
  position: absolute;
  inset: -40%;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(
    104deg,
    transparent 34%,
    rgba(255, 255, 255, 0.10) 44%,
    rgba(255, 255, 255, 0.34) 50%,
    rgba(255, 255, 255, 0.10) 56%,
    transparent 66%
  );
  transform: translate3d(calc(var(--tx) * 26%), calc(var(--ty) * 18%), 0);
  transition: opacity 200ms ease;
}

.card-face--front::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
}

.card-tier {
  font-family: var(--font-mono); font-size: 8px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--tier-colour);
}
.card-name { font-family: var(--font-display); font-size: 14px; line-height: 1.05;
             overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.card-score { font-family: var(--font-display); font-size: 30px; line-height: .95; }
.card-stats { font-family: var(--font-mono); font-size: 9px; color: var(--ink-dim); line-height: 1.5; }

.card-crest {
  flex: 1;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-size: 34px;
  letter-spacing: .06em;
  color: rgba(255, 255, 255, .13);
  overflow: hidden;
}
.card-crest img { width: 54%; height: auto; opacity: .5; border-radius: 8px; }

/* Content sits above both pseudo-elements. */
.card-face--front > * { position: relative; z-index: 1; }

/* ---- rarity treatments ---- */

/* Common: matte. The foil stays at zero opacity, which is the point of it. */

/* Rare: a subtle sweep that only appears as the card is tilted. */
.card--rare { --tier-colour: var(--tier-rare); }
.card--rare .card-face--front::before { opacity: .5; }

/* Epic: the sweep animates on its own as well as tracking the tilt, and the
 * edge picks up the tier colour. */
.card--epic { --tier-colour: var(--tier-epic); box-shadow: inset 0 0 0 1px rgba(155, 81, 224, .5); }
.card--epic .card-face--front::before { opacity: .85; animation: foil-sweep 4.5s ease-in-out infinite; }

/* Legendary: full holographic. Two gradients at different angles, both moving,
 * which is what separates a hologram from a shine. */
.card--legendary { --tier-colour: var(--tier-legendary);
                   box-shadow: inset 0 0 0 1px rgba(252, 185, 0, .55), 0 0 18px rgba(252, 185, 0, .16); }
.card--legendary .card-face--front::before { opacity: 1; animation: foil-sweep 3.2s ease-in-out infinite; }
.card--legendary .card-face--front::after {
  opacity: .38;
  background: repeating-linear-gradient(
    calc(112deg + var(--tx) * 14deg),
    rgba(255, 47, 208, .30) 0px, rgba(155, 81, 224, .30) 9px,
    rgba(74, 159, 212, .30) 18px, rgba(0, 208, 132, .30) 27px,
    rgba(252, 185, 0, .30) 36px, rgba(255, 47, 208, .30) 45px
  );
  transform: translate3d(calc(var(--tx) * 8%), calc(var(--ty) * 8%), 0);
}

/* Cursed: desaturated and cracked. Cursed cards are the point of the album and
 * have to look as considered as Legendary ones.
 *
 * The desaturation is a translucent grey plate rather than `filter: saturate()`.
 * A filter forces the whole subtree onto its own composited layer and re-rasters
 * it on every tilt frame, and with two or three cursed cards on screen that is
 * the frame budget gone. An overlay costs one extra paint of one rectangle. */
.card--cursed { --tier-colour: var(--tier-cursed); border-style: dashed; }
.card--cursed .card-face--front::before {
  opacity: .22;
  background: linear-gradient(104deg, transparent 40%, rgba(255,255,255,.18) 50%, transparent 60%);
}
.card--cursed .card-face--front::after {
  opacity: 1;
  background:
    linear-gradient(0deg, rgba(90, 92, 96, .55), rgba(90, 92, 96, .55)),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 140'%3E%3Cg stroke='rgba(255,255,255,0.16)' fill='none' stroke-width='0.7'%3E%3Cpath d='M18 0 L34 42 L22 68 L41 96 L30 140'/%3E%3Cpath d='M34 42 L62 33 L86 58'/%3E%3Cpath d='M41 96 L70 88 L100 104'/%3E%3Cpath d='M62 33 L58 6'/%3E%3Cpath d='M22 68 L0 79'/%3E%3C/g%3E%3C/svg%3E");
  background-size: cover;
}

@keyframes foil-sweep {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}

/* Reduced motion keeps the treatments, loses the movement. Somebody who has
 * asked for less motion should still be able to tell a Legendary card from a
 * Common one. */
@media (prefers-reduced-motion: reduce) {
  .card { transition: none; transform: none; }
  .card-face--front::before { animation: none; }
}

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

/* `table-layout: fixed` is load-bearing, not tidiness. With auto layout a cell
 * containing a `white-space: nowrap` sub-line expands the column to fit it, the
 * table grows past the viewport, and the numeric columns on the right are simply
 * gone -- with no scrollbar and nothing to suggest they exist. Fixed layout makes
 * the first column absorb whatever is left and lets the ellipsis do its job. */
.rows { width: 100%; table-layout: fixed; border-collapse: collapse; font-size: 13px; }
.rows th.num, .rows td.num { width: 66px; }
.rows th.num-narrow, .rows td.num-narrow { width: 52px; }
/* The bench-regret table only. Its score cell carries "best 80.4" underneath,
   which is nine mono characters and does not fit the 66 px every other numeric
   column uses: it truncated to "best 80…", a number cut in half, which is worse
   than no number. The width comes out of the Left column rather than out of the
   manager's name, so the row is the same total width it was. */
.rows th.num-wide, .rows td.num-wide { width: 80px; }
/* And the swap itself wraps instead of truncating. It reads "Zeke Applewhite
   (19.3) for Delroy Ashgrove (0.0) at FLEX" -- fifty-five characters in a
   column with room for about thirty-one, so it lost the benched player, his
   score and the slot. That is the entire half of the sentence this tab exists
   to show. */
.rows.regret td:first-child .sub { white-space: normal; line-height: 1.45; }
.rows td:first-child { overflow: hidden; }
.rows th { font-family: var(--font-mono); font-size: 10px; letter-spacing: .1em;
           text-transform: uppercase; color: var(--ink-faint); text-align: left;
           padding: 0 8px 6px; font-weight: 400; }
.rows td { padding: 7px 8px; border-top: 1px solid var(--border); }
.rows td.num { font-family: var(--font-mono); text-align: right; font-variant-numeric: tabular-nums; }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%;
       background: hsl(var(--hue) 62% 52%); margin-right: 7px; vertical-align: baseline; }

/* ---------- commentary feed ---------- */

.feed { list-style: none; margin: 0; padding: 0; }
.feed-item {
  display: grid;
  grid-template-columns: 74px minmax(0, 1fr);
  gap: 10px;
  align-items: baseline;
  padding: 8px 0;
  border-top: 1px solid var(--border);
}
.feed-item:first-child { border-top: 0; }
.feed-kind {
  font-family: var(--font-mono);
  font-size: 8.5px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-faint);
  /* Louder moments read louder. `--mag` is the Moment's own 0-1 magnitude, the
   * same number the audio bus scales its horn off. */
  opacity: calc(0.55 + var(--mag, 0.5) * 0.45);
}
.feed-item--touchdown .feed-kind { color: var(--amber); }
.feed-item--bench_disaster .feed-kind { color: var(--red); }
.feed-item--doom .feed-kind { color: var(--red); }
.feed-item--clinch .feed-kind { color: var(--green); }
.feed-item--lead_change .feed-kind { color: var(--magenta); }
.feed-item--goose_egg .feed-kind { color: var(--ink-faint); }
.feed-text { font-size: 13px; color: var(--ink-dim); }
.feed-text b { color: var(--ink); font-weight: 600; }

.rows td.num .sub { text-align: right; }
.rows td .sub {
  display: block;
  font-family: var(--font-mono); font-size: 9.5px; color: var(--ink-faint);
  margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rows td.pos { color: var(--green); }
.rows td.neg { color: var(--red); }
.rows td.dim { color: var(--ink-faint); }

/* A probability reads as a number and a bar at once, without a chart library. */
.prob {
  position: relative;
  display: inline-block;
  padding: 1px 5px;
  border-radius: 4px;
  background: linear-gradient(90deg, rgba(0,208,132,.28) var(--p, 0%), transparent var(--p, 0%));
}

/* ---------- this phone's team ---------- */

/* Marked by identity.js on whatever belongs to the manager holding the phone.
 * Deliberately quiet: a bright treatment on your own row in a list of ten reads
 * as an alert rather than as orientation. */
.is-mine { position: relative; }
.mside.is-mine { background: linear-gradient(90deg, hsl(var(--hue) 60% 50% / .10), transparent 65%); }
.mside.is-mine .mside-id b::after {
  content: 'you';
  font-family: var(--font-mono);
  font-size: 8.5px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-faint);
  margin-left: 7px;
  vertical-align: 1px;
}
.card.is-mine { box-shadow: 0 0 0 2px var(--magenta); }

.chooser {
  position: fixed;
  inset: 0;
  z-index: 190;
  overflow-y: auto;
  background: rgba(9, 11, 16, .96);
  backdrop-filter: blur(6px);
  padding: max(28px, env(safe-area-inset-top)) 18px calc(28px + env(safe-area-inset-bottom));
}
.chooser-inner { max-width: 460px; margin: 0 auto; }
.chooser-title {
  font-family: var(--font-display); font-size: 26px; letter-spacing: .03em;
  margin: 8px 0 6px;
}
.chooser-note { margin: 0 0 18px; font-size: 12.5px; color: var(--ink-faint); }
.chooser-grid { display: grid; gap: 8px; }

.chooser-team {
  display: grid;
  grid-template-columns: 38px minmax(0, 1fr);
  align-items: center;
  gap: 11px;
  width: 100%;
  /* Well over the 44 px minimum: ten of these are the only thing on screen. */
  min-height: 58px;
  padding: 9px 12px;
  text-align: left;
  color: var(--ink);
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
  touch-action: manipulation;
}
.chooser-team:active { transform: scale(.99); }
.chooser-crest {
  width: 38px; height: 38px;
  border-radius: 9px;
  display: grid; place-items: center;
  background: linear-gradient(135deg, hsl(var(--hue) 62% 46%), hsl(var(--hue) 58% 28%));
  font-family: var(--font-display); font-size: 13px;
  color: rgba(255,255,255,.94);
}
.chooser-names { min-width: 0; }
.chooser-names b { display: block; font-size: 14.5px; }
.chooser-names span {
  display: block;
  font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chooser-skip {
  display: block;
  margin: 16px auto 0;
  padding: 10px 18px; min-height: 44px;
  font: inherit; color: var(--ink-faint);
  background: none; border: 0; cursor: pointer;
}

/* ---------- the big board ---------- */

/* One matchup at a time. The others stay in the DOM rather than being removed,
 * so an htmx swap does not have to rebuild them and the rotation can resume
 * mid-cycle without a flash. */
.board-window .matchup { display: none; }
.board-window .matchup--shown { display: block; animation: board-in 420ms ease; }
.board-window .matchup + .matchup { margin-top: 0; padding-top: 0; border-top: 0; }

.board-dots { float: right; display: flex; gap: 5px; align-items: center; }
.board-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--border-bright);
  transition: background 200ms ease;
}
.board-dot--on { background: var(--magenta); }

@keyframes board-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* The takeover. Covers the board for the loudest moments, because the board's
 * whole purpose is to be legible from across a room and a touchdown should not
 * require anybody to find the right row first. */
.takeover {
  position: fixed;
  inset: 0;
  z-index: 160;
  display: grid;
  place-content: center;
  gap: 10px;
  text-align: center;
  padding: 6%;
  background: linear-gradient(128deg, #1e73be 0%, #9b51e0 52%, #ff2fd0 100%);
  animation: takeover-in 320ms cubic-bezier(.2,1.1,.4,1);
}
.takeover--gone { animation: takeover-out 400ms ease forwards; }
.takeover--bench_disaster { background: linear-gradient(128deg, #4a4a4a, #8d1f2c); }
.takeover--goose_egg      { background: linear-gradient(128deg, #3a3f48, #5a5f68); }
.takeover--doom           { background: linear-gradient(128deg, #401018, #8d1f2c); }

.takeover-kind {
  font-family: var(--font-mono); font-size: 12px; letter-spacing: .3em;
  color: rgba(255,255,255,.75);
}
.takeover-text {
  font-family: var(--font-display); font-size: 34px; line-height: 1.08;
  color: #fff; text-wrap: balance;
}
.takeover-who {
  font-family: var(--font-mono); font-size: 13px; letter-spacing: .16em;
  color: rgba(255,255,255,.88);
}

@keyframes takeover-in  { from { opacity: 0; transform: scale(.94); } to { opacity: 1; transform: scale(1); } }
@keyframes takeover-out { to { opacity: 0; transform: scale(1.03); } }

/* ---------- the watch-now rail ---------- */

.rail { list-style: none; margin: 0; padding: 0; }
.rail-item {
  display: grid;
  grid-template-columns: 92px minmax(0, 1fr);
  gap: 10px; align-items: baseline;
  padding: 8px 0;
  border-top: 1px solid var(--border);
}
.rail-item:first-child { border-top: 0; }
.rail-flag {
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .12em;
  color: var(--ink-faint); white-space: nowrap;
}
.rail-item--redzone .rail-flag { color: var(--magenta); }
.rail-item--close .rail-flag { color: var(--amber); }
.rail-item--alone .rail-flag { color: var(--brand-bright); }
.rail-text { font-size: 13px; color: var(--ink-dim); }

@media (prefers-reduced-motion: reduce) {
  .board-window .matchup--shown, .takeover, .takeover--gone { animation: none; }
}

/* ---------- the red-zone countdown ---------- */

.countdown {
  position: fixed;
  left: 0; right: 0;
  bottom: calc(var(--tabbar-height) + env(safe-area-inset-bottom));
  z-index: 120;
  padding: 14px;
  pointer-events: none;
  animation: countdown-in 260ms cubic-bezier(.2,1.1,.4,1);
}
.countdown--closing { animation: countdown-out 500ms ease forwards; }

.countdown-inner {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-lg);
  padding: 14px 16px;
  text-align: center;
  background: linear-gradient(128deg, #6a1a58, #b3123f);
  box-shadow: 0 10px 40px rgba(179, 18, 63, .4);
}

/* The beat. A scaling pseudo-layer rather than a box-shadow pulse, because a
 * shadow repaints the whole element every frame and this runs while a horn is
 * about to fire. */
.countdown-pulse {
  position: absolute;
  inset: -40%;
  background: radial-gradient(circle, rgba(255,255,255,.30), transparent 62%);
  animation: countdown-beat 640ms ease-in-out infinite;
  transform-origin: 50% 50%;
}
.countdown[data-outcome='score'] .countdown-pulse { animation: none; opacity: .9; }

.countdown-label {
  position: relative;
  font-family: var(--font-display); font-size: 22px; letter-spacing: .14em;
  color: #fff;
}
.countdown-who {
  position: relative;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em;
  color: rgba(255,255,255,.92); margin-top: 3px;
}
.countdown-detail {
  position: relative;
  font-family: var(--font-mono); font-size: 9.5px;
  color: rgba(255,255,255,.7); margin-top: 3px;
}

@keyframes countdown-beat {
  0%, 100% { transform: scale(.82); opacity: .35; }
  50%      { transform: scale(1.05); opacity: .75; }
}
@keyframes countdown-in {
  from { transform: translateY(110%); opacity: 0; }
  to   { transform: translateY(0); opacity: 1; }
}
@keyframes countdown-out {
  to { transform: translateY(110%); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .countdown, .countdown--closing { animation: none; }
  .countdown-pulse { animation: none; opacity: .5; }
}

/* ---------- the pack ---------- */

.pack {
  position: relative;
  /* Tall enough to feel like an object rather than a banner: it is the only
   * thing on the screen until it is opened. */
  aspect-ratio: 5 / 6;
  margin-bottom: 12px;
  display: grid;
  place-items: center;
  cursor: pointer;
  touch-action: manipulation;
  user-select: none;
}

.pack-foil {
  position: absolute;
  inset: 0;
  border-radius: 14px;
  display: grid;
  place-content: center;
  gap: 6px;
  padding-bottom: 52px;
  text-align: center;
  background:
    repeating-linear-gradient(115deg, rgba(255,255,255,.05) 0 3px, transparent 3px 11px),
    linear-gradient(128deg, #1e73be 0%, #9b51e0 52%, #ff2fd0 100%);
  box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
  /* Serrated top and bottom edges, the way a foil packet is sealed. */
  clip-path: polygon(
    0% 6%, 4% 0%, 8% 6%, 12% 0%, 16% 6%, 20% 0%, 24% 6%, 28% 0%, 32% 6%, 36% 0%,
    40% 6%, 44% 0%, 48% 6%, 52% 0%, 56% 6%, 60% 0%, 64% 6%, 68% 0%, 72% 6%,
    76% 0%, 80% 6%, 84% 0%, 88% 6%, 92% 0%, 96% 6%, 100% 0%, 100% 94%, 96% 100%,
    92% 94%, 88% 100%, 84% 94%, 80% 100%, 76% 94%, 72% 100%, 68% 94%, 64% 100%,
    60% 94%, 56% 100%, 52% 94%, 48% 100%, 44% 94%, 40% 100%, 36% 94%, 32% 100%,
    28% 94%, 24% 100%, 20% 94%, 16% 100%, 12% 94%, 8% 100%, 4% 94%, 0% 100%
  );
  transform-origin: 50% 50%;
  transition: transform 480ms cubic-bezier(.6,-0.2,.8,.6), opacity 480ms ease;
}

.pack-mark {
  font-family: var(--font-display); font-size: 46px; line-height: 1;
  letter-spacing: .06em; color: rgba(255, 255, 255, .96);
  text-shadow: 0 2px 10px rgba(0, 0, 0, .3);
}
.pack-week, .pack-count {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .22em;
  color: rgba(255, 255, 255, .8);
}

.pack-button {
  position: relative;
  z-index: 1;
  align-self: end;
  margin-bottom: 26px;
  min-height: 44px;
  padding: 11px 22px;
  font-family: var(--font-display); font-size: 14px; letter-spacing: .18em;
  color: #0d1017;
  background: rgba(255, 255, 255, .92);
  border: 0; border-radius: 999px;
  cursor: pointer;
  touch-action: manipulation;
}
.pack-button:active { transform: scale(.96); }

/* The tear. The foil scales apart and fades; nothing else moves, so the whole
 * thing stays on the compositor. */
.pack--ripped .pack-foil { transform: scaleY(1.16) translateY(-8%); opacity: 0; }
.pack--ripped .pack-button { opacity: 0; transition: opacity 200ms ease; }

.panel-note {
  float: right;
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .1em;
  color: var(--ink-faint); text-transform: none;
}

/* The playoff line. A single rule, so the table shows the cut rather than
 * making somebody count down it. */
.mv--in + tr:not(.mv--in) td { border-top: 2px solid var(--border-bright); }
.mv--in td:first-child { font-weight: 600; }
.rows tr.mv td .sub { white-space: normal; line-height: 1.45; }

/* Verdict chips. The base rule carries shape only and no colour: a background
 * on it would sit after the modifiers in source order and win at equal
 * specificity, which is exactly what happened when a careless edit left a stray
 * one here and every "IN" came out the amber of "WIN 2". */
.verdict {
  font-family: var(--font-display);
  font-size: 12px;
  letter-spacing: .1em;
  padding: 3px 8px;
  border-radius: 999px;
  white-space: nowrap;
}

/* Multiverse */
.verdict--in    { background: rgba(0,208,132,.18); color: var(--green); }
.verdict--out   { background: rgba(255,255,255,.05); color: var(--ink-faint); }
.verdict--win   { background: rgba(252,185,0,.16); color: var(--amber); }
.verdict--needs { background: rgba(255,77,94,.14); color: var(--red); }

/* Cheer */
.cheer--cheer .verdict      { background: rgba(0,208,132,.18); color: var(--green); }
.cheer--boo .verdict        { background: rgba(255,77,94,.16); color: var(--red); }
.cheer--conflicted .verdict { background: rgba(252,185,0,.16); color: var(--amber); }
.cheer--stake .verdict      { background: rgba(74,159,212,.16); color: var(--brand-bright); }
.cheer--nothing .verdict    { background: rgba(255,255,255,.05); color: var(--ink-faint); }
.cheer--nothing td          { color: var(--ink-faint); }
.cheer-flag {
  font-family: var(--font-mono); font-size: 8px; letter-spacing: .14em;
  color: var(--magenta); vertical-align: 1px;
}
.cheer td.num { width: 108px; }

/* The reason is the content of this tab, not a caption for it, so it wraps to
 * two lines rather than ellipsising at one. "You have Delroy Marchbank and O..."
 * answers nothing. */
/* `.rows td .sub` above is one class plus one element and wins on specificity
 * over a two-class selector, so this is spelled out rather than assumed. */
.rows tr.cheer td .sub {
  white-space: normal;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  line-height: 1.45;
}

.recap { margin: 0; font-size: 14px; line-height: 1.6; color: var(--ink); }
.recap-source {
  margin: 10px 0 0;
  font-family: var(--font-mono); font-size: 10px; line-height: 1.5;
  color: var(--ink-faint);
}

.album-note {
  margin: 14px 0 0;
  font-size: 12px;
  color: var(--ink-faint);
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}

.confetti {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 150;
}

@media (prefers-reduced-motion: reduce) {
  .pack-foil { transition: none; }
}

/* ---------- the unlock gate ---------- */

/* One full-screen tap that grants the gyroscope permission and (from Phase 4)
 * unlocks the AudioContext. Both need a user gesture and iOS will only grant
 * them from inside one, so they share a single tap: asking twice is how the
 * second request gets refused. */
.gate {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  background: radial-gradient(circle at 50% 38%, #1a2133 0%, #0b0e15 70%);
  opacity: 1;
  transition: opacity 300ms ease;
}
.gate--gone { opacity: 0; pointer-events: none; }
.gate-inner { text-align: center; padding: 0 24px; }
.gate-mark {
  display: block;
  font-family: var(--font-display);
  font-size: 72px;
  line-height: 1;
  letter-spacing: .05em;
  background: linear-gradient(92deg, var(--brand-bright), var(--violet) 45%, var(--magenta));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  margin-bottom: 28px;
}
.gate-button {
  font-family: var(--font-display);
  font-size: 17px;
  letter-spacing: .16em;
  color: var(--ink);
  background: transparent;
  border: 1px solid var(--border-bright);
  border-radius: 999px;
  /* Comfortably over the 44 px minimum: it is the only thing on the screen. */
  padding: 16px 30px;
  min-height: 56px;
  cursor: pointer;
  touch-action: manipulation;
}
.gate-button:active { transform: scale(.97); }
.gate-note { margin: 16px 0 0; font-size: 12px; color: var(--ink-faint); }

/* ---------- tab bar ---------- */

.tabbar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 60;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  /* The home indicator on an iPhone and Android's gesture bar both sit here. */
  padding-bottom: env(safe-area-inset-bottom);
  height: calc(var(--tabbar-height) + env(safe-area-inset-bottom));
  background: rgba(15, 19, 29, 0.94);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--border);
}

.tabbar a {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  /* 44px minimum, in the thumb zone, nothing interactive in the top corners. */
  min-height: 44px;
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  /* Kills the 300 ms double-tap-zoom delay without disabling pinch zoom. */
  touch-action: manipulation;
}
.tabbar a[aria-current='page'] { color: var(--ink); }
.tabbar a[aria-current='page'] .tab-pip { background: var(--magenta); }
.tab-pip { width: 16px; height: 3px; border-radius: 2px; background: var(--border-bright); }

:root[data-tv='1'] .tabbar, :root[data-tv='1'] .mute-toggle { display: none; }

/* ---------- motion ---------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
}
