/* ============================================================
   Plex Hub design system — shared by the friend site + admin.
   Dark, poster-forward, one gold accent. Mobile-first.
   ============================================================ */
/* ---------------- theming ----------------
   Every colour a theme can change lives here. Themes work by REDEFINING these
   on :root — nothing else needs to know which theme is active.

   The two rules that make a light theme actually possible:
   1. `--ink` / `--ink-*` are "a wash of the FOREGROUND colour" (white on dark,
      black on light). Hardcoded rgba(255,255,255,.06) hovers looked invisible
      or filthy on a light background; these flip automatically.
   2. `--scrim` is "a wash of the BACKGROUND colour", for text over posters —
      that stays dark in every theme, because poster art is always vivid. */
:root{
  /* Netflix canvas: NEUTRAL dark — no blue cast anywhere. #141414 is the
     Netflix base; surfaces step up in plain greys, text greys are neutral
     (#b3b3b3 is Netflix's own dim grey). */
  --bg:#141414; --bg2:#181818; --surface:#1f1f1f; --card:#242424; --raise:#2f2f2f;
  --ink-1:rgba(255,255,255,.03); --ink-2:rgba(255,255,255,.05); --ink-3:rgba(255,255,255,.08);
  --ink-4:rgba(255,255,255,.12); --ink-5:rgba(255,255,255,.22); --ink-6:rgba(255,255,255,.35);
  --scrim-1:rgba(0,0,0,.5); --scrim-2:rgba(0,0,0,.68); --scrim-3:rgba(0,0,0,.82);
  --line:var(--ink-3); --line2:rgba(255,255,255,.14);
  /* #808080 is Netflix's own dim grey but it measured 3.93:1 on --card, under
     the 4.5 bar, for .empty and the "On the way" label. #8c8c8c is 4.62 on
     --card and 4.90 on --surface. */
  --ptw:132px; --txt:#ffffff; --dim:#b3b3b3; --faint:#8c8c8c;
  /* ===== R6 chunk G14.2 — the type scale =====
     Measured on 2026-08-10 by walking every family screen at 375px and reading
     the computed styles: **21 distinct font sizes on screen at once**, 24 in
     this stylesheet — 13px, 13.5px and 14px all in use, 9.5/10/10.5/11 all in
     use. Sizes half a pixel apart cannot be told apart on a phone. They do not
     read as care; they read as nothing, while quietly guaranteeing that two
     things which ought to match do not.

     Nine steps, each at least a pixel from its neighbour — because a step
     nobody can see is not a step. (The first draft of this scale had 13.5 and
     14 side by side; its own test caught it, which is the whole argument for
     writing the test.) Everything on both surfaces uses one of them, and
     `test_type_scale.py` fails on a raw pixel size anywhere else. */
  --fs-micro:11px;   /* badges, timestamps, the smallest legible thing */
  --fs-meta:12.5px;  /* secondary lines under a title */
  --fs-small:13.5px; /* dense rows, chips, buttons, list text */
  --fs-read:15px;    /* the reading size — paragraphs, descriptions */
  --fs-title:17px;   /* a card or section title */
  --fs-head:20px;    /* a screen heading */
  --fs-big:26px;     /* a number worth looking at */
  --fs-hero:30px;    /* the one thing on the screen */
  --fs-giant:44px;   /* a poster-sized number */
  /* R6 chunk G14.1 — the smallest a finger may be asked to hit. Apple says 44,
     Material says 48dp; 44 is the one this design can carry. */
  --tap:44px;
  /* Skeleton row height. Set per-use inline by phWait(); declared here so the
     token is defined rather than only ever arriving from JavaScript. */
  --phskh:58px;

  --accent:#e5a00d; --accent2:#f6c744; --accent-ink:#191919;
  /* kept as aliases so the ~200 existing --gold/--gold2 uses keep working */
  --gold:var(--accent); --gold2:var(--accent2);
  /* Accent-coloured TEXT. On dark surfaces the accent already passes, so this
     is simply the accent; the light theme overrides it with a darker value. */
  --accent-text:var(--accent);
  --gold-grad:linear-gradient(135deg,var(--accent2),var(--accent) 60%,color-mix(in srgb,var(--accent) 70%,#000));
  --accent-soft:color-mix(in srgb,var(--accent) 12%,transparent);
  --ok:#2ec27e; --bad:#ff5d5d; --info:#58a6ff;
  /* --bad is a FILL as well as an ink: it backs the delete button and the
     failure badge with white on top. As TEXT it is fine here (6.12:1 on the
     page) and was never redefined for light, where it lands at 3.01:1 on a
     card. Same split the accent already has - the fill keeps its value, text
     gets its own token - so nothing that is painted ON --bad has to move. */
  --bad-text: var(--bad); --ok-text: var(--ok);
  /* The LIVE dot and the record button. A single literal cannot serve both
     themes here: #ff3b5c is 4.90 on a dark card and 3.48 on a light one, and
     #b3261e is the exact mirror - 6.54 light, 2.61 dark. So it is a token. */
  --live-red:#ff3b5c;
  --r:14px; --r-sm:10px;
  --shadow:0 10px 30px rgba(0,0,0,.45);
  --topbar-1:rgba(20,20,20,.97); --topbar-2:rgba(20,20,20,.82); --nav-bg:rgba(14,17,22,.97);
  /* Safe-area insets, defined ONCE (R5-I2). Consumers use var(--sa-*), never
     env() directly — env() is not settable from script, so inlining it made
     notch behaviour impossible to test. Left/right matter in LANDSCAPE, which
     is exactly when a full-screen video meets the notch. */
  --sa-t:env(safe-area-inset-top,0px);
  --sa-r:env(safe-area-inset-right,0px);
  --sa-b:env(safe-area-inset-bottom,0px);
  --sa-l:env(safe-area-inset-left,0px);
  --nav-h:62px;
  /* Shell revamp: the ONE number for "how tall is the bottom bar". Same in
     home and section mode; anything floating above the bar anchors to it. */
  --ph-bar-h:calc(var(--nav-h) + var(--sa-b));
  /* Chunk 36 - the height of the top bar, so anything parking under it can
     stop guessing. This is the PHONE value and the honest default; hub.js
     overwrites it with the measured height on load and on resize, because the
     same bar is 56px in a desktop window and the music browse row was sitting
     6px underneath it with its chips clipped. Defined here rather than left to
     a var() fallback: a token that only exists at runtime reads as a typo, and
     the token guard is right to say so. */
  --ph-top-h:calc(var(--sa-t) + 50px);
  /* The album header's colour. Set from the cover's own pixels at runtime
     (muHeroTint); this is what it looks like before that happens, or when
     the cover has no colour to give — a black-and-white sleeve, no artwork.
     Defined per theme so a light theme never inherits a dark header. */
  --mu-hero:#232b3a;
  color-scheme:dark;
}

/* ---- LIGHT ----
   Not an inversion. Surfaces get LIGHTER as they come forward (the opposite of
   dark), shadows soften to near-nothing, and the accent darkens so gold text
   stays readable on white — bright gold on white is unreadable. */
:root[data-theme="light"]{
  --bg:#f6f7f9; --bg2:#ffffff; --surface:#ffffff; --card:#ffffff; --raise:#eef1f5;
  --ink-1:rgba(16,20,28,.03); --ink-2:rgba(16,20,28,.05); --ink-3:rgba(16,20,28,.10);
  --ink-4:rgba(16,20,28,.14); --ink-5:rgba(16,20,28,.24); --ink-6:rgba(16,20,28,.38);
  --line:var(--ink-3); --line2:rgba(16,20,28,.16);
  /* Audited every text node in every view against its real background (R5-G10).
     --faint was the big one: at #8a94a3 it measured 2.86:1 on #fff, and it is
     the colour of .sub, .empty, .k and .s — exactly the washed-out text that
     was reported. #66707f measures 5.01 on #fff and 4.68 on #f6f7f9.

     --accent/--accent2 are deliberately NOT darkened. They are mostly a FILL
     (with a hardcoded #191919 ink on .btn), and darkening them cut that
     button's ink contrast from 7.24:1 to 5.45:1 — fixing text nobody reads by
     degrading text everybody does. Accent-coloured TEXT gets its own token
     below instead. */
  --txt:#11151c; --dim:#59626f; --faint:#66707f;
  /* --accent-ink is the ink that sits ON the accent fill, and in light mode it
     was near-white (#fffdf6) on a gold that light mode had already darkened:
     3.65:1, under AA, in all eleven places it is painted — the selected EPG
     day, the changed-setting pill, the sleep-timer badge, the share tick, the
     download and queue selection ticks. Found on the Live guide's "Today"
     pill; it was never a Live bug. Gold is a light fill, so it takes dark ink:
     the same #191919 the dark theme has always used on it, 4.74:1 here and
     unchanged there. Safe on the two ticks because an unselected tick has no
     glyph to reveal. The five other accents (ocean/forest/rose/violet, and
     Netflix mode) declare their own ink and are all already 4.79+. */
  --accent:#b3790a; --accent2:#d99b12; --accent-ink:#191919;
  /* Accent as TEXT on a light surface. #9e6400 was measured against the page
     (4.58 on #f6f7f9) and a card (4.91 on #fff) - but not against --raise, the
     colour of a chip, where it came to 4.33 and missed AA. Measured live in
     chunk 37 on the Backup sub-tabs. #8c5800 clears it everywhere it is
     actually painted: 5.28 on --raise, 5.58 on the page, 5.98 on a card.
     Text only, so this does not touch the accent FILL that .btn's ink was
     balanced against. */
  --accent-text:#8c5800;
  /* Failure text on a light surface. #ff5d5d is 2.81 on the page, 3.01 on a
     card and 2.66 on --raise: unreadable in all three. #b3261e clears AA with
     room everywhere it is painted (6.10 / 6.54 / 5.77) and is still plainly
     red rather than brown. Chosen with margin on purpose - the accent's own
     near-miss was a value that passed on two surfaces and failed on --raise. */
  --bad-text:#b3261e;
  /* And the success colour, which is the same story: #2ec27e is a FILL (the
     health dot, the shield) and as text on a light card it measures 2.30:1 -
     WORSE than the red was. Found by the same panel on its first honest run.
     #177046 clears AA everywhere it is painted (5.69 / 6.10 / 5.38). */
  --ok-text:#177046;
  --live-red:#b3261e;
  --shadow:0 6px 20px rgba(16,20,28,.10);
  --topbar-1:rgba(246,247,249,.97); --topbar-2:rgba(246,247,249,.82); --nav-bg:rgba(246,247,249,.97);
  --mu-hero:#dfe4ec;
  color-scheme:light;
}

/* ---- accent variants (work in both light and dark) ---- */
:root[data-accent="ocean"]{ --accent:#2f8fd6; --accent2:#5fb4ef; --accent-ink:#04121d; }
:root[data-theme="light"][data-accent="ocean"]{ --accent:#1f6fa8; --accent2:#2f8fd6; --accent-ink:#f4fbff; }
:root[data-accent="forest"]{ --accent:#3faa72; --accent2:#65cf95; --accent-ink:#04150d; }
:root[data-theme="light"][data-accent="forest"]{ --accent:#2c7f53; --accent2:#3faa72; --accent-ink:#f3fff8; }
:root[data-accent="rose"]{ --accent:#e0567f; --accent2:#f584a6; --accent-ink:#1d0710; }
:root[data-theme="light"][data-accent="rose"]{ --accent:#b83a61; --accent2:#e0567f; --accent-ink:#fff5f8; }
:root[data-accent="violet"]{ --accent:#8b6ce8; --accent2:#ab93f2; --accent-ink:#0d0720; }
:root[data-theme="light"][data-accent="violet"]{ --accent:#6a48cc; --accent2:#8b6ce8; --accent-ink:#f8f5ff; }

/* ---- Full Netflix experience ---- opt-in from Settings. Overrides whatever
   accent/theme is chosen with the classic Netflix red; every gold/soft token
   derives from --accent, so this recolours the whole app in one place.
   (NB: never write star-slash sequences like "--gold*" + "/" inside comments —
   it terminated this comment early and ATE the rule below.) */
:root[data-netflix="1"]{ --accent:#e50914 !important; --accent2:#f6121d !important; --accent-ink:#ffffff !important; }
/* extra mile: active filter pills go Netflix-red instead of the neutral white */
:root[data-netflix="1"] .libseg button.on,
:root[data-netflix="1"] .libchip.on{ background:var(--accent); color:#fff; border-color:var(--accent); }

/* ---- OLED: true black, for phones ---- */
:root[data-theme="oled"]{
  --bg:#000000; --bg2:#050506; --surface:#0b0b0d; --card:#101012; --raise:#191a1d;
  --line:rgba(255,255,255,.10);
  --shadow:0 10px 30px rgba(0,0,0,.8);
  --topbar-1:rgba(0,0,0,.97); --topbar-2:rgba(0,0,0,.82); --nav-bg:rgba(0,0,0,.97);
  color-scheme:dark;
}

/* Theme switches shouldn't strobe. Transition only the properties that carry
   colour, and never inside the video player (repainting a playing <video>'s
   chrome mid-transition causes visible tearing). */
:root[data-theme-anim] body,
:root[data-theme-anim] .card,
:root[data-theme-anim] .murow,
:root[data-theme-anim] .bnav,
:root[data-theme-anim] .mubar{ transition:background-color .18s ease, color .18s ease, border-color .18s ease; }
.vp, .vp *{ transition:none !important; }
/* The container had NO rule, so these `display:flex` buttons stacked as blocks —
   five accent colours running 190px down the Settings page instead of sitting in
   a row. */
.swatches{ display:flex; flex-wrap:wrap; align-items:center; gap:10px; }
.swatch{ width:38px; height:38px; border-radius:50%; padding:0; cursor:pointer;
  border:2px solid transparent; background:transparent; display:flex; align-items:center; justify-content:center;
  transition:border-color .15s, transform .12s; }
.swatch i{ display:block; width:26px; height:26px; border-radius:50%; }
.swatch:hover{ transform:scale(1.06); }
.swatch.on{ border-color:var(--txt); }
.swatch[data-acc="gold"]   i{ background:linear-gradient(135deg,#f6c744,#e5a00d); }
.swatch[data-acc="ocean"]  i{ background:linear-gradient(135deg,#5fb4ef,#2f8fd6); }
.swatch[data-acc="forest"] i{ background:linear-gradient(135deg,#65cf95,#3faa72); }
.swatch[data-acc="rose"]   i{ background:linear-gradient(135deg,#f584a6,#e0567f); }
.swatch[data-acc="violet"] i{ background:linear-gradient(135deg,#ab93f2,#8b6ce8); }

*{ box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
html{ -webkit-text-size-adjust:100%; }
/* The DOCUMENT is the scroll container. Earlier `html{height:100%}` +
   `overflow-x:hidden` on <html> made Android WebView + desktop treat <html> as
   a fixed-height scroller and swallow wheel/touch scrolling. Only the body
   locks the x-axis + rubber-band; vertical scroll stays native everywhere. */
/* R7 U5 — the bar's height is now a VARIABLE, and body owns it.
   The bar used to be one fixed row, so `calc(var(--nav-h) + var(--sa-b))` was
   spelled out at ten different anchor points. It is now a tab row PLUS an
   optional control line, so that expression is no longer the answer and ten
   copies of a wrong answer is nine too many. --ph-slot-h is the control line
   (0 unless a section actually put something in the slot) and --ph-bar-h is
   the whole bar; everything that floats above the bar reads --ph-bar-h and
   follows for free.
   It is declared HERE and not on :root because a custom property's var()s
   resolve where the property is DECLARED — --ph-slot-h is set on body by the
   mode class, so --ph-bar-h has to be declared on body to see it. :root keeps
   the single-row value as the honest fallback for a page with no mode class. */
body{ margin:0; padding:0; background:var(--bg); color:var(--txt);
  font:15px/1.45 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Inter,sans-serif;
  width:100%; max-width:100%; overflow-x:hidden; overscroll-behavior-y:none;
  position:relative; min-height:100vh;
  --ph-slot-h:0px;
  --ph-bar-h:calc(var(--nav-h) + var(--sa-b) + var(--ph-slot-h));
  padding-bottom:calc(var(--ph-bar-h) + 8px); }
/* The video scrubber is a drag control with no scroll container of its own, so
   it has to claim the horizontal axis by hand — dragging the seek bar must never
   turn into a page scroll.
   `.hrow` and `.fchips` used to be in this list too, and it cost the page its
   vertical scroll. touch-action INTERSECTS down the ancestor chain, so a finger
   landing on any tile inside a rail was permitted pan-x and nothing else: the
   vertical swipe was dropped and the home page simply stopped scrolling once
   your thumb was over "New this week". Neither rail ever needed the opt-in —
   `.hrow` (~line 458) and `.fchips` (~line 1509) both carry `overflow-x:auto`,
   which is what actually pans them sideways. */
.vp .seek{ touch-action:pan-x; }
main,.view,.card{ max-width:100%; }
h1,h2,h3{ margin:0 0 10px; font-weight:700; letter-spacing:.2px; }
h2{ font-size:var(--fs-title); } h3{ font-size:var(--fs-read); }
a{ color:var(--accent-text); text-decoration:none; }
::selection{ background:rgba(229,160,13,.35); }
::-webkit-scrollbar{ height:6px; width:8px; } ::-webkit-scrollbar-thumb{ background:#2a3140; border-radius:4px; }

/* ---------- top bar ---------- */
.topbar{ position:sticky; top:0; z-index:40; display:flex; align-items:center; gap:10px;
  padding:calc(var(--sa-t) + 10px) 16px 10px;
  /* Netflix top bar: the page colour fading translucent, no border line.
     This used to be a literal rgba(20,20,20,...) in every theme, so in light
     mode a near-black bar carried --txt (#11151c) and --gold — the brand read
     1.01:1, effectively invisible. Driven off the theme now. */
  background:linear-gradient(180deg,var(--topbar-1),var(--topbar-2)); backdrop-filter:blur(14px);
  border-bottom:0; }
.brand{ display:flex; align-items:center; gap:9px; font-weight:800; font-size:var(--fs-title); letter-spacing:.3px; }
.brand .mark{ width:30px; height:30px; border-radius:8px; flex:0 0 30px; }
.brand .hub{ color:var(--accent-text); }
.topbar .sp{ flex:1; }
.avatar{ width:32px; height:32px; border-radius:50%; object-fit:cover; border:2px solid var(--gold);
  background:var(--card); }
.avatar.ph{ display:flex; align-items:center; justify-content:center; font-weight:700; color:var(--gold); }

/* ---------- bottom nav ---------- */
.bnav{ position:fixed; left:0; right:0; bottom:0; z-index:40; display:flex; flex-wrap:wrap; align-content:stretch;
  height:var(--ph-bar-h); padding-bottom:var(--sa-b);
  background:var(--nav-bg); backdrop-filter:blur(16px); border-top:1px solid var(--line); }
/* flex-basis is 1px and NOT 0, and that one pixel is the whole layout.
   Flex line-breaking measures each item at its flex BASE size, before any
   growing: at basis 0 a tab's hypothetical width is 0, so `slot(100%) + 0` is
   not wider than the bar and every tab was packed onto the slot's own line —
   where the slot is inflexible, leaving nothing to distribute. The result was
   a bar of 0px-wide tabs: present, opaque, focusable, and completely
   invisible. (Found by driving it at 390px; every unit assertion passed.)
   Any non-zero basis breaks the line, and 1px is small enough that even all
   nine tabs (9px) can never force a THIRD line. They still divide the row
   equally, because grow does the rest. */
.bnav .bi{ flex:1 1 1px; min-width:0; height:var(--nav-h); overflow:hidden; white-space:nowrap; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px;
  color:var(--faint); font-size:var(--fs-micro); font-weight:600; cursor:pointer; user-select:none; position:relative; visibility:visible;
  transition:opacity .16s ease, transform var(--ph-press), background-color var(--ph-press), color var(--ph-press), visibility 0s linear 0s; }
.bnav .bi .ic{ font-size:var(--fs-head); line-height:1; }
/* The ACTIVE nav item is accent-coloured TEXT, so it takes --accent-text.
   On --gold2 it measured 2.27:1 once the nav stopped being hardcoded dark. */
.bnav .bi.on{ color:var(--accent-text); }
.bnav .bi .bdg{ position:absolute; top:6px; right:calc(50% - 20px); min-width:16px; height:16px; padding:0 4px;
  border-radius:8px; background:var(--gold); color:#141414; font-size:var(--fs-micro); font-weight:800;
  display:flex; align-items:center; justify-content:center; }

/* ---------- bottom bar modes (shell revamp chunk 1, redesigned R7 U5) ----------
   Pierre, twice: "I kinda don't like how it MORPHS into this thing related on
   the page you're on. And then you have to go back to the home page to be able
   to go to any other page" — and, about the website, "the bottom bar gets
   overly filled … it's hard to manage."

   Both sentences were the same rule, and this is it now:

       THE TAB ROW IS ALWAYS THE TAB ROW.

   It used to fold. Entering any section set opacity:0 / visibility:hidden /
   flex-grow:0 on every `.bi`, so Netflix was two taps and a page load away
   from Spotify: back to Home first, then out again. The desktop shell had
   already refused that trade (desktop.css un-hides the tabs in a sidebar,
   where there is room) — the phone/website row is now held to the same rule,
   with the section's controls given a LINE OF THEIR OWN above the tabs
   instead of the tabs' space.

   So the bar is a wrapping flex row of at most two lines:

       line 1   .bslot   the section's controls   — only when it has any
       line 2   .bi …    the tabs                 — always, every page

   The back-to-Home button is retired here (it is `display:none` below, kept in
   the markup for the offline/no-script path): Home is a LOCKED tab that can
   never be switched off, so a second, wider button that also means "Home" was
   72px of the row spent saying something the row already said.

   The height is no longer constant, which is why it became a variable rather
   than an assumption: --ph-slot-h is 0 on Home and on any section whose slot
   came back empty, and --ph-bar-h = nav + safe area + slot. Everything that
   floats above the bar reads --ph-bar-h, so it follows without being told.
   Snapping (not animating) that height is deliberate and is D1's rule: the
   geometry swaps instantly and only opacity crossfades, because .bnav is
   position:fixed over a 16px backdrop-blur and animating its box re-composites
   the blur every frame. */
.bnav .bback{ order:-1; display:none; flex:0 0 0px; min-width:0; overflow:hidden; padding:0; margin:0; border:0; background:none;
  flex-direction:column; align-items:center; justify-content:center; gap:3px; opacity:0; pointer-events:none; visibility:hidden;
  color:var(--accent-text); font:inherit; font-size:var(--fs-micro); font-weight:700; cursor:pointer; white-space:nowrap;
  transition:opacity .16s ease .04s, transform var(--ph-press,120ms), visibility 0s linear .16s; }
.bnav .bback .bback-ic{ display:flex; align-items:center; gap:0; }
.bnav .bback .bback-ic .ic-svg:first-child{ transform:scaleX(-1); margin-right:-3px; }  /* chevright flipped = back */
.bnav .bback:active{ transform:scale(.92); }
/* The slot takes the WHOLE first line (flex-basis:100% is what forces the
   wrap) and is zero-height until a section fills it, so Home and an empty
   slot are the plain 62px tab row they have always been. */
.bnav .bslot{ order:-1; flex:0 0 100%; height:var(--ph-slot-h); min-width:0; overflow:hidden; opacity:0; visibility:hidden; display:flex; align-items:center; gap:8px;
  padding:0; transition:opacity .16s ease .06s, visibility 0s linear .16s; }
.bnav .bslot-title{ display:flex; align-items:center; gap:8px; color:var(--txt); font-weight:700; font-size:var(--fs-small); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.bnav .bslot-title .ic-svg{ color:var(--accent-text); flex:0 0 auto; }
/* transition-delay lists below are POSITIONAL against each rule's transition
   list above — .bslot is opacity,visibility. Adding a property up there means
   adding a delay here, in the same position. */
/* A section that HAS controls gets the line; one whose renderer returned
   nothing keeps --ph-slot-h at 0 and the bar stays one row. `:empty` is the
   honest test: PHBar.paint() writes '' into the slot rather than padding it
   with a title the lit tab is already showing. */
body.ph-bar-section{ --ph-slot-h:46px; }
body.ph-bar-section:has(#bnav-slot:empty){ --ph-slot-h:0px; }
body.ph-bar-section .bnav .bslot{ opacity:1; visibility:visible; padding:0 12px 0 12px; transition-delay:.06s,0s; }
body.ph-bar-section:has(#bnav-slot:empty) .bnav .bslot{ opacity:0; visibility:hidden; }
/* No mode class at all (script failed before boot): the tab row, which is now
   also what every other mode shows. */

/* ---------- section slot controls (shell revamp chunk 2) ----------
   Round icon buttons and wide "icon + word" buttons, all >=44px tall via the
   bar's own height so the visible 38px control keeps a full-height hit area.
   The row scrolls sideways if a section ever wants more than fits — never
   wraps, never grows the bar. */
/* No touch-action here: overflow-x:auto already pans it sideways, and pinning
   pan-x would forbid a vertical swipe that starts on the slot (see the note by
   `.vp .seek`). */
.bnav .bslot-ctl{ display:flex; align-items:center; gap:6px; width:100%; height:100%; overflow-x:auto; overflow-y:hidden;
  scrollbar-width:none; -webkit-overflow-scrolling:touch; }
.bnav .bslot-ctl::-webkit-scrollbar{ display:none; }
.bnav .bsb{ flex:0 0 auto; height:38px; min-width:38px; padding:0 9px; border-radius:19px; border:1px solid var(--line);
  background:var(--raise); color:var(--txt); display:inline-flex; align-items:center; justify-content:center; gap:6px;
  font:inherit; font-size:var(--fs-small); font-weight:700; white-space:nowrap; cursor:pointer; position:relative;
  transition:transform var(--ph-press), background-color var(--ph-press), color var(--ph-press); }
.bnav .bsb:active{ transform:scale(.94); }
.bnav .bsb.on{ color:var(--accent-text); border-color:var(--gold); }
.bnav .bsb-pp{ background:var(--gold); color:#141414; border-color:transparent; min-width:44px; }
.bnav .bsb-w span{ display:inline; }
.bnav .bsq b{ position:absolute; top:-4px; right:-2px; min-width:16px; height:16px; padding:0 4px; border-radius:8px;
  background:var(--gold); color:#141414; font-size:var(--fs-micro); font-weight:800; display:flex; align-items:center; justify-content:center; }
.bnav .bsvol{ flex:1 1 60px; min-width:50px; max-width:120px; height:38px; margin:0; accent-color:var(--gold); }
/* On a 320px phone the six music controls did not fit: the row scrolled and the
   Up-next button — the one carrying a COUNT — sat 32px past the screen edge,
   reachable only by a sideways drag inside a 62px bar that nobody would guess
   at. The volume slider is the one control a phone already has hardware for,
   so it is what gives way. Measured, not guessed: 320px is where it stops
   fitting; every wider phone keeps the slider. */
@media (max-width:359px){ .bnav .bsvol{ display:none; } }
.bnav .bslot-crumb{ color:var(--faint); font-weight:700; font-size:var(--fs-small); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0; }
/* Shell revamp chunk 6: while YouTube is the section on the bar, its tabs live
   in the slot and its own bottom bar steps aside — one bar, not two. PHDock's
   base already treats a display:none .yt-bnav as absent. */
body.ph-bar-section[data-bar-section="yt"] #v-yt .yt-bnav{ display:none !important; }
/* …and the 120px the module reserved beneath its content for that bar goes with it
   (yt.css sets it to 60px wherever the bar is hidden; same number here). */
body.ph-bar-section[data-bar-section="yt"] #v-yt .yt-main{ padding-bottom:calc(var(--ph-bar-h) + 8px); }
/* the "‹" of a back-style wide button: chevright flipped */
.bnav .bsb-w .ic-svg[data-flip], .bnav .bslot-ctl button[onclick^="PHSET.back"] .ic-svg{ transform:scaleX(-1); }

/* ---------- views / layout ---------- */
main{ max-width:1060px; margin:0 auto; padding:14px 16px 30px; }
.view{ display:none; } .view.on{ display:block; animation:vin .18s ease; }
@keyframes vin{ from{ opacity:0; transform:translateY(6px);} to{ opacity:1; transform:none;} }
/* YouTube tab enters with opacity only: vin's transform makes the animating
   view the containing block for position:fixed children, so the module's
   fixed bottom bar visibly jumped for 180ms on every tab switch. */
#v-yt.view.on{ animation:vfade .18s ease; }
@keyframes vfade{ from{ opacity:0; } to{ opacity:1; } }
.sect{ margin:18px 0 6px; display:flex; align-items:baseline; gap:8px; }
.sect h2{ margin:0; } .sect .sub{ color:var(--faint); font-size:var(--fs-meta); }
/* A shelf heading that opens the shelf. It has to LOOK like it does something,
   or it is a hidden shortcut nobody finds — the chevron is the same one the
   "See all" beside it carries. */
.h2door{ cursor:pointer; }
.h2door::after{ content:'\203A'; margin-left:5px; color:var(--faint); font-weight:700;
  display:inline-block; transition:transform .15s ease; }
.h2door:hover, .h2door:focus-visible{ color:var(--accent-text); }
.h2door:hover::after{ transform:translateX(2px); color:var(--accent-text); }
.h2door:focus-visible{ outline:2px solid var(--accent-text); outline-offset:3px; border-radius:4px; }
.empty{ color:var(--faint); padding:22px 8px; text-align:center; }

/* ---------- inputs & buttons ---------- */
.input, input[type=text],input[type=search],input[type=password],input[type=email]{
  width:100%; padding:12px 14px; border-radius:var(--r-sm); border:1px solid var(--line2);
  background:var(--surface); color:var(--txt); font-size:var(--fs-read); outline:none; }
.input:focus{ border-color:var(--gold); box-shadow:0 0 0 3px rgba(229,160,13,.15); }
/* wrap: the Downloads tab row is four buttons, which ran 76px off the side
   of a 375px phone before this. */
.searchrow{ display:flex; flex-wrap:wrap; gap:8px; margin:4px 0 14px; }
/* A text box in one of these rows is `width:100%` by default, which in a
   wrapping flex row shoves the Search button onto a line of its own — always,
   at every width. Let it SHRINK instead: it takes the leftover space, the
   button sits beside it, and the 180px basis still lets a genuinely narrow
   phone wrap, which is what flex-wrap was added for. Button rows with no input
   (the four-button Downloads tabs) are untouched. */
.searchrow > .input{ flex:1 1 180px; width:auto; min-width:0; }
/* Square icon-only action that sits in a searchrow without stretching. */
.icobtn{ flex:0 0 44px; display:inline-flex; align-items:center; justify-content:center;
  padding:0; width:44px; min-width:44px; height:44px; box-sizing:border-box; }
.icobtn svg{ width:20px; height:20px; }
/* ---- Import music modal -------------------------------------------------
   Centred card rather than a bottom sheet: this is a task with a result to
   read, not a list of choices to tap. Capped at 92vh with the track list as
   the only scrolling part, so the header and the actions stay put on a long
   playlist. */
.muimp-wrap{ position:fixed; inset:0; z-index:120; background:rgba(0,0,0,.62);
  backdrop-filter:blur(3px); display:flex; align-items:center; justify-content:center; padding:16px; }
.muimp-card{ width:min(560px,100%); max-height:92vh; display:flex; flex-direction:column;
  background:var(--surface); border:1px solid var(--line2); border-radius:var(--r-lg,16px);
  box-shadow:0 24px 60px rgba(0,0,0,.5); overflow:hidden; }
.muimp-bar{ display:flex; align-items:center; gap:10px; padding:14px 16px; border-bottom:1px solid var(--line); }
.muimp-title{ flex:1; font-weight:800; font-size:var(--fs-read); }
.muimp-body{ padding:16px; overflow:auto; display:flex; flex-direction:column; gap:12px; }
.muimp-center{ align-items:center; text-align:center; padding:34px 16px; }
.muimp-t{ font-weight:800; font-size:var(--fs-title); overflow-wrap:anywhere; }
.muimp-s{ color:var(--dim); font-size:var(--fs-small); line-height:1.5; overflow-wrap:anywhere; }
.muimp-s .okc{ color:var(--ok-text); } .muimp-s .dlc{ color:var(--gold2,#e5a00d); }
.muimp-field{ display:flex; gap:8px; }
.muimp-field .input{ flex:1 1 auto; min-width:0; width:auto; }
.muimp-hint{ font-size:var(--fs-meta); color:var(--dim); }
.muimp-hint.ok{ color:var(--ok-text); } .muimp-hint.bad{ color:var(--bad-text); }
.muimp-bad{ color:var(--bad-text); font-weight:700; font-size:var(--fs-small); overflow-wrap:anywhere; }
.muimp-note{ font-size:var(--fs-meta); color:var(--gold2,#e5a00d); background:rgba(229,160,13,.10);
  border:1px solid rgba(229,160,13,.30); border-radius:10px; padding:10px 12px; line-height:1.5; }
.muimp-head{ display:flex; gap:12px; align-items:center; }
.muimp-cov{ width:64px; height:64px; border-radius:10px; object-fit:cover; flex:0 0 64px; }
.muimp-cov.ph{ display:flex; align-items:center; justify-content:center; background:var(--raise); color:var(--dim); }
.muimp-list{ border:1px solid var(--line); border-radius:12px; overflow:hidden; }
.muimp-trk{ display:flex; align-items:center; gap:10px; padding:9px 12px; border-bottom:1px solid var(--line); }
.muimp-trk:last-child{ border-bottom:0; }
.muimp-ti{ flex:1; min-width:0; }
.muimp-t1{ font-size:var(--fs-small); font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.muimp-t2{ font-size:var(--fs-meta); color:var(--dim); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.muimp-badge{ flex:0 0 auto; font-size:var(--fs-micro); font-weight:800; padding:3px 8px; border-radius:999px; white-space:nowrap; }
.muimp-badge.ok{ color:var(--ok-text); background:rgba(95,208,138,.14); }
.muimp-badge.dl{ color:var(--gold2,#e5a00d); background:rgba(229,160,13,.14); }
.muimp-more{ align-self:flex-start; }
.muimp-acts{ display:flex; flex-wrap:wrap; gap:8px; margin-top:2px; }
.muimp-spin{ width:34px; height:34px; border-radius:50%; border:3px solid var(--line2);
  border-top-color:var(--gold); animation:muimpspin .8s linear infinite; }
@keyframes muimpspin{ to{ transform:rotate(360deg); } }
@media (max-width:420px){ .muimp-acts .btn{ flex:1 1 auto; } }


.btn{ border:0; border-radius:var(--r-sm); padding:11px 16px; font-size:var(--fs-small); font-weight:700; cursor:pointer;
  background:var(--gold-grad); color:#191919; transition:filter .15s, transform .05s; white-space:nowrap; }
.btn:active{ transform:scale(.97); } .btn:hover{ filter:brightness(1.08); }
.btn:disabled{ opacity:.55; cursor:default; }
.btn.sec{ background:var(--raise); color:var(--txt); border:1px solid var(--line2); }
.btn.ghost{ background:transparent; color:var(--dim); border:1px solid var(--line); }
.btn.danger{ background:rgba(255,93,93,.15); color:var(--bad-text); border:1px solid rgba(255,93,93,.35); }
.btn.sm{ padding:7px 11px; font-size:var(--fs-meta); border-radius:9px; }
.chip{ display:inline-flex; align-items:center; gap:5px; padding:4px 10px; border-radius:999px;
  font-size:var(--fs-meta); font-weight:700; background:var(--raise); color:var(--dim); border:1px solid var(--line); }
.chip.ok{ color:var(--ok-text); border-color:rgba(46,194,126,.4); background:rgba(46,194,126,.1); }
/* --gold2 is a FILL colour; as text on white it measures 2.43:1 (chunk 37,
   Settings, "Waiting for confirmation"). The accent's text token is the one
   that has been measured for this job in both themes. */
.chip.warn{ color:var(--accent-text); border-color:rgba(229,160,13,.4); background:rgba(229,160,13,.1); }
.chip.bad{ color:var(--bad-text); border-color:rgba(255,93,93,.4); background:rgba(255,93,93,.1); }
.chip.info{ color:var(--info); border-color:rgba(88,166,255,.4); background:rgba(88,166,255,.1); }

/* ===== ROWS THAT SCROLL SIDEWAYS SAY SO =====
   Lifted from the YouTube module (yt.css:189, R7 Y1), which fixed this for its
   own rows and deliberately left the rest of the app clipping. Measured at
   390px: 21 of Home's 24 sideways rows overflowed and not one of them said so —
   the 4th quick action cut mid-chip, "120 min" cut mid-word, every Spotify
   shelf ending mid-poster, with nothing at either edge to suggest a swipe would
   help. The fade is driven by data-xf, which PHXF (hub.js) sets from
   scrollLeft: it appears only on a side there is genuinely more content on, and
   moves to the other side when you reach the end — a permanent fade is
   decoration, and the thing a viewer needs to learn is *which way* to push.
   Rows opt in by carrying .ph-xs, which the same code adds, so no markup had to
   change. The class is NEUTRAL on purpose: this is the app's rule now, not
   YouTube's, and yt.css keeps its own .yt-xs for the non-hub shells. */
.ph-xs[data-xf="r"]{ -webkit-mask-image:linear-gradient(to right,#000 calc(100% - 34px),transparent);
  mask-image:linear-gradient(to right,#000 calc(100% - 34px),transparent); }
.ph-xs[data-xf="l"]{ -webkit-mask-image:linear-gradient(to left,#000 calc(100% - 34px),transparent);
  mask-image:linear-gradient(to left,#000 calc(100% - 34px),transparent); }
.ph-xs[data-xf="lr"]{ -webkit-mask-image:linear-gradient(to right,transparent,#000 34px,#000 calc(100% - 34px),transparent);
  mask-image:linear-gradient(to right,transparent,#000 34px,#000 calc(100% - 34px),transparent); }

/* ---------- home quick-actions + category rows ---------- */
.quickrow{ display:flex; gap:8px; overflow-x:auto; padding:2px 0 4px; margin:12px 0 2px; scroll-snap-type:x proximity; }
.quickrow::-webkit-scrollbar{ display:none; }
.qchip{ flex:0 0 auto; display:inline-flex; align-items:center; gap:7px; padding:9px 14px; border-radius:12px;
  font-size:var(--fs-small); font-weight:700; color:var(--txt); background:var(--raise); border:1px solid var(--line);
  cursor:pointer; transition:.14s; white-space:nowrap; }
.qchip:hover{ border-color:var(--gold); color:var(--accent-text); }
.qchip:active{ transform:scale(.97); }
.seeall{ display:inline-flex; align-items:center; gap:2px; background:none; border:0; cursor:pointer;
  color:var(--faint); font-size:var(--fs-meta); font-weight:700; padding:0; }
.seeall:hover{ color:var(--accent-text); }
.ptile .rdy-badge{ position:absolute; top:6px; left:6px; padding:2px 7px; border-radius:6px; font-size:var(--fs-micro);
  font-weight:800; letter-spacing:.3px; color:#0a1a0f; background:var(--ok); box-shadow:0 2px 8px rgba(0,0,0,.4); }

/* ---------- poster tiles / rows / grid — Netflix cards ---------- */
.hrow{ display:flex; gap:8px; overflow-x:auto; padding:8px 2px 20px; scroll-snap-type:x proximity; }
.hrow::-webkit-scrollbar{ display:none; }
.ptile{ flex:0 0 var(--ptw,132px); width:var(--ptw,132px); cursor:pointer; scroll-snap-align:start; position:relative;
  /* long grids: skip layout/paint for off-screen tiles — the big scroll-jank fix */
  content-visibility:auto; contain-intrinsic-size:var(--ptw,132px) calc(var(--ptw,132px) * 1.79);
  transition:transform .26s cubic-bezier(.2,.7,.2,1); }
.ptile .pw{ position:relative; width:var(--ptw,132px); height:calc(var(--ptw,132px) * 1.5); border-radius:8px; overflow:hidden;
  background:var(--card); box-shadow:0 5px 16px rgba(0,0,0,.4); transition:box-shadow .26s; }
/* Faint film-strip placeholder shown while a poster loads and, crucially, if it
   fails — so a missing TMDB poster reads as an empty poster, not a broken-image
   icon. PHIMG hides a permanently-failed <img> in place (visibility:hidden)
   rather than removing it, so this shows through and the tile keeps its size. */
.ptile .pw::after{ content:""; position:absolute; inset:0; z-index:0; opacity:.22;
  background:center/38px no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23e5a00d' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='16' rx='2'/%3E%3Cpath d='M7 4v16M17 4v16M3 9h4M17 9h4M3 15h4M17 15h4'/%3E%3C/svg%3E"); }
.ptile img{ position:relative; z-index:1; width:100%; height:100%; object-fit:cover; display:block; }
/* Caption below the poster — the Netflix MOBILE treatment (see hover media query,
   where it's hidden on real-pointer devices in favour of the hover overlay). */
.ptile .t{ margin-top:7px; font-size:var(--fs-meta); font-weight:600; color:var(--txt);
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.ptile .s{ font-size:var(--fs-micro); color:var(--faint); }
/* Hover overlay — title + meta over a gradient, revealed on the poster itself
   (Netflix desktop). Sits inside .pw so it never needs the tile to overflow. */
.ptile .ov{ position:absolute; left:0; right:0; bottom:0; z-index:3; padding:30px 11px 11px;
  background:linear-gradient(0deg, rgba(0,0,0,.94) 12%, rgba(0,0,0,0)); opacity:0;
  transform:translateY(6px); transition:opacity .26s, transform .26s; pointer-events:none; }
.ptile .ov .ovt{ font-size:var(--fs-small); font-weight:750; line-height:1.16; color:#fff;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.ptile .ov .ovm{ font-size:var(--fs-micro); color:var(--dim); margin-top:3px; }
.ptile .prog{ position:absolute; left:0; right:0; bottom:0; z-index:4; height:4px; background:var(--scrim-1); }
.ptile .prog>i{ display:block; height:100%; background:var(--gold-grad); }
.ptile .pbadge{ position:absolute; top:8px; right:8px; z-index:5; }
/* "NEW" / "NEW EPISODES" pill — content landed this week. */
.ptile .newb{ position:absolute; top:8px; left:8px; z-index:5; padding:3px 8px; border-radius:6px;
  font-size:var(--fs-micro); font-weight:850; letter-spacing:.04em; background:var(--gold-grad); color:#191919;
  box-shadow:0 2px 8px rgba(0,0,0,.35); }
/* ★ rating badge (bottom-right of the poster). */
.ptile .rateb{ position:absolute; right:7px; bottom:9px; z-index:5; padding:2px 6px; border-radius:6px;
  font-size:var(--fs-micro); font-weight:800; background:var(--scrim-2); color:#f0c75b; }
/* "On server" tag (top-right) — owned titles in discovery/search results. */
.ptile .ownb{ position:absolute; top:8px; right:8px; z-index:5; padding:3px 8px; border-radius:6px; font-size:var(--fs-micro);
  font-weight:800; letter-spacing:.03em; background:rgba(70,211,105,.16); color:#46d369; border:1px solid rgba(70,211,105,.4); }
.librow-sub{ font-size:var(--fs-meta); color:var(--faint); font-weight:500; }
.pgrid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(132px,1fr)); gap:16px 12px; }
.pgrid .ptile{ width:auto; flex:none; contain-intrinsic-size:auto 236px; }
.pgrid .ptile .pw{ width:100%; height:0; padding-bottom:150%; }
.pgrid .ptile img{ position:absolute; inset:0; height:100%; }
.pw .ph{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  color:var(--faint); font-size:var(--fs-big); }
/* A title that legitimately has no poster anywhere. Naming it beats an empty
   frame — the card reads as finished rather than still loading. */
.pw .ph.phname{ padding:12% 10%; font-size:inherit;
  background:linear-gradient(160deg, hsl(var(--phh,220) 26% 20%), hsl(var(--phh,220) 30% 11%)); }
.pw .ph.phname span{ color:var(--dim); font-size:var(--fs-meta); font-weight:650; line-height:1.35;
  text-align:center; display:-webkit-box; -webkit-line-clamp:4; -webkit-box-orient:vertical;
  overflow:hidden; overflow-wrap:anywhere; }
/* Netflix hover — ONLY where a real pointer can hover (desktop app + website).
   Touch (iOS) keeps captions below and never triggers the scale/overlay. */
@media (hover:hover) and (pointer:fine){
  /* Only tiles that carry the hover overlay drop their caption — rows not yet
     upgraded to the overlay keep their captions, so nothing loses its title. */
  .ptile:has(.ov) .t, .ptile:has(.ov) .s{ display:none; }
  .ptile:hover{ transform:scale(1.09); z-index:20; }
  .ptile:hover .pw{ box-shadow:0 22px 46px rgba(0,0,0,.62); }
  .ptile:hover .ov{ opacity:1; transform:none; }
}

/* ---------- Library hero billboard (Netflix) ---------- */
.libhero{ position:relative; height:min(46vh,380px); min-height:280px; border-radius:16px; overflow:hidden;
  margin:0 0 22px; display:flex; align-items:flex-end; box-shadow:0 12px 40px rgba(0,0,0,.45); }
.libhero-bg{ position:absolute; inset:0; z-index:0; background-size:cover; background-position:center 20%;
  filter:blur(24px) saturate(1.12) brightness(.9); transform:scale(1.15); }
.libhero-scrim{ position:absolute; inset:0; z-index:1;
  background:linear-gradient(90deg, rgba(8,8,12,.95) 0%, rgba(8,8,12,.55) 44%, rgba(8,8,12,.12) 74%),
             linear-gradient(0deg, var(--bg) 2%, rgba(8,8,12,0) 55%); }
.libhero-poster{ position:absolute; right:5%; top:13%; bottom:0; z-index:2; aspect-ratio:2/3; border-radius:12px;
  overflow:hidden; box-shadow:0 18px 46px rgba(0,0,0,.62); }
.libhero-poster img{ width:100%; height:100%; object-fit:cover; display:block; }
.libhero-inner{ position:relative; z-index:3; padding:0 6% 28px; max-width:600px; }
.libhero-badge{ display:inline-block; font-size:var(--fs-micro); font-weight:800; letter-spacing:1.5px; text-transform:uppercase;
  color:var(--accent-text); margin-bottom:12px; }
.libhero-title{ font-size:clamp(30px,4.6vw,54px); font-weight:900; line-height:1; letter-spacing:-.5px; margin-bottom:12px;
  text-shadow:0 2px 22px rgba(0,0,0,.55); }
.libhero-meta{ font-size:var(--fs-small); color:var(--dim); margin-bottom:18px; font-weight:600; }
.libhero-actions{ display:flex; gap:12px; flex-wrap:wrap; }
.libhero .btn{ display:inline-flex; align-items:center; gap:8px; border:0; border-radius:7px; font-size:var(--fs-read); font-weight:800;
  padding:11px 24px; cursor:pointer; transition:background .18s, transform .12s; }
.libhero .btn:active{ transform:scale(.97); }
.libhero .hero-play{ background:#fff; color:#141414; }
.libhero .hero-play:hover{ background:rgba(255,255,255,.82); }
.libhero .hero-info{ background:rgba(84,84,96,.55); color:#fff; }
.libhero .hero-info:hover{ background:rgba(84,84,96,.4); }
@media (max-width:640px){
  .libhero{ height:min(54vh,430px); border-radius:12px; }
  .libhero-poster{ display:none; }
  .libhero-inner{ max-width:none; padding-bottom:24px; }
}

/* ---------- Library category rows (Netflix browse) ---------- */
.librows{ display:block; }
.librow{ margin:0 0 6px; }
.librow-head{ display:flex; align-items:baseline; gap:12px; margin:0 0 2px; padding:0 2px; }
.librow-head h2{ font-size:clamp(15px,1.6vw,20px); font-weight:750; letter-spacing:.2px; margin:0; }
/* How many are in this shelf, beside its name - quieter than the name, because
   it is a fact about the shelf and not the shelf itself. */
.librow-n{ font-size:var(--fs-meta); color:var(--faint); font-weight:600; letter-spacing:0; }
.librow-see{ font-size:var(--fs-meta); color:var(--faint); font-weight:600; cursor:pointer; transition:color .2s, opacity .2s; }
.librow-see:hover{ color:var(--txt); }
@media (hover:hover) and (pointer:fine){ .librow-see{ opacity:0; } .librow:hover .librow-see{ opacity:1; } }

/* ---------- Library filter bar (Netflix) ---------- */
.libbar{ position:sticky; top:0; z-index:25; display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  padding:12px 2px 12px; margin:0 0 4px; background:linear-gradient(180deg, var(--bg) 72%, rgba(20,20,20,0)); }
.libseg{ display:flex; background:rgba(255,255,255,.06); border-radius:22px; padding:3px; flex:0 0 auto; }
.libseg button{ border:0; background:transparent; color:var(--dim); font-size:var(--fs-small); font-weight:700; padding:6px 16px;
  border-radius:18px; cursor:pointer; transition:background .16s, color .16s; }
.libseg button.on{ background:#fff; color:#141414; }
.libchips{ display:flex; gap:8px; flex-wrap:wrap; }
.libchip{ border:1px solid rgba(255,255,255,.16); background:rgba(255,255,255,.04); color:var(--dim); border-radius:20px;
  padding:6px 14px; font-size:var(--fs-meta); font-weight:600; cursor:pointer; transition:.16s; white-space:nowrap; }
.libchip:hover{ border-color:rgba(255,255,255,.4); color:var(--txt); }
.libchip.on{ background:#fff; color:#141414; border-color:#fff; }
.libbar-grow{ flex:1 1 20px; }
.libsearch{ display:flex; align-items:center; gap:7px; background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.12); border-radius:20px; padding:0 12px; height:36px; }
.libsearch svg{ width:15px; height:15px; stroke:var(--faint); fill:none; stroke-width:2; }
.libsearch input{ background:transparent; border:0; outline:0; color:var(--txt); font-size:var(--fs-small); width:150px; }
.libsearch input::placeholder{ color:var(--faint); }
.libsort{ background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.12); color:var(--txt); border-radius:20px;
  padding:0 12px; height:36px; font-size:var(--fs-small); font-weight:600; cursor:pointer; }
.libcount{ font-size:var(--fs-meta); color:var(--faint); flex:0 0 auto; }
/* Narrowed reads differently from whole. "576 titles" is a fact about the
   library; "48 of 576" is a fact about what the filters just did, and it is the
   one worth looking at. */
.libcount.narrowed{ color:var(--gold); font-weight:800; }
@media (max-width:640px){
  .libbar{ gap:8px; top:0; }
  .libbar-grow{ display:none; }
  /* give the filter box its own full-width row so it's actually usable on phones */
  .libsearch{ order:9; flex:1 1 100%; height:38px; }
  .libsearch input{ width:100%; flex:1; }
  /* On a phone the bar has no room for a number that never changes - but it
     always has room for the one that says a filter is on. Hiding the count
     unconditionally meant the screen where it matters most was the screen that
     never showed it: you filter, the grid shrinks, and nothing tells you by how
     much or why. */
  .libcount{ display:none; }
  .libcount.narrowed{ display:inline-block; order:10; flex:1 1 100%; }
}
/* Arabic / Hebrew titles render in their own reading direction (RTL) without
   flipping the whole layout — English titles are unaffected. */
.ptile .t, .ptile .ov .ovt, .libhero-title, .librow-head h2,
.detail .dtitle, .epinfo .eptitle{ unicode-bidi:plaintext; }

/* ---------- cards / list rows ---------- */
.card{ background:var(--card); border:1px solid var(--line); border-radius:var(--r); padding:12px; margin:10px 0; }
.lrow{ display:flex; align-items:center; gap:12px; }
.lrow .grow{ flex:1; min-width:0; }
.lrow .ttl{ font-weight:700; font-size:var(--fs-small); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.lrow .meta{ color:var(--faint); font-size:var(--fs-meta); margin-top:2px; }
.lrow .poster{ width:52px; height:76px; border-radius:9px; object-fit:cover; background:var(--raise); flex:0 0 52px; }
.actions{ display:flex; gap:7px; flex-wrap:wrap; justify-content:flex-end; align-items:center; }

/* ---------- progress bars ---------- */
.pbar{ height:7px; border-radius:5px; background:var(--raise); overflow:hidden; }
.pbar>i{ display:block; height:100%; background:var(--gold-grad); border-radius:5px; transition:width .5s; }
.pbar.ok>i{ background:var(--ok); }

/* ---------- sheets / modals ---------- */
/* Above the YouTube mini-player (z-120): a track's ⋯ sheet opened while a
   video plays in the dock must not hide behind the mini bar. */
.sheet-veil{ position:fixed; inset:0; z-index:124; background:var(--scrim-2); backdrop-filter:blur(3px);
  display:none; }
.sheet-veil.on{ display:block; }
.sheet{ position:fixed; left:0; right:0; bottom:0; z-index:125; max-height:80vh; overflow-y:auto;
  background:var(--bg2); border-radius:18px 18px 0 0; border:1px solid var(--line2); border-bottom:0;
  padding:8px 16px calc(20px + var(--sa-b)); display:none; box-shadow:var(--shadow); }
.sheet.on{ display:block; animation:shup .22s ease; }
@keyframes shup{ from{ transform:translateY(40px); opacity:.4; } to{ transform:none; opacity:1; } }
.sheet .grab{ width:38px; height:4px; border-radius:2px; background:var(--line2); margin:6px auto 12px; }
/* A sheet's title must survive scrolling. "Your Spotify playlists" is a long
   list, and on the PHONE the title scrolled away leaving a wall of covers with
   nothing naming them — the desktop got this fix first, which is why Pierre saw
   no change on his iPhone. Negative margins let the bar span the sheet's own
   padding, so nothing slides out from underneath it. */
.sheet h3{
  position:sticky; top:0; z-index:2;
  margin:-8px -16px 12px; padding:14px 16px 10px;
  background:var(--bg2);
  border-bottom:1px solid var(--line);
  border-radius:18px 18px 0 0;
}
.sheet .grab + h3{ margin-top:-4px; }
.sopt{ display:flex; align-items:center; gap:11px; width:100%; text-align:left; padding:12px; margin:6px 0;
  border-radius:12px; border:1px solid var(--line); background:var(--surface); color:var(--txt);
  font-size:var(--fs-small); font-weight:600; cursor:pointer; }
.sopt:hover{ background:var(--raise); }
.sopt img{ width:38px; height:38px; border-radius:8px; object-fit:cover; }
@media(min-width:720px){ .sheet{ left:50%; right:auto; width:520px; transform:translateX(-50%); border-radius:18px; bottom:24px; } }

/* ---------- toast ---------- */
/* R7.9 - the toast is a DARK ISLAND. Its background is a hardcoded #20262f in
   both themes, but its ink was the THEME's --txt, so in light mode it painted
   #11151c on #20262f: 1.20:1, invisible. Every toast in the app - every
   "Added to Liked Songs", every failure, the DJ announcing itself - was a
   blank slab for anybody using light mode.

   Same rule as the pill and the player: a surface that opts out of the theme's
   background opts out of its ink too, and carries its own. --accent-text is in
   here for the action button, which R7.8 moved onto the light-surface gold
   (#8c5800) and thereby made 2.54:1 on this dark slab - a regression this
   chunk is undoing at the same time as the older one. */
#toast, .ph-toast{
  --txt:#f2f4f8; --dim:#a9b0bb; --faint:#8b93a0;
  --line2:rgba(255,255,255,.16); --accent-text:#f6c744;
}
#toast{ position:fixed; left:50%; transform:translateX(-50%) translateY(20px);
  bottom:calc(var(--ph-bar-h) + 18px); z-index:99;
  background:#20262f; color:var(--txt); border:1px solid var(--line2); padding:11px 18px; border-radius:12px;
  font-size:var(--fs-small); font-weight:600; opacity:0; pointer-events:none; transition:.25s; box-shadow:var(--shadow);
  max-width:88vw; }
#toast.show{ opacity:1; transform:translateX(-50%) translateY(0); }

/* ---------- files / seasons ---------- */
.files{ margin-top:8px; border-top:1px solid var(--line); padding-top:8px; }
.frow{ display:flex; align-items:center; gap:8px; padding:7px 4px; font-size:var(--fs-small); border-radius:8px; }
.frow:hover{ background:var(--surface); }
.frow .fn{ flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--dim); }
.seas{ margin:6px 0; }
.seas .shd{ display:flex; align-items:center; gap:8px; padding:8px 6px; font-weight:700; font-size:var(--fs-small);
  cursor:pointer; border-radius:8px; }
.seas .shd:hover{ background:var(--surface); }
.toggle{ color:var(--accent-text); font-size:var(--fs-meta); cursor:pointer; font-weight:600; }

/* ---------- Shazam-style song ID ---------- */
.shazbtn{ display:flex; align-items:center; gap:14px; width:100%; padding:16px 18px; margin-bottom:12px;
  border:0; border-radius:16px; cursor:pointer; text-align:left;
  background:linear-gradient(120deg,#1c9fff,#0a6fd6 70%,#7b3ff2); color:#fff; box-shadow:0 8px 22px rgba(10,111,214,.35); }
.shazbtn:active{ transform:scale(.99); }
.shazbtn .shazic{ font-size:var(--fs-big); display:inline-flex; }
.shazbtn .shazic svg{ width:26px; height:26px; }
.shazbtn .shaztx{ display:flex; flex-direction:column; line-height:1.3; }
.shazbtn .shaztx b{ font-size:var(--fs-read); } .shazbtn .shaztx small{ opacity:.85; font-size:var(--fs-meta); }
.shazov{ position:fixed; inset:0; z-index:120; display:none; align-items:center; justify-content:center;
  background:rgba(6,10,18,.82); backdrop-filter:blur(10px); padding:24px; }
.shazov.on{ display:flex; }
.shazbox{ position:relative; width:min(360px,92vw); background:var(--surface); border:1px solid var(--line2);
  border-radius:22px; padding:28px 22px; text-align:center; box-shadow:var(--shadow); }
.shazx{ position:absolute; top:12px; right:12px; border:0; background:transparent; color:var(--dim); cursor:pointer; }
.shazpulse{ position:relative; width:120px; height:120px; margin:6px auto 18px; display:flex; align-items:center; justify-content:center; font-size:var(--fs-giant); }
.shazpulse .ring{ position:absolute; inset:0; border-radius:50%; border:2px solid #1c9fff; opacity:0; animation:shazring 2s ease-out infinite; }
.shazpulse .ring:nth-child(2){ animation-delay:1s; }
.shazpulse.thinking{ animation:shazspin 1.4s linear infinite; }
@keyframes shazring{ 0%{ transform:scale(.5); opacity:.7; } 100%{ transform:scale(1.3); opacity:0; } }
@keyframes shazspin{ to{ transform:rotate(360deg); } }
.shaztitle{ font-size:var(--fs-head); font-weight:800; margin-bottom:4px; }
.shazmsg{ color:var(--dim); font-size:var(--fs-small); }
.shazhit .shazcov{ width:150px; height:150px; border-radius:14px; object-fit:cover; margin:0 auto 14px;
  display:flex; align-items:center; justify-content:center; font-size:var(--fs-giant); background:var(--card); }
.shazacts{ display:flex; flex-direction:column; gap:8px; margin-top:18px; }
.shazacts .btn{ width:100%; justify-content:center; }

/* ---------- episode selector (season chips + episode cards w/ stills) ---------- */
/* Netflix episode list: hover-highlighted rows, 16:9 still, number + title. */
.epcard{ display:flex; gap:16px; padding:14px 12px; border-radius:10px; border-bottom:1px solid var(--line);
  transition:background .16s; align-items:center; }
.epcard:hover{ background:rgba(255,255,255,.045); }
.epcard:last-child{ border-bottom:0; }
.epcard.missing{ opacity:.58; }
.epthumb{ position:relative; flex:0 0 170px; width:170px; aspect-ratio:16/9; align-self:flex-start;
  border-radius:8px; overflow:hidden; background:var(--card); display:flex; align-items:center; justify-content:center;
  box-shadow:0 5px 14px rgba(0,0,0,.4); }
.epthumb img{ width:100%; height:100%; object-fit:cover; }
.epthumb.noimg::after{ content:"🎬"; font-size:var(--fs-big); opacity:.4; }
.epthumb .epnum{ position:absolute; left:8px; bottom:8px; z-index:2; padding:2px 8px; border-radius:6px;
  font-size:var(--fs-micro); font-weight:850; background:rgba(0,0,0,.7); color:#fff; }
.epthumb .epplay{ position:absolute; inset:0; z-index:3; display:flex; align-items:center; justify-content:center;
  border:0; background:rgba(0,0,0,.42); color:#fff; cursor:pointer; opacity:0; transition:opacity .18s; }
.epthumb:hover .epplay{ opacity:1; }
/* A touch screen has no hover, so this control — which covers the whole still
   and DOES play the episode when tapped — was permanently invisible on a
   phone: the thumbnail started a video with nothing on it saying it would.
   Shown, with a lighter scrim so it reads as an affordance over the artwork
   rather than a curtain across it. */
@media (hover:none){
  .epthumb .epplay{ opacity:1; background:linear-gradient(180deg, rgba(0,0,0,.06), rgba(0,0,0,.34)); }
  .epthumb .epplay .ic-svg{ filter:drop-shadow(0 2px 6px rgba(0,0,0,.75)); }
}
.epinfo{ flex:1; min-width:0; display:flex; flex-direction:column; gap:4px; }
.epinfo .eptitle{ font-weight:750; font-size:var(--fs-read); }
.epinfo .epmeta{ color:var(--faint); font-size:var(--fs-meta); }
.epinfo .epov{ color:var(--dim); font-size:var(--fs-small); line-height:1.5; display:-webkit-box; -webkit-line-clamp:2;
  -webkit-box-orient:vertical; overflow:hidden; }
.epinfo .epactions{ display:flex; gap:8px; margin-top:8px; align-items:center; }
/* Season selector chips (detail) — match the library chip language. */
#det-seasons .fchip{ border:1px solid rgba(255,255,255,.16); background:rgba(255,255,255,.04); color:var(--dim);
  border-radius:20px; padding:6px 15px; font-size:var(--fs-meta); font-weight:600; }
#det-seasons .fchip:hover{ border-color:rgba(255,255,255,.4); color:var(--txt); background:rgba(255,255,255,.04); }
#det-seasons .fchip.on{ background:#fff; color:#141414; border-color:#fff; }
@media(max-width:520px){ .epcard{ gap:12px; padding:12px 6px; } .epthumb{ flex-basis:128px; width:128px; } .epinfo .epov{ -webkit-line-clamp:3; } }

/* ============================================================
   VIDEO PLAYER (fullscreen overlay)
   ============================================================ */
.vp{ position:fixed; inset:0; z-index:200; background:#000; display:none;
  touch-action:none; overscroll-behavior:none; }
.vp.on{ display:block; }
.vp video{ width:100%; height:100%; object-fit:contain; background:#000; }
/* MINIMIZED — a bottom bar like the music minibar; the same <video> keeps playing
   (no reload), tap the bar to expand. */
.vp.min{ inset:auto; left:8px; right:8px; bottom:calc(var(--ph-bar-h) + 8px);
  height:60px; border-radius:14px; overflow:hidden; background:#1b212bf5; border:1px solid var(--line2);
  box-shadow:var(--shadow); z-index:78; cursor:pointer; touch-action:auto; }
.vp.min video{ position:absolute; left:0; top:0; width:60px; height:60px; object-fit:cover; }
.vp.min .lay, .vp.min .vbuf, .vp.min .vprep, .vp.min .vresume, .vp.min .vmenu, .vp.min .skipintro, .vp.min .vflash, .vp.min .vupnext{ display:none !important; }
.vp .vminibar{ display:none; }
.vp.min .vminibar{ display:flex; position:absolute; left:60px; right:0; top:0; bottom:0; align-items:center; gap:4px; padding:0 6px 0 12px; }
.vp.min .vminibar .vmb-prog{ position:absolute; left:-60px; right:0; top:0; height:3px; background:var(--ink-4); }
.vp.min .vminibar .vmb-prog>i{ display:block; height:100%; width:0; background:var(--gold); }
.vp.min .vminibar .vmb-t{ flex:1; min-width:0; font-size:var(--fs-small); font-weight:750; color:var(--txt); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.vp.min .vminibar .vmb-btn{ flex:0 0 auto; width:34px; height:34px; border:0; background:transparent; color:var(--txt); display:flex; align-items:center; justify-content:center; cursor:pointer; border-radius:8px; font-size:var(--fs-read); }
.vp.min .vminibar .vmb-btn:active{ background:var(--ink-3); }
.vp.min .vminibar .vmb-x{ color:var(--dim); }
/* "Zoom to fill" — crop to the screen (no letterbox) on phones. Toggled by the
   fill button + remembered. */
.vp.fill video{ object-fit:cover; }
/* Buffering spinner — shown whenever the video is waiting/seeking so a slow
   seek reads as "loading", not "frozen". */
.vp .vbuf{ position:absolute; inset:0; z-index:4; display:none; align-items:center; justify-content:center;
  pointer-events:none; }
.vp .vbuf.on{ display:flex; }
.vp .vbuf .spin{ width:54px; height:54px; border-radius:50%; border:3px solid var(--ink-5);
  border-top-color:var(--gold); animation:vspin .8s linear infinite; box-shadow:0 0 18px var(--scrim-1); }
/* LIVE badge in the player top bar — only for live streams. */
.vp .vlive{ display:none; align-items:center; gap:5px; padding:4px 11px; border-radius:999px;
  background:rgba(255,59,92,.16); border:1px solid rgba(255,59,92,.55); color:#ff5c6e;
  font-size:var(--fs-micro); font-weight:900; letter-spacing:.03em; white-space:nowrap; }
.vp.islive .vlive{ display:inline-flex; }
.vp .lay{ position:absolute; inset:0; opacity:1; transition:opacity .25s; }
.vp.hidectl .lay{ opacity:0; pointer-events:none; }
.vp .vtop{ position:absolute; top:0; left:0; right:0; display:flex; align-items:center; gap:12px;
  padding:calc(var(--sa-t) + 14px) 18px 26px;
  background:linear-gradient(180deg,rgba(0,0,0,.75),transparent); }
.vp .vtt{ flex:1; min-width:0; }
.vp .vtt .t1{ font-weight:850; font-size:var(--fs-head); letter-spacing:-.2px; text-shadow:0 1px 8px rgba(0,0,0,.75);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.vp .vtt .t2{ color:var(--dim); font-size:var(--fs-small); font-weight:500; margin-top:1px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* Live streams have no fixed timeline: hide the scrubber, times, and speed. */
.vp.islive #vp-seek,.vp.islive #vp-cur,.vp.islive #vp-dur,.vp.islive #vp-speed{ display:none; }
.vp.islive .vtt .t2{ color:#ff3b5c; font-weight:800; letter-spacing:.04em; }
/* Clean Netflix icon buttons — transparent, subtle hover (was an always-on pill,
   which made the crowded top bar feel even busier). */
.vp .vbtn{ background:transparent; border:0; color:#fff; width:42px; height:42px; border-radius:50%; font-size:var(--fs-title);
  cursor:pointer; display:flex; align-items:center; justify-content:center; flex:0 0 auto;
  transition:background .16s, transform .12s; }
.vp .vbtn:hover{ background:rgba(255,255,255,.15); }
.vp .vbtn:active{ background:rgba(255,255,255,.24); transform:scale(.94); }
.vp .vcenter{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center; gap:40px; pointer-events:none; }
.vp .vcenter .vbtn{ pointer-events:auto; width:56px; height:56px; font-size:var(--fs-big); background:rgba(0,0,0,.32); }
.vp .vcenter .vbtn:hover{ background:rgba(0,0,0,.5); }
.vp .vcenter .vbtn.big{ width:74px; height:74px; font-size:var(--fs-hero); background:rgba(255,255,255,.95); color:#141414; }
.vp .vcenter .vbtn.big:hover{ background:#fff; }
.vp .vbot{ position:absolute; left:0; right:0; bottom:0; padding:32px 18px calc(var(--sa-b) + 14px);
  background:linear-gradient(0deg,rgba(0,0,0,.82),rgba(0,0,0,.35) 55%,transparent); }
.vp .vseek{ display:flex; align-items:center; gap:12px; }
.vp .vctrls{ display:flex; align-items:center; gap:6px; margin-top:8px; }
.vp .vgrow{ flex:1; }
.vp .vtime{ font-size:var(--fs-meta); color:var(--dim); font-variant-numeric:tabular-nums; min-width:44px; }
.vp .seek{ position:relative; flex:1; height:22px; display:flex; align-items:center; cursor:pointer; }
.vp .seek .tr{ position:absolute; left:0; right:0; height:5px; border-radius:3px; background:var(--ink-5); overflow:hidden; }
.vp .seek .bf{ position:absolute; left:0; height:5px; border-radius:3px; background:var(--ink-6); }
.vp .seek .pl{ position:absolute; left:0; height:5px; border-radius:3px; background:var(--gold); }
.vp .seek .knob{ position:absolute; width:14px; height:14px; border-radius:50%; background:var(--gold2);
  transform:translateX(-50%); box-shadow:0 0 6px var(--scrim-2); }
.vp .skipintro{ position:absolute; right:18px; bottom:118px; padding:13px 22px; border-radius:13px; border:0;
  background:var(--gold-grad); color:#141414; font-weight:800; font-size:var(--fs-read); cursor:pointer; display:none;
  box-shadow:0 8px 24px var(--scrim-1); z-index:5; letter-spacing:.2px; }
/* Skip-intro stays visible even when the auto-hidden controls fade. */
.vp.hidectl .skipintro.on{ display:block; }
.vp .skipintro.on{ display:block; animation:shup .25s ease; }
/* It has to FADE, not vanish — the pill is removed by dropping .on, and
   display:none cuts a transition off before its first frame. So the base state
   stays display:block and hides via opacity + visibility instead, with
   visibility's own transition DELAYED by the fade duration so the element is
   still there to be seen fading and untappable the instant it is gone. */
.vp .skipintro{ display:block; opacity:0; transform:translateY(6px); pointer-events:none; visibility:hidden;
  transition:opacity .35s ease, transform .35s ease, visibility 0s linear .35s; }
.vp .skipintro.on{ opacity:1; transform:translateY(0); pointer-events:auto; visibility:visible; transition-delay:0s; }
.vp .vresume{ position:absolute; inset:0; z-index:5; display:flex; flex-direction:column; align-items:center;
  justify-content:center; gap:14px; background:var(--scrim-3); backdrop-filter:blur(4px); }
.vp .vprep{ position:absolute; inset:0; z-index:6; display:flex; flex-direction:column; align-items:center;
  justify-content:center; gap:12px; background:var(--scrim-3); backdrop-filter:blur(4px); color:#fff; padding:24px; text-align:center; }
.vp .vprep .pmsg{ font-weight:700; font-size:var(--fs-read); }
.vp .vprep .spin{ width:44px; height:44px; border-radius:50%; border:3px solid var(--ink-5);
  border-top-color:var(--gold); animation:vspin .8s linear infinite; }
@keyframes vspin{ to{ transform:rotate(360deg); } }
.vp .vresume .rt{ font-weight:800; font-size:var(--fs-title); }
.vp .vresume .rbtns{ display:flex; gap:12px; }
.vp .vmenu{ position:absolute; right:14px; bottom:96px; background:#191f28f2; border:1px solid var(--line2);
  border-radius:13px; padding:8px; min-width:200px; display:none; box-shadow:var(--shadow); z-index:6; }
.vp .vmenu.on{ display:block; }
.vp .vmenu .mi{ display:flex; align-items:center; gap:9px; width:100%; padding:10px 12px; border-radius:9px; border:0;
  background:transparent; color:var(--txt); font-size:var(--fs-small); font-weight:600; text-align:left; cursor:pointer; }
.vp .vmenu .mi:hover{ background:var(--raise); }
.vp .vmenu .mi.on{ color:var(--gold2); }
.vp .vmenu .mhd{ padding:9px 12px 4px; color:var(--dim); font-size:var(--fs-micro); font-weight:800;
  text-transform:uppercase; letter-spacing:.07em; }
.vp .vmenu .mhd:not(:first-child){ margin-top:4px; border-top:1px solid var(--line); padding-top:10px; }
.vp .vmenu{ max-height:min(430px,60vh); overflow-y:auto; }

/* ============================================================
   MUSIC PLAYER (mini bar + expanded)
   ============================================================ */
/* ---------- unified bottom dock (PHDock, hub.js) ---------- */
/* A mini-bar the dock has collapsed behind the switcher. */
.dock-collapsed{ display:none !important; }
#phdock-switch{ position:fixed; right:10px; z-index:79; display:flex; gap:6px; transition:bottom .18s ease; }
#phdock-switch .dchip{ display:flex; align-items:center; gap:5px; padding:6px 11px; border-radius:999px;
  background:#141922f2; border:1px solid var(--line2); color:var(--txt); font-size:var(--fs-meta); font-weight:750;
  box-shadow:var(--shadow); cursor:pointer; backdrop-filter:blur(10px); }
#phdock-switch .dchip svg{ pointer-events:none; }
#phdock-switch .dchip span{ pointer-events:none; }

.mubar{ position:fixed; left:8px; right:8px; bottom:calc(var(--ph-bar-h) + 8px);
  z-index:78; background:#1b212bf5; border:1px solid var(--line2); border-radius:14px; padding:8px 12px;
  display:none; align-items:center; gap:11px; box-shadow:var(--shadow); backdrop-filter:blur(14px); cursor:pointer;
  transition:bottom .18s ease; }
/* Inside a full-screen page (playlist/title detail) the bottom nav is covered —
   pin the mini-player to the real bottom instead of floating at nav height. */
body:has(.detail.on) .mubar{ bottom:calc(var(--sa-b) + 10px); }
.mubar.on{ display:flex; }
.mubar .cov{ width:40px; height:40px; border-radius:9px; object-fit:cover; background:var(--raise);
  display:flex; align-items:center; justify-content:center; font-size:var(--fs-head); flex:0 0 40px; }
.mubar .mi{ flex:1; min-width:0; }
.mubar .m1{ font-weight:700; font-size:var(--fs-small); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.mubar .m2{ color:var(--faint); font-size:var(--fs-meta); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.mubar .mbtns{ display:flex; gap:2px; }
.mubar button{ -webkit-appearance:none; appearance:none; background:transparent; border:0; color:var(--txt);
  font-size:var(--fs-head); width:34px; height:38px; cursor:pointer; border-radius:50%; padding:0;
  display:flex; align-items:center; justify-content:center; }
.mubar button:active{ background:var(--raise); }
.mubar .mkill{ color:var(--faint); font-size:var(--fs-small); }
.mubar .mkill:active{ color:var(--txt); }
.mubar .mprog{ position:absolute; left:12px; right:12px; bottom:0; height:2.5px; border-radius:2px; background:var(--ink-4); overflow:hidden; }
.mubar .mprog>i{ display:block; height:100%; background:var(--gold); }

.munow{ position:fixed; inset:0; z-index:85; background:linear-gradient(180deg,#1c1c1c,#141414 70%); display:none;
  flex-direction:column; align-items:center; padding:calc(var(--sa-t) + 18px) 26px calc(var(--sa-b) + 26px);
  /* V2-D6: the player had NO overflow, so on a short screen — or with lyrics
     open, or the queue expanded — anything past the fold was simply clipped and
     unreachable. Only the inner lyrics box scrolled, which is "just the bottom
     part" in Pierre's words. The whole sheet scrolls now.
     Swipe-down-to-dismiss already asks the scroller whether it is at the top
     before arming, so scrolling up here still dismisses and scrolling down in
     the middle still scrolls. */
  overflow-y:auto; -webkit-overflow-scrolling:touch; overscroll-behavior:contain; }
/* Children must not shrink to fit a now-scrollable parent. */
.munow > *{ flex-shrink:0; }
.munow.on{ display:flex; animation:vin .2s ease; }
.munow .close{ align-self:flex-start; background:var(--raise); border:1px solid var(--line2); color:var(--txt);
  width:38px; height:38px; border-radius:50%; font-size:var(--fs-title); cursor:pointer; }
.munow .bigcov{ width:min(72vw,300px); height:min(72vw,300px); border-radius:22px; object-fit:cover; background:var(--card);
  box-shadow:0 18px 50px var(--scrim-2); margin:26px 0 20px; display:flex; align-items:center; justify-content:center; font-size:60px /* glyph box, not type */; }
/* THE TRANSPORT CONTROLS MOVED DEPENDING ON THE SONG. Both lines wrapped, so a
   long title took the title from 28px to 55px and a long artist+album took the
   caption from 21 to 41 - measured at 390x844, that walked the seek bar from
   y463 to y511 and the play/pause row from y528 to y576. Forty-eight pixels of
   drift means your thumb lands on a different control depending on what is
   playing. Both lines are now single-line and ellipsised, so the player is the
   same shape for every song. */
.munow .n1{ font-size:var(--fs-head); font-weight:800; text-align:center;
  max-width:100%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.munow .n2{ color:var(--dim); font-size:var(--fs-small); margin-top:4px; text-align:center;
  max-width:100%; display:flex; align-items:center; justify-content:center; gap:8px; white-space:nowrap; }
/* The caption is a flex row (links + the Lyrics button), so the ellipsis has to
   live on the text half and min-width:0 is what lets it actually shrink. */
.munow .n2 #mn-t2x{ min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.munow .n2 #mn-lyrbtn{ flex:0 0 auto; margin-left:0 !important; }
/* Artist and album in the player navigate, so they have to look like it. An
   underline rather than a colour: the player sits under large artwork and a
   coloured link fights it. */
.munow .n2 .mnlink{ color:var(--txt); cursor:pointer; text-decoration:underline;
  text-underline-offset:3px; text-decoration-thickness:1px; text-decoration-color:var(--line2); padding:2px 0; }
.munow .n2 .mnlink:active{ opacity:.55; }
.munow .n2 .mnsep{ color:var(--faint); }
.munow .mseek{ width:100%; max-width:420px; margin:22px 0 4px; }
.munow .mtimes{ display:flex; justify-content:space-between; width:100%; max-width:420px; color:var(--faint);
  font-size:var(--fs-meta); font-variant-numeric:tabular-nums; }
.munow .mvol{ display:flex; align-items:center; gap:11px; width:100%; max-width:420px; margin:14px 0 2px; }
.munow .mvol .vic{ color:var(--dim); cursor:pointer; display:flex; flex:0 0 auto; }
.munow .mvol .vic:active{ color:var(--txt); }
.munow .mvol .slider{ flex:1; }
/* Transport row: 7 controls must CENTER and FIT every phone. Fixed 52px buttons
   + 20px gaps totalled 498px — wider than an iPhone (390px), so the row leaked
   out of the viewport. Fluid gap + a narrow-screen size step keep the whole row
   inside 360px-wide screens with the play button always dead-centre. */
.munow .mctl{ display:flex; align-items:center; justify-content:center; gap:clamp(4px,2.4vw,20px);
  margin-top:14px; width:100%; max-width:420px; }
.munow .mctl button{ background:transparent; border:0; color:var(--txt); font-size:var(--fs-big); cursor:pointer;
  width:52px; height:52px; border-radius:50%; flex:0 0 auto; display:flex; align-items:center; justify-content:center; padding:0; }
.munow .mctl .pp{ background:var(--gold-grad); color:#141414; width:66px; height:66px; font-size:var(--fs-big); }
@media (max-width:460px){
  .munow .mctl button{ width:44px; height:44px; font-size:var(--fs-head); }
  .munow .mctl .pp{ width:58px; height:58px; font-size:var(--fs-big); }

}
.munow .mctl .sh.on, .munow .mctl .rp.on{ color:var(--gold2); }
.munow .close.on{ color:var(--gold2); border-color:var(--gold2); }
/* Driving mode: giant art + three oversized targets, all secondary chrome gone.
   Sized for a glance-and-tap while the phone is dashboard-mounted. */
.munow.driving .bigcov{ width:min(90vw,440px); height:min(90vw,440px); cursor:pointer; }
.munow.driving .n1{ font-size:var(--fs-big); }
.munow.driving .n2{ font-size:var(--fs-title); }
/* B3 . Fewer things to hit. The top row normally carries eight controls; on a
   dashboard the only two that belong are "leave the player" and "leave driving
   mode". Sleep timer, output picker, cast, the ... menu and the playing-from
   line are all parked behind leaving driving mode - none of them is a
   glance-and-tap action, and every extra target makes the two real ones
   smaller and easier to miss. */
.munow.driving #mn-ctx,
.munow.driving #mn-sleep,
.munow.driving #mn-out,
.munow.driving #mn-cast,
.munow.driving #mn-more,
.munow.driving #mn-lyrbtn,
.munow.driving .mvol,
.munow.driving #mn-lyrics,
.munow.driving #mn-queue,
.munow.driving #mn-sh,
.munow.driving [data-mu-rp],
.munow.driving #mn-speed{ display:none !important; }
/* Top-bar speed chip: text among icon buttons — auto width, chip typography.
   (The transport row is the symmetric Spotify five: shuffle prev PLAY next
   repeat — play is dead-centre; speed lives up here now, sleep button gone.) */
.munow .close.mnspeed{ width:auto; min-width:42px; padding:0 12px; font-size:var(--fs-small); font-weight:800; margin-left:8px; }
/* B1 — THE SEEK BAR WAS IN THAT HIDE LIST. Driving mode kept prev/play/next
   and took away the only way to move inside a song, which is exactly the
   report: "I can't skip through the song with my finger sliding through the
   song bar." It is back, scaled for a thumb aimed at a phone on a dashboard
   rather than a fingertip on a phone in your hand — a 14px track inside a 52px
   touch band, with a thumb you can find without looking straight at it. Safe to
   add height here because the player scrolls now (D6); before that, anything
   past the fold was unreachable. */
.munow.driving .mseek{ margin:26px 0 8px; }
.munow.driving .mseek .slider{ height:52px; }
.munow.driving .mseek .slider::-webkit-slider-runnable-track{ height:14px; border-radius:7px; }
.munow.driving .mseek .slider::-webkit-slider-thumb{ width:34px; height:34px; margin-top:-10px; }
.munow.driving .mseek .slider::-moz-range-track{ height:14px; border-radius:7px; }
.munow.driving .mseek .slider::-moz-range-progress{ height:14px; border-radius:7px; }
.munow.driving .mseek .slider::-moz-range-thumb{ width:34px; height:34px; }
/* Elapsed / remaining is the readout you glance at, so it is glance-sized. */
.munow.driving .mtimes{ font-size:var(--fs-title); font-weight:700; color:var(--dim); }
/* The two buttons that remain are thumb-sized, and pushed apart so a miss on
   one cannot land on the other. */
.munow.driving > div:first-child{ justify-content:space-between; }
.munow.driving #mn-down,
.munow.driving #mn-drive{ width:54px; height:54px; font-size:var(--fs-head); }
.munow.driving .mctl{ gap:34px; margin-top:26px; }
.munow.driving .mctl button{ width:84px; height:84px; font-size:36px /* glyph box, not type */; }
.munow.driving .mctl .pp{ width:104px; height:104px; font-size:var(--fs-giant); }
/* What's-new sheet rows */
.wnrow{ padding:11px 0; border-bottom:1px solid var(--line2); }
.wnrow:last-of-type{ border-bottom:0; }
.wnt{ font-weight:800; font-size:var(--fs-read); margin-bottom:3px; }
.wnd{ color:var(--dim); font-size:var(--fs-small); line-height:1.5; }
/* Styled range sliders (Apple-Music grade): accent progress fill up to --p
   (JS repaints on every value change), white thumb with depth, a 24px touch
   band, and touch-action:none so grabbing one NEVER turns into a page scroll
   or the now-playing swipe-down dismiss. */
input[type=range].slider{ -webkit-appearance:none; appearance:none; width:100%; height:24px;
  background:transparent; outline:none; touch-action:none; margin:0; }
input[type=range].slider::-webkit-slider-runnable-track{ height:6px; border-radius:3px;
  background:linear-gradient(90deg, var(--accent) 0%, var(--accent) var(--p,0%), rgba(255,255,255,.22) var(--p,0%)); }
input[type=range].slider::-webkit-slider-thumb{ -webkit-appearance:none; width:18px; height:18px; margin-top:-6px;
  border-radius:50%; background:#fff; border:0; box-shadow:0 2px 8px rgba(0,0,0,.55); cursor:pointer; }
input[type=range].slider:active::-webkit-slider-thumb{ transform:scale(1.15); }
input[type=range].slider::-moz-range-track{ height:6px; border-radius:3px; background:rgba(255,255,255,.22); }
input[type=range].slider::-moz-range-progress{ height:6px; border-radius:3px; background:var(--accent); }
input[type=range].slider::-moz-range-thumb{ width:18px; height:18px; border-radius:50%; background:#fff; border:0;
  box-shadow:0 2px 8px rgba(0,0,0,.55); }

/* ============================================================
   MUSIC (Spotify-style rows, playlist pages)
   ============================================================ */
/* Measured against Spotify's own mobile web at a 390px viewport (album and
   artist pages), not from memory. Spotify: 64px row pitch, 48px artwork at 4px
   radius sitting on the 16px content inset, title 16/400 white, caption 13/400
   #b3b3b3. Ours was 60 / 46 / r8 / x22 / 14.5-600 / 12px #808080 — every value
   a little tighter, a little bolder and a lot dimmer.
   The negative margin is what puts the artwork ON the 16px inset while the
   press highlight still bleeds 12px past it on each side, which is exactly the
   382px-wide box on a 358px content column that Spotify measures at. */
.murow{ display:flex; align-items:center; gap:12px; padding:8px 12px; margin:0 -12px; border-radius:10px; cursor:pointer; }
.murow:active{ background:var(--surface); }
/* The player's queue is an overflow-x:auto box, so the row's edge-bleed turned
   into a real 12px horizontal scroll (measured: scrollWidth 342 vs clientWidth
   330). Inside it the row keeps its metrics and loses only the bleed. */
/* R6 G6.2/G6.3 — the queue is part of the page, not a box inside it. It gets a
   heading and real room, and the sheet keeps scrolling past the last row so the
   final song is not jammed against the home indicator. */
.mnq{ padding-bottom:calc(var(--sa-b) + 24px); }
/* R6 G6.4 — FOUND BY DRIVING IT at 375x812. Three things were wrong here and
   all three are the kind you only see on a phone:
     1. the queue header (toggle + Clear + Save as playlist) was wider than the
        screen, so the whole queue overflowed sideways;
     2. song titles were CUT mid-word instead of ellipsised, because the flex
        child had no min-width to shrink into;
     3. the header buttons were 30px tall — under the 44px anybody can
        reliably hit with a thumb. */
.mnq .sect{ flex-wrap:wrap; gap:6px; row-gap:8px; }
.mnq .sect .btn{ min-height:40px; padding:0 12px; }
.mnq .qtoggle{ flex:0 0 auto; }
#mn-queue .murow{ min-width:0; }
#mn-queue .murow .mi{ min-width:0; flex:1; }
#mn-queue .m1, #mn-queue .m2{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; }
.munow .close{ min-width:40px; min-height:40px; }
/* The Up next / History tabs were 24px tall and the row dots 38 — both under a
   thumb. Measured again after the first pass; these were what was left. */
.mnq .qtab{ min-height:40px; padding:0 14px; }
#mn-queue .mdots{ min-width:44px; min-height:44px; }
.mnq .mnqh{ font-size:var(--fs-micro); font-weight:800; letter-spacing:.09em; text-transform:uppercase;
  color:var(--dim); margin:2px 6px 8px; text-align:left; }
#mn-queue .murow{ margin:0; padding:10px 6px; touch-action:pan-y; }
#mn-queue .qgrab{ flex:0 0 22px; color:var(--faint); font-size:var(--fs-small); letter-spacing:-2px;
  cursor:grab; touch-action:none; user-select:none; text-align:center; }
#mn-queue .qgrab:active{ cursor:grabbing; color:var(--txt); }
#mn-queue .qmeta{ cursor:pointer; text-decoration:underline; text-underline-offset:2px;
  text-decoration-color:var(--line2); }
.murow .mcov{ width:48px; height:48px; border-radius:4px; object-fit:cover; background:var(--card); flex:0 0 48px;
  display:flex; align-items:center; justify-content:center; color:var(--faint); font-size:var(--fs-head); }
/* Cover wrapper exists so a play affordance can sit ON the artwork, the way
   every music app signals "this row is playable". Pointer-only: a phone has no
   hover, and a permanent triangle over every cover would just be noise. */
.mcovw{ position:relative; flex:0 0 48px; width:48px; height:48px; display:block; border-radius:4px; overflow:hidden; }
.mcovp{ position:absolute; inset:0; display:none; align-items:center; justify-content:center;
  background:rgba(0,0,0,.55); color:#fff; }
@media (hover:hover) and (pointer:fine){
  .murow:hover{ background:var(--surface); }
  .murow:hover .mcovp{ display:flex; }
}
/* Artist and album columns only exist on a wide screen (see the grid below);
   phones keep the single "Artist · Album" caption under the title. */
.murow .martist, .murow .malbum{ display:none; }
/* Music-cover ♪ placeholder (cover absent OR failed to load) — consistent even
   outside a .murow (ready-to-watch row, playlists, mcovFail replacements). */
div.mcov, div.plgcov{ display:flex; align-items:center; justify-content:center;
  background:var(--card); color:var(--accent-text); font-size:var(--fs-head); border-radius:8px; }
.murow .mi{ flex:1; min-width:0; }
.murow .m1{ font-size:var(--fs-title); font-weight:400; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.murow .m1.playing{ color:var(--accent-text); }
.murow .dlmark{ color:#1db954; font-size:var(--fs-micro); margin-left:6px; vertical-align:1px; }
.murow .m2{ font-size:var(--fs-small); color:var(--dim); margin-top:1px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* The download-group header (.plghead) was NOT in this list, so its ⋯ rendered
   as a raw browser button - grey, boxy and out of place next to the styled
   rows. It gets the same treatment as everywhere else. */
.murow .mdots, .pltile .mdots, .plghead .mdots{ -webkit-appearance:none; appearance:none; background:transparent; border:0;
  color:var(--dim); width:38px; height:38px; border-radius:50%; font-size:var(--fs-head); cursor:pointer; flex:0 0 38px;
  display:flex; align-items:center; justify-content:center; padding:0; margin-left:auto; }
.murow .mdots:active, .pltile .mdots:active, .plghead .mdots:active{ background:var(--raise); }
.plghead .mdots:hover{ color:var(--txt); background:var(--ink-2); }
/* Let the TITLE take the free space and the controls sit at the right edge.
   Pushing the buttons over with margin-left:auto also works, but it starves
   the title: measured at 900px it left the name only 90px (truncating long
   playlist names) with 631px of dead space beside it. Giving .grow the flex
   instead means the name gets the room and the buttons still hug right. */
.plghead .grow{ flex:1 1 auto; min-width:0; }
.plghead .btn.sm{ flex:0 0 auto; margin-left:0; }
.plghead .mdots{ margin-left:0; }
.plghead .plgchev{ flex:0 0 auto; display:flex; align-items:center; color:var(--faint); }
/* Like (heart). Outline when unliked, filled gold when liked — the same
   language Spotify uses. `margin-left:auto` lives on .mdots, so the heart sits
   directly left of it without needing its own spacer. */
.murow .mlike{ -webkit-appearance:none; appearance:none; background:transparent; border:0; padding:0;
  color:var(--faint); width:34px; height:34px; flex:0 0 34px; border-radius:50%; cursor:pointer;
  display:flex; align-items:center; justify-content:center; margin-left:auto; transition:color .12s, transform .12s; }
.murow .mlike svg{ fill:none; }
.murow .mlike.on{ color:var(--accent-text); }
.murow .mlike.on svg{ fill:currentColor; }
.murow .mlike:active{ transform:scale(.86); }
/* the heart already claimed margin-left:auto, so the dots must not claim it again */
.murow .mlike ~ .mdots{ margin-left:0; }
.pltile .plcov.liked{ background:linear-gradient(135deg,#7a5cff,#c94b9b); color:#fff; }
/* "now playing" equalizer bars next to the gold title */
.eq{ display:inline-flex; align-items:flex-end; gap:2px; height:11px; margin-right:6px; }
.eq i{ width:2.5px; background:var(--gold2); animation:eqb 1s ease-in-out infinite; border-radius:1px; }
.eq i:nth-child(1){ height:5px; } .eq i:nth-child(2){ height:10px; animation-delay:.25s; } .eq i:nth-child(3){ height:7px; animation-delay:.5s; }
@keyframes eqb{ 0%,100%{ transform:scaleY(.5);} 50%{ transform:scaleY(1);} }
.muplay-pills{ display:flex; gap:9px; margin:4px 0 12px; }
.muplay-pills .btn{ flex:0 0 auto; display:flex; align-items:center; gap:6px; border-radius:999px; padding:10px 20px; }

/* ---- One-page music: shelves above the song list ---------------------- */
/* #muall replaced this box, so it only holds filter state now. */
#mufilter{ display:none !important; }
.mushelf{ margin:2px 0 14px; }
.mushelf .sect{ display:flex; align-items:center; gap:9px; margin:6px 0 8px; }
.mushelf .sect h2{ display:flex; align-items:center; gap:7px; }
.mushelf .sect .btn.sm{ margin-left:auto; flex:0 0 auto; }
/* Two buttons on one shelf heading: "See all" takes the margin-left:auto and
   Play sits beside it, rather than both fighting for it and stacking. */
.mushelf .sect .shelf-all{ margin-left:auto; }
.mushelf .sect .shelf-all ~ .btn.sm{ margin-left:0; }
.mutile{ flex:0 0 128px; width:128px; cursor:pointer; }
.mutile .shelfcov{ width:128px; height:128px; flex:0 0 128px; border-radius:10px;
  object-fit:cover; background:var(--card); display:flex; align-items:center;
  justify-content:center; color:var(--faint); }
.mutile .t{ margin-top:7px; font-size:var(--fs-small); font-weight:600; white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis; }
.mutile .s{ font-size:var(--fs-meta); color:var(--faint); white-space:nowrap;
  overflow:hidden; text-overflow:ellipsis; }
@media (hover:hover){ .mutile:hover .shelfcov{ outline:2px solid var(--accent); outline-offset:2px; } }
/* R9 — the shelf tile's ... menu. Same position, size and reveal rule as the
   artist/album grid's .mgtile-play, because it is the same gesture on the same
   kind of tile and the two should not look like different ideas. */
.mutile-cw{ position:relative; display:block; line-height:0; }
.mutile-more{ position:absolute; right:6px; bottom:6px; width:30px; height:30px; border-radius:50%;
  border:0; background:rgba(20,20,20,.72); color:#fff; display:flex; align-items:center; justify-content:center;
  backdrop-filter:blur(6px); cursor:pointer; opacity:0; transition:opacity .15s ease, transform var(--ph-press,120ms); }
.mutile-more:active{ transform:scale(.9); }
@media (hover:none){ .mutile-more{ opacity:1; } }
@media (hover:hover){ .mutile:hover .mutile-more, .mutile:focus-within .mutile-more{ opacity:1; } }

.chtabs{ margin:2px 0 6px; }

/* Chunk 41 - the Top 100. A chart row is a normal song row with its position
   in front of it: the rank is the whole point of a chart, so it gets a fixed
   column that never collapses, and the tabular figures keep 1 and 100 in the
   same lane. */
.chrow{ display:flex; align-items:center; gap:10px; }
.chrank{ flex:0 0 30px; text-align:right; font-size:var(--fs-small); font-weight:800;
  color:var(--dim); font-variant-numeric:tabular-nums; }
.chrow.has .chrank{ color:var(--accent-text); }
.chrow .mcovw{ flex:0 0 auto; }
.chrow .mi{ flex:1 1 auto; min-width:0; }
.chact{ flex:0 0 auto; color:var(--dim); display:flex; align-items:center; padding:0 4px; }
.chrow.has .chact{ color:var(--accent-text); }

/* Chunk 36 - Backup at 1280 was one 1030px column: the primary button ran a
   thousand pixels wide and the explanations ran ~160 characters to the line.
   Nothing was broken; it was a phone page stretched. The status, progress and
   failure blocks get a reading measure and centre; the photo GRID keeps the
   whole window, because more columns of photographs is what a wide screen is
   actually good for. */
#v-photos #bk-status, #v-photos #bk-progress, #v-photos #bk-failed{
  max-width:720px; margin-left:auto; margin-right:auto; }
#v-photos .sw-empty{ max-width:62ch; margin-left:auto; margin-right:auto; }

/* Chunk 34 - a failed backup stays on screen. It is the one outcome on this
   page that a toast cannot carry: you need the names to know what to do, and
   a button to do it with. Bad-coloured but not alarming - nothing was lost,
   the files are still on the phone. */
.bk-fail{ padding:14px 16px; border:1px solid color-mix(in srgb, var(--bad) 38%, transparent); }
.bk-fail-h{ font-size:var(--fs-title); font-weight:800; margin-bottom:8px; }
.bk-fail-r{ display:flex; gap:10px; align-items:baseline; padding:3px 0; font-size:var(--fs-small); }
.bk-fail-r .n{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.bk-fail-r .w{ flex:0 0 auto; color:var(--dim); }
.bk-fail-a{ display:flex; gap:9px; margin-top:12px; }
.bk-fail-a .btn{ flex:1 1 auto; }

/* Chunk 31 - searching hides the home page underneath it. Measured: with a
   query typed, the greeting, the three starter cells and the "New from your
   artists" shelf still filled the first two screens, and the matching songs
   began below the fold. Hidden by class, so clearing the box restores them
   without a rebuild. */
#v-music.mu-searching #mu-greet,
#v-music.mu-searching #mu-quick,
#v-music.mu-searching #mu-new,
#v-music.mu-searching #mu-deep{ display:none; }
.musearch-none{ text-align:left; padding:10px 2px; }

/* S3 — while a query is up there is ONE list, and #mu-local owns it.
   The Library pane filters itself on every keystroke (renderMuLib), so on the
   Library tab the same owned songs were printed twice, a screen apart, with the
   downloadable ones below both. Listening is a different question entirely.
   `!important` is REQUIRED, not decoration: muTab writes an inline
   `style.display` on these panes, and an inline declaration beats a class rule
   at any specificity. Same trick as `#mufilter` (below) and the YouTube
   in-view bar. */
#v-music.mu-searching #mu-lib,
#v-music.mu-searching #mu-stats{ display:none !important; }

/* S3 — which rows you already have, and which you do not. Pierre asked for
   "just an icon or a label to make it clear which ones are downloaded and which
   ones aren't"; in-flight rows reuse .chip.info / .chip.bad rather than growing
   a third vocabulary. No new tokens.

   It lives on the CAPTION line, ahead of the artist, and NEVER on the title
   line. Measured at 390px: as an inline prefix to the title it took enough
   width that eight consecutive results all read "BIR…" — one merged list you
   cannot read is worse than two you can. `nowrap` so the marker itself is never
   what gets cut; `.m2` already ellipsises, so the artist/album tail is what
   gives way. */
.mown,.mnew{ display:inline-flex; align-items:center; gap:3px; white-space:nowrap;
  font-size:var(--fs-micro); font-weight:700; vertical-align:-1px; }
.mown{ color:var(--ok-text); }
.mnew{ color:var(--dim); }
/* A chip is a control-sized thing; on a caption line it only has to be a word. */
#mu-local .m2 .chip, #mu-local .martist .chip{ padding:1px 7px; font-size:var(--fs-micro);
  vertical-align:-1px; }
/* On a phone the word on the Get button costs the artist its last ~30px, and
   the artist is the ENTIRE signal when Deezer returns nine covers of one song
   under nine identical titles. Measured: 5 of 12 artists clipped with the word,
   because an unowned row carries three controls where an owned one carries two.
   The download glyph plus title/aria-label say the same thing in a third of the
   space. Above 420px there is room for both. */
@media (max-width:420px){
  #mu-local .murow .btn .btl{ display:none; }
  #mu-local .murow .btn{ padding:7px 9px; }
}

/* ---- Music library control row -----------------------------------------
   Filter + browse mode + Play/Shuffle. Stacked on a phone (each part wants the
   full width), one line from 760px up — which is where the four separate rows
   used to push the first song 260px down a 900px-tall window. */
.muctl{ display:flex; flex-direction:column; gap:8px; margin-bottom:8px; }
.muctl #mufilter{ margin:0; }

/* Chunk 30 - browsing loses the controls that do the browsing. Measured at
   390x844 with 51 albums: scrolled 1400px down, the Songs / Artists / Albums
   chips sat 1220px ABOVE the viewport, and the only way back to them was to
   scroll the whole grid up again. Films & TV already sticks its control row
   (.libbar); Music now does the same. The top bar is 50px plus the safe-area
   inset, so the chips park directly under it, and the covers pass BEHIND an
   opaque strip rather than through a transparent one. Only .muctl sticks -
   .musmart underneath it would cost another 46px of a phone screen for
   secondary actions. */
#mu-lib .muctl{ position:sticky; top:calc(var(--ph-top-h) + var(--ph-tabs-h)); z-index:24;
  /* SOLID for its own height, with the fade hung underneath it (::after).
     A gradient that fades over the row itself lets whatever is scrolling past
     show THROUGH the row: seen at 844x390, where the previous song's cover and
     its artist name sat inside the chip band and read as a stuck half-row. The
     soft edge is worth keeping - it just belongs below the row, not in it. */
  background:var(--bg);
  padding:8px 0 8px; margin-bottom:0; }
#mu-lib .muctl::after{ content:''; position:absolute; left:0; right:0; top:100%; height:12px;
  background:linear-gradient(180deg, var(--bg), transparent); pointer-events:none; }
.muctl #mu-modes{ padding-bottom:0; }
.muacts{ display:flex; gap:9px; }
.muacts .btn{ flex:1 1 auto; display:flex; align-items:center; justify-content:center; gap:6px;
  border-radius:999px; padding:10px 20px; }
@media (min-width:760px){
  .muctl{ flex-direction:row; align-items:center; gap:12px; margin-bottom:10px; }
  .muctl #mufilter{ flex:1 1 auto; min-width:0; }
  .muctl #mu-modes{ flex:0 0 auto; overflow:visible; }
  .muacts{ flex:0 0 auto; }
  .muacts .btn{ flex:0 0 auto; }
}

/* ---- Music library as real columns on a wide screen --------------------
   The flat flex row left a measured 630px of dead space between the artist
   caption and the buttons at 1280px. From 900px up a track row becomes a grid —
   cover | title+artist | album | like | ⋯ — so the width carries information
   instead of nothing.

   Keyed on `.mucols`, NOT on the containing list: artist/album GROUP rows are
   also `.murow` but have no album cell, and container-scoping this stranded
   their shuffle button in the middle of the row. */
@media (min-width:900px){
  .murow.mucols{
    display:grid; align-items:center;
    grid-template-columns:48px minmax(0,1.5fr) minmax(0,1fr) minmax(0,1fr) 34px 38px;
    gap:14px; padding:6px 10px; margin:0;
  }
  /* The stacked caption is a phone affordance — on a wide row the artist gets a
     real column of its own. It is always present, unlike the album, so the
     middle of the row is never blank. */
  .murow.mucols .m2{ display:none; }
  .murow.mucols .martist, .murow.mucols .malbum{
    display:block; font-size:var(--fs-small);
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  }
  .murow.mucols .martist{ color:var(--dim); }
  .murow.mucols .malbum{ color:var(--faint); font-size:var(--fs-meta); }
  /* Vertically centre a title that no longer has a caption under it. */
  .murow.mucols .m1{ font-size:var(--fs-small); font-weight:600; }
  /* The grid places these, so the old auto-margins would fight it. */
  .murow.mucols .mlike, .murow.mucols .mdots{ margin-left:0; }
  /* Long-form reading measure: a song list stretched across a 2560px monitor is
     unreadable — the eye loses the row on the way to the album. */
  #mu-lib, #mu-pls{ max-width:1180px; }
}
/* playlist tiles (Spotify "Your Library" look) */
/* A playlist is a .mutile now (chunk 23) — the same tile as albums and
   artists. What is left here is only what a playlist adds: its cover fills
   the tile's art box, and the Liked one keeps its gradient. */
/* Scoped to the grid on purpose: the row rules further down (Library,
   Playlists and Downloaded at one density) still dress .plghead's rows, and
   an unscoped rule here would lose to them by source order. */
/* The IMAGE fills the art box; the box itself keeps its square from
   .mgtile-art. Setting height:100% on the box instead let the grid stretch
   it into a tall rectangle whose caption then collided with the tile below
   — visible at 390px, invisible to a width measurement. */
.mugrid .pltile img.plcov{ width:100%; height:100%; border-radius:0; flex:none; object-fit:cover; }
.mugrid .pltile .mgtile-art.liked{ display:flex; align-items:center; justify-content:center;
  color:#fff; background:linear-gradient(135deg,#7a5cff,#c94b9b); }
.mugrid .pltile .plcov:not(img){ width:100%; height:100%; display:flex; align-items:center;
  justify-content:center; color:var(--gold); font-size:var(--fs-title); }
.mugrid .pltile .mgtile-art{ background:linear-gradient(135deg,#2a3140,#171b23); }
.mugrid .pltile{ padding:0; gap:0; }
/* collapsible download groups */
.plgroup{ margin:6px 0; border:1px solid var(--line); border-radius:13px; overflow:hidden; background:var(--card); }
.plghead{ display:flex; align-items:center; gap:12px; padding:10px; cursor:pointer; }
.plghead:active{ background:var(--surface); }
.plghead .plgcov{ width:46px; height:46px; border-radius:9px; object-fit:cover; flex:0 0 46px;
  background:linear-gradient(135deg,#2a3140,#171b23); display:flex; align-items:center; justify-content:center; color:var(--gold); }
.plghead .pln{ font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.plghead .plm{ font-size:var(--fs-meta); color:var(--faint); }
.plgchev{ color:var(--faint); transition:transform .2s ease; display:inline-flex; }
.plgchev.open{ transform:rotate(180deg); }
.plgbody{ padding:2px 8px 8px; }
/* playlist full page (reuses .detail overlay) */
/* Measured off Spotify's own album page at 390px (see docs/MUSIC-REWORK.md):
   the ARTWORK is the only centred thing - 156px square, hard corners - and the
   title, the meta line and the actions are all left-aligned on the same 16px
   inset as the track rows below them. We were centring the whole block and
   running the artwork at 203px, so the hero ate half the screen and then handed
   over to a left-aligned list. */
.mudetail .mhero{ padding:calc(var(--sa-t) + 54px) max(16px,calc((100% - 700px) / 2 + 16px)) 18px; text-align:left;
  /* chunk 22: --mu-hero is set from the cover's own pixels when it loads;
     the old flat blue-grey is the fallback for a cover we cannot read (a
     black-and-white sleeve, a tainted canvas, no artwork at all). */
  background:linear-gradient(180deg, var(--mu-hero), var(--bg) 92%);
  transition:background .35s ease; }
.mudetail .mhero .bigc{ width:min(40vw,156px); height:min(40vw,156px); border-radius:0; object-fit:cover;
  box-shadow:0 18px 44px var(--scrim-1); background:var(--card); display:flex; align-items:center; justify-content:center;
  margin:0 auto; color:var(--gold); font-size:52px /* glyph box, not type */; }
.mudetail .mhero .pn{ font-size:var(--fs-big); font-weight:700; margin-top:16px; }
.mudetail .mhero .pm{ font-size:var(--fs-small); color:var(--txt); margin-top:8px; }
/* R9 — a name inside a heading or a subtitle that opens the thing it names.
   A <button> so it is reachable by keyboard and announced as a control, styled
   all the way back down to text so the line still reads as the sentence it is.
   Same treatment the player's second line has had since R5-G6 (.mnlink). */
.mhlink{ -webkit-appearance:none; appearance:none; background:none; border:0; padding:0;
  font:inherit; color:inherit; cursor:pointer; text-decoration:underline;
  text-decoration-color:var(--line2); text-underline-offset:3px; }
.mhlink:hover{ color:var(--accent-text); text-decoration-color:currentColor; }
.mhlink:active{ opacity:.55; }
/* The hero's actions. Spotify at 390px: 48px transparent icon buttons packed at
   the left starting at x=4 (24px glyphs), and a 56px FILLED round play button
   hard right at x=318 - its right edge landing on the same 16px inset as
   everything else. We were showing a pair of equal-weight pills, so "play this
   album" carried no more emphasis than any other control.
   The fill is OUR accent, not Spotify's green: copying a competitor's brand
   colour in would be wrong, and it would clash with every other accent here. */
.mhero-acts{ display:flex; align-items:center; gap:0; margin:14px -12px 0; }
.mhero-ic{ -webkit-appearance:none; appearance:none; background:transparent; border:0; color:var(--dim);
  width:48px; height:48px; flex:0 0 48px; border-radius:50%; padding:0; cursor:pointer;
  display:flex; align-items:center; justify-content:center; font-size:var(--fs-big); }
.mhero-ic:active{ background:var(--raise); }
.mhero-fab{ -webkit-appearance:none; appearance:none; border:0; margin-left:auto; margin-right:12px;
  width:56px; height:56px; flex:0 0 56px; border-radius:50%; background:var(--gold2); color:#000;
  padding:0; cursor:pointer; display:flex; align-items:center; justify-content:center;
  box-shadow:0 8px 20px var(--scrim-1); }
.mhero-fab:active{ transform:scale(.94); }
/* 14px here put the detail pages' artwork at x=14 while the Library tab put it
   at x=16 - a 2px step you feel when you move between them. Spotify is 16
   everywhere, and so are we now. */
.mudetail .mbody{ padding:6px 16px calc(var(--sa-b) + 120px); max-width:700px; margin:0 auto; }
/* The wide grid row carries 10px of its own inner padding, so its artwork and ⋯
   sit 10px inside the row box; the hero has to match the row CONTENT or the
   album title is staggered 10px from the covers under it. On a phone the row has
   no such inset and 16px already lines up.
   Keyed to 900px, NOT 720: that is where .murow.mucols actually switches to the
   column grid, and the 10px is that grid's own padding. Keyed to 720 (measured)
   it staggered the hero 10px the OTHER way for every width in 720-899, where the
   rows are still in phone mode - the base rule's max(16px, ...) already puts the
   hero on the 700px column correctly there.
   This block sits AFTER the base .mudetail .mhero rule on purpose: a media query
   adds NO specificity, so an override written earlier in the file simply loses. */
@media (min-width:900px){
  .mudetail .mhero{ padding-left:max(26px,calc((100% - 700px) / 2 + 26px));
    padding-right:max(26px,calc((100% - 700px) / 2 + 26px)); }
}
/* Spotify's section headings measure 20px/700 at a 390px viewport; ours were the
   app-wide h2 at 17px. Scoped to music on purpose - the global h2 is shared with
   Home, Live, Netflix, YouTube and Settings, and this chunk is the music page. */
#v-music .sect h2, .mudetail .sect h2{ font-size:var(--fs-head); }

/* ============================================================
   TITLE DETAIL (Netflix-style) — full-screen show/movie page
   ============================================================ */
.detail{ position:fixed; inset:0; z-index:75; background:var(--bg); overflow-y:auto; display:none;
  overscroll-behavior:none; }
.detail.on{ display:block; animation:vin .2s ease; }
.detail .hero{ position:relative; height:min(56vh,540px); min-height:320px; background-size:cover; background-position:center 16%; }
.detail .hero::after{ content:''; position:absolute; inset:0;
  background:linear-gradient(180deg, rgba(20,20,20,.1) 0%, rgba(20,20,20,.32) 42%, rgba(20,20,20,.82) 82%, var(--bg) 100%),
             linear-gradient(90deg, rgba(20,20,20,.45) 0%, rgba(20,20,20,0) 52%); }
.detail .hero.noimg{ background:radial-gradient(120% 90% at 72% 20%,#3a3a3a,#232323 55%,#141414); }
.detail .hback{ position:absolute; top:calc(var(--sa-t) + 14px); left:16px; z-index:3;
  width:42px; height:42px; border-radius:50%; background:var(--scrim-1); backdrop-filter:blur(6px);
  border:0; color:#fff; display:flex; align-items:center; justify-content:center; cursor:pointer; transition:background .18s; }
.detail .hback:hover{ background:var(--scrim-2); }
.detail .hbody{ position:relative; z-index:2; margin-top:min(-15vh,-120px);
  padding:0 clamp(16px,4vw,44px) calc(var(--sa-b) + 48px); max-width:1040px; margin-left:auto; margin-right:auto; }
.detail .dtop{ display:flex; gap:20px; align-items:flex-end; }
.detail .dposter{ width:clamp(96px,12vw,140px); aspect-ratio:2/3; height:auto; border-radius:10px; object-fit:cover;
  box-shadow:0 14px 40px rgba(0,0,0,.6); flex:0 0 auto; background:var(--card); }
.detail .dtitle{ font-size:clamp(28px,4.4vw,50px); font-weight:900; line-height:1.02; letter-spacing:-.5px;
  text-shadow:0 2px 20px rgba(0,0,0,.55); }
.detail .dmeta{ color:var(--dim); font-size:var(--fs-small); margin-top:11px; display:flex; flex-wrap:wrap; gap:12px; align-items:center; font-weight:600; }
.detail .drate{ display:inline-flex; align-items:center; gap:4px; color:var(--accent-text); font-weight:800; }
.detail .dgenres{ display:flex; gap:8px; flex-wrap:wrap; margin:16px 0 4px; }
.detail .dgenres .chip{ background:rgba(255,255,255,.08); border:1px solid rgba(255,255,255,.14); border-radius:16px;
  padding:4px 12px; font-size:var(--fs-meta); font-weight:600; color:var(--dim);
  cursor:pointer; transition:background .15s, color .15s, border-color .15s; font-family:inherit; }
.detail .dgenres .chip:hover, .detail .dgenres .chip:focus-visible{
  background:rgba(255,255,255,.16); border-color:rgba(255,255,255,.3); color:var(--txt); }
/* Said once, where the missing things would have been. */
.detail .dmetafail{ margin:14px 0 2px; text-align:left; }
.detail .dactions{ display:flex; gap:12px; margin:18px 0 8px; flex-wrap:wrap; align-items:center; }
.detail .dactions .btn{ display:inline-flex; align-items:center; gap:8px; padding:12px 26px; font-size:var(--fs-read); font-weight:800;
  border-radius:7px; flex:0 0 auto; }
.detail .dactions .btn:not(.sec){ background:#fff; color:#141414; }
.detail .dactions .btn:not(.sec):hover{ background:rgba(255,255,255,.82); }
.detail .dactions .btn.sec{ background:rgba(109,109,120,.5); color:#fff; border:0; }
.detail .dactions .btn.sec:hover{ background:rgba(109,109,120,.36); }
.detail .dov{ color:#e5e5e5; font-size:var(--fs-read); line-height:1.6; margin:14px 0 6px; max-width:720px; }
/* overflow-x:auto pans the cast row; no touch-action, or a vertical swipe that
   lands on a face cannot scroll the detail page (see the note by `.vp .seek`). */
/* The cast row is 1,388px of faces in a 350px window on a phone — four times
   the screen — and it hid its scrollbar and showed no other cue, so nine of
   the twelve people in a film were behind a gesture nothing suggested. The
   mask fades the right edge exactly while there is more to the right, and
   disappears at the end of the scroll, which is the cue: a cut-off face reads
   as "keep going", a clean edge as "that is everyone". */
.detail .dcast{ display:flex; gap:16px; overflow-x:auto; padding:8px 2px 14px;
  scroll-snap-type:x proximity; overscroll-behavior-x:contain;
  -webkit-mask-image:linear-gradient(90deg,#000 0,#000 calc(100% - 46px),transparent 100%);
  mask-image:linear-gradient(90deg,#000 0,#000 calc(100% - 46px),transparent 100%); }
/* At the end there is nothing more to promise, so the fade goes away. */
.detail .dcast.atend{ -webkit-mask-image:none; mask-image:none; }
.detail .dcast .dc{ scroll-snap-align:start; }
.detail .dcast::-webkit-scrollbar{ display:none; }
.detail .dc{ flex:0 0 84px; width:84px; text-align:center; cursor:pointer; }
.detail .dc img, .detail .dc .noav{ width:84px; height:84px; border-radius:50%; object-fit:cover; background:var(--card);
  display:flex; align-items:center; justify-content:center; color:var(--faint); transition:transform .18s; }
.detail .dc:hover img{ transform:scale(1.06); }
.detail .dc .cn{ font-size:var(--fs-meta); font-weight:700; margin-top:8px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.detail .dc .cc{ font-size:var(--fs-micro); color:var(--faint); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
@media (max-width:640px){
  .detail .dactions .btn{ flex:1 1 130px; justify-content:center; }
}

/* ---------- gates (login / profile pick) ---------- */
.gate{ position:fixed; inset:0; z-index:60; background:var(--bg); overflow-y:auto;
  display:flex; flex-direction:column; align-items:center; padding:11vh 22px 40px; }
.gate .glogo{ width:74px; height:74px; border-radius:18px; margin-bottom:18px; }
.gate h1{ font-size:var(--fs-big); } .gate .gsub{ color:var(--dim); margin:2px 0 26px; text-align:center; }
.gate .gbox{ width:100%; max-width:360px; display:flex; flex-direction:column; gap:10px; }
/* discreet recovery trigger — a faint dot at the very bottom of the login gate */
.rc-hint{ margin:30px auto 6px; width:46px; height:30px; line-height:28px; text-align:center;
  color:var(--faint); opacity:.32; font-size:var(--fs-big); cursor:pointer; user-select:none; transition:opacity .2s; }
.rc-hint:hover,.rc-hint:active{ opacity:.7; }
.profiles{ display:grid; grid-template-columns:repeat(auto-fill,minmax(96px,1fr)); gap:14px; width:100%; max-width:520px; }
.prof{ display:flex; flex-direction:column; align-items:center; gap:8px; cursor:pointer; padding:10px 4px;
  border-radius:14px; border:1px solid transparent; }
.prof:hover{ background:var(--surface); border-color:var(--line); }
.prof img{ width:64px; height:64px; border-radius:50%; object-fit:cover; background:var(--card); }
.prof .pn{ font-size:var(--fs-small); font-weight:700; text-align:center; }
.prof .lock{ font-size:var(--fs-micro); color:var(--faint); }

/* ---------- misc ---------- */
.statgrid{ display:grid; grid-template-columns:repeat(auto-fit,minmax(130px,1fr)); gap:10px; margin:10px 0; }
/* A <button> now (each stat leads to what it counts), so it has to be reset back
   to looking like the card it always was — a button inherits neither the font
   nor the left-alignment, and the browser's default border would double up. */
.stat{ background:var(--card); border:1px solid var(--line); border-radius:var(--r); padding:12px 14px;
  display:block; width:100%; text-align:left; font:inherit; color:inherit; cursor:pointer;
  -webkit-appearance:none; appearance:none; }
.stat:active{ transform:scale(.985); }
.stat .v{ font-size:var(--fs-head); font-weight:800; color:var(--accent-text); }
.stat .k{ font-size:var(--fs-meta); color:var(--faint); font-weight:600; margin-top:2px; }
.setrow{ display:flex; align-items:center; gap:12px; padding:13px 2px; border-bottom:1px solid var(--line); }
/* min-width:0 — a flex:1 child will not shrink below its content's intrinsic
   width without it, which pushed the owner health rows to 400px inside a
   375px screen. */
.setrow .sk{ flex:1; min-width:0; } .setrow .sk .t{ font-weight:700; font-size:var(--fs-small); }
/* break anywhere: the owner health rows print absolute Windows paths
   ("search interpreter set (C:\Users\...)"), one unbreakable token that
   pushed the whole page 57px wide on a 375px phone. */
.setrow .sk .d{ color:var(--faint); font-size:var(--fs-meta); margin-top:1px;
  overflow-wrap:anywhere; }
.sw{ position:relative; width:46px; height:26px; flex:0 0 46px; cursor:pointer; }
.sw input{ display:none; }
.sw .tr2{ position:absolute; inset:0; border-radius:14px; background:var(--raise); border:1px solid var(--line2); transition:.2s; }
.sw .kn{ position:absolute; top:3px; left:3px; width:20px; height:20px; border-radius:50%; background:var(--dim); transition:.2s; }
.sw input:checked + .tr2{ background:rgba(229,160,13,.35); border-color:var(--gold); }
.sw input:checked + .tr2 + .kn{ left:23px; background:var(--gold2); }

/* ===== settings page (redesign) — scoped to #v-set ===== */
#v-set{ padding-bottom:34px; }
.set-hero{ display:flex; align-items:center; gap:14px; padding:17px 16px; margin:4px 0 2px;
  border-radius:18px; border:1px solid var(--line);
  background:linear-gradient(150deg, rgba(246,199,68,.13), rgba(26,31,41,0) 58%), var(--card); }
.set-hero-av{ width:60px; height:60px; flex:0 0 60px; border-radius:50%; object-fit:cover;
  background:var(--raise); border:2px solid rgba(246,199,68,.4); }
.set-hero-main{ flex:1; min-width:0; }
.set-hero-name{ font-weight:800; font-size:var(--fs-head); letter-spacing:-.01em; color:var(--txt);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.set-hero-chips{ display:flex; flex-wrap:wrap; gap:6px; margin-top:7px; }
.set-hero-chips:empty{ display:none; }
#v-set .sect{ margin:22px 4px 9px; }
#v-set .sect h2{ font-size:var(--fs-meta); font-weight:700; letter-spacing:.09em; text-transform:uppercase; color:var(--faint); white-space:nowrap; flex:0 0 auto; }
/* The one-line summary beside a heading: it never pushes the heading onto two
   lines ("YOUR / FILES" beside a long sentence); it truncates instead. */
#v-set .sect .sub{ font-size:var(--fs-micro); color:var(--faint); text-transform:none; letter-spacing:0; flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
#v-set .card{ background:var(--card); border:1px solid var(--line); border-radius:16px; padding:0 15px; margin:0; }
#v-set .card > .meta{ padding:13px 0 3px; color:var(--dim); font-size:var(--fs-small); line-height:1.5; }
#v-set .setrow{ padding:14px 0; gap:14px; min-height:36px; align-items:center; }
#v-set .setrow .sk .t{ font-size:var(--fs-read); font-weight:650; color:var(--txt); }
#v-set .setrow .sk .d{ font-size:var(--fs-meta); line-height:1.5; margin-top:3px; color:var(--dim); }
#v-set .setrow .btn.sm, #v-set .setrow a.btn{ flex:0 0 auto; white-space:nowrap; }
#v-set .card .empty{ padding:15px 0; text-align:left; }
.skel{ background:linear-gradient(90deg,var(--card),var(--raise),var(--card)); background-size:200% 100%;
  animation:skl 1.2s infinite; border-radius:12px; }
@keyframes skl{ from{ background-position:200% 0; } to{ background-position:-200% 0; } }

/* ---------- polish pass (icons, states, motion) ---------- */
.ic-svg{ vertical-align:-3.5px; flex:0 0 auto; }
button .ic-svg, a .ic-svg{ pointer-events:none; }
.btn .ic-svg{ margin-right:2px; }
.btn.sm .ic-svg{ vertical-align:-3px; }
/* Chunk 39 - this list was five classes long, and the app has many more
   controls that take keyboard focus: the chips that switch music browse mode,
   the sub-tabs on Backup and Live, the buttons the bottom bar puts in its
   slot, the album and photo tiles, a playlist row. Tabbed to, every one of
   them showed NOTHING - measured in Chrome with real Tab keypresses, because
   element.focus() from a script never matches :focus-visible and reported the
   opposite for both a chip and a .btn. */
.btn:focus-visible, .bi:focus-visible, .input:focus-visible, .sopt:focus-visible, .tabb:focus-visible,
.fchip:focus-visible, .sw-tab:focus-visible, .bsb:focus-visible, .mufchip:focus-visible,
.mgtile:focus-visible, .ph-tile:focus-visible, .plrow:focus-visible, .mdots:focus-visible,
.mlike:focus-visible, .mhero-ic:focus-visible, .mhero-fab:focus-visible{
  outline:2px solid var(--gold); outline-offset:2px; }
.btn.sec:hover{ background:#262e3c; border-color:var(--line2); }
.btn.ghost:hover{ color:var(--txt); border-color:var(--line2); background:var(--ink-1); }
.card{ transition:border-color .16s ease; }
.card:hover{ border-color:var(--line2); }
/* .ptile hover/scale now lives in the Netflix card block above (scales the whole
   tile behind a hover-capable media query); the old .pw nudge is superseded. */
.ptile:active{ transform:scale(.97); }
.bnav .bi .ic-svg{ transition:transform .15s ease; }
.bnav .bi.on .ic-svg{ transform:translateY(-1px) scale(1.08); }
.frow .btn, .frow a.btn{ opacity:.92; }
.chip .ic-svg{ vertical-align:-2.5px; }
/* skeleton building blocks */
.skel-tile{ flex:0 0 108px; }
.skel-tile .pw{ background:none; }
.skel-tile .b1{ width:108px; height:160px; border-radius:12px; }
.skel-tile .b2{ width:80%; height:11px; margin-top:8px; border-radius:6px; }
.skel-row{ display:flex; gap:10px; overflow:hidden; padding:4px 2px 10px; }
/* a skeleton tile that IS a grid cell: fills its column instead of the 108px row size */
.pgrid > .skel-gtile{ flex:none; width:auto; }
.pgrid > .skel-gtile .b1{ width:100%; aspect-ratio:2/3; height:auto; }
.skel-card{ height:74px; margin:10px 0; border-radius:var(--r); }
/* player gesture flash */
.vp .vflash{ position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); z-index:4;
  background:rgba(0,0,0,.65); color:#fff; font-weight:800; font-size:var(--fs-head); padding:14px 22px;
  border-radius:999px; opacity:0; pointer-events:none; }
.vp .vflash.on{ animation:vfl .8s ease forwards; }
@keyframes vfl{ 0%{ opacity:0; transform:translate(-50%,-50%) scale(.8);} 15%{ opacity:1; transform:translate(-50%,-50%) scale(1);} 70%{ opacity:1;} 100%{ opacity:0; } }
/* filter chips row (library) */
.fchips{ display:flex; gap:7px; overflow-x:auto; padding:2px 2px 10px; }
.fchips::-webkit-scrollbar{ display:none; }
.fchip{ flex:0 0 auto; padding:7px 13px; border-radius:999px; font-size:var(--fs-meta); font-weight:700;
  background:var(--surface); color:var(--dim); border:1px solid var(--line); cursor:pointer; white-space:nowrap; }
.fchip.on{ background:var(--gold-grad); color:#191919; border-color:transparent; }
/* inline icon inside a chip (music smart shortcuts) — replaces the old emoji */
.musmart .fchip{ display:inline-flex; align-items:center; gap:6px; }
.musmart .fchip svg{ width:14px; height:14px; flex:0 0 auto; }

/* ---- live sports ---- */
.livecard{ background:var(--surface); border:1px solid var(--line); border-radius:14px; padding:12px 13px; margin-bottom:10px; }
.livecard .lchead{ display:flex; align-items:center; gap:11px; }
.livecard .lt{ width:34px; height:34px; flex:0 0 34px; object-fit:contain; border-radius:7px; background:var(--ink-2); }
/* Initials stand in for a team the feed has no badge for. */
.livecard .lt.ph{ display:flex; align-items:center; justify-content:center;
  font-size:var(--fs-meta); font-weight:700; letter-spacing:.02em; color:var(--dim); }
.livecard .lctt{ flex:1; min-width:0; text-align:center; }
.livecard .lctitle{ font-weight:800; font-size:var(--fs-small); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.livecard .lcmeta{ color:var(--dim); font-size:var(--fs-meta); margin-top:2px; display:flex; gap:7px; align-items:center; justify-content:center; }
.livedot{ color:var(--live-red); font-weight:800; font-size:var(--fs-micro); letter-spacing:.03em; animation:livepulse 1.6s infinite; }
@keyframes livepulse{ 0%,100%{opacity:1} 50%{opacity:.45} }
.ttime{ color:var(--dim); font-weight:700; font-size:var(--fs-meta); }
.livecard .teamstar{ flex:0 0 auto; width:26px; height:26px; border:0; background:transparent; color:var(--faint);
  font-size:var(--fs-read); cursor:pointer; border-radius:7px; }
.livecard .teamstar.on{ color:var(--accent-text); }
.lcchans{ display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; }
/* Long channel names (".btn" is white-space:nowrap) used to overrun the card —
   cap each button to the card width and ellipsize instead of leaking out. */
.lcchans .btn.sm{ max-width:100%; overflow:hidden; text-overflow:ellipsis; }
.lcnone{ color:var(--dim); font-size:var(--fs-meta); }
.changrid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(94px,1fr)); gap:8px; margin-bottom:6px; }
.chantile{ display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; padding:11px 6px; min-height:78px;
  background:var(--surface); border:1px solid var(--line); border-radius:12px; color:inherit; cursor:pointer; }
.chantile:hover{ border-color:var(--ink-5); }
.chantile img{ width:40px; height:40px; object-fit:contain; }
.chantile .cn{ font-size:var(--fs-micro); font-weight:700; text-align:center; line-height:1.2; color:var(--txt); overflow:hidden;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; }

/* ⏺ record button — live streams only. */
.vp .vrec{ display:none; color:#ff5c5c; font-size:var(--fs-title); }
.vp.islive .vrec{ display:flex; }
/* Watch Together — hide the party button on live streams. */
.vp.islive .vwt-btn{ display:none; }
.vp .vwtpanel{ position:absolute; top:64px; right:14px; z-index:7; width:min(280px,84vw); display:none;
  background:#191f28f2; border:1px solid var(--line2); border-radius:14px; padding:14px; box-shadow:var(--shadow); }
.vp .vwtpanel.on{ display:block; }
.vp .vwtpanel .wt-h{ font-weight:800; font-size:var(--fs-small); margin-bottom:6px; }
.vp .vwtpanel .wt-code{ font-size:var(--fs-hero); font-weight:900; letter-spacing:.16em; color:var(--gold2); text-align:center;
  margin:6px 0 4px; }
.vp .vwtpanel .wt-sub{ color:var(--dim); font-size:var(--fs-meta); line-height:1.4; margin-bottom:10px; }
.vp .vwtpanel .wt-mem{ color:var(--gold2); font-size:var(--fs-meta); margin-bottom:10px; }
.vp .vwtpanel .wt-or{ color:var(--faint); font-size:var(--fs-micro); text-transform:uppercase; letter-spacing:.08em;
  text-align:center; margin:12px 0 8px; }
.vp .vwtpanel .btn{ width:100%; justify-content:center; margin-top:4px; }
.vp .vwtpanel .btn.sec{ width:auto; }

/* Up Next (binge auto-play) prompt — a card in the player's bottom-right. */
.vp .vupnext{ position:absolute; right:16px; bottom:78px; z-index:6; max-width:min(360px,82vw); animation:vin .2s ease; }
.vp .vupnext .un-card{ background:rgba(16,19,24,.94); border:1px solid var(--line); border-radius:14px;
  padding:12px 14px; box-shadow:0 14px 34px var(--scrim-1); }
.vp .vupnext .un-lbl{ color:var(--dim); font-size:var(--fs-micro); font-weight:800; text-transform:uppercase; letter-spacing:.07em; }
.vp .vupnext .un-title{ color:var(--txt); font-size:var(--fs-small); font-weight:750; margin:3px 0 11px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.vp .vupnext .un-btns{ display:flex; gap:8px; }
.vp.islive .vupnext{ display:none; }

/* Respect the OS "reduce motion" accessibility setting: tame decorative motion
   (card lifts, slide-ins, pulses, skeleton shimmer) and snap scrolling — but
   keep loading spinners turning, since a frozen spinner reads as "stuck". */
@media (prefers-reduced-motion: reduce){
  *, *::before, *::after{
    animation-duration:.001ms !important;
    animation-delay:0ms !important;
    animation-iteration-count:1 !important;
    transition-duration:.001ms !important;
    transition-delay:0ms !important;
  }
  html, body{ scroll-behavior:auto !important; }
  /* keep essential loading indicators animating */
  .spin, [class*="spin"]{ animation-duration:.9s !important; animation-iteration-count:infinite !important; }
}

/* ============================ top-bar button (Downloads) ============================ */
.topbtn{ position:relative; width:36px; height:36px; border-radius:10px; border:1px solid var(--line2);
  background:var(--card); color:var(--txt); display:flex; align-items:center; justify-content:center;
  cursor:pointer; flex:0 0 auto; transition:border-color .15s,color .15s; }
.topbtn:hover{ border-color:var(--line2); }
.topbtn.on{ color:var(--accent-text); border-color:var(--gold); }
.topbtn .bdg{ position:absolute; top:-5px; right:-5px; min-width:16px; height:16px; padding:0 4px;
  border-radius:8px; background:var(--gold); color:#141414; font-size:var(--fs-micro); font-weight:800;
  display:flex; align-items:center; justify-content:center; }

/* ============================ YouTube ============================ */
.ytgrid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(190px,1fr)); gap:14px 12px; margin-top:6px; }
.ytscroll{ display:flex; gap:12px; overflow-x:auto; padding:4px 2px 10px; scroll-snap-type:x proximity;
  -webkit-overflow-scrolling:touch; }
.ytscroll::-webkit-scrollbar{ height:0; }
.ytscroll .ytcard{ flex:0 0 200px; scroll-snap-align:start; }
.ytcard{ cursor:pointer; }
.ytthumb{ position:relative; width:100%; aspect-ratio:16/9; border-radius:12px; overflow:hidden;
  background:var(--surface); }
.ytthumb img{ width:100%; height:100%; object-fit:cover; display:block; transition:transform .25s,opacity .2s; }
.ytcard:hover .ytthumb img{ transform:scale(1.04); }
.ytdur{ position:absolute; bottom:6px; right:6px; background:var(--scrim-3); color:#fff; font-size:var(--fs-micro);
  font-weight:700; padding:1px 5px; border-radius:5px; }
.ytlive{ position:absolute; bottom:6px; right:6px; background:#e02020; color:#fff; font-size:var(--fs-micro);
  font-weight:800; padding:1px 6px; border-radius:5px; letter-spacing:.4px; }
.ytcap{ padding:8px 2px 0; }
.ytttl{ font-size:var(--fs-small); font-weight:650; line-height:1.3; color:var(--txt);
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.ytcsub{ font-size:var(--fs-meta); color:var(--dim); margin-top:3px; }
.ytchips{ display:flex; flex-wrap:wrap; gap:10px; margin-top:6px; }
.ytchan{ display:flex; flex-direction:column; align-items:center; gap:6px; width:88px; cursor:pointer; }
.ytchan img{ width:64px; height:64px; border-radius:50%; object-fit:cover; background:var(--surface);
  border:1px solid var(--line2); }
.ytchan span{ font-size:var(--fs-meta); text-align:center; color:var(--dim); line-height:1.25;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.ytchanhead{ display:flex; align-items:center; gap:14px; padding:6px 2px 14px; margin-bottom:4px;
  border-bottom:1px solid var(--line); }
.ytchanhead img,.ytchanav{ width:66px; height:66px; border-radius:50%; object-fit:cover; background:var(--surface);
  border:1px solid var(--line2); flex:0 0 66px; }
.ytchanname{ font-size:var(--fs-title); font-weight:800; margin-bottom:8px; }

/* fullscreen YouTube player */
.ytov{ position:fixed; inset:0; z-index:120; background:var(--bg); display:none; flex-direction:column;
  overflow:hidden; }
.ytov-bar{ display:flex; align-items:center; gap:10px; padding:calc(var(--sa-t) + 8px) 12px 8px; }
.ytov-close{ width:34px; height:34px; border-radius:50%; border:0; background:var(--card); color:var(--txt);
  display:flex; align-items:center; justify-content:center; cursor:pointer; }
.ytov-brand{ font-weight:800; letter-spacing:.3px; color:var(--dim); }
.ytp-stage{ background:#000; width:100%; }
.ytp-vid{ width:100%; max-height:min(56vh,420px); background:#000; display:block; }
.ytp-scroll{ flex:1; overflow-y:auto; padding:14px 16px calc(var(--sa-b) + 24px);
  -webkit-overflow-scrolling:touch; max-width:1060px; margin:0 auto; width:100%; }
.ytp-title{ font-size:var(--fs-title); font-weight:750; line-height:1.35; }
.ytp-meta{ font-size:var(--fs-meta); color:var(--dim); margin-top:5px; }
.ytp-actions{ display:flex; gap:8px; flex-wrap:wrap; margin:12px 0; }
.ytp-desc{ font-size:var(--fs-small); color:var(--dim); line-height:1.5; white-space:pre-wrap; word-break:break-word;
  max-height:130px; overflow-y:auto; padding:10px 12px; background:var(--surface); border-radius:10px; }
.ytp-more-h{ font-size:var(--fs-small); font-weight:800; margin:18px 0 8px; }

/* YouTube account connect card */
.ytconn{ display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;
  background:var(--card); border:1px solid var(--line2); border-radius:14px; padding:12px 14px; margin-bottom:12px; }
.ytconn-l{ display:flex; align-items:center; gap:11px; min-width:0; flex:1; }
.ytconn-ic{ width:34px; height:34px; flex:0 0 34px; border-radius:9px; background:#ff0000; color:#fff;
  display:flex; align-items:center; justify-content:center; font-size:var(--fs-title); }
.ytconn-t{ font-weight:750; font-size:var(--fs-small); }
.ytconn-s{ font-size:var(--fs-meta); color:var(--dim); margin-top:2px; line-height:1.5; }
.ytpill{ display:inline-block; font-size:var(--fs-micro); font-weight:800; padding:1px 7px; border-radius:999px;
  background:rgba(46,194,126,.16); color:var(--ok-text); }
.ytpill.ok{ background:rgba(46,194,126,.16); color:var(--ok-text); }

/* One-action toast (handoff prompt). Sits above the music bar so it never
   covers the controls it is asking about. */
.toastact{ position:fixed; left:14px; right:14px; bottom:calc(var(--ph-bar-h) + 86px); z-index:96;
  display:none; align-items:center; gap:12px; padding:12px 14px; border-radius:14px;
  background:var(--card); border:1px solid var(--line2); box-shadow:var(--shadow); }
.toastact.on{ display:flex; }
.toastact span{ flex:1; min-width:0; font-size:var(--fs-small); }
.toastact .btn{ flex:0 0 auto; }

/* Themed dialog — replaces the engine's own confirm()/alert() box, which
   renders as a square grey drop-down from the top of the window with the
   origin printed above the text, ignoring the theme entirely. */
.phdlg-wrap{ position:fixed; inset:0; z-index:200; display:none; align-items:center; justify-content:center; padding:24px; }
.phdlg-wrap.on{ display:flex; }
.phdlg-veil{ position:absolute; inset:0; background:var(--scrim-2); backdrop-filter:blur(3px); }
.phdlg{ position:relative; width:min(440px,100%); background:var(--bg2); color:var(--txt);
  border:1px solid var(--line2); border-radius:16px; box-shadow:var(--shadow);
  padding:20px 20px 16px; animation:phdlgin .16s ease; }
@keyframes phdlgin{ from{ transform:translateY(10px) scale(.98); opacity:0; } to{ transform:none; opacity:1; } }
.phdlg-t{ font-size:var(--fs-title); font-weight:800; margin-bottom:8px; }
.phdlg-b{ font-size:var(--fs-small); color:var(--dim); line-height:1.5; white-space:pre-wrap; max-height:46vh; overflow-y:auto; }
.phdlg-a{ display:flex; gap:10px; justify-content:flex-end; margin-top:18px; }
.phdlg-a .btn{ flex:0 0 auto; padding:9px 18px; }

/* Trailer overlay — a focused modal video (ad-free via /api/yt/stream). */
.trailerov{ position:fixed; inset:0; z-index:210; display:none; align-items:center; justify-content:center; padding:20px; }
.trailerov.on{ display:flex; }
.trailerback{ position:absolute; inset:0; background:rgba(0,0,0,.9); }
.trailerbox{ position:relative; width:min(1100px,100%); }
.trailerhead{ display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; color:var(--txt); }
.trailerhead span{ font-weight:800; font-size:var(--fs-read); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.trailerx{ background:transparent; border:0; color:var(--txt); width:38px; height:38px; border-radius:50%; cursor:pointer;
  display:flex; align-items:center; justify-content:center; }
.trailerx:hover{ background:var(--ink-3); }
.trailerbox video{ width:100%; max-height:78vh; border-radius:12px; background:#000; box-shadow:var(--shadow); }

/* Compact text controls (playback speed) in the player button rows. */
.mctl .mnspeed, #vp-speed{ font-size:var(--fs-meta); font-weight:800; }
.mctl .mnspeed{ width:auto; min-width:38px; padding:0 8px; border-radius:19px; }
.mctl .mnspeed.on{ color:var(--accent2); }

/* Your stats card (Settings). */
.statgrid{ display:grid; grid-template-columns:repeat(2,1fr); gap:10px; margin-bottom:6px; }
.statcell{ background:var(--ink-2); border-radius:12px; padding:12px; text-align:center; }
.statnum{ font-size:var(--fs-big); font-weight:800; color:var(--accent-text); }
.statlbl{ font-size:var(--fs-meta); color:var(--dim); margin-top:2px; }
.statsect{ margin-top:12px; }
.stath{ font-size:var(--fs-meta); font-weight:800; color:var(--faint); text-transform:uppercase; letter-spacing:.4px; margin-bottom:6px; }
.statmini{ display:flex; justify-content:space-between; gap:12px; padding:5px 0; font-size:var(--fs-small); border-bottom:1px solid var(--line); }
.statmini:last-child{ border-bottom:0; }
.statmini > span:first-child{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.statn{ flex:0 0 auto; color:var(--accent-text); font-weight:700; }
@media(min-width:700px){ .statgrid{ grid-template-columns:repeat(4,1fr); } }

/* Bulk-select in Downloads. */
.dlselbar{ display:flex; align-items:center; gap:10px; margin:0 0 8px; }
.dlselcount{ font-size:var(--fs-small); color:var(--dim); font-weight:600; }
.murow.selmode{ cursor:pointer; }
.murow.sel{ background:var(--accent-soft); }
.murow .selck{ flex:0 0 22px; width:22px; height:22px; border-radius:50%; border:2px solid var(--line2);
  display:flex; align-items:center; justify-content:center; color:var(--accent-ink); margin-right:2px; }
.murow.sel .selck{ background:var(--accent); border-color:var(--accent); }

/* ============ Netflix browse (R2 chunk 3) ============
   The library's default face: full-bleed billboard + landscape category rows.
   Lives inside #v22-discovery-root; the search grid keeps the old classes. */

/* ---- Downloads > Music: actionable work first, finished collapsed ---- */
.mudl{ align-items:center; gap:10px; }
.mudl-art{ width:38px; height:38px; border-radius:6px; object-fit:cover; flex:0 0 38px;
  background:rgba(255,255,255,.07); display:block; }
/* A failure has to be findable at a glance, not read for. */
.mudl.bad{ border-left:3px solid var(--bad,#e5484d); padding-left:9px; }
.mudl.bad .meta{ color:var(--bad,#e5484d); }
.mudl-head{ font-size:var(--fs-meta); letter-spacing:.04em; text-transform:uppercase;
  color:var(--dim,#9a9a9a); margin:4px 0 6px; }
.mudl-toggle{ width:100%; justify-content:center; margin:12px 0 6px; }

/* Full-width canvas while the Netflix browse owns the library tab. */
main:has(#v-lib.v22-enhanced.on){ max-width:none; padding-left:0; padding-right:0; }
main:has(#v-lib.v22-enhanced.on) #v-lib{ padding:0; }

/* Same for YouTube, which is edge-to-edge on youtube.com and was the one page
   Pierre called out as not filling a maximised window. Measured at 1920 before
   this rule: the view was 1060px wide at x=426 — 860px of dead margin, 55% of
   the window, and only 3 feed columns. The view already carried an inline
   max-width:none, but the cap is on this <main>, and a child cannot widen past
   its parent. */
main:has(#v-yt.on){ max-width:none; padding-left:0; padding-right:0; padding-top:0; }
main:has(#v-yt.on) #v-yt{ padding:0; margin:0; }
#v-lib.v22-enhanced .v22-discovery{ --npadx:4vw; }

/* compact search row (Netflix keeps chrome minimal) */
.nsearchrow{ display:flex; align-items:center; gap:10px; padding:10px var(--npadx,4vw) 4px; }
.nsearchrow .v22-search-box{ flex:1; max-width:420px; }
.v22-results-meta{ padding:0 var(--npadx,4vw); }
#v22-search-results>.pgrid{ padding:0 var(--npadx,4vw) 30px; }
#v22-filters{ margin:0 var(--npadx,4vw); }

/* ---- billboard ---- */
.nbill{ position:relative; min-height:min(58vh,540px); background-size:cover; background-position:center 18%;
  display:flex; align-items:flex-end; margin:6px 0 26px; }
.nbill-scrim{ position:absolute; inset:0;
  background:linear-gradient(180deg, rgba(20,20,20,0) 35%, rgba(20,20,20,.55) 74%, #141414 99%),
             linear-gradient(90deg, rgba(20,20,20,.72) 0%, rgba(20,20,20,.18) 42%, rgba(20,20,20,0) 65%); }
.nbill-inner{ position:relative; z-index:1; padding:0 var(--npadx,4vw) clamp(26px,5vh,54px); max-width:640px; }
.nbill-eyebrow{ font-size:var(--fs-meta); font-weight:800; letter-spacing:.18em; text-transform:uppercase; color:#e5e5e5;
  text-shadow:0 1px 8px rgba(0,0,0,.6); margin-bottom:10px; }
.nbill-title{ display:block; padding:0; border:0; background:none; text-align:left; cursor:pointer; color:inherit;
  font-family:inherit; font-size:clamp(32px,5vw,64px); font-weight:900; line-height:1.02; letter-spacing:-.5px;
  color:#fff; text-shadow:0 2px 22px rgba(0,0,0,.55); margin-bottom:12px; }
.nbill-meta{ font-size:var(--fs-read); font-weight:600; color:#e5e5e5; text-shadow:0 1px 8px rgba(0,0,0,.6); margin-bottom:20px; }
.nbill-actions{ display:flex; gap:12px; flex-wrap:wrap; }
.nbtn{ display:inline-flex; align-items:center; gap:9px; border:0; border-radius:6px; cursor:pointer;
  font-size:clamp(14px,1.2vw,18px); font-weight:750; padding:11px 26px; transition:background .15s; }
.nbtn-play{ background:#fff; color:#141414; }
.nbtn-play:hover{ background:rgba(255,255,255,.78); }
.nbtn-info{ background:rgba(109,109,110,.7); color:#fff; }
.nbtn-info:hover{ background:rgba(109,109,110,.45); }

/* ---- rows ---- */
.nrow-sec{ margin:0 0 22px; }
/* Chunk 49 - #e5e5e5 is a dark-theme colour written as a literal. Measured in
   light mode on Films & TV: every row heading came to 1.18:1 against the page,
   which is a heading nobody can read. --txt is the same near-white in dark. */
.nrow-h{ font-size:clamp(15px,1.4vw,21px); font-weight:750; color:var(--txt); margin:0 0 10px; padding:0 var(--npadx,4vw); }
.nrow{ display:flex; gap:8px; overflow-x:auto; overscroll-behavior-x:contain; padding:4px var(--npadx,4vw) 8px;
  scroll-snap-type:x proximity; scrollbar-width:none; }
.nrow::-webkit-scrollbar{ display:none; }

/* ---- a row heading that is a door (R2 chunk N1) ----
   Rows stop at 24 cards. "Movies (English)" showed 24 of 148 and there was no
   way to reach the other 124 from the page whose job is browsing them. Where a
   row is capped its heading becomes a button carrying the real count. */
.nrow-h-btn{ display:flex; align-items:baseline; gap:9px; width:100%; margin:0 0 10px;
  padding:0 var(--npadx,4vw); border:0; background:none; text-align:left; cursor:pointer;
  font-size:clamp(15px,1.4vw,21px); font-weight:750; color:var(--txt); }
.nrow-see{ font-size:var(--fs-meta); font-weight:700; color:var(--dim); white-space:nowrap;
  opacity:.85; transition:color .16s, opacity .16s; }
.nrow-h-btn:hover .nrow-see, .nrow-h-btn:focus-visible .nrow-see{ color:var(--accent-text); opacity:1; }

/* ---- one shelf, in full ----
   Back first, then the name on its own line. Side by side, "Everything, newest
   first" ran off the right edge of a 390px screen with the back button eating
   the width it needed. */
.nshelf-head{ display:flex; flex-direction:column; align-items:flex-start; gap:10px;
  padding:6px var(--npadx,4vw) 12px; }
.nshelf-head h2{ margin:0; max-width:100%; font-size:clamp(19px,2.4vw,28px); font-weight:800;
  color:var(--txt); line-height:1.15; }
.nshelf-head .v22-btn{ flex:0 0 auto; }
@media (min-width:700px){
  .nshelf-head{ flex-direction:row; align-items:center; gap:14px; }
}

/* ---- "Browse by genre", back on the screen ----
   The chips and their results are markup inside #v-lib, which v2.2 discovery
   hides wholesale; phNfGenres() moves the two nodes in here so the `>`
   combinator stops matching. This is the frame around them. */
.nf-genres{ padding:6px var(--npadx,4vw) 26px; }
.v22-discovery:not(.nbrowsing) .nf-genres{ display:none; }
.nf-genres .nrow-h{ padding:0; margin:0 0 2px; }
.nf-genres-sub{ margin:0 0 10px; color:var(--dim); font-size:var(--fs-meta); }
.nf-genres #genrechips{ margin:0; }
.nf-genres #genreview{ margin-top:4px; }
.nf-genres #genreview .hrow{ padding-left:0; padding-right:0; }
#genrechips.flashme{ animation:nfflash 1.3s ease; border-radius:12px; }
@keyframes nfflash{ 0%,100%{ box-shadow:0 0 0 0 rgba(229,160,13,0); }
  25%{ box-shadow:0 0 0 4px rgba(229,160,13,.36); } }

/* ---- landscape card ---- */
.ncard{ position:relative; flex:0 0 var(--ncw,clamp(210px,16.2vw,300px)); width:var(--ncw,clamp(210px,16.2vw,300px));
  aspect-ratio:16/9; border-radius:6px; overflow:hidden; background:#242424; cursor:pointer;
  scroll-snap-align:start; transition:transform .22s ease, box-shadow .22s ease; }
.ncard img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
.ncard .ph{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:#555; font-size:var(--fs-big); }
.ncard .nsh{ position:absolute; inset:auto 0 0 0; height:56%;
  background:linear-gradient(180deg, rgba(0,0,0,0), rgba(0,0,0,.72)); }
.ncard .nt{ position:absolute; left:10px; right:10px; bottom:8px; font-size:var(--fs-small); font-weight:750; color:#fff;
  text-shadow:0 1px 6px rgba(0,0,0,.8); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.ncard .prog{ position:absolute; left:0; right:0; bottom:0; height:3px; background:rgba(255,255,255,.25); }
.ncard .prog>i{ display:block; height:100%; background:var(--accent,#e5a00d); }
/* Watched. The browse cards showed a progress bar for a half-finished title
   and nothing at all for a finished one, so "seen it" was a fact the app knew
   and never told you on the screen where you choose what to watch. */
.ncard .nseen{ position:absolute; top:6px; right:6px; z-index:4; width:20px; height:20px; border-radius:50%;
  display:flex; align-items:center; justify-content:center; font-weight:900;
  font-size:12px; /* glyph box, not type — sizes the ✓ drawing inside a 20px disc */
  background:rgba(20,20,20,.78); color:#fff; box-shadow:0 1px 6px rgba(0,0,0,.55); }
.ncard.noart{ display:flex; align-items:flex-end; }
.ncard.noart .nt{ white-space:normal; text-overflow:clip; line-height:1.25; max-height:3.8em; overflow:hidden; }
.ncard .nov{ position:absolute; inset:auto 0 0 0; padding:26px 11px 10px; opacity:0; pointer-events:none;
  background:linear-gradient(180deg, rgba(0,0,0,0), rgba(0,0,0,.88) 55%); transition:opacity .18s; }
.ncard .novt{ font-size:var(--fs-small); font-weight:800; color:#fff; }
.ncard .novm{ font-size:var(--fs-meta); color:#b3b3b3; margin-top:2px; }
@media (hover:hover) and (pointer:fine){
  .ncard:hover{ transform:scale(1.22); z-index:12; box-shadow:0 18px 42px rgba(0,0,0,.7); }
  .ncard:hover .nt, .ncard:hover .nsh{ opacity:0; }
  .ncard:hover .nov{ opacity:1; }
  .nrow{ padding-top:26px; padding-bottom:30px; margin-top:-22px; }
}

/* ---- phone: portrait cards (Netflix mobile) + tighter billboard ---- */
.ncard.np{ aspect-ratio:2/3; flex-basis:var(--ncwp,clamp(112px,30vw,150px)); width:var(--ncwp,clamp(112px,30vw,150px)); }
@media (max-width:640px){
  .nbill{ min-height:56vw; margin-bottom:18px; }
  .nbill-inner{ padding-bottom:20px; }
  .nbill-title{ font-size:clamp(26px,8vw,38px); }
  .nbtn{ padding:9px 18px; }
  .nrow-sec{ margin-bottom:16px; }
}

/* Episode actions match Netflix chrome: white primary Play, ghost secondary
   (the global gold .btn made every episode row shout). */
.epinfo .epactions .btn{ background:#fff; color:#141414; }
.epinfo .epactions .btn:hover{ background:rgba(255,255,255,.8); }
.epinfo .epactions .btn.sec{ background:rgba(255,255,255,.12); color:#fff; }
.epinfo .epactions .btn.sec:hover{ background:rgba(255,255,255,.22); }

/* Brand mark follows the accent (CSS beats the baked fill attribute), so the
   Full-Netflix toggle turns the play-triangle red too. */
.brand .mark polygon, .ad-brand .mark polygon{ fill:var(--accent); }

/* ---- Netflix row chevrons (desktop) + browse-mode chrome (R2 chunk 12) ---- */
.nrow-wrap{ position:relative; }
.nrow-arr{ position:absolute; top:0; bottom:8px; width:44px; z-index:15; border:0; cursor:pointer;
  display:none; align-items:center; justify-content:center; color:#fff; opacity:0; transition:opacity .18s; padding:0; }
.nrow-prev{ left:0; background:linear-gradient(90deg, rgba(20,20,20,.85), rgba(20,20,20,0)); border-radius:0 4px 4px 0; }
.nrow-next{ right:0; background:linear-gradient(270deg, rgba(20,20,20,.85), rgba(20,20,20,0)); border-radius:4px 0 0 4px; }
@media (hover:hover) and (pointer:fine){
  .nrow-arr{ display:flex; }
  .nrow-wrap:hover .nrow-arr{ opacity:1; }
  .nrow-arr:hover{ color:#fff; transform:scale(1.15); }
}
/* Browse mode: the "N titles on your server" meta line stops burning a row
   (search results still show their count). */
.v22-discovery.nbrowsing .v22-results-meta{ display:none; }

/* Preview button on Get-music results: a quiet circular control that fills with
   the accent as the 30s clip plays, so you can see how much is left. */
.pvbtn{ flex:0 0 auto; width:34px; height:34px; border-radius:50%; cursor:pointer; padding:0;
  display:flex; align-items:center; justify-content:center; color:var(--txt);
  border:1px solid var(--line2); background:var(--raise); transition:border-color .15s, color .15s; }
.pvbtn:hover{ border-color:var(--accent); color:var(--accent); }
.pvbtn.on{ color:#141414; border-color:transparent;
  background:conic-gradient(var(--accent) var(--pv,0%), rgba(255,255,255,.22) 0); }
.pvbtn.on svg{ filter:drop-shadow(0 0 2px rgba(0,0,0,.35)); }

/* "PLAYING FROM …" in the now-playing header.
   The player used to be a dead end — you arrived from a playlist, an album or
   the library and the only way out was to minimise and navigate again. This
   names the source and is tappable to return to it. It takes the flexible middle
   of the header so the buttons either side keep their positions. */
.mn-ctx{ flex:1; min-width:0; text-align:center; cursor:pointer; padding:2px 8px; }
.mn-ctx-k{ font-size:var(--fs-micro); letter-spacing:.12em; color:var(--faint); text-transform:uppercase; }
.mn-ctx-n{ font-size:var(--fs-meta); font-weight:650; color:var(--txt);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.mn-ctx:active{ opacity:.6; }


/* ============================================================
   R5-G1/G3 — MOTION + PRESS FEEDBACK
   Audited before writing: overlays already animate on open (vin/shup) and the
   global prefers-reduced-motion block already neutralises every animation and
   transition. What was missing was the other half of "alive": TOUCH. A row,
   a card, a tile and a chip all had zero press state on a phone, so a tap
   produced nothing until the next screen arrived - which reads as a dropped
   tap, and is why people tap twice.

   One shared token set so every surface answers at the same speed, and the
   scale is deliberately small (.97-.98): a press should be felt, not watched.
   ============================================================ */
:root{
  --ph-press: 120ms cubic-bezier(.2,.7,.4,1);
  --ph-move: 180ms cubic-bezier(.2,.7,.4,1);
}

/* Rows: a background wash rather than a scale — scaling a full-width row
   makes its neighbours look like they moved. */
.murow, .lrow, .setrow, .yt-uprow{ transition: background-color var(--ph-press); }
.murow:active, .lrow:active, .yt-uprow:active{ background: var(--surface); }

/* Cards and tiles: these are discrete objects, so they take the scale. */
.card, .ptile, .pltile, .mutile, .mgtile, .yt-card, .yt-shcard, .yt-hcard, .plgroup .plghead{
  transition: transform var(--ph-press), background-color var(--ph-press);
}
.card:active, .ptile:active, .pltile:active, .mutile:active, .mgtile:active,
.yt-card:active, .yt-shcard:active, .yt-hcard:active, .plgroup .plghead:active{
  transform: scale(.975);
}

/* Controls: buttons and chips get a firmer press than content does.
   `.bnav .bi` is deliberately NOT in this list. It used to be, and because this
   rule has the same specificity but comes ~1600 lines later, it replaced the
   tab's own `transition` outright — so the bar's fold never animated at all, in
   either direction, for as long as the fold has existed. The tab declares its
   complete transition (press AND fold) in one place up at `.bnav .bi`; adding it
   back here would silently switch the fold off again. */
.btn, .fchip, .mdots, .mlike, .mhero-ic, .mhero-fab, .sopt{
  transition: transform var(--ph-press), background-color var(--ph-press), color var(--ph-press);
}
.btn:active, .fchip:active, .mdots:active, .mlike:active,
.mhero-ic:active, .mhero-fab:active, .sopt:active{ transform: scale(.94); }
.bnav .bi:active{ transform: scale(.92); }

/* Empty states (R5-G7): an icon, a sentence, and — where there is something
   useful to do — one action. The bare grey line said "nothing here" without
   saying what to do about it. */
.phempty{ display:flex; flex-direction:column; align-items:center; text-align:center;
  padding:34px 22px; color:var(--dim); }
.phempty .ic{ width:52px; height:52px; border-radius:16px; display:flex; align-items:center;
  justify-content:center; background:var(--card); color:var(--faint); margin-bottom:12px; }
.phempty .t{ font-size:var(--fs-read); font-weight:700; color:var(--txt); }
.phempty .s{ font-size:var(--fs-meta); margin-top:5px; max-width:34ch; line-height:1.5; }
.phempty .btn{ margin-top:14px; }

/* ===== R5-G8 · Toast stack =====
   #toast above is now only the legacy host; messages render into #ph-toasts as
   individual elements so a second one does not erase the first. Sits on the
   same bottom offset the single toast used, above the nav bar. */
#ph-toasts{ position:fixed; left:50%; transform:translateX(-50%); z-index:99;
  bottom:calc(var(--ph-bar-h) + 18px);
  display:flex; flex-direction:column; align-items:center; gap:8px;
  pointer-events:none; width:max-content; max-width:92vw; }
.ph-toast{ display:flex; align-items:center; background:#20262f; color:var(--txt);
  border:1px solid var(--line2); padding:11px 18px; border-radius:12px;
  font-size:var(--fs-small); font-weight:600; box-shadow:var(--shadow); max-width:88vw;
  opacity:0; transform:translateY(12px) scale(.96); transition:opacity .2s, transform .2s;
  pointer-events:auto; }
.ph-toast.on{ opacity:1; transform:none; }
/* Success/error read at a glance from the edge stripe — colour only, so a
   message is never carried by colour alone. */
.ph-toast.ok{ border-left:3px solid #3fb950; }
.ph-toast.err{ border-left:3px solid #f85149; background:#2a1f22; }
.ph-toast-act{ margin-left:12px; background:transparent; border:0; color:var(--accent-text);
  font-weight:800; font-size:var(--fs-small); cursor:pointer; padding:0; }
/* R8.4 - a repeat counts up on the card that is already there instead of
   raising another one. The count sits beside the words, and the card gives a
   small kick so the repeat is seen rather than silently absorbed. The kick is
   on the BADGE, not the card: .ph-toast.on sets transform:none with a
   transition on transform, so animating the card's transform would fight it
   and leave a jump when the animation ends. The global reduce-motion block
   above already flattens this. */
.ph-toast-n{ display:inline-block; margin-left:8px; font-weight:800; font-size:var(--fs-small);
  color:var(--dim); font-variant-numeric:tabular-nums; }
.ph-toast.bump .ph-toast-n{ animation:phToastN .38s ease; }
@keyframes phToastN{ 0%{ transform:scale(1); } 35%{ transform:scale(1.35); } 100%{ transform:scale(1); } }
.ph-toast.bump{ animation:phToastEdge .38s ease; }
@keyframes phToastEdge{ 0%{ border-color:var(--line2); } 40%{ border-color:var(--accent-text); } 100%{ border-color:var(--line2); } }

/* ===== R5-G2 · List + guide skeletons ===== */
.skel-list{ display:flex; flex-direction:column; gap:2px; }
.skel-lrow{ display:flex; align-items:center; gap:12px; padding:9px 2px; }
.skel-lrow .s-art{ width:46px; height:46px; border-radius:8px; flex:0 0 46px; }
.skel-lrow .s-tx{ flex:1; min-width:0; }
.skel-lrow .s-l1{ height:12px; width:62%; border-radius:6px; }
.skel-lrow .s-l2{ height:10px; width:38%; border-radius:6px; margin-top:8px; }
.skel-guide{ display:flex; flex-direction:column; gap:10px; }
.skel-gcard{ background:var(--card); border:1px solid var(--line); border-radius:12px; padding:14px; }
.skel-gcard .s-gt{ height:14px; width:52%; border-radius:6px; }
.skel-gcard .s-gl{ height:11px; width:78%; border-radius:6px; margin-top:10px; }

/* ===== R5-G12 · Header density on phones =====
   Measured at 390x844 before touching anything: the top bar ate 56px, main
   added 14px, and the first .sect added another 18px of margin on top of that
   - so on Live the first row of content started at y=171, a fifth of the
   screen gone before anything you came for. These trim the stack that the
   measurement actually showed, nothing speculative:
     topbar 56 -> 50, main pad 14 -> 9, .sect margin 18/6 -> 13/5, and a
   first-child .sect no longer double-spaces against main's padding.
   Measured after, at 390x844: first content row on Home 82 -> 71, Library
   80 -> 69, Downloads 80 -> 69, Music 76 -> 65, Live 94 -> 67 (and Live's
   first guide row 171 -> 143).
   Phone-only: the desktop app has the room and looks airy, not empty.
   Three more rules were tried here and REMOVED after measuring: `.sect h2
   {margin-bottom:0}` (line 161 already sets margin:0), and first-child
   margin resets for .searchrow (never a first child) and .fchips (already
   0) — all three matched nothing or changed nothing. */
@media (max-width:520px){
  .topbar{ padding:calc(var(--sa-t) + 7px) 14px 7px; }
  main{ padding-top:9px; }
  .sect{ margin:13px 0 5px; }
  .view > .sect:first-child{ margin-top:2px; }
}

/* ===== R5-G5 . Pull-to-refresh indicator ===== */
#ph-ptr{ position:fixed; top:calc(var(--sa-t) + 6px); left:50%; z-index:60;
  transform:translateX(-50%); opacity:0; pointer-events:none; transition:opacity .12s; }
.ph-ptr-c{ width:34px; height:34px; border-radius:50%; background:var(--card);
  border:1px solid var(--line2); box-shadow:var(--shadow);
  display:flex; align-items:center; justify-content:center; color:var(--dim); }
.ph-ptr-c svg{ width:18px; height:18px; }
#ph-ptr.ready .ph-ptr-c{ color:var(--gold); border-color:var(--gold); }
#ph-ptr.spin .ph-ptr-c svg{ animation:ph-ptr-spin .8s linear infinite; }
@keyframes ph-ptr-spin{ to{ transform:rotate(360deg); } }
@media (prefers-reduced-motion:reduce){ #ph-ptr.spin .ph-ptr-c svg{ animation:none; } }

/* ===== R5-G6 . Now-playing colour wash =====
   --mu-wash comes from the cover art and is already clamped to a readable
   lightness in JS, so these lean on it directly. The wash is a gradient that
   fades out well before the controls, which is why the text and buttons below
   keep their normal contrast. */
.munow.washed{
  /* TWO layers, wash on top of the player's OWN dark gradient. Setting a bare
     background-image here replaces that base gradient outright (it is an image,
     not a colour) and the full-screen player goes see-through - you watch the
     library scroll behind the now-playing screen. Restating the base as the
     second layer is what keeps it opaque. */
  background-image:
    linear-gradient(180deg, color-mix(in srgb, var(--mu-wash) 78%, transparent) 0%,
                            color-mix(in srgb, var(--mu-wash) 34%, transparent) 34%,
                            transparent 62%),
    linear-gradient(180deg,#1c1c1c,#141414 70%); }
.mubar.washed{
  /* Same two-layer rule; the bar's base is a flat colour so it is restated as
     a solid stop rather than a gradient. */
  background-image:
    linear-gradient(90deg, color-mix(in srgb, var(--mu-wash) 48%, transparent), transparent 62%),
    linear-gradient(#1b212bf5,#1b212bf5); }
@supports not (color: color-mix(in srgb, red, blue)){
  .munow.washed{ background-image:
    linear-gradient(180deg, var(--mu-wash) 0%, transparent 58%),
    linear-gradient(180deg,#1c1c1c,#141414 70%); }
  .mubar.washed{ background-image:
    linear-gradient(90deg, var(--mu-wash), transparent 60%),
    linear-gradient(#1b212bf5,#1b212bf5); }
}

/* ===== R5-H5 . Keyboard shortcut overlay ===== */
#ph-keys{ position:fixed; inset:0; z-index:120; background:var(--scrim-2);
  display:flex; align-items:center; justify-content:center; padding:20px; }
.ph-keys-box{ background:var(--card); border:1px solid var(--line2); border-radius:16px;
  padding:20px 22px; box-shadow:var(--shadow); min-width:280px; max-width:min(420px,92vw); }
.ph-keys-h{ font-weight:800; font-size:var(--fs-title); margin-bottom:14px; }
.ph-keys-r{ display:flex; align-items:center; gap:14px; padding:6px 0; font-size:var(--fs-small); color:var(--dim); }
.ph-keys-r kbd{ flex:0 0 74px; text-align:center; background:var(--raise); border:1px solid var(--line2);
  border-radius:6px; padding:3px 0; font:inherit; font-size:var(--fs-meta); font-weight:700; color:var(--txt); }
.ph-keys-box .btn{ margin-top:14px; width:100%; }

/* ===== R5-I2 . Safe areas on the full-screen surfaces =====
   Audited every position:fixed rule. These reached the physical screen edge
   with no inset, so on a notched iPhone the top row sat under the notch and
   the bottom row under the home indicator. The app shell (top bar, nav, mini
   bar, player, sheets, YT watch/mini/nav) already handled it; these did not.

   Left/right are included because they are ZERO in portrait and non-zero in
   LANDSCAPE — which is exactly when a full-screen video is on screen. */
.detail, .gate, .ytov{
  padding-top:var(--sa-t); padding-bottom:var(--sa-b);
  padding-left:var(--sa-l); padding-right:var(--sa-r); }
/* Centred dialogs: the box is centred so the notch rarely covers it, but on a
   short screen in landscape it can reach the edge. Fold the inset into the
   padding they already have rather than adding a second layer. */
.shazov, .trailerov, .muimp-wrap, .phdlg-wrap, #ph-keys{
  padding-top:calc(20px + var(--sa-t)); padding-bottom:calc(20px + var(--sa-b));
  padding-left:calc(20px + var(--sa-l)); padding-right:calc(20px + var(--sa-r)); }
/* The video player is black to the edges on purpose — the PICTURE should fill
   the screen. Only its controls are inset, so nothing tappable hides under the
   notch or the home indicator. */
.vp > *:not(video):not(.vpstage){
  margin-top:var(--sa-t); margin-bottom:var(--sa-b);
  margin-left:var(--sa-l); margin-right:var(--sa-r); }

/* ===== R5-I4 . Download integrity banner ===== */
#ph-dlcheck{ position:fixed; left:50%; transform:translateX(-50%);
  top:calc(var(--sa-t) + 10px); z-index:95;
  background:var(--card); border:1px solid var(--line2); border-radius:999px;
  padding:8px 16px; font-size:var(--fs-small); font-weight:600; color:var(--dim);
  box-shadow:var(--shadow); display:flex; align-items:center; gap:9px; }
#ph-dlcheck .sp{ width:13px; height:13px; border-radius:50%;
  border:2px solid var(--ink-4); border-top-color:var(--gold); animation:ph-dlspin .7s linear infinite; }
@keyframes ph-dlspin{ to{ transform:rotate(360deg); } }
@media (prefers-reduced-motion:reduce){ #ph-dlcheck .sp{ animation:none; } }

/* ===== R5-B6 . Sleep-timer button badge =====
   muSleepPaint() has always drawn a <span class="slt"> with the minutes left,
   and there was no rule for it anywhere — it inherited the button's font and
   sat on top of the icon. Now a small corner badge. */
.munow #mn-sleep{ position:relative; }
.munow #mn-sleep .slt{ position:absolute; right:1px; bottom:1px;
  background:var(--accent); color:var(--accent-ink); font-size:var(--fs-micro); font-weight:800;
  line-height:1; padding:2px 3px; border-radius:5px; min-width:12px; text-align:center; }
.munow #mn-sleep.on{ color:var(--accent-text); }


/* Edge-swipe back indicator. Sits above content but below sheets/players, and
   never takes pointer events — it is feedback, not a control. */
#ph-eb{
  position:fixed; left:2px; top:50%;
  transform:translateY(-50%) translateX(0);
  width:42px; height:42px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  background:rgba(20,22,28,.82);
  border:1px solid rgba(255,255,255,.14);
  color:#cfd6e4;
  box-shadow:0 6px 20px rgba(0,0,0,.38);
  opacity:0; pointer-events:none; z-index:900;
  transition:opacity .12s linear, background .12s linear, color .12s linear, border-color .12s linear;
  backdrop-filter:blur(6px);
}
#ph-eb svg{ width:22px; height:22px; }
/* "ready" is the whole point of the indicator: it tells you the release will
   act BEFORE you release. */
#ph-eb.ready{
  background:var(--accent, #7aa2ff);
  border-color:transparent;
  color:var(--accent-text, #0b0d12);
}
@media (prefers-color-scheme: light){
  #ph-eb{ background:rgba(255,255,255,.92); border-color:rgba(0,0,0,.12); color:#33405c; }
}
:root[data-theme="light"] #ph-eb{ background:rgba(255,255,255,.92); border-color:rgba(0,0,0,.12); color:#33405c; }
:root[data-theme="dark"]  #ph-eb{ background:rgba(20,22,28,.82); border-color:rgba(255,255,255,.14); color:#cfd6e4; }
/* Respect a user who has asked for less motion: no slide, just the fade. */
@media (prefers-reduced-motion: reduce){
  #ph-eb{ transition:opacity .12s linear; }
}


/* JUMP BACK IN (UI-U1) — the top strip of the dashboard.
   Landscape, not posters: this row answers "where was I", so the still, the
   progress bar and the time remaining ARE the content. Posters answer "what is
   this", which is what every other shelf already does. */
.jumpsec{ margin:2px 0 6px; }
.jrow{
  display:flex; gap:12px; overflow-x:auto; padding:2px 14px 10px;
  scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch;
  scrollbar-width:none;
}
.jrow::-webkit-scrollbar{ display:none; }
.jcard{
  position:relative; flex:0 0 234px; height:132px; border-radius:14px;
  overflow:hidden; cursor:pointer; scroll-snap-align:start;
  background:var(--card, #171a21);
  border:1px solid var(--line);
  transition:transform .12s ease;
}
.jcard:active{ transform:scale(.975); }
.jcard > img{ width:100%; height:100%; object-fit:cover; display:block; }
.jph{ width:100%; height:100%; display:flex; align-items:center; justify-content:center;
      color:var(--dim); }
/* The gradient is what makes white text legible over an unknown still. */
.jgrad{ position:absolute; inset:auto 0 0 0; height:70%;
        background:linear-gradient(to top, rgba(0,0,0,.86) 0%, rgba(0,0,0,.55) 42%, rgba(0,0,0,0) 100%); }
.jmeta{ position:absolute; left:11px; right:11px; bottom:11px; }
.jt{ font-weight:650; font-size:var(--fs-small); line-height:1.25; color:#fff;
     display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.jsub{ font-size:var(--fs-meta); color:rgba(255,255,255,.78); margin-top:2px;
     white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* Which of the three sources this is — the row mixes them, so it must say. */
.jkind{
  position:absolute; top:9px; left:9px; z-index:2;
  font-size:var(--fs-micro); font-weight:700; letter-spacing:.03em; text-transform:uppercase;
  padding:3px 7px; border-radius:6px;
  background:rgba(0,0,0,.55); color:#fff; backdrop-filter:blur(4px);
}
/* The dismiss goes top-RIGHT here: .tile-x defaults to top-left, which is
   exactly where .jkind ("VIDEO" / "MUSIC") already sits on these cards. */
.jcard > .jx{ left:auto; right:9px; top:9px; }
.jprog{ position:absolute; left:0; right:0; bottom:0; height:3px; background:rgba(255,255,255,.22); }
.jprog > i{ display:block; height:100%; background:var(--accent, #7aa2ff); }
@media (min-width:720px){ .jcard{ flex-basis:268px; height:150px; } }
@media (prefers-reduced-motion: reduce){ .jcard{ transition:none; } }


/* YOUR SUBSCRIPTIONS ON HOME (UI-U2)
   16:9 thumb with the title beneath — the shape everyone already reads as
   "a YouTube video". Deliberately NOT the .jcard shape: that row means
   "resume", this one means "new". */
.ytrow{
  display:flex; gap:12px; overflow-x:auto; padding:2px 14px 10px;
  scroll-snap-type:x proximity; -webkit-overflow-scrolling:touch; scrollbar-width:none;
}
.ytrow::-webkit-scrollbar{ display:none; }
.ytile{ flex:0 0 208px; cursor:pointer; scroll-snap-align:start; transition:transform .12s ease; }
.ytile:active{ transform:scale(.975); }
.ythumb{
  width:100%; aspect-ratio:16/9; border-radius:11px; overflow:hidden;
  background:var(--card,#171a21); border:1px solid var(--line);
}
.ythumb img{ width:100%; height:100%; object-fit:cover; display:block; }
.ytt{
  margin-top:7px; font-size:var(--fs-small); font-weight:620; line-height:1.3;
  color:var(--txt);
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
.ytc{ font-size:var(--fs-meta); color:var(--dim); margin-top:3px;
      white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.ytm{ font-size:var(--fs-meta); color:var(--dim); opacity:.85; margin-top:1px; }
@media (min-width:720px){ .ytile{ flex-basis:236px; } }
@media (prefers-reduced-motion: reduce){ .ytile{ transition:none; } }


/* RECENT PLACES (UI-U3)
   Chips, not cards: a place is a destination. It needs a name and one tap, not
   artwork and a progress bar — that is what the Jump back in row above is for. */
.placesec{ margin:0 0 4px; }
.pchips{
  display:flex; gap:8px; overflow-x:auto; padding:2px 14px 10px;
  -webkit-overflow-scrolling:touch; scrollbar-width:none;
}
.pchips::-webkit-scrollbar{ display:none; }
.pchip{
  display:inline-flex; align-items:center; gap:8px; flex:0 0 auto;
  max-width:210px; padding:7px 12px 7px 7px; border-radius:999px;
  background:var(--card,#171a21); border:1px solid var(--line);
  color:var(--txt); font-size:var(--fs-small); font-weight:560;
  cursor:pointer; transition:transform .12s ease, border-color .12s ease;
}
.pchip:active{ transform:scale(.96); }
.pchip:hover{ border-color:var(--accent,#7aa2ff); }
.pcart{ width:24px; height:24px; border-radius:50%; object-fit:cover; flex:0 0 24px; }
.pcic{
  width:24px; height:24px; border-radius:50%; flex:0 0 24px;
  display:flex; align-items:center; justify-content:center;
  background:var(--raise); color:var(--dim);
}
.pctx{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
@media (prefers-reduced-motion: reduce){ .pchip{ transition:none; } }


/* SWIPE ACTIONS ON A ROW (UI-U7)
   The labels are drawn by the ROW's own pseudo-elements from its data
   attributes, so no extra DOM is created per row — these lists are rebuilt
   constantly and anything added here would be rebuilt with them. */
[data-sw]{ position:relative; }
[data-sw].sw-live{
  transition:none; z-index:1;
  background:var(--card,#171a21);   /* opaque, or the labels show through */
}
/* Snap back is the only animated part; the drag itself must track the finger. */
[data-sw]:not(.sw-live){ transition:transform .16s cubic-bezier(.2,.7,.3,1); }
[data-sw].sw-live::before,
[data-sw].sw-live::after{
  position:absolute; top:0; bottom:0; display:flex; align-items:center;
  font-size:var(--fs-meta); font-weight:700; letter-spacing:.02em; text-transform:uppercase;
  color:#fff; padding:0 16px; z-index:-1; pointer-events:none;
}
/* Swiping RIGHT reveals the action on the LEFT — the label has to sit where the
   row came FROM, or it appears to belong to the wrong gesture. */
[data-sw].sw-live.sw-right::before{
  content:attr(data-swr-label);
  left:0; right:auto; width:100vw; justify-content:flex-start;
  background:linear-gradient(90deg, var(--accent,#7aa2ff) 0%, rgba(122,162,255,.55) 100%);
  border-radius:10px 0 0 10px;
}
[data-sw].sw-live.sw-left::after{
  content:attr(data-swl-label);
  right:0; left:auto; width:100vw; justify-content:flex-end;
  background:linear-gradient(270deg, #e0567a 0%, rgba(224,86,122,.55) 100%);
  border-radius:0 10px 10px 0;
}
/* Past the commit point the label goes solid — the same "you can let go now"
   signal the edge-back chevron uses. */
[data-sw].sw-ready.sw-right::before{ background:var(--accent,#7aa2ff); }
[data-sw].sw-ready.sw-left::after{ background:#e0567a; }
@media (prefers-reduced-motion: reduce){ [data-sw]:not(.sw-live){ transition:none; } }


/* ---------- Backup: is my stuff safe? (chunk 18) ----------
   The answer goes first, in the size of an answer. Three states, because
   "nothing yet" and "still checking" are different things and a strip that
   shows one while meaning the other is worse than no strip. */
.bk-safe{ display:flex; flex-direction:column; gap:2px; padding:14px 16px; margin:0 0 10px;
  border:1px solid var(--line); border-radius:16px; background:var(--card); }
.bk-safe .bk-n{ font-size:var(--fs-title); font-weight:800; line-height:1.15; }
.bk-safe .bk-lbl{ color:var(--dim); font-size:var(--fs-meta); line-height:1.4; }
.bk-safe-ok .bk-n{ color:var(--accent-text); }
.bk-safe-none .bk-n{ color:var(--txt); }
.bk-safe-wait{ opacity:.72; }
/* A disclosure keeps every word of an explanation without spending the first
   screen on it. */
.bk-disc{ margin-top:12px; }
.bk-disc > summary{ cursor:pointer; color:var(--accent-text); font-weight:700;
  font-size:var(--fs-small); padding:8px 0; min-height:var(--tap); display:flex; align-items:center; gap:6px; }
.bk-disc > summary::-webkit-details-marker{ display:none; }
.bk-disc > summary::before{ content:'›'; display:inline-block; transition:transform .18s ease; font-size:1.1em; }
.bk-disc[open] > summary::before{ transform:rotate(90deg); }
.bk-disc .d, .bk-disc .setnote{ margin-top:2px; }

/* Artists, albums and playlists that match a search: chips, because they are
 doors rather than things to read (chunk 21). */
.mufchips{ display:flex; flex-wrap:wrap; gap:8px; margin:10px 0 4px; }
.mufchip{ display:inline-flex; align-items:center; gap:7px; min-height:var(--tap);
  padding:8px 14px; border-radius:999px; border:1px solid var(--line2); background:var(--card);
  color:var(--txt); font:inherit; font-size:var(--fs-small); font-weight:700; cursor:pointer;
  max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  transition:transform var(--ph-press), border-color var(--ph-press); }
.mufchip:active{ transform:scale(.95); }
.mufchip .ic-svg{ color:var(--accent-text); flex:0 0 auto; }

/* ---------- Artists and albums, browsed by their covers (chunk 20) ----------
 Both were the same 48px-thumbnail list as the songs, so three different
 questions had one answer: read every line. A cover is how somebody
 recognises an album, so the cover gets the room. Two across a phone, more
 as the screen allows; the tile is the tap target and the corner button is
 the same menu the row's dots opened. */
.ph-dup-del{ margin-top:10px; width:100%; }

/* Picking several photos at once (chunk 27). While the mode is on every tile
   shows an empty circle, so it is obvious that a tap now picks rather than
   opens, and the bar says how many and what can be done with them. */
.ph-tile{ position:relative; }
.ph-tile.picking::after{ content:''; position:absolute; top:6px; right:6px; width:22px; height:22px;
  border-radius:50%; border:2px solid rgba(255,255,255,.85); background:rgba(20,20,20,.35); }
.ph-tile.picked::after{ content:'✓'; display:flex; align-items:center; justify-content:center;
  border-color:var(--accent); background:var(--accent); color:var(--accent-ink); font-weight:900; font-size:var(--fs-meta); }
.ph-tile.picked img{ opacity:.72; }
.ph-selbar{ position:fixed; left:10px; right:10px; bottom:calc(var(--ph-bar-h) + 10px); z-index:80;
  display:flex; align-items:center; gap:8px; padding:10px 12px; border-radius:16px;
  background:var(--card); border:1px solid var(--line); box-shadow:0 12px 30px var(--scrim-1); }
.ph-selbar .ph-seln{ flex:1; font-weight:800; font-size:var(--fs-small); }

/* The starter cells carry sentences rather than titles ("Import a playlist"),
   so they wrap instead of truncating — a door labelled "Import a pla…" is a
   door you have to guess at (chunk 25). */
.muq.muq-start .muq-t{ white-space:normal; line-height:1.25; }

.mugrid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:18px 12px; padding:4px 0 8px; align-items:start; }
.mgtile{ cursor:pointer; min-width:0; }
.mgtile-art{ position:relative; aspect-ratio:1; border-radius:12px; overflow:hidden; background:var(--card);
  box-shadow:0 6px 18px rgba(0,0,0,.35); }
.mgtile-art img{ width:100%; height:100%; object-fit:cover; display:block; }
.mutile.isartist .mgtile-art{ border-radius:50%; }
.mgtile-play{ position:absolute; right:6px; bottom:6px; width:34px; height:34px; border-radius:50%;
  border:0; background:rgba(20,20,20,.72); color:#fff; display:flex; align-items:center; justify-content:center;
  backdrop-filter:blur(6px); cursor:pointer; opacity:0; transition:opacity .15s ease, transform var(--ph-press,120ms); }
.mgtile-play:active{ transform:scale(.9); }
/* On a phone there is no hover, so the button is always there; a pointer that
   can hover gets it on hover, the way a cover wall should look at rest. */
@media (hover:none){ .mgtile-play{ opacity:1; } }
@media (hover:hover){ .mgtile:hover .mgtile-play, .mgtile:focus-within .mgtile-play{ opacity:1; } }
.mgtile-t{ margin-top:8px; font-size:var(--fs-small); font-weight:700; line-height:1.3;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.mgtile.isartist .mgtile-t, .mgtile.isartist .mgtile-s{ text-align:center; }
.mgtile-s{ margin-top:2px; color:var(--dim); font-size:var(--fs-micro); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* The backup pre-flight: what is about to be sent, before it is (chunk 19). */
.bk-plan{ margin:2px 0 12px; }
.bk-plan-l{ color:var(--dim); font-size:var(--fs-small); line-height:1.5; }
.bk-plan-1{ color:var(--txt); font-size:var(--fs-read); font-weight:800; margin-bottom:4px; }
.bk-plan-names{ background:var(--raise); border:1px solid var(--line); border-radius:12px;
  padding:10px 12px; margin-bottom:14px; color:var(--dim); font-size:var(--fs-meta); line-height:1.6;
  max-height:132px; overflow:auto; word-break:break-all; }
.bk-plan-more{ color:var(--faint); }

/* LONG-PRESS FEEDBACK (UI-U11)
   The press must LOOK held immediately, otherwise the ~half second before the
   menu appears reads as the tap having missed. */
[data-lp]{
  /* The system's own long-press menu ("Copy image", "Open in new tab") used to
     race ours on iOS and Android over any row containing artwork or text, so
     the hold either did nothing visible or produced two menus. Suppressed only
     on elements that HAVE a menu of ours to show. */
  -webkit-touch-callout:none;
}
[data-lp].lp-hold{
  background:color-mix(in srgb, var(--accent,#7aa2ff) 12%, transparent);
  /* The press has to feel like pressure, not just a colour: the row gives way
     slightly under the finger and comes back when the menu opens. Small on
     purpose — a big scale on a list row shifts its neighbours. */
  transform:scale(.978);
  transition:background .18s ease, transform .18s cubic-bezier(.2,.7,.3,1);
}
@media (prefers-reduced-motion: reduce){ [data-lp].lp-hold{ transition:none; transform:none; } }


/* ARRANGE YOUR HOME (UI-U4) */
.arrbtn{
  margin-left:auto; align-self:center; flex:0 0 auto;
  font-size:var(--fs-meta); font-weight:650; padding:6px 11px; border-radius:999px;
  background:var(--raise); color:var(--dim);
  border:1px solid var(--line); cursor:pointer;
}
.arrbtn:hover{ color:var(--txt); border-color:var(--accent,#7aa2ff); }
/* Two of them now (Jump to / Arrange). Only the FIRST takes the slack, or the
   pair drifts apart with the greeting squeezed between them. */
.arrbtn + .arrbtn{ margin-left:6px; }
/* At 360px and under the greeting, the avatar and two pills do not all fit —
   the label goes and the pill keeps its icon-sized hit area rather than the row
   scrolling or the name being truncated to nothing. */
@media (max-width:360px){ .arrbtn{ padding:6px 9px; } }
.arr-sub{ font-size:var(--fs-meta); color:var(--dim); margin:-4px 0 10px; }
.arr-list{ display:flex; flex-direction:column; gap:6px; max-height:52vh; overflow-y:auto; }
.arr-row{
  display:flex; align-items:center; gap:8px; padding:7px 9px; border-radius:10px;
  background:var(--card,#171a21); border:1px solid var(--line);
}
.arr-row.off{ opacity:.45; }
.arr-name{ flex:1; min-width:0; font-size:var(--fs-small); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.arr-btn{
  flex:0 0 auto; min-width:30px; height:28px; border-radius:8px; cursor:pointer;
  background:var(--raise); color:var(--txt);
  border:1px solid var(--line); font-size:var(--fs-micro);
}
.arr-btn.wide{ min-width:56px; }
.arr-btn[disabled]{ opacity:.3; cursor:default; }


/* DOUBLE-TAP SEEK MARKER (UI-U18)
   A 10s jump in a slow scene looks identical to nothing happening, so the
   gesture has to say it worked. */
.vp-seekflash{
  position:absolute; top:50%; transform:translateY(-50%);
  padding:10px 16px; border-radius:999px; pointer-events:none; z-index:40;
  background:rgba(0,0,0,.62); color:#fff; font-weight:750; font-size:var(--fs-read);
  opacity:0; transition:opacity .16s ease;
  backdrop-filter:blur(4px);
}
.vp-seekflash.on{ opacity:1; }
.vp-seekflash.l{ left:8%; right:auto; }
.vp-seekflash.r{ right:8%; left:auto; }
@media (prefers-reduced-motion: reduce){ .vp-seekflash{ transition:none; } }


/* GESTURE GUIDE (UI-U22) */
.tiplist{ display:flex; flex-direction:column; gap:2px; max-height:56vh; overflow-y:auto; }
.tiprow{ padding:9px 2px; border-bottom:1px solid var(--line); }
.tiprow:last-child{ border-bottom:0; }
.tipt{ font-size:var(--fs-small); font-weight:650; color:var(--txt); }
.tipd{ font-size:var(--fs-meta); color:var(--dim); margin-top:2px; line-height:1.35; }

/* DRAG AFFORDANCE ON DISMISSIBLE OVERLAYS (UI-U22)
   Swipe-down-to-dismiss is invisible until you try it. A grabber is the
   universal "this pulls down" sign, and costs no layout: it is drawn by the
   overlay's own ::before, pinned to the top and ignoring pointer events so it
   can never intercept the gesture it advertises. */
.detail.on::before,
.munow.on::before{
  content:''; position:absolute; top:7px; left:50%; transform:translateX(-50%);
  width:38px; height:4px; border-radius:2px;
  background:rgba(255,255,255,.30); z-index:60; pointer-events:none;
}
@media (prefers-color-scheme: light){
  .detail.on::before,.munow.on::before{ background:rgba(0,0,0,.22); }
}
:root[data-theme="light"] .detail.on::before,
:root[data-theme="light"] .munow.on::before{ background:rgba(0,0,0,.22); }


/* DETAIL AS A SHEET (UI-U10)
   A detail page covered the screen edge to edge, so opening one read as leaving
   where you were rather than looking at something on top of it. On a phone it
   now stops short of the top, rounds its upper corners and casts a shadow, so
   the page behind stays visible in the gap and the thing you came from is
   obviously still there.

   PHONE ONLY. On a wide screen a sheet pinned under a thin strip looks like a
   mistake, and the full-bleed treatment is right there — so the media query is
   the feature, not a caveat.

   Deliberately only presentation: no change to .detail's scrolling, stacking or
   internals, because this element is shared by the library detail AND the music
   detail (.mudetail), and restructuring a surface two features rely on is how a
   cosmetic change becomes an outage. The grabber (U22) and swipe-down (U15)
   already exist, so the sheet reading is one the gestures already support. */
@media (max-width:719px){
  .detail{
    inset:6vh 0 0 0;
    border-radius:18px 18px 0 0;
    box-shadow:0 -14px 44px rgba(0,0,0,.45);
    /* the rounded corners must actually clip the hero image behind them */
    overflow:hidden auto;
  }
  .detail.on{ animation:phSheetIn .24s cubic-bezier(.2,.7,.3,1); }
  /* The hero is measured against the viewport, so it has to lose the 6vh the
     sheet gave up or the first screen sits lower than it used to. */
  .detail .hero{ height:min(50vh,540px); }
}
@keyframes phSheetIn{
  from{ transform:translateY(26px); opacity:.4; }
  to{ transform:translateY(0); opacity:1; }
}
@media (prefers-reduced-motion: reduce){
  .detail.on{ animation:none; }
}

/* ===== C6 · show page: what I have, and what I am missing ===== */
/* A season chip you have NOTHING of has to read as an absence, not as an
   ordinary tab — it is the whole point of listing it. The count rides inside
   the chip so the shape of the row tells you before you read a word. */
/* Scoped with the ID on purpose. `#det-seasons .fchip` already sets the border
   (specificity 1,1,0), so a plain `.fchip.gap` (0,2,0) loses and the chip keeps
   its ordinary outline — which is exactly what happened the first time: the
   badge coloured correctly, the border silently did not. Measured, not assumed. */
#det-seasons .fchip.gap{ border-color:var(--gold2); }
#det-seasons .fchip.gap i{ font-style:normal; margin-left:7px; padding:1px 6px; border-radius:7px;
  background:var(--gold2); color:#141414; font-size:var(--fs-micro); font-weight:800; }
#det-seasons .fchip.none{ opacity:.8; border-style:dashed; border-color:var(--bad-text); }
#det-seasons .fchip.none i{ background:var(--bad); color:#fff; }
/* A selected chip must still read as selected, gap or not. */
#det-seasons .fchip.on i{ background:#141414; color:#fff; }
/* The show-wide headline, next to "Episodes". */
.sub.epsgap{ color:var(--accent-text); font-weight:700; }
.sub.epsok{ color:var(--dim); }
/* Per-episode rating, inline after the title. */
.eprate{ margin-left:8px; font-size:var(--fs-meta); font-weight:800; color:var(--accent-text);
  white-space:nowrap; letter-spacing:.2px; }
/* "Missing" vs "Airs 12 Aug" — the second is not a complaint, and must not
   look like one. */
.chip.epmiss{ color:var(--bad-text); border-color:var(--bad-text); font-weight:700; }
.chip.epsoon{ color:var(--faint); }

/* ===== C5 . Download quality sheet ===== */
.dlqhint{ color:var(--dim); font-size:var(--fs-small); line-height:1.5; margin:-2px 0 12px; }
/* .sopt is the shared sheet-option button; this only adds the three-part
   layout, so a change to sheet options everywhere still reaches these. */
.dlqrow{ display:grid !important; grid-template-columns:1fr auto; row-gap:2px;
  align-items:baseline; text-align:left; }
.dlqn{ grid-column:1; font-weight:800; font-size:var(--fs-read); }
.dlqn i{ font-style:normal; margin-left:8px; padding:2px 7px; border-radius:999px;
  background:var(--raise); border:1px solid var(--line2); color:var(--dim);
  font-size:var(--fs-micro); font-weight:700; letter-spacing:.2px; }
.dlqs{ grid-column:1; color:var(--dim); font-size:var(--fs-meta); }
/* The estimate spans both rows on the right, so the number lines up with the
   name it belongs to however long the description is. */
.dlqe{ grid-column:2; grid-row:1 / span 2; align-self:center;
  color:var(--txt); font-size:var(--fs-small); font-weight:800; font-variant-numeric:tabular-nums;
  white-space:nowrap; padding-left:14px; }
.dlqalways{ display:flex; align-items:center; gap:10px; margin-top:14px; padding:2px;
  color:var(--dim); font-size:var(--fs-small); cursor:pointer; }
.dlqalways input{ width:18px; height:18px; accent-color:var(--accent); flex:0 0 auto; }

/* ===== C3 . Seek preview thumbnail ===== */
/* position:fixed, not absolute: the box is placed against the VIEWPORT from the
   seek bar's own rect, which keeps it correct in fullscreen, in the minimised
   player and on a phone in landscape — three different containing blocks that
   an absolutely-positioned box would each get wrong in its own way. */
.vpthumb{ position:fixed; z-index:9; transform:translateX(-50%); display:none;
  border-radius:8px; overflow:hidden; background:#000 center/cover no-repeat;
  border:2px solid rgba(255,255,255,.85); box-shadow:0 10px 30px var(--scrim-1);
  pointer-events:none; }
.vpthumb.on{ display:block; }
/* The timestamp sits on the frame, because a preview without one tells you what
   is there but not where it is. */
.vpthumb b{ position:absolute; left:0; right:0; bottom:0; text-align:center;
  font-size:var(--fs-micro); font-weight:800; font-variant-numeric:tabular-nums; color:#fff;
  padding:2px 0 3px; background:linear-gradient(transparent, rgba(0,0,0,.85)); }

/* ===== F1 . Asking for things ===== */
/* The one thing you came to the search results to do. Gold rather than the
   neutral .btn sec that "＋ List" uses, so the row has an obvious answer. */
.btn.askbtn{ background:var(--gold-grad); color:#141414; border:0; font-weight:800;
  display:inline-flex; align-items:center; gap:7px; }
.btn.askbtn.sm{ padding:7px 12px; font-size:var(--fs-meta); }
.btn.askbtn svg{ width:14px; height:14px; flex:0 0 auto; }
.btn.askbtn:disabled{ opacity:.65; }
/* A "Get this song" row is a music row that is NOT in the library, so it gets
   the same shape as one that is - and the Get button carries the difference. */
.murow.phget{ cursor:default; }

/* ===== F3 . What happened to my request ===== */
/* The age rides inside the chip, dimmer than the label: the state is the
   headline, how long it has been that way is the footnote. */
.chip .chipage{ font-style:normal; opacity:.72; margin-left:5px; font-weight:600; }
/* A home request row can now carry a second line and a progress bar. */
.reqrow .m.bad{ color:var(--bad-text); }
.pbar.tiny{ height:4px; margin-top:5px; }
/* "Nothing has moved" — a warning, not an error. Nothing failed; it is just
   not happening, and the row says so and offers the next move. */
.reqstale{ display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  margin-top:9px; padding:9px 11px; border-radius:10px;
  background:var(--raise); border:1px solid var(--line2); }
.reqstale span{ flex:1; min-width:150px; color:var(--dim); font-size:var(--fs-meta); line-height:1.45; }

/* ===== D4 . Collaborative playlists ===== */
.plshared{ display:inline-flex; align-items:center; gap:4px; color:var(--accent-text); font-weight:700; }
.plshared svg{ width:11px; height:11px; }
.plshare-row{ display:flex !important; align-items:center; gap:12px; text-align:left; }
.plshare-row .tick{ flex:0 0 auto; width:22px; height:22px; border-radius:50%;
  display:flex; align-items:center; justify-content:center; border:2px solid var(--line2); }
/* --accent-ink, not --accent-text: the latter IS var(--accent) in the dark
   theme, so a selected row's tick was drawn gold on gold and could not be
   seen at all. Found by a guard written for the settings badge — the same
   mistake, in code nobody was looking at. */
.plshare-row.on .tick{ background:var(--accent); color:var(--accent-ink); border-color:var(--accent); }
.plshare-row .shstate{ margin-left:auto; color:var(--dim); font-size:var(--fs-meta); font-weight:700; }

/* ===== D2 . Equaliser ===== */
.eqrow{ display:flex; align-items:center; gap:12px; padding:5px 0; }
.eqrow .eqf{ flex:0 0 44px; font-size:var(--fs-meta); font-weight:800; color:var(--txt);
  text-align:right; font-variant-numeric:tabular-nums; }
.eqrow .eqf small{ color:var(--faint); font-weight:600; margin-left:2px; }
.eqrow .slider{ flex:1; }
.eqrow .eqv{ flex:0 0 34px; font-size:var(--fs-meta); font-weight:800; color:var(--dim);
  font-variant-numeric:tabular-nums; text-align:right; }
/* ===== D3 . Quality badge in the player ===== */
.muqual{ font-size:var(--fs-micro); font-weight:800; letter-spacing:.3px; padding:2px 7px;
  border-radius:6px; border:1px solid var(--line2); color:var(--dim); white-space:nowrap; }
.muqual.lossless{ color:var(--accent-text); border-color:var(--gold2); }

/* ===== Option A · the Spotify shape for the music tab =====================
   Pierre picked A from three mockups. Two things define it: a HOME that opens
   on a greeting and the things you return to, and shelves you scroll sideways
   rather than a 1,539-row list.

   Written PHONE-FIRST on purpose. Everything below the media query is the
   phone layout; the query adds the desktop one. Until now a single stylesheet
   drawn for a big screen was copied to the phone, which is what made the phone
   feel like a squeezed desktop page. */

/* The greeting. Large, tight, and the first thing on the screen. */
.mugreet h2{ margin:2px 0 14px; font-size:var(--fs-big); font-weight:800; letter-spacing:-.025em; }

/* The quick-pick grid: one column on a narrow phone, two everywhere else.
   Spotify's cells are wide and short — art flush left, one line of label, the
   whole cell a tap target. */
.muquick{ display:grid; grid-template-columns:1fr; gap:8px; margin-bottom:26px; }
@media (min-width:380px){ .muquick{ grid-template-columns:1fr 1fr; } }
.muq{ display:flex; align-items:center; gap:11px; height:56px; padding:0;
  background:var(--raise); border:1px solid var(--line2); border-radius:7px;
  overflow:hidden; cursor:pointer; text-align:left; color:var(--txt);
  font:inherit; transition:background .12s ease; }
.muq:active{ background:var(--card); }
.muq:focus-visible{ outline:2px solid var(--accent); outline-offset:1px; }
.muq-a{ flex:0 0 auto; width:56px; height:56px; object-fit:cover; display:flex;
  align-items:center; justify-content:center; }
.muq-ph{ background:var(--gold-grad); color:#141414; }
.muq-t{ flex:1; min-width:0; font-size:var(--fs-small); font-weight:700; line-height:1.25;
  padding-right:9px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* Section chips read as sections, not filters — bigger, quieter, no border. */
/* .musections is gone: R8.2 replaced those five pills with the .mutabs strip,
   and a rule for a class nothing carries is a trap for whoever reads it next. */

/* Shelf headings sit tighter to their rail, the way Spotify's do. */
.mushelf .sect{ margin-bottom:9px; }
.mushelf .sect h2{ font-size:var(--fs-title); font-weight:800; letter-spacing:-.015em; }

/* ---- desktop and up ----------------------------------------------------
   A phone layout stretched to 1400px is just as wrong as the reverse: the
   quick grid becomes four columns and the greeting grows, because a desktop
   has the room and a phone does not. */
@media (min-width:860px){
  .mugreet h2{ font-size:32px /* glyph box, not type */; margin-bottom:18px; }
  .muquick{ grid-template-columns:repeat(3,1fr); gap:10px; margin-bottom:32px; }
  .muq{ height:62px; }
  .muq-a{ width:62px; height:62px; }
  .muq-t{ font-size:var(--fs-small); }
  .mushelf .sect h2{ font-size:var(--fs-head); }
}
@media (min-width:1200px){
  .muquick{ grid-template-columns:repeat(4,1fr); }
}

/* ===== Option A · chunk 2 · the restyle ==================================
   Chunk 1 gave the music tab Spotify's SHAPE. This gives it the finish.

   THE SIGNATURE THAT WAS MISSING: Spotify draws every artist as a circle and
   every album, playlist and song as a square. That single rule is how you know
   what kind of thing a row is before reading a word of it, and Plex Hub drew
   all of them as identical squares. */
.murow.isartist .mcov,
.murow.isartist .mcovw,
.muq-a.isartist,
.mutile.isartist .shelfcov{ border-radius:50%; }
/* A round cover needs its fallback glyph centred in the circle, not in a box
   that happens to be clipped. */
.murow.isartist div.mcov{ border-radius:50%; }

/* Shelf tiles at Spotify's phone proportions. 128px was drawn when this
   stylesheet also had to serve a desktop; on a 390px screen it left an odd
   half-tile of dead space at the right edge. 150 shows two and a hint of the
   third, which is the cue that the row scrolls. */
.mutile{ flex:0 0 150px; width:150px; }
.mutile .shelfcov{ width:150px; height:150px; flex:0 0 150px; border-radius:6px; }
.mutile .t{ margin-top:9px; font-size:var(--fs-small); font-weight:650; }
.mutile .s{ font-size:var(--fs-meta); margin-top:1px; }
/* Artist tiles caption the kind, because a circle alone does not say "artist"
   to someone who has never used Spotify. */
.mutile.isartist .s::before{ content:"Artist"; }

/* Song rows: Spotify's pitch is roomier than ours was. The art stays 48px —
   that part was already measured against the real app — but the row breathes. */
.murow{ padding:9px 12px; gap:13px; }
.murow .m1{ font-size:var(--fs-read); }
.murow .m2{ font-size:var(--fs-meta); margin-top:2px; }

/* Section headings: bigger, tighter, and closer to what follows them. */
.mushelf .sect h2, #mu-lib .sect h2{ font-size:var(--fs-title); font-weight:800; letter-spacing:-.02em; }

/* The mini player. Spotify's sits directly above the nav, nearly edge to edge,
   with the progress line pinned to its very bottom rather than inset. */
.mubar{ left:6px; right:6px; border-radius:9px; }
/* flex-basis, NOT just width — the base rule pins `flex:0 0 40px`, which wins
   over a plain width and left the art 40 wide by 42 tall. A squashed cover on
   the one element that is always on screen. Measured, not assumed. */
.mubar .cov{ width:42px; height:42px; flex:0 0 42px; border-radius:5px; }
.mubar .m1{ font-size:var(--fs-small); }
.mubar .m2{ font-size:var(--fs-micro); }
.mubar .mprog{ left:0; right:0; bottom:0; height:2px; border-radius:0 0 9px 9px; }

/* Playlist rows in Your Library — square art, two lines, Spotify's density. */
.pltile{ padding:8px 6px; gap:12px; }
.pltile .plcov{ width:56px; height:56px; flex:0 0 56px; border-radius:5px; }
.pltile .pln{ font-size:var(--fs-small); font-weight:650; }
.pltile .plm{ font-size:var(--fs-meta); margin-top:2px; }

@media (min-width:860px){
  /* Desktop has the room for a bigger shelf, and a hover state worth having. */
  .mutile{ flex:0 0 168px; width:168px; }
  .mutile .shelfcov{ width:168px; height:168px; flex:0 0 168px; }
  .murow{ padding:8px 12px; }
}

/* ===== Music detail sheet · the close rail ================================
   WHY STICKY AND NOT FIXED. The X used to be inline position:fixed. A fixed
   element is positioned against the nearest transformed ancestor, not the
   viewport, and .detail is transformed twice over: `animation:vin` on open and
   every frame of swipe-to-dismiss. So the button drifted precisely while you
   were dragging. Measured: computed top 14px, actual rect top 91px.

   Sticky resolves against the scroll container regardless of transforms. The
   rail is zero-height so it holds the button without occupying any layout. */
.detail .mudt-rail{ position:sticky; top:0; height:0; z-index:6;
  display:block; touch-action:none; }
.detail .mudt-rail .hback{ position:absolute; top:calc(var(--sa-t) + 14px); left:16px; }
/* The grab handle. The global swipe-to-dismiss refuses to arm once a list is
   scrolled — correct, or the gesture would fight the list — which left a
   scrolled sheet with no touch way out at all. A drag that starts on this
   handle is unambiguous, so it is the one that always dismisses. */
.detail .mudt-grab{ display:none; }
.mudetail .mudt-grab{ display:block; position:absolute; top:calc(var(--sa-t) + 7px); left:50%;
  transform:translateX(-50%); width:38px; height:4px; border-radius:2px;
  background:var(--line2); opacity:.9; cursor:grab; }
.mudetail .mudt-grab::after{ content:''; position:absolute; left:-40px; right:-40px;
  top:-12px; bottom:-14px; }   /* a thumb-sized target around a hairline */
@media (min-width:860px){ .mudetail .mudt-grab{ display:none; } }

/* A downloaded group's title opens its playlist now, so it should look like it
   can be tapped on its own. */
.plghead .pln.plgo{ cursor:pointer; }
.plghead .pln.plgo:active{ opacity:.6; }

/* ===== Option A, the rest of the sections ================================
   Chunk 1 gave Home the Spotify shape and stopped there, which is why the
   other three read as a different app. */

/* Library > Discover. A "get this" affordance on the tile itself: these are
   the only tiles in the app whose tap means "fetch it", and that has to be
   legible before the tap, not after. */
.mutile{ position:relative; }
.disc-get{ position:absolute; top:6px; right:6px; width:24px; height:24px;
  border-radius:50%; display:flex; align-items:center; justify-content:center;
  background:var(--accent); color:#08120b; box-shadow:0 2px 8px rgba(0,0,0,.45);
  opacity:.92; transition:opacity .16s, transform .16s; }
/* Always visible, not hover-revealed. These are the only tiles in the app whose
   tap means "fetch this", and a phone has no hover to reveal it with — the
   @media (hover:none) dodge also misses hybrid touchscreens that report hover. */
.mutile:hover .disc-get{ opacity:1; transform:scale(1.08); }
.mutile:active .disc-get{ transform:scale(.94); }

/* Playlists: the sort row, matching the browse chips in the Library. */
.mupl-sort{ margin:0 0 10px; gap:6px; }
/* Import · New playlist · Connect Spotify came to 364px on a 320px screen
   (chunk 28). Three separate actions, so they wrap onto a second line
   rather than scroll: a control you have to drag the row to find is a
   control most people never find. */
.muplact{ flex-wrap:wrap; }
.muplact > *{ min-width:0; }

/* Rows at one density across Library, Playlists and Downloaded. These were
   three slightly different sizes before, which is most of why the sections
   looked unrelated to each other. */
.pltile, .plghead{ padding:8px 6px; gap:12px; }
.pltile .plcov, .plghead .plgcov{ width:56px; height:56px; flex:0 0 56px; border-radius:5px; }
.pltile .pln, .plghead .pln{ font-size:var(--fs-small); font-weight:650; }
.pltile .plm, .plghead .plm{ font-size:var(--fs-meta); margin-top:2px; }
/* One line, ellipsised. A three-line playlist name pushed the rows out of any
   rhythm and made the list impossible to scan. */
.pltile .pln, .plghead .pln{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }

/* The artwork rule is a rule, not a Home flourish: artists are circles and
   everything else is a square, everywhere a row can appear. */
.murow.isartist .mcov, .murow.isartist .mcovw, .murow.isartist img.mcov{ border-radius:50%; }

/* Discover shelves breathe like Home's. */
#mu-lib .mushelf{ margin-bottom:18px; }
#mu-lib .mushelf .sect h2{ font-size:var(--fs-title); font-weight:800; letter-spacing:-.02em; }

/* The artist hero, round — completing the rule that started on the Home grid.
   .mudetail .mhero .bigc pins border-radius:0, so this has to out-specify it. */
.detail.mudetail.mu-artist .mhero .bigc{ border-radius:50%; }

/* ===== H5 · the now-playing sheet, in Spotify's shape ====================
   MEASURED at 390x844: the top row carried eight controls and squeezed the
   playing-from label to 59px — six characters for the one element that says
   where the music is coming from. Three now; the rest moved into the ... sheet
   where each gets a name instead of being an unlabelled 38px circle. */
.munow .mn-top{ display:flex; width:100%; align-items:center; gap:8px; }
/* The label takes the room the five departed buttons were using. */
.munow .mn-top .mn-ctx{ flex:1 1 auto; min-width:0; text-align:center;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.munow .mn-top .close{ flex:0 0 auto; align-self:center; }
/* Parked in the sheet. Driving mode reveals its own exit below. */
.munow #mn-speed, .munow #mn-sleep, .munow #mn-cast, .munow #mn-drive{ display:none; }
.munow #mn-out{ display:none !important; }   /* reached from the sheet */
.munow.driving #mn-drive{ display:flex; }

/* Title block: hard left, with the save control in the row it belongs in.
   Centred text under centred art gives the eye nothing to anchor on. */
.munow .mn-head{ display:flex; align-items:center; gap:12px; width:100%; max-width:420px; margin-top:2px; }
.munow .mn-headtx{ flex:1 1 auto; min-width:0; }
.munow .mn-head .n1, .munow .mn-head .n2{ text-align:left; justify-content:flex-start; }
.munow .mn-like{ flex:0 0 auto; width:40px; height:40px; border-radius:50%;
  background:transparent; border:0; color:var(--dim); display:flex;
  align-items:center; justify-content:center; cursor:pointer; }
.munow .mn-like.on{ color:var(--gold2); }
.munow .mn-like:active{ transform:scale(.9); }

/* The action bar. Lyrics used to be an 11px pill wedged into the artist line,
   competing with the ellipsis for room on the one line that must not wrap. */
.munow .mn-acts{ display:flex; width:100%; max-width:420px; gap:8px; margin-top:16px; }
.munow .mn-act{ flex:1 1 0; display:flex; flex-direction:column; align-items:center;
  gap:5px; padding:9px 4px; border-radius:10px; background:var(--raise);
  border:1px solid var(--line2); color:var(--dim); font-size:var(--fs-micro); font-weight:650;
  cursor:pointer; min-width:0; }
.munow .mn-act span{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%; }
.munow .mn-act:active{ background:var(--card); }
.munow .mn-act.on{ color:var(--gold2); border-color:var(--gold2); }
.munow.driving .mn-acts{ display:none; }

/* A sheet row that carries a current value on the right. */
.sopt .soptv{ float:right; color:var(--dim); font-weight:650; }

/* "Up next" expanded. The queue had 108px for 804px of rows; the artwork is the
   one thing on this screen you are not reading while you scan a track list. */
.munow.qopen .bigcov{ display:none; }
.munow.qopen #mn-lyrics{ display:none !important; }
.munow.qopen #mn-queue{ flex:1 1 auto; margin-top:10px; min-height:38vh; }
.munow.qopen .mn-head{ margin-top:10px; }

/* ===== Cast transport controls ==========================================
   The picker shipped without them: the app could start a film on the TV and
   then had no pause, no seek, no volume and no stop. */
.castnow{ margin:2px 0 10px; }
.castnow .castt{ font-size:var(--fs-read); font-weight:750; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.castnow .casts{ color:var(--dim); font-size:var(--fs-meta); margin-top:2px; }
.castseek{ width:100%; }
.castbtns{ display:flex; align-items:center; justify-content:center; gap:14px; margin:12px 0 4px; }
.castbtns .btn{ width:52px; height:44px; display:flex; align-items:center; justify-content:center; padding:0; }
.castvol{ display:flex; align-items:center; gap:11px; margin-top:10px; }
.castvol .slider{ flex:1; }
.castvol .vic{ color:var(--dim); cursor:pointer; display:flex; flex:0 0 auto; }

/* The chip: something is on the television, and there is always a way back to
   it from anywhere in the app. Above the nav, clear of the mini player. */
/* MEASURED at 390x844 with music playing: at 118px the chip's bottom edge sat
   at y726 and the mini player's top at y714 — a 12px overlap. The bar is only
   there when something is playing, so the offset has to move with it. Same
   :has() approach the mini player itself already uses to dodge an open detail. */
.castchip{ position:fixed; left:50%; transform:translateX(-50%);
  bottom:calc(var(--sa-b) + 118px); z-index:70;
  display:flex; align-items:center; gap:6px; max-width:70vw;
  padding:6px 12px; border-radius:999px; cursor:pointer;
  background:var(--accent); color:#08120b; border:0;
  font-size:var(--fs-meta); font-weight:800; box-shadow:0 6px 18px var(--scrim-2); }
.castchip span{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.castchip:active{ opacity:.85; }
body:has(#mubar.on) .castchip{ bottom:calc(var(--sa-b) + 146px); }

/* ===== R6 chunk 1 — honest downloads (D1.1–D1.10) =========================
   qBittorrent's vocabulary used to reach the screen unedited. These styles
   carry the replacement: a sentence per part, the reason under it, and the
   stage it has actually reached. Colour is never the only signal — every
   state also says what it is in words. */
.dl-totals{
  display:flex;flex-wrap:wrap;align-items:center;gap:6px;
  margin:0 0 10px;padding:9px 12px;border:1px solid var(--line);border-radius:10px;
  background:var(--card);font-size:var(--fs-meta);color:var(--faint);
}
.dl-totals b{color:var(--txt);font-variant-numeric:tabular-nums}
.dl-t-sep{opacity:.4}
.dl-t-q{color:var(--faint)}
.dl-t-d{color:var(--bad-text)}
.dl-pick{
  margin-top:7px;padding:7px 9px;border-radius:8px;background:var(--surface);
  font-size:var(--fs-meta);line-height:1.45;
}
.dl-pick b{display:block;font-weight:600;color:var(--txt);word-break:break-all}
.dl-pick span{color:var(--faint)}
.dl-parts{margin-top:8px;border-top:1px solid var(--line);padding-top:4px}
.dl-part{margin:9px 0 0}
.dl-part-top{display:flex;justify-content:space-between;gap:8px;font-size:var(--fs-meta)}
.dl-part-name{font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dl-part-pct{color:var(--faint);flex:none;font-variant-numeric:tabular-nums}
.dl-part-state{font-size:var(--fs-meta);font-weight:600;margin-top:4px}
.dl-part-peers{font-size:var(--fs-micro);color:var(--faint);margin-top:1px;font-variant-numeric:tabular-nums}
.dl-part-why{font-size:var(--fs-micro);color:var(--faint);margin-top:3px;line-height:1.45;opacity:.9}
.dl-stages{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}
.dl-stage{
  font-size:var(--fs-micro);letter-spacing:.03em;padding:2px 6px;border-radius:999px;
  background:var(--line);color:var(--faint);
}
.dl-stage.on{background:rgba(229,160,13,.16);color:var(--gold)}
.dl-stage.at{background:var(--gold);color:#181818;font-weight:700}
.dl-logbtn{margin-top:9px}
.dl-log{
  margin-top:7px;max-height:190px;overflow:auto;border:1px solid var(--line);
  border-radius:8px;padding:7px 9px;background:var(--bg);
}
.dl-logline{font-size:var(--fs-micro);color:var(--faint);line-height:1.5;padding:3px 0;word-break:break-word}
.dl-logline+.dl-logline{border-top:1px solid var(--line)}
.dl-logline span{
  display:inline-block;min-width:74px;color:var(--gold);font-weight:600;
}
.dl-finbtn{margin-top:10px}

/* ===== R6 chunk 2 — ask from Home, and be told (H5.1–H5.10) ============== */
.askrow{display:flex;gap:8px;align-items:center;margin-bottom:8px}
.askrow .input{flex:1;min-width:0}
#h-askmic.rec{background:var(--gold);color:#181818;animation:askpulse 1.1s ease-in-out infinite}
@keyframes askpulse{0%,100%{opacity:1}50%{opacity:.55}}
.askexact{font-size:var(--fs-meta);color:var(--faint);margin:2px 0 6px}
.poster.sm{width:38px;height:57px;border-radius:5px;object-fit:cover;flex:none;background:var(--line)}
.arrival{
  display:flex;align-items:center;gap:10px;padding:10px 12px;margin-bottom:8px;
  border:1px solid var(--gold);border-radius:12px;
  background:linear-gradient(180deg,rgba(229,160,13,.10),transparent);
}
.arrival .rt{flex:1;min-width:0}
.arrival .rt .t{font-weight:700;font-size:var(--fs-small);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.arrival .rt .m{color:var(--faint);font-size:var(--fs-meta)}
.alsoreq{color:var(--faint);font-size:var(--fs-micro);opacity:.9}
/* A <button> now: the week's counts open the requests they are counting. */
.digest{
  display:flex;flex-wrap:wrap;align-items:center;gap:7px;margin-bottom:12px;
  padding:9px 12px;border:1px solid var(--line);border-radius:10px;
  background:var(--card);font-size:var(--fs-meta);color:var(--faint);
  width:100%;text-align:left;font-family:inherit;cursor:pointer;
  -webkit-appearance:none;appearance:none;
}
.digest:hover{ border-color:var(--line2,var(--line)); color:var(--txt); }
.digest .dggo{ margin-left:auto; display:inline-flex; align-items:center; }
/* The artwork and the name on an "it arrived" card open it. */
.arr-open{ cursor:pointer; min-width:0; }
.arr-open:focus-visible{ outline:2px solid var(--accent-text); outline-offset:2px; border-radius:6px; }
.digest b{color:var(--txt);font-variant-numeric:tabular-nums}
.dgt{font-size:var(--fs-micro);letter-spacing:.1em;text-transform:uppercase;color:var(--gold);font-weight:700}
.dgsep{opacity:.4}

/* ===== R6 chunk 3 — arranging Home (H6.1–H6.10) ========================== */
/* H6.7 — the same shelf, wrapped instead of scrolling sideways. */
#v-home .as-grid .hrow{ flex-wrap:wrap; overflow-x:visible; scroll-snap-type:none; }
/* H6.5 — the hero, three ways. */
/* "Compact" has to be visibly compact — a first pass only took 81px to 73px,
   which is not a choice anyone would notice making. The greeting shrinks to a
   line of text with a small avatar. */
#v-home[data-hero="compact"] .hero{ margin:2px 0 0; gap:9px; }
#v-home[data-hero="compact"] .hero img{ width:30px; height:30px; border-width:1.5px; }
#v-home[data-hero="compact"] .hero .hi{ font-size:var(--fs-small); }
#v-home[data-hero="compact"] .hero .sub{ display:none; }
#v-home[data-hero="compact"] .arrbtn{ padding:3px 9px; font-size:var(--fs-meta); }
#v-home[data-hero="none"] .hero{ display:none; }
/* H6.4 — the pinned title. */
.pinned{ display:flex; align-items:center; gap:12px; padding:10px 12px; margin-bottom:12px;
  border:1px solid var(--line); border-radius:14px; cursor:pointer;
  background:linear-gradient(180deg,rgba(229,160,13,.10),transparent); }
.pinned img,.pinart{ width:44px; height:66px; border-radius:6px; object-fit:cover; flex:none; background:var(--line); }
.pintx{ flex:1; min-width:0; }
.pinlab{ font-size:var(--fs-micro); letter-spacing:.12em; text-transform:uppercase; color:var(--gold); font-weight:700; }
.pint{ font-weight:700; font-size:var(--fs-small); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* H6.6 — see all. */
.seeall{ margin-left:auto; background:none; border:1px solid var(--line2,var(--line)); color:var(--faint);
  font:inherit; font-size:var(--fs-meta); font-weight:600; padding:3px 9px; border-radius:999px; cursor:pointer; }
.seeall:hover{ color:var(--txt); }
.seeall-ov{ position:fixed; inset:0; z-index:130; background:var(--bg); overflow-y:auto; padding:0 14px 40px; }
.seeall-top{ position:sticky; top:0; z-index:2; display:flex; align-items:center; gap:12px;
  padding:12px 0; background:var(--bg); font-weight:700; }
/* Arrange sheet additions. */
.arr-seg{ display:flex; flex-wrap:wrap; align-items:center; gap:6px; margin:8px 0; }
.arr-seg>span{ font-size:var(--fs-micro); letter-spacing:.08em; text-transform:uppercase; color:var(--faint);
  min-width:62px; }
.arr-btn.on{ background:var(--gold); color:#181818; font-weight:700; }
.arr-grip{ cursor:grab; color:var(--faint); font-size:var(--fs-small); letter-spacing:-3px; padding:0 2px;
  touch-action:none; user-select:none; }
.arr-row.dragging{ opacity:.6; background:var(--surface); }
.arr-acts{ display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; }
.arr-acts .btn{ flex:1 1 46%; }
.arr-io{ width:100%; height:130px; margin-top:8px; font-family:ui-monospace,Consolas,monospace;
  font-size:var(--fs-micro); resize:vertical; }
.arr-diff{ margin:6px 0 0; padding-left:18px; color:var(--faint); font-size:var(--fs-small); line-height:1.7; }

/* ===== R6 chunk 4 — first run and empty states (H10.1–H10.10) =========== */
.emptycard{ border:1px dashed var(--line2,var(--line)); border-radius:14px; padding:16px 14px;
  margin-bottom:12px; text-align:center; }
.emptycard.down{ border-style:solid; border-color:var(--bad-text); }
.ec-t{ font-weight:700; font-size:var(--fs-read); margin-bottom:4px; }
.ec-s{ color:var(--faint); font-size:var(--fs-meta); margin-bottom:10px; }
.ec-acts{ display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin-top:10px; }
.whatsnew{ border:1px solid var(--line); border-radius:12px; padding:12px 14px; margin-bottom:12px;
  background:linear-gradient(180deg,rgba(229,160,13,.07),transparent); }
.wn-t{ font-size:var(--fs-micro); letter-spacing:.12em; text-transform:uppercase; color:var(--gold);
  font-weight:700; margin-bottom:6px; }
.whatsnew ul{ margin:0 0 10px; padding-left:18px; color:var(--txt); font-size:var(--fs-small); line-height:1.65; }
.ob-genres{ margin-top:10px; }
.ob-genres .fchip{ margin-bottom:6px; }
/* H10.8 — a one-line hint at the moment the gesture would help. */
.ph-tip{ position:fixed; left:50%; bottom:96px; z-index:140; transform:translate(-50%,14px);
  opacity:0; transition:opacity .3s, transform .3s; pointer-events:none;
  background:var(--card); border:1px solid var(--line2,var(--line)); border-radius:999px;
  padding:8px 15px; font-size:var(--fs-meta); color:var(--txt); box-shadow:0 10px 30px -14px #000;
  max-width:min(88vw,360px); text-align:center; }
.ph-tip.go{ opacity:1; transform:translate(-50%,0); }
@media (prefers-reduced-motion:reduce){ .ph-tip{ transition:opacity .01s; } }

/* ===== R6 chunk G1 — other devices on this account ====================== */
.devcard{ border:1px solid var(--line); border-radius:14px; padding:12px 14px; margin-bottom:10px;
  background:linear-gradient(180deg,rgba(229,160,13,.06),transparent); }
.devtop{ display:flex; align-items:center; gap:8px; margin-bottom:5px; }
.devname{ font-weight:700; font-size:var(--fs-small); }
.devkind,.devstate{ font-size:var(--fs-micro); letter-spacing:.08em; text-transform:uppercase; color:var(--faint); }
.devstate{ margin-left:auto; color:var(--gold); }
.devtrack{ font-size:var(--fs-small); margin-bottom:6px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.devtrack span{ color:var(--faint); }
.devtime{ display:flex; justify-content:space-between; font-size:var(--fs-micro); color:var(--faint);
  margin-top:3px; font-variant-numeric:tabular-nums; }
.devbtns{ display:flex; flex-wrap:wrap; gap:6px; margin-top:8px; }
.devvol{ display:flex; align-items:center; gap:8px; margin-top:9px; font-size:var(--fs-micro); color:var(--faint); }
.devvol input{ flex:1; accent-color:var(--gold); }
.devsync{ margin-top:7px; font-size:var(--fs-micro); color:var(--ok-text); font-variant-numeric:tabular-nums; }
/* R6 D2.10 — the hopeless verdict card */
.reqstale.hopeless{ border-color:var(--bad-text); background:linear-gradient(180deg,rgba(226,104,95,.10),transparent); }
/* R6 H2.6 — time remaining on a continue card */
.cont-rem{ color:var(--gold); font-size:var(--fs-micro); font-weight:600; }

/* R6 M6.2/M6.3 — lyric search + karaoke view */
.lyricoffer{ padding:10px 12px; margin-bottom:10px; border:1px dashed var(--line2,var(--line));
  border-radius:10px; font-size:var(--fs-small); color:var(--faint); display:flex; align-items:center;
  gap:8px; flex-wrap:wrap; }
.lyricoffer b{ color:var(--txt); }
/* The now-playing lyrics box carries an INLINE font-size:var(--fs-read), which beats a
   plain class rule (the inline-style-wins gotcha). !important is the honest fix
   here rather than restructuring the markup for one toggle. */
#mn-lyrics.karaoke{ font-size:var(--fs-big) !important; line-height:1.8; font-weight:600; }
#mn-lyrics.karaoke > div{ opacity:.5; transition:opacity .2s, transform .2s; }
#mn-lyrics.karaoke > div[style*="800"]{ opacity:1; transform:scale(1.04); }
/* The vocal blend, shown only inside the karaoke view. */
.kar-vox{ width:100%; padding:6px 18px 12px; text-align:center; }
.kar-vox label{ display:block; font-size:var(--fs-small); font-weight:700; color:var(--dim); margin-bottom:6px; }
.kar-vox #kar-val{ color:var(--accent); }
.kar-vox input[type=range]{ width:100%; max-width:340px; accent-color:var(--accent); }
.kar-note{ font-size:var(--fs-micro); color:var(--faint); line-height:1.5; margin-top:6px;
  max-width:360px; margin-left:auto; margin-right:auto; }

/* ============ Listen: audiobooks & podcasts (R6 chunk 12) ============ */
#v-listen{ padding-bottom:calc(var(--ph-bar-h) + 96px); }
/* Chunk 28 — measured at 320px: the third tab ("Free up space") ended 10px
   past the screen edge, taking the page's own scrollbar with it. These are
   chips, and chip rows in this app scroll sideways; this one simply never
   said so. */
/* overflow-x:auto pans the chips; no touch-action, or a vertical swipe starting
   on a chip cannot scroll the page (see the note by `.vp .seek`). */
.sw-tabs{ display:flex; gap:8px; padding:0 16px 8px; overflow-x:auto; scrollbar-width:none;
  -webkit-overflow-scrolling:touch; }
.sw-tabs::-webkit-scrollbar{ display:none; }
.sw-tab{ flex:0 0 auto; padding:7px 16px; border-radius:999px; border:1px solid var(--line2);
  background:var(--surface); color:var(--dim); font-size:var(--fs-small); font-weight:700; cursor:pointer; }
.sw-tab.on{ background:var(--gold); color:#111; border-color:var(--gold); }
.sw-add{ display:flex; gap:8px; padding:4px 16px 14px; }
#sw-feed{ flex:1; min-width:0; background:var(--surface); border:1px solid var(--line2); color:var(--txt);
  border-radius:10px; padding:11px 13px; font-size:var(--fs-small); }
#sw-feed::placeholder{ color:var(--faint); }
.sw-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(108px,1fr)); gap:14px; padding:0 16px; }
.sw-card{ cursor:pointer; }
.sw-card-art{ aspect-ratio:1; border-radius:12px; overflow:hidden; background:var(--card);
  display:flex; align-items:center; justify-content:center; font-size:34px /* glyph box, not type */; box-shadow:0 4px 14px rgba(0,0,0,.35); }
.sw-card-art img{ width:100%; height:100%; object-fit:cover; }
.sw-card-t{ font-size:var(--fs-meta); font-weight:700; margin-top:7px; line-height:1.25;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.sw-card-s{ font-size:var(--fs-micro); color:var(--dim); margin-top:2px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.sw-empty{ grid-column:1/-1; color:var(--dim); font-size:var(--fs-small); line-height:1.6; padding:24px 6px; text-align:center; }
.sw-empty b{ color:var(--txt); }

.sw-strip-h{ font-size:var(--fs-small); font-weight:800; padding:2px 16px 8px; }
.sw-strip{ display:flex; gap:12px; overflow-x:auto; padding:0 16px 16px; scrollbar-width:none; }
.sw-strip::-webkit-scrollbar{ display:none; }
.sw-cont{ flex:0 0 148px; cursor:pointer; }
.sw-cont-art{ height:90px; border-radius:12px; overflow:hidden; background:var(--card);
  display:flex; align-items:center; justify-content:center; font-size:var(--fs-hero); }
.sw-cont-art img{ width:100%; height:100%; object-fit:cover; }
.sw-cont-t{ font-size:var(--fs-meta); font-weight:700; margin-top:6px; line-height:1.25;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
.sw-cont-bar{ height:3px; background:var(--line2); border-radius:2px; margin:5px 0 3px; overflow:hidden; }
.sw-cont-bar span{ display:block; height:100%; background:var(--gold); }
.sw-cont-s{ font-size:var(--fs-micro); color:var(--dim); }

.sw-sheet-h{ display:flex; gap:14px; align-items:center; margin-bottom:14px; }
.sw-sheet-art{ width:72px; height:72px; border-radius:12px; overflow:hidden; background:var(--card);
  display:flex; align-items:center; justify-content:center; font-size:var(--fs-hero); flex:none; }
.sw-sheet-art img{ width:100%; height:100%; object-fit:cover; }
.sw-sheet-t{ font-size:var(--fs-title); font-weight:800; line-height:1.2; }
.sw-sheet-s{ font-size:var(--fs-small); color:var(--dim); margin-top:3px; }
.sw-eps{ margin-top:12px; display:flex; flex-direction:column; }
.sw-ep{ display:flex; gap:12px; align-items:center; padding:11px 4px; border-top:1px solid var(--line);
  cursor:pointer; }
.sw-ep:hover{ background:var(--surface); }
.sw-ep-play{ width:30px; height:30px; border-radius:50%; background:var(--gold); color:#111; flex:none;
  display:flex; align-items:center; justify-content:center; font-size:var(--fs-meta); font-weight:800; }
.sw-ep-t{ font-size:var(--fs-small); font-weight:600; line-height:1.3; }
.sw-ep-s{ font-size:var(--fs-meta); color:var(--dim); margin-top:2px; }

/* the spoken-word now-playing bar — sits directly above the bottom nav */
.sw-bar{ position:fixed; left:0; right:0; bottom:var(--ph-bar-h); z-index:39;
  display:flex; align-items:center; gap:11px; padding:9px 12px;
  background:var(--nav-bg,rgba(20,20,20,.96)); backdrop-filter:blur(16px); border-top:1px solid var(--line); }
.sw-bar-art{ width:44px; height:44px; border-radius:9px; overflow:hidden; background:var(--card); flex:none;
  display:flex; align-items:center; justify-content:center; font-size:var(--fs-big); }
.sw-bar-art img{ width:100%; height:100%; object-fit:cover; }
.sw-bar-mid{ flex:1; min-width:0; }
.sw-bar-title{ font-size:var(--fs-small); font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.sw-bar-scrub{ margin:5px 0 2px; }
#sw-seek{ width:100%; height:4px; -webkit-appearance:none; appearance:none; background:var(--line2);
  border-radius:2px; accent-color:var(--gold); }
.sw-bar-time{ display:flex; justify-content:space-between; font-size:var(--fs-micro); color:var(--dim); }
.sw-bar-ctl{ display:flex; align-items:center; gap:4px; flex:none; }
.sw-cbtn{ border:none; background:transparent; color:var(--txt); font-size:var(--fs-meta); font-weight:700;
  min-width:34px; height:34px; border-radius:8px; cursor:pointer; padding:0 6px; }
.sw-cbtn:hover{ background:var(--raise); }
.sw-cbtn.sw-play{ background:var(--gold); color:#111; font-size:var(--fs-small); }
.sw-cbtn.on{ color:var(--gold); }
@media(max-width:520px){ .sw-cbtn#sw-back,.sw-cbtn#sw-fwd{ display:none; } }

/* ============ Netflix browse & shelves (R6 chunk 13) ============ */
.libsurprise{ flex:0 0 auto; border:1px solid var(--line2); background:var(--surface); color:var(--txt);
  border-radius:999px; padding:6px 13px; font-size:var(--fs-meta); font-weight:700; cursor:pointer; white-space:nowrap; }
.libsurprise:hover{ border-color:var(--gold); color:var(--gold); }
.libviewseg{ flex:0 0 auto; }
.libviewseg button{ font-size:var(--fs-read); line-height:1; min-width:30px; }
.libchip.toggle{ border-color:var(--line2); }
.libchip.toggle.on{ background:var(--gold); color:#111; border-color:var(--gold); }
.libchip-sep{ display:inline-block; width:1px; align-self:center; height:16px; background:var(--line2); margin:0 4px; flex:0 0 auto; }

/* the watched corner tick on a grid poster */
.ptile .pw{ position:relative; }
.seenb{ position:absolute; top:6px; left:6px; z-index:3; width:24px; height:24px; border-radius:50%;
  display:flex; align-items:center; justify-content:center; font-size:var(--fs-small); font-weight:800; cursor:pointer;
  background:rgba(0,0,0,.55); color:#fff; border:1.5px solid rgba(255,255,255,.5);
  opacity:0; transition:opacity .12s, background .12s; }
.pw:hover .seenb{ opacity:.85; }
.ptile.seen .seenb{ opacity:1; background:var(--gold); color:#111; border-color:var(--gold); }

/* list / detailed-list views */
.liblist, .libdetail{ display:flex; flex-direction:column; gap:2px; padding:0 2px; }
.lrow2{ display:flex; align-items:center; gap:12px; padding:8px 10px; border-radius:10px; cursor:pointer; }
.lrow2:hover{ background:var(--surface); }
.lrow2+.lrow2{ border-top:1px solid var(--line); }
.lrow2-art{ position:relative; width:44px; height:64px; flex:none; border-radius:6px; overflow:hidden; background:var(--card); }
.libdetail .lrow2-art{ width:54px; height:80px; }
.lrow2-art img{ width:100%; height:100%; object-fit:cover; }
.lrow2-tick{ position:absolute; top:2px; left:2px; width:18px; height:18px; border-radius:50%; background:var(--gold);
  color:#111; font-size:var(--fs-micro); font-weight:800; display:flex; align-items:center; justify-content:center; }
.lrow2-mid{ flex:1; min-width:0; }
.lrow2-t{ font-size:var(--fs-small); font-weight:600; line-height:1.3; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.lrow2-s{ font-size:var(--fs-meta); color:var(--dim); margin-top:2px; }
.lrow2.seen .lrow2-t{ color:var(--dim); }
.lrow2-seen{ flex:none; border:1px solid var(--line2); background:transparent; color:var(--dim);
  border-radius:999px; padding:5px 11px; font-size:var(--fs-meta); font-weight:700; cursor:pointer; }
.lrow2.seen .lrow2-seen{ background:var(--gold); color:#111; border-color:var(--gold); }

/* ============ Series handling — watched marks (R6 chunk 14) ============ */
.epthumb{ position:relative; }
.epseentick{ position:absolute; top:5px; left:5px; z-index:3; width:20px; height:20px; border-radius:50%;
  background:var(--gold); color:#111; font-size:var(--fs-meta); font-weight:800;
  display:flex; align-items:center; justify-content:center; box-shadow:0 1px 4px rgba(0,0,0,.5); }
.epcard.seen .epthumb img{ opacity:.62; }
.epcard.seen{ position:relative; }
.btn.onwatched{ background:var(--gold) !important; color:#111 !important; border-color:var(--gold) !important; }
.btn.epseen{ white-space:nowrap; }

/* ============ Photos & home video (R6 chunk 15) ============ */
#v-photos{ padding-bottom:calc(var(--ph-bar-h) + 40px); }
.ph-loading{ color:var(--dim); font-size:var(--fs-small); padding:24px 16px; }
.ph-banner{ display:flex; align-items:center; gap:12px; margin:0 16px 14px; padding:12px 15px;
  background:linear-gradient(135deg,rgba(95,208,138,.12),rgba(229,160,13,.08)); border:1px solid var(--line2); border-radius:12px; }
.ph-shield{ width:34px; height:34px; border-radius:50%; background:var(--good); color:#0c0b0a; font-weight:800;
  font-size:var(--fs-title); display:flex; align-items:center; justify-content:center; flex:none; }
.ph-bt{ font-size:var(--fs-small); font-weight:800; }
.ph-bs{ font-size:var(--fs-meta); color:var(--dim); margin-top:2px; }

.ph-mstrip{ display:flex; gap:10px; overflow-x:auto; padding:0 16px 14px; scrollbar-width:none; }
.ph-mstrip::-webkit-scrollbar{ display:none; }
.ph-mem{ position:relative; flex:0 0 120px; height:120px; border-radius:12px; overflow:hidden; cursor:pointer; }
.ph-mem img{ width:100%; height:100%; object-fit:cover; }
.ph-mem-y{ position:absolute; left:8px; bottom:6px; color:#fff; font-weight:800; font-size:var(--fs-read); text-shadow:0 1px 4px rgba(0,0,0,.8); }

.ph-mhead{ font-size:var(--fs-small); font-weight:800; color:var(--dim); padding:12px 16px 6px; position:sticky; top:0;
  background:linear-gradient(var(--bg),var(--bg) 70%,transparent); z-index:2; }
.ph-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(104px,1fr)); gap:3px; padding:0 8px; }
.ph-grid.small{ grid-template-columns:repeat(auto-fill,minmax(72px,1fr)); }
.ph-tile{ position:relative; aspect-ratio:1; overflow:hidden; cursor:pointer; background:var(--card); border-radius:3px; }
.ph-tile img{ width:100%; height:100%; object-fit:cover; display:block; }
.ph-tile:hover img{ opacity:.88; }
.ph-vid{ position:absolute; right:6px; bottom:5px; color:#fff; font-size:var(--fs-meta); text-shadow:0 1px 3px #000; }

.ph-places-h,.ph-place-h{ font-size:var(--fs-small); font-weight:700; color:var(--dim); padding:10px 16px 6px; }
.ph-place{ margin-bottom:10px; }
.ph-place-h{ color:var(--txt); }

.ph-reclaim{ margin:4px 16px 14px; padding:16px; border:1px solid var(--line2); border-radius:12px; text-align:center;
  background:var(--surface); }
.ph-reclaim-b{ font-size:var(--fs-big); font-weight:800; color:var(--gold); }
.ph-reclaim-l{ font-size:var(--fs-meta); color:var(--dim); margin-top:2px; }
.ph-dup{ display:flex; align-items:center; gap:12px; padding:10px 16px; border-top:1px solid var(--line); flex-wrap:wrap; }
.ph-dup-keep{ position:relative; width:64px; height:64px; border-radius:8px; overflow:hidden; flex:none; border:2px solid var(--good); }
.ph-dup-keep img{ width:100%; height:100%; object-fit:cover; }
.ph-dup-keep span{ position:absolute; left:0; right:0; bottom:0; background:var(--good); color:#0c0b0a; font-size:var(--fs-micro); font-weight:800; text-align:center; }
.ph-dup .ph-grid{ flex:1; padding:0; }
.ph-tile.dupx{ border:1px solid rgba(232,80,80,.5); }
.ph-x{ position:absolute; top:2px; right:3px; color:#ff6b6b; font-weight:800; font-size:var(--fs-meta); text-shadow:0 1px 2px #000; }
.ph-dup-r{ font-size:var(--fs-meta); color:var(--dim); white-space:nowrap; }
.ph-burst{ padding:8px 16px; border-top:1px solid var(--line); }
.ph-burst-l{ font-size:var(--fs-meta); color:var(--dim); margin-top:5px; }

.ph-lb{ position:fixed; inset:0; z-index:120; background:rgba(0,0,0,.94); display:none; flex-direction:column; }
.ph-lb.on{ display:flex; }
.ph-lb-bar{ display:flex; justify-content:space-between; align-items:center; padding:14px 16px; color:#eee; font-size:var(--fs-small); }
.ph-lb-x{ background:transparent; border:none; color:#fff; font-size:var(--fs-big); cursor:pointer; line-height:1; }
.ph-lb-stage{ flex:1; display:flex; align-items:center; justify-content:center; overflow:hidden; }
.ph-lb-stage img,.ph-lb-stage video{ max-width:100%; max-height:100%; object-fit:contain; }
.ph-lb-nav{ position:absolute; top:50%; transform:translateY(-50%); background:rgba(0,0,0,.4); color:#fff;
  border:none; width:44px; height:64px; font-size:var(--fs-hero); cursor:pointer; }
.ph-lb-nav.prev{ left:0; } .ph-lb-nav.next{ right:0; }

/* ============ The household layer (R6 chunk 16) ============ */
.hh-switch{ flex:0 0 auto; display:flex; align-items:center; gap:4px; background:var(--surface); border:1px solid var(--line2);
  color:var(--txt); border-radius:999px; padding:6px 12px; font-size:var(--fs-meta); font-weight:700; cursor:pointer; position:relative; }
.hh-switch:hover{ border-color:var(--gold); }
.hh-chev{ font-size:var(--fs-micro); color:var(--dim); }
.hh-live{ width:7px; height:7px; border-radius:50%; background:var(--good); box-shadow:0 0 0 2px var(--bg); }
.hh-hint{ font-size:var(--fs-meta); color:var(--dim); margin-bottom:12px; }
.hh-list{ display:flex; flex-direction:column; gap:6px; }
.hh-prow{ display:flex; align-items:center; gap:12px; padding:10px; border-radius:12px; border:1px solid var(--line);
  background:var(--surface); color:var(--txt); cursor:pointer; text-align:left; width:100%; }
.hh-prow:hover:not(:disabled){ border-color:var(--gold); }
.hh-prow.me{ opacity:.9; cursor:default; }
.hh-av{ position:relative; width:42px; height:42px; border-radius:50%; overflow:hidden; background:var(--card); flex:none;
  display:flex; align-items:center; justify-content:center; font-size:var(--fs-head); }
.hh-av img{ width:100%; height:100%; object-fit:cover; }
.hh-dot{ position:absolute; right:0; bottom:0; width:12px; height:12px; border-radius:50%; background:var(--faint);
  border:2px solid var(--surface); }
.hh-dot.on{ background:var(--good); }
.hh-pn{ flex:1; min-width:0; display:flex; flex-direction:column; }
.hh-pn b{ font-size:var(--fs-small); }
.hh-ps{ font-size:var(--fs-meta); color:var(--dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.hh-cur{ font-size:var(--fs-micro); color:var(--gold); font-weight:700; flex:none; }
.hh-go{ font-size:var(--fs-meta); color:var(--dim); flex:none; }

.hh-otile .pw{ position:relative; }
.hh-rm{ position:absolute; top:5px; right:5px; z-index:3; width:22px; height:22px; border-radius:50%;
  background:rgba(0,0,0,.6); color:#fff; border:none; font-size:var(--fs-micro); cursor:pointer; opacity:0; transition:opacity .12s; }
.hh-otile .pw:hover .hh-rm{ opacity:1; }

.hh-act{ margin:6px 16px 4px; border:1px solid var(--line); border-radius:12px; overflow:hidden; background:var(--surface); }
.hh-act summary{ list-style:none; cursor:pointer; display:flex; align-items:center; gap:8px; padding:12px 14px; font-weight:700; font-size:var(--fs-small); }
.hh-act summary::-webkit-details-marker{ display:none; }
.hh-act-t{ flex:1; }
.hh-act-n{ font-size:var(--fs-micro); font-weight:800; color:#0c0b0a; background:var(--gold); border-radius:999px; padding:2px 8px; }
.hh-feed{ padding:2px 0 6px; }
/* A <button> now — each activity row opens the title it names. Reset back to
   the row it always looked like: a button inherits neither font nor alignment. */
.hh-arow{ display:flex; gap:11px; align-items:flex-start; padding:9px 14px; border-top:1px solid var(--line);
  width:100%; text-align:left; background:none; color:inherit; font:inherit; cursor:pointer;
  -webkit-appearance:none; appearance:none; border-left:0; border-right:0; border-bottom:0; }
.hh-arow:hover{ background:var(--raise); }
.hh-arow:active{ background:var(--raise); }
.hh-aic{ width:22px; height:22px; border-radius:50%; background:var(--card); color:var(--dim); font-size:var(--fs-micro);
  display:flex; align-items:center; justify-content:center; flex:none; }
.hh-aic.ok{ background:var(--good); color:#0c0b0a; }
.hh-al{ flex:1; min-width:0; font-size:var(--fs-small); line-height:1.4; }
.hh-al b{ font-weight:700; }
.hh-at{ font-size:var(--fs-micro); color:var(--dim); margin-top:2px; }

/* ============ Music queue: Up next / History toggle (R6 chunk 17) ============ */
.qtoggle{ display:flex; gap:4px; }
.qtab{ background:transparent; border:1px solid var(--line2); color:var(--dim); border-radius:999px;
  padding:4px 12px; font-size:var(--fs-meta); font-weight:700; cursor:pointer; }
.qtab.on{ background:var(--gold); color:#111; border-color:var(--gold); }

/* ============ Search & jump-to omnibox (R6 chunk 18) ============ */
.omni{ margin:4px 0 14px; }
.omni-box{ display:flex; align-items:center; gap:10px; background:var(--surface); border:1px solid var(--line2);
  border-radius:12px; padding:11px 14px; }
.omni-box:focus-within{ border-color:var(--gold); }
.omni-ic{ width:18px; height:18px; flex:none; stroke:var(--dim); fill:none; stroke-width:2; }
#h-omniq{ flex:1; min-width:0; background:transparent; border:none; outline:none; color:var(--txt); font-size:var(--fs-read); }
#h-omniq::placeholder{ color:var(--faint); }
#h-omniq::-webkit-search-cancel-button{ filter:invert(.5); }
.omni-recent{ display:flex; flex-wrap:wrap; gap:7px; margin-top:9px; }
.omni-chip{ background:var(--surface); border:1px solid var(--line2); color:var(--dim); border-radius:999px;
  padding:5px 12px; font-size:var(--fs-meta); cursor:pointer; }
.omni-chip:hover{ border-color:var(--gold); color:var(--txt); }
.omni-chip.clr{ color:var(--faint); }
.omni-res{ margin-top:10px; display:flex; flex-direction:column; gap:14px; }
.omni-res:empty{ margin:0; }
.omni-none{ color:var(--dim); font-size:var(--fs-small); padding:10px 4px; }
.omni-grp{ }
.omni-gh{ font-size:var(--fs-meta); font-weight:800; text-transform:uppercase; letter-spacing:.04em; color:var(--dim); padding:0 4px 6px; }
.omni-gh span{ font-weight:600; text-transform:none; letter-spacing:0; color:var(--faint); }
.omni-item{ border-radius:10px; cursor:pointer; }
.omni-item.sel, .omni-item:hover{ background:var(--surface); }
.omni-item.sel{ outline:2px solid var(--gold); outline-offset:-2px; }
.omni-row{ display:flex; align-items:center; gap:12px; padding:8px 10px; }
.omni-th{ width:38px; height:54px; border-radius:6px; overflow:hidden; background:var(--card); flex:none;
  display:flex; align-items:center; justify-content:center; font-size:var(--fs-head); }
.omni-th.sq{ width:44px; height:44px; }
.omni-th.wide{ width:80px; height:46px; }
.omni-th img{ width:100%; height:100%; object-fit:cover; }
.omni-mi{ flex:1; min-width:0; }
.omni-t{ font-size:var(--fs-small); font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.omni-s{ font-size:var(--fs-meta); color:var(--dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.omni-row svg{ flex:none; color:var(--dim); }

/* ============ Smart playlists (R6 chunk 19) ============ */
.fchip.smart-new{ border-style:dashed; }
.fchip.smart-chip{ background:linear-gradient(135deg,rgba(229,160,13,.18),var(--surface)); border-color:var(--gold); }
.smart-x{ margin-left:6px; color:var(--dim); font-weight:400; font-size:var(--fs-micro); }
.smart-x:hover{ color:#ff6b6b; }
.sp-rule{ display:flex; gap:6px; align-items:center; margin-bottom:8px; }
.sp-rule .sp-f{ flex:0 0 auto; } .sp-rule .sp-o{ flex:0 0 auto; }
.sp-rule .sp-v{ flex:1; min-width:0; padding:8px 10px; }
.sp-del{ flex:none; background:transparent; border:none; color:var(--dim); font-size:var(--fs-small); cursor:pointer; }
.sp-del:hover{ color:#ff6b6b; }
.sp-count{ margin:12px 0 4px; font-size:var(--fs-small); color:var(--dim); }
.sp-count b{ color:var(--gold); font-size:var(--fs-head); }
.sp-build .libsort{ padding:8px 10px; font-size:var(--fs-small); }

/* ============ Rows that know you (R6 chunk 20) ============ */
.phr-head{ display:flex; align-items:center; gap:8px; }
.phr-why,.phr-hide{ background:transparent; border:1px solid var(--line2); color:var(--dim);
  border-radius:999px; padding:3px 9px; font-size:var(--fs-micro); font-weight:700; cursor:pointer; }
.phr-why:hover{ border-color:var(--gold); color:var(--gold); }
.phr-hide:hover{ border-color:#ff6b6b; color:#ff6b6b; }
.hidrows{ display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }

/* ===== R6 chunk 21 — the guide (EPG grid, on-now, programme detail) =====
   The lane is one long horizontally-scrolled strip; the channel cell is
   position:sticky so it stays readable while you scroll the schedule. --ccw is
   the channel-column width and is used by the "now" line's left offset, so the
   two can never drift apart. */
.epg-days{ display:flex; gap:6px; overflow-x:auto; padding:0 16px 8px; scrollbar-width:none; }
.epg-days::-webkit-scrollbar{ display:none; }
.epg-day{ flex:0 0 auto; padding:6px 13px; border-radius:999px; border:1px solid var(--line2);
  background:var(--surface); color:var(--dim); font-size:var(--fs-meta); font-weight:700; cursor:pointer; }
.epg-day.on{ background:var(--gold); color:var(--accent-ink); border-color:var(--gold); }
.epg-filters{ display:flex; flex-wrap:wrap; gap:8px; align-items:center; padding:0 16px 10px; }
.epg-sel{ display:flex; align-items:center; gap:6px; font-size:var(--fs-micro); color:var(--faint);
  font-weight:700; text-transform:uppercase; letter-spacing:.04em; }
.epg-sel select{ background:var(--surface); color:var(--txt); border:1px solid var(--line2);
  border-radius:8px; padding:5px 8px; font-size:var(--fs-meta); font-weight:600; max-width:170px; }
.epg-legend{ display:flex; gap:14px; padding:0 16px 8px; font-size:var(--fs-micro); color:var(--faint); }
.epg-legend .lg::before{ content:''; display:inline-block; width:9px; height:9px; border-radius:3px;
  margin-right:5px; vertical-align:-1px; background:var(--raise); }
.epg-legend .lg.live::before{ background:var(--gold); }
.epg-legend .lg.mine::before{ background:#4fb0e8; }
.epg-legend .lg.fav::before{ background:transparent; }
.epg-wrap{ overflow-x:auto; overflow-y:hidden; padding-bottom:6px; -webkit-overflow-scrolling:touch; }
.epg-inner{ --ccw:154px; position:relative; min-width:min-content; }
.epg-row{ display:flex; align-items:stretch; border-bottom:1px solid var(--line); }
.epg-cc{ position:sticky; left:0; z-index:3; flex:0 0 var(--ccw); width:var(--ccw);
  display:flex; align-items:center; gap:7px; padding:6px 8px; background:var(--bg);
  border-right:1px solid var(--line2); }
.epg-corner{ font-size:var(--fs-micro); color:var(--faint); font-weight:700; }
.epg-logo{ flex:0 0 26px; height:26px; border-radius:6px; display:grid; place-items:center;
  font-size:var(--fs-micro); font-weight:800; color:#fff; letter-spacing:.02em;
  background:linear-gradient(135deg,hsl(var(--ch) 55% 42%),hsl(var(--ch) 55% 28%)); }
.epg-cn{ flex:1 1 auto; font-size:var(--fs-meta); font-weight:700; color:var(--txt); line-height:1.2;
  overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; }
.epg-fav{ flex:0 0 auto; background:transparent; border:0; color:var(--faint); font-size:var(--fs-small); cursor:pointer; padding:2px; }
.epg-fav.on{ color:var(--gold); }
.epg-lane{ position:relative; height:54px; flex:0 0 auto; }
.epg-ruler .epg-lane{ height:26px; }
.epg-h{ position:absolute; top:0; bottom:0; border-left:1px solid var(--line);
  font-size:var(--fs-micro); color:var(--faint); font-weight:700; padding:6px 0 0 6px; }
.epg-b{ position:absolute; top:4px; bottom:4px; border-radius:7px; border:1px solid var(--line2);
  background:var(--card); color:var(--txt); text-align:left; padding:5px 7px; cursor:pointer;
  overflow:hidden; display:flex; flex-direction:column; justify-content:center; gap:2px; }
.epg-b:hover{ border-color:var(--gold); }
.epg-b.live{ background:color-mix(in srgb,var(--accent) 22%,var(--card)); border-color:var(--gold); }
.epg-b.mine{ border-color:#4fb0e8; box-shadow:inset 3px 0 0 #4fb0e8; }
.epg-b.cl{ border-top-left-radius:0; border-bottom-left-radius:0; border-left-style:dashed; }
.epg-b.cr{ border-top-right-radius:0; border-bottom-right-radius:0; border-right-style:dashed; }
.eb-t{ font-size:var(--fs-meta); font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.eb-s{ font-size:var(--fs-micro); color:var(--dim); }
.epg-gap{ position:absolute; left:8px; top:50%; transform:translateY(-50%); font-size:var(--fs-micro); color:var(--faint); }
.epg-now{ position:absolute; top:0; bottom:0; width:2px; background:#ff4d4f; z-index:4; pointer-events:none; }
.epg-now::before{ content:''; position:absolute; top:0; left:-3px; width:8px; height:8px;
  border-radius:50%; background:#ff4d4f; }
.livecard.mine{ box-shadow:inset 3px 0 0 #4fb0e8; }
.mytag{ color:#4fb0e8; font-weight:700; font-size:var(--fs-micro); }
.epg-d{ padding:4px 2px 10px; }
.epg-dt{ font-size:var(--fs-head); font-weight:800; color:var(--txt); line-height:1.25; }
.epg-dm{ margin-top:4px; font-size:var(--fs-meta); color:var(--dim); }
.epg-dw{ margin-top:8px; font-size:var(--fs-small); color:var(--txt); font-weight:600; }
.epg-tz{ margin-left:8px; font-size:var(--fs-micro); color:var(--faint); font-weight:600; }
.epg-dteams{ display:flex; align-items:center; gap:12px; margin:14px 0 10px; }
.epg-team{ flex:1 1 0; display:flex; align-items:center; gap:8px; font-size:var(--fs-small); font-weight:700; color:var(--txt); }
.epg-vs{ flex:0 0 auto; font-size:var(--fs-micro); color:var(--faint); font-weight:800; }
.epg-dchan{ font-size:var(--fs-meta); color:var(--dim); margin-bottom:10px; }
@media(max-width:620px){ .epg-inner{ --ccw:118px; } .epg-sel select{ max-width:120px; } }


/* R6 chunk 24 — the live player's source controls. Live only, like .vrec. */
.vp .vsrc,.vp .vbroken{ display:none; font-size:var(--fs-title); }
.vp.islive .vsrc,.vp.islive .vbroken{ display:flex; }
.vp .vbroken{ color:#ffb020; }
.lvs-row{ display:flex; align-items:center; gap:10px; padding:11px 4px;
  border-bottom:1px solid var(--line); cursor:pointer; }
.lvs-row.on{ background:var(--accent-soft); border-radius:8px; }
.lvs-dot{ flex:0 0 9px; width:9px; height:9px; border-radius:50%; background:var(--faint); }
.lvs-dot.ok{ background:#3fbf6f; }
.lvs-dot.bad{ background:#ff5c5c; }
.lvs-name{ flex:1 1 auto; font-size:var(--fs-small); font-weight:700; color:var(--txt);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.lvs-badge{ flex:0 0 auto; font-size:var(--fs-micro); font-weight:700; color:var(--dim);
  border:1px solid var(--line2); border-radius:999px; padding:2px 8px; }
.lvs-badge.bad{ color:var(--live-red); border-color:#ff8b8355; }
.lvs-note{ font-size:var(--fs-meta); color:var(--faint); padding:8px 2px 0; line-height:1.45; }

/* ===== R6 chunk 26 — the completeness bar on a show page =====
   The number was already there as a sentence. A bar is what makes "34 of 46"
   read as a state rather than a fact you have to think about. */
.gapwrap{ margin:2px 16px 12px; }
.gaprow{ display:flex; align-items:baseline; gap:8px; margin-bottom:5px; }
.gaplbl{ font-size:var(--fs-meta); font-weight:700; color:var(--txt); }
.gappct{ margin-left:auto; font-size:var(--fs-meta); font-weight:800; color:var(--dim); }
.gapbar{ height:6px; border-radius:3px; background:var(--raise); overflow:hidden; }
.gapbar i{ display:block; height:100%; border-radius:3px; background:var(--gold);
  transition:width .3s ease; }
.gapbar i.full{ background:#3fbf6f; }
.gapact{ display:flex; align-items:center; gap:10px; margin-top:8px; }
.gapmiss{ font-size:var(--fs-meta); font-weight:700; color:var(--live-red); }
.gapok{ font-size:var(--fs-meta); font-weight:700; color:#3fbf6f; }
.gapnote{ margin-top:6px; font-size:var(--fs-meta); color:var(--faint); }

/* ===== R6 chunk 27 — the detail page's hero, facts and recommendations ===== */
.dhero{ position:relative; }
/* The body slides UP over the hero (.hbody has margin-top:min(-15vh,-120px)),
   so a box pinned to the hero's bottom edge is a box nobody can read — the
   first version put the logo, the hook and Play underneath the poster. This
   sits above that overlap, and above the hero's own scrim. */
.dherobox{ position:absolute; left:0; right:0; bottom:max(15vh,128px); z-index:3;
  padding:0 clamp(16px,4vw,44px);
  display:flex; flex-direction:column; align-items:flex-start; gap:10px;
  pointer-events:none; }
.dherobox > *{ pointer-events:auto; }
.dlogo{ max-width:min(62%,320px); max-height:88px; object-fit:contain;
  filter:drop-shadow(0 2px 10px rgba(0,0,0,.55)); }
.dlogotext{ font-size:var(--fs-big); font-weight:900; line-height:1.1; color:#fff;
  text-shadow:0 2px 12px rgba(0,0,0,.6); max-width:80%; }
.dhook{ font-size:var(--fs-small); font-weight:600; color:rgba(255,255,255,.9); max-width:80%;
  text-shadow:0 1px 8px rgba(0,0,0,.6); }
.dheroact{ display:flex; align-items:center; gap:10px; margin-top:2px; }
/* Play is the biggest thing on the screen, which is the whole point of a hero.
   Measured, not asserted: the drive compares its rendered AREA against every
   other button on the page, and the first version lost to "Pin to Home". */
.dplay{ font-size:var(--fs-title); font-weight:800; padding:15px 38px; border-radius:11px;
  min-width:172px; justify-content:center; }
.dhero.playing .dherobox{ display:none; }
.dtrailer{ position:absolute; inset:0; background:#000; }
.dtrailer video{ width:100%; height:100%; object-fit:contain; background:#000; }
.dtrailerx{ position:absolute; top:8px; right:8px; width:34px; height:34px; border-radius:50%;
  border:0; background:rgba(0,0,0,.6); color:#fff; display:grid; place-items:center; cursor:pointer; }
.dfacts{ display:flex; flex-wrap:wrap; gap:6px; margin:2px 16px 6px; }
.dfact{ font-size:var(--fs-meta); font-weight:700; color:var(--dim); border:1px solid var(--line2);
  border-radius:999px; padding:3px 10px; }
.dorigin{ margin:0 16px 10px; font-size:var(--fs-meta); color:var(--faint); }
.powned{ position:absolute; left:6px; top:6px; font-size:var(--fs-micro); font-weight:800;
  padding:2px 7px; border-radius:999px; background:rgba(63,191,111,.9); color:#06210f; }
.ptile .pw{ position:relative; }

/* ===== R6 chunk 28 (M10.1) — the marquee =====
   Applied ONLY when the title genuinely overflows (phMarquee measures it), so a
   short song sits still. Both the distance and the duration come from the
   measured overflow, which keeps the speed constant instead of the duration. */
#mb-t1.mq, #mn-t1.mq{ overflow:hidden; }
/* inline-block ALWAYS, not only while scrolling: phMarquee measures this span
   to decide whether to scroll at all, and an inline span inside a nowrap,
   overflow-hidden parent reports a width of zero. */
#mb-t1 .mqi, #mn-t1 .mqi{ display:inline-block; white-space:nowrap; }
#mb-t1.mq .mqi, #mn-t1.mq .mqi{ animation:phmq var(--mqd,12s) linear infinite; }
@keyframes phmq{
  0%,10%   { transform:translateX(0); }
  45%,55%  { transform:translateX(var(--mqx,-40px)); }
  90%,100% { transform:translateX(0); }
}
@media (prefers-reduced-motion: reduce){
  #mb-t1.mq .mqi, #mn-t1.mq .mqi{ animation:none; }
}
/* ===== R6 chunk 28 (M10.8) — the problem list in Settings ===== */
.probhead{ display:flex; align-items:center; gap:10px; font-size:var(--fs-meta); font-weight:800;
  color:var(--dim); margin:6px 0 8px; }
.probhead .btn{ margin-left:auto; }
.probrow{ padding:9px 12px; border-radius:10px; background:var(--surface);
  border:1px solid var(--line); margin-bottom:6px; }
.probrow .pw1{ font-size:var(--fs-small); font-weight:700; color:var(--txt); }
.probrow .pw2{ font-size:var(--fs-meta); color:var(--dim); margin-top:2px; word-break:break-word; }
.probrow .pw3{ font-size:var(--fs-micro); color:var(--faint); margin-top:3px; }
/* ===== R6 chunk 28 (M10.2) — the known-wrong songs list ===== */
.wsrow{ display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:11px;
  background:var(--surface); border:1px solid var(--line); margin-bottom:6px; }
.wsrow .w1{ flex:1 1 auto; min-width:0; }
.wsrow .wt{ font-size:var(--fs-small); font-weight:700; color:var(--txt); overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap; }
.wsrow .wm{ font-size:var(--fs-meta); color:var(--dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.wsverdict{ flex:0 0 auto; font-size:var(--fs-micro); font-weight:800; padding:2px 8px; border-radius:999px;
  background:var(--raise); color:var(--dim); }
.wsverdict.bad{ background:rgba(255,92,92,.16); color:var(--live-red); }

/* ===== R6 chunk 29 — Settings: the shell around the existing controls ===== */
.setnav{ display:flex; align-items:center; gap:8px; padding:6px 16px 10px; flex-wrap:wrap; }
.setback{ background:transparent; border:0; color:var(--gold); font-size:var(--fs-small); font-weight:800;
  cursor:pointer; padding:6px 2px;
  /* Shell revamp chunk 5: inside a category the back title takes its OWN line
     (as a nav bar does), so search + Only changed always share the next one
     and nothing truncates — "Downloads and storage" and the search box were
     eating each other on one line. On the landing the button is hidden and
     the row is search + pill, one line. */
  flex:1 0 100%; text-align:left; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.setsearch{ flex:1 1 0; min-width:110px; display:flex; }
.setsearch input{ flex:1 1 auto; min-width:0; width:100%; text-overflow:ellipsis; background:var(--surface); border:1px solid var(--line2);
  border-radius:999px; color:var(--txt); font-size:var(--fs-small); padding:9px 14px; outline:none; }
.setsearch input:focus{ border-color:var(--gold); }
.setchg{ flex:0 0 auto; white-space:nowrap; background:transparent; border:1px solid var(--line2); color:var(--dim);
  border-radius:999px; padding:8px 13px; font-size:var(--fs-meta); font-weight:700; cursor:pointer; }
.setchg.on{ background:var(--gold); color:var(--accent-ink); border-color:var(--gold); }
.setcard{ display:flex; align-items:center; gap:10px; width:calc(100% - 32px); margin:0 16px 8px;
  padding:13px 15px; border-radius:13px; background:var(--surface); border:1px solid var(--line);
  color:var(--txt); text-align:left; cursor:pointer; }
.setcard:hover{ border-color:var(--gold); }
.setcard .sc1{ font-size:var(--fs-small); font-weight:750; flex:0 0 auto; }
.setcard .sc2{ font-size:var(--fs-meta); color:var(--dim); flex:1 1 auto; overflow:hidden;
  text-overflow:ellipsis; white-space:nowrap; }
.setcard .scchg{ flex:0 0 auto; font-size:var(--fs-micro); font-weight:800; padding:2px 8px;
  border-radius:999px; background:var(--accent-soft); color:var(--accent-text); }
.setcard .scwhere{ flex:0 0 auto; font-size:var(--fs-micro); font-weight:700; color:var(--faint);
  border:1px solid var(--line2); border-radius:999px; padding:2px 8px; }
.setcard .sc3{ flex:0 0 auto; color:var(--faint); font-size:var(--fs-title); }
.setnote{ padding:2px 18px 10px; font-size:var(--fs-meta); color:var(--dim); }
/* ===== R6 chunk 36 - watching behind the live edge ===== */
/* The LIVE badge must stop claiming to be live the moment you are not. */
.vp.dvron .vlive{ background:rgba(255,255,255,.14); color:#cfd6e2; }
.vp.dvron .vlive::before{ content:'aa '; }
.vp.islive .vcenter .vbtn:first-child{ transform:scale(1.15); }

/* R6 G14.2 — form controls inherit the app's type instead of the browser's.
   13.3333px kept turning up in the measurements: that is Chrome's default for
   an unstyled button or input, which means it was never a decision at all. */
button, input, select, textarea, optgroup{ font-family:inherit; font-size:inherit; line-height:inherit; }

/* ===== R6 chunk G14.1 — tap targets, measured at 375px =====

   Walked every family screen at phone width and measured every control. The
   worst offenders, by count:

     .tile-x    22 x 22px, sixteen of them — and it REMOVES something, sitting
                on the corner of a poster you are trying to tap to play;
     .btn.sm    28px tall, forty-two on the music page alone;
     .yt-kebab  34px, twenty-four of them;
     .seeall    25px tall, twelve;
     .sw-tab / .fchip / .yt-chip   31-36px.

   The wrong fix is to make them all 44px tall: that is a different, worse
   design, and it would push the music list off the screen. The right one is to
   grow the HIT AREA and leave the ink alone — a centred pseudo-element that
   nobody can see and every thumb can find.

   Phone widths only. A mouse is precise and a desktop row is dense on purpose. */
@media (max-width: 900px){
  /* Re-measured after the first pass: 137 of 155 controls were covered, and
     these are the eighteen it missed — found by asking the page again rather
     than by assuming the list was complete. */
  .tile-x, .btn.sm, .yt-kebab, .seeall, .sw-tab, .fchip, .yt-chip, .qchip,
  .setchg, .yt-iconbtn, .yt-bluebtn, .yt-ghostbtn,
  .hh-switch, .arrbtn, .phr-why, .phr-hide, .v22-link-btn, .btn.sec, .btn{ position:relative; }
  .tile-x::after, .btn.sm::after, .yt-kebab::after, .seeall::after, .sw-tab::after,
  .fchip::after, .yt-chip::after, .qchip::after, .setchg::after, .yt-iconbtn::after,
  .yt-bluebtn::after, .yt-ghostbtn::after,
  .hh-switch::after, .arrbtn::after, .phr-why::after, .phr-hide::after,
  .v22-link-btn::after, .btn.sec::after, .btn::after{
    content:''; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
    width:max(100%, var(--tap)); height:max(100%, var(--tap));
    /* Invisible, and it must never sit above the thing it belongs to. */
    z-index:0;
  }
  /* The X on a continue-watching tile is destructive AND lives on a poster, so
     it gets the hit area and a bigger dot to aim at — 22px of visible target
     for "forget this" is a mis-tap waiting to happen either way. */
  .tile-x{ width:28px; height:28px; }
}

/* ===== R6 chunk G14.3 — loading, empty and failed, on the family app =====
   The admin got these in G11.5. This surface matters more, and it had eighty
   `.empty` divs saying things like "None yet." and "Search failed." — a
   failure written in exactly the same voice as an empty shelf, so a broken app
   looked like an app with nothing in it. */
.phst{ padding:30px 20px; text-align:center; color:var(--dim); }
.phst-t{ font-weight:800; color:var(--txt); font-size:var(--fs-title); }
.phst-h{ font-size:var(--fs-small); color:var(--faint); margin-top:8px; line-height:1.6;
  max-width:34ch; margin-left:auto; margin-right:auto; }
.phst .btn{ margin-top:16px; }
.phst.bad .phst-t{ color:var(--bad,#e5484d); }
.phsk{ display:grid; gap:10px; padding:6px 0; }
.phsk i{ display:block; height:var(--phskh,58px); border-radius:12px;
  background:linear-gradient(90deg, var(--card) 25%, var(--raise) 37%, var(--card) 63%);
  background-size:400% 100%; animation:phskshine 1.3s ease infinite; }
@keyframes phskshine{ from{ background-position:100% 0; } to{ background-position:0 0; } }

/* ===== R6 chunk H2.2 — a Continue card showing the frame you stopped on =====
   A poster is 2:3 and a still is 16:9. Rather than letterbox one into the
   other's shape, the still fills the tile and is cropped — which is what a
   Netflix-style card does anyway, and keeps every tile in a row the same size.
   `object-position:50% 40%` favours the upper middle, where faces usually are. */
.ptile .pw img.is-still{ object-fit:cover; object-position:50% 40%; }
/* A gentle fade so the swap from poster to frame is not a flicker. */
.ptile .pw img{ transition:opacity .18s ease; }

/* ===== R6 chunk H2.5 — swiping a Continue card =====
   The tile follows the finger; the tint says which of the two answers it is.
   No transition while dragging (it must track the finger exactly), and the
   snap-back is handled by clearing the inline transform. */
/* `pan-y` is load-bearing, and it only started working when `.hrow` stopped
   saying `pan-x` (see the note at the top of this file). contSwipeBind() drives
   this gesture with Pointer Events and never calls preventDefault(), so the ONLY
   thing keeping the browser from claiming the drag — and cancelling the gesture
   with pointercancel — is this declaration. It has to say exactly `pan-y`:
     - the browser keeps the vertical axis, so a downward swipe still scrolls the
       page (against `.hrow{pan-x}` this used to intersect to NOTHING, which is
       why a Continue card could be panned in neither direction);
     - the browser gives up the horizontal axis, so the sideways drag reaches the
       swipe handler instead of panning the rail.
   Dropping it entirely would restore the page scroll but silently kill
   swipe-to-dismiss; widening it to include pan-x would do the same. */
#h-cont .ptile{ touch-action:pan-y; will-change:transform; }
#h-cont .ptile.sw-hide{ box-shadow:inset 0 0 0 2px var(--bad,#e5484d); }
#h-cont .ptile.sw-seen{ box-shadow:inset 0 0 0 2px var(--ok); }

/* ===== R6 chunk H1.7 — the runtime IS the point of this card =====
   The row exists because time is the constraint, so the length is the loudest
   thing on the tile rather than something to hunt for underneath it. */
.ptile .pw .shortbadge{ position:absolute; right:6px; bottom:6px; z-index:2;
  padding:3px 7px; border-radius:7px; font-size:var(--fs-micro); font-weight:800;
  background:var(--scrim-3); color:#fff; letter-spacing:.02em; }

/* ===== R6 chunk H1.3 — what is LEFT is what decides the evening =====
   Same corner and weight as the H1.7 runtime badge, because both answer the
   same question: is this a thing I can actually do tonight? "Last one" earns
   the accent — it is the one card here you can finish for good. */
.ptile .pw .leftbadge{ position:absolute; right:6px; bottom:6px; z-index:2;
  padding:3px 7px; border-radius:7px; font-size:var(--fs-micro); font-weight:800;
  background:var(--scrim-3); color:#fff; letter-spacing:.02em; }
.ptile .pw .leftbadge.one{ background:var(--accent); color:var(--accent-ink); }

/* ===== R6 chunk H1.6 — what KIND of new this is =====
   The two kinds answer the same question from different places, so they share
   a shape and differ only in colour. Top-left, opposite H1.7/H1.3's bottom-
   right badges, because a tile can legitimately carry both. */
.ptile .pw .landedbadge{ position:absolute; left:6px; top:6px; z-index:2;
  padding:3px 7px; border-radius:7px; font-size:var(--fs-micro); font-weight:800;
  letter-spacing:.04em; background:var(--accent); color:var(--accent-ink); }
.ptile .pw .landedbadge.seq{ background:var(--info); color:#0b0b0d; }
.ptile .s.dimmer{ color:var(--faint); }

/* ===== R6 chunk OFF1 — offline is a state, so it gets a bar not a toast =====
   It sits above everything and stays until it stops being true. Amber rather
   than red: being offline is not an error, it is a mode the app supports. */
#ph-offbar{ position:sticky; top:0; z-index:60; display:flex; align-items:center; gap:10px;
  padding:9px calc(12px + var(--sa-l)) 9px calc(12px + var(--sa-r));
  padding-top:calc(9px + var(--sa-t));
  background:color-mix(in srgb,var(--accent) 16%,var(--bg)); color:var(--txt);
  border-bottom:1px solid color-mix(in srgb,var(--accent) 40%,transparent);
  font-size:var(--fs-small); }
#ph-offbar .dot{ width:9px; height:9px; border-radius:50%; background:var(--accent); flex:none;
  box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 25%,transparent); }
#ph-offbar .tx{ flex:1; min-width:0; line-height:1.35; }
#ph-offbar .tx .sub{ color:var(--dim); }
/* The tabs that cannot work offline read as unavailable rather than broken.
   Home and Library are no longer among them: both paint from a local cache and
   both open, so dimming them said "broken" about two tabs that work. */
body.ph-offline .nav-btn[data-tab="req"]{ opacity:.45; }

/* ===== Offline parity — the same app, honest about what it can do =====
 *
 * Every one of these is the SAME element as online, marked. Nothing here hides
 * a thing: unavailable content is dimmed, tagged and still tappable (it says
 * why), because a list that quietly drops half your music is what made an
 * offline Music tab look empty next to a full device.                        */

/* The per-page notice. One in the DOM at a time, at the top of whichever view
   is on. `.hard` is a page with genuinely nothing local. */
.phoffnote{ display:flex; align-items:flex-start; gap:10px; margin:0 0 12px;
  padding:11px 13px; border-radius:12px;
  background:color-mix(in srgb,var(--accent) 9%,var(--card));
  border:1px solid color-mix(in srgb,var(--accent) 26%,transparent);
  color:var(--txt); font-size:var(--fs-small); line-height:1.4; }
.phoffnote.hard{ background:var(--card); border-color:var(--line); }
.phoffnote .ic{ flex:none; display:flex; color:var(--accent); margin-top:1px; }
.phoffnote.hard .ic{ color:var(--dim); }
.phoffnote .tx{ flex:1; min-width:0; }
.phoffnote .sub{ display:block; color:var(--dim); margin-top:2px; }
.phoffnote .btn{ flex:none; }

/* "Showing the 7 songs on this device · Show all 1558" — the filter says so. */
.muoffnote{ margin:0 4px 8px; color:var(--dim); font-size:var(--fs-meta); line-height:1.4; }
.muoffnote .lnk{ background:none; border:0; padding:0 0 0 4px; cursor:pointer;
  color:var(--accent); font:inherit; text-decoration:underline; }
#mu-offbar, #lib-offnote{ margin:0 16px 10px; }

/* Artwork lives on the server. Offline, a cover that cannot load reads as "no
   picture" (its alt glyph), never as the browser's broken-image icon. */
.mcov{ text-align:center; }

/* A loading skeleton under "this needs the server" is the app promising
   something is on its way when nothing is. Only on the views that genuinely
   have nothing local — everywhere else a skeleton is still telling the truth. */
.ph-off-dead .skel, .ph-off-dead .skel-list, .ph-off-dead .skel-row,
.ph-off-dead .skel-guide, .ph-off-dead .skel-tile, .ph-off-dead .skel-gcard,
.ph-off-dead .phsk, .ph-off-dead .epsload{ display:none; }

/* A row that cannot play right now. Dimmed, not deleted. */
.murow.unavail{ opacity:.42; }
.murow.unavail .mcovw .mcovp{ display:none; }
.murow.unavail .m1{ text-decoration:none; }
.offtag{ margin-left:6px; padding:1px 6px; border-radius:999px;
  background:var(--raise); color:var(--dim); font-size:var(--fs-micro); white-space:nowrap; }

/* A playlist with nothing downloaded. Present, named, and not startable. */
.mgtile.unavail{ opacity:.45; }
.mgtile.unavail .mgtile-play{ display:none; }

/* Browse modes that are the web rather than your music. */
.fchip.off-dead{ opacity:.4; pointer-events:none; }

/* Library, offline: the downloaded titles come first, as library items. */
#lib-offshelf:empty{ display:none; }
#lib-offshelf .offcard{ cursor:pointer; }
body.ph-offline #library{ opacity:.55; }
body.ph-offline #lib-offshelf{ opacity:1; }

/* ===== R6 chunk M2.3 — who added which song =====
   Small, in the corner of the artwork: it answers "who put this here" without
   competing with the song itself. A shared playlist whose tracks are anonymous
   is just a playlist several people can edit. */
.plwho-wrap{ position:relative; }
.plwho{ position:absolute; left:38px; top:34px; z-index:2;
  width:20px; height:20px; border-radius:50%; overflow:hidden;
  display:inline-flex; align-items:center; justify-content:center;
  background:var(--raise); border:2px solid var(--bg);
  font-size:var(--fs-micro); font-weight:800; color:var(--txt); }
.plwho img{ width:100%; height:100%; object-fit:cover; display:block; }
/* In the header the emblems sit in a row rather than on artwork. */
.plmadeby{ display:flex; align-items:center; gap:6px; margin-top:6px;
  color:var(--dim); font-size:var(--fs-meta); }
.plmadeby .plwho.sm{ position:static; width:22px; height:22px; margin-right:-8px; }
/* The LABEL, named rather than positional: `span:last-child` also matched the
   initial inside an avatar circle and shoved that letter off-centre. */
.plmadeby .plby{ margin-left:14px; }

/* ============ R6 X6.1 — the ten-foot layout ============================
   The same app, seen from a sofa instead of held in a hand. Everything here is
   scoped to html[data-tv="1"], so a phone and a laptop are untouched.

   The four things that actually matter at three metres, in order of how much
   they matter: you must be able to SEE where the focus is, the focus must not
   sit under the edge of the panel, text must be bigger than a thumb needs, and
   anything that only exists for a finger should go. */
html[data-tv="1"]{
  /* Overscan. A real television clips 2-5% of every edge, and a nav bar that a
     phone puts flush against the bottom lands underneath the bezel. */
  --tv-safe: 3vh;
  font-size: 118%;
}
html[data-tv="1"] body{ padding: var(--tv-safe) calc(var(--tv-safe) * 1.4); }

/* The ring is ALWAYS on here, not only for :focus-visible. On a remote there is
   no other modality — a ring that hides itself because the last event was not a
   keypress leaves somebody pressing a direction and seeing nothing move. */
html[data-tv="1"] :focus{
  outline: 3px solid var(--accent) !important;
  outline-offset: 3px;
  border-radius: 8px;
}
/* And a second, softer cue, because an outline alone disappears against a
   bright poster — which is most of this app. */
html[data-tv="1"] :focus{ box-shadow: 0 0 0 6px rgba(0,0,0,.55), 0 8px 30px rgba(0,0,0,.5); }
html[data-tv="1"] .ptile:focus, html[data-tv="1"] .card:focus{ transform: scale(1.06); z-index: 5; }
html[data-tv="1"] .ptile, html[data-tv="1"] .card{ transition: transform .12s ease; }

/* Bigger targets: a remote lands on a whole element, so the padding is about
   legibility rather than accuracy. */
html[data-tv="1"] .bnav .bi{ font-size: var(--fs-small); padding: 14px 10px; }
html[data-tv="1"] .btn{ padding: 12px 20px; font-size: var(--fs-small); }
html[data-tv="1"] .sect h2{ font-size: var(--fs-big); }

/* Things that exist only for a finger. A pull-to-refresh hint, a swipe tip and
   a drag handle are all instructions nobody on a sofa can follow. */
html[data-tv="1"] .ph-ptr, html[data-tv="1"] .ph-tip,
html[data-tv="1"] .drag, html[data-tv="1"] #ph-demo-offer{ display: none !important; }

/* ===== Queue reorder (R6) =====
   The drag used to paint a text selection across every row it passed, which
   made a working drop feel broken. Cleared on the same event that sets it, so
   an interrupted drag cannot leave the sheet unselectable. */
body.mu-dragging, body.mu-dragging *{
  user-select: none !important; -webkit-user-select: none !important;
  -webkit-touch-callout: none;
}
.qgrab{ touch-action: none; cursor: grab; }
body.mu-dragging .qgrab{ cursor: grabbing; }
/* Everything BUT the row in hand animates, so the gap opens smoothly while the
   dragged row tracks the finger exactly. A transition on the dragged row would
   make it lag behind the pointer. */
#mn-queue .murow{ transition: transform .16s cubic-bezier(.2,.7,.3,1); }
#mn-queue .murow.qdrag{
  transition: none; position: relative; z-index: 6;
  background: var(--surface); border-radius: 10px;
  box-shadow: 0 10px 28px rgba(0,0,0,.45);
}

/* ===================== the bottom-bar editor (Settings) =====================
   The bar you are editing, shown as itself. Tiles are the same icon+label pair
   the real bar uses, read from its own elements — see phNavEditor. */
.nvbar{ display:flex; gap:8px; margin-top:12px; padding:10px; border-radius:14px;
  background:var(--surface); border:1px solid var(--line); overflow-x:auto;
  /* The drag needs the horizontal gesture; without this iOS treats the first
     sideways movement as a scroll and the tile never follows the finger. */
  touch-action:pan-y; scrollbar-width:none; }
.nvbar::-webkit-scrollbar{ display:none; }
.nvt{ flex:0 0 auto; display:flex; flex-direction:column; align-items:center; gap:5px;
  min-width:64px; padding:9px 8px; border-radius:11px; cursor:grab;
  background:var(--raise); border:1px solid var(--line); color:var(--txt);
  transition:transform .16s ease, opacity .16s ease, border-color .16s ease; user-select:none; }
.nvt .nvi{ display:flex; opacity:.92; }
.nvt .nvl{ font-size:var(--fs-micro); font-weight:700; white-space:nowrap; }
.nvt.lock{ border-style:dashed; opacity:.82; }
.nvt.dragging{ cursor:grabbing; opacity:.55; transform:scale(1.06);
  border-color:var(--gold); box-shadow:0 6px 20px rgba(0,0,0,.35); }

/* Every page, always — the ones already in the bar dimmed with a tick rather
   than removed, so the full set of what the app can do stays visible. */
.nvshelf{ display:grid; grid-template-columns:repeat(auto-fill,minmax(88px,1fr));
  gap:8px; margin-top:12px; }
.nvs{ position:relative; display:flex; flex-direction:column; align-items:center; gap:6px;
  padding:12px 6px; border-radius:12px; cursor:pointer; font:inherit;
  background:var(--raise); border:1px solid var(--line); color:var(--txt);
  transition:border-color .16s ease, background .16s ease, opacity .16s ease; }
.nvs:hover{ border-color:var(--gold); }
.nvs .nvl{ font-size:var(--fs-micro); font-weight:700; text-align:center; }
.nvs .nvk{ position:absolute; top:5px; right:7px; font-size:var(--fs-micro);
  font-weight:900; color:var(--faint); }
.nvs.on{ opacity:.5; border-color:var(--gold); }
.nvs.on .nvk{ color:var(--gold); }
.nvs.lock{ opacity:.42; cursor:default; border-style:dashed; }
.nvs.lock:hover{ border-color:var(--line); }
.nvs.full{ opacity:.34; }
@media (prefers-reduced-motion:reduce){
  .nvt,.nvs{ transition:none; }
}

/* ================= the settings landing: nine categories ==================
   It was 34 near-identical text cards, two of them called "Network" and two
   "Your files". These are taller, carry an icon, and say what is inside by
   name — "Playback, Sound, Music" locates a setting in a way that "5 of 9 on"
   never did. */
.setgrp{ display:flex; align-items:center; gap:12px; width:calc(100% - 32px);
  margin:0 16px 8px; padding:14px; border-radius:var(--r); cursor:pointer;
  text-align:left; font:inherit; color:var(--txt);
  background:var(--card); border:1px solid var(--line);
  transition:border-color .16s ease, background .16s ease; }
.setgrp:hover{ border-color:var(--gold); }
.setgrp:active{ background:var(--raise); }
.setgrp .sgi{ flex:0 0 auto; display:flex; align-items:center; justify-content:center;
  width:38px; height:38px; border-radius:11px;
  background:var(--raise); border:1px solid var(--line); color:var(--gold); }
.setgrp .sgt{ flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:3px; }
.setgrp .sg1{ font-size:var(--fs-small); font-weight:750; }
/* Two lines, not one truncated. "Appearance, Bottom bar, Getting ar…" hid the
   very words somebody is scanning for; three of the eight cards were cut
   mid-list. Clamped at two so the cards stay the same height as each other. */
.setgrp .sg2{ font-size:var(--fs-micro); color:var(--dim); line-height:1.35;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden; white-space:normal;
  /* Always the height of two lines, so a one-line summary does not make a
     shorter card than its neighbour — the landing is a list, and a list of
     eight cards in two different heights reads as a mistake. */
  min-height:2.7em; }
/* The row a search hit landed on: a gold wash that fades, so the eye finds it
   after the scroll without anything moving. */
#v-set .setrow.ph-hit{ animation:phHit 2s ease-out 1; border-radius:10px; }
@keyframes phHit{ 0%{ background:rgba(229,160,13,.28); } 60%{ background:rgba(229,160,13,.18); } 100%{ background:transparent; } }
@media (prefers-reduced-motion:reduce){ #v-set .setrow.ph-hit{ animation:none; outline:2px solid var(--gold); outline-offset:2px; } }
/* --accent-INK, not --accent-text. In the dark theme --accent-text IS
   var(--accent), so the first version of this badge painted gold on gold and
   the count was invisible — a solid gold pill with nothing in it. --accent-ink
   is the token that exists for text sitting ON the accent colour. Caught by
   looking at the screenshot, which no assertion here was going to do. */
.setgrp .scchg{ flex:0 0 auto; min-width:20px; text-align:center;
  font-size:var(--fs-micro); font-weight:800; padding:2px 7px; border-radius:999px;
  background:var(--accent); color:var(--accent-ink); }
.setgrp .sc3{ flex:0 0 auto; display:flex; color:var(--faint); }
@media (prefers-reduced-motion:reduce){ .setgrp{ transition:none; } }

/* =========== R7.1.3 — settings controls a finger can actually hit ===========
   Measured, not assumed: every control in every settings category, scrolled
   into view and hit-tested at ±21px from its centre. 35 failed, all on the
   vertical axis — 23 switches at 26px tall, 5 colour swatches at 38, the
   theme/accent chips at 34, the sort pill at 36.

   The app already has a hit-area pattern (a ::after sized to --tap). It does
   not rescue these: inside a flex .setrow the overlay is competing with the
   row's own text for the same points, and the probe shows it losing. So these
   get REAL height instead of an invisible overlay — the ink stays the size it
   was, the element around it grows.

   Scoped to #v-set on purpose. These same classes are dense-by-design
   elsewhere (the music page has forty-two .btn.sm), and 44px everywhere would
   be a different, worse layout. */
#v-set .sw{ height:var(--tap); flex:0 0 46px; }
/* The track keeps its 26px; it is centred in the taller label rather than
   stretched, so nothing looks different and the whole label is tappable. */
#v-set .sw .tr2{ top:50%; bottom:auto; transform:translateY(-50%); height:26px; }
#v-set .sw .kn{ top:50%; margin-top:-10px; }

#v-set .swatch{ width:var(--tap); height:var(--tap); }

/* Shell revamp chunk 3 — settings row controls that respect the label.
   A <select> in a row took its full intrinsic width ("Nothing — just resume")
   and squeezed the title into a two-word column with the description wrapping
   nine lines beside it. The label owns the row; the control gets at most 45%
   and shrinks. Same for a wide button. */
#v-set .setrow > select, #v-set .setrow > .libsort, #v-set .setrow > input.input, #v-set .setrow > .input{ flex:0 1 auto; width:auto; max-width:45%; min-width:96px; box-sizing:border-box; }
#v-set .setrow > .btn{ max-width:45%; overflow:hidden; text-overflow:ellipsis; }
/* Keyboard / TV: the switch had no visible focus at all. */
#v-set .sw:has(input:focus-visible) .tr2{ outline:2px solid var(--gold); outline-offset:2px; }
#v-set .sw:has(input:disabled){ opacity:.45; pointer-events:none; }
/* Shell revamp chunk 4 — a card of platform-gated rows.
   #s-music-card holds rows that each show only where their feature works
   (crossfade/levelling need settable volume, EQ needs WebAudio, data saver is
   app-only). The card disappears when none is visible and draws the divider
   only between visible rows — so it never renders as an empty box or a row
   with a rule under it and nothing below. */
#v-set .card:not(:has(> .setrow-wrap:not([style*="none"]))):has(> .setrow-wrap){ display:none; }
#v-set .card > .setrow-wrap:not([style*="none"]) ~ .setrow-wrap:not([style*="none"]) > .setrow{ border-top:1px solid var(--line); }


#v-set .fchip,
#v-set .libsort{ min-height:var(--tap); display:inline-flex; align-items:center; }

/* ============ R7.2.1 — the backup gallery, grouped by day ============
   Kind tiles, not thumbnails, and that is a consequence rather than a
   shortcut: the files are end-to-end encrypted, so the server has never seen
   their contents. A real thumbnail grid would mean downloading and decrypting
   every tile on screen. The NAME survives encryption, so the kind does. */
.bksafe{ margin:0 0 14px; padding:12px 14px; border-radius:var(--r);
  background:var(--surface); border:1px solid var(--line); font-size:var(--fs-small); }
.bksafe b{ display:block; margin-bottom:4px; }
.bksafe .bkmix{ color:var(--dim); font-size:var(--fs-micro); margin-top:3px; }
.bkday{ position:sticky; top:0; z-index:1; padding:10px 2px 6px;
  font-size:var(--fs-micro); font-weight:800; letter-spacing:.08em; text-transform:uppercase;
  color:var(--dim); background:var(--card); }
.bkgrid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(96px,1fr));
  gap:8px; margin-bottom:6px; }
.bktile{ position:relative; display:flex; flex-direction:column; align-items:center;
  justify-content:center; gap:5px; min-height:var(--tap); aspect-ratio:1;
  padding:10px 7px; border-radius:12px; cursor:pointer; font:inherit; color:var(--txt);
  background:var(--raise); border:1px solid var(--line); overflow:hidden;
  transition:border-color .16s ease; }
.bktile:hover{ border-color:var(--gold); }
.bktile .bkic{ display:flex; color:var(--gold); opacity:.9; }
.bktile .bknm{ font-size:var(--fs-micro); font-weight:700; text-align:center;
  /* Two lines then an ellipsis — a backup is full of long camera names and one
     of them must not make its tile a different size from its neighbours. */
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden; overflow-wrap:anywhere; line-height:1.25; }
.bktile .bksz{ font-size:var(--fs-micro); color:var(--faint); }
.bktile .bkwarn{ position:absolute; top:5px; right:6px; width:16px; height:16px;
  border-radius:50%; background:var(--bad); color:#fff; font-size:var(--fs-micro); font-weight:900;
  display:flex; align-items:center; justify-content:center; }
@media (prefers-reduced-motion:reduce){ .bktile{ transition:none; } }

/* ===== R7.1 — the "Wrong song?" picker ==============================
   Rebuilt because the old rows could not be listened to and could not be told
   apart: one "Use this" button that downloads, above a title and a channel
   that for these tracks are eight near-identical lines. Three things carry the
   decision now — the artwork, a play button that streams the candidate through
   the ad-free proxy without downloading anything, and how its length compares
   with the file you already have. */
.rmxrow{ display:flex; align-items:center; gap:11px; padding:9px 10px; border-radius:12px;
  border:1px solid var(--line); background:var(--card); margin-bottom:8px; }
.rmxrow.mine{ background:var(--raise); }
/* The nearest length is the answer far more often than the top-ranked title,
   so it gets a border rather than a note somebody has to read. */
.rmxrow.best{ border-color:var(--accent); }
.rmxrow .rmxart{ flex:0 0 52px; width:52px; height:52px; border-radius:9px; object-fit:cover;
  background:var(--raise); overflow:hidden; }
.rmxrow .rmxbody{ flex:1 1 auto; min-width:0; }
.rmxrow .rmxt{ font-size:var(--fs-small); font-weight:700; color:var(--txt);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.rmxrow .rmxm{ font-size:var(--fs-meta); color:var(--dim); margin-top:2px;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.rmxrow .rmxchips{ display:flex; flex-wrap:wrap; gap:5px; margin-top:5px; }
.rmxrow .rmxact{ flex:0 0 auto; display:flex; align-items:center; gap:7px; }
.rmxd, .rmxw{ font-size:var(--fs-micro); font-weight:700; padding:2px 7px; border-radius:999px;
  border:1px solid var(--line2); color:var(--dim); }
.rmxd.ok{ color:var(--accent-text); border-color:var(--accent); }
/* A big difference is deliberately NOT painted with --bad: that token is
   #ff5d5d and light mode never redefines it, so it lands at 3.01:1 on a card
   (logged as R7.6). It also is not an error — a 52-minute mix is a fact about
   the candidate. "+52:10 vs yours" is loud enough on its own; this only makes
   it read as the number that matters. */
.rmxd.bad{ color:var(--txt); border-color:var(--line); font-weight:800; }
.rmxw{ font-weight:600; }
/* On a phone the two controls will not fit beside the text, so they take their
   own line. Done with wrap first, and the screenshot showed why that is not
   good enough: once the chips are in the body, the whole body wraps BELOW the
   artwork, leaving the cover alone on a line and each card 185px tall — eight
   of those is a page and a half of scrolling. A grid says the thing wrap can
   only be asked for nicely: cover and text always share the top row, and only
   the buttons drop. */
@media (max-width:420px){
  .rmxrow{ display:grid; align-items:center; column-gap:11px; row-gap:8px;
    grid-template-columns:52px minmax(0,1fr);
    grid-template-areas:"art body" "act act"; }
  .rmxrow .rmxart{ grid-area:art; }
  .rmxrow .rmxbody{ grid-area:body; }
  .rmxrow .rmxact{ grid-area:act; justify-content:flex-end; }
}

/* ===== R7.3 — Explore: hear it, or keep it, from the tile ==============
   A tile could only ever put its own name in the search box. "Try out new
   songs" needs the two things R7.1 and R7.2 built: play a song you do not own,
   and file it in a playlist in one tap. Both sit on the artwork, always
   visible for the same reason .disc-get is — a phone has no hover, and these
   are the only controls that matter on a tile.

   .disc-get stays for the album shelf, which has no single track to play. */
.disc-acts{ position:absolute; top:6px; right:6px; display:flex; gap:5px; }
.disc-acts .pvbtn.sm{ width:26px; height:26px; background:rgba(0,0,0,.55);
  border-color:rgba(255,255,255,.34); color:#fff; box-shadow:0 2px 8px rgba(0,0,0,.45); }
.disc-acts .pvbtn.sm:hover{ border-color:var(--accent); color:var(--accent); }
/* Over artwork, so the ink is a fixed light on a fixed scrim rather than a
   theme token — the background here is somebody's album cover, not the page.
   The .on state keeps .pvbtn's accent conic fill, which reads on both. */
.disc-acts .pvbtn.sm.on{ color:var(--accent-ink); }

/* ===== R7.4 — the DJ's set ==========================================
   Pierre asked for "simple text output and just a list of the songs it
   recommands". So: what it said, then the songs, each with the one line that
   explains why it is there. The reason is the part that makes it read as a DJ
   rather than a shuffle, so it gets its own line rather than being appended to
   the artist. */
.djintro{ font-size:var(--fs-small); line-height:1.5; color:var(--txt);
  background:var(--raise); border:1px solid var(--line); border-radius:12px;
  padding:11px 13px; margin:-2px 0 12px; }
.djwhy{ font-size:var(--fs-micro); color:var(--accent-text); margin-top:3px;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.djrow{ cursor:pointer; }

/* ===== R8.1 — the Listening page ====================================
   Pierre asked for pictures, graphs and something closer to a Wrapped than a
   spreadsheet. The numbers were always real; what they lacked was the music.

   Everything here is built from artwork the app already has on disk (the log
   keeps each play's file, so a cover comes free) — nothing fetches a portrait
   from the internet to make a page look richer. */
.sthero{ position:relative; overflow:hidden; border-radius:18px; margin:2px 0 14px;
  border:1px solid var(--line); background:var(--card); }
.sthero-bg{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  filter:blur(26px) saturate(1.5); transform:scale(1.25); opacity:.42; }
/* The scrim is what makes the numbers legible over ANY cover — a pale album
   and a black one both end up dark under it, which is the whole point of not
   trusting the artwork to be dark. */
.sthero.has .sthero-in{ background:linear-gradient(180deg,rgba(8,10,14,.55),rgba(8,10,14,.86)); }
.sthero-in{ position:relative; padding:18px 18px 16px; }
.sthero.has .sthero-k, .sthero.has .sthero-s, .sthero.has .sthero-tl{ color:rgba(255,255,255,.78); }
.sthero.has .sthero-n, .sthero.has .sthero-tn{ color:#fff; }
.sthero-k{ font-size:var(--fs-micro); font-weight:800; letter-spacing:.09em; text-transform:uppercase;
  color:var(--dim); }
.sthero-n{ font-size:clamp(38px,12vw,58px); font-weight:900; line-height:1.02; letter-spacing:-.03em;
  color:var(--txt); margin-top:4px; }
.sthero-n span{ font-size:var(--fs-title); font-weight:800; margin-left:7px; letter-spacing:0; }
.sthero-s{ font-size:var(--fs-small); color:var(--dim); margin-top:3px; }
.sthero-top{ display:flex; align-items:center; gap:11px; margin-top:14px; }
.sthero-face{ width:46px; height:46px; border-radius:50%; object-fit:cover; flex:0 0 46px;
  border:2px solid rgba(255,255,255,.22); }
.sthero-tl{ font-size:var(--fs-micro); font-weight:800; letter-spacing:.06em; text-transform:uppercase; color:var(--dim); }
.sthero-tn{ font-size:var(--fs-head); font-weight:800; color:var(--txt); }

.stsec{ margin:0 0 18px; }
/* The chart is columns of plays per day. No axis numbers: the shape is the
   information, and a 320-wide chart with labels on it is unreadable anyway. */
.stchart{ background:var(--card); border:1px solid var(--line); border-radius:14px; padding:12px 12px 8px; }
.stchart svg{ display:block; width:100%; height:64px; }
.stchart svg rect{ fill:var(--accent); opacity:.9; }
.stchart-x{ display:flex; justify-content:space-between; font-size:var(--fs-micro); color:var(--faint); margin-top:6px; }

.stfaces{ gap:14px; padding:2px 0 4px; }
.stface{ flex:0 0 96px; width:96px; background:none; border:0; padding:0; cursor:pointer; text-align:center; }
.stface-w{ position:relative; display:block; width:96px; height:96px; }
.stface-w img{ width:96px; height:96px; border-radius:50%; object-fit:cover; background:var(--raise); }
.stface-r{ position:absolute; left:2px; bottom:2px; min-width:22px; height:22px; border-radius:11px;
  display:flex; align-items:center; justify-content:center; padding:0 6px;
  background:var(--accent); color:var(--accent-ink); font-size:var(--fs-micro); font-weight:900; }
.stface-n{ display:block; margin-top:7px; font-size:var(--fs-meta); font-weight:700; color:var(--txt);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.stface-p{ display:block; font-size:var(--fs-micro); color:var(--dim); }

.stgrid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(104px,1fr)); gap:13px; }
.sttile{ background:none; border:0; padding:0; cursor:pointer; text-align:left; min-width:0; }
.sttile-w{ position:relative; display:block; }
.sttile-w img{ width:100%; aspect-ratio:1; border-radius:11px; object-fit:cover; background:var(--raise); }
.sttile-r{ position:absolute; left:6px; top:6px; min-width:20px; height:20px; border-radius:10px;
  display:flex; align-items:center; justify-content:center; padding:0 5px;
  background:var(--scrim-2); color:#fff; font-size:var(--fs-micro); font-weight:900; }
.sttile-t{ display:block; margin-top:6px; font-size:var(--fs-meta); font-weight:700; color:var(--txt);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.sttile-a{ display:block; font-size:var(--fs-micro); color:var(--dim);
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* The history rows gained a cover, so the columns had to give one up. */
.sthrow .sthc{ flex:0 0 28px; width:28px; height:28px; border-radius:5px; object-fit:cover;
  background:var(--raise); }

/* ===== R8.2 — the Music section tabs =================================
   Pierre: "the top tabs in Spotify are very odd ... keep the same names, but
   work those tabs at the top ... do something with that that makes some UI
   nicely."

   They were five pills that looked exactly like the FILTER pills below them,
   so the eye could not tell "which section am I in" from "which sort am I
   using" - and on Library and Listening you met two rows of pills before any
   music. This is one strip: names in a row, an underline that slides to the
   one you are on, pinned under the header so the answer stays on screen.

   The strip sits at --ph-top-h and the library's own mode row (which was
   already sticky at that height) now sits BELOW it at --ph-tabs-h more, so
   the two never overlap when the page scrolls. */
.mutabs{ position:sticky; top:var(--ph-top-h); z-index:25; display:flex; gap:22px;
  align-items:center; padding:2px 0 0; margin:0 0 10px; background:var(--bg);
  overflow-x:auto; scrollbar-width:none; -webkit-overflow-scrolling:touch; }
.mutabs::-webkit-scrollbar{ display:none; }
.mutab{ position:relative; flex:0 0 auto; background:none; border:0; padding:9px 1px 11px;
  font-size:var(--fs-small); font-weight:700; color:var(--dim); cursor:pointer;
  white-space:nowrap; transition:color .16s; }
.mutab:hover{ color:var(--txt); }
.mutab.on{ color:var(--txt); font-weight:800; }
/* One element that MOVES rather than a border per tab: the slide is what makes
   the strip read as one control instead of five. */
.mutab-ink{ position:absolute; left:0; bottom:0; height:3px; border-radius:3px 3px 0 0;
  background:var(--accent); opacity:0; pointer-events:none;
  transition:transform .22s cubic-bezier(.4,0,.2,1), width .22s cubic-bezier(.4,0,.2,1), opacity .16s; }
/* The one bit of chrome that has to know the strip's height. Measured rather
   than assumed anywhere it matters — see the drive. */
:root{ --ph-tabs-h:42px; }
@media (prefers-reduced-motion:reduce){ .mutab-ink{ transition:none; } }

/* ===== R8.3 — a playlist looks like what is in it ====================
   A playlist tile took the cover of its FIRST track, so a shelf of mixes was a
   shelf of near-identical squares: it told you what a playlist starts with,
   never what it is. Four distinct covers is how anybody recognises their own
   mixes, and it is the one place a grid of tiles becomes readable at a glance.

   Four or nothing: three covers and a gap reads as a failed load rather than a
   design, so plTileFace falls back to the single cover it always used. */
.plmos{ display:grid; grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr;
  width:100%; aspect-ratio:1; border-radius:10px; overflow:hidden; background:var(--raise); }
.plmos img{ width:100%; height:100%; object-fit:cover; display:block; }
/* The heart stays, as a badge over the mosaic: Liked Songs has to stay
   recognisable at a glance even once it is wearing four covers. */
.mgtile-art{ position:relative; }
.plmos-badge{ position:absolute; right:6px; bottom:6px; width:26px; height:26px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  background:var(--accent); color:var(--accent-ink); box-shadow:0 2px 8px rgba(0,0,0,.45); }
