@font-face {
  font-family: "Geist";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/s/geist-400.woff2") format("woff2");
}
@font-face {
  font-family: "Geist";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/s/geist-600.woff2") format("woff2");
}
@font-face {
  font-family: "JetBrains Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/s/jbmono-400.woff2") format("woff2");
}

:root {
  color-scheme: dark;
  --ink: #0b0b0e;
  --slate: #131318;
  --card: #1a1a21;
  --edge: #282831;
  --chalk: #eceef2;
  --ash: #8d8d9a;
  --faint: #5c5c68;
  --cerise: #ff8cb3;
  --on-cerise: #1d1016;
  --sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}

* { box-sizing: border-box; }

/* The hidden attribute is display:none in the UA sheet, which any author rule
   with a display beats. This page has plenty of those, so say it louder once
   rather than remember it at every call site. */
[hidden] { display: none !important; }

body {
  margin: 0;
  min-height: 100svh;
  padding: clamp(1rem, 3vw, 2rem);
  display: flex;
  flex-direction: column;
  gap: clamp(0.9rem, 2vw, 1.25rem);
  align-items: center;
  justify-content: center;
  background: var(--ink);
  color: var(--chalk);
  font: 400 15px/1.55 var(--sans);
  -webkit-font-smoothing: antialiased;
}

::selection { background: var(--cerise); color: var(--on-cerise); }

:focus-visible {
  outline: 2px solid var(--cerise);
  outline-offset: 3px;
  border-radius: 4px;
}

/* Every band shares the player's width so the page reads as one column. */
.band {
  width: 100%;
  max-width: min(1180px, 94vw);
}

/* Masthead: who made this on the left, which clip it is on the right. The id
   is the only handle anyone has on a clip, so it belongs beside the name. */
.masthead {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

/* On a page with no clip there is nothing to sit opposite, so the mark joins
   the message instead of hanging off the left edge on its own. */
.masthead.solo {
  justify-content: center;
  margin-bottom: 0.35rem;
}

.glyph { width: 21px; height: auto; display: block; flex: none; }

.wordmark {
  font-weight: 600;
  font-size: 0.9rem;
  letter-spacing: -0.01em;
  color: var(--chalk);
}

/* Never uppercased: an id is case-sensitive, and a viewer who reads it off the
   page and types it back has to get the same nine characters. */
.ident {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  color: var(--faint);
}

.frame {
  border-radius: 12px;
  overflow: hidden;
  background: #000;
  border: 1px solid var(--edge);
  box-shadow: 0 18px 50px rgb(0 0 0 / 0.5);
}

video {
  display: block;
  width: 100%;
  height: auto;
  max-height: 78svh;
  background: #000;
  cursor: pointer;
}

/* ── Player ──
   The bar sits under the picture rather than over it, the way the transport
   does in the desktop app, so nothing is ever covering the clip. */

.controls {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.4rem 0.7rem 0.5rem;
  background: var(--slate);
  border-top: 1px solid var(--edge);
}

.ctl {
  flex: none;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--chalk);
  cursor: pointer;
  transition: background 120ms ease;
}
.ctl:hover { background: var(--card); }
.ctl svg { width: 16px; height: 16px; fill: currentColor; }

/* The seek line. Watched is a wave, unwatched is a line: while the clip runs
   the wave travels, and the moment it stops it settles flat. That is the whole
   playing-or-not indicator, so nothing else has to say it. */
.seek {
  flex: 1 1 auto;
  min-width: 3rem;
  height: 22px;
  cursor: pointer;
  touch-action: none;
}
.seek .line { display: block; width: 100%; height: 22px; }

.rest {
  stroke: var(--edge);
  stroke-width: 3;
  stroke-linecap: round;
}
.buffered {
  stroke: #33333f;
  stroke-width: 3;
  stroke-linecap: round;
}

/* Clipping and scaling live on separate groups: on one element the scale would
   take the clip with it and the wave would reveal the wrong stretch. */
.amp {
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 260ms cubic-bezier(0.2, 0, 0, 1);
}
.still .amp { transform: scaleY(0.04); }

