/* Ten-foot-ready layout at 1920x1080: large text, a visible focus ring, and
   nothing that depends on a pointer. The same DOM drives a TV remote later
   ([D-43] input model), so every control is reachable by arrows + Enter.

   Cascade discipline, learned the hard way (web-player plan §1.2 item 5): a
   rule for one element lives in ONE place. A second `#vol` block further down
   this file once silently undid the collapsed volume slider, and a phone
   `@media` block that came BEFORE the base rule it meant to override was dead
   at equal specificity. Media queries therefore sit at the END of the file. */
:root {
  color-scheme: dark;
  --bg: #0b0d11;
  --panel: #151922;
  --fg: #e9edf2;
  --dim: #98a2b0;
  --line: #232a35;
  --accent: #3b82f6;
}
* { box-sizing: border-box; }
/* The page's scrollbar is always there, so moving to a page too short to
   scroll does not widen everything by its width and shift the layout. Not in
   the player, which fills the screen and never scrolls; a bar there would sit
   beside the picture. Where scrollbars overlay the page (phones, macOS) this
   changes nothing. */
html { overflow-y: scroll; }
html.in-player { overflow-y: auto; }
body {
  margin: 0; background: var(--bg); color: var(--fg);
  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
.hidden { display: none !important; }
:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; border-radius: 6px; }
/* Tiles show focus on :focus, not only :focus-visible. A D-pad IS the pointer
   on a TV, and `:focus-visible` heuristics do not reliably fire for focus moved
   programmatically — which is exactly how arrow navigation moves it. Without
   this the viewer cannot see where they are, which on a ten-foot screen makes
   the grid unusable. A pointer user who clicks a tile navigates away
   immediately, so the ring costs them nothing. */
.tile:focus { outline: 3px solid var(--accent); outline-offset: 3px; }

