:root {
  --bg: #0a0b10;
  --bg-elev: #15161f;
  --bg-card: #1b1d29;
  --fg: #f4f5f8;
  --muted: #9aa0b4;
  --muted-2: #6b7088;
  --accent: #8c5cff;
  --accent-2: #a87bff;
  --accent-glow: rgba(140, 92, 255, 0.45);
  --ok: #51cf66;
  /* "we already hold this file" — see the health-dot block for why it is not
     another green. */
  --cached: #3bc9db;
  --warn: #ffd43b;
  --err: #ff6b6b;
  --card-w: 168px;
  --radius: 12px;
  --shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
}

* { box-sizing: border-box; }
/* The D-pad's "optimal viewing region" (js/tv-nav.js scrolls with
   `block: "nearest"`, which otherwise parks the focused element flush with the
   edge of the viewport — i.e. UNDER the sticky top bar, which is 65-83px tall
   depending on width, and behind the phone tab bar at the bottom). Set on the
   scroll root so it covers EVERY focusable — season tabs, hero buttons and the
   ✓ badge on a row all sat at 0 while the per-class `scroll-margin` further
   down reached four row types and nothing else.
   The bottom value also buys a row of lookahead when walking down a list. */
html { scroll-padding: 100px 0 80px; }
html, body {
  margin: 0;
  background: radial-gradient(1200px 600px at 80% -10%, #1a1530 0%, var(--bg) 55%) fixed;
  color: var(--fg);
  font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}
a { color: inherit; text-decoration: none; }
::-webkit-scrollbar { height: 10px; width: 10px; }
::-webkit-scrollbar-thumb { background: #2a2d3d; border-radius: 6px; }
::-webkit-scrollbar-thumb:hover { background: #3a3e52; }

/* ---- Top bar ---- */
.topbar {
  position: sticky; top: 0; z-index: 50;
  display: flex; align-items: center; gap: 30px;
  padding: 14px 34px;
  background: linear-gradient(180deg, rgba(10,11,16,.95), rgba(10,11,16,.6) 80%, transparent);
  backdrop-filter: blur(10px);
}
.brand {
  font-weight: 800; font-size: 20px; letter-spacing: .2px;
  display: flex; align-items: center; gap: 8px;
  background: linear-gradient(90deg, #fff, var(--accent-2));
  -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
.nav { display: flex; gap: 22px; }
.nav a { color: var(--muted); font-weight: 600; font-size: 15px; padding: 6px 2px; position: relative; transition: color .15s; }
.nav a:hover { color: var(--fg); }
.nav a.active { color: var(--fg); }
.nav a.active::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: -6px; height: 3px;
  background: var(--accent); border-radius: 3px;
}
.topbar .spacer { flex: 1; }
/* Discreet link to the underlying sidecar meta-share dashboard. Pill-styled
   so it reads as a secondary "where this comes from" affordance, not a primary
   nav item. Hidden via [hidden] until /api/config provides a public URL. */
.topbar .nav-ext {
  color: var(--muted); font-weight: 600; font-size: 13px;
  padding: 6px 12px; border: 1px solid rgba(255,255,255,.12);
  border-radius: 999px; white-space: nowrap;
  transition: color .15s, border-color .15s;
}
.topbar .nav-ext:hover { color: var(--fg); border-color: var(--accent); }
.topbar .nav-ext[hidden] { display: none; }
.topbar .nav-search {
  display: flex; align-items: center; gap: 8px; background: var(--bg-elev);
  border: 1px solid #262838; border-radius: 22px; padding: 7px 14px; width: 230px;
  color: var(--muted); cursor: text; transition: border-color .15s, width .2s;
}
/* Widen on focus, but do NOT recolour the border: the form is a focus stop
   (tabindex="0" in index.html) so the shared highlight ring already draws an
   accent outline around the whole pill. Tinting the border as well put a second
   purple line just inside the first — redundant, and it read as a rendering
   artefact rather than as "this box is live". */
.topbar .nav-search:focus-within { width: 280px; }
.topbar .nav-search input { background: none; border: 0; outline: 0; color: var(--fg); width: 100%; font-size: 14px; }
/* Secondary-chrome wrapper. `display: contents` makes its children direct flex
   items of .topbar, so this element is layout-invisible here and the desktop bar
   renders exactly as it did before the wrapper existed. css/mobile.css turns the
   same element into the phone "More" sheet — a CSS relocation, no DOM move (see
   the comment on the element in index.html). */
.topbar-extra { display: contents; }
/* Phone-only search shortcut; the inline .nav-search box covers desktop. */
.topbar-search-btn { display: none; }
/* Preferred-language ("languages I read") chip + popover — ported from
   meta-share's component (same markup/classes), adapted to meta-watch's
   color tokens so it matches the topbar. Click the mini chip to open the
   editable chip popover. */
.lang-pref-mini {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 4px 11px; border-radius: 999px;
  border: 1px solid #262838; background: var(--bg-elev);
  font-size: 13px; color: var(--muted);
  cursor: pointer; user-select: none; position: relative;
}
.lang-pref-mini:hover { border-color: var(--accent); color: var(--fg); }
.lang-pref-mini-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--fg); }
.lang-pref-mini-more { opacity: .6; }
.lang-pref-popover {
  position: absolute; top: 130%; right: 0; min-width: 18rem;
  padding: 10px 14px; background: var(--bg-card);
  border: 1px solid #262838; border-radius: 12px;
  box-shadow: 0 10px 30px rgba(0,0,0,.5);
  display: flex; flex-wrap: wrap; gap: 7px 9px; align-items: center;
  z-index: 50; font-size: 13px; color: var(--fg); cursor: auto;
}
.lang-pref-popover.hidden { display: none; }
.lang-pref-label { font-weight: 600; color: var(--muted); }
.lang-pref-empty { font-style: italic; opacity: .7; }
.lang-pref-divider { opacity: .4; }
.lang-pref-chip {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 10px; border-radius: 999px;
  border: 1px solid #262838; background: var(--bg-elev);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px; cursor: pointer; user-select: none; color: var(--fg);
}
.lang-pref-chip:hover { border-color: var(--accent); }
.lang-pref-chip-active { background: var(--accent); color: #fff; border-color: var(--accent); }
.lang-pref-x { opacity: .8; font-size: .85em; }
.lang-pref-input {
  width: 6.5rem; padding: 3px 10px; border: 1px solid #262838;
  border-radius: 999px; background: transparent; color: inherit;
  font: inherit; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px;
}

.app { padding: 0 0 90px; min-height: 70vh; }
/* Shared horizontal page gutter for the content lists (home rows, search grid,
   their headers). Kept in one place so the rail title, cards, and empty/status
   text all line up at the same inset off the viewport edge. */
/* `--pad-x` is the *deep-page* inset (hero copy, detail hero, episode list),
   historically a hardcoded 50px and therefore a hair tighter than `--gutter`.
   Kept as its own token rather than folded into --gutter so the desktop look is
   unchanged; css/mobile.css shrinks both together. */
:root { --gutter: 56px; --pad-x: 50px; --poster-h: calc(var(--card-w) * 3 / 2); }
.section { padding: 0 var(--gutter); }
.view-status { color: var(--muted); padding: 60px var(--gutter); font-size: 16px; line-height: 1.7; }
.view-status .big { font-size: 20px; color: var(--fg); font-weight: 700; display: block; margin-bottom: 8px; }
/* Library page: a failed "forget" reported without replacing the wall it is
   about (js/library.js). `[hidden]` needs the explicit rule — `display:flex`
   below would otherwise win over the UA's `display:none`. */
.lib-note {
  display: flex; align-items: center; gap: 8px;
  margin: 0 var(--gutter) 14px; padding: 10px 14px; border-radius: var(--radius);
  background: rgba(255,86,86,.10); border: 1px solid rgba(255,86,86,.35);
  color: var(--fg); font-size: 13.5px; line-height: 1.5;
}
.lib-note[hidden] { display: none; }

/* ---- Hero / featured ---- */
.hero {
  position: relative; height: 62vh; min-height: 420px; max-height: 620px;
  display: flex; align-items: flex-end; margin-bottom: 8px;
  background-size: cover; background-position: center 20%;
}
.hero::after {
  content: ""; position: absolute; inset: 0;
  background:
    linear-gradient(0deg, var(--bg) 2%, rgba(10,11,16,.2) 55%, rgba(10,11,16,.55) 100%),
    linear-gradient(90deg, rgba(10,11,16,.92) 0%, rgba(10,11,16,.35) 45%, transparent 75%);
}
.hero-inner { position: relative; z-index: 2; padding: 0 var(--pad-x) 52px; max-width: 760px; }
.hero h1 { font-size: 52px; line-height: 1.02; margin: 0 0 14px; font-weight: 800; text-shadow: 0 2px 20px rgba(0,0,0,.6); }
.hero .chips { margin-bottom: 14px; }
.hero p { color: #d6d9e3; font-size: 16px; line-height: 1.6; max-width: 640px;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.hero .actions { display: flex; gap: 12px; margin-top: 22px; }

/* ---- Buttons ---- */
.btn {
  display: inline-flex; align-items: center; gap: 9px; cursor: pointer;
  border: 0; border-radius: 10px; padding: 13px 26px; font-size: 15px; font-weight: 700;
  transition: transform .12s, background .15s, box-shadow .15s;
}
.btn:active { transform: scale(.97); }
.btn-primary { background: #fff; color: #11121a; }
.btn-primary:hover { background: #e9e9f2; }
.btn-accent { background: var(--accent); color: #fff; box-shadow: 0 6px 24px var(--accent-glow); }
.btn-accent:hover { background: var(--accent-2); }
.btn-ghost { background: rgba(255,255,255,.12); color: #fff; backdrop-filter: blur(4px); }
.btn-ghost:hover { background: rgba(255,255,255,.2); }
.btn-icon { padding: 13px 16px; }

/* ---- Chips ---- */
.chips { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.chip {
  font-size: 12.5px; font-weight: 600; color: var(--muted);
  background: rgba(255,255,255,.07); border: 1px solid rgba(255,255,255,.08);
  padding: 4px 11px; border-radius: 20px;
}
.chip.accent { color: var(--accent-2); border-color: var(--accent-glow); }

/* ---- Rows ---- */
.row { margin: 30px 0; }
.row-head { display: flex; align-items: baseline; gap: 12px; padding: 0 var(--gutter); margin-bottom: 14px; }
.row-head h2 { font-size: 21px; margin: 0; font-weight: 800; letter-spacing: .2px; }
.row-head .count { color: var(--muted-2); font-size: 13px; font-weight: 600; }
.strip-wrap { position: relative; }
.strip {
  display: flex; gap: 16px; overflow-x: auto;
  /* Both gutters are margins so the scroll *viewport itself* is inset. The left
     and right breathing room is then identical at every scroll position — a
     padding-left would be part of the scrollable content and slide away the
     moment you scroll right, gluing the cards to the left edge. */
  margin-inline: var(--gutter);
  /* Generous vertical padding so a focused/hovered card's lift (-6px) + outline
     + glow isn't clipped: overflow-x:auto coerces overflow-y to auto, so the
     rail would otherwise crop the highlight at the top. Keep the .strip-arrow
     `top` calc below in sync with this top value. */
  padding: 16px 0 20px;
  scroll-snap-type: x proximity;
  /* Stremio-style rail: hide the native scrollbar, page via the arrows. */
  scrollbar-width: none; -ms-overflow-style: none;
}
.strip::-webkit-scrollbar { display: none; height: 0; }
.strip .card { scroll-snap-align: start; }

/* Circular pager buttons that straddle the gutter/poster edge (Stremio-like).
   They reveal on rail hover and sit vertically centred on the poster (not the
   card incl. its caption) via --poster-h. A soft scrim behind each keeps the
   glyph legible over bright artwork. */
.strip-arrow {
  position: absolute; z-index: 6;
  top: calc(16px + var(--poster-h) / 2); transform: translateY(-50%);
  width: 48px; height: 48px; border-radius: 50%; padding: 0;
  display: grid; place-items: center; cursor: pointer;
  border: 1px solid rgba(255,255,255,.35);
  background: rgba(12,13,20,.88); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  color: #fff; font-size: 20px; line-height: 1; font-weight: 700;
  box-shadow: 0 8px 24px rgba(0,0,0,.6);
  opacity: 0; transition: opacity .18s ease, transform .14s ease, background .15s, border-color .15s;
}
.strip-arrow-left { left: calc(var(--gutter) - 24px); }
.strip-arrow-right { right: calc(var(--gutter) - 24px); }
.strip-arrow:hover { background: var(--accent); border-color: var(--accent); box-shadow: 0 8px 26px var(--accent-glow); transform: translateY(-50%) scale(1.07); }
.strip-arrow:active { transform: translateY(-50%) scale(.9); }
.strip-arrow { user-select: none; }
.strip-wrap:hover .strip-arrow { opacity: 1; }
.strip-wrap.at-start .strip-arrow-left,
.strip-wrap.at-end .strip-arrow-right,
.strip-wrap.no-overflow .strip-arrow { opacity: 0; pointer-events: none; }
.grid {
  display: grid; gap: 20px; padding: 8px var(--gutter) 20px;
  grid-template-columns: repeat(auto-fill, minmax(var(--card-w), 1fr));
}
.row-empty { color: var(--muted-2); padding: 0 var(--gutter) 8px; font-size: 14px; }

/* ---- Raw sources (second-class search results) ---- */
/* A de-emphasized section under the enriched grid: unenriched but playable video
   hits. Dimmed and set behind a divider so the enriched grid stays the headline. */
.raw-results { margin-top: 8px; }
.raw-results:not(:empty) { border-top: 1px solid var(--muted-2, #6b7088); border-color: color-mix(in srgb, var(--muted-2) 35%, transparent); padding-top: 22px; margin-top: 18px; }
.raw-head { margin-bottom: 4px; }
.raw-head h2 { font-size: 17px; color: var(--muted); font-weight: 700; }
.raw-note { color: var(--muted-2); font-size: 13px; padding: 0 var(--gutter); margin: 0 0 6px; }

/* Compact release LIST — one dense one-line row per raw hit (torrent-tracker /
   Stremio stream-picker style), replacing the posterless poster grid. */
.raw-list { display: flex; flex-direction: column; gap: 3px; padding: 4px var(--gutter) 20px; }
.raw-row {
  display: flex; align-items: center; gap: 10px; min-width: 0;
  padding: 6px 12px; border-radius: 8px; border: 1px solid transparent;
  background: var(--bg-card); color: var(--fg); cursor: pointer;
  font-size: 13px; line-height: 1.3; opacity: .82;
  transition: opacity .12s ease, border-color .12s ease, background .12s ease;
}
.raw-row:hover, .raw-row:focus { opacity: 1; border-color: var(--accent); background: #21232f; outline: none; }
/* availability dot + play affordance */
.raw-row .raw-dot { flex: 0 0 auto; margin: 0; }
.raw-row .raw-play { flex: 0 0 auto; color: var(--accent-2); font-size: 11px; width: 12px; text-align: center; }
.raw-row:hover .raw-play, .raw-row:focus .raw-play { color: var(--accent); }
/* quality chip */
.raw-row .raw-q {
  flex: 0 0 auto; font-weight: 800; font-size: 11px; letter-spacing: .02em;
  color: var(--accent-2); border: 1px solid var(--accent-glow); border-radius: 5px; padding: 2px 6px;
}
/* protocol indicator */
.raw-row .raw-proto {
  flex: 0 0 auto; text-transform: uppercase; font-weight: 700; font-size: 10.5px; letter-spacing: .04em;
  color: var(--muted); border: 1px solid #2a2e40; border-radius: 5px; padding: 2px 6px;
}
.raw-row .raw-proto--torrent { color: #6fd3a0; border-color: color-mix(in srgb, #6fd3a0 45%, transparent); }
.raw-row .raw-proto--nzb     { color: #ffb454; border-color: color-mix(in srgb, #ffb454 45%, transparent); }
.raw-row .raw-proto--ipfs    { color: #66b3ff; border-color: color-mix(in srgb, #66b3ff 45%, transparent); }
/* indexer */
.raw-row .raw-indexer { flex: 0 0 auto; color: var(--muted-2); font-size: 12px; white-space: nowrap; }
/* release-tags slot (chips styled by js/release-tags.js) */
.raw-row .raw-tags { flex: 0 0 auto; display: flex; align-items: center; gap: 4px; }
/* release name — the main label, monospace-ish, ellipsis on overflow */
.raw-row .raw-name {
  flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12.5px; color: var(--muted);
}
.raw-row:hover .raw-name, .raw-row:focus .raw-name { color: var(--fg); }
.raw-row .raw-name .raw-se { color: var(--accent-2); font-weight: 800; margin-right: 6px; }
/* empty inline bits collapse so the row stays tight */
.raw-row .empty { display: none; }
.raw-row.unavailable .raw-dot { background: var(--err); }

/* ---- Cards ---- */
.card { width: var(--card-w); flex: 0 0 auto; display: block; cursor: pointer; }
.grid .card { width: auto; }
.card-poster {
  position: relative; aspect-ratio: 2/3; border-radius: var(--radius); overflow: hidden;
  background: linear-gradient(135deg, #20222f, #15161f); box-shadow: var(--shadow);
  transition: transform .18s ease, box-shadow .18s ease, outline-color .18s; outline: 2px solid transparent;
}
/* The highlight lives on the poster (below), so suppress the browser's default
   outline on the focused card anchor — otherwise it draws a 1px box around the
   whole card (poster + caption) on top of the poster treatment. */
.card:focus { outline: none; }
/* EXACTLY ONE highlight at a time — but now two devices can raise it, and only
   the one that is driving ever does (js/input-mode.js): `:focus` when the remote
   drives, `.mw-hover` when the mouse does. Note it is still NOT keyed off plain
   `:hover`: a parked pointer would light up a second card while the D-pad walks
   elsewhere, which was the original bug. In D-pad mode `#app` is inert to
   hit-testing (see the input-mode block near the ring below), so `:hover` cannot
   fire at all; in mouse mode nothing holds focus, so `:focus` cannot. */
.card:focus .card-poster,
.card.mw-hover .card-poster {
  transform: translateY(-6px) scale(1.035); outline-color: var(--accent);
  box-shadow: 0 14px 40px rgba(0,0,0,.55), 0 0 0 1px var(--accent-glow);
}
.card-poster img { width: 100%; height: 100%; object-fit: cover; display: block; }
.card-poster.noimg { display: flex; align-items: center; justify-content: center; padding: 12px; }
.card-poster.noimg::after { content: attr(data-title); color: var(--muted); font-size: 13px; text-align: center; font-weight: 600; }
.card-badge {
  position: absolute; top: 8px; left: 8px; z-index: 2; font-size: 10.5px; font-weight: 800;
  letter-spacing: .4px; text-transform: uppercase; padding: 3px 7px; border-radius: 6px;
  background: rgba(140,92,255,.92); color: #fff;
}
.card-fade {
  position: absolute; inset: auto 0 0 0; height: 55%; opacity: 0;
  background: linear-gradient(0deg, rgba(0,0,0,.85), transparent); transition: opacity .18s;
  display: flex; align-items: flex-end; padding: 10px;
}
.card:focus .card-fade,
.card.mw-hover .card-fade { opacity: 1; }
.card-fade .play-dot { width: 38px; height: 38px; border-radius: 50%; background: var(--accent); color: #fff;
  display: grid; place-items: center; font-size: 15px; box-shadow: 0 4px 14px var(--accent-glow); }
/* A card with no resolved release used to force the fade open with a pulsing
   "⟳ streams…" pill. Removed: on the card-tier wall that is the resting state of
   nearly every title, so the pill was permanent visual noise with no action
   behind it. Cards without a stream now look like any other and route to the
   detail page, which resolves releases. */
/* "Forget this title" — the Library wall's per-tile control (rendered only when
   the caller passes `opts.onRemove`; see js/card.js). Revealed on whichever
   highlight is live — `.mw-hover` for the mouse, `:focus` for the remote — for
   the reason above: plain `:hover` alone would leave it invisible when the
   remote drives, and `:focus` alone invisible when the mouse does.
   Above the fade (z-index 4) so it stays clickable over the play dot. */
.card-remove {
  position: absolute; top: 8px; right: 8px; z-index: 4;
  width: 26px; height: 26px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 12px; font-weight: 800; line-height: 1;
  color: #fff; background: rgba(8,9,14,.82); border: 1px solid rgba(255,255,255,.22);
  backdrop-filter: blur(4px); cursor: pointer;
  opacity: 0; transform: scale(.82); pointer-events: none;
  transition: opacity .15s ease, transform .15s ease, background .15s ease, border-color .15s ease;
}
.card.mw-hover .card-remove, .card:focus .card-remove, .card:focus-within .card-remove {
  opacity: 1; transform: scale(1); pointer-events: auto;
}
.card-remove:hover { background: var(--err); border-color: transparent; transform: scale(1.08); }
/* A tile that came back: the optimistic removal already happened in the list, so
   this only shows on the rollback path, to point at which tile the error line
   above is about. Purely visual — the tile stays clickable, because the retry is
   the same ✕ and a dead tile would deny it. js/library.js clears the class after
   a few seconds. */
.card.removing { opacity: .5; }
/* Touch has no hover to reveal it with, so it simply stays visible. */
@media (hover: none) {
  .card-remove { opacity: 1; transform: none; pointer-events: auto; }
}
.progress-bar { position: absolute; bottom: 0; left: 0; right: 0; height: 4px; background: rgba(255,255,255,.18); z-index: 3; }
.progress-bar > i { display: block; height: 100%; background: var(--accent); }
.card-meta { padding: 9px 3px 0; }
.card-title { font-size: 13.5px; font-weight: 600; line-height: 1.3;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.card-sub { font-size: 12px; color: var(--muted-2); margin-top: 3px; }

/* ---- Search ---- */
.search-hero { padding: 34px var(--gutter) 6px; }
.search-form { display: flex; gap: 10px; max-width: 680px; }
/* No `outline: none` here. It was killing the browser's default ring, but it
   also out-specified the app's own highlight (`:where(… input …)` further down
   carries no class weight), so the focused search box was left with the 6px
   accent GLOW and no edge — a purple smear instead of a selection. The default
   ring is not a risk: the highlight block overrides `outline` outright on
   :focus, and nothing else focuses this field. */
.search-form input {
  flex: 1; padding: 14px 18px; border-radius: 12px; border: 1px solid #2a2d3d;
  background: var(--bg-elev); color: var(--fg); font-size: 16px;
}
/* No accent border on focus — same reason as `.topbar .nav-search` above: the
   input is a focus stop, so the highlight ring already circles it, and tinting
   the border too drew a second purple line just inside the ring. */
.search-stop.hidden { display: none; }
.search-suggest { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
.search-suggest .chip { cursor: pointer; }
.search-suggest .chip:hover { color: var(--fg); border-color: var(--accent); }

/* ---- Detail ---- */
.detail-hero {
  position: relative; min-height: 56vh; background-size: cover; background-position: center 18%;
  display: flex; align-items: flex-end;
}
.detail-hero::after { content: ""; position: absolute; inset: 0;
  background: linear-gradient(0deg, var(--bg) 3%, rgba(10,11,16,.35) 60%, rgba(10,11,16,.5)),
              linear-gradient(90deg, rgba(10,11,16,.9), transparent 70%); }
.detail-inner { position: relative; z-index: 2; display: flex; gap: 32px; padding: 46px var(--pad-x); align-items: flex-end; }
.detail-poster { width: 210px; flex: 0 0 auto; border-radius: 14px; overflow: hidden; box-shadow: var(--shadow); }
.detail-poster img { width: 100%; display: block; }
.detail-info { flex: 1; max-width: 720px; }
.detail-info h1 { margin: 0 0 12px; font-size: 40px; font-weight: 800; }
.detail-info .chips { margin-bottom: 16px; }
.detail-desc { color: #d6d9e3; line-height: 1.7; font-size: 15.5px; }
.detail-actions { display: flex; gap: 12px; margin-top: 22px; flex-wrap: wrap; }

/* ---- Hero resume line (js/detail/hero.js refreshPlay) ----
   Sits between the synopsis and the buttons: the bar answers "how far in",
   the note answers "how much is left" / "this is the next one". Both are
   hidden outright when there's no position — an empty bar reads as 0%, which
   is a claim, where absence is not. */
.hero-resume { display: flex; align-items: center; gap: 12px; margin-top: 18px; max-width: 420px; }
.hero-resume[hidden] { display: none; } /* `display:flex` overrides the UA hidden rule */
.hero-resume-bar { flex: 1; height: 4px; border-radius: 2px; background: rgba(255,255,255,.18); overflow: hidden; }
.hero-resume-bar > i { display: block; height: 100%; background: var(--accent); }
.hero-resume-bar[hidden] { display: none; }
.hero-resume-note { flex: 0 0 auto; font-size: 13px; font-weight: 600; color: var(--muted-2); }

/* ---- Episodes: season tabs + thumbnailed list ---- */
.episodes { padding: 22px var(--pad-x) 40px; }

.season-tabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px;
  border-bottom: 1px solid #20232f; padding-bottom: 14px; }
/* `display:flex` overrides the UA hidden rule — the inline source view hides the
   strip while it owns the section (detail.js `applySectionMode`). */
.season-tabs[hidden] { display: none; }
.season-tab {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  padding: 9px 16px; border-radius: 10px; border: 1px solid #262838; background: var(--bg-elev);
  color: var(--muted); font-weight: 700; font-size: 14px; transition: all .15s;
}
.season-tab:hover { color: var(--fg); border-color: #3a3e52; }
.season-tab.active { color: #fff; background: var(--accent); border-color: var(--accent); box-shadow: 0 6px 20px var(--accent-glow); }
.season-tab .season-count { font-size: 11px; font-weight: 800; padding: 1px 8px; border-radius: 20px;
  background: rgba(255,255,255,.14); color: inherit; }
/* The Raw tab is second-class (unverified hits) — dashed while idle, solid once
   selected so the active state still reads like any other tab. */
.season-tab-raw { border-style: dashed; }
.season-tab-raw.active { border-style: solid; }

.episode-list { display: flex; flex-direction: column; gap: 10px; }
.episode-list[hidden], .raw-panel[hidden] { display: none; }

/* Raw tab panel: the same compact release rows the search page uses, minus the
   standalone `.raw-results` section's gutter padding — `.episodes` already
   supplies it. */
.raw-panel .row-empty { padding: 0; }
.raw-panel .raw-note { padding: 0; margin: 0 0 12px; }
.raw-panel .raw-reason { padding: 0 0 8px; color: var(--muted); }
.raw-panel .raw-list { padding: 0 0 8px; }
.ep-row {
  display: flex; align-items: center; gap: 16px; width: 100%; text-align: left; cursor: pointer;
  padding: 10px 14px 10px 10px; border-radius: 12px; border: 1px solid #1f2230; background: var(--bg-card);
  color: var(--fg); transition: border-color .15s, background .15s;
}
.ep-row:hover { border-color: var(--accent); background: #21232f; }
.ep-row:hover .ep-cta { background: var(--accent); color: #fff; }
.ep-thumb {
  position: relative; flex: 0 0 auto; width: 150px; aspect-ratio: 16/9; border-radius: 9px;
  overflow: hidden; background: linear-gradient(135deg, #20222f, #15161f);
}
.ep-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ep-body { flex: 1; min-width: 0; }
.ep-name { font-weight: 700; font-size: 15px; margin-bottom: 4px; }
.ep-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 5px; }
.ep-chips[hidden] { display: none; } /* `display:flex` overrides the UA hidden rule */
.ep-chip {
  font-size: 11px; font-weight: 700; line-height: 1; letter-spacing: .02em;
  padding: 3px 7px; border-radius: 6px; border: 1px solid #2a2e40; color: var(--muted-2);
  text-transform: uppercase;
}
.ep-chip-q { color: var(--accent-2); border-color: var(--accent-glow); }
.ep-chip-lang { color: var(--muted); }
.ep-chip-sub { color: var(--muted-2); border-style: dashed; }
.ep-verinfo { font-size: 13px; color: var(--muted-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ep-cta { flex: 0 0 auto; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
  background: rgba(255,255,255,.08); color: var(--accent-2); font-size: 15px; transition: all .15s; }

/* ---- Watch state on an episode row (js/detail/episode-row.js setWatchState) --
   The in-progress bar is the SAME `.progress-bar` the Continue Watching poster
   uses (see the card section above) — one visual language for "where you are",
   wherever it appears.

   A WATCHED row is deliberately not dimmed: `.ep-row.unavailable` below already
   owns opacity+grayscale and means "no reachable source". Watched is the
   opposite of unreachable, so it stays bright and reads only through the tick
   and a slightly recessed title. */
/* The tick is a CONTROL, not only a badge (js/detail/episode-row.js): it is
   present on every row that names an episode coordinate, so "I have seen this"
   can be said without playing anything. Unmarked it is a faint outline that
   firms up on hover/focus; marked it is the solid accent dot it has always
   been, so the read-only appearance of a watched row is unchanged. */
.ep-seen {
  position: absolute; top: 6px; right: 6px; z-index: 3;
  width: 20px; height: 20px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 11px; font-weight: 800; line-height: 1;
  cursor: pointer;
  background: rgba(0,0,0,.45);
  color: rgba(255,255,255,.35);
  border: 1px solid rgba(255,255,255,.35);
  box-shadow: 0 2px 8px rgba(0,0,0,.45);
  opacity: .55;
  transition: opacity .12s ease, color .12s ease, background .12s ease;
}
.ep-row:hover .ep-seen,
.ep-seen:hover,
.ep-seen:focus-visible { opacity: 1; color: #fff; }
.ep-seen:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.ep-seen.marked {
  background: var(--accent); color: #fff; border-color: transparent; opacity: 1;
}
.ep-row.watched .ep-label { color: var(--muted); }
.ep-row.watched .ep-thumb img { opacity: .72; }
.ep-row.watched:hover .ep-thumb img { opacity: 1; }

/* ---- On-device episode rows (#/offline/<id>, js/downloads/series-view.js) ----
   Same row as the detail page, but the whole body is a play button with a
   delete button beside it — so the wrapper drops its padding and the button
   takes over the flex layout. (A <button> cannot contain a <button>: the same
   constraint that shapes the stream drawer's rows.) */
.ep-row-dl { padding: 0; gap: 0; }
.ep-row-dl .ep-play {
  flex: 1 1 auto; min-width: 0; display: flex; align-items: center; gap: 16px;
  padding: 10px 6px 10px 10px; border: 0; background: transparent;
  color: inherit; font: inherit; text-align: left; cursor: pointer;
}
.ep-row-dl .ep-thumb { display: block; }
.ep-row-dl .ep-body { display: block; flex: 1; min-width: 0; }
.ep-row-dl .ep-name { display: flex; align-items: center; gap: 8px; }
.ep-row-dl .ep-del {
  flex: 0 0 auto; align-self: center; margin-right: 10px;
  width: 34px; height: 34px; padding: 0; cursor: pointer;
  border-radius: 9px; border: 1px solid #262a3c; background: #1a1c28;
  color: var(--muted); font-size: 14px; line-height: 1;
  transition: border-color .12s, background .12s, color .12s;
}
.ep-row-dl .ep-del:hover { border-color: var(--err, #ff6b6b); color: var(--err, #ff6b6b); background: #221a20; }
.ep-row-dl .ep-del:disabled { opacity: .5; cursor: default; }
.offline-season {
  margin: 22px 0 12px; font-size: 13px; font-weight: 800; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}
.offline-season:first-child { margin-top: 0; }
.detail-sub { margin-top: 14px; font-size: 13px; color: var(--muted); }

/* ---- Stream side panel (opens on episode click) ---- */
.stream-drawer { position: fixed; inset: 0; z-index: 100; }
.stream-drawer[hidden] { display: none; }
.drawer-backdrop { position: absolute; inset: 0; background: rgba(6,7,11,.55);
  opacity: 0; transition: opacity .22s ease; }
.stream-drawer.open .drawer-backdrop { opacity: 1; }
.drawer-panel {
  position: absolute; top: 0; right: 0; height: 100%; width: min(460px, 92vw);
  background: #111219; border-left: 1px solid #23263a; box-shadow: -12px 0 40px rgba(0,0,0,.5);
  padding: 24px; overflow-y: auto; transform: translateX(100%); transition: transform .22s ease;
  display: flex; flex-direction: column; gap: 6px;
}
.stream-drawer.open .drawer-panel { transform: translateX(0); }
.drawer-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 16px; }
.drawer-kicker { font-size: 11px; font-weight: 800; letter-spacing: .8px; text-transform: uppercase; color: var(--accent-2); }
.drawer-title { margin: 4px 0 0; font-size: 22px; font-weight: 800; }
.drawer-close { flex: 0 0 auto; width: 36px; height: 36px; border-radius: 50%; border: 1px solid #2a2d3d;
  background: var(--bg-elev); color: var(--fg); cursor: pointer; font-size: 14px; }
.drawer-close:hover { background: #21232f; }

/* ---- Inline mode: the series detail page's source VIEW ----
   Same panel, in flow. Clicking an episode hands this section's space over to
   that episode's releases and the season strip / list step aside (detail.js
   `applySectionMode`), so it reads as one browse going a level deeper rather
   than a sheet covering the page it came from. Everything the overlay does to
   pin itself to the right edge has to be undone here — position, size,
   transform, scroll container and chrome — or the panel would still be a 460px
   column, just nailed to the section instead of the viewport. */
.stream-drawer.inline { position: static; inset: auto; z-index: auto; }
.stream-drawer.inline .drawer-panel {
  position: static; width: 100%; height: auto;
  padding: 0; border-left: 0; background: transparent; box-shadow: none;
  overflow: visible; transform: none; transition: none;
  /* The overlay fades in with the backdrop; inline has none, so the switch gets
     its own short fade — enough to read as a transition, not as a redraw. */
  opacity: 0;
}
.stream-drawer.inline.open .drawer-panel { opacity: 1; transition: opacity .16s ease; }
/* Back first, then the title, both pinned left — the overlay's
   `space-between` (title left, ✕ right) would fling the title to the far edge
   once the button takes the first slot. */
.stream-drawer.inline .drawer-head {
  align-items: center; justify-content: flex-start; gap: 16px; margin-bottom: 14px;
}
/* Back sits FIRST (order: -1) and reads as a verb: it is the way out of this
   view, not a corner dismiss. */
.stream-drawer.inline .drawer-close {
  order: -1; width: auto; height: 34px; border-radius: 20px; padding: 0 14px;
  font-size: 12.5px; font-weight: 700; letter-spacing: .01em;
}
.stream-drawer.inline .drawer-close:hover { border-color: var(--accent); color: var(--fg); }
/* Wide rows, not a 460px column: at the section's full width a release name
   fits on one or two lines instead of five. Capped so the play button and its
   chips don't drift a screen apart on a desktop monitor. */
.stream-drawer.inline .drawer-list,
.stream-drawer.inline .drawer-filters { max-width: 1100px; }
/* ---- Persistent mode: a FILM's Versions tab ----
   Same inline panel, but nobody opens or closes it — it is the tab's content,
   the way the episode list is a season tab's (js/detail/stream-drawer.js
   `opts.persistent`). It ships with no head and no Back button, so all that is
   left to say is that it is visible from the start: `.inline` starts the panel
   faded out for the swap animation the film page never performs. */
.versions-panel[hidden] { display: none; }
.stream-drawer.persistent .drawer-panel { opacity: 1; transition: none; }

/* Hold *some* height across the swap so the page doesn't jump when a 40-episode
   list becomes a 2-source one under a smooth scroll. Only a floor, and a modest
   one: a taller reservation just prints empty space under a short source list. */
.episodes.sources-open { min-height: 38vh; }
/* The search row stays while one episode's sources are on screen — a deep link
   lands straight there and starts searching, so the status line, "Stop search",
   "Search again" and the other-names popover are exactly what is wanted. The two
   SEASON verbs do not: they act on the episode list that is no longer visible,
   and `refreshDeepSearchBtn` keeps owning their `hidden` for the list view. */
.episodes.sources-open .season-seen-btn,
.episodes.sources-open .deep-search-btn { display: none; }

/* ---- Quality / language / source / protocol filter chips (all on until the
   reader switches one off; that OFF state is remembered in localStorage).
   Shared by the stream drawer and the detail page's Raw tab — see
   js/detail/facet-filters.js. ---- */
.drawer-filters, .raw-filters { display: flex; flex-direction: column; gap: 10px;
  padding: 0 0 14px; margin-bottom: 12px; border-bottom: 1px solid #23263a; }
.drawer-filters[hidden], .raw-filters[hidden] { display: none; }
/* The bar's head: the one line that survives the collapse. It carries the count
   and, once anything is off, Reset — so a narrowed list always says so and
   always offers the undo, without the chips having to be on screen. */
.filter-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
/* The chips. `hidden` (not a height or an opacity) is what keeps a collapsed
   bar out of js/focus.js's candidate set, and the author `display` below would
   beat the UA [hidden] rule without the second line. */
.filter-body { display: flex; flex-direction: column; gap: 10px; }
.filter-body[hidden] { display: none; }
/* Quiet and text-like: this opens a panel, it does not filter anything, so it
   must not read as a seventh chip. The chevron is a drawn path (same two
   strokes as the stream row's disclosure) and turns over when open. */
.filter-toggle {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
  padding: 4px 8px 4px 5px; border-radius: 20px; border: 1px solid transparent;
  background: none; color: var(--muted-2); font-weight: 800; font-size: 10px;
  letter-spacing: .06em; text-transform: uppercase; transition: color .15s, border-color .15s;
}
.filter-toggle:hover { color: var(--fg); border-color: #2a2e40; }
.filter-toggle svg { transition: transform .15s; }
.filter-toggle.open svg { transform: rotate(180deg); }
.filter-toggle.open { color: var(--fg); }
/* Nothing to expand (every dimension has one value): the word stays, so the
   count keeps its label, but there is no control where none would do anything. */
.filter-toggle-off { padding: 4px 8px 4px 5px; color: var(--muted-2); font-weight: 800;
  font-size: 10px; letter-spacing: .06em; text-transform: uppercase; opacity: .55; }
.filter-group { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; }
.filter-label { font-size: 10px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted-2); margin-right: 4px; }
/* "12 of 85" — how much of the episode's source list survives the chips. Sits
   at the head of the bar as a readout, not a control, so it is deliberately
   quieter than a chip and carries no border or hover. Goes accent when the
   chips are actually hiding something, which is the one state worth noticing. */
.filter-count { font-size: 11px; color: var(--muted-2); margin-right: auto; white-space: nowrap; }
.filter-count-n { font-weight: 800; color: var(--muted); }
.filter-count[data-filtered] .filter-count-n { color: var(--accent, #7aa2f7); }
.filter-chip {
  cursor: pointer; padding: 4px 10px; border-radius: 20px; border: 1px solid #2a2e40;
  background: var(--bg-elev); color: var(--muted-2); font-weight: 700; font-size: 11.5px;
  letter-spacing: .02em; transition: all .15s;
}
.filter-chip:hover { color: var(--fg); border-color: #3a3e52; }
/* OFF chips read muted/struck; ON (active) chips glow with the accent. */
.filter-chip:not(.active) { opacity: .45; text-decoration: line-through; }
.filter-chip.active { color: #fff; background: var(--accent); border-color: var(--accent);
  box-shadow: 0 4px 14px var(--accent-glow); }
/* The escape hatch from a standing OFF choice — only rendered while something
   is switched off, so it reads as "undo this", not as part of the bar. Quiet
   and text-like on purpose: it must not compete with the chips it clears. */
.filter-reset {
  cursor: pointer; padding: 4px 10px; border-radius: 20px; border: 1px dashed #3a3e52;
  background: none; color: var(--muted-2); font-weight: 700; font-size: 11.5px;
  letter-spacing: .02em; transition: all .15s;
}
.filter-reset:hover { color: var(--fg); border-color: var(--fg); }

.drawer-list { display: flex; flex-direction: column; gap: 9px; }
/* The play button carries no chrome of its own — `.stream-row` below is the
   card. Keeping the border here made the disclosure look like a separate
   control floating beside the row rather than part of it. */
.stream-item {
  display: grid; grid-template-columns: 1fr auto auto; grid-template-areas: "main badge go"; column-gap: 12px;
  align-items: center; text-align: left; cursor: pointer; width: 100%;
  padding: 11px 6px 11px 14px; border: 0; border-radius: 0; background: transparent;
  color: var(--fg);
}
/* One line: [dot QUALITY] [chips…]. Wraps only when the chip run is long. */
.stream-main { grid-area: main; min-width: 0; display: flex; flex-wrap: wrap;
  align-items: center; gap: 4px 10px; }
.stream-q { display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto;
  font-weight: 800; font-size: 14px; color: var(--accent-2); }
.stream-q-unk { color: var(--muted); font-weight: 700; }
/* The raw release name ON the collapsed row: the only line that separates two
   releases of the same episode. Small and muted so it reads as a subtitle to the
   quality/chip line above it, but clamped to two wrapped lines rather than a
   single ellipsis — a scene name identifies itself by its tail (…H.264-ToonsHub),
   which is exactly what one-line truncation eats. `flex-basis:100%` drops it
   onto its own line inside `.stream-main`. */
.stream-sub {
  flex: 1 1 100%; min-width: 0; margin-top: 1px;
  font-size: 11.5px; line-height: 1.35; color: var(--muted);
  font-family: var(--mono, ui-monospace, monospace);
  overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
/* The descriptive half of the disclosure panel: the facet legend, then the
   caveat. The separator from the download actions below lives HERE rather than
   on `.stream-detail`, which only some releases have — otherwise a release with
   no caveat got no rule, and one with a caveat got the rule in the middle. */
.stream-facets-block { flex: 1 1 100%; min-width: 0;
  padding-bottom: 9px; margin-bottom: 2px; border-bottom: 1px solid #20233350; }
/* The legend itself: one row per dimension, in the filter bar's order. The
   label borrows the bar's type ramp (copied, not shared — a change to one is
   not automatically right for the other) so the panel reads as an index into
   the bar rather than as a second, unrelated list. */
.stream-facets { display: flex; flex-direction: column; gap: 5px; }
.stream-facets[hidden] { display: none; }
.stream-facet { display: flex; align-items: center; flex-wrap: wrap; gap: 5px; }
.stream-facet-label { flex: 0 0 62px; font-size: 10px; font-weight: 800; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted-2); }
/* Values reuse `.stream-chip` — the row's own badge look — on purpose: the same
   value must be recognisable in both places. They are inert <span>s and get no
   cursor, no hover and no tap target; anything that made them feel pressable
   would undo the point. */
.stream-facet .stream-chip { cursor: default; }
/* Two dimensions that have never had a badge on the row. Quality is the neutral
   one (the row spends its largest element on it already); Match is the only
   facet whose VALUE is worth a colour, because it answers "does this row belong
   on this page at all". */
.stream-chip-qual { color: var(--fg); border-color: #3a3e52; }
.stream-chip-match { color: var(--muted-2); text-transform: none; }
.stream-chip-match[data-tier="confirmed"] { color: var(--ok, #51cf66); border-color: #2a4a33; background: #14201a; }
.stream-chip-match[data-tier="id"] { color: var(--fg); border-color: #3a3e52; }
.stream-chip-match[data-tier="title"] { color: var(--warn, #ffd43b); border-color: #4a3d18; background: #211d10; }
/* The panel's caveat line (a weakly-bound release). Panel-only and full width:
   it is a sentence, not a badge. */
.stream-detail { flex: 1 1 100%; min-width: 0; font-size: 11.5px; line-height: 1.5; color: var(--muted);
  margin-top: 8px; }
.stream-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; min-width: 0; }
.stream-chip { font-size: 10px; font-weight: 700; line-height: 1; letter-spacing: .02em;
  padding: 3px 6px; border-radius: 6px; border: 1px solid #2a2e40; color: var(--muted-2); text-transform: uppercase; }
.stream-chip-lang { color: var(--accent-2); border-color: var(--accent-glow); }
.stream-chip-sub { color: var(--muted); }
/* Protocol reads as provenance, not as a language — filled rather than
   outlined so the eye separates "how it arrives" from "what it sounds like". */
.stream-chip-proto { color: var(--muted-2); background: #1e2130; border-color: #2f3348; }
/* WHERE it came from, beside how it arrives. Outlined and tinted apart from the
   filled protocol chip so a row reads as "[NZB] [nzb.life]" — two different
   questions — rather than as one two-word badge. */
.stream-chip-src { color: #c9a227; border-color: #4a3d18; background: #211d10; text-transform: none; }
/* The source under the playhead, in the watch view's switcher (see
   js/player/sources.js). Accent-filled: it is a *state*, not a facet, and the
   reader is scanning the list for exactly this row. */
.stream-chip-now { color: #fff; background: var(--accent); border-color: var(--accent); }
/* Where the confirmed releases end. A label rather than a bare rule, because an
   unexplained divider in a list of near-identical rows reads as a rendering
   glitch. */
.stream-sep {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 10px 2px 4px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--muted-2);
}
/* Where the append-only tail begins (drawer `update()`): releases that landed
   while the panel was open. Accent-tinted — it is the one divider that marks
   something that just happened rather than a standing property of the list. */
.stream-sep.arrivals { color: var(--accent-2); }
.stream-sep.arrivals::after { background: var(--accent-glow); }
.stream-sep::after {
  content: "";
  flex: 1;
  height: 1px;
  background: #2f3348;
}

/* ---------------------------------------------------------------- offline downloads
   A stream row is a WRAPPER around the play button, because the play button is
   a <button> and cannot contain the disclosure <button>. Grid so the collapsible
   panel can span the full width underneath both.
   See js/downloads/row.js + docs/offline-downloads.md. */
/* The WRAPPER is the card: border, background, hover and the rounded corners
   all live here, and the play button + disclosure sit inside it borderless.
   Previously each of the three drew its own box, so an expanded row read as
   three unrelated cards stacked up — the panel in particular looked like
   another stream. `overflow: hidden` lets the children square themselves off
   against the wrapper's radius with no per-corner bookkeeping. */
.stream-row {
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  border-radius: 11px; border: 1px solid #23263a; background: #15161f; overflow: hidden;
  transition: border-color .12s, background .12s;
}
.stream-row:hover { border-color: var(--accent); background: #1c1e2a; }
.stream-row.open { border-color: var(--accent); background: #171925; }
/* Playing now (watch-view switcher): marked, but inert — picking it again would
   rebuild the pipeline on identical bytes. The row keeps its offline control,
   so it must not be blanket-disabled here. */
.stream-row.playing { border-color: var(--accent); background: #191c2b; }
.stream-row.playing:hover { background: #191c2b; }
.stream-row.playing .stream-item { cursor: default; }
.stream-row.playing .stream-go { color: var(--accent); }
.stream-row .stream-item { grid-column: 1; }
/* Compact square, vertically centred — NOT stretched to the row height, which
   made it read as a scrollbar pinned to the drawer edge rather than as a
   control belonging to the row. */
.stream-disclosure {
  grid-column: 2; align-self: center; margin-right: 8px;
  display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0; cursor: pointer;
  border-radius: 8px; border: 1px solid #262a3c; background: #1a1c28;
  color: var(--muted-2); font-size: 14px; line-height: 1;
  transition: border-color .12s, background .12s, color .12s, transform .12s;
}
.stream-disclosure:hover { border-color: var(--accent); background: #1e2130; color: var(--fg); }
.stream-disclosure.open { transform: rotate(180deg); color: var(--accent-2); }
/* Full width inside the card, separated by a rule rather than a second border,
   so it reads as the row opening up rather than as a new element appearing. */
.stream-extra {
  grid-column: 1 / -1;
  display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
  padding: 11px 14px 13px; border-top: 1px solid #23263a; background: #10111a;
}
/* An author `display` beats the UA's `[hidden] { display: none }`, so without
   this every COLLAPSED row still drew an empty 24px bordered strip under it —
   the single biggest source of visual noise in the drawer. */
.stream-extra[hidden] { display: none; }
.stream-extra .dl-line, .stream-extra .stream-facets-block { flex: 1 1 100%; }
/* Status glyph. Lives INSIDE the play button (it is inert text, so it nests
   legally) and stays visible when the panel is collapsed — the collapsible
   hides actions, never state. */
.stream-dl-badge { grid-area: badge; font-size: 11px; font-weight: 800; letter-spacing: .02em;
  padding: 3px 7px; border-radius: 999px; border: 1px solid #2a2e40; white-space: nowrap; }
.stream-dl-badge.done { color: var(--ok, #51cf66); border-color: color-mix(in srgb, var(--ok, #51cf66) 45%, transparent); }
.stream-dl-badge.busy { color: var(--accent-2); border-color: var(--accent-glow); }

.dl-line { font-size: 12px; color: var(--muted-2); }
.dl-line.ok { color: var(--ok, #51cf66); font-weight: 700; }
.dl-line.warn { color: var(--warn, #ffd43b); }
/* Consequence of the action above it — deliberately quieter than the warning,
   and it sits BELOW the buttons so it explains rather than gates. */
.dl-line.hint { font-size: 11px; color: var(--muted); opacity: .85; }
.dl-bar { width: 100%; height: 6px; border-radius: 999px; background: #23263a; overflow: hidden; }
.dl-bar-fill { height: 100%; background: var(--accent); border-radius: 999px; transition: width .3s ease; }
.dl-btn {
  font: inherit; font-size: 12px; font-weight: 700; cursor: pointer;
  padding: 7px 12px; border-radius: 9px; border: 1px solid #2a2e40;
  background: #1a1c28; color: var(--fg); transition: border-color .12s, background .12s, opacity .12s;
}
.dl-btn:hover { border-color: var(--accent); background: #22243250; }
.dl-btn:disabled { opacity: .5; cursor: default; }
.dl-btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.dl-btn.danger:hover { border-color: var(--err, #ff6b6b); color: var(--err, #ff6b6b); }
.stream-go { grid-area: go; width: 38px; height: 38px; border-radius: 50%; display: grid; place-items: center;
  background: var(--accent); color: #fff; font-size: 14px; box-shadow: 0 4px 14px var(--accent-glow); }

/* ---- Player ---- */
/* IMMERSIVE. The watch view covers the viewport: no top bar, no tab bar, no
 * page scroll, square corners, video edge to edge. `body.mw-watching` is set by
 * enterImmersive() in js/player/player-view.js and removed by stopPlayer().
 *
 * This replaced a `max-width: (measured height) × 16/9` calc that derived the
 * stage's width from whatever the top bar and status row left over. Every term
 * of it is gone — there is no chrome above the video any more, so there is
 * nothing to measure and nothing to subtract. Do not reintroduce a page-level
 * element here expecting room to be made for it: the player's chrome all lives
 * inside .video-container (index.html says why).
 */
body.mw-watching { overflow: hidden; }
/* Not `visibility`/`opacity`: these are also js/tv-nav.js's spatial-navigation
 * candidates, and it rejects zero-rect elements — so `display: none` is what
 * actually keeps a remote from arrowing up into an invisible top bar. */
body.mw-watching .topbar,
body.mw-watching .tabbar,
body.mw-watching .mw-sheet-backdrop { display: none !important; }

#mw-player-host {
  position: fixed;
  inset: 0;
  z-index: 40;          /* over #app, under the identity modal (200) */
  background: #000;
}
.player-wrap { width: 100%; height: 100%; }
.status-badge { font-size: 12.5px; color: var(--ok); font-weight: 700; display: inline-flex; align-items: center; gap: 6px; }
.status-badge::before { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; flex: 0 0 auto; }
.status-badge.pulse::before { animation: pulse 1.1s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
.spacer { flex: 1; }
/* Live download-rate chip. Neutral while the needed rate is still unknown;
 * green/red once we can compare achievable throughput against the bitrate.
 * It sits over the picture now, so the pill is translucent rather than solid. */
.speed-chip { font-size: 11.5px; font-weight: 700; color: var(--muted);
  display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px;
  border-radius: 999px; background: rgba(12,14,20,.72); border: 1px solid rgba(255,255,255,.2);
  white-space: nowrap; }
.speed-chip.ok { color: #7ee08d; border-color: rgba(81,207,102,.45); }
.speed-chip.bad { color: #ff8b8b; border-color: rgba(255,107,107,.5); }

/* ---- Player overlays (all inside .video-container — see index.html) ---- */
/* "Your connection is below the title's bitrate". Bottom-centre, just above the
   control bar: a toast over the picture rather than a banner above it. It fades
   with the rest of the chrome (.is-idle, css/player-ui.css). */
.net-warn {
  position: absolute; z-index: 4;
  left: 50%; bottom: 5.2rem; transform: translateX(-50%);
  width: min(660px, calc(100% - 3rem));
  padding: 9px 13px; border-radius: 10px;
  background: rgba(42,36,22,.86); border: 1px solid #5a4a2a; color: #ffd98a;
  font-size: 12.5px; line-height: 1.5; text-align: center;
  backdrop-filter: blur(8px); pointer-events: none;
}
/* Usenet materialisation buffering bar (shown during the gateway full-download
 * window for nzb sources). Centred under the loading spinner. */
.mw-buffer {
  position: absolute; z-index: 4;
  left: 50%; top: 50%; transform: translateX(-50%);
  margin-top: 46px;                 /* clears the 56px spinner, centred at 50% */
  width: min(420px, calc(100% - 3rem));
  text-align: center; pointer-events: none;
}
.mw-buffer-track { height: 7px; border-radius: 999px; background: rgba(255,255,255,.14); overflow: hidden; border: 1px solid rgba(255,255,255,.16); }
.mw-buffer-fill { height: 100%; width: 0; border-radius: 999px; background: var(--accent, #4c8dff);
  transition: width .4s ease; }
/* Repairing/assembling: download done, tail is indeterminate → animated stripes. */
.mw-buffer-fill.indeterminate { background-image: linear-gradient(110deg,
  rgba(255,255,255,.18) 25%, transparent 25%, transparent 50%,
  rgba(255,255,255,.18) 50%, rgba(255,255,255,.18) 75%, transparent 75%);
  background-size: 22px 22px; animation: mw-buffer-stripes .8s linear infinite; }
@keyframes mw-buffer-stripes { to { background-position: 22px 0; } }
.mw-buffer-label { margin-top: 6px; font-size: 12.5px; color: #e7ecf5; font-weight: 600;
  text-shadow: 0 1px 3px rgba(0,0,0,.9); }
/* The stage fills the host; .video-container inside it is the fullscreen
 * element and owns every overlay. */
.player-stage { position: relative; width: 100%; height: 100%; }
@keyframes spin { to { transform: rotate(360deg); } }

/* The failure offer (js/player/player-view.js `paintFailureOffer`): what went
   wrong, every source already tried this attempt, and the ways out. Centred
   over the black stage — it is inside .video-container, so unlike the old
   below-the-video box it is fully usable in fullscreen. z-index 16 clears the
   click shield (whose whole job is swallowing clicks), the control bar (10) and
   the settings menu (15) — the ⓘ panel (20) is the one thing above it, since
   its detail is exactly what a failure is a reason to go read. */
.unsupported {
  position: absolute; z-index: 16;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(620px, calc(100% - 3rem)); max-height: min(80%, 560px); overflow: auto;
  padding: 18px 20px; border-radius: 14px;
  background: rgba(42,20,22,.92); border: 1px solid #5a2a2e; color: #ffb3b8;
  backdrop-filter: blur(10px); box-shadow: 0 18px 60px rgba(0,0,0,.6);
}
.unsupported__msg { font-weight: 600; }
.unsupported__log { margin-top: 10px; font-size: 12px; line-height: 1.5; opacity: .72; }
.unsupported__actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; }
/* .btn's own padding is sized for hero CTAs and is far too heavy in here. */
.unsupported .btn { padding: 8px 16px; font-size: 13px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---- The now-playing text column (inside .now-playing-overlay) ---- */
/* Release bits: episode · year · quality · audio · subs · origin. */
.player-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 0;
  margin: 0; font-size: 12.5px; color: rgba(255,255,255,.88); }
/* [hidden] has to actually hide: the author `display:flex`/`inline-flex` above
   outranks the UA stylesheet's `[hidden] { display: none }`, which would leave
   an empty box — and so a phantom row gap — in the overlay's column. Same for
   the two clamped blocks below, whose `display` is also author-set. */
.player-meta[hidden], .speed-chip[hidden],
.player-synopsis[hidden], .player-filename[hidden] { display: none; }
.player-meta-bit { white-space: nowrap; }
.player-meta-bit:not(:first-child)::before { content: "·"; margin: 0 8px; opacity: .5; }
/* Two lines and no more: the synopsis is context while the title loads, not a
   thing to read over the picture. The full text is in the ⓘ panel. */
/* Brighter than a page-body grey would be: this is read over the picture, and
   the muted tokens are calibrated against an opaque background. */
.player-synopsis {
  margin: 0; font-size: 13px; line-height: 1.5; color: rgba(255,255,255,.86);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
}
/* Lives in the ⓘ panel now, where the whole 120-character release name can
   wrap. `break-all` rather than `break-word`: these have no spaces, so the only
   place to break one is mid-token. */
.player-filename {
  margin: 6px 16px 16px; font-size: 12px; color: #d6dde9;
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  line-height: 1.55; word-break: break-all;
}

/* ---- The ⓘ panel's contents ---- */
.player-tech-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 28px;
  padding: 4px 16px 16px; }
.player-tech-dl { display: grid; grid-template-columns: auto 1fr; gap: 5px 14px; margin: 0; align-content: start; }
/* Both are read over MOVING VIDEO, not over a page — the ⓘ panel is a thin
   scrim (css/player-ui.css). Anything as dim as --muted disappears the moment
   a bright shot passes under it, so what makes the label subordinate here is
   its SIZE and its uppercasing, not a low opacity. Measured against a pure
   white frame, which is the case that breaks every softer choice. */
.player-tech-dl dt { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: rgba(255,255,255,.9); }
.player-tech-dl dd { margin: 0; font-size: 12.5px; color: #fff; }
.player-tech-dl dd.mw-trackers { font-size: 11.5px; line-height: 1.5; word-break: break-all; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
/* Time-to-first-frame stopwatch: tabular figures so the 10 Hz digits don't
   jitter the row; green once the frame actually landed. */
.player-tech-dl dd.mw-ttmvp { font-variant-numeric: tabular-nums; }
.player-tech-dl dd.mw-ttmvp.done { color: #58d68d; font-weight: 600; }
/* The on-device buffer rows are about the *device*, not about this stream, so
   they span the full grid as their own strip under the media/network columns
   instead of flowing into the left column — where their (longer) labels would
   push the value column out of line with the stream rows directly above them.
   `:empty` keeps the divider from drawing before the first read lands. */
#mw-tech-device { grid-column: 1 / -1; margin-top: 4px; padding-top: 11px; border-top: 1px solid rgba(255,255,255,.12); }
#mw-tech-device:empty { display: none; }
/* The one action inside the tech panel (clear the on-device playback buffer).
   .btn's 13px/26px is sized for hero CTAs and would tower over a 12.5px <dd>,
   so it is scaled down to sit on the same line as the readout it belongs to. */
.player-tech-dl dd .mw-cache-clear { padding: 5px 11px; font-size: 11.5px; font-weight: 600; border-radius: 7px; }
.player-tech-dl dd .mw-cache-clear[disabled] { opacity: .55; cursor: default; }
/* Build stamp in the panel (js/player/player-view.js::buildRow). Amber when the
   server has moved on from the build this tab is running — same signal as the
   corner badge, which is hidden while a film is open. */
.player-tech-dl dd .mw-build-copy--stale { color: var(--warn); border-color: rgba(255, 212, 59, .45); }
.player-tech-json-head { padding: 0 16px; font-size: 11px; text-transform: uppercase;
  letter-spacing: .04em; color: rgba(255,255,255,.9); }
.player-tech-json { margin: 6px 16px 16px; padding: 12px 14px; border-radius: 9px;
  background: rgba(0,0,0,.55); border: 1px solid rgba(255,255,255,.14); color: #d6dde9;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; line-height: 1.5;
  /* No max-height/own scroller any more: the ⓘ panel body is the one scroll
     container, and a nested one here meant a wheel over the JSON scrolled the
     blob while the panel stood still. `overflow-x` alone keeps a long single
     line (a magnet URI) from widening the sheet. */
  overflow-x: auto; white-space: pre; -moz-tab-size: 2; tab-size: 2; }
@media (max-width: 720px) { .player-tech-grid { grid-template-columns: 1fr; } }

/* ---- Skeletons ---- */
.skel-poster { aspect-ratio: 2/3; border-radius: var(--radius);
  background: linear-gradient(100deg, #15161f 30%, #20222f 50%, #15161f 70%);
  background-size: 200% 100%; animation: shimmer 1.4s infinite; }
@keyframes shimmer { to { background-position: -200% 0; } }

/* The former ad-hoc `@media (max-width: 720px)` block lived here. It has been
   folded into the single responsive system in css/mobile.css (phone ≤640,
   tablet 641–1024) so there is one breakpoint scheme, not two competing ones. */

/* --- Availability filter (probe.js) ----------------------------------------
   Direct-play cards + stream variants whose bytes the meta-share peer can't
   currently fetch are marked `.unavailable`: greyed and de-saturated as a hint,
   but still clickable — the probe is often a transient miss, so the user is left
   free to try anyway. The poster grows a small "Unavailable" tag. `.probing` is
   set while the check is in flight (unstyled — most cards resolve available, so
   a flash would be noise). */
.card.unavailable,
.stream-item.unavailable,
.ep-row.unavailable {
  opacity: 0.42;
  filter: grayscale(1);
  cursor: pointer;
}
.card.unavailable .card-poster { position: relative; }
.card.unavailable .card-poster::before {
  content: "Unavailable";
  position: absolute;
  inset: auto 0 0 0;
  z-index: 2;
  padding: 4px 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-align: center;
  color: var(--fg);
  background: rgba(255, 107, 107, 0.82); /* --err, translucent */
}

/* --- Release health dot (probe.js grade) -----------------------------------
   A small traffic-light dot on each episode row (rolled up across its releases)
   and each stream variant: cyan = the server already holds the whole file (no
   availability question at all), green = bytes on hand / fast, orange =
   reachable but slow or uncertain, red = the peer can't serve it right now,
   neutral = the probe is still in flight. Driven by `data-health`, updated in
   place once the (cached) 1-byte probe lands. Transient — never persisted.

   Cyan rather than a second green: the cached tier is a different *kind* of
   answer from the traffic light (it is not a measurement of reachability), and
   two greens a shade apart would read as a rendering artefact rather than as a
   distinction — including for the ~6% of viewers with a red/green deficiency,
   for whom green-vs-green is the one pair that carries no information at all.
   Cyan keeps its distance from --ok on both axes (hue and lightness) and is not
   already spoken for in this palette (--accent is violet). */
.health-dot {
  display: inline-block;
  width: 9px; height: 9px;
  border-radius: 50%;
  margin-right: 7px;
  vertical-align: middle;
  background: #3a3e4f; /* unknown / in-flight */
  transition: background .2s ease, box-shadow .2s ease;
}
.health-dot[data-health="cached"]  { background: var(--cached); box-shadow: 0 0 7px rgba(59,201,219,.6); }
.health-dot[data-health="green"]   { background: var(--ok);  box-shadow: 0 0 7px rgba(81,207,102,.6); }
.health-dot[data-health="orange"]  { background: #ff922b;    box-shadow: 0 0 7px rgba(255,146,43,.5); }
.health-dot[data-health="red"]     { background: var(--err); box-shadow: 0 0 7px rgba(255,107,107,.5); }
.health-dot[data-health="unknown"] { background: #3a3e4f;    box-shadow: none; }

/* ---- In-flight query indicator (topbar) ---- */
.qx-indicator {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 4px 11px; border-radius: 999px;
  border: 1px solid #262838; background: var(--bg-elev);
  color: var(--muted); font-size: 13px; cursor: pointer;
  position: relative; transition: border-color .15s, color .15s;
}
.qx-indicator[hidden] { display: none; }
.qx-indicator:hover { border-color: var(--accent); color: var(--fg); }
.qx-indicator.active { color: var(--fg); border-color: var(--accent); }
.qx-spin {
  width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid rgba(255,255,255,.18); border-top-color: var(--accent);
  animation: qx-spin .8s linear infinite;
}
@keyframes qx-spin { to { transform: rotate(360deg); } }
.qx-count { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-weight: 600; }

.qx-popover {
  position: absolute; top: 56px; right: 34px; z-index: 60;
  min-width: 18rem; max-width: 26rem;
  padding: 8px; background: var(--bg-card);
  border: 1px solid #262838; border-radius: 12px;
  box-shadow: 0 12px 32px rgba(0,0,0,.55);
  font-size: 13px; color: var(--fg);
  display: flex; flex-direction: column; gap: 4px;
}
.qx-popover.hidden { display: none; }
.qx-empty { padding: 10px 12px; color: var(--muted); font-style: italic; }
.qx-item {
  display: grid; grid-template-columns: 14px 1fr auto auto; gap: 8px; align-items: center;
  padding: 8px 10px; border-radius: 8px; background: rgba(255,255,255,.02);
}
.qx-item-stop {
  appearance: none; cursor: pointer; line-height: 1;
  width: 20px; height: 20px; padding: 0; border-radius: 6px;
  background: rgba(255,255,255,.05); color: var(--muted);
  border: 1px solid #2c2f44; font-size: 12px;
  transition: border-color .15s, color .15s, background .15s;
}
.qx-item-stop:hover { border-color: var(--danger, #e0556b); color: var(--danger, #e0556b); background: rgba(224,85,107,.08); }
.qx-item-spin {
  width: 10px; height: 10px; border-radius: 50%;
  border: 2px solid rgba(255,255,255,.18); border-top-color: var(--accent);
  animation: qx-spin .8s linear infinite;
}
.qx-item-text { min-width: 0; }
.qx-item-label { font-weight: 600; color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.qx-item-detail { color: var(--muted-2); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.qx-item-elapsed { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--muted); font-size: 12px; }
/* The fan-out one row owns: the upstream queries it spawned. Indented under the
   label rather than listed as siblings, because a season walk can contribute
   dozens and they are one user action, not dozens. */
.qx-item-fan {
  margin-left: 8px; font-weight: 400; font-size: 11px; color: var(--muted);
  padding: 1px 6px; border-radius: 999px; background: rgba(255,255,255,.05);
}
.qx-children { margin-top: 6px; display: flex; flex-direction: column; gap: 2px; max-height: 180px; overflow-y: auto; }
.qx-child {
  display: grid; grid-template-columns: 1fr auto auto; gap: 8px; align-items: center;
  font-size: 11px; color: var(--muted-2); padding-left: 8px;
  border-left: 2px solid rgba(255,255,255,.08);
}
.qx-child-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Records returned so far. A query sitting at 0 is the thing the idle cutoff
   exists to end, so it is worth reading at a glance. */
.qx-child-count { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--muted); }
.qx-child-elapsed { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
/* Terminal marker on a settled search grid — see search.js::markSettled. */
.row-head .settled { color: var(--muted-2); font-size: 12px; font-weight: 400; margin-left: 8px; }

/* ---- "Find more episodes" affordance + popover (detail page) ---- */
.findmore-row { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 6px 0 0; position: relative; }
/* What the page is doing right now (js/detail.js `renderSearchState`). Pushed
   to the far left of the row so it reads as a status line rather than a fourth
   button, and it never competes with the verbs for the eye's right edge. */
.search-status { margin-right: auto; color: var(--muted); font-size: 12px; min-height: 1em; }
/* Running: the animated ellipsis is the only motion on the page, which is the
   point — "still searching" and "finished, found nothing" were indistinguishable
   before it. Pure CSS so nothing has to tick a timer to keep it alive. */
.search-status.live::after {
  content: "…"; display: inline-block; width: 1.2em; text-align: left;
  animation: search-dots 1.2s steps(4, end) infinite;
}
@keyframes search-dots { 0% { content: ""; } 25% { content: "."; } 50% { content: ".."; } 75% { content: "…"; } }
@media (prefers-reduced-motion: reduce) { .search-status.live::after { animation: none; content: "…"; } }
/* "Search again" — the idle-state twin of Stop, in the same slot. Dashed like
   "Find more" because it is the same kind of act (ask the network for more),
   not a destructive one. */
.search-retry {
  appearance: none; cursor: pointer; font: inherit; font-size: 13px;
  border-radius: 999px; padding: 6px 14px;
  background: transparent; color: var(--muted);
  border: 1px dashed #2c2f44;
  transition: border-color .15s, color .15s, background .15s;
}
.search-retry:hover { border-color: var(--accent); color: var(--fg); background: rgba(255,255,255,.03); }
.search-retry.hidden { display: none; }
.findmore-row[hidden] { display: none; } /* `display:flex` overrides the UA hidden rule */
.findmore-btn {
  appearance: none; background: transparent; color: var(--muted);
  border: 1px dashed #2c2f44; border-radius: 999px;
  padding: 6px 14px; font-size: 13px; cursor: pointer;
  transition: border-color .15s, color .15s, background .15s;
}
.findmore-btn:hover { border-color: var(--accent); color: var(--fg); background: rgba(255,255,255,.03); }
.findmore-btn.hidden { display: none; }
.findmore-stop {
  appearance: none; cursor: pointer; font: inherit; font-size: 13px;
  border-radius: 999px; padding: 6px 14px;
  background: rgba(224,85,107,.08); color: var(--danger, #e0556b);
  border: 1px solid var(--danger, #e0556b);
  transition: background .15s, color .15s;
}
.findmore-stop:hover { background: var(--danger, #e0556b); color: #fff; }
/* "Mark season seen" — the season-scoped twin of the hero's series button
   (js/detail.js). Shares the findmore pill shape because it lives in the same
   row and acts on the same list; solid when the whole season is already marked,
   so the button reads as a state as well as a verb. */
.season-seen-btn {
  appearance: none; cursor: pointer; font: inherit; font-size: 13px;
  border-radius: 999px; padding: 6px 14px;
  background: transparent; color: var(--muted);
  border: 1px solid #2c2f44;
  transition: border-color .15s, color .15s, background .15s;
}
.season-seen-btn:hover { border-color: var(--accent); color: var(--fg); background: rgba(255,255,255,.03); }
.season-seen-btn.marked { background: var(--accent); border-color: transparent; color: #fff; }
.season-seen-btn[disabled] { opacity: .5; cursor: default; }
/* "Deep search season" — asks for every episode of the selected season
   individually (js/detail.js → catalog::episode_walk). Same pill as its
   neighbours because it acts on the same list; dashed like "Find more" rather
   than solid like the seen verb, because it starts a search rather than
   recording a fact. */
.deep-search-btn {
  appearance: none; cursor: pointer; font: inherit; font-size: 13px;
  border-radius: 999px; padding: 6px 14px;
  background: transparent; color: var(--muted);
  border: 1px dashed #2c2f44;
  transition: border-color .15s, color .15s, background .15s;
}
.deep-search-btn:hover:not([disabled]) { border-color: var(--accent); color: var(--fg); background: rgba(255,255,255,.03); }
.deep-search-btn[disabled] { opacity: .5; cursor: default; }
/* In flight: solid border + accent text, so "this is running" is legible at a
   glance on a tab strip the viewer is otherwise reading for episode numbers. */
.deep-search-btn.running { border-style: solid; border-color: var(--accent); color: var(--accent); opacity: 1; }
.findmore-stop.hidden { display: none; }
.findmore-pop {
  position: absolute; top: 110%; right: 0; z-index: 40;
  min-width: 22rem; max-width: 30rem;
  background: var(--bg-card); border: 1px solid #262838; border-radius: 12px;
  box-shadow: 0 12px 32px rgba(0,0,0,.55);
  padding: 12px; display: flex; flex-direction: column; gap: 8px;
}
.findmore-pop.hidden { display: none; }
.findmore-pop-label { color: var(--muted); font-size: 12px; }
.findmore-suggest { display: flex; flex-wrap: wrap; gap: 6px; }
.findmore-suggest .chip { cursor: pointer; }
.findmore-suggest .chip:hover { border-color: var(--accent); }
/* A chip is one query. In flight: accent, and inert — it is cancelled from the
   topbar ledger's ✕ or the row's "Stop search", never from two places at once.
   Settled: marked, but still clickable, because re-asking is additive. */
.findmore-suggest .chip.running { border-color: var(--accent); color: var(--accent); cursor: default; }
.findmore-suggest .chip.done { color: var(--muted-2); }
/* The language strip over the name chips (js/detail/find-more.js). Present only
   when the work really goes by names in more than one language; `on` is the
   current narrowing, and "All names" is the default. */
.findmore-langs { display: flex; flex-wrap: wrap; gap: 6px; }
.findmore-langs .chip { cursor: pointer; font-size: 11.5px; }
.findmore-langs .chip:hover { border-color: var(--accent); }
.findmore-langs .chip.on { border-color: var(--accent); color: var(--fg); background: rgba(255,255,255,.04); }

/* ── Identity chip + modal ────────────────────────────────────────────── */
.identity-chip {
  display: inline-flex; align-items: center; gap: 4px;
  background: var(--bg-elev); color: var(--fg);
  border: 1px solid #262838; border-radius: 14px;
  padding: 4px 10px; font: inherit; font-size: 12px; line-height: 1.2;
  cursor: pointer; transition: border-color .15s, background .15s;
  margin-left: 6px;
}
.identity-chip:hover { border-color: var(--accent); }
.identity-chip.active { background: rgba(96,165,250,0.12); border-color: var(--accent); }

.identity-modal { position: fixed; inset: 0; z-index: 200; }
.identity-modal.hidden { display: none; }
.identity-modal-backdrop {
  position: absolute; inset: 0; background: rgba(0,0,0,0.6);
}
/* The card SCROLLS. `.identity-modal` is fixed/inset:0 with no scroll container
   of its own, so without this any body taller than the viewport is clipped and
   simply unreachable — which is what happened to the bottom of a long profile
   roster, and to every panel that swaps into the body (the secret-key reveal,
   the invite panel). A remote cannot even scroll the page to it: scrollIntoView
   needs a scrollable ancestor, and there wasn't one. */
.identity-modal-card {
  position: relative; margin: 8vh auto 0; max-width: 32rem; width: calc(100% - 2rem);
  background: var(--bg-card); color: var(--fg);
  border: 1px solid #262838; border-radius: 12px;
  box-shadow: 0 12px 32px rgba(0,0,0,0.55);
  padding: 20px;
  max-height: 84vh; overflow-y: auto; overscroll-behavior: contain;
}
.identity-modal-card h3 { margin: 0 0 12px; font-size: 18px; }
.identity-label { color: var(--muted); font-size: 12px; margin: 12px 0 4px; }
.identity-mono {
  font-family: monospace; font-size: 12px; word-break: break-all;
  background: var(--bg-elev); border: 1px solid #262838;
  border-radius: 6px; padding: 8px 10px;
}
.identity-secret { background: #1f0a0a; border-color: #7f1d1d; }
.identity-meta { color: var(--muted); font-size: 12px; margin: 8px 0; }
.identity-warn {
  background: #7f1d1d; color: #fee2e2;
  padding: 10px 12px; border-radius: 6px; font-size: 13px; line-height: 1.4;
}
.identity-actions {
  display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px;
}
.identity-actions .btn {
  background: var(--bg-elev); color: var(--fg); border: 1px solid #262838;
  border-radius: 8px; padding: 6px 14px; font: inherit; font-size: 13px;
  cursor: pointer;
}
.identity-actions .btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.identity-actions .btn-secondary { background: var(--bg-elev); }
.identity-actions .btn-danger { background: #7f1d1d; color: #fff; border-color: #7f1d1d; }
.identity-actions .btn-ghost { background: transparent; }
.identity-actions .btn:hover { filter: brightness(1.1); }
.identity-import.hidden { display: none; }
.identity-import { display: flex; flex-direction: column; gap: 8px; margin-top: 12px; }
.identity-input {
  background: var(--bg-elev); color: var(--fg); border: 1px solid #262838;
  border-radius: 6px; padding: 8px 10px; font-family: monospace; font-size: 13px;
}
.identity-input:focus { outline: 0; border-color: var(--accent); }
.identity-error {
  color: #f87171; background: #1f0a0a; border: 1px solid #7f1d1d;
  border-radius: 6px; padding: 8px 10px; margin-top: 10px; font-size: 12px;
}
.identity-error.hidden { display: none; }
.identity-confirm {
  background: #1f0a0a; border: 1px solid #7f1d1d;
  border-radius: 6px; padding: 10px 12px; margin-top: 12px; font-size: 13px;
}
.identity-confirm.hidden { display: none; }

/* --- Multi-account profile picker --- */
.mw-chip-avatar {
  width: 18px; height: 18px; border-radius: 50%; object-fit: cover;
  display: inline-block; vertical-align: middle;
}
.mw-avatar-fallback {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg-elev); font-size: 11px;
}
.mw-chip-name { max-width: 12ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.mw-acct-list { display: flex; flex-direction: column; gap: 6px; }
.mw-acct-row {
  display: flex; align-items: center; gap: 10px;
  background: var(--bg-elev); border: 1px solid #262838;
  border-radius: 10px; padding: 8px 10px;
}
.mw-acct-row.active { border-color: var(--accent); background: rgba(96,165,250,0.10); }
.mw-acct-avatar {
  width: 36px; height: 36px; border-radius: 50%; object-fit: cover; flex: 0 0 auto;
}
.mw-acct-meta { flex: 1 1 auto; min-width: 0; }
.mw-acct-name { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A non-active row is itself the switch target (js/identity.js gives it
   role=button + tabindex): a remote should not have to aim at the small button
   on the right, which is a pointer affordance. The active row stays inert. */
.mw-acct-row[role="button"] { cursor: pointer; }
.mw-acct-row[role="button"]:hover { background: #21232f; }
.mw-acct-name em { color: var(--accent); font-style: normal; font-size: 11px; }
.mw-acct-uid { color: var(--muted); font-family: monospace; font-size: 11px; }
.mw-acct-actions { display: flex; gap: 6px; flex: 0 0 auto; }
.mw-acct-actions .btn {
  background: var(--bg-card); color: var(--fg); border: 1px solid #262838;
  border-radius: 8px; padding: 5px 10px; font: inherit; font-size: 12px; cursor: pointer;
}
.mw-acct-actions .btn:hover { filter: brightness(1.15); }

.mw-profile-edit { display: flex; flex-direction: column; gap: 4px; margin-top: 14px;
  border-top: 1px solid #262838; padding-top: 12px; }
.mw-acct-addbox { margin-top: 14px; border-top: 1px solid #262838; padding-top: 12px; }
.mw-acct-addbox.hidden { display: none; }

.mw-avatar-grid { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 4px; }
.mw-avatar-pick {
  padding: 0; border: 2px solid transparent; border-radius: 50%;
  background: none; cursor: pointer; line-height: 0;
}
.mw-avatar-pick img { width: 44px; height: 44px; border-radius: 50%; object-fit: cover; display: block; }
.mw-avatar-pick:hover { border-color: #41435a; }
.mw-avatar-pick.selected { border-color: var(--accent); }

/* ---- "Who's watching?" launch picker (js/who.js) ----
   The full-screen half of the profile surface: shown in #app, with the shell
   stripped by `body.signed-out` exactly like the gate, whenever a device holds
   more than one profile and nobody has said which one is watching yet.

   Sized in `clamp()` throughout on purpose — this is the one screen that must
   read at 10 feet on a television AND fit four tiles across a phone, and it has
   no rails or posters to borrow a scale from. */
.who {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(20px, 4vh, 44px);
  min-height: 76vh; padding: 6vh 24px 8vh; text-align: center;
}
.who-title { margin: 0; font-size: clamp(26px, 4.2vw, 46px); font-weight: 700; }
.who-grid {
  display: flex; flex-wrap: wrap; justify-content: center; align-items: flex-start;
  gap: clamp(16px, 3vw, 40px); max-width: 1100px;
}
.who-tile {
  position: relative; display: flex; flex-direction: column; align-items: center;
  gap: 12px; width: clamp(96px, 14vw, 176px);
  padding: 0; border: 0; background: none; font: inherit; cursor: pointer;
  color: var(--muted); transition: color .15s;
}
.who-avatar {
  width: 100%; aspect-ratio: 1; border-radius: 12px; overflow: hidden;
  background: var(--bg-elev); border: 3px solid transparent;
  box-sizing: border-box; transition: border-color .15s;
}
.who-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.who-avatar-fallback,
.who-avatar-add {
  display: flex; align-items: center; justify-content: center;
  font-size: clamp(34px, 5vw, 64px); line-height: 1; color: var(--muted-2);
}
.who-avatar-add { background: none; border-style: dashed; border-color: #2b2d3d; }
/* The profile the app is currently scoped to. It is the BORDER and not an inset
   ring: the picture fills the box and paints over any inset shadow. Declared
   before the hover/focus rules below so that pointing at a tile still wins —
   both selectors have the same specificity, so order is the tie-break. */
.who-tile.active .who-avatar { border-color: var(--accent); }
/* Hover and focus are the SAME state here (js/tv-nav.js focuses what the mouse
   is over), so the white edge is on both; the accent ring around the whole tile
   comes from the global focus rule below. */
.who-tile:hover, .who-tile:focus { color: var(--fg); }
.who-tile:hover .who-avatar, .who-tile:focus .who-avatar { border-color: #fff; }
.who-tile:focus { border-radius: 16px; }
.who-name {
  max-width: 100%; font-size: clamp(13px, 1.3vw, 18px);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Manage mode: the pencil sits over the picture, so a tile reads as "edit this
   one" rather than "watch as this one". */
.who-pencil {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 44px; height: 44px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(10, 11, 16, .72); color: #fff; font-size: 20px;
}
.who-tile.managing .who-avatar { filter: brightness(.55); }
.who-actions { display: flex; gap: 10px; }
.who-actions .btn { font-size: 14px; padding: 10px 20px; }
.who-edit {
  width: min(26rem, 100%); text-align: left; margin: 0 auto;
  background: var(--bg-card); border: 1px solid #262838; border-radius: 12px;
  padding: 20px; display: flex; flex-direction: column; gap: 4px;
}
.who-edit-title { margin: 0 0 6px; font-size: 18px; }

/* ---- Sign-in gate (js/auth-view.js) ----
   Shown in #app when no profile is active on this device. The app shell is
   stripped to the brand alone: every other control in the topbar acts on a
   signed-in user, and offering them here would just produce 412s. */
body.signed-out .topbar-extra,
body.signed-out .topbar .nav,
body.signed-out .topbar-search-btn,
body.signed-out .tabbar { display: none; }

.auth-card {
  max-width: 30rem; margin: 8vh auto 0; padding: 24px;
  background: var(--bg-card); border: 1px solid #262838; border-radius: 12px;
  box-shadow: 0 12px 32px rgba(0,0,0,0.45);
}
.auth-title { margin: 0 0 6px; font-size: 22px; }
.auth-sub { color: var(--muted); font-size: 14px; line-height: 1.5; margin: 0 0 16px; }
.auth-hint { color: var(--muted); font-size: 13px; margin: 0 0 8px; }
.auth-label { display: block; color: var(--muted); font-size: 12px; margin: 14px 0 4px; }
.auth-warn {
  background: #7f1d1d; color: #fee2e2; margin-bottom: 4px;
  padding: 10px 12px; border-radius: 8px; font-size: 13px; line-height: 1.45;
}
.auth-input {
  width: 100%; box-sizing: border-box;
  background: var(--bg-elev); color: var(--fg); border: 1px solid #262838;
  border-radius: 8px; padding: 10px 12px; font: inherit; font-size: 14px;
}
.auth-input:focus { outline: 0; border-color: var(--accent); }
.auth-secret-row { display: flex; align-items: center; gap: 6px; }
.auth-secret-row .auth-input { font-family: monospace; letter-spacing: .02em; }
.auth-reveal { flex: 0 0 auto; padding: 8px 10px; font-size: 15px; }
/* Kept in the DOM (password managers need a username field to pair with the
   secret) but out of the way — display:none would make some managers skip it. */
.auth-hidden-user {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.auth-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 18px; }
.auth-card .btn {
  background: var(--bg-elev); color: var(--fg); border: 1px solid #262838;
  border-radius: 8px; padding: 8px 16px; font: inherit; font-size: 14px; cursor: pointer;
}
.auth-card .btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
.auth-card .btn-ghost { background: transparent; }
.auth-card .btn:hover { filter: brightness(1.1); }
.auth-card .btn[disabled] { opacity: .6; cursor: default; }
.auth-adopt { margin-top: 18px; border-top: 1px solid #262838; padding-top: 14px; }
.auth-adopt.hidden { display: none; }
/* The invite note under the gate's buttons: why "Create a profile" is missing,
   or that the link you followed checked out. Informational, not an error —
   being uninvited is a normal state, not a failure. */
.auth-invite {
  color: var(--muted); font-size: 13px; line-height: 1.5;
  margin-top: 14px; padding-top: 12px; border-top: 1px solid #262838;
}
.auth-invite.hidden { display: none; }

/* ---- Device pairing (js/device.js on the TV, js/link.js on the phone) ----
   The QR is the load-bearing element here: it is read by a phone camera, from
   a sofa, off a screen that may be dim or at an angle. So it gets real size and
   a real light ground — a QR inverted or tinted to match a dark theme is a QR
   that does not scan. The SVG carries its own quiet zone (four modules), which
   is why nothing here adds padding around it. */
.auth-alt {
  display: block; width: 100%; margin-top: 12px;
  background: transparent !important; color: var(--muted) !important;
  border-color: transparent !important; font-size: 13px !important;
}
.auth-alt:hover, .auth-alt:focus { color: var(--fg) !important; }

.auth-card.mw-pair { max-width: 44rem; }
.mw-pair-body {
  display: flex; flex-wrap: wrap; gap: 22px; align-items: center;
  margin-top: 4px;
}
.mw-pair-qr {
  flex: 0 0 auto; width: min(48vh, 320px); max-width: 100%;
  border-radius: 10px; overflow: hidden; background: #fff;
  /* Empty when the payload overflowed the encoder — the typed code still
     works, so the box collapses rather than leaving a white hole. */
  line-height: 0;
}
.mw-pair-qr:empty { display: none; }
.mw-pair-qr svg { display: block; width: 100%; height: auto; }
.mw-pair-side { flex: 1 1 15rem; min-width: 0; }
.mw-pair-steplabel {
  color: var(--muted-2); font-size: 12px; text-transform: uppercase;
  letter-spacing: .06em; margin-bottom: 4px;
}
.mw-pair-url {
  font-size: 15px; word-break: break-all; margin-bottom: 14px; color: var(--fg);
}
.mw-pair-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: clamp(26px, 4vw, 38px); letter-spacing: .12em;
  color: var(--fg); margin-bottom: 14px;
}
.mw-pair-status { color: var(--muted); font-size: 13px; }

/* The approval screen's facts table: what you are about to sign in, and the
   code to check it against. */
.mw-pair-facts {
  display: grid; grid-template-columns: auto 1fr; gap: 6px 14px;
  margin: 16px 0; font-size: 14px;
}
.mw-pair-facts dt { color: var(--muted); }
.mw-pair-facts dd { margin: 0; word-break: break-word; }
.mw-pair-facts-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  letter-spacing: .1em;
}

/* link.html is a standalone page — no app shell, so it brings its own frame. */
body.mw-link-page { padding: 0 16px 40px; }
.mw-link-top {
  font-size: 18px; font-weight: 600; color: var(--fg);
  padding: 18px 4px 0; text-align: center;
}
body.mw-link-page .auth-card { margin-top: 5vh; }

/* --- Invite panel (js/invite.js), inside the profile modal --- */
.mw-invite-fresh { margin: 12px 0; }
.mw-invite-fresh.hidden { display: none; }
.mw-invite-list { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.mw-invite-row {
  display: flex; align-items: center; gap: 10px;
  background: var(--bg-elev); border: 1px solid #262838;
  border-radius: 10px; padding: 8px 10px;
}
.mw-invite-meta { flex: 1 1 auto; min-width: 0; }
.mw-invite-status { font-size: 13px; }
.mw-invite-id { color: var(--muted); font-family: monospace; font-size: 11px; }
.auth-error {
  color: #f87171; background: #1f0a0a; border: 1px solid #7f1d1d;
  border-radius: 8px; padding: 10px 12px; margin-top: 14px; font-size: 13px;
}
.auth-error.hidden { display: none; }

/* ---- Highlight (10-foot UI) ----
   ONE ring, raised by whichever device is driving (js/input-mode.js): `:focus`
   for the remote, `.mw-hover` for the mouse. Identical on purpose — the viewer
   should not have to learn two highlights — but they can never be on screen
   together, because each mode switches the other off at the source (focus is
   released in mouse mode; `#app` is inert to hit-testing in D-pad mode).
   Driven by :focus and not :focus-visible because the remote's focus is always
   programmatic, and the ring IS the cursor there.
   Cards are excluded: they have their own lift/outline treatment above. */
:where(button, a[href]:not(.card), input, select, textarea, [tabindex]:not(.card),
       .chip, .player-settings-menu__item):is(:focus, .mw-hover) {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: 8px;
  box-shadow: 0 0 0 6px var(--accent-glow);
}
/* Pill/round controls want a matching radius so the ring hugs the shape. */
.chip:is(:focus, .mw-hover),
.lang-pref-chip:is(:focus, .mw-hover),
.season-tab:is(:focus, .mw-hover),
.nav-ext:is(:focus, .mw-hover) { border-radius: 999px; }
.btn:is(:focus, .mw-hover) { border-radius: 10px; }
/* The top-bar search pill is ONE control, not two — but it is built from two
   elements, and both of them can raise the ring at the same time:
     • the FORM is the focus stop (`tabindex="0"`, index.html), so the pointer
       resolves to it and it takes `.mw-hover`;
     • the FIELD inside is `tabindex="-1"` — invisible to the remote, but still
       an <input>, and the generic rule above keys off the TAG, so the click
       that puts the caret in it lit the field up too. Its `outline` was already
       held at 0 by `.topbar .nav-search input` (higher specificity), but
       NOTHING held the `box-shadow`: what reached the screen was the 6px accent
       GLOW alone, a soft purple halo hugging the field, 14px inside the ring on
       the pill. Two highlights, and the inner one had no edge — so it read as a
       smear rather than as a second selection, which is why it survived the
       first pass at this (the accent border tint on `:focus-within`, above).
   Fix: the field never paints (it has no independent state worth showing — its
   caret IS the box's state), and the box paints for it via `:focus-within`,
   which is the one thing the box's own `:focus` cannot see. Same ring in all
   three cases, so the highlight never jumps or doubles:
     pointer over the box  → `.mw-hover` on the form
     caret in the field    → `:focus-within` on the form  ← here
     remote on the box     → `:focus` on the form
   Radius is inherited from `.topbar .nav-search` (22px, higher specificity than
   the generic 8px), so the ring hugs the pill in every one of them. */
.nav-search input:is(:focus, .mw-hover) { outline: none; box-shadow: none; }
.nav-search:focus-within {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  box-shadow: 0 0 0 6px var(--accent-glow);
}
.ep-row:is(:focus, .mw-hover),
.stream-item:is(:focus, .mw-hover),
.mw-acct-row:is(:focus, .mw-hover),
.findmore-pop:is(:focus, .mw-hover) { border-radius: 12px; }
/* A stream row's two controls live INSIDE `.stream-row`, which is
   `overflow: hidden` (it is the card, and it has to clip the panel's background
   to its own corners). The standard ring sits 2px OUTSIDE its element plus a 6px
   glow, so all of that was being clipped away: what reached the screen was a
   stray purple crescent between the play button and the caret, which read as a
   rendering artefact rather than as "this row is selected".
   Drawn INSIDE the element instead — same colour, same weight, nothing to clip.
   The two keep separate rings on purpose: the D-pad steps from ▶ to ⌄ within one
   row, and a single row-level highlight could not say which one is live. */
.stream-row .stream-item:is(:focus, .mw-hover),
.stream-row .stream-disclosure:is(:focus, .mw-hover) {
  outline-offset: -3px;
  box-shadow: none;
}
.stream-row .stream-item:is(:focus, .mw-hover) { border-radius: 11px; }
.stream-row .stream-disclosure:is(:focus, .mw-hover) { border-radius: 8px; }
/* Lookahead for the D-pad inside a PANEL. tv-nav scrolls with `block:
   "nearest"`, which parks the focused row flush against the edge of its scroll
   container — inside the stream drawer or the profile card that means you can
   never see what the next press will land on. A scroll margin buys one row of
   context in each direction.
   Page-level rows (.ep-row, .raw-row) are deliberately NOT here: they get the
   same lookahead, and the clearance under the sticky top bar, from the
   `scroll-padding` on the scroll root at the top of this file. Listing them in
   both places would stack the two offsets. */
.stream-row, .mw-acct-row { scroll-margin: 80px; }
/* Round icon buttons (strip chevrons, avatar picks) get a circular ring. */
.strip-arrow:is(:focus, .mw-hover),
.mw-avatar-pick:is(:focus, .mw-hover) { border-radius: 50%; }
/* The video stage: a softer inset highlight (a thick outline would sit over the
   picture). Arrows seek/volume here, so this just marks "the player has focus". */
/* The stage is a focus stop for the remote but NOT a hover target: a glow that
   appears merely because the mouse is resting over the picture would be on
   screen for the whole film. (It still takes `.mw-hover`, because the mouse →
   D-pad handover has to be able to seed the cursor here.) */
#videoContainer.mw-hover { outline: none; box-shadow: none; }
#videoContainer:focus {
  outline: none;
  box-shadow: inset 0 0 0 3px var(--accent-glow);
  transition: box-shadow .25s ease; /* fade in step with .player-controls */
}
/* ...but it is chrome, so it hides with the chrome. Tapping the stage to
   play/pause focuses it (tabindex="0") and nothing ever blurs it again, so on
   touch — fullscreen especially — the ring would otherwise frame the picture
   for the whole film after the control bar and the title overlay have faded. */
#videoContainer.is-idle:focus { box-shadow: none; }
/* Nav links already draw an underline bar when active; keep the ring tight. */
.nav a:is(:focus, .mw-hover) { outline-offset: 4px; }

/* ---- D-pad mode: the mouse is switched off ----
   `data-pointer="off"` is set by js/input-mode.js once the remote has actually
   been used, and only while it is driving.
   Hiding the cursor is not enough on its own: it does not MOVE the pointer, and
   no web API can (Pointer Lock is the only thing that hides it at platform
   level, and it steals Escape — which is our Back key). A pointer parked over an
   episode row goes on matching `:hover` for as long as the remote is driving, so
   a row two screens up stays lit while the D-pad walks. Making the content inert
   to hit-testing is what actually ends that, and it ends it for every `:hover`
   rule in this file at once, including ones written later.
   The cost, taken deliberately: the first click after re-activating the mouse
   *without moving it first* is swallowed — the flip back happens in that same
   event, too late for its default action. Any real pointer movement, which is
   how an air-mouse is switched back on, restores everything before the click.
   `#app` only: the top bar stays live so pointer events are certainly still
   delivered, and the player host keeps its own mouse-driven chrome. */
html[data-pointer="off"], html[data-pointer="off"] * { cursor: none !important; }
html[data-pointer="off"] #app { pointer-events: none; }

/* ---------------------------------------------------------------- build badge
   The version of the shell THIS TAB is running, bottom-left, deliberately
   faint: it is a diagnostic, not chrome, and it sits over content on every
   view. It brightens on hover/focus and turns amber when the server has moved
   on — that amber state is the point of the whole thing, since a tab running a
   cached shell from an older release is where misleading bug reports come from.
   Painted by js/version.js; hidden during playback, where nothing but the film
   belongs on screen. */
#mw-build {
  position: fixed;
  left: 8px;
  bottom: 6px;
  z-index: 40;
  padding: 2px 6px;
  border-radius: 6px;
  font-size: 10px;
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
  letter-spacing: .02em;
  color: var(--muted-2);
  background: rgba(10, 11, 16, .55);
  cursor: pointer;
  user-select: all; /* selectable by hand where the clipboard API is refused */
  opacity: .5;
  transition: opacity .15s ease, color .15s ease;
}
#mw-build:hover { opacity: 1; color: var(--muted); }
#mw-build.mw-build--stale {
  opacity: 1;
  color: var(--warn);
  background: rgba(255, 212, 59, .1);
}
body.mw-watching #mw-build { display: none; }

/* The update prompt. A worker for a newer release has taken over while this
   tab keeps running the modules it booted with — see js/version.js for why
   this asks instead of reloading. */
#mw-update {
  position: fixed;
  left: 50%;
  bottom: 18px;
  transform: translateX(-50%);
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px 10px 16px;
  border-radius: 999px;
  font-size: 13px;
  color: var(--fg);
  background: var(--bg-elev);
  box-shadow: var(--shadow);
}
#mw-update button {
  border: 0;
  border-radius: 999px;
  padding: 6px 14px;
  font: inherit;
  font-weight: 600;
  color: #fff;
  background: var(--accent);
  cursor: pointer;
}
#mw-update button:hover { background: var(--accent-2); }
/* The "installed but it cannot take over" state — a sentence, not a label, so
   the pill becomes a card and is allowed to wrap. See js/version.js::showStuck. */
#mw-update.mw-update--stuck {
  align-items: flex-start;
  max-width: min(92vw, 520px);
  padding: 12px 14px;
  border-radius: 14px;
  line-height: 1.45;
}
#mw-update.mw-update--stuck button { flex: 0 0 auto; }

/* ── Report form (js/report.js) ───────────────────────────────────────────
   One line: a note box and Send. Mounted twice — at the top of the player's ⓘ
   panel (dark variant in css/player-ui.css) and in the top bar's 🐛 popover.
   The BOX is the D-pad's focus stop, not the <input>: see report.js. */
.mw-report { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.mw-report-field {
  flex: 1 1 14rem; min-width: 0; display: flex; align-items: center;
  background: var(--bg-elev); border: 1px solid #262838; border-radius: 10px;
  padding: 0 10px; cursor: text;
}
.mw-report-field:focus-within { border-color: var(--accent); }
.mw-report-field input {
  flex: 1; min-width: 0; padding: 8px 0;
  background: transparent; border: 0; outline: 0;
  color: var(--fg); font: inherit; font-size: 13px;
}
.mw-report-send { padding: 7px 14px; font-size: 12.5px; font-weight: 600; border-radius: 10px; white-space: nowrap; }
.mw-report-send[aria-disabled="true"] { opacity: .6; cursor: default; }
/* The 🐛 is a .nav-ext pill, but a <button>: it has to shed the UA button
   chrome the <a> pills never had. */
.topbar .mw-report-btn { background: transparent; font-family: inherit; line-height: 1.2; cursor: pointer; }
.mw-report-popover { right: 16px; width: min(26rem, calc(100vw - 32px)); max-width: none; padding: 12px; gap: 8px; }
.mw-report-hint { color: var(--muted); font-size: 12px; }

/* <meta-service-menu> lives in shadow DOM and cannot read the vars above, so
   map this UI's palette onto the four tokens it does read. */
meta-service-menu {
  --mm-nav-fg: var(--fg);
  --mm-nav-bg: var(--bg-elev, var(--bg-card, var(--bg)));
  --mm-nav-border: var(--border, rgba(255, 255, 255, 0.14));
  --mm-nav-accent: var(--accent);
}