.squiggle {
  fill: none;
  stroke: var(--cerise);
  stroke-width: 3;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

/* One period is 34 units wide, so travelling exactly that far and starting over
   leaves no seam. Keep this in step with WAVE in player.ts. */
.scroll { animation: travel 1.5s linear infinite; }
.still .scroll { animation-play-state: paused; }
@keyframes travel {
  from { transform: translateX(0); }
  to { transform: translateX(-34px); }
}

/* A wave that travels is motion for its own sake if you have asked for less of
   it; the shape still says which part you have watched. */
@media (prefers-reduced-motion: reduce) {
  .scroll { animation: none; }
}

.thumb { fill: var(--cerise); }

/* ── Sections, for the grouped views ── */
.sortbar { display: flex; gap: 0.4rem; }
.section-head {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  margin: 0.4rem 0 0.7rem;
  padding-bottom: 0.45rem;
  border-bottom: 1px solid var(--edge);
}
.section-head h2 {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}
section.band + section.band { margin-top: 1.4rem; }

/* On a phone the seek bar was the first thing squeezed, which is backwards:
   it is the control you actually aim at. Give it the whole first row and let
   the buttons share the second. */
@media (max-width: 540px) {
  .controls { flex-wrap: wrap; padding-top: 0.55rem; }
  .seek { order: -1; flex: 1 0 100%; }
  .time { margin-right: auto; }
}

.seek:focus-visible { outline: none; }
.seek:focus-visible .thumb { stroke: var(--chalk); stroke-width: 2; }

.time {
  flex: none;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--ash);
  font-variant-numeric: tabular-nums;
}

.vol {
  flex: none;
  width: 62px;
  accent-color: var(--cerise);
  cursor: pointer;
}
/* A phone has volume keys and no pointer to aim a 62px slider with, and iOS
   ignores a page setting volume anyway. Both halves of the query, so a desktop
   that reports its pointer oddly keeps its slider. */
@media (hover: none) and (pointer: coarse) {
  .vol { display: none; }
}

/* Sat over a paused clip, because the first thing anyone wants to do with a
   clip they were sent is start it. */
.big-play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
}
.big-play span {
  display: grid;
  place-items: center;
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: rgb(0 0 0 / 0.55);
  backdrop-filter: blur(2px);
}
.big-play svg { width: 22px; height: 22px; fill: var(--chalk); margin-left: 3px; }

.frame { position: relative; }

/* ── The tube ──
   A shader, not a stack of gradients. Curvature needs the picture resampled
   through a lens, which CSS cannot do: a displacement filter shifts pixels but
   never rebuilds them, so it buys black edges rather than a bulge. The canvas
   sits over the video and the video keeps driving layout, so nothing about the
   box changes when it is switched on. */

.screen { position: relative; display: block; }

.tube-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: none;
  z-index: 2;
}
/* Both gated on the canvas having drawn at least once. Before the video has
   decoded a frame there is nothing to put on the canvas, and swapping to it
   early is how a paused clip becomes a black rectangle. */
.frame.crt.tube-live .tube-canvas { display: block; }

/* Hidden rather than removed: it still decodes, still carries the audio, still
   sets the height of everything. It just stops being the thing you look at. */