/* login */
#login { max-width: 32rem; margin: 16vh auto; padding: 0 1.5rem; }
#login h1 { font-size: 1.7rem; margin: 0 0 .25rem; letter-spacing: .02em; }
#login p { color: var(--dim); margin: 0 0 2rem; }
#login form { display: flex; gap: .5rem; }
#code {
  flex: 1; padding: .75rem 1rem; font-size: 1.15rem; letter-spacing: .18em;
  text-transform: uppercase; background: var(--panel); color: var(--fg);
  border: 1px solid var(--line); border-radius: .5rem;
}
button {
  padding: .7rem 1.2rem; font-size: 1rem; border: 0; border-radius: .5rem;
  background: var(--accent); color: #fff; cursor: pointer;
}
button.ghost { background: var(--panel); color: var(--fg); border: 1px solid var(--line); }
button:disabled { opacity: .5; cursor: default; }
.err { color: #f87171; min-height: 1.5rem; margin-top: .75rem; }
p.ok { color: #8fd19e; margin-top: .75rem; }
/* [D-115] admin */
.admin-tabs { flex-wrap: wrap; }
.badge.bad { background: #3a1f1f; color: #f87171; }
.badge.ok { background: #1f3a2a; color: #8fd19e; }
.admin-banner { margin: 1rem 2rem 0; padding: .75rem 1rem; border: 1px solid #5a4a1f; background: #2a2414; border-radius: .6rem; font-size: .9rem; }
.admin-banner code { font-size: 1.1rem; letter-spacing: .08em; }
#admin-err { margin: .5rem 2rem 0; }
#admin-body .card.wide { grid-column: 1 / -1; }
#admin-body .plist .t { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; overflow-wrap: anywhere; }
#admin-body .plist .actions { margin-top: .4rem; }
/* [D-122] the history's outcome filter. */
#admin-body .history-filter { flex-wrap: wrap; margin: .5rem 0 .75rem; }
.popup-done { padding: 2rem; color: var(--fg); }
html.popup-done body > *:not(.popup-done) { display: none !important; }

/* library */
header.bar {
  display: flex; align-items: center; gap: 1rem;
  padding: 1rem 2rem; border-bottom: 1px solid var(--line);
}
header.bar h1 { font-size: 1.25rem; margin: 0; letter-spacing: .04em; }
header.bar h1 a { color: inherit; text-decoration: none; }
header.bar .spacer { flex: 1; }
/* [D-117] One look per kind of button. A pill is status only, and in the
   header it sits on the bar's centre line: the tile's `margin-top` below does
   not apply here. The page's name beside Back is plain text, not a pill. */
header.bar .badge { margin-top: 0; flex: none; }
.crumb {
  color: var(--dim); font-size: .95rem; min-width: 0; max-width: 18rem;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Navigation buttons: places, not commands. Text with no box; the one on
   screen is lit (`aria-current`), as the phone's tab bar does. The admin
   page's tabs are the same kind. */
.nav { display: flex; gap: .25rem; }
.nav-btn {
  padding: .5rem .85rem; background: none; color: var(--dim); white-space: nowrap;
}
.nav-btn:hover { color: var(--fg); background: #ffffff0d; }
.nav-btn[aria-current="page"] { color: var(--fg); background: var(--panel); }
/* The account button: the signed-in email's first letter in a circle. */
.acct {
  display: flex; align-items: center; justify-content: center; flex: none;
  width: 2.5rem; height: 2.5rem; padding: 0; border-radius: 50%;
  background: #2b3342; color: var(--fg); border: 1px solid var(--line);
  font-weight: 600; font-size: 1.05rem; line-height: 1;
}
.acct:hover, .acct[aria-expanded="true"] { border-color: var(--accent); }
.acct svg { width: 1.35rem; height: 1.35rem; fill: currentColor; }
.row { padding: 1.5rem 2rem 0; }
.row h2 { font-size: 1.05rem; color: var(--dim); font-weight: 600; margin: 0 0 .75rem; }
.row h2 .badge { margin: 0 0 0 .5rem; vertical-align: middle; }
.row h2 .ghost { margin-left: .75rem; padding: .2rem .6rem; font-size: .8rem; vertical-align: middle; }
/* [D-121] Shows and Movies: the heading with its count, and the sort on the
   same line. */
.shelf-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin: 0 0 .75rem; }
.shelf-head h2 { margin: 0; }
.shelf-head .sort { color: var(--dim); font-size: .9rem; white-space: nowrap; }
.shelf-head .sort select { margin-left: .35rem; }
/* [D-121] Home's links that only a phone needs (its tab bar has no room). */
.phone-only { display: none; }
.zone.places { padding-bottom: 0; }
/* Poster density is device-local (Settings → Poster size): one variable, both
   grids. 16:9 stills need wider columns than 2:3 posters. */
:root { --tile-min: 190px; }
.tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--tile-min), 1fr)); gap: 1rem; }
.tiles.wide { grid-template-columns: repeat(auto-fill, minmax(calc(var(--tile-min) * 1.37), 1fr)); }

/* Header search box: one field, two zones behind it. */
.search { display: flex; margin: 0; }
#q {
  width: 14rem; max-width: 40vw; padding: .5rem .75rem; font-size: .95rem;
  background: var(--panel); color: var(--fg); border: 1px solid var(--line); border-radius: .5rem;
}
#q:focus { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The detail band: backdrop behind, poster and text in front, the actions
   the desktop's detail overlay has (Resume / Play / Mark watched). */
.detail { position: relative; overflow: hidden; border-bottom: 1px solid var(--line); }
.detail .backdrop {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  opacity: .28; filter: blur(2px); pointer-events: none;
}
.detail-body {
  position: relative; display: flex; gap: 1.5rem; padding: 1.5rem 2rem;
  background: linear-gradient(90deg, #0b0d11f0 0%, #0b0d11c0 60%, #0b0d1180 100%);
}
.detail .poster { width: 11rem; aspect-ratio: 2/3; object-fit: cover; border-radius: .6rem; flex: none; background: #0f1218; }
.detail-text { display: flex; flex-direction: column; gap: .6rem; min-width: 0; }
.detail-text h2 { margin: 0; font-size: 1.6rem; line-height: 1.2; }
.detail-text h2 .year { color: var(--dim); font-weight: 400; font-size: 1.1rem; }
.detail .meta-line { color: var(--dim); font-size: .95rem; }
.detail .overview { margin: 0; max-width: 60rem; line-height: 1.55; }
.detail .cast { color: var(--dim); font-size: .9rem; }
.detail .actions { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: .4rem; }

/* Search hits, Coming Soon and Watchlist rows: a list, not a grid — these
   carry a badge and actions per row. */
.zone { padding: 1.25rem 2rem 0; }
.zone h2 { font-size: 1.05rem; color: var(--dim); font-weight: 600; margin: 0 0 .6rem; }
.zone .muted { color: var(--dim); font-size: .9rem; }
.hit {
  display: flex; align-items: center; gap: .9rem; padding: .5rem .6rem;
  border: 1px solid var(--line); border-radius: .6rem; background: var(--panel); margin-bottom: .5rem;
}
.hit .hit-art { width: 3rem; aspect-ratio: 2/3; object-fit: cover; border-radius: .3rem; background: #0f1218; flex: none; }
.hit .hit-text { flex: 1; min-width: 0; }
.hit .t { font-size: .95rem; }
.hit .s { font-size: .8rem; color: var(--dim); }
.hit .badge { margin: 0; flex: none; }
.hit .badge.ok { background: #1f3a2a; color: #8fd19e; }
.hit .badge.warn { background: #3a2f1a; color: #f2c46d; }
.hit .hit-actions { display: flex; gap: .4rem; flex: none; }
.hit .hit-actions button { padding: .35rem .7rem; font-size: .85rem; }
.hit .hit-actions button:disabled { opacity: .6; }
.date-head { color: var(--dim); font-size: .85rem; margin: 1rem 0 .4rem; letter-spacing: .04em; text-transform: uppercase; }
.tile {
  background: var(--panel); border: 1px solid var(--line); border-radius: .6rem;
  overflow: hidden; cursor: pointer; text-align: left; padding: 0; color: inherit;
  display: flex; flex-direction: column;
}
.tile .art { aspect-ratio: 2/3; width: 100%; object-fit: cover; background: #0f1218; display: block; }
.tile.wide .art { aspect-ratio: 16/9; }
/* How far through a title you are. Sits between the art and the label so it
   reads as belonging to the image, the way every media UI places it. */
.tile .prog { height: 3px; background: #2a3140; }
.tile .prog i { display: block; height: 100%; background: var(--accent); }
.tile .meta { padding: .6rem .7rem .7rem; }
/* Watched: a check over the art's corner, the desktop's ✓. */
.tile .done {
  position: absolute; top: .4rem; right: .4rem; width: 1.5rem; height: 1.5rem;
  border-radius: 50%; background: #2b8a3ee6; color: #fff; font-size: .9rem;
  display: flex; align-items: center; justify-content: center; pointer-events: none;
}
.tile { position: relative; }
/* A tile's actions ([D-97]): one ⋯ button, shown on hover or focus so the
   grid stays a grid, reachable by keyboard, and never the tile's own click.
   It opens `#tile-menu`, where every action has its words: the two bare
   glyphs this replaced, ✓ and ×, read as "watched" and "close". It stays up
   while its menu is open, wherever the pointer went. */
.tile .acts {
  position: absolute; top: .35rem; left: .35rem; display: flex; gap: .25rem;
  opacity: 0; transition: opacity .15s ease;
}
.tile:hover .acts, .tile:focus-within .acts, .tile:focus .acts, .tile.menu-open .acts { opacity: 1; }
.tile .acts button {
  display: flex; align-items: center; justify-content: center;
  padding: 0; width: 2rem; height: 2rem; border-radius: 50%;
  background: #000000b0; color: #fff; border: 1px solid #ffffff30;
}
.tile .acts button:hover, .tile .acts button[aria-expanded="true"] { background: #000000e0; }
.tile .acts svg { width: 1.2rem; height: 1.2rem; fill: currentColor; }
/* A menu (menu.js: the tile ⋯ menu, the account menu): fixed, placed under
   its button by script (flipped above it when there is no room below), over
   everything in the library. Its items are menu items: full width, no box. */
.popmenu {
  position: fixed; z-index: 8; min-width: 14rem; padding: .3rem;
  background: var(--panel); border: 1px solid var(--line); border-radius: .6rem;
  box-shadow: 0 10px 28px #000b; display: flex; flex-direction: column;
}
.popmenu button {
  display: flex; align-items: center; gap: .65rem; width: 100%; text-align: left;
  padding: .55rem .75rem; font-size: .95rem; border-radius: .4rem;
  background: none; color: var(--fg);
}
.popmenu button:hover { background: #ffffff14; }
.popmenu button:focus-visible { background: #ffffff14; outline: 2px solid var(--accent); outline-offset: -2px; }
.popmenu .sep { border: 0; border-top: 1px solid var(--line); margin: .25rem 0; width: 100%; }
.tile-menu .mi-icon { width: 1.1rem; text-align: center; color: var(--dim); flex: none; }
/* [D-117] The account menu: who is signed in, then the account's pages and
   actions. Before whoami answers there is no one to name, and no rule. */
.acct-menu { min-width: 16rem; max-width: min(22rem, calc(100vw - 1rem)); }
.acct-menu .who { padding: .5rem .75rem .55rem; }
.acct-menu .who .k { font-size: .75rem; color: var(--dim); }
.acct-menu .who .e { font-size: .95rem; overflow-wrap: anywhere; }
.acct-menu .who.warn .k { color: #f2c46d; }
.acct-menu .who.warn .e { font-size: .85rem; color: var(--dim); }
.acct-menu .who.hidden + .sep { display: none; }
.tile .t { font-size: .95rem; line-height: 1.3; }
.tile .s { font-size: .8rem; color: var(--dim); margin-top: .2rem; }
.badge {
  display: inline-block; font-size: .72rem; padding: .1rem .4rem; border-radius: .3rem;
  background: #2b3342; color: var(--dim); margin-top: .35rem;
}
/* The operator's break-glass session, named as such in the header ([D-76]):
   it acts as the gateway's own account, so it is worth a warning colour. */
.badge.warn { background: #3a2f1a; color: #f2c46d; }
/* Not playable: shown (the catalog is the catalog) but visibly not a play
   target, and not focusable as one. */
.tile.unavailable { cursor: default; opacity: .72; }
.tile.unavailable .badge { background: #3a2a2a; color: #e6b8b8; }
/* [D-118] a title on its way in: its badge says so in the accent colour, a
   show's season header counts its episodes in flight, and a movie's page
   says where it stands in a line under its title. */
.tile .badge.dl, .tile.unavailable .badge.dl, .row h2 .badge.dl-roll { background: #1e2f4a; color: #9cc3ff; }
.row h2 .badge.dl-roll { margin-left: .5rem; vertical-align: middle; }
.detail .dl-line { margin: .3rem 0 .5rem; font-weight: 600; color: #9cc3ff; }
.detail .dl-line .muted { font-weight: 400; font-size: .85rem; word-break: break-all; }
/* Big views stay cheap — a long-running show is a thousand episode tiles, a
   library a thousand posters: a tile or list row off screen skips its style,
   layout and paint until it scrolls near, and keeps its measured size once it
   has been drawn (`auto`). The estimates only need to be close: a poster is
   2:3 over a caption, a still 16:9.
   Per TILE rather than per section, for three reasons: one section can be the
   whole library; paint containment on a section would clip the focus ring of
   the tiles along its edges; and the D-pad's geometry (ui.js
   `moveFocusWithin`) reads each tile's own box, which a skipped tile answers
   without laying out its contents.
   Gated on `contain-intrinsic-size: auto`: without it every tile that left
   the screen would fall back to the estimate, and the page would shift under
   a viewer scrolling back up. Those browsers simply lay everything out, as
   before. */
@supports (contain-intrinsic-size: auto 1px) {
  .tile {
    content-visibility: auto;
    contain-intrinsic-size: auto var(--tile-min) auto calc(var(--tile-min) * 1.7 + 4.5rem);
  }
  .tile.wide { contain-intrinsic-size: auto calc(var(--tile-min) * 1.37) auto calc(var(--tile-min) * .9 + 4.5rem); }
  .hit { content-visibility: auto; contain-intrinsic-size: auto 20rem auto 3.5rem; }
}

/* [D-75] a freshness notice, not an error: the page below it is fully usable,
   so it informs rather than alarms — no red, no icon, and it does not push the
   content off the first screen. */
.notice {
  margin: 0 0 12px;
  padding: 8px 12px;
  border-radius: 6px;
  background: #2a2a1f;
  color: #ded7b0;
  font-size: 13px;
}

/* ── feedback (ux-guidelines P1/P2/P7) ──────────────────────────────────── */
/* An indeterminate wait: a spinner AND a phase label, so it is never a mystery
   what is being waited for. `.busy` alone is the library's inline form;
   `.busy.overlay` sits over the player. */
.busy {
  display: flex; align-items: center; gap: .75rem; color: var(--dim);
  padding: 1.5rem 2rem; font-size: .95rem;
}
.busy.overlay {
  position: absolute; inset: 0; justify-content: center; padding: 0;
  color: #fff; font-size: 1.05rem; background: transparent; pointer-events: none;
  text-shadow: 0 1px 3px #000;
}
.spin {
  width: 1.4rem; height: 1.4rem; flex: none; border-radius: 50%;
  border: 3px solid #ffffff33; border-top-color: var(--accent);
  animation: spin .8s linear infinite;
}
.busy.overlay .spin { width: 2.4rem; height: 2.4rem; border-width: 4px; }
@keyframes spin { to { transform: rotate(360deg); } }
/* A wait that failed: what happened, and one click to try again. The player's
   form is a centred card; the library's is inline where the content would be. */
.fail {
  display: flex; flex-direction: column; gap: 1rem; align-items: flex-start;
  background: #151922f0; border: 1px solid var(--line); border-radius: .75rem;
  padding: 1.25rem 1.5rem; max-width: 32rem;
}
.fail p { margin: 0; line-height: 1.45; }
.fail .actions { display: flex; gap: .6rem; }
#player .fail {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(32rem, calc(100vw - 2rem));
}
.fail.inline { margin: 1.5rem 2rem; }
/* A newer flix is being served ([D-89]): one line under the header, with
   Reload. Never a modal, so a remote's ring and a pointer both carry on. */
.update {
  display: flex; align-items: center; flex-wrap: wrap; gap: .5rem 1rem;
  margin: 1rem 2rem 0; padding: .6rem 1rem;
  background: var(--panel); border: 1px solid var(--accent); border-radius: .5rem;
}
.update span { flex: 1; min-width: 12rem; }
/* A transient line for the library (what `alert()` used to do, minus the
   modal): an error that belongs to no view in particular. */
.toast {
  position: fixed; left: 50%; bottom: 1.5rem; transform: translateX(-50%);
  background: #000000d0; border: 1px solid var(--line); border-radius: .5rem;
  padding: .6rem 1rem; font-size: .95rem; max-width: min(40rem, calc(100vw - 2rem));
  text-align: center; z-index: 5;
}

/* player */
#player { position: fixed; inset: 0; background: #000; display: flex; flex-direction: column; }
#player video { flex: 1; width: 100%; height: 100%; background: #000; }
.chrome {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 1rem 2rem 1.5rem;
  background: linear-gradient(transparent, rgba(0,0,0,.85) 45%);
  display: flex; flex-direction: column; gap: .75rem;
}
/* The seek bar: 6 px drawn, a taller invisible hit area (the ::before), and
   `touch-action: none` so a finger can scrub without scrolling the page. */
.scrub {
  position: relative; height: 6px; background: #2a3140; border-radius: 3px; cursor: pointer;
  touch-action: none;
}
.scrub::before { content: ""; position: absolute; inset: -10px 0; }
.scrub .bufs span { position: absolute; top: 0; bottom: 0; background: #46506380; border-radius: 3px; }
.scrub .pos { position: absolute; inset: 0 auto 0 0; background: var(--accent); border-radius: 3px; }
.scrub:hover, .scrub.dragging { height: 8px; margin-top: -1px; }
/* The time under the pointer, and the scrub position while dragging. */
.scrub .bubble {
  position: absolute; bottom: 14px; transform: translateX(-50%);
  background: #000000d0; border: 1px solid var(--line); border-radius: .35rem;
  padding: .15rem .5rem; font-size: .8rem; font-variant-numeric: tabular-nums;
  color: #fff; white-space: nowrap; pointer-events: none;
}

/* Post-play card ([D-46]): bottom-right, above the chrome. */
.upnext {
  position: absolute; right: 2rem; bottom: 7rem; width: min(26rem, calc(100vw - 2rem));
  background: #151922f2; border: 1px solid var(--line); border-radius: .75rem;
  padding: 1rem 1.25rem; display: flex; flex-direction: column; gap: .35rem;
  box-shadow: 0 8px 24px #0009;
}
.upnext .k { font-size: .8rem; color: var(--dim); text-transform: uppercase; letter-spacing: .08em; }
.upnext .t { font-size: 1.1rem; font-weight: 600; }
.upnext .s { color: var(--dim); font-size: .95rem; }
.upnext .line { margin-top: .35rem; font-size: .9rem; color: #f2c46d; min-height: 1.3rem; }
.upnext .actions { display: flex; gap: .6rem; margin-top: .5rem; }
/* ONE row. `flex-wrap: wrap` was how a laptop ended up with a two-line bar:
   the width was there, but four labelled selects and three text buttons could
   not fit a single line, so they folded instead of being reorganised. The
   selects now live behind the gear, which leaves this row short enough to
   never need to wrap. */
.controls { display: flex; align-items: center; gap: .25rem; flex-wrap: nowrap; position: relative; }
.controls .spacer { flex: 1; }
.controls .time {
  font-variant-numeric: tabular-nums; color: #fff; opacity: .85;
  padding: 0 .5rem; white-space: nowrap; flex: none; font-size: .9rem;
}

/* Icon buttons, sized like a video player's rather than a form's. */
.ic {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.5rem; height: 2.5rem; padding: 0; flex: none;
  background: none; border: 0; border-radius: 50%; color: #fff; cursor: pointer;
}
.ic svg { width: 1.5rem; height: 1.5rem; fill: currentColor; display: block; }
.ic:hover { background: #ffffff1f; }
.ic:focus-visible { outline: 2px solid #6ea8ff; outline-offset: -2px; }
.ic[aria-pressed="true"] { color: var(--accent); }

/* Only one of each icon pair shows at a time.
   Selectors are `.ic svg.x` rather than `.x`: `.ic svg` above sets
   `display:block` and out-specifies a bare class, so the plain form left every
   pair showing BOTH glyphs at once. */
.ic svg.i-play { display: none; }
.ic svg.i-pause { display: block; }
#player.paused .ic svg.i-play { display: block; }
#player.paused .ic svg.i-pause { display: none; }

.ic svg.i-enter { display: block; }
.ic svg.i-exit { display: none; }
#player.fs .ic svg.i-enter { display: none; }
#player.fs .ic svg.i-exit { display: block; }

.ic svg.i-vol { display: block; }
.ic svg.i-muted { display: none; }
#mute.is-muted svg.i-vol { display: none; }
#mute.is-muted svg.i-muted { display: block; }

/* The volume slider grows out of the speaker on hover/focus, as YouTube's
   does, so it costs no width at rest. This is the ONLY `#vol` width rule. */
.volwrap { display: flex; align-items: center; flex: none; }
#vol {
  width: 0; opacity: 0; padding: 0; margin: 0;
  transition: width .18s ease, opacity .18s ease, margin .18s ease;
  accent-color: #fff;
}
.volwrap:hover #vol, .volwrap:focus-within #vol { width: 5rem; opacity: 1; margin: 0 .5rem 0 .25rem; }

/* The settings popover. Anchored to the bar's right edge, above it. */
.menu {
  position: absolute; right: 0; bottom: calc(100% + .6rem);
  background: #11151cf2; border: 1px solid var(--line); border-radius: .6rem;
  padding: .75rem; display: flex; flex-direction: column; gap: .6rem;
  min-width: 15rem; max-width: calc(100vw - 2rem); box-shadow: 0 8px 24px #0009;
}
.menu label {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  font-size: .9rem; color: var(--dim);
}
.menu select { min-width: 8rem; max-width: 60vw; }
.menu.hidden { display: none; }
select {
  background: var(--panel); color: var(--fg); border: 1px solid var(--line);
  border-radius: .4rem; padding: .5rem .6rem; font-size: .95rem;
}
/* The title, for the moment after you press play and for a TV across a room
   — the document title is not visible in fullscreen. Fades with the chrome. */
#nowtitle {
  position: absolute; top: 0; left: 0; right: 0; padding: 1.25rem 2rem 2.5rem;
  background: linear-gradient(rgba(0,0,0,.75), transparent);
  font-size: 1.1rem; pointer-events: none;
}

/* Auto-hide ([D-57], [D-90]): chrome and pointer go away three seconds after
   the last input and come back on the next one. Only while PLAYING — a paused
   player that hides its own controls is a player you cannot restart. Opacity
   rather than `display`, so it fades; the script takes focus out of the bar
   as it hides. */
#player.idle .chrome, #player.idle #nowtitle { opacity: 0; }
#player.idle { cursor: none; }
.chrome, #nowtitle { transition: opacity .25s ease; }

/* Subtitles are drawn HERE, not by the browser.
   A browser lays cues along the bottom edge of the video — exactly where the
   control bar is — and `VTTCue.line` does not reliably re-layout a cue that is
   already displayed, so the last line of dialogue ends up behind the Subtitles
   dropdown. Rendering them ourselves (the track stays `hidden`, so cues still
   fire but the browser paints nothing) puts them under our control: they lift
   clear of the chrome when it is up, and the size is ours to set for a screen
   that is across a room rather than on a desk. */
#cues {
  position: absolute; left: 0; right: 0; bottom: 8%;
  display: flex; flex-direction: column; align-items: center; gap: .25rem;
  padding: 0 8%; pointer-events: none; text-align: center;
  transition: bottom .25s ease;
}
#cues span, #cues-top span {
  background: rgba(0, 0, 0, .66);
  padding: .1em .45em; border-radius: .25rem;
  font-size: clamp(1rem, 2.6vw, 2rem); line-height: 1.3;
  text-shadow: 0 1px 2px #000;
  /* A two-line cue keeps its line break. */
  white-space: pre-line;
}
#cues:empty, #cues-top:empty { display: none; }
/* Lines the subtitle track anchors to the TOP of the picture (typeset signs
   from an ASS `\an8`, which the server's converter marks `line:0`) go up
   there instead of joining the dialogue at the bottom; below the title line
   while the chrome is up. */
#cues-top {
  position: absolute; left: 0; right: 0; top: 6%;
  display: flex; flex-direction: column; align-items: center; gap: .25rem;
  padding: 0 8%; pointer-events: none; text-align: center;
  transition: top .25s ease;
}
#player:not(.idle) #cues-top { top: 14%; }
/* Chrome up: lift above it. Chrome hidden: back down to the usual position.
   Settings popover open: lift above THAT too, or the dialogue sits behind the
   Quality dropdown — the exact overlap the custom renderer exists to avoid. */
#player:not(.idle) #cues { bottom: 18%; }
#player.menu-open #cues { bottom: 55%; }

#stats {
  position: absolute; top: 1rem; right: 1rem; max-width: 30rem;
  background: #000000c0; border: 1px solid var(--line); border-radius: .5rem;
  padding: .75rem 1rem; font: 12px/1.5 ui-monospace, monospace; color: #cfe3ff;
  white-space: pre-wrap;
}

/* ── sign-in ([web-auth]) ───────────────────────────────────────────────── */
/* The SSO buttons are anchors, not buttons: they must work as plain links so a
   TV remote's OK key and a half-loaded page both do the right thing. */
.signin { display: flex; flex-direction: column; gap: .6rem; max-width: 22rem; }
.sso {
  display: block; text-align: center; text-decoration: none;
  padding: .7rem 1rem; border-radius: .5rem;
  border: 1px solid var(--line); background: #1b2330; color: inherit;
  font-weight: 600;
}
.sso:hover, .sso:focus-visible { background: #24304250; border-color: #6ea8ff; }
.sso:focus-visible { outline: 2px solid #6ea8ff; outline-offset: 2px; }
#login-alt { margin-top: 1.5rem; max-width: 30rem; color: var(--dim); }
#login-alt summary { cursor: pointer; }
#login-alt label { display: block; margin: .75rem 0 .25rem; font-size: .9rem; }
#login .alt { margin-top: 1rem; font-size: .9rem; }
#pairing { padding: 1rem; }
#pair-waiting { margin-top: 1.5rem; display: flex; flex-direction: column; gap: .5rem; }
#pair-waiting .row-item {
  display: flex; align-items: center; gap: .75rem;
  border: 1px solid var(--line); border-radius: .5rem; padding: .6rem .9rem;
}
#pair-waiting code { font: 600 1.1rem ui-monospace, monospace; letter-spacing: .05em; }

/* The device half of pairing (`/pair`): one code, readable across a room.
   Lives here rather than inline in pair.html so the page can carry the same
   Content-Security-Policy as the rest of the site (no inline styles). */
#pair { text-align: center; padding: 2rem 1rem; }
#pair .where { font-size: 1.4rem; }
#pair .code {
  font: 700 clamp(2.5rem, 12vw, 6rem)/1.2 ui-monospace, monospace;
  letter-spacing: .12em; margin: 1.5rem 0;
}
#pair .dim { color: var(--dim); }

/* A transient explanation over the player — used when the rung changes, since
   an unexplained quality drop and reload reads as a fault. */
#notice {
  position: absolute; top: 1rem; left: 50%; transform: translateX(-50%);
  background: #000000d0; border: 1px solid var(--line); border-radius: .5rem;
  padding: .5rem .9rem; font-size: .95rem; max-width: 80%; text-align: center;
  transition: opacity .3s ease;
}
#notice.hidden { display: none; }

/* ── small screens ──────────────────────────────────────────────────────── */
/* After every base rule they override, at equal specificity — the order IS
   the override. Measured against a 375 px phone: the bar must not overflow,
   and every button must stay on screen. */
/* ---- settings, server, about (Phase 4) ---- */
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); gap: 1.25rem; padding: 1.5rem 2rem; align-items: start; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: .75rem; padding: 1.25rem 1.5rem; }
.card h2 { margin: 0 0 .25rem; font-size: 1.1rem; }
.card h3 { margin: 1.25rem 0 .5rem; font-size: .95rem; color: var(--dim); }
.card .muted { color: var(--dim); font-size: .9rem; margin: 0 0 1rem; }
.card label { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin: .6rem 0; font-size: .95rem; }
.card label input, .card label select {
  width: 11rem; max-width: 50%; padding: .45rem .6rem; font-size: .95rem;
  background: var(--bg); color: var(--fg); border: 1px solid var(--line); border-radius: .4rem;
}
.card .actions { display: flex; align-items: center; gap: .8rem; margin-top: 1rem; flex-wrap: wrap; }
.card .actions .muted { margin: 0; }
.plist .item { padding: .5rem 0; border-top: 1px solid var(--line); font-size: .9rem; }
.plist .item:first-child { border-top: 0; }
.plist .item .muted { margin: 0; font-size: .85rem; }
.plist .path { color: var(--dim); font-size: .8rem; word-break: break-all; }
/* [D-122] "replaced <file>" and an NZB's name are one dotted word: they
   wrap rather than widen the card past a phone's screen. */
.plist .reason { color: #f2c46d; font-size: .85rem; margin-top: .2rem; overflow-wrap: anywhere; }
.plist .badge { margin: 0 0 0 .5rem; vertical-align: middle; }
#whats-new details { margin: .5rem 0; }
#whats-new summary { cursor: pointer; font-weight: 600; }
#whats-new ul { margin: .4rem 0 0; padding-left: 1.2rem; font-size: .9rem; line-height: 1.45; }
#whats-new li { margin: .3rem 0; }
#whats-new li.headline { list-style: none; margin: .8rem 0 .2rem -1.2rem; font-weight: 600; }

/* ── share links ([D-79]) ────────────────────────────────────────────────── */
/* The share panel floats over the page (a dialog, not a layer: Escape and
   Close dismiss it, Back is untouched). The link rows are read-only inputs
   so a remote or a finger can select-and-copy where the clipboard API is
   refused. */
.panel {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(36rem, calc(100vw - 2rem)); max-height: calc(100vh - 2rem); overflow: auto;
  background: var(--panel); border: 1px solid var(--line); border-radius: .75rem;
  padding: 1.25rem 1.5rem; z-index: 7; box-shadow: 0 12px 32px #000a;
  display: flex; flex-direction: column; gap: .75rem;
}
.panel h2 { margin: 0; font-size: 1.15rem; }
.panel .muted { color: var(--dim); font-size: .9rem; margin: 0; }
.panel label { display: flex; align-items: center; gap: .6rem; font-size: .95rem; }
.panel .actions { display: flex; gap: .6rem; }
.linkrow { display: flex; align-items: center; gap: .5rem; }
.linkrow label { flex: none; width: 6.5rem; color: var(--dim); font-size: .9rem; }
.linkrow input {
  flex: 1; min-width: 0; padding: .45rem .6rem; font-size: .85rem;
  background: var(--bg); color: var(--fg); border: 1px solid var(--line); border-radius: .4rem;
  font-family: ui-monospace, monospace;
}
.linkrow button { flex: none; padding: .45rem .8rem; }
/* A viewer on a share link sees one title and the player: the header keeps
   only the name and the badge that says what this is. */
html.share header.bar button, html.share header.bar .search, html.share .tabs { display: none !important; }
html.share #library { padding-bottom: 0; }

/* ── the sign-in a television sees (app.js `#login-tv`) ─────────────────── */
/* The pairing code inline, in the same shape as the standalone /pair page:
   readable across a room, nothing else competing with it. */
#login-tv { margin: 1rem 0 1.5rem; }
#login-tv p { margin: 0 0 .35rem; color: var(--fg); }
#login-tv .where { font-size: 1.4rem; }
#login-tv .code {
  font: 700 clamp(2.5rem, 8vw, 6rem)/1.2 ui-monospace, monospace;
  letter-spacing: .12em; margin: 1rem 0;
}
#login-tv .dim { color: var(--dim); }

/* ── safe areas (phones with a notch or a home bar) ─────────────────────── */
/* `env()` is zero everywhere else, so these cost a desktop nothing. The
   player is the one full-bleed surface; its controls stay inside the insets
   and the picture paints under them. */
.chrome {
  padding-left: calc(2rem + env(safe-area-inset-left));
  padding-right: calc(2rem + env(safe-area-inset-right));
  padding-bottom: calc(1.5rem + env(safe-area-inset-bottom));
}
#nowtitle {
  padding-top: calc(1.25rem + env(safe-area-inset-top));
  padding-left: calc(2rem + env(safe-area-inset-left));
  padding-right: calc(2rem + env(safe-area-inset-right));
}
#notice { top: calc(1rem + env(safe-area-inset-top)); }
#stats { top: calc(1rem + env(safe-area-inset-top)); right: calc(1rem + env(safe-area-inset-right)); }
/* The dynamic viewport: a phone browser's address bar comes and goes, and
   `100vh` is the size WITHOUT it — so a bottom-anchored control bar sat
   behind the bar. `dvh` follows the bar. */
@supports (height: 100dvh) {
  #player { height: 100dvh; }
}
/* A pull at the top of the page on a phone is a pull-to-refresh, which
   reloads the app and loses the view — and in the player it is a disaster.
   Scrolling stays; the gesture that leaves the page does not. */
html.touch { overscroll-behavior-y: contain; }

/* ── the phone's tab bar ────────────────────────────────────────────────── */
/* Hidden until the phone/tablet block below shows it; never on a TV. */
.tabs {
  display: none; position: fixed; left: 0; right: 0; bottom: 0; z-index: 6;
  background: #0b0d11f2; border-top: 1px solid var(--line);
  padding: .3rem .4rem calc(.3rem + env(safe-area-inset-bottom));
  padding-left: calc(.4rem + env(safe-area-inset-left));
  padding-right: calc(.4rem + env(safe-area-inset-right));
  backdrop-filter: blur(8px);
}
.tabs .tab {
  flex: 1; min-height: 2.75rem; padding: .4rem .2rem; border: 0; border-radius: .5rem;
  background: none; color: var(--dim); font-size: .78rem; line-height: 1.15;
}
.tabs .tab[aria-current="page"] { color: var(--fg); background: var(--panel); }

/* ── stats ([D-111]) ───────────────────────────────────────────────────── */
/* One hue for every chart, the accent: magnitude, never a colour to match
   against a legend (breakdowns are labelled rows). Columns are at most 24px
   thick with 4px rounded data ends, square at the baseline, and air between
   them; gridlines are hairlines one step off the panel. Values are text in
   text colours, never the bar colour. */
.stats h3 { font-size: .95rem; color: var(--fg); font-weight: 600; margin: 0 0 .5rem; }
.st-section { margin: 0 0 1.5rem; }
.st-filters { display: flex; flex-wrap: wrap; gap: .5rem 1.25rem; margin: 0 0 1.25rem; }
.st-chips { display: flex; flex-wrap: wrap; gap: .4rem; }
.st-chips:empty { display: none; }
.chip {
  padding: .35rem .85rem; font-size: .85rem; border-radius: 999px;
  background: var(--panel); color: var(--dim); border: 1px solid var(--line);
}
.chip.on { background: #1b2740; color: var(--fg); border-color: var(--accent); }
.st-now { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(26rem, 100%), 1fr)); gap: .75rem; }
.st-card {
  display: flex; gap: .9rem; padding: .75rem; min-width: 0;
  border: 1px solid var(--line); border-radius: .6rem; background: var(--panel);
}
.st-art { width: 3.5rem; aspect-ratio: 2/3; object-fit: cover; border-radius: .3rem; background: #0f1218; flex: none; }
.st-art.none { opacity: 0; }
.st-card-text { flex: 1; min-width: 0; }
.st-card-text .t { font-size: .95rem; overflow-wrap: anywhere; }
.st-card-text .s { font-size: .8rem; color: var(--dim); overflow-wrap: anywhere; }
.st-card-text .s.st-profile { color: var(--fg); }
.st-progress { height: 4px; border-radius: 2px; background: var(--line); margin: .45rem 0; overflow: hidden; }
.st-progress span { display: block; height: 100%; background: var(--accent); border-radius: 2px; }
.st-body { transition: opacity .15s; }
.st-body.loading { opacity: .55; }
.st-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: .75rem; margin: 0 0 1rem; }
.st-tile { padding: .8rem 1rem; border: 1px solid var(--line); border-radius: .6rem; background: var(--panel); }
.st-tile-label { font-size: .8rem; color: var(--dim); }
.st-tile-value { font-size: 1.6rem; font-weight: 600; line-height: 1.25; }
.st-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 1rem; }
.st-grid.three { grid-template-columns: repeat(3, 1fr); }
.viz {
  position: relative; min-width: 0; margin: 0 0 1rem; padding: .9rem 1rem 1rem;
  border: 1px solid var(--line); border-radius: .6rem; background: var(--panel);
}
.viz figcaption { font-size: .9rem; font-weight: 600; margin: 0 0 .7rem; }
.viz-sub { font-size: .8rem; color: var(--dim); margin: -.3rem 0 .6rem; }
.viz .muted { color: var(--dim); font-size: .85rem; margin: 0; }
.viz-plot { position: relative; height: 10rem; margin: 1.1rem 0 0 3.2rem; }
.viz-grid { position: absolute; left: 0; right: 0; border-top: 1px solid var(--line); }
.viz-grid span {
  position: absolute; right: calc(100% + .45rem); top: -.55rem; font-size: .7rem;
  color: var(--dim); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.viz-cols { position: absolute; inset: 0; display: flex; align-items: flex-end; border-bottom: 1px solid #3a4352; }
.viz-col { position: relative; flex: 1; height: 100%; display: flex; align-items: flex-end; justify-content: center; padding: 0 1px; }
.viz-bar { width: 100%; max-width: 24px; background: var(--accent); border-radius: 4px 4px 0 0; }
.viz-col:hover .viz-bar { background: #60a5fa; }
.viz-peak {
  position: absolute; left: 50%; transform: translate(-50%, -.25rem); font-size: .7rem;
  color: var(--fg); white-space: nowrap; pointer-events: none;
}
.viz-x { display: flex; margin: .3rem 0 0 3.2rem; }
.viz-x span { flex: 1; min-width: 0; font-size: .7rem; color: var(--dim); text-align: center; white-space: nowrap; }
.viz-tip {
  position: absolute; transform: translate(-50%, -100%); z-index: 2; pointer-events: none;
  display: flex; gap: .45rem; padding: .3rem .55rem; white-space: nowrap; font-size: .78rem;
  background: #0b0d11; border: 1px solid var(--line); border-radius: .4rem;
}
.viz-tip strong { color: var(--fg); }
.viz-tip span { color: var(--dim); }
.viz-rows { display: grid; gap: .5rem; }
.viz-row { display: grid; grid-template-columns: minmax(4.5rem, 7.5rem) 1fr auto; align-items: center; gap: .6rem; font-size: .85rem; }
.viz-label { color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.viz-track { display: block; height: 10px; }
.viz-fill { display: block; height: 100%; background: var(--accent); border-radius: 0 4px 4px 0; }
.viz-value { color: var(--fg); font-variant-numeric: tabular-nums; }
.st-table-wrap { overflow-x: auto; }
.st-table { width: 100%; border-collapse: collapse; font-size: .85rem; }
.st-table th {
  text-align: left; color: var(--dim); font-weight: 500; padding: .35rem .6rem;
  border-bottom: 1px solid var(--line); white-space: nowrap;
}
.st-table td { padding: .45rem .6rem; border-bottom: 1px solid var(--line); white-space: nowrap; }
.st-table tr:last-child td { border-bottom: 0; }
.st-table .num { font-variant-numeric: tabular-nums; }
.st-time { display: flex; align-items: center; gap: .6rem; min-width: 11rem; }
.st-time .viz-track { flex: 1; }
.st-top { list-style: none; margin: 0; padding: 0; display: grid; gap: .55rem; }
.st-top li { display: flex; gap: .75rem; align-items: center; min-width: 0; }
.st-top .st-art, .st-play-head .st-art { width: 2.5rem; }
.st-history { display: grid; gap: .4rem; margin-bottom: 1rem; }
.st-play { border: 1px solid var(--line); border-radius: .6rem; background: var(--panel); }
.st-play.short { opacity: .62; }
.st-play-head {
  display: flex; align-items: center; gap: .8rem; width: 100%; padding: .5rem .6rem;
  background: none; color: var(--fg); text-align: left; border-radius: .6rem;
}
.st-badges { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: .3rem; flex: none; }
.st-badges .badge { margin: 0; }
.st-badges .badge.ok { background: #1f3a2a; color: #8fd19e; }
.st-detail {
  display: grid; grid-template-columns: max-content 1fr; gap: .3rem 1rem;
  margin: 0; padding: .3rem 1rem .9rem 3.9rem; font-size: .82rem;
}
.st-detail dt { color: var(--dim); }
.st-detail dd { margin: 0; overflow-wrap: anywhere; }

/* ── input that cannot hover ────────────────────────────────────────────── */
/* A finger (or a remote) cannot hover: what hover reveals on a desktop is
   simply visible. Touch targets grow to a fingertip. */
@media (hover: none) {
  .tile .acts { opacity: 1; }
}
@media (pointer: coarse) {
  /* Samsung's browser claims its touchscreen can hover, so `hover: none` alone
     misses it; a coarse pointer is the second signal. */
  .tile .acts { opacity: 1; }
  .tile .acts button { width: 2.5rem; height: 2.5rem; }
  .ic { min-width: 2.75rem; min-height: 2.75rem; }
  header.bar button.ghost, .nav-btn { min-height: 2.6rem; }
  .acct { width: 2.75rem; height: 2.75rem; }
  .popmenu button { min-height: 2.75rem; }
  .hit .hit-actions button { min-height: 2.5rem; }
  .card label input, .card label select { min-height: 2.6rem; }
}
@media (prefers-reduced-motion: reduce) {
  .chrome, #nowtitle, #vol, #cues, #notice, .tile .acts { transition: none; }
}

/* ── phones and tablets ─────────────────────────────────────────────────── */
/* After every base rule they override, at equal specificity — the order IS
   the override. Measured against a 375 px phone: the bar must not overflow,
   and every button must stay on screen. Up to 900 px the navigation buttons
   leave the header for the tab bar, where a thumb expects them, and the
   account button stays at the top right; a television keeps the header
   (`html.tv` rules below win, being more specific). */
@media (max-width: 900px) {
  html:not(.tv) header.bar .wide-only { display: none; }
  html:not(.tv) .tabs { display: flex; }
  html:not(.tv) .phone-only { display: block; }
  html:not(.tv) #library { padding-bottom: calc(3.75rem + env(safe-area-inset-bottom)); }
  html:not(.tv) .toast { bottom: calc(4.25rem + env(safe-area-inset-bottom)); }
  html:not(.tv) header.bar { padding: .75rem 1rem; gap: .6rem; }
  html:not(.tv) .update { margin: .75rem 1rem 0; }
  html:not(.tv) .row { padding: 1rem 1rem 0; }
  html:not(.tv) .zone { padding: 1rem 1rem 0; }
  html:not(.tv) .cards { padding: 1rem; grid-template-columns: 1fr; }
  html:not(.tv) .tiles { grid-template-columns: repeat(auto-fill, minmax(calc(var(--tile-min) * .8), 1fr)); gap: .75rem; }
  html:not(.tv) .tiles.wide { grid-template-columns: repeat(auto-fill, minmax(calc(var(--tile-min) * 1.15), 1fr)); }
}
@media (max-width: 700px) {
  header.bar { flex-wrap: wrap; }
  .card label { flex-direction: column; align-items: stretch; }
  .card label input, .card label select { width: auto; max-width: none; }
  header.bar .search { order: 10; flex-basis: 100%; }
  .update span { flex-basis: 100%; }
  #q { width: 100%; max-width: none; }
  header.bar button.ghost { padding: .45rem .7rem; font-size: .9rem; }
  .tiles { grid-template-columns: repeat(auto-fill, minmax(calc(var(--tile-min) * .74), 1fr)); gap: .75rem; }
  .tiles.wide { grid-template-columns: repeat(auto-fill, minmax(var(--tile-min), 1fr)); }
  .detail-body { flex-direction: column; padding: 1rem; }
  .detail .poster { width: 7rem; }
  .detail-text h2 { font-size: 1.25rem; }
  .chrome {
    padding: .75rem calc(1rem + env(safe-area-inset-right)) calc(1rem + env(safe-area-inset-bottom)) calc(1rem + env(safe-area-inset-left));
  }
  .ic { width: 2.5rem; height: 2.5rem; }
  .controls .time { font-size: .85rem; }
  #nowtitle { padding: calc(1rem + env(safe-area-inset-top)) 1rem 2rem; font-size: 1rem; }
  .hit { flex-wrap: wrap; }
  .hit .hit-actions { flex-basis: 100%; justify-content: flex-end; }
  /* [D-111] Stats in one column; the play detail drops its poster indent. */
  .st-tiles { grid-template-columns: 1fr 1fr; }
  .st-grid, .st-grid.three { grid-template-columns: 1fr; }
  .st-tile-value { font-size: 1.35rem; }
  .st-play-head { flex-wrap: wrap; }
  .st-play-head .st-card-text { flex-basis: calc(100% - 3.5rem); }
  .st-badges { flex-basis: 100%; justify-content: flex-start; padding-left: 3.3rem; }
  .st-detail { grid-template-columns: 1fr; gap: 0; padding: .2rem 1rem .9rem; }
  .st-detail dd { margin-bottom: .45rem; }
  .viz-plot, .viz-x { margin-left: 2.7rem; }
  .viz-x span.alt { visibility: hidden; }
}
@media (max-width: 480px) {
  /* Mute-only: a slider you would have to hover has no meaning on a phone, and
     the width it would take is what pushed the gear, fullscreen and close
     buttons off the right edge. */
  .volwrap #vol { display: none; }
  .ic { width: 2.5rem; height: 2.5rem; min-width: 2.5rem; min-height: 2.5rem; }
  .ic svg { width: 1.35rem; height: 1.35rem; }
  .controls { gap: .15rem; }
  /* Seven fingertip-sized buttons plus the clock do not fit 375 px; the
     picture-in-picture button is the one a phone can spare (Chrome offers
     PiP itself from the media notification and on Home in fullscreen). */
  #pip { display: none; }
  .controls .time { font-size: .8rem; padding: 0 .25rem; }
  .chrome {
    padding: .5rem calc(.75rem + env(safe-area-inset-right)) calc(.75rem + env(safe-area-inset-bottom)) calc(.75rem + env(safe-area-inset-left));
    gap: .5rem;
  }
  #stats { left: .5rem; right: .5rem; max-width: none; font-size: 11px; }
  .busy { padding: 1rem; }
  .fail.inline { margin: 1rem; }
  .upnext { right: .75rem; left: .75rem; width: auto; bottom: 5.5rem; }
  /* Three posters across a phone held upright, two stills — and the
     off-screen size estimates (see `.tile` above) to match. */
  html:not(.tv) .tiles { grid-template-columns: repeat(3, 1fr); gap: .6rem; }
  html:not(.tv) .tiles.wide { grid-template-columns: repeat(2, 1fr); }
  html:not(.tv) .tile { contain-intrinsic-size: auto 30vw auto calc(48vw + 3rem); }
  html:not(.tv) .tile.wide { contain-intrinsic-size: auto 46vw auto calc(26vw + 3.5rem); }
  .tile .meta { padding: .45rem .5rem .55rem; }
  .tile .t { font-size: .85rem; }
  .tile .s { font-size: .72rem; }
  .row h2 { font-size: .95rem; }
  #login { margin: 8vh auto; }
}
@media (max-width: 360px) {
  .controls .time { display: none; }
}
/* A phone on its side: the chrome takes as little height as it can. */
@media (max-height: 480px) and (pointer: coarse) {
  .chrome { padding-top: .25rem; gap: .35rem; }
  #nowtitle { padding-bottom: 1.25rem; font-size: .95rem; }
  .upnext { bottom: 4.5rem; }
  #cues { bottom: 6%; }
  #player:not(.idle) #cues { bottom: 22%; }
}

/* ── television (ten-foot) ──────────────────────────────────────────────── */
/* Stamped on <html> by platform.js. A 1920×1080 TV is the same width as a
   monitor, so nothing here can be a width query: it is the form factor.
   Everything is in rem, so one root size does most of the scaling; the rest
   is overscan (the outer ~3.5% of a TV picture may be off the panel), a
   focus ring you can see from a sofa, and hover replaced by focus. */
html.tv { font-size: 22px; }
@media (min-width: 1700px) { html.tv { font-size: 24px; } }
html.tv body { font-size: 1rem; }
html.tv .tv-hide, html.tv .tabs { display: none !important; }
html.tv :focus { outline: 4px solid var(--accent); outline-offset: 3px; }
html.tv :focus:not(:focus-visible) { outline: 4px solid var(--accent); }
html.tv .tile:focus { outline: 4px solid var(--accent); outline-offset: 4px; transform: scale(1.03); }
html.tv .tile { transition: transform .12s ease; }
/* (`--tile-min` on a TV is set by ui.js's applyDensity — a size up from the
   chosen density — because the inline property it writes wins over any
   rule here.) */
html.tv header.bar { padding: 1.2rem 3.5vw; gap: .8rem; }
html.tv header.bar h1 { font-size: 1.3rem; }
/* The update bar's Reload first, Later last ([D-89]): the ring moves by
   geometry, so Up from the first row reaches what sits over its tiles, and
   Reload at the left edge sits over the first column. At the far end of the
   bar, Up went past it to the search box. The text stays first for a reader. */
html.tv .update { margin: 1rem 3.5vw 0; }
html.tv #update-reload { order: -1; }
html.tv .row { padding: 1.4rem 3.5vw 0; }
html.tv .zone { padding: 1.25rem 3.5vw 0; }
html.tv .cards { padding: 1.5rem 3.5vw; }
html.tv .detail-body { padding: 1.5rem 3.5vw; }
html.tv .tiles { gap: 1.1rem; }
html.tv #rows { padding-bottom: 6vh; }
html.tv #q { width: 12rem; }
html.tv #login { max-width: 46rem; margin: 10vh auto; padding: 0 3.5vw; }
html.tv #login-alt { max-width: 40rem; }
/* The player: the bar and the title clear the overscan band; the controls
   are sized for a remote's focus ring, not a pointer. Volume is the TV's own
   (the set routes it to its speakers), and fullscreen and picture-in-picture
   have no meaning on a screen that is already the whole room. */
html.tv .chrome { padding: 1rem 4vw 4vh; gap: .9rem; }
html.tv #nowtitle { padding: 3vh 4vw 3rem; font-size: 1.2rem; }
html.tv .ic { width: 3rem; height: 3rem; }
html.tv .ic svg { width: 1.7rem; height: 1.7rem; }
html.tv .ic:focus { outline: 3px solid #fff; outline-offset: 0; background: #ffffff2e; }
html.tv .volwrap { display: none; }
html.tv .scrub { height: 8px; }
html.tv .scrub:focus { outline: none; box-shadow: 0 0 0 3px var(--accent); height: 10px; margin-top: -1px; }
html.tv .scrub .bubble { font-size: 1rem; }
html.tv #cues { bottom: 10%; }
html.tv #player:not(.idle) #cues { bottom: 20%; }
html.tv #cues span, html.tv #cues-top span { font-size: clamp(1.4rem, 2.8vw, 2.4rem); }
html.tv .menu { min-width: 20rem; bottom: calc(100% + 1rem); }
html.tv .upnext { right: 4vw; bottom: 9rem; width: min(30rem, 40vw); }
html.tv #stats { font-size: 15px; }
html.tv #notice { font-size: 1.1rem; }
html.tv .fail { max-width: 40rem; }
html.tv .toast { font-size: 1.05rem; }
