:root {
  /* A reading palette. The video app's violet-on-near-black is for a dark room
     and a ten-foot UI; a reader is used in daylight, for long stretches, and its
     content is mostly white pages — so the chrome is a warm dark that a page
     does not glare against. */
  --bg: #12100e;
  --bg-elev: #1c1917;
  --bg-card: #232020;
  --fg: #f5f1ea;
  --muted: #b3a99b;
  --muted-2: #7d7469;
  --accent: #c98a4b;
  --accent-2: #e0a568;
  --accent-glow: rgba(201, 138, 75, 0.4);
  --ok: #7bb661;
  /* "we already hold this file" — deliberately not another green. */
  --cached: #4bb8c9;
  --warn: #e0b45a;
  --err: #e06c6c;
  --card-w: 156px;
  /* Book/comic covers are portrait. 2:3 is the publishing standard and what
     every upstream in this domain returns; a 16:9 tile would letterbox all of
     them. */
  --cover-ratio: 1.5;
  --radius: 12px;
  --shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
  /* Watch-view geometry. --mr-player-pad is the horizontal gutter of
     .player-wrap; it appears in the max-width calc there because `box-sizing:
     border-box` makes max-width include the padding. --mr-player-bottom-reserve
     is screen space the video must NOT occupy — 0 here, the fixed phone tab bar
     in css/mobile.css. --mr-avail-h itself is written from
     js/player/player-view.js (measureStage); the 100vh fallback keeps the page
     sane before the first measurement and if the script never runs. */
  --mr-player-pad: 24px;
  --mr-player-bottom-reserve: 0px;
  /* Breathing room under the stage. Ending the video exactly ON the viewport
     edge reads as a clipping bug rather than a decision, and it leaves nothing
     to suggest the synopsis and stream details are one scroll away. This much
     clears .player-info's top margin and breaks the top of its first line of
     text over the fold — the ordinary "there is more below" signal. It is one
     number: raise it to show a full line, drop it to 0 for edge-to-edge. */
  --mr-player-peek: 28px;

  /* ---- Tokens the ported stylesheet never had ----
     Everything above this line came across from meta-watch. Below is the part
     that makes a restyle possible at all: before these existed, every padding
     and every font-size in the app was a bare literal (13px, 13.5px, 11.5px,
     10.5px…), which is why nothing lined up with anything else. */

  /* Type. Inter was declared in the font stack but never actually loaded, so
     the app has always rendered in system-ui. Own that rather than pretend. */
  --font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
  --fs-xs: 11px;
  --fs-sm: 12.5px;
  --fs-md: 14px;
  --fs-lg: 17px;
  --fs-xl: 22px;
  --fs-2xl: 28px;

  /* Space. A 4px base — the smallest step that still reads as deliberate at
     the densities a cover grid wants. */
  --sp-1: 4px;
  --sp-2: 8px;
  --sp-3: 12px;
  --sp-4: 16px;
  --sp-5: 24px;
  --sp-6: 32px;
  --sp-7: 48px;

  --border: rgba(245, 241, 234, 0.09);
  --border-strong: rgba(245, 241, 234, 0.16);
  --focus: var(--accent-2);
  --radius-sm: 8px;
  --radius-lg: 16px;

  /* The rail. One number: every content surface is inset by it, and the phone
     breakpoint zeroes it when the rail becomes a drawer. */
  --rail-w: 208px;
}

* { box-sizing: border-box; }

/* Scoped `.hidden` rules exist per component throughout this file, but there
   was never a bare one — which is why `#mr-identity-modal.modal.hidden` was
   rendering inline, in flow, on every page load. */
[hidden], .hidden { display: none !important; }
/* 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. */
/* A left rail steals no vertical space, so only the phone tab bar needs
   reserving here — the old `100px` top was sized for a sticky top bar. */
html { scroll-padding: 0 0 80px; }
html, body {
  margin: 0;
  /* ⚠ This gradient used to start at #1a1530 — meta-watch's violet, carried
     over verbatim with the rest of its stylesheet. One unported hex was the
     single biggest reason the app read as "the video app with books in it",
     because it tinted the entire page behind a warm palette. */
  color: var(--fg);
  font-family: var(--font);
  font-size: var(--fs-md);
  -webkit-font-smoothing: antialiased;
}
/* ⚠ On `html` only. It used to be on `html, body` with `fixed`; phone browsers
   ignore `fixed`, so on a page shorter than the screen (Account, the sign-in
   gate) the body's copy stopped at the body's height and drew a hard seam
   across the middle of the screen. `html` always paints the whole canvas. */