.frame.crt.tube-live video { visibility: hidden; }
.frame.crt { background: #000; }

/* ── Changing channel ── */
.frame .static {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  opacity: 0;
  transition: opacity 110ms linear;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.75'/%3E%3C/svg%3E");
}
.frame.switching .static { opacity: 1; animation: fizz 90ms steps(2) infinite; }
@keyframes fizz {
  0% { background-position: 0 0; }
  50% { background-position: 47px 31px; }
  100% { background-position: 91px 73px; }
}

/* The picture tears off the top of the screen and the next one arrives. */
.frame.switching video,
.frame.switching .tube-canvas { transform: translateY(-6%) scaleY(1.03); opacity: 0.45; }
.frame video,
.frame .tube-canvas { transition: transform 220ms ease, opacity 200ms linear; }

/* With the tube off there is nothing to justify static, so it is a plain dip. */
.frame.fading video, .frame.fading .tube-canvas { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .frame.switching .static { animation: none; }
  .frame.switching video, .frame.switching .tube-canvas { transform: none; }
}

/* The toggle wears its own name; there is no icon for a cathode ray tube. */
.ctl.crt-toggle {
  width: auto;
  padding: 0 0.5rem;
  font: 600 0.63rem var(--mono);
  letter-spacing: 0.08em;
  color: var(--faint);
}
.ctl.crt-toggle[aria-pressed="true"] { color: var(--cerise); }

.frame:fullscreen {
  display: flex;
  flex-direction: column;
  justify-content: center;
  max-width: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  background: #000;
}
.frame:fullscreen video { max-height: calc(100svh - 48px); }

.label {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0.75rem 1.25rem;
}

h1 {
  margin: 0;
  font-size: clamp(1.02rem, 0.95rem + 0.4vw, 1.3rem);
  font-weight: 600;
  letter-spacing: -0.015em;
  line-height: 1.25;
}

/* Everything a machine measured, in the order you would ask about it. The
   separator trails its item rather than leading the next one, so a line that
   wraps on a narrow screen does not open with a stray dot. */
.facts {
  margin: 0.3rem 0 0;
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--ash);
}
.facts span { white-space: nowrap; }
.facts span:not(:last-child)::after {
  content: "·";
  margin-left: 0.5rem;
  color: var(--faint);
}

.grow { flex: 1 1 12rem; min-width: 0; }

/* The only thing on the page you can press, so it takes the accent. */
.get {
  flex: none;
  display: inline-flex;
  align-items: center;
  padding: 0.5rem 0.95rem;
  border-radius: 7px;
  background: var(--cerise);
  color: var(--on-cerise);
  font-weight: 600;
  font-size: 0.79rem;
  text-decoration: none;
  transition: background 120ms ease;
}
.get:hover { background: #ffa3c2; }

/* ── Who and what ── */
.credits {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--edge);
}

.art {
  flex: none;
  width: 46px;
  aspect-ratio: 2 / 3;
  border-radius: 6px;
  object-fit: cover;
  background: var(--card);
  border: 1px solid var(--edge);
}
/* Older apps have a tall capsule at a predictable address. Newer ones only have
   a landscape header, and a header centre-cropped into a box shape loses the
   half of it that says which game this is, so it gets its own shape instead. */
.art-wide { width: 82px; aspect-ratio: 16 / 9; }

.art-none {
  display: grid;
  place-items: center;
  font-family: var(--mono);
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  color: var(--faint);
}

.who { display: flex; flex-direction: column; gap: 0.22rem; min-width: 0; }

.game {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.by {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.79rem;
  color: var(--ash);
}

.face {
  flex: none;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  object-fit: cover;
  background: var(--card);
}
.face-none {
  display: grid;
  place-items: center;
  font-family: var(--mono);
  font-size: 0.58rem;
  color: var(--faint);
  border: 1px solid var(--edge);
}

.say {
  margin: 0;
  max-width: 34rem;
  color: var(--ash);
  font-size: 0.92rem;
}

.middle {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.55rem;
  text-align: center;
  padding: 3rem 0;
}
.middle h1 { font-size: 1.15rem; }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* ── Pages that hold more than one clip ──
   The browser, the shuffle and a saved playlist all sit at the top of a tall
   page rather than centred in it, because they scroll. */
body.list {
  justify-content: flex-start;
  gap: 1rem;
}

.topbar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}
.topbar .masthead { flex: 0 0 auto; }
.nav { display: flex; align-items: center; gap: 0.4rem; margin-left: auto; }

.tab {
  padding: 0.42rem 0.8rem;
  border-radius: 7px;
  border: 1px solid var(--edge);
  color: var(--ash);
  font-size: 0.78rem;
  font-weight: 500;
  text-decoration: none;
  transition: background 120ms ease, color 120ms ease;
}
.tab:hover { background: var(--card); color: var(--chalk); }
.tab.on { background: var(--cerise); border-color: transparent; color: var(--on-cerise); font-weight: 600; }

.count {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--faint);
}

/* Cards reflow to fill whatever width they are given, same as the desktop
   library, rather than sitting in a fixed column count. */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
  gap: 0.9rem;
}

.card { text-decoration: none; display: block; }
.card .shot {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: 10px;
  overflow: hidden;
  background: var(--card);
  border: 1px solid var(--edge);
  transition: border-color 120ms ease;
}
.card:hover .shot { border-color: #3d3d49; }
.card .shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card:hover .shot img { opacity: 0.75; }

.pill {
  position: absolute;
  right: 6px;
  bottom: 6px;
  padding: 2px 6px;
  border-radius: 4px;
  background: rgb(11 11 14 / 0.8);
  font-family: var(--mono);
  font-size: 0.62rem;
  color: var(--chalk);
}

.card h2 {
  margin: 0.5rem 0 0.15rem;
  font-size: 0.82rem;
  font-weight: 500;
  color: var(--ash);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.card:hover h2 { color: var(--chalk); }
.card .sub {
  margin: 0;
  font-family: var(--mono);
  font-size: 0.66rem;
  color: var(--faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.more {
  display: block;
  margin: 0.5rem auto 2rem;
  padding: 0.55rem 1.1rem;
  border-radius: 7px;
  border: 1px solid var(--edge);
  color: var(--ash);
  font-size: 0.8rem;
  text-decoration: none;
  width: max-content;
}
.more:hover { background: var(--card); color: var(--chalk); }

/* ── The gate ── */
.gate { display: flex; flex-direction: column; gap: 0.7rem; align-items: center; }
.gate form { display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center; }

input[type="text"],
input[type="password"],
input[type="file"] {
  padding: 0.55rem 0.75rem;
  border-radius: 7px;
  border: 1px solid var(--edge);
  background: var(--card);
  color: var(--chalk);
  font: 400 0.85rem var(--sans);
  min-width: 0;
}
input[type="text"]:focus, input[type="password"]:focus { border-color: var(--cerise); outline: none; }

/* Also worn by links, so it has to cancel what a link brings with it. */
.press {
  display: inline-block;
  text-decoration: none;
  padding: 0.55rem 1.05rem;
  border-radius: 7px;
  border: 0;
  background: var(--cerise);
  color: var(--on-cerise);
  font: 600 0.82rem var(--sans);
  cursor: pointer;
}
.press:hover { background: #ffa3c2; }
.press[disabled] { opacity: 0.45; cursor: default; }

.wrong { color: var(--warn); font-size: 0.82rem; margin: 0; }

/* ── Upload ── */
.drop {
  display: grid;
  place-items: center;
  gap: 0.35rem;
  padding: 2.6rem 1.5rem;
  border: 1px dashed #3a3a46;
  border-radius: 12px;
  background: var(--slate);
  text-align: center;
  cursor: pointer;
  transition: border-color 120ms ease, background 120ms ease;
}
.drop.over { border-color: var(--cerise); background: var(--card); }
.drop strong { font-size: 0.95rem; font-weight: 600; }
.drop span { font-size: 0.8rem; color: var(--faint); }

.form-row { display: flex; flex-direction: column; gap: 0.3rem; }
.form-row label {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--faint);
}
.stack { display: flex; flex-direction: column; gap: 0.85rem; }

.chosen {
  display: flex;
  gap: 0.85rem;
  align-items: flex-start;
}
.chosen img {
  width: 168px;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 8px;
  background: var(--card);
  border: 1px solid var(--edge);
}

.meter {
  height: 4px;
  border-radius: 2px;
  background: var(--edge);
  overflow: hidden;
}
.meter i { display: block; height: 100%; width: 0; background: var(--cerise); }

/* ── Queue ── */
.queue { display: flex; flex-direction: column; gap: 0.3rem; }
.queue a {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  padding: 0.35rem;
  border-radius: 8px;
  text-decoration: none;
  color: var(--ash);
}
.queue a:hover { background: var(--card); color: var(--chalk); }
.queue a[aria-current="true"] { background: var(--card); color: var(--cerise); }
.queue img {
  width: 76px;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: 5px;
  flex: none;
  background: var(--card);
}
.queue .n {
  font-family: var(--mono);
  font-size: 0.66rem;
  color: var(--faint);
  width: 1.6rem;
  flex: none;
  text-align: right;
}
.queue .t {
  font-size: 0.8rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}