/* ABOBA DJ stylesheet. It styles public/index.html and the elements that public/app.js creates at run time.
   Desktop layout: the left column has the video (a room scene), the stream info and the content sections.
   The right column has the setlist, the music player box and the emote bar.
   Performance rules:
   - Never use a filter, blend mode, mask or shadow that changes each frame. It repaints on the GPU.
   - CSS variables that change each frame (--env, --int, --fried, --beat, --hue) live only on #booth.
   - app.js writes such a variable only when its value changes.
   Text is never smaller than 14px (rule from the ux-critic skill). */
/* Design tokens, in oklch() colors. --frog is the green accent. --bass is the red accent. --varnish is near-black text on green. */
:root {
  --night: oklch(0.13 0.012 255);
  --panel: oklch(0.17 0.014 255);
  --panel-2: oklch(0.21 0.016 255);
  --panel-3: oklch(0.25 0.018 255);
  --line: oklch(0.3 0.016 255);
  --ink: oklch(0.94 0.01 95);
  --muted: oklch(0.75 0.014 255);
  --faded: oklch(0.62 0.014 255);
  --frog: oklch(0.78 0.17 142);
  --frog-bg: oklch(0.78 0.17 142 / 0.14);
  --bass: oklch(0.68 0.19 25);
  --varnish: oklch(0.1 0.01 255);
  --ease: cubic-bezier(0.22, 1, 0.36, 1); /* ease-out: fast start, smooth stop */
  --spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* overshoot: a springy end */
  --r: 12px;
  --sticky-h: 64px; /* height of the sticky player bar */
  --set: 380px; /* width of the setlist column */
  --emo: 88px; /* width of the emote column */
}
* { box-sizing: border-box; }
html { background: var(--night); scroll-behavior: smooth; scroll-padding-top: calc(var(--sticky-h) + 12px); } /* anchor jumps stop below the sticky player bar */
body { margin: 0; min-height: 100dvh; color: var(--ink); background: var(--night); font-family: "Geist", system-ui, sans-serif; font-size: 1rem; line-height: 1.5; overflow-x: hidden; }
button { font: inherit; color: inherit; cursor: pointer; background: none; border: 0; padding: 0; }
:focus-visible { outline: 2px solid var(--frog); outline-offset: 2px; border-radius: 6px; }
h1, h2, h3, p, ol { margin: 0; }
a { color: var(--frog); text-underline-offset: 3px; }
/* .sr: text for screen readers only. It is hidden from sight. */
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
.link { color: var(--frog); text-decoration: underline; text-underline-offset: 3px; font-size: inherit; }
/* img.em: an emote image inside a line of text */
img.em { height: 1.5em; width: auto; vertical-align: middle; margin: -0.3em 0.1em 0; }

/* ---------- small buttons ---------- */
.nav-btn {
  min-height: 44px; padding: 0 0.85rem; border-radius: 999px; border: 1px solid var(--line); background: var(--panel-2);
  display: inline-flex; align-items: center; gap: 0.4rem; font-size: 0.92rem; font-weight: 550; white-space: nowrap; transition: border-color 0.2s, background 0.2s;
}
.nav-btn:hover { border-color: var(--muted); background: var(--panel-3); }
.nav-btn svg { width: 15px; height: 15px; color: var(--frog); }
#btnSecrets.found { border-color: var(--frog); }

/* ---------- page grid: .content on the left, .side on the right ---------- */
.app { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; }
.content { min-width: 0; }
/* .side: the sticky right column, one screen high.
   Row 1: the setlist on the left, and the emote bar on the right (it spans both rows). Row 2: the player box. */
.side { position: sticky; top: 0; height: 100dvh; display: grid; grid-template-columns: var(--set) var(--emo); grid-template-rows: minmax(0, 1fr) auto; border-left: 1px solid var(--line); background: var(--panel); }
.setlist { grid-column: 1; grid-row: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; }
/* Below 1100px (phones and tablets) the page has one column.
   display: contents removes the .content and .side wrappers, so "order" can sort their children.
   Order: player, player box, emotes, setlist, CDs, EPs, Hall of Fame, Words & Themes, credits. */
@media (max-width: 1099px) {
  .app { grid-template-columns: minmax(0, 1fr); }
  .content, .side { display: contents; }
  .player { order: 1; } .emotes { order: 3; } .setlist { order: 4; } #secCds { order: 5; } #secEps { order: 5; } #secHall { order: 6; } #secStats { order: 7; } .credits { order: 8; }
  .setlist { margin: 1rem 1rem 0; border: 1px solid var(--line); border-radius: var(--r); background: var(--panel); }
}

/* ---------- video: the room scene ---------- */
/* .player: the video and the stream info */
.player { padding: 1rem 1.25rem 0; }
@media (max-width: 1099px) { .player { padding: 0; } }
/* .video: container-type makes the "cqw" unit work (1cqw = 1% of this box width).
   Text and positions inside the scene scale with the video width. */
.video { position: relative; container-type: inline-size; border-radius: var(--r); overflow: hidden; background: #000; }
@media (max-width: 1099px) { .video { border-radius: 0; } }
/* .scene (id booth): the room image and all layers on top of it.
   isolation: isolate keeps the negative z-index layers inside the scene.
   Layers (z-index), back to front: -3 room image, -2 lasers, -1 visualizer.
   Then 0 monitor and chat, 2 DJ, 3 desk and mic, 5 fullscreen button, 6 speech balloon.
   The variables are set here by app.js: --env (volume envelope), --int (intensity), --fried (heat level), --beat and --hue (color shift). */
.scene { position: relative; aspect-ratio: 1658 / 926; overflow: hidden; isolation: isolate; user-select: none; --env: 0; --int: 0; --fried: 0; --beat: 0; --hue: 0; }
.room-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -3; will-change: transform; }
/* .lasers: five beams behind the DJ. They show only while .scene has the class .fried. Opacity follows --fried. */
.lasers { position: absolute; inset: 0; z-index: -2; opacity: var(--fried); visibility: hidden; pointer-events: none; }
.scene.fried .lasers { visibility: visible; }
.lasers i {
  position: absolute; bottom: 12%; left: 50%; width: 3px; height: 130%; transform-origin: 50% 100%; /* each beam starts at the booth and is taller than the scene */
  background: linear-gradient(to top, oklch(0.85 0.22 calc(142 + var(--hue))), transparent 80%);
  animation: sweep 3.2s ease-in-out infinite alternate; /* each beam swings 28deg (from -14deg to +14deg) around its base angle */
}
/* --a is the base angle of each beam. The negative delays start the beams out of phase. */
.lasers i:nth-child(1) { --a: -60deg; animation-delay: -0.4s; }
.lasers i:nth-child(2) { --a: -30deg; animation-delay: -1.1s; }
.lasers i:nth-child(3) { --a: 0deg; animation-delay: -1.8s; }
.lasers i:nth-child(4) { --a: 30deg; animation-delay: -2.2s; }
.lasers i:nth-child(5) { --a: 60deg; animation-delay: -2.9s; }
@keyframes sweep { from { rotate: calc(var(--a) - 14deg); } to { rotate: calc(var(--a) + 14deg); } }
/* .viz: mirrored bars behind ABOBA. app.js draws them on a canvas from the audio analyser. */
.viz { position: absolute; left: 4%; width: 92%; bottom: 0; height: 36%; z-index: -1; pointer-events: none; }

/* .live-badge: the LIVE badge and the fake viewer count. max(14px, ...) keeps text at 14px or more and scales with the video. */
.live-badge { position: absolute; top: 2.2%; right: 1.4%; display: flex; gap: 0.5em; align-items: center; font-size: max(14px, 1.05cqw); font-weight: 650; }
.live { background: var(--bass); color: #fff; padding: 0.1em 0.45em; border-radius: 4px; letter-spacing: 0.04em; }
.viewers { background: oklch(0 0 0 / 0.6); color: #fff; padding: 0.1em 0.45em; border-radius: 4px; font-variant-numeric: tabular-nums; }
/* .score-live: the score of the emote-click game. It sits under the badge: the badge top plus about two text lines. */
.score-live { position: absolute; top: calc(2.2% + max(14px, 1.05cqw) * 1.9); right: 1.4%; font-size: max(14px, 1.05cqw); font-weight: 600; color: #fff; background: oklch(0 0 0 / 0.6); padding: 0.1em 0.5em; border-radius: 4px; font-variant-numeric: tabular-nums; pointer-events: none; }
.score-live b { font-family: "Geist Mono", monospace; }
.score-live .combo { color: var(--frog); font-family: "Geist Mono", monospace; font-weight: 700; margin-left: 0.3em; }
.viewers::before { content: ""; display: inline-block; width: 0.55em; height: 0.55em; border-radius: 50%; background: var(--bass); margin-right: 0.35em; vertical-align: 0.05em; }
/* .alert: a pop-up message at the top left of the scene. Hidden until .on slides it in. */
.alert {
  position: absolute; top: 1.6%; left: 1%; max-width: 40%; padding: 0.35em 0.7em; border: 2px solid #fff; border-radius: 6px; background: oklch(0.12 0.01 255 / 0.92);
  color: #fff; font-size: max(14px, 1.3cqw); font-weight: 600; line-height: 1.25; opacity: 0; translate: -12px 0; transition: opacity 0.35s, translate 0.45s var(--ease); pointer-events: none;
}
.alert.on { opacity: 1; translate: 0 0; }
/* Narrow video: the alert stays on one line, so it ends above the monitor brand and does not cover it. */
@container (max-width: 560px) { .alert { max-width: 62%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } }
.alert b { color: var(--frog); }

/* .monitor: brand and countdown on the screen of the room monitor. The karaoke sits below them.
   Position in % of the 1658x926 room image: left 26.6%, top 14.47%, width 37.7%, height 38.44%.
   The values were measured on the image. Measure again if the image changes. */
.monitor { position: absolute; left: 26.6%; top: 14.47%; width: 37.7%; height: 38.44%; display: flex; justify-content: space-between; align-items: flex-start; padding: 0.9cqw 1.2cqw; pointer-events: none; }
.mon-brand { font-family: "Anybody", sans-serif; font-weight: 900; font-stretch: 140%; font-size: max(14px, 1.3cqw); color: #fff; letter-spacing: -0.01em; line-height: 1; }
.mon-brand b { color: var(--frog); font-stretch: 70%; margin-left: 0.1em; }
.mon-count { font-family: "Geist", sans-serif; font-weight: 700; font-size: max(14px, 1.7cqw); color: #fff; font-variant-numeric: tabular-nums; line-height: 1; }
/* .karaoke: the lyrics on the monitor. They start under the brand row (3.2cqw) and fill the top 72% of the monitor. The rest stays free, because the head of ABOBA covers it. */
.karaoke {
  position: absolute; left: 26.6%; top: calc(14.47% + 3.2cqw); width: 37.7%; height: calc(38.44% * 0.72 - 3.2cqw);
  display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; padding: 0 1.4cqw; overflow: hidden; pointer-events: none;
}
.line { max-width: 100%; text-wrap: balance; }
/* .line.current: the line that is sung now. It is gray until sung. */
.line.current {
  font-family: "Anybody", sans-serif; font-weight: 850; font-stretch: 105%; font-size: var(--size, 3cqw); line-height: 1.1; letter-spacing: -0.01em; word-spacing: 0.08em;
  color: oklch(0.6 0.01 255); padding: 0.12em 0.25em;
}
/* Word states. .w is one word. .sung is a word already sung: white. .active is the word sung now: a green fill grows left to right with --p (0 to 1). */
.line.current .w { display: inline-block; transition: color 0.12s linear, transform 0.25s var(--spring); transform-origin: 50% 80%; }
.line.current .w.sung { color: #fff; }
/* The active word uses background-clip: text, so its element must hold plain text only. A transformed child paints outside the clip and leaves a stray green letter. */
.line.current .w.active {
  color: transparent; transform: translateY(-0.05em) scale(1.05);
  background: linear-gradient(90deg, var(--frog) calc(var(--p, 0) * 100%), #fff calc(var(--p, 0) * 100% + 0.01%));
  -webkit-background-clip: text; background-clip: text;
}
.line.current .w.aboba { color: var(--frog); }
.line.current .w.aboba.active { transform: translateY(-0.1em) scale(1.15) rotate(-3deg); }
/* .enter: the line drops in word by word. --i is the word index. The words start 40ms apart. */
.line.current.enter .w { animation: drop-in 0.4s var(--spring) backwards; animation-delay: calc(var(--i) * 40ms); }
@keyframes drop-in { from { opacity: 0; translate: 0 -0.5em; scale: 0.75; } }
/* .shaking: loud parts. The active words wave up and down. The delay is negative and differs per word, so the wave runs along the line. */
.karaoke.shaking .line.current .w.active, .karaoke.shaking .line.current .w.aboba { animation: wave 0.42s ease-in-out infinite alternate; animation-delay: calc(var(--i) * -90ms); }
@keyframes wave { from { translate: 0 0.04em; } to { translate: 0 -0.1em; } }
/* .line.next: a preview of the next line, cut with an ellipsis when too long */
.line.next { color: oklch(0.58 0.01 255); margin-top: 0.4em; font-size: max(14px, 1.35cqw); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
/* .line.prompt: when a song has no usable lyrics, the page shows the prompt of the chatter as still text. */
.line.prompt { font-family: "Geist", sans-serif; font-size: max(14px, 1.2cqw); font-weight: 400; color: var(--muted); line-height: 1.45; }

/* .msgs: the fake stream chat. It sits on the dark chat panel painted in the room image.
   Position in % of the image: left 69.48%, top 15.87%, width 19.54%, height 60.15%.
   Messages stack from the bottom (justify-content: flex-end). */
.msgs {
  position: absolute; left: 69.48%; top: 15.87%; width: 19.54%; height: 60.15%; list-style: none; padding: 0.8cqw 0.9cqw; margin: 0;
  display: flex; flex-direction: column; justify-content: flex-end; overflow: hidden; pointer-events: none;
  font-size: 1.12cqw; line-height: 1.35; color: #fff; font-weight: 500;
}
.msgs li { padding: 0.18em 0; overflow-wrap: anywhere; animation: msg 0.3s var(--ease); }
@keyframes msg { from { opacity: 0; translate: 0 0.6em; } }
.msgs b { font-weight: 700; }
.msgs img { height: 1.9em; width: auto; vertical-align: middle; margin: -0.35em 0.08em 0; }
.msgs .system { background: oklch(0.78 0.17 142 / 0.2); border-left: 0.25em solid var(--frog); padding: 0.35em 0.5em; margin: 0.25em 0; border-radius: 0.3em; }
.msgs .system small { display: block; color: oklch(0.85 0.01 255); font-size: 0.85em; }
@container (max-width: 560px) { .msgs { display: none; } }

/* .dj: ABOBA, the DJ. It is the animated 7TV emote, on purpose (the owner preferred it to a generated sprite).
   The desk hides the lower body. Headphones and a microphone are drawn on top. */
.dj { position: absolute; left: 50%; bottom: 16%; width: 34%; translate: -50% 0; z-index: 2; }
/* .dj-body: the emote and the headphones. app.js bounces it with the volume envelope. */
.dj-body { position: relative; transform-origin: 50% 100%; will-change: transform; }
.dj-body img { display: block; width: 100%; height: auto; cursor: pointer; -webkit-user-drag: none; }
.scene.fried .dj-body img { filter: saturate(calc(1 + var(--fried) * 2.5)) hue-rotate(calc(var(--fried) * var(--hue) * 1deg)); }
/* .headphones: a PNG over the head of the emote. The offsets and the 106% width fit it to the head. */
.headphones { position: absolute; left: -3%; width: 106%; height: auto; top: -38%; pointer-events: none; }
/* .hype: a short head shake on a peak. .spin: a double turn (720deg). */
.dj-body.hype { animation: hype 0.9s var(--ease); }
@keyframes hype { 30% { rotate: -7deg; translate: 0 -10%; } 60% { rotate: 7deg; translate: 0 -5%; } }
.dj-body.spin { animation: spin 1.1s var(--ease); }
@keyframes spin { to { rotate: 720deg; } }
/* .mic: a button right of ABOBA, in front of the body. It tilts from its bottom right corner on hover. */
.mic { position: absolute; left: 60%; top: 14%; width: 46%; z-index: 3; cursor: pointer; transition: rotate 0.3s var(--spring); transform-origin: 100% 100%; }
.mic:hover { rotate: -4deg; }
.mic img { width: 100%; height: auto; display: block; pointer-events: none; }
/* .bubble: the speech bubble left of ABOBA. The ::after triangle points to him. */
.bubble {
  position: absolute; right: 96%; top: -30%; z-index: 6; width: max-content; max-width: 26cqw; text-align: center; line-height: 1.15; text-wrap: balance;
  background: #fff; color: var(--varnish); font-family: "Anybody", sans-serif; font-weight: 900; font-stretch: 125%;
  font-size: max(14px, 1.45cqw); padding: 0.35em 0.8em; border-radius: 0.8em; opacity: 0; scale: 0.6; pointer-events: none; transform-origin: 100% 60%;
  transition: opacity 0.2s, scale 0.35s var(--spring);
}
.bubble::after { content: ""; position: absolute; right: -9px; top: 50%; translate: 0 -50%; border: 9px solid transparent; border-right: 0; border-left-color: #fff; }
.bubble.on { opacity: 1; scale: 1; }
.bubble img { height: 1.5em; width: auto; vertical-align: middle; margin: -0.3em 0.05em 0; }

/* .desk: the desk image, 47% of the scene width, centered at the bottom. It is low on purpose, so it does not cover ABOBA. */
.desk { position: absolute; left: 50%; bottom: -1%; width: 47%; translate: -50% 0; z-index: 3; }
.desk > img { display: block; width: 100%; height: auto; pointer-events: none; }
/* .platter: invisible button over each record of the desk image. The user drags it to scratch.
   The circle is flattened (scaleY 0.42) to match the perspective of the desk image.
   The left offsets (7.85% and 66.97% of the desk width) place the two buttons on the two records.
   touch-action: none lets a finger drag the platter without scrolling the page. */
.platter { position: absolute; left: 7.85%; top: -23.3%; width: 25%; aspect-ratio: 1; border-radius: 50%; transform: scaleY(0.42); cursor: grab; touch-action: none; }
.platter.right { left: 66.97%; }
.platter:active { cursor: grabbing; }
/* .record: the rotating shine (conic gradient). app.js turns it with the music. */
.record { position: absolute; inset: 6%; border-radius: 50%; will-change: transform;
  background: conic-gradient(from 0deg, transparent 0 8%, oklch(1 0 0 / 0.16) 12%, transparent 16% 50%, oklch(1 0 0 / 0.12) 62%, transparent 66%); }

/* ---------- .player-box: the music player in the right column ---------- */
/* Desktop: column 1, row 2 of .side, under the setlist. It holds the cover, track, progress bar, controls, volume, Discord score and token. */
.player-box { grid-column: 1; grid-row: 2; border-top: 1px solid var(--line); padding: 0.9rem 1rem 1rem; display: grid; gap: 0.55rem; background: var(--panel-2); }
.pb-track { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.8rem; align-items: center; }
.pb-cover { width: 64px; height: 64px; border-radius: 8px; object-fit: cover; box-shadow: 0 8px 20px -10px oklch(0 0 0 / 0.9); }
.pb-text { min-width: 0; }
.pb-title { font-family: "Anybody", sans-serif; font-weight: 900; font-stretch: 115%; font-size: 1.2rem; line-height: 1.15; letter-spacing: -0.01em; display: flex; gap: 0.4rem; align-items: baseline; min-width: 0; }
.pb-title .marquee { flex: 1 1 auto; }
/* .marquee: a one-line box for a song name. With .run (set by fitMarquee in app.js) the name scrolls to its end,
   waits, and scrolls back. Only translate animates. Hover pauses it. */
.marquee { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.marquee.run { text-overflow: clip; mask-image: linear-gradient(90deg, transparent, #000 10px, #000 calc(100% - 14px), transparent); } /* soft edges; the mask itself never changes */
.marquee.run > span { display: inline-block; animation: marquee var(--dur, 8s) ease-in-out 1.2s infinite alternate; }
.marquee.run:hover > span { animation-play-state: paused; }
@keyframes marquee { 0%, 12% { translate: 0 0; } 88%, 100% { translate: var(--shift, 0) 0; } }
/* two lines, then an ellipsis */
.pb-sub { color: var(--muted); font-size: 0.875rem; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.pb-seek { display: flex; align-items: center; gap: 0.6rem; margin-top: 0.2rem; }
.pb-t { font-family: "Geist Mono", monospace; font-size: 0.875rem; color: var(--muted); font-variant-numeric: tabular-nums; min-width: 4ch; }
.pb-t:last-child { text-align: right; }
/* .seek-bar: the seek bar. It is 24px high so a finger can hit it. ::before draws the 5px rail and <i> draws the progress. */
.player-box .seek-bar { flex: 1; height: 24px; position: relative; cursor: pointer; }
.player-box .seek-bar::before { content: ""; position: absolute; left: 0; right: 0; top: 10px; height: 5px; border-radius: 3px; background: var(--panel-3); }
.player-box .seek-bar i { position: absolute; left: 0; top: 10px; height: 5px; width: 0; border-radius: 3px; background: var(--frog); }
.pb-controls { display: flex; align-items: center; justify-content: space-between; }
/* .score: net vote of a song in the setlist (bangers minus slops) */
.score { display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 36px; padding: 0 0.6rem; border-radius: 999px; background: var(--panel-2); border: 1px solid var(--line); font-family: "Geist Mono", monospace; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--muted); }
.score[data-sign="up"] { color: var(--frog); }
.score[data-sign="down"] { color: var(--bass); }
/* .vote: BANGER and SLOP buttons in the player box */
.vote { flex: 1; display: inline-flex; justify-content: flex-end; gap: 0.4rem; }
.vote-btn { min-height: 44px; padding: 0 0.8rem; border-radius: 999px; border: 1px solid var(--line); background: var(--panel-2); color: var(--ink); font-weight: 800; letter-spacing: 0.03em; font-size: 0.85rem; cursor: pointer; display: inline-flex; align-items: center; gap: 0.4rem; }
.vote-btn img { height: 22px; width: auto; }
.vote-btn b { font-family: "Geist Mono", monospace; font-variant-numeric: tabular-nums; }
.vote-btn.vote-banger[aria-pressed="true"] { background: var(--frog); color: #111; border-color: var(--frog); }
.vote-btn.vote-slop[aria-pressed="true"] { background: var(--bass); color: var(--varnish); border-color: var(--bass); }
.vote-btn:hover { border-color: var(--ink); }
/* .disc: a pill with the biggest Discord react of a song */
.disc { display: inline-flex; align-items: center; gap: 0.35rem; min-height: 36px; padding: 0 0.7rem 0 0.45rem; border-radius: 999px; background: var(--panel-2); border: 1px solid var(--line); font-family: "Geist Mono", monospace; font-weight: 700; font-variant-numeric: tabular-nums; }
.disc img { height: 22px; width: auto; }
.disc .emo, .disc-now .emo { font-family: system-ui, sans-serif; font-size: 1.1rem; line-height: 1; }
.item.current .disc { color: var(--frog); }
.disc-now { flex: 1; display: inline-flex; align-items: center; justify-content: flex-end; gap: 0.4rem; color: var(--muted); font-size: 0.9rem; }
.disc-now b { color: var(--ink); font-family: "Geist Mono", monospace; font-size: 1rem; }
.disc-now img { height: 24px; width: auto; }
.pb-bottom { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.pb-vol { display: flex; align-items: center; gap: 0.6rem; }
.pb-vol input { flex: 1; width: auto; }
.pb-bottom .pb-info { min-height: 44px; }
.player-box .token-card { width: 100%; }
.player-box .token-card code { flex: 1 1 0; min-width: 0; }
/* Phones: the box is a normal block (order 2, right after the video) with its own border. */
@media (max-width: 1099px) { .player-box { grid-column: auto; grid-row: auto; order: 2; margin: 1rem 1rem 0; border: 1px solid var(--line); border-radius: var(--r); } }
/* .ctl: a round icon button. 44px is the minimum touch size. */
.ctl { width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center; transition: background 0.2s, scale 0.2s var(--ease), color 0.2s; flex: none; position: relative; color: var(--ink); }
.ctl:hover { background: var(--panel-3); }
.ctl:active { scale: 0.92; }
.ctl svg { width: 19px; height: 19px; }
.ctl.play { width: 50px; height: 50px; background: var(--frog); color: var(--varnish); }
.ctl.play:hover { background: oklch(0.84 0.16 142); }
.ctl.play svg { width: 22px; height: 22px; }
/* .tog: a toggle button (shuffle, repeat). It is green when on. data-mode is the repeat mode: off, all or one. */
.ctl.tog { color: var(--muted); }
.ctl.tog[aria-pressed="true"], .ctl.tog[data-mode="all"], .ctl.tog[data-mode="one"] { color: var(--frog); background: var(--frog-bg); }
/* .one: the small "1" on the repeat icon, shown in the "one" mode */
.ctl .one { position: absolute; right: 6px; bottom: 4px; font-size: 0.7rem; font-family: "Geist Mono", monospace; display: none; }
.ctl[data-mode="one"] .one { display: block; }
.volume { display: flex; align-items: center; gap: 0.35rem; color: var(--muted); }
.volume svg { width: 20px; height: 20px; }
.volume input { width: 84px; accent-color: var(--frog); height: 28px; }

/* Narrow video (under 560px): the karaoke leaves the monitor and becomes a normal block under the video.
   Text on the monitor would be too small to read. The speech balloon moves above ABOBA. */
@container (max-width: 560px) {
  .karaoke { position: static; width: auto; height: auto; min-height: 7.4rem; padding: 0.9rem 1rem 0.6rem; background: var(--panel); align-items: flex-start; text-align: left; }
  .line.current { font-size: 1.55rem; }
  .line.next { font-size: 0.98rem; }
  .line.prompt { font-size: 0.98rem; }
  .monitor { padding: 1.4cqw 1.8cqw; }
  .bubble { right: auto; left: 50%; top: -38%; translate: -50% 0; transform-origin: 50% 100%; max-width: 70cqw; }
  .bubble::after { right: auto; left: 50%; top: auto; bottom: -8px; translate: -50% 0; border: 8px solid transparent; border-bottom: 0; border-top-color: #fff; }
}

/* ---------- .emotes: the emote bar in the right column ---------- */
/* Desktop: column 2 of .side, spanning both rows. Each button throws one emote. .throw-style sets the throw style. .throw-bar is the scrolling list. */
.emotes { grid-column: 2; grid-row: 1 / span 2; display: flex; flex-direction: column; min-height: 0; border-left: 1px solid var(--line); }
.emotes-title { font-size: 0.875rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); text-align: center; padding: 0.8rem 0 0.4rem; }
.throw-style { margin: 0 8px 0.5rem; min-height: 44px; border-radius: 999px; background: var(--frog); color: var(--varnish); font-weight: 700; font-size: 0.875rem; }
.throw-bar { flex: 1 1 0; min-height: 0; overflow-y: auto; display: grid; grid-auto-rows: 58px; gap: 6px; padding: 0 8px 1rem; scrollbar-width: thin; scrollbar-color: var(--line) transparent; overscroll-behavior: contain; }
.throw-bar button { border-radius: 12px; display: grid; place-items: center; background: var(--panel-2); border: 1px solid var(--line); transition: border-color 0.15s, scale 0.15s var(--ease), background 0.15s; }
.throw-bar button:hover { border-color: var(--frog); background: var(--frog-bg); }
.throw-bar button:active { scale: 0.86; }
.throw-bar img { max-height: 40px; max-width: 86%; width: auto; pointer-events: none; }
/* Phones: the emote bar becomes a wrapping grid in the page flow (order 3). */
@media (max-width: 1099px) {
  .emotes { border: 0; padding: 1rem 1rem 0; display: grid; grid-template-columns: auto minmax(0, 1fr); justify-items: start; align-items: center; gap: 0.6rem 0.8rem; }
  .emotes-title { padding: 0; font-size: 1rem; }
  .throw-style { margin: 0; padding: 0 1rem; }
  /* Three rows that scroll sideways, the full width of the screen. The cut tile at the right edge shows that there is more. */
  .throw-bar { grid-column: 1 / -1; justify-self: stretch; min-width: 0; overflow-x: auto; overflow-y: hidden; grid-template-columns: none; grid-auto-flow: column; grid-template-rows: repeat(3, 58px); grid-auto-columns: 66px; margin-inline: -1rem; padding: 0 1rem 6px; scroll-snap-type: x proximity; scroll-padding-inline: 1rem; }
  .throw-bar button { scroll-snap-align: start; }
}

/* ---------- .channel: stream info under the video ---------- */
.channel { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.75rem 1rem; align-items: center; padding: 1.2rem 1.25rem 0.5rem; }
@media (max-width: 1099px) { .channel { padding: 1.4rem 1rem 0.5rem; } }
/* A round frame. The emote is enlarged to 118% and moved down, so the face fills the circle. */
.avatar { width: 64px; height: 64px; border-radius: 50%; border: 3px solid var(--bass); display: grid; place-items: center; overflow: hidden; background: var(--panel-2); flex: none; align-self: start; }
.avatar img { width: 118%; height: auto; margin-top: 18%; }
.live-title { font-size: 1.05rem; font-weight: 700; line-height: 1.35; letter-spacing: -0.005em; text-wrap: pretty; overflow-wrap: anywhere; }
.channel-sub { color: var(--muted); font-size: 0.92rem; margin-top: 0.2rem; }
.channel-sub b { color: var(--frog); }
.rank { font-family: "Anybody", sans-serif; font-stretch: 125%; font-weight: 850; color: var(--frog); }
.tags { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.45rem; }
.tags span { font-size: 0.875rem; font-weight: 600; padding: 0.1rem 0.6rem; border-radius: 999px; background: var(--panel-3); color: var(--muted); }
/* .slop: the joke disclaimer. It spans both columns. */
.slop { grid-column: 1 / -1; color: var(--faded); font-size: 0.9rem; max-width: 70ch; }
@media (max-width: 480px) { .avatar { width: 48px; height: 48px; } }
/* .token-card: the replay token pill, with a copy button. It starts hidden. */
.token-card { display: inline-flex; align-items: center; gap: 0.45rem; padding: 3px 3px 3px 0.75rem; min-width: 0; border-radius: 999px; background: var(--panel-2); border: 1px solid var(--line); }
/* Needed because display: inline-flex above would override the hidden attribute. */
.token-card[hidden] { display: none; }
.token-label { font-size: 0.875rem; color: var(--muted); white-space: nowrap; }
.token-card code { font-family: "Geist Mono", monospace; font-size: 0.9rem; color: var(--ink); overflow: hidden; text-overflow: ellipsis; }
.copy { white-space: nowrap; flex: none; display: inline-flex; align-items: center; gap: 0.3rem; min-height: 44px; padding: 0 0.8rem; border-radius: 999px; background: var(--panel-3); font-weight: 650; font-size: 0.9rem; transition: background 0.15s, scale 0.15s var(--ease); }
.copy:hover { background: var(--line); }
.copy:active { scale: 0.92; }
.copy svg { width: 15px; height: 15px; }
/* Very narrow screens: hide the label to save room for the token. */
@media (max-width: 480px) { .token-label { display: none; } .token-card code { font-size: 0.875rem; } }

/* ---------- .setlist: the song list in the right column ---------- */
.setlist .scroller { flex: 1 1 0; min-height: 0; }
.set-header { display: flex; align-items: center; gap: 0.5rem; padding: 0.7rem 0.8rem; border-bottom: 1px solid var(--line); }
.set-header h2 { font-family: "Anybody", sans-serif; font-weight: 900; font-stretch: 130%; font-size: 1.3rem; letter-spacing: -0.02em; line-height: 1; }
/* .help: a round "?" button that explains the ranking */
.help { flex: none; width: 28px; height: 28px; border-radius: 50%; border: 1px solid var(--line); color: var(--muted); font-weight: 700; font-size: 0.875rem; display: grid; place-items: center; }
.help:hover { color: var(--frog); border-color: var(--frog); }
.search { flex: 1; min-width: 0; display: flex; align-items: center; gap: 0.4rem; padding: 0 0.65rem; border-radius: 10px; background: var(--panel-2); border: 1px solid var(--line); }
.search:focus-within { border-color: var(--frog); }
.search svg { width: 18px; height: 18px; color: var(--muted); flex: none; }
.search input { flex: 1; min-width: 0; min-height: 44px; background: none; border: 0; color: var(--ink); font: inherit; outline: none; }
.search input::placeholder { color: var(--faded); }
/* .scroller: the list scrolls here. overscroll-behavior: contain keeps the scroll from moving the page. */
.scroller { flex: 1 1 0; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.empty { color: var(--muted); padding: 1rem; }
.list { list-style: none; padding: 0.35rem 0.45rem 1rem; display: grid; grid-template-columns: minmax(0, 1fr); gap: 2px; }
/* .item: one song row, with .pos (rank), .play-btn (the name button) and .actions.
   content-visibility: auto skips rendering of rows off screen. contain-intrinsic-size reserves about 62px per row, so the scrollbar stays stable. */
.item { display: grid; grid-template-columns: 2.2rem minmax(0, 1fr) auto; align-items: center; gap: 0.5rem; padding: 0.25rem 0.35rem; border-radius: 10px; transition: background 0.15s; content-visibility: auto; contain-intrinsic-size: auto 62px; }
.item:hover { background: var(--panel-2); }
.item.current { background: var(--frog-bg); }
.item[hidden] { display: none; }
.pos { font-family: "Anybody", sans-serif; font-weight: 850; font-stretch: 120%; font-size: 1.15rem; color: var(--muted); text-align: right; font-variant-numeric: tabular-nums; }
.item.current .pos { color: var(--frog); }
.play-btn { text-align: left; min-height: 48px; display: grid; gap: 1px; padding: 0.15rem 0; min-width: 0; }
.play-btn .title-row { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.king-slot:empty { display: none; }
.king-slot { flex: none; }
.play-btn .song-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.play-btn .meta { display: flex; gap: 0.35rem; align-items: center; color: var(--muted); font-size: 0.875rem; min-height: 22px; white-space: nowrap; overflow: hidden; }
.play-btn .meta img { height: 20px; width: auto; }
.play-btn .meta-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.play-btn .meta-text .by { color: var(--faded); }
.play-btn .now-tag { color: var(--frog); font-weight: 650; }
.play-btn .new-tag { color: #111; background: var(--frog); font-weight: 700; font-size: .85em; padding: 0 .4em; border-radius: 4px; text-transform: uppercase; letter-spacing: .04em; }
.actions { display: flex; align-items: center; gap: 0.2rem; }
/* .score-wrap: the score, and on hover (or tap) the two quick vote emotes in its place */
.score-wrap { position: relative; display: inline-flex; }
.score { cursor: pointer; }
.quick-vote { position: absolute; right: 0; top: 50%; display: flex; gap: 0.25rem; padding: 0.2rem; border-radius: 999px; background: var(--panel-2); border: 1px solid var(--line); opacity: 0; pointer-events: none; transform: translateY(-50%) scale(0.9); transform-origin: right center; transition: opacity 0.15s, transform 0.15s var(--ease); z-index: 2; }
.score-wrap:hover .quick-vote, .score-wrap:focus-within .quick-vote, .score-wrap.open .quick-vote { opacity: 1; pointer-events: auto; transform: translateY(-50%) scale(1); }
.qv { width: 40px; height: 40px; border-radius: 999px; display: grid; place-items: center; border: 1px solid transparent; transition: background 0.15s; }
.qv img { height: 26px; width: auto; }
.qv-banger:hover, .qv-banger[aria-pressed="true"] { background: var(--frog-bg); border-color: var(--frog); }
.qv-slop:hover, .qv-slop[aria-pressed="true"] { background: oklch(from var(--bass) l c h / 0.18); border-color: var(--bass); }
@media (hover: none) { .score-wrap:hover:not(.open) .quick-vote { opacity: 0; pointer-events: none; } }
/* #1 of the setlist: AI SLOP KING */
.king-tag { display: inline-flex; align-items: center; gap: 0.25rem; color: var(--frog); font-weight: 800; font-size: 0.75rem; letter-spacing: 0.04em; }
.king-tag svg { width: 14px; height: 14px; }
.item.king .pos { color: var(--frog); }
/* The info button shows only on the focused row, on hover and on the playing row.
   It was noise when repeated on every row. */
.info { width: 32px; height: 32px; border-radius: 8px; display: grid; place-items: center; color: var(--faded); opacity: 0; transition: opacity 0.15s, color 0.15s, background 0.15s; }
.item:hover .info, .item:focus-within .info, .item.current .info { opacity: 1; }
.info:hover { background: var(--panel-3); color: var(--ink); }
.info svg { width: 18px; height: 18px; }
/* Touch screens have no hover, so the info button shows only on the playing row. */
@media (hover: none) { .info { display: none; } .item.current .info { display: grid; } }
/* ---------- content sections: CDs, Game EPs, Hall of Fame, Words & Themes ---------- */
/* .section: content-visibility: auto skips rendering of sections off screen. 900px is the guessed height until they render. */
.section { padding: 2.75rem 1.25rem 0; content-visibility: auto; contain-intrinsic-size: auto 900px; }
@media (max-width: 1099px) { .section { padding: 2.5rem 1rem 0; } }
.section > h2 { font-family: "Anybody", sans-serif; font-weight: 900; font-stretch: 135%; font-size: clamp(1.8rem, 3.4vw, 2.6rem); line-height: 1; letter-spacing: -0.025em; text-wrap: balance; }
.explain { color: var(--muted); font-size: 0.98rem; max-width: 64ch; text-wrap: pretty; }
.section > .explain { padding: 0.6rem 0 1.5rem; }

/* CDs: each card has a 3D acrylic case and a title. The grid has 3 columns, then 2 under 900px, then 1 under 520px. */
.cds { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.25rem; }
@media (max-width: 900px) { .cds { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Phones: two CDs per row, so the list is half as long. The text inside gets a smaller inset. */
@media (max-width: 520px) { .cds { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.75rem; } .cd h3, .cd .cd-sub, .cd .cd-meta { padding-inline: 0.8rem; } .cd h3 { font-size: 1.05rem; font-stretch: 100%; } .cd-meta { margin-bottom: 0.9rem; } }
.cd { display: grid; grid-template-rows: auto 1fr; min-width: 0; border-radius: calc(var(--r) + 6px); background: linear-gradient(180deg, var(--panel-2), var(--panel)); border: 1px solid var(--line); overflow: hidden; transition: border-color 0.2s, translate 0.25s var(--ease); }
.cd:hover { border-color: oklch(0.78 0.17 142 / 0.6); translate: 0 -3px; }
/* .cd-cover: perspective gives the 3D case its depth. The right padding leaves room for the disc that slides out. */
.cd-cover { display: block; width: 100%; perspective: 1100px; padding: 7% 20% 5% 9%; background: radial-gradient(90% 70% at 40% 45%, oklch(0.3 0.04 142 / 0.45), transparent 70%); }
/* .case: turned in 3D. --ry and --rx set the angles. */
.case { position: relative; display: block; aspect-ratio: 1; transform-style: preserve-3d; transform: rotateY(var(--ry, 16deg)) rotateX(var(--rx, 3deg)); transition: transform 0.5s var(--ease); }
/* a shorter transition on hover and when open, so the tilt reacts fast */
.cd-cover:hover .case, .cd.open .case { transition-duration: 0.15s; }
/* The layers of the case in 3D: art at +10px, shine at +10.5px (above the art), spine turned 90deg at the left edge, disc behind at -2px. */
.case .art, .case .spine, .case .shine, .case .cd-disc { position: absolute; }
.case .art { inset: 0; width: 100%; height: 100%; object-fit: cover; border-radius: 3px; transform: translateZ(10px); background: var(--panel-2); box-shadow: inset 0 0 0 1px oklch(1 0 0 / 0.08); }
.case .shine { inset: 0; border-radius: 3px; transform: translateZ(10.5px); pointer-events: none; background: linear-gradient(115deg, oklch(1 0 0 / 0.22) 0%, oklch(1 0 0 / 0.05) 32%, transparent 45%, transparent 70%, oklch(1 0 0 / 0.08) 100%); border: 1px solid oklch(1 0 0 / 0.14); }
.case .spine { left: 0; top: 0; width: 20px; height: 100%; transform-origin: 0 50%; transform: translateZ(10px) rotateY(90deg); background: linear-gradient(90deg, oklch(0.2 0.01 255), oklch(0.12 0.01 255)); border-radius: 2px; }
.case .cd-disc {
  inset: 3%; border-radius: 50%; transform: translateZ(-2px); transition: transform 0.6s var(--ease);
  background: radial-gradient(circle, var(--night) 0 8%, oklch(0.82 0.02 255) 8.5% 10%, transparent 10.5%),
              conic-gradient(from 20deg, oklch(0.88 0.05 200), oklch(0.86 0.07 320), oklch(0.92 0.07 90), oklch(0.86 0.07 160), oklch(0.88 0.05 200));
  box-shadow: 0 0 0 1px oklch(0.7 0.02 255);
}
/* The disc slides out of the case on hover, when the CD is open and when it plays. */
.cd-cover:hover .cd-disc, .cd.open .cd-disc, .cd.playing .cd-disc { transform: translateZ(-2px) translateX(34%); }
/* a soft shadow on the floor under the case */
.cd-cover::after { content: ""; display: block; height: 14px; margin: 5% 6% 0 4%; border-radius: 50%; background: radial-gradient(closest-side, oklch(0 0 0 / 0.55), transparent); }
.cd h3, .cd .cd-sub, .cd .cd-meta { padding-inline: 1.25rem; }
.cd h3 { font-family: "Anybody", sans-serif; font-weight: 900; font-stretch: 125%; font-size: clamp(1.1rem, 1.5vw, 1.35rem); line-height: 1.05; letter-spacing: -0.015em; padding-top: 0.2rem; }
.cd h3 button { text-align: left; display: inline-flex; align-items: center; min-height: 44px; margin-block: -10px; } /* 44px target, negative margin keeps the card height */
.cd h3 button:hover { color: var(--frog); }
.cd-sub { color: var(--muted); font-size: 0.95rem; text-wrap: pretty; margin-top: 0.35rem; }
.cd-meta { color: var(--faded); font-size: 0.9rem; font-family: "Geist Mono", monospace; margin: 0.35rem 0 1.25rem; }
.cd.playing h3 { color: var(--frog); }
.cd.open .art { box-shadow: inset 0 0 0 2px var(--frog); }
/* .cd-panel: the open CD. It spans the full grid row (grid-column: 1 / -1) and shows the cover and the track list. */
.cd-panel {
  grid-column: 1 / -1; position: relative; display: grid; grid-template-columns: minmax(15rem, 24rem) minmax(0, 1fr); gap: 1.5rem 2.75rem; align-items: start;
  padding: 1.75rem; border-radius: calc(var(--r) + 4px); background: var(--panel); border: 1px solid var(--line); animation: unfold 0.4s var(--ease);
}
@keyframes unfold { from { opacity: 0; translate: 0 -8px; } }
.cd-panel .cd-cover { padding: 2% 18% 6% 0; cursor: default; }
.cd-panel .case { --ry: 14deg; }
.cd-panel .cd-disc { transform: translateZ(-2px) translateX(30%); }
/* While it plays, the disc turns 360deg every 3 seconds. */
.cd-panel.playing .cd-disc { animation: spin-cd 3s linear infinite; }
@keyframes spin-cd { from { transform: translateZ(-2px) translateX(30%) rotate(0deg); } to { transform: translateZ(-2px) translateX(30%) rotate(360deg); } }
.close-cd { position: absolute; top: 0.75rem; right: 0.75rem; width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center; color: var(--muted); transition: background 0.15s, color 0.15s; }
.close-cd:hover { background: var(--panel-3); color: var(--ink); }
.close-cd svg { width: 20px; height: 20px; }
.cd-body { min-width: 0; display: grid; gap: 0.3rem; padding-top: 0.4rem; }
.cd-panel h3 { font-family: "Anybody", sans-serif; font-weight: 900; font-stretch: 130%; font-size: clamp(1.7rem, 3vw, 2.4rem); line-height: 1; letter-spacing: -0.02em; text-wrap: balance; padding-right: 3rem; }
.cd-panel .cd-sub { font-size: 1rem; max-width: 56ch; }
.cd-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 0.8rem 0 0.9rem; }
/* .tracks: the track list. .track-num is the number, .track-title the title, .track-dur the duration. Rows fade in one by one (--i is the row index, 25ms apart). */
.tracks { list-style: none; padding: 0.5rem 0 0; display: grid; gap: 1px; border-top: 1px solid var(--line); }
.tracks li { animation: rise 0.35s var(--ease) backwards; animation-delay: calc(var(--i) * 25ms); }
@keyframes rise { from { opacity: 0; translate: 0 6px; } }
.tracks button { width: 100%; display: grid; grid-template-columns: 2.2rem minmax(0, 1fr) auto; gap: 0.75rem; align-items: center; min-height: 52px; padding: 0.3rem 0.6rem; border-radius: 8px; text-align: left; transition: background 0.15s; }
.tracks button:hover { background: var(--panel-2); }
.tracks .track-num { font-family: "Geist Mono", monospace; color: var(--faded); font-size: 0.9rem; text-align: right; }
.tracks button:hover .track-num { color: var(--frog); }
.tracks .track-title { display: grid; min-width: 0; }
.tracks .track-title b { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tracks .track-title small { color: var(--muted); font-size: 0.875rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tracks .track-dur { font-family: "Geist Mono", monospace; color: var(--muted); font-size: 0.9rem; }
.tracks button.current { background: var(--frog-bg); }
.tracks button.current .track-title b, .tracks button.current .track-num { color: var(--frog); }
/* Narrow screens: the cover goes above the track list. */
@media (max-width: 760px) { .cd-panel { grid-template-columns: 1fr; padding: 1.1rem; } .cd-panel .cd-cover { max-width: 17rem; } }

/* .hall: a grid of chatter cards. .legend is one card.
   The top 3 get a green border. The first one gets a green fill. */
.hall { list-style: none; padding: 0; display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr)); }
.legend { display: grid; grid-template-columns: 2.4rem minmax(0, 1fr); gap: 0.6rem; padding: 0.9rem 0.8rem; border-radius: var(--r); align-items: start; background: var(--panel); border: 1px solid var(--line); }
.legend:nth-child(-n+3) { border-color: oklch(0.78 0.17 142 / 0.5); }
.legend:nth-child(1) { background: var(--frog-bg); }
.legend .pos { padding-top: 0.05rem; }
.legend:nth-child(1) .pos { color: var(--frog); }
.legend-name { font-weight: 700; font-size: 1.02rem; display: flex; align-items: center; gap: 0.4rem; overflow-wrap: anywhere; }
.legend-name img { height: 24px; width: auto; }
.legend-meta { color: var(--muted); font-size: 0.9rem; }
.legend-songs { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.4rem; }
.legend-songs button { min-height: 44px; padding: 0 0.7rem; border-radius: 999px; border: 1px solid var(--line); font-size: 0.9rem; transition: border-color 0.2s, color 0.2s; }
.legend-songs button:hover { border-color: var(--frog); color: var(--frog); }

/* Words & Themes (.stats): big numbers, word counts and records. Captions use the chat dialect. */
.stats { display: grid; gap: 1.5rem; }
.numbers { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 13rem), 1fr)); gap: 10px; }
.num { padding: 1rem 1.1rem; border-radius: var(--r); background: var(--panel); border: 1px solid var(--line); display: grid; gap: 0.25rem; align-content: start; }
.num b { font-family: "Anybody", sans-serif; font-weight: 900; font-stretch: 125%; font-size: clamp(1.9rem, 3.2vw, 2.6rem); line-height: 1; letter-spacing: -0.02em; color: var(--frog); display: flex; align-items: center; gap: 0.35rem; }
.num b img { height: 0.9em; width: auto; }
.num span { color: var(--muted); font-size: 0.92rem; text-wrap: pretty; }
.num span img { height: 1.4em; vertical-align: middle; margin-top: -0.25em; }
.stats h3 { font-size: 1.05rem; font-weight: 700; margin-bottom: 0.6rem; }
.counts { display: grid; gap: 1.75rem; }
.count { list-style: none; padding: 0.7rem 0 0; display: flex; flex-wrap: wrap; gap: 1.1rem 0.7rem; align-items: flex-start; }
/* --tilt is a small tilt per card. It straightens on hover. */
.count li { position: relative; display: grid; gap: 0.2rem; padding: 0.85rem 1rem 0.8rem; border-radius: var(--r); background: var(--panel); border: 1px solid var(--line); rotate: var(--tilt, 0deg); transition: rotate 0.3s var(--spring), scale 0.3s var(--spring), border-color 0.2s; }
.count li:hover { rotate: 0deg; scale: 1.05; border-color: var(--frog); }
.count li.top { background: var(--panel-2); border-color: oklch(0.78 0.17 142 / 0.45); }
/* a small badge on the top edge of the card */
.count small { position: absolute; top: -0.75rem; left: 0.75rem; padding: 0 0.45rem; border-radius: 999px; background: var(--frog); color: var(--varnish); font-family: "Geist Mono", monospace; font-weight: 700; font-size: 0.875rem; line-height: 1.5; }
.count b { font-family: "Anybody", sans-serif; font-weight: 900; font-stretch: 125%; font-size: 1.9rem; line-height: 1; letter-spacing: -0.02em; color: var(--frog); font-variant-numeric: tabular-nums; }
.count li.top b { font-size: 2.7rem; }
.count span { display: flex; align-items: center; gap: 0.4rem; max-width: 20ch; color: var(--ink); font-size: 0.95rem; line-height: 1.3; text-wrap: pretty; }
.count span img { height: 1.7em; width: auto; flex: none; }
.records { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr)); gap: 10px; }
.records button { text-align: left; padding: 0.8rem 1rem; border-radius: var(--r); background: var(--panel); border: 1px solid var(--line); display: grid; gap: 0.15rem; transition: border-color 0.2s; }
.records button:hover { border-color: var(--frog); }
.records small { color: var(--faded); font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 650; }
.records b { font-weight: 650; }
.records span { color: var(--muted); font-size: 0.9rem; font-family: "Geist Mono", monospace; }

/* credits (.credits): the footer with the source note, two buttons and the disclaimer */
.credits { padding: 4rem 1.25rem 2.5rem; display: grid; gap: 1rem; }
.quin { display: flex; gap: 1rem; align-items: flex-start; background: var(--panel); border: 1px solid var(--line); border-radius: var(--r); padding: 1rem 1.1rem; max-width: 48rem; }
.quin img { border-radius: 50%; flex: none; }
.quin p { max-width: 62ch; color: var(--muted); }
.quin a { padding-block: 3px; } /* the link box reaches the 24px minimum target */
.quin .quin-name { color: var(--ink); font-weight: 650; margin-bottom: 0.2rem; }
.footer-buttons { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.disclaimer { color: var(--muted); font-size: 0.92rem; max-width: 70ch; text-wrap: pretty; }

/* ---------- .sticky: the sticky player bar at the top ---------- */
/* A fixed bar that slides in from the top when the video leaves the screen.
   z-index 60 puts it above the rain (50) and the smoke (45). On desktop, right leaves the side columns free. */
.sticky {
  position: fixed; top: 0; left: 0; right: calc(var(--set) + var(--emo)); z-index: 60; height: var(--sticky-h);
  background: oklch(0.15 0.016 255 / 0.97); border-bottom: 1px solid var(--line); box-shadow: 0 10px 30px -14px oklch(0 0 0 / 0.8);
  translate: 0 -100%; transition: translate 0.35s var(--ease);
}
@media (max-width: 1099px) { .sticky { right: 0; } }
.sticky.on { translate: 0 0; }
.sticky[hidden] { display: none; }
/* On desktop the player box is always in view in the side column, so the bar never shows. */
@media (min-width: 1100px) { .sticky { display: none !important; } }
/* Desktop: the content column leaves a lane on the left for the "send a banger" tab, so the tab never covers the video. */
@media (min-width: 1100px) { .content { padding-left: 28px; } }
.sticky-inner { height: 100%; padding: 0 1.25rem; display: flex; align-items: center; gap: 0.9rem; }
.sticky-dj { height: 32px; width: auto; flex: none; }
.sticky-name { display: flex; align-items: center; gap: 0.3em; flex: 0 1 auto; min-width: 0; max-width: 22rem; min-height: 44px; text-align: left; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sticky-line { flex: 1 1 0; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: "Anybody", sans-serif; font-weight: 800; font-stretch: 110%; font-size: 1.05rem; color: var(--faded); }
.sticky-line .w.sung { color: var(--ink); }
.sticky-line .w.active, .sticky-line .w.aboba { color: var(--frog); }
.sticky-controls { display: flex; gap: 0.3rem; align-items: center; flex: none; }
.sticky-controls .ctl.play { width: 44px; height: 44px; }
/* a thin progress line on the bottom edge of the bar */
.sticky-progress { position: absolute; left: 0; bottom: -1px; height: 2px; width: 0; background: var(--frog); }
/* Narrow screens: hide the lyric line and the previous button. */
@media (max-width: 700px) { .sticky-inner { padding: 0 0.75rem; gap: 0.6rem; } .sticky-line { display: none; } .sticky-name { flex: 1 1 0; max-width: none; } #stickyPrev { display: none; } }

/* ---------- emote rain and page effects ---------- */
/* .rain: a full-screen layer for the falling emotes (z-index 50).
   The layer ignores the pointer, but each emote accepts it. The page below stays usable, and emotes are clickable for points.
   will-change: transform puts each emote on its own GPU layer, because app.js moves them every frame. */
.rain { position: fixed; inset: 0; pointer-events: none; overflow: hidden; z-index: 50; }
.rain img { position: absolute; top: 0; left: 0; will-change: transform; pointer-events: auto; cursor: crosshair; user-select: none; -webkit-user-drag: none; }
/* .hit: turns off clicks on that emote */
.rain img.hit { pointer-events: none; }
/* .score-pop: the floating score text */
.rain .score-pop { position: absolute; top: 0; left: 0; font-family: "Anybody", sans-serif; font-weight: 900; font-stretch: 130%; font-size: 1.5rem; color: var(--frog); pointer-events: none; white-space: nowrap; }
/* .smoke: a soft haze over the page (z-index 45). It fades in with .on. */
.smoke { position: fixed; inset: 0; z-index: 45; pointer-events: none; opacity: 0; transition: opacity 1.2s;
  background: radial-gradient(40rem 30rem at 20% 80%, oklch(0.8 0.02 140 / 0.3), transparent 70%), radial-gradient(40rem 30rem at 80% 70%, oklch(0.8 0.02 140 / 0.25), transparent 70%); }
.smoke.on { opacity: 1; }
/* "Fried" mode on the whole page (body.fried): strong color and a hue cycle every 1.2 seconds. In fullscreen it moves to .scene. */
body.fried .app { animation: fry 1.2s linear infinite; }
@keyframes fry { from { filter: saturate(3) contrast(1.4) hue-rotate(0deg); } to { filter: saturate(3) contrast(1.4) hue-rotate(360deg); } }
/* .toast: a short message at the top center. It sits under the sticky bar (bar height plus 12px). */
.toast {
  position: fixed; left: 50%; top: calc(var(--sticky-h) + 12px); translate: -50% -12px; z-index: 70; opacity: 0; pointer-events: none; max-width: calc(100% - 32px); text-align: center;
  background: var(--ink); color: var(--varnish); padding: 0.55rem 1.1rem; border-radius: 10px; font-weight: 600; transition: opacity 0.25s, translate 0.35s var(--spring);
}
.toast.on { opacity: 1; translate: -50% 0; }

/* ---------- .drawer: side panels that slide in ---------- */
/* .drawer: a <dialog> that slides in from the right. It shows song details (#details) and the "wtf is this" text (#about). margin-left: auto pushes it to the right edge. */
.drawer {
  margin: 0 0 0 auto; height: 100dvh; max-height: none; width: min(34rem, 100%); padding: 1.5rem 1.5rem 2.5rem; border: 0; border-left: 1px solid var(--line);
  background: var(--panel); color: var(--ink); overflow-y: auto; overscroll-behavior: contain;
}
.drawer[open] { animation: slide-in 0.4s var(--ease); }
@keyframes slide-in { from { translate: 100% 0; } }
.drawer::backdrop { background: oklch(0.08 0.01 255 / 0.6); }
.drawer h2 { font-family: "Anybody", sans-serif; font-weight: 900; font-stretch: 130%; font-size: 1.9rem; line-height: 1.05; letter-spacing: -0.02em; text-wrap: balance; margin: 0.25rem 3rem 0.75rem 0; }
.drawer h3 { font-size: 1rem; font-weight: 700; margin: 1.1rem 0 0.3rem; }
.drawer p { color: var(--muted); max-width: 62ch; text-wrap: pretty; }
.close { position: absolute; top: 1rem; right: 1rem; width: 44px; height: 44px; border-radius: 50%; font-size: 1.6rem; line-height: 1; border: 1px solid var(--line); }
.close:hover { border-color: var(--muted); }
.det-fields { display: grid; grid-template-columns: auto 1fr; gap: 0.4rem 1rem; margin: 0.5rem 0 0; }
.det-fields dt { color: var(--muted); }
.det-fields dd { margin: 0; font-weight: 500; }
.chips { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.chips span { font-size: 0.9rem; border: 1px solid var(--line); border-radius: 6px; padding: 0.1rem 0.5rem; color: var(--muted); }
.reactions { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.reactions span { display: inline-flex; align-items: center; gap: 0.35rem; background: var(--panel-2); border-radius: 999px; padding: 0.2rem 0.65rem 0.2rem 0.35rem; font-variant-numeric: tabular-nums; }
.reactions img { height: 22px; width: auto; }
.token { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin-top: 0.4rem; }
.token code { font-family: "Geist Mono", monospace; background: var(--panel-2); padding: 0.5rem 0.7rem; border-radius: 8px; font-size: 0.95rem; }
.btn { min-height: 44px; padding: 0 1.1rem; border-radius: 10px; background: var(--frog); color: var(--varnish); font-weight: 650; display: inline-flex; align-items: center; gap: 0.45rem; text-decoration: none; transition: background 0.15s, scale 0.15s var(--ease); }
.btn:hover { background: oklch(0.84 0.16 142); }
.btn:active { scale: 0.96; }
.btn svg { width: 16px; height: 16px; }
.btn.secondary { background: none; color: var(--ink); border: 1px solid var(--line); }
.btn.secondary:hover { background: var(--panel-3); }
.det-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1rem; }
.drawer .transcript { white-space: pre-line; color: var(--ink); opacity: 0.88; }

/* ---------- .intro: the full-screen intro ---------- */
/* .intro: an overlay above everything (z-index 80). The browser blocks sound before a click, so the .start-btn button starts the music. */
.intro { position: fixed; inset: 0; z-index: 80; display: grid; place-content: center; justify-items: center; gap: 1rem; text-align: center; padding: 1.5rem; background: oklch(0.1 0.012 255 / 0.97); }
.intro p { color: var(--muted); max-width: 42ch; }
.intro-title { margin: 0; font-family: "Anybody", sans-serif; font-weight: 900; font-stretch: 140%; font-size: clamp(2rem, 7vw, 3.2rem); line-height: 1; letter-spacing: -0.02em; }
.intro-title b { color: var(--frog); font-stretch: 70%; margin-left: 0.1em; }
.intro .intro-what { margin: 0; color: var(--ink); font-size: 1.05rem; }
.intro-volume { gap: 0.5rem; padding: 0.4rem 0.9rem; border: 1px solid var(--line); border-radius: 999px; background: var(--panel-2); color: var(--ink); }
.intro-volume input { width: 150px; }
.intro-volume output { min-width: 4ch; font-family: "Geist Mono", monospace; font-size: 0.9rem; color: var(--muted); text-align: right; }
.intro .intro-note { margin: 0; color: var(--frog); font-size: 0.95rem; }
.intro .intro-fun { margin: 0.5rem 0 0; font-size: 0.875rem; max-width: 46ch; }
.start-btn {
  display: grid; justify-items: center; gap: 0.6rem; padding: 1.4rem 2.2rem; border-radius: 20px; background: var(--frog); color: var(--varnish);
  font-family: "Anybody", sans-serif; font-weight: 900; font-stretch: 140%; font-size: 1.6rem; letter-spacing: -0.01em; transition: scale 0.2s var(--ease);
}
.start-btn:hover { scale: 1.04; }
/* Intro motion. Only transform and opacity animate, so the GPU does little work. */
/* safe center: on a short screen the intro scrolls from the top instead of being cut. */
.intro { overflow: hidden auto; align-content: safe center; }
.intro > :not(.intro-fly) { position: relative; z-index: 1; animation: intro-in 0.7s var(--ease) both; }
.intro > :nth-child(2) { animation-delay: 0.05s; } .intro > :nth-child(3) { animation-delay: 0.12s; }
.intro > :nth-child(4) { animation-delay: 0.2s; } .intro > :nth-child(5) { animation-delay: 0.28s; }
.intro > :nth-child(6) { animation-delay: 0.36s; } .intro > :nth-child(7) { animation-delay: 0.42s; }
.intro > :nth-child(8) { animation-delay: 0.5s; } .intro > :nth-child(9) { animation-delay: 0.58s; }
@keyframes intro-in { from { opacity: 0; transform: translateY(14px); } }
.intro-title b { display: inline-block; animation: dj-pump 0.47s ease-in-out infinite alternate; transform-origin: 50% 80%; }
@keyframes dj-pump { to { transform: scale(1.08) rotate(-3deg); } }
.start-btn img { animation: dj-bob 0.47s ease-in-out infinite alternate; }
@keyframes dj-bob { to { transform: translateY(-5px) rotate(2deg); } }
/* .intro-fly: emotes that float from the bottom to the top. Each one has its own --x, --s, --dur, --delay, --dx, --r. */
.intro-fly { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.intro-fly img { position: absolute; left: var(--x); top: 100%; width: var(--s); height: auto; opacity: 0.32; animation: intro-fly var(--dur) linear var(--delay) infinite; will-change: transform; }
@keyframes intro-fly { from { transform: translate3d(0, 0, 0) rotate(0deg); } to { transform: translate3d(var(--dx), calc(-100vh - 140px), 0) rotate(var(--r)); } }
/* .intro-new: the newest changelog line. A click opens the changelog. */
.intro-new { display: inline-flex; align-items: center; gap: 0.5rem; min-height: 36px; padding: 0.25rem 0.85rem 0.25rem 0.3rem; border: 1px solid var(--line); border-radius: 999px; background: var(--panel-2); color: var(--ink); font-size: 0.875rem; transition: border-color 0.15s; }
.intro-new:hover { border-color: var(--frog); }
.intro-new b { padding: 0.1rem 0.5rem; border-radius: 999px; background: var(--frog); color: var(--varnish); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; }
/* .changelog: one line per change, the date in mono on the left */
.changelog { list-style: none; padding: 0; margin: 0.5rem 0 0; display: grid; gap: 0.6rem; }
.changelog li { display: grid; grid-template-columns: 4.5rem 1fr; gap: 0.75rem; color: var(--ink); max-width: 62ch; }
.changelog time { font-family: "Geist Mono", monospace; font-size: 0.85rem; color: var(--frog); padding-top: 0.1rem; }

/* .gone: the fade-out after the click */
.intro.gone { opacity: 0; pointer-events: none; transition: opacity 0.4s var(--ease); }

/* Users who prefer less motion: animations and transitions end at once. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
/* the 3D case stays flat */
  .case { transform: none !important; }
}

/* Phones: the right column dissolves into the page. The fixed grid positions of the desktop layout are removed.
   This rule stays near the end of the file, so it wins over the base rules. */
@media (max-width: 1099px) { .setlist, .emotes, .player-box { grid-column: auto; grid-row: auto; } }

/* ---------- intro: the list of things to do ---------- */
.intro-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.35rem; color: var(--muted); font-size: 0.95rem; }
.intro-list li::before { content: "+"; color: var(--frog); font-weight: 700; margin-right: 0.45rem; }
.intro-list img { height: 1.5em; width: auto; vertical-align: middle; margin-top: -0.2em; }

/* ---------- "more below" hint: the arrow and the "see all" button ---------- */
.setlist, .emotes { position: relative; }
/* .more: a bar at the bottom of the setlist and of the emote bar. A gradient fades the list under it.
   pointer-events: none lets clicks pass through, except on its buttons. .end hides the arrow at the end of the list. */
.more { position: absolute; left: 0; right: 0; bottom: 0; z-index: 2; display: flex; justify-content: center; align-items: center; gap: 0.5rem; padding: 2.4rem 0.6rem 0.7rem; background: linear-gradient(to bottom, transparent, var(--panel) 55%); pointer-events: none; }
.more button { pointer-events: auto; }
.more-arrow { display: inline-flex; align-items: center; gap: 0.35rem; min-height: 44px; padding: 0 0.95rem 0 0.75rem; border-radius: 999px; background: var(--frog); color: var(--varnish); font-weight: 700; font-size: 0.95rem; box-shadow: 0 6px 18px -6px oklch(0 0 0 / 0.8); transition: scale 0.2s var(--spring), opacity 0.2s; }
.more-arrow svg { width: 18px; height: 18px; animation: bob-down 1.4s var(--ease) infinite; }
@keyframes bob-down { 0%, 100% { translate: 0 -1px; } 50% { translate: 0 3px; } }
.more-arrow:hover { scale: 1.06; }
.more-arrow:active { scale: 0.94; }
.more-all { min-height: 44px; padding: 0 0.95rem; border-radius: 999px; background: var(--panel-3); border: 1px solid var(--muted); color: var(--ink); font-weight: 650; font-size: 0.95rem; transition: border-color 0.15s, background 0.15s; }
.more-all:hover { border-color: var(--frog); }
.more.end .more-arrow { opacity: 0; pointer-events: none; }
/* The arrow is not needed when the panel is open at full size. */
.side.open-set .more-arrow, .side.open-emo .more-arrow { display: none; }
/* In the narrow emote column, the arrow and the button stack. */
#moreEmo { flex-direction: column; gap: 0.4rem; padding: 2rem 6px 0.6rem; }
#moreEmo .more-arrow { width: 44px; height: 44px; padding: 0; justify-content: center; }
#moreEmo .more-all { min-height: 44px; padding: 0 0.6rem; font-size: 0.875rem; }
/* Bottom padding keeps the .more bar from covering the last items. */
.setlist .list { padding-bottom: 4.5rem; }
.throw-bar { padding-bottom: 7rem; }

/* Desktop "see all": the setlist (.open-set) or the emote bar (.open-emo) takes the whole side column. The other panels hide. */
@media (min-width: 1100px) {
  .side.open-set .setlist { grid-column: 1 / -1; grid-row: 1 / -1; }
  .side.open-set .player-box, .side.open-set .emotes { display: none; }
  .side.open-emo .emotes { grid-column: 1 / -1; grid-row: 1 / -1; border-left: 0; }
  .side.open-emo .setlist, .side.open-emo .player-box { display: none; }
  .side.open-emo .throw-bar { grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); grid-auto-rows: 72px; padding: 0 12px 6rem; }
  .side.open-emo .throw-bar img { max-height: 52px; }
  .side.open-emo .throw-style { align-self: center; padding: 0 1.2rem; }
}
/* Phones: no "see all" for emotes, because the bar already shows every emote. */
@media (max-width: 1099px) {
  #moreEmo { display: none; }
  .throw-bar { padding-bottom: 0; }
  /* Here and not in the first phone block: the base rule .setlist .scroller (flex 1 1 0) comes later in the file and won there,
     so the list was 0 px high on phones. */
  .setlist .scroller { flex: none; max-height: 72dvh; }
  /* "See all" on phones: the list grows to full length and the page scrolls it. The list stops being a scroll box,
     because an empty scroll box with overscroll-behavior: contain catches the touch and the page does not move.
     The "show less" bar sticks to the bottom of the screen. */
  .side.open-set .setlist .scroller { max-height: none; overflow: visible; overscroll-behavior: auto; }
  .side.open-set #moreSet { position: sticky; }
}

/* ---------- player box, minimized (.mini) ----------
   .mini keeps only the track row and a small play button. app.js saves the choice in localStorage. */
.pb-track { grid-auto-flow: column; grid-auto-columns: auto; }
.pb-minimize { width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center; color: var(--muted); border: 1px solid var(--line); transition: color 0.15s, border-color 0.15s, rotate 0.3s var(--ease); }
.pb-minimize:hover { color: var(--ink); border-color: var(--muted); }
.pb-minimize svg { width: 20px; height: 20px; }
.pb-mini-play { display: none; }
.player-box.mini { gap: 0; padding-block: 0.6rem; }
.player-box.mini > :not(.pb-track) { display: none; }
.player-box.mini .pb-cover { width: 44px; height: 44px; }
.player-box.mini .pb-sub { display: none; }
.player-box.mini .pb-mini-play { display: grid; width: 44px; height: 44px; }
.player-box.mini .pb-minimize { rotate: 180deg; }

/* ---------- fullscreen (body.fullscreen) ---------- */
.fullscreen-btn { position: absolute; right: 1.2%; bottom: 2.4%; z-index: 5; width: max(44px, 3.2cqw); height: max(44px, 3.2cqw); border-radius: 10px; display: grid; place-items: center; color: #fff; background: oklch(0 0 0 / 0.6); border: 1px solid oklch(1 0 0 / 0.25); transition: background 0.15s, border-color 0.15s, scale 0.2s var(--spring); }
.fullscreen-btn:hover { background: oklch(0 0 0 / 0.8); border-color: var(--frog); scale: 1.06; }
.fullscreen-btn svg { width: 55%; height: 55%; }
/* --fs-side: width of the emote column (landscape). --fs-bottom: height of the emote row (portrait). The video fits in the space that is left. */
body.fullscreen { --fs-side: 136px; --fs-bottom: 0px; overflow: hidden; background: #000; }
@media (orientation: portrait) { body.fullscreen { --fs-side: 0px; --fs-bottom: 116px; } }
.fullscreen .channel, .fullscreen .section, .fullscreen .credits, .fullscreen .setlist, .fullscreen .player-box, .fullscreen .sticky, .fullscreen #moreEmo { display: none !important; }
.fullscreen .side { background: none; border: 0; }
/* The video is centered in the free space. Its width is the smaller of the free width and the free height times the image ratio (1658/926). */
.fullscreen .video { position: fixed; z-index: 40; top: calc((100dvh - var(--fs-bottom)) / 2); left: calc((100vw - var(--fs-side)) / 2); translate: -50% -50%; width: min(calc(100vw - var(--fs-side)), calc((100dvh - var(--fs-bottom)) * 1658 / 926)); border-radius: 0; }
.fullscreen .viz { left: 0; width: 100%; height: 62%; }
.fullscreen .emotes { position: fixed; z-index: 41; top: 0; right: 0; bottom: 0; left: auto; width: var(--fs-side); display: flex; flex-direction: column; padding: 0; margin: 0; background: var(--panel); border-left: 1px solid var(--line); }
.fullscreen .throw-bar { flex: 1 1 0; min-height: 0; overflow-y: auto; grid-template-columns: 1fr; grid-auto-rows: 100px; gap: 8px; padding: 0 10px 1rem; }
.fullscreen .throw-bar img { max-height: 76px; }
.fullscreen .throw-style { margin: 0 10px 0.6rem; }
@media (orientation: portrait) {
  .fullscreen .emotes { top: auto; left: 0; width: auto; height: var(--fs-bottom); flex-direction: row; align-items: center; border-left: 0; border-top: 1px solid var(--line); }
  .fullscreen .emotes-title { display: none; }
  .fullscreen .throw-style { margin: 0 0 0 10px; padding: 0 0.9rem; flex: none; }
  .fullscreen .throw-bar { height: 100%; overflow-x: auto; overflow-y: hidden; grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: 92px; grid-template-rows: 1fr; padding: 10px; }
  .fullscreen .throw-bar img { max-height: 64px; }
}
/* In fullscreen, the fried effect moves from .app to .scene, so the filter applies to the video only. */
body.fried.fullscreen .app { animation: none; }
body.fried.fullscreen .scene { animation: fry 1.2s linear infinite; }
/* Late rules. Users who ask for less motion get no arrow bounce and no card tilt. Small phones get two columns of word counts. Emotes in word counts have a size cap. */
@media (prefers-reduced-motion: reduce) { .intro-fly { display: none; } .more-arrow svg { animation: none; } .count li { rotate: 0deg; } }
@media (max-width: 520px) { .count li { flex: 1 1 calc(50% - 0.7rem); } .count span { max-width: none; } }
.count span img { max-width: 3em; object-fit: contain; }

/* ---------- send a banger (#sendTab, #send) ---------- */
/* .send-tab: a vertical tab on the left edge, at mid height. z-index 55 puts it above the rain (50), below the sticky bar (60). */
.send-tab {
  position: fixed; left: 0; top: 50%; z-index: 55; translate: 0 -50%;
  writing-mode: vertical-rl; rotate: 180deg; /* the text reads from bottom to top */
  min-width: 40px; padding: 1rem 0.55rem; border-radius: 10px 0 0 10px; /* the 180deg turn moves these round corners to the right side */
  background: var(--frog); color: var(--varnish); font-weight: 700; font-size: 0.9375rem; letter-spacing: 0.01em;
  transition: background 0.15s, scale 0.15s var(--ease);
}
.send-tab:hover { background: oklch(0.84 0.16 142); }
.send-tab:active { scale: 0.96; }
body.fullscreen .send-tab { display: none; }
/* Attention effect. Only transform and opacity animate, so the GPU does not repaint.
   1. tab-nudge: every 6 s the tab slides out 10 px and shakes, then goes back.
   2. ::after: a green ring grows from the tab and fades out, in the same rhythm.
   It stops after the first open (.seen) and for users who ask for less motion. */
.send-tab { display: flex; align-items: center; gap: 0.5rem; animation: tab-nudge 6s var(--ease) 3s infinite; }
.send-tab img { rotate: 180deg; width: 32px; height: auto; } /* the img is last in the HTML: the turn puts it on top and upright */
/* 12 px of the tab stay off screen (left -12px, extra padding on the screen side), so the nudge shows no gap at the edge. */
.send-tab { left: -12px; padding-right: calc(0.55rem + 12px); }
.send-tab::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit; border: 2px solid var(--frog);
  opacity: 0; pointer-events: none; animation: tab-ring 6s ease-out 3s infinite;
}
@keyframes tab-nudge {
  0%, 82%, 100% { translate: 0 -50%; rotate: 180deg; }
  86% { translate: 10px -50%; rotate: 176deg; }
  90% { translate: 10px -50%; rotate: 184deg; }
  94% { translate: 10px -50%; rotate: 178deg; }
}
@keyframes tab-ring {
  0%, 82% { opacity: 0; scale: 1; }
  84% { opacity: 0.9; scale: 1; }
  100% { opacity: 0; scale: 1.35 1.12; }
}
.send-tab.seen, .send-tab.seen::after { animation: none; }
@media (prefers-reduced-motion: reduce) { .send-tab, .send-tab::after { animation: none; } }
/* Phones and tablets: only the ABOBA shows (the text stays for screen readers), so the tab covers less of the page. */
@media (max-width: 1099px) {
  .send-tab { min-width: 0; min-height: 52px; padding: 0.55rem calc(0.35rem + 12px) 0.55rem 0.35rem; }
  .send-tab-text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .send-tab img { width: 30px; }
}
.send-form { display: grid; gap: 0.35rem; margin-top: 1.2rem; }
.send-form label { font-weight: 650; margin-top: 0.7rem; }
.send-form label b, .send-form label span { font-weight: 500; font-size: 0.875rem; color: var(--faded); margin-left: 0.35rem; }
.send-form label b { color: var(--frog); }
.send-form input, .send-form textarea {
  font: inherit; color: var(--ink); background: var(--panel-2); border: 1px solid var(--line); border-radius: 10px;
  padding: 0.65rem 0.8rem; min-height: 44px; width: 100%; resize: vertical;
}
.send-form input:focus-visible, .send-form textarea:focus-visible { outline: 2px solid var(--frog); outline-offset: 1px; }
.send-form #sendToken { font-family: "Geist Mono", monospace; font-size: 0.97rem; }
.send-form input[aria-invalid="true"] { border-color: var(--bass); }
.send-hint { font-size: 0.875rem; color: var(--faded); }
.send-hint.error, .send-status.error { color: var(--bass); }
.send-hint.ok { color: var(--frog); }
.send-captcha { margin-top: 0.9rem; min-height: 65px; }
.send-form .btn { justify-self: start; margin-top: 0.4rem; }
.send-form .btn:disabled { opacity: 0.6; cursor: wait; }
.send-status { min-height: 1.5em; font-weight: 500; }
.send-status.ok { color: var(--frog); }

/* Secret "thorn": the shield that shows for 3 s during Thorn Daddy, and the loot that orbits ABOBA after a click. */
.egg-icon { position: absolute; z-index: 6; width: clamp(34px, 5%, 64px); padding: 0; border: 0; background: none; cursor: pointer; animation: egg-pop 0.35s var(--ease), egg-bob 0.8s ease-in-out 0.35s infinite alternate; }
.egg-icon img, .orbit img { display: block; width: 100%; height: auto; image-rendering: pixelated; -webkit-user-drag: none; }
@keyframes egg-pop { from { scale: 0; rotate: -30deg; } to { scale: 1; rotate: 0deg; } }
@keyframes egg-bob { to { translate: 0 -8px; } }
.orbit { position: absolute; inset: 0; pointer-events: none; z-index: 3; container-type: size; }
/* Ellipse path: the span moves on x and the img moves on y, each with ease-in-out, a quarter period apart.
   The full turn is 4 s. --k (0 to 3) puts the four items a quarter turn apart. */
.orbit span { position: absolute; left: 50%; top: 40%; width: clamp(24px, 14cqw, 60px); margin-left: calc(clamp(24px, 14cqw, 60px) / -2); animation: orbit-x 2s ease-in-out infinite alternate; animation-delay: calc(var(--k) * -1s); }
.orbit span img { animation: orbit-y 2s ease-in-out infinite alternate; animation-delay: calc(var(--k) * -1s - 1s); }
@keyframes orbit-x { from { transform: translateX(-62cqw); } to { transform: translateX(62cqw); } }
@keyframes orbit-y { from { transform: translateY(-55cqh); } to { transform: translateY(45cqh); } }
@media (prefers-reduced-motion: reduce) { .orbit span { animation-play-state: paused; } .egg-icon { animation: none; } }
/* .hints: the secret hints in the "wtf is this" drawer. Same color as the drawer text. */
.drawer .hints { color: var(--muted); padding-left: 1.2rem; margin: 0.5rem 0 0; display: grid; gap: 0.2rem; }