html { background: var(--bg) radial-gradient(1100px 620px at 78% -12%, #241d16 0%, var(--bg) 58%) no-repeat; min-height: 100%; }
a { color: inherit; text-decoration: none; }
::-webkit-scrollbar { height: 10px; width: 10px; }
::-webkit-scrollbar-thumb { background: #332c25; border-radius: 6px; }
::-webkit-scrollbar-thumb:hover { background: #453b31; }

/* ============================================================================
   The navigation rail
   ==========================================================================*/
.rail {
  position: fixed; inset: 0 auto 0 0; z-index: 60;
  width: var(--rail-w);
  display: flex; flex-direction: column;
  gap: var(--sp-1);
  padding: var(--sp-4) var(--sp-3);
  background: var(--bg-elev);
  border-right: 1px solid var(--border);
}
.rail-brand {
  display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-3);
  margin-bottom: var(--sp-4);
  font-size: var(--fs-lg); letter-spacing: .2px;
}
.rail-mark { color: var(--accent); font-size: 20px; }
.rail-word { color: var(--muted); }
.rail-word b { color: var(--fg); font-weight: 700; }

.rail-nav { display: flex; flex-direction: column; gap: 2px; }
.rail-item {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-3);
  border-radius: var(--radius-sm);
  color: var(--muted);
  font-size: var(--fs-md);
  background: none; border: 0; width: 100%; text-align: left;
  cursor: pointer; font-family: inherit;
}
.rail-item:hover { background: rgba(245, 241, 234, 0.05); color: var(--fg); }
.rail-item.active { background: rgba(201, 138, 75, 0.14); color: var(--accent-2); }
.rail-icon { font-size: 17px; line-height: 1; width: 20px; text-align: center; }
.rail-spacer { flex: 1; }
.rail-peers {
  display: flex; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
}
.peer-link { font-size: var(--fs-xs); color: var(--muted-2); }
.peer-link:hover { color: var(--accent-2); }
.rail-account { margin-top: var(--sp-1); }

/* Phone: the rail becomes a drawer and .tabbar takes over. */
.rail-scrim {
  position: fixed; inset: 0; z-index: 55;
  background: rgba(0, 0, 0, 0.5);
}

/* The content surface clears the rail. One rule, one token.
   ⚠ #reader-host is deliberately NOT included: it is a full-bleed overlay that
   covers the rail too, and insetting it would letterbox every page. */
#app { padding-left: var(--rail-w); }

/* ---- 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;
}
.topbar .nav-search:focus-within { border-color: var(--accent); 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-read's
   color tokens. 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; }
/* ⚠ Anchored to the RAIL, not to a topbar — `position: fixed` with the offsets
   written by js/lang.js::placePopover, exactly like .qx-popover above it. The
   inherited `position: absolute; top: 130%; right: 0` came from meta-watch,
   where the chip sits in a full-width top bar; measured against a 208px fixed
   rail it puts an 18rem panel off the left edge of the screen. The custom
   properties (rather than inline left/top) are what let css/mobile.css pin the
   panel to the bottom of a phone screen without an !important fight. */
.lang-pref-popover {
  position: fixed; z-index: 60;
  left: var(--lp-x, calc(var(--rail-w) + 8px));
  top: var(--lp-y, auto);
  min-width: 18rem; max-width: 26rem;
  max-height: calc(100vh - 24px); overflow-y: auto;
  /* `100vh` counts the area behind a phone's collapsing address bar; `dvh` is the
     visible height. The line above is the fallback. */
  max-height: calc(100dvh - 24px);
  padding: 10px 14px; background: var(--bg-card);
  border: 1px solid #262838; border-radius: 12px;
  box-shadow: 0 12px 32px rgba(0,0,0,.55);
  display: flex; flex-wrap: wrap; gap: 7px 9px; align-items: center;
  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); }
/* A disabled action must LOOK disabled everywhere, not just where a view
   remembered to style it. Without this a greyed-out Read button still rendered
   as the bright primary call to action and still animated on :active — it read
   as a working button that silently did nothing, which is the exact defect the
   disabled state was added to fix. */
.btn[disabled] { opacity: .45; cursor: not-allowed; }
.btn[disabled]:active { transform: none; }
.btn-primary { background: #fff; color: #11121a; }
.btn-primary:not([disabled]):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, driven solely by :focus. js/tv-nav.js moves
   focus to the card under the mouse on real pointer movement, so "the hovered
   card" and "the focused card" are always the same single element. Note: NOT
   keyed off :hover — that would light up a second card (the one under a
   stationary mouse) while the keyboard drives focus elsewhere. */
.card:focus .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 { 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 hover AND on
   :focus, because js/tv-nav.js moves focus to the card under the pointer on real
   pointer movement — keying it off :hover alone would leave it invisible when
   the remote drives, and off :focus alone it would lag the mouse by a frame.
   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: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; }
.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; outline: none;
}
.search-form input:focus { border-color: var(--accent); }
.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; }
/* 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; }

/* ---- 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; }
.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; }
.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 panel's caveat line (a title-matched 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);
  padding-bottom: 9px; margin-bottom: 2px; border-bottom: 1px solid #20233350; }
.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 { 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 ---- */
/* The stage is 16:9 and width-driven (.video-container, css/player-ui.css), so
 * the ONLY way to make the video fill the screen vertically is to bound this
 * column's width by the height left over under the topbar:
 *
 *     max-width = availableHeight × 16/9 + gutters
 *
 * which lands the stage's bottom edge exactly at the bottom of the viewport.
 * There is deliberately no fixed cap any more (the old 1180px one is what left
 * a third of a 1080p screen unused): on a tall/wide display the video simply
 * gets bigger. Where the height constraint does not bind — phones, short
 * windows — the calc exceeds 100% and this behaves exactly as `width: 100%`,
 * which is why no media query is needed for the phone case.
 *
 * --mr-avail-h is measured, not hardcoded: the topbar is 62/58/56px across the
 * three breakpoints and the row below wraps, and #mr-net-warn / #mr-buffer come
 * and go mid-load. See measureStage() in js/player/player-view.js.
 *
 * No bottom padding: .player-info owns the space below the fold, so the only
 * thing between the stage and the viewport bottom is --mr-player-peek. */
.player-wrap {
  max-width: calc((var(--mr-avail-h, 100vh)
                   - var(--mr-player-bottom-reserve) - var(--mr-player-peek)) * 16 / 9
                  + var(--mr-player-pad) * 2);
  margin: 0 auto;
  padding: 8px var(--mr-player-pad) 0;
}
/* Diagnostics strip under the player chrome: status badge, speed chip, and the
   metadata bits (#mr-meta is a flex item of this row, not a block below it —
   see index.html). The title and every action moved inside .video-container so
   they survive fullscreen — see the note in index.html. */
.player-top { display: flex; align-items: center; flex-wrap: wrap; gap: 6px 12px; margin-bottom: 10px; }
.status-badge { font-size: 13px; 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; }
.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. */
.speed-chip { font-size: 12px; font-weight: 700; color: var(--muted);
  display: inline-flex; align-items: center; gap: 5px; padding: 2px 9px;
  border-radius: 999px; background: #1a1c28; border: 1px solid #23263a; white-space: nowrap; }
.speed-chip.ok { color: #51cf66; border-color: #2b5a33; }
.speed-chip.bad { color: #ff6b6b; border-color: #5a2a2e; }
.net-warn { margin: 0 0 12px; max-width: 900px; padding: 9px 13px; border-radius: 10px;
  background: #2a2416; border: 1px solid #5a4a2a; color: #ffd98a; font-size: 12.5px; line-height: 1.5; }
/* Usenet materialisation buffering bar (shown during the gateway full-download
 * window for nzb sources). */
.mr-buffer { margin: 0 0 12px; max-width: 900px; }
.mr-buffer-track { height: 7px; border-radius: 999px; background: #1d2330; overflow: hidden; border: 1px solid #2b3548; }
.mr-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. */
.mr-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: mr-buffer-stripes .8s linear infinite; }
@keyframes mr-buffer-stripes { to { background-position: 22px 0; } }
.mr-buffer-label { margin-top: 6px; font-size: 12.5px; color: var(--muted, #9aa6b6); font-weight: 600; }
/* The video itself + control bar / overlays live inside the ported
 * .video-container (see css/player-ui.css). The stage is just its mount point. */
.player-stage { position: relative; }
@keyframes spin { to { transform: rotate(360deg); } }
.unsupported { margin-top: 16px; padding: 18px 20px; border-radius: 12px; background: #2a1416; border: 1px solid #5a2a2e; color: #ffb3b8; }

/* ---- Player info (metadata strip + synopsis + collapsible tech) ---- */
/* A flex ITEM of .player-top (see index.html), hence no margin of its own —
   the row's gap is the separator. */
.player-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 0;
  margin: 0; font-size: 13px; color: var(--muted); }
/* [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 flex item — and so a phantom row gap — in .player-top. */
.player-meta[hidden], .speed-chip[hidden] { display: none; }
.player-meta-bit { white-space: nowrap; }
.player-meta-bit:not(:first-child)::before { content: "·"; margin: 0 9px; opacity: .5; }
/* Everything below the fold. Owns the page's bottom padding because
   .player-wrap has none — that is what lets the stage end flush with the
   viewport bottom. */
.player-info { max-width: 900px; margin: 18px 0 0; padding-bottom: 90px;
  display: flex; flex-direction: column; gap: 12px; }
.player-synopsis { margin: 0; font-size: 14px; line-height: 1.6; color: var(--muted-2); }
.player-filename { font-size: 12px; color: var(--muted);
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.player-tech { border: 1px solid #23263a; border-radius: 11px; background: #14151d; }
.player-tech > summary { cursor: pointer; padding: 11px 15px; font-size: 12.5px; font-weight: 700;
  letter-spacing: .02em; color: var(--muted); list-style: none; user-select: none; }
.player-tech > summary::-webkit-details-marker { display: none; }
.player-tech > summary::before { content: "▸"; display: inline-block; margin-right: 8px; transition: transform .15s; opacity: .7; }
.player-tech[open] > summary::before { transform: rotate(90deg); }
.player-tech > summary:hover { color: var(--fg); }
.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; }
.player-tech-dl dt { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
.player-tech-dl dd { margin: 0; font-size: 12.5px; color: var(--muted-2); }
.player-tech-dl dd.mr-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.mr-ttmvp { font-variant-numeric: tabular-nums; }
.player-tech-dl dd.mr-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. */
#mr-tech-device { grid-column: 1 / -1; margin-top: 4px; padding-top: 11px; border-top: 1px solid #23263a; }
#mr-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 .mr-cache-clear { padding: 5px 11px; font-size: 11.5px; font-weight: 600; border-radius: 7px; }
.player-tech-dl dd .mr-cache-clear[disabled] { opacity: .55; cursor: default; }
.player-tech-json-head { padding: 0 16px; font-size: 11px; text-transform: uppercase;
  letter-spacing: .04em; color: var(--muted); }
.player-tech-json { margin: 6px 16px 16px; padding: 12px 14px; border-radius: 9px;
  background: #0e0f16; border: 1px solid #23263a; color: var(--muted-2);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; line-height: 1.5;
  max-height: 340px; overflow: auto; white-space: pre; -moz-tab-size: 2; tab-size: 2; }
@media (max-width: 720px) { .player-tech-grid { grid-template-columns: 1fr; } }

/* ---- Skeletons ----

   One shimmer, several shapes. The `.skel-poster` gradient hardcoded a cool
   near-black from before this app had its own warm palette; the shared
   `--skel-*` pair below is that gradient re-derived from `--bg-card`, so a
   placeholder now reads as an empty slot of THIS page rather than a hole
   punched through to the video app's theme. */
:root { --skel-base: #201d1b; --skel-sheen: #2c2825; }
.skel-poster, .skel-cover, .skel-line, .skel-chapter {
  background: linear-gradient(100deg,
    var(--skel-base) 30%, var(--skel-sheen) 50%, var(--skel-base) 70%);
  background-size: 200% 100%; animation: shimmer 1.4s infinite;
}
.skel-poster { aspect-ratio: 2/3; border-radius: var(--radius); }
/* ⚠ Matches `.work-cover`, not `.skel-poster`. The wall's tiles are 2:3; the
   work hero's cover is `1 / var(--cover-ratio)` and a placeholder of the wrong
   ratio makes the whole hero jump the moment the real cover lands. */
.skel-cover { width: 200px; aspect-ratio: 1 / var(--cover-ratio); flex: none;
  border-radius: var(--radius); }
.skel-line { height: 13px; border-radius: 6px; margin-bottom: 10px; max-width: 62ch; }
.skel-line-title { height: 26px; width: 42%; margin-bottom: 14px; }
.skel-line-wide { width: 80%; }
.skel-chapter { height: 38px; border-radius: 8px; margin-bottom: 6px; }
@keyframes shimmer { to { background-position: -200% 0; } }
/* A shimmer that never stops is noise for anyone who asked the OS to stop
   moving things. The placeholder still reads as "not content" without it. */
@media (prefers-reduced-motion: reduce) {
  .skel-poster, .skel-cover, .skel-line, .skel-chapter { animation: none; }
}

/* 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 (rail) ---- */
.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; }

/* Anchored to the indicator, which lives near the BOTTOM of the 208px rail —
   not to a topbar. The old `top: 56px; right: 34px` was left over from the
   topbar layout: measured against the fixed rail it put the panel up beside the
   brand, and since the panel is wider (18rem) than the rail it ran off the left
   edge of the screen. main.js::placePop writes --qx-x/--qx-y from the button's
   own rect; the fallbacks below are what a paint before that lands looks like.
   Kept as custom properties rather than inline `left`/`top` so the phone rules
   in css/mobile.css can still pin it to the viewport without fighting inline
   styles. */
.qx-popover {
  position: fixed; z-index: 60;
  left: var(--qx-x, calc(var(--rail-w) + 8px));
  top: var(--qx-y, auto);
  min-width: 18rem; max-width: 26rem;
  max-height: calc(100vh - 24px); overflow-y: auto;
  /* `100vh` counts the area behind a phone's collapsing address bar; `dvh` is the
     visible height. The line above is the fallback. */
  max-height: calc(100dvh - 24px);
  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; justify-content: flex-end; gap: 8px; padding: 6px 0 0; position: relative; }
.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); }

/* ── 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 --- */
.mr-chip-avatar {
  width: 18px; height: 18px; border-radius: 50%; object-fit: cover;
  display: inline-block; vertical-align: middle;
}
.mr-avatar-fallback {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg-elev); font-size: 11px;
}
.mr-chip-name { max-width: 12ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.mr-acct-list { display: flex; flex-direction: column; gap: 6px; }
.mr-acct-row {
  display: flex; align-items: center; gap: 10px;
  background: var(--bg-elev); border: 1px solid #262838;
  border-radius: 10px; padding: 8px 10px;
}
.mr-acct-row.active { border-color: var(--accent); background: rgba(96,165,250,0.10); }
.mr-acct-avatar {
  width: 36px; height: 36px; border-radius: 50%; object-fit: cover; flex: 0 0 auto;
}
.mr-acct-meta { flex: 1 1 auto; min-width: 0; }
.mr-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. */
.mr-acct-row[role="button"] { cursor: pointer; }
.mr-acct-row[role="button"]:hover { background: #21232f; }
.mr-acct-name em { color: var(--accent); font-style: normal; font-size: 11px; }
.mr-acct-uid { color: var(--muted); font-family: monospace; font-size: 11px; }
.mr-acct-actions { display: flex; gap: 6px; flex: 0 0 auto; }
.mr-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;
}
.mr-acct-actions .btn:hover { filter: brightness(1.15); }

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

.mr-avatar-grid { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 4px; }
.mr-avatar-pick {
  padding: 0; border: 2px solid transparent; border-radius: 50%;
  background: none; cursor: pointer; line-height: 0;
}
.mr-avatar-pick img { width: 44px; height: 44px; border-radius: 50%; object-fit: cover; display: block; }
.mr-avatar-pick:hover { border-color: #41435a; }
.mr-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 — and it is ALL that
   is shown. Every control in the shell acts on a signed-in user (the rail's
   Home/Library/Search/Sources routes, Account, the peer links, the language
   chooser behind `/api/prefs/languages`, the in-flight query indicator), so
   leaving any of it up around the gate only offers actions that 401.

   meta-watch strips itself the same way; the difference here is that meta-read
   hangs all of that off the rail rather than a top bar, so the whole rail goes
   — and with it the inset #app keeps for it, which would otherwise push the
   card off-centre by half a rail. The phone drawer's scrim goes too: there is
   nothing left to slide in. */
body.signed-out .rail,
body.signed-out .rail-scrim,
body.signed-out .tabbar { display: none; }
body.signed-out #app { padding-left: 0; }

.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.mr-pair { max-width: 44rem; }
.mr-pair-body {
  display: flex; flex-wrap: wrap; gap: 22px; align-items: center;
  margin-top: 4px;
}
.mr-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;
}
.mr-pair-qr:empty { display: none; }
.mr-pair-qr svg { display: block; width: 100%; height: auto; }
.mr-pair-side { flex: 1 1 15rem; min-width: 0; }
.mr-pair-steplabel {
  color: var(--muted-2); font-size: 12px; text-transform: uppercase;
  letter-spacing: .06em; margin-bottom: 4px;
}
.mr-pair-url {
  font-size: 15px; word-break: break-all; margin-bottom: 14px; color: var(--fg);
}
.mr-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;
}
.mr-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. */
.mr-pair-facts {
  display: grid; grid-template-columns: auto 1fr; gap: 6px 14px;
  margin: 16px 0; font-size: 14px;
}
.mr-pair-facts dt { color: var(--muted); }
.mr-pair-facts dd { margin: 0; word-break: break-word; }
.mr-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.mr-link-page { padding: 0 16px 40px; }
.mr-link-top {
  font-size: 18px; font-weight: 600; color: var(--fg);
  padding: 18px 4px 0; text-align: center;
}
body.mr-link-page .auth-card { margin-top: 5vh; }

/* --- Invite panel (js/invite.js), inside the profile modal --- */
.mr-invite-fresh { margin: 12px 0; }
.mr-invite-fresh.hidden { display: none; }
.mr-invite-list { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.mr-invite-row {
  display: flex; align-items: center; gap: 10px;
  background: var(--bg-elev); border: 1px solid #262838;
  border-radius: 10px; padding: 8px 10px;
}
.mr-invite-meta { flex: 1 1 auto; min-width: 0; }
.mr-invite-status { font-size: 13px; }
.mr-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; }

/* ---- Focus highlight (10-foot UI / single cursor) ----
   ONE highlight for mouse and remote alike. js/tv-nav.js moves focus to the
   element under the mouse, so "hovered" and "focused" are the same element, and
   this ring follows the pointer just as it follows the D-pad. Driven by :focus
   (not :focus-visible) so a mouse-driven focus shows the ring too — the whole
   point is that the two highlights are identical. Cards are excluded: they have
   their own lift/outline treatment (.card:focus .card-poster, above). */
:where(button, a[href]:not(.card), input, select, textarea, [tabindex]:not(.card),
       .chip, .player-settings-menu__item):focus {
  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:focus,
.lang-pref-chip:focus,
.season-tab:focus,
.nav-ext:focus { border-radius: 999px; }
.btn:focus { border-radius: 10px; }
.ep-row:focus,
.stream-item:focus,
.mr-acct-row:focus,
.findmore-pop:focus { 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:focus,
.stream-row .stream-disclosure:focus {
  outline-offset: -3px;
  box-shadow: none;
}
.stream-row .stream-item:focus { border-radius: 11px; }
.stream-row .stream-disclosure:focus { 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, .mr-acct-row { scroll-margin: 80px; }
/* Round icon buttons (strip chevrons, avatar picks) get a circular ring. */
.strip-arrow:focus,
.mr-avatar-pick:focus { 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". */
#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:focus { outline-offset: 4px; }

/* ===========================================================================
   Reading-specific surfaces
   ---------------------------------------------------------------------------
   Appended rather than woven in, so the shared chrome above stays diffable
   against meta-watch's copy — the two apps' topbar, auth gate and identity
   modal are the same furniture and fixes should keep flowing between them.
   =========================================================================== */

/* Covers are portrait, not 16:9. */
.card-art {
  position: relative;
  aspect-ratio: 1 / var(--cover-ratio);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--bg-card);
}
.card-cover { width: 100%; height: 100%; object-fit: cover; display: block; }
.card-cover-none {
  display: grid; place-items: center; font-size: 2rem; opacity: 0.35;
}
.card-badges {
  position: absolute; left: 6px; bottom: 6px; right: 6px;
  display: flex; flex-wrap: wrap; gap: 4px;
}
.badge {
  font-size: 10px; line-height: 1.4; padding: 1px 6px; border-radius: 999px;
  background: rgba(0,0,0,0.62); color: var(--fg); backdrop-filter: blur(4px);
}
.badge-free { background: rgba(123,182,97,0.85); color: #10240a; }
.badge-slow { background: rgba(224,180,90,0.85); color: #2a1f06; }
.badge-raw  { background: rgba(224,108,108,0.85); color: #2a0808; }
.badge-count { background: rgba(0,0,0,0.72); color: var(--muted); }

/* The three provenance axes, each with its own weight — see js/provenance.js.
   Deliberately NOT one uniform run of pills: transport ("how are the bytes
   fetched"), birth ("who published it") and reach ("how did this get to me just
   now") are three different questions with three different lifetimes, and a row
   that renders them identically reads as one three-word badge. The video side
   learned this first and says so at its own .stream-chip-proto / -src split.
   Ordered and weighted by descending durability. */
.line-chips, .card-chips {
  display: inline-flex; gap: 4px; flex-wrap: wrap; align-items: center; min-width: 0;
}
.chip {
  font-size: 10px; line-height: 1.5; padding: 0 6px; border-radius: 999px;
  white-space: nowrap; border: 1px solid transparent;
}
/* Derived from the cid key-set by multicodec — a fact about the bytes, never
   stored anywhere. Filled, like a unit label. */
.chip-proto { background: rgba(255,255,255,0.09); color: var(--muted-2); letter-spacing: .02em; }
/* Who published it. Durable, intrinsic, and the one a reader may recognise. */
.chip-src { color: #c9a227; border-color: rgba(201,162,39,0.38); background: rgba(201,162,39,0.10); }
/* How it reached us on THIS search. Transient, so it must never outweigh the
   other two — outline only, muted. */
.chip-reach { color: var(--muted-2); border-color: rgba(255,255,255,0.14); }
.card-progress {
  position: absolute; top: 6px; right: 6px;
  font-size: 10px; padding: 1px 6px; border-radius: 999px;
  background: rgba(0,0,0,0.62); color: var(--fg);
}

.grid {
  display: grid; gap: 18px;
  grid-template-columns: repeat(auto-fill, minmax(var(--card-w), 1fr));
}

/* The raw bucket is a labelled section, never a fallback — its note says so. */
.row-raw { margin-top: 32px; opacity: 0.92; }

/* Raw items are LINES, not cards.
   ⚠ Not a style preference — the section's definition restated in CSS. Raw
   means "not enough metadata to draw a card" (ARCHITECTURE.md §6.0, Axis A) and
   the first thing a card needs is a cover, so a poster grid here renders as
   nothing but 📖 placeholders. meta-watch already learned this; meta-read
   reproduced it and drew forty empty rectangles on a `kaiju` search. */
.lines { display: flex; flex-direction: column; gap: 1px; }
.line {
  display: flex; align-items: baseline; gap: 10px;
  padding: 7px 10px; border-radius: 6px;
  color: var(--fg); text-decoration: none;
  background: var(--bg-card);
}
.line:hover { background: var(--bg-hover, rgba(255,255,255,0.06)); }
.line-title {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 13px;
}
.line-sub {
  flex: 0 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 11px; color: var(--muted-2);
}
.line-reason {
  flex: none; font-size: 10px; line-height: 1.4; padding: 1px 6px;
  border-radius: 999px;
  background: rgba(224,108,108,0.28); color: var(--muted);
}
.row-note {
  display: block; font-size: 12px; font-weight: 400; color: var(--muted-2);
  margin-top: 2px;
}

/* --- the work page ------------------------------------------------------ */
.work { padding: 24px; max-width: 1100px; margin: 0 auto; }
.work-hero { display: flex; gap: 24px; margin-bottom: 28px; }
.work-cover {
  width: 200px; aspect-ratio: 1 / var(--cover-ratio); object-fit: cover;
  border-radius: var(--radius); box-shadow: var(--shadow); flex: none;
}
.work-cover-none { display: grid; place-items: center; font-size: 3rem; background: var(--bg-card); }
.work-meta h1 { margin: 0 0 6px; font-size: 1.7rem; }
.work-credits { color: var(--muted); margin-bottom: 10px; }
.work-genres { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 14px; }
.work-badges { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }

/* The series banner. It sits BEHIND the hero and is faded into the page
   background from both edges, because it is decoration behind text: an
   unfaded banner puts the title over arbitrary artwork and the contrast is
   whatever the upstream happened to draw. `position: relative` on .work-hero
   and `z-index` on its children are what keep the text above it. */
.work-hero { position: relative; }
.work-hero > .work-cover, .work-hero > .work-meta { position: relative; z-index: 1; }
.work-backdrop {
  position: absolute; inset: -24px -24px auto -24px; height: calc(100% + 24px);
  background-size: cover; background-position: center 30%;
  border-radius: var(--radius); opacity: 0.20; pointer-events: none;
  /* Faded on all four edges, not just the bottom: the hero is narrower than
     the viewport, so a banner cut off at the container edge draws two hard
     vertical seams down the page. Two masks composited with `intersect` —
     `mask-composite` is what makes them AND rather than the last one winning. */
  -webkit-mask-image:
    linear-gradient(to bottom, rgba(0,0,0,0.95), transparent 94%),
    linear-gradient(to right, transparent, #000 12%, #000 78%, transparent);
  mask-image:
    linear-gradient(to bottom, rgba(0,0,0,0.95), transparent 94%),
    linear-gradient(to right, transparent, #000 12%, #000 78%, transparent);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* The blurb. Clamped to five lines by default — long enough to be a synopsis,
   short enough that the chapter list is still above the fold on a laptop. The
   full text is always in the DOM (find-in-page keeps working); only the
   rendering is clamped. */
.work-blurb { margin: 0 0 12px; max-width: 62ch; }
.work-blurb p { margin: 0; color: var(--muted); line-height: 1.55; white-space: pre-line; }
.work-blurb-clamped p {
  display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical;
  overflow: hidden;
}
.work-blurb-more {
  background: none; border: none; padding: 4px 0 0; cursor: pointer;
  color: var(--accent-2); font-size: var(--fs-sm); font-family: inherit;
}
.work-blurb-more:hover { text-decoration: underline; }

/* Who described this work. Quiet by design — it is provenance, not a credit. */
.work-provenance {
  display: flex; gap: 10px; flex-wrap: wrap; align-items: baseline;
  font-size: var(--fs-sm); color: var(--muted-2); margin-bottom: 14px;
}
.work-sourcelink { color: var(--muted); text-decoration: none; }
.work-sourcelink:hover { color: var(--accent-2); text-decoration: underline; }

/* "Catalogued, but nobody is hosting it." Deliberately a paragraph rather than
   an error box: nothing failed, and styling it as a failure sends readers to
   check their connection. */
.work-empty {
  border-left: 2px solid rgba(255,255,255,0.10); padding: 2px 0 2px 14px;
  max-width: 62ch;
}
.work-note-2 { font-size: var(--fs-sm); margin-top: 6px; }

/* The first-paint hero placeholder. Deliberately NOT `.work-hero` — see the
   note on `heroSkeleton` in js/work.js: that class is what `paint`'s rebuild
   check keys on, and wearing it makes the real page never get built. So the
   geometry is repeated here rather than inherited. */
.work-hero-skel { display: flex; gap: 24px; margin-bottom: 28px; }
.work-hero-skel .work-meta { flex: 1; min-width: 0; padding-top: 6px; }

/* The chapter list's containers. Referenced by js/work.js since it was written
   and never styled — `patch` queries `.work-chapters` on every streamed frame,
   so it has to exist, but nothing here needs it to look like anything beyond
   holding the volume blocks apart. */
.work-chapters { display: block; }
.vol-block + .vol-block { margin-top: 6px; }

/* ---- The failure state, and why it is not `.work-error` ----

   `.work-error` is centred, red, and forty pixels of padding — a whole-page
   dead end. A dropped stream is not that: the hero above it is still true, and
   the rows that arrived before the drop are still readable. So this renders as
   a note in the flow, in the same quiet shape as the "nobody is hosting it"
   block it sits beside, with the one control that helps. */
.work-failed { border-left-color: rgba(224, 108, 108, 0.35); }
.work-retry { margin-top: 10px; }
.work-error + .work-retry { text-align: center; }

/* The disabled Read button while the retrieval is still running. Sized off the
   button's own font so it lines up with the label at any scale. */
.spinner {
  display: inline-block; width: 0.85em; height: 0.85em; margin-right: 7px;
  vertical-align: -0.1em; border-radius: 50%;
  border: 2px solid currentColor; border-right-color: transparent;
  animation: spin 0.7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 2.4s; } }
.chip {
  font-size: 11px; padding: 2px 9px; border-radius: 999px;
  background: var(--bg-card); color: var(--muted);
}
.work-actions { display: flex; gap: 10px; }
.work-section { margin-bottom: 32px; }
.work-section h2 { font-size: 1.1rem; margin: 0 0 12px; }
.work-note { color: var(--muted-2); }
.vol-title { font-size: 0.95rem; color: var(--muted); margin: 18px 0 8px; }

/* The chapter list. A hole in the ordinals is deliberate — see js/work.js. */
.chapter-list { list-style: none; margin: 0; padding: 0; }
.chapter {
  border-bottom: 1px solid rgba(255,255,255,0.06);
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.chapter-open {
  flex: 1; display: flex; align-items: center; gap: 12px;
  padding: 11px 4px; color: inherit; text-decoration: none; min-width: 0;
}
.chapter-open:hover { background: rgba(255,255,255,0.03); }
.chapter-read .chapter-label { color: var(--muted-2); }
.chapter-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chapter-lang, .chapter-scanlator, .chapter-pos {
  font-size: 11px; color: var(--muted-2); flex: none;
}
.chapter-tick { color: var(--ok); flex: none; }
/* The "N hidden by language" counter beside the Chapters heading. Quiet, and
   deliberately NOT an alert: hiding is what the reader asked for. It exists so
   a short list is never mistaken for a short work. */
.work-filtered {
  margin-left: 10px; font-size: 11px; font-weight: 400; color: var(--muted-2);
}
/* ⚠ Deliberately quiet, and NOT a dimming of the row. On a fresh long-running
   work most chapters carry this, so anything that reads as "degraded" would
   describe a fully-present library as mostly broken. It marks a one-round-trip
   wait, nothing more. */
.chapter-pending {
  font-size: 10px; color: var(--muted-2); flex: none;
  border: 1px solid rgba(255,255,255,0.10); border-radius: 999px;
  padding: 1px 7px; letter-spacing: 0.02em;
}
.chapter-versions {
  font-size: 11px; color: var(--muted); background: none;
  border: 1px solid rgba(255,255,255,0.12); border-radius: 999px;
  padding: 2px 9px; cursor: pointer;
}
.chapter-versions.open { border-color: rgba(255,255,255,0.28); color: var(--text); }
.chapter-drawer { flex-basis: 100%; padding: 0 0 10px 4px; }

/* The source chooser: one row per version of this chapter. Reads left to right
   as tier → who → what differs, which is the order the questions come in. */
.version-list { display: flex; flex-direction: column; gap: 4px; }
.version {
  display: flex; gap: 10px; align-items: center; padding: 6px 8px;
  border-radius: 8px; background: var(--bg-card); color: inherit; text-decoration: none;
  font-size: 12px;
}
.version:hover { background: var(--bg-elev); }
.version-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.version-source { display: flex; align-items: baseline; gap: 6px; }
/* Which version the chapter row itself opens. A label, not a state: the row
   stays selectable, because opening a reader twice is harmless. */
.version-default {
  font-size: 10px; color: var(--muted-2); border: 1px solid rgba(255,255,255,0.14);
  border-radius: 999px; padding: 0 6px;
}
.version-release { color: var(--muted-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.version-chips { display: flex; gap: 4px; flex-wrap: wrap; justify-content: flex-end; flex: none; }
.version-chip {
  font-size: 10px; padding: 1px 6px; border-radius: 999px;
  background: rgba(255,255,255,0.07); color: var(--muted-2); white-space: nowrap;
}
.version-chip-lang { color: var(--muted); }
.version-chip-read { color: var(--ok); }
.version-go { flex: none; font-size: 11px; color: var(--accent-2); }
/* A version nobody is hosting. Kept in the list on purpose — "we know of this
   and cannot fetch it" is information — but inert and visibly so. */
.version-dead { opacity: 0.55; cursor: default; }
.version-dead .version-go { color: var(--muted-2); }
/* Listed, but no transport can redeem its locator from here (reading-model.md
   §3.4). Same inert treatment as a dead row — Read never opens it — with its own
   word, because "nobody holds this" and "nothing here can reach it" send whoever
   reads the list after different causes. */
.version-unreachable .version-go { font-style: italic; }
/* Bound to its work by title, not by an id (§3.3). Amber and dashed — a caution,
   not a failure: the version is real and readable, it may just be a different
   work with the same name. */
.version-chip-matched { color: #e8cf9a; background: rgba(224,180,90,0.14); }
.chapter-matched, .edition-matched {
  font-size: 10px; flex: none; padding: 1px 7px; border-radius: 999px;
  border: 1px dashed rgba(224,180,90,0.45); color: #e8cf9a;
}
/* The work-level "mark read" verb (§6). Quiet like the sources toggle beside it;
   the "on" state borrows the read tick's colour. */
.chapter-mark {
  font-size: 11px; color: var(--muted); background: none; font-family: inherit;
  border: 1px solid rgba(255,255,255,0.12); border-radius: 999px;
  padding: 2px 9px; cursor: pointer; flex: none;
}
.chapter-mark:hover:not(:disabled) { color: var(--text); border-color: rgba(255,255,255,0.28); }
.chapter-mark.on { color: var(--ok); border-color: rgba(123,182,97,0.4); }
.chapter-mark:disabled { opacity: 0.5; cursor: default; }
/* Where the append-only tail begins: everything below arrived from an explicit
   "Look for more", after the reader was already reading the list. */
.version-sep {
  font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--muted-2); padding: 8px 2px 2px;
}
.version-more { display: flex; align-items: center; gap: 8px; padding: 8px 2px 0; }
.chapter-more {
  font-size: 11px; color: var(--muted); background: none;
  border: 1px solid rgba(255,255,255,0.12); border-radius: 999px;
  padding: 3px 10px; cursor: pointer; font-family: inherit;
}
.chapter-more:hover:not(:disabled) { color: var(--text); border-color: rgba(255,255,255,0.28); }
.chapter-more:disabled { opacity: 0.5; cursor: default; }
.version-more-note { font-size: 11px; color: var(--muted-2); }

/* Source tier. Colour carries the same order the ranking does, so a glance
   down the list reads as a gradient rather than as decoration. */
.chapter-tier, .edition-tier, .version-tier {
  font-size: 10px; padding: 1px 7px; border-radius: 999px; flex: none;
  background: rgba(255,255,255,0.07); color: var(--muted);
}
.tier-local { background: rgba(123,182,97,0.22); color: #b6e0a3; }
.tier-ipfs { background: rgba(75,184,201,0.22); color: #9fe0ea; }
.tier-torrent { background: rgba(224,180,90,0.20); color: #e8cf9a; }
/* Between torrent and locator, matching the tier order: no wait before page
   one, but a round-trip per page and no content addressing. */
.tier-pages { background: rgba(206,142,86,0.20); color: #ecc09a; }
.tier-locator { background: rgba(224,108,108,0.20); color: #f0aeae; }
.tier-none { background: rgba(255,255,255,0.06); color: var(--muted-2); }

.edition-list { display: flex; flex-direction: column; gap: 8px; }
.edition {
  display: flex; align-items: center; gap: 12px; padding: 10px 12px;
  background: var(--bg-card); border-radius: 10px;
}
.edition-format {
  font-size: 10px; letter-spacing: 0.06em; padding: 2px 7px; border-radius: 4px;
  background: rgba(255,255,255,0.08); color: var(--muted); flex: none;
}
.edition-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.edition-pub, .edition-elsewhere { font-size: 11px; color: var(--muted-2); }
.edition-lang, .badge-lang {
  font-size: 10px; font-weight: 600; letter-spacing: .04em; padding: 1px 6px;
  border-radius: 4px; border: 1px solid var(--border, #332c25); color: var(--muted, #b8aca0);
}
.edition-other-heading { margin: 18px 0 8px; font-size: 14px; color: var(--muted, #b8aca0); font-weight: 600; }
.search-lang-note { margin: 14px 0 0; font-size: 12px; color: var(--muted-2); }
.btn-small { font-size: 12px; padding: 4px 12px; }

/* --- library / search / board -------------------------------------------- */
.library, .search { padding: 24px; max-width: 1400px; margin: 0 auto; }
.library-head, .search-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; margin-bottom: 20px; flex-wrap: wrap;
}
.library-filter {
  background: var(--bg-card); border: 1px solid rgba(255,255,255,0.1);
  color: var(--fg); border-radius: 999px; padding: 7px 16px; min-width: 220px;
}
.board-empty, .search-empty, .library-empty {
  max-width: 560px; margin: 60px auto; text-align: center; color: var(--muted);
}
.board-empty h2, .library-empty h2 { color: var(--fg); }
.board-empty-note, .search-empty-note { font-size: 13px; color: var(--muted-2); }
.board-loading, .search-loading, .library-loading, .work-loading {
  padding: 40px; text-align: center; color: var(--muted-2);
}
.board-error, .library-error, .work-error { padding: 40px; text-align: center; color: var(--err); }

/* ===========================================================================
   The Suwayomi-shaped rework
   ---------------------------------------------------------------------------
   Everything below was written for meta-read's OWN markup. The block above it
   is meta-watch's stylesheet, ported verbatim and never reconciled — which is
   why the shell rendered with browser defaults until this pass: the markup and
   the stylesheet were using two different class vocabularies.
   =========================================================================== */

/* ---- Orphans: markup that existed with no rule at all ---- */

/* index.html uses .modal / .modal-backdrop; the ported sheet styles
   .identity-modal*. With no `.hidden` rule either, the profile panel rendered
   inline, in page flow, on every single page load. */
.modal-backdrop {
  position: fixed; inset: 0; z-index: 199;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(2px);
}
.modal {
  position: fixed; z-index: 200;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(520px, calc(100vw - var(--sp-6)));
  max-height: min(80vh, 720px); overflow: auto;
  padding: var(--sp-5);
  background: var(--bg-elev);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

/* work.js and identity.js both ask for .btn-secondary; only a scoped
   `.identity-actions .btn-secondary` existed, so the work page's Save button
   fell through to a bare .btn. */
.btn-secondary {
  background: var(--bg-elev);
  border: 1px solid var(--border-strong);
  color: var(--fg);
}
.btn-secondary:hover { border-color: var(--accent); }

/* The ported focus/lift rules target .card-poster; this markup emits
   .card-art, so cards had no hover and no focus treatment whatsoever. */
.card { display: block; }
.card-art { transition: transform .16s ease, box-shadow .16s ease; }
.card:hover .card-art,
.card:focus-visible .card-art {
  transform: translateY(-4px);
  box-shadow: 0 10px 26px rgba(0, 0, 0, .5);
}
.card:focus-visible { outline: none; }
.card:focus-visible .card-art { outline: 2px solid var(--focus); outline-offset: 2px; }
.card-title {
  margin-top: var(--sp-2); font-size: var(--fs-sm); font-weight: 600;
  line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2;
  -webkit-box-orient: vertical; overflow: hidden;
}
.card-sub { margin-top: 2px; font-size: var(--fs-xs); color: var(--muted-2); }

/* ---- View shell ---- */

.app { padding: 0 0 90px; min-height: 70vh; }
.view { padding: var(--sp-6) var(--sp-6) 0; max-width: 1500px; }
.view-head {
  display: flex; align-items: center; gap: var(--sp-4);
  margin-bottom: var(--sp-5); flex-wrap: wrap;
}
.view-head h1 { margin: 0; font-size: var(--fs-2xl); font-weight: 700; letter-spacing: -.2px; }
.view-head .spacer { flex: 1; }
.view-sub { margin: calc(var(--sp-4) * -1) 0 var(--sp-5); color: var(--muted-2); font-size: var(--fs-sm); }

/* One input treatment for every view header that has one. */
.field {
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--fg);
  border-radius: 999px;
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-md); font-family: inherit;
  min-width: 260px;
}
.field:focus { outline: none; border-color: var(--accent); }
.field::placeholder { color: var(--muted-2); }

/* The designed empty state. Used by every surface that can legitimately have
   nothing in it — which, on a fresh stack with no feeders, is most of them.
   An empty panel reads as breakage; this reads as an answer. */
.empty {
  display: grid; place-items: center; gap: var(--sp-2);
  padding: var(--sp-7) var(--sp-4);
  text-align: center; color: var(--muted-2);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
}
.empty-mark { font-size: 30px; opacity: .5; }
.empty-title { color: var(--fg); font-size: var(--fs-lg); font-weight: 600; }
.empty-note { max-width: 46ch; line-height: 1.5; font-size: var(--fs-sm); }

/* ---- Home ---- */
/* ⚠ .row-title, .board, .strip and .grid are reconcile anchors — home.js and
   search.js find and key off them. Restyle, never rename. */
.board { padding: var(--sp-6) 0 0; }
.row { margin-bottom: var(--sp-6); }
.row-title {
  margin: 0 0 var(--sp-3);
  padding-inline: var(--sp-6);      /* was missing entirely, so headings sat
                                       left of the cards they labelled */
  font-size: var(--fs-lg); font-weight: 700;
}
.row-note { font-size: var(--fs-xs); font-weight: 400; color: var(--muted-2); margin-left: var(--sp-2); }
.strip {
  display: flex; gap: var(--sp-4); overflow-x: auto;
  padding: var(--sp-1) var(--sp-6) var(--sp-3);
  scroll-snap-type: x proximity; scrollbar-width: none;
}
.strip::-webkit-scrollbar { display: none; }
.strip > .card { flex: 0 0 var(--card-w); width: var(--card-w); scroll-snap-align: start; }
.board-loading, .board-error, .board-empty { padding: var(--sp-7) var(--sp-6); color: var(--muted-2); }
.board-empty-note { font-size: var(--fs-sm); margin-top: var(--sp-2); }

/* ---- Library / Search ---- */
.library, .search { padding: var(--sp-6); max-width: 1500px; }
.library-head, .search-head {
  display: flex; align-items: center; gap: var(--sp-4);
  margin-bottom: var(--sp-5); flex-wrap: wrap;
}
.library-head h1, .search-head h1 {
  margin: 0; font-size: var(--fs-2xl); font-weight: 700; letter-spacing: -.2px;
}
.library-filter { margin-left: auto; }
.library-loading, .library-empty, .library-error,
.search-loading, .search-empty { padding: var(--sp-7) 0; color: var(--muted-2); }
.library-empty, .search-empty { text-align: center; }
.search-empty-note, .library-empty-note {
  font-size: var(--fs-sm); color: var(--muted-2); margin-top: var(--sp-2);
}

/* ---- Sources ---- */
.sources-list { display: grid; gap: var(--sp-3); max-width: 720px; }
.source-row {
  display: flex; align-items: center; gap: var(--sp-4);
  padding: var(--sp-4);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
.source-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto;
  background: var(--muted-2);
}
.source-dot.ok { background: var(--ok); box-shadow: 0 0 0 3px rgba(123, 182, 97, .16); }
.source-meta { flex: 1; min-width: 0; }
.source-name { font-weight: 600; }
.source-axes { margin-top: 2px; font-size: var(--fs-xs); color: var(--muted-2); }
.source-kind { font-size: var(--fs-xs); color: var(--muted-2); }

/* ---- Account ---- */
.account { padding: var(--sp-6); max-width: 720px; }
.account-panel {
  padding: var(--sp-5);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
}

/* The rail carries an Account entry, so the standalone identity chip would be a
   second, differently-styled control for the same thing sitting right under it.
   identity.js still manages the element (and its modal still opens from the
   Account page's buttons) — it is just not a rail affordance any more. */
.rail .identity-chip { display: none; }

/* The in-flight query indicator, as a rail row rather than a topbar pill. */
.rail .qx-indicator {
  display: flex; align-items: center; gap: var(--sp-2);
  width: 100%; padding: var(--sp-2) var(--sp-3);
  background: none; border: 1px solid var(--border);
  border-radius: 999px; color: var(--muted);
  font: inherit; font-size: var(--fs-xs); cursor: pointer;
}
.rail .qx-indicator:hover { border-color: var(--accent); color: var(--fg); }

/* Same treatment for the language chip: a rail row, not a topbar pill. */
.rail .lang-pref-mini {
  display: flex; align-items: center; gap: var(--sp-2);
  width: 100%; padding: var(--sp-2) var(--sp-3);
  background: none; border: 1px solid var(--border);
  border-radius: 999px; color: var(--muted);
  font-size: var(--fs-xs);
}
.rail .lang-pref-mini:hover { border-color: var(--accent); color: var(--fg); }
.rail .lang-pref-mini-globe { font-size: 13px; line-height: 1; }

/* <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);
}

/* ---- keyboard focus and reduced motion ------------------------------------
   `:where` keeps the specificity at zero, so the cards and rows that draw their
   own focus treatment (`.card:focus`, `.raw-row:focus`) are not overridden.
   `:focus-visible` shows the ring for the keyboard and not for a mouse click. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent-2);
  outline-offset: 2px;
}

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