/* app.css — index.html's STRUCTURE layer.

   Split out of a 268 KB single-file index.html, and restructured again on
   2026-08-19 as part of the Darkroom retheme. Four files now, with one job
   each, loaded in this order:

       tokens.css      the design system — palette, type, space, motion
       brand.css       utilities for the drawn assets in /static/brand/
       app.css         THIS FILE — structure, mechanics, states, a11y
       darkroom.css    the design: what everything looks like
       adultswim.css   the one alternate theme

   Two blocks left this file in that pass and are not coming back:

   • The 1,124-line Adult Swim theme moved to adultswim.css. Every read of
     this file used to be 45% a theme nobody was looking at, and a base-layer
     edit could silently collide with a theme-only selector.

   • PHASE E — the 600-line corrective design layer written over the previous
     direction — was DELETED rather than left underneath darkroom.css. Four
     stacked passes disagreeing about the same component is how this file got
     to 3,600 lines; adding a fifth with a new palette would have been the
     same mistake. The handful of rules in it that were structural rather
     than decorative (the .cwgrid/.nowgrid scrollbar suppression, two stray
     backdrop-filters, the .np-state treatment) were carried into
     darkroom.css by name, with the reasons written down there.

   What remains here is the part that is true regardless of how the site
   looks: layout, positioning, overflow and scroll behaviour, overlay and
   focus mechanics, ARIA-driven states, and the responsive breakpoints.
   If a rule here only says what colour or shape something is, it belongs in
   darkroom.css.

   Cached independently of the HTML: index.html is served no-cache (it changes
   every deploy and must revalidate), while this is served with a long max-age,
   so a deploy that only touches markup no longer re-downloads the CSS. */

/* The swappable backdrop system (Nocturne dot-grid stage, the retired Canopy
   forest, and the [data-atmos] switch between them) has been removed along
   with the theme picker. The one backdrop now is the ambient jewel glow in
   tokens.css (.aurora-sky). Nothing here paints the background any more. */

  /* ── header nav ──────────────────────────────────────────────────────────
     Watch / Read / Listen. These were three full-width tiles ("doors") sitting
     at the top of the CONTENT column, which meant the first thing a member saw
     below the hero was navigation rather than their own library — and the
     three big "Request →" pills on them were three of the five competing
     request affordances the page used to have. They are navigation, so they
     are in the nav. The whole .doors/.door/.door-main/.door-req block is gone
     with them; nothing else referenced it. */
  .topnav{display:flex; align-items:center; gap:2px; flex:none;}
  .navlink{position:relative; padding:var(--sp-2) var(--sp-4); border-radius:var(--radius-pill);
    color:var(--ink-dim); font-size:var(--t-meta); font-weight:600; text-decoration:none;
    white-space:nowrap; transition:color .18s var(--ease), background .18s var(--ease);}
  .navlink:hover{color:var(--ink); background:color-mix(in srgb, var(--accent-base) 12%, transparent);}
  .navlink:focus-visible{outline:2px solid var(--focus-ring); outline-offset:2px;}
  /* accent underline that wipes in on hover — theme-reactive, sits under the pill */
  .navlink::after{content:""; position:absolute; left:14px; right:14px; bottom:5px; height:1.5px; border-radius:2px;
    background:linear-gradient(90deg,var(--accent-base),var(--amber-base)); transform:scaleX(0); transform-origin:left center;
    transition:transform .26s var(--ease-standard);}
  .navlink:hover::after, .navlink:focus-visible::after{transform:scaleX(1);}
  @media(prefers-reduced-motion:reduce){.navlink::after{transition:none;}}
  /* Below 900px the wordmark + nav + controls stop fitting on one line. The
     nav is the part that survives collapse best — Watch is reachable from the
     hero and the catalog button, Read/Listen from the Request chooser's
     siblings — so it is what folds away first, before the search pill (640px)
     and long before anything in .topright. */
  @media(max-width:900px){ .topnav{display:none;} }

  /* ── unified components (UI pass A) ──────────────────────────────────────
     Defined once here; every feature-specific class below (.cap, .who, .lgen,
     .ck, .syn-more, .sstate, .lbtn, .pv-btn, .libcontrols input/select, login
     inputs, .lb/.lbox/.loginbox/.pv-box) is wired to reuse these rather than
     reinventing padding/radius/shadow — see each section's comment below for
     how. Semantic color overrides (e.g. genre labels staying --accent-2) are
     kept as small deltas after the shared rule, per the plan. */

  /* eyebrow — the one uppercase-micro-label treatment, for things that really
     are micro-labels: stat captions, spec keys, genre tags, status words.
     NOT for section headings — the rail headings used to ride this rule and
     were illegible as structure; see .railhead .cap further down. */
  .eyebrow, .cap, .npc .who, .syn-g, .lgen, .crow .ck, .storage .ck,
  .roadmap .ck, .sname .sstate, .syn-more{
    font-size:var(--t-eyebrow); letter-spacing:.28em; text-transform:uppercase;
    color:var(--muted); font-weight:700; margin:0;
  }
  /* Nocturne eyebrow variant — the mockup's own neon-gradient-clipped label
     treatment (its .eyebrow.neon), for the one or two spots per view where
     an eyebrow IS the brand moment (the gate's "Media Forest" label) rather
     than a quiet section header. Additive: everything above still applies
     (size/tracking/weight), this only swaps the color to the gradient. */
  .eyebrow.neon{background:var(--neon); -webkit-background-clip:text; background-clip:text; color:transparent;}
  .syn-g, .lgen, .crow .ck, .storage .ck, .roadmap .ck, .syn-more{ color:var(--accent-2); }

  /* btn — one button system, three tiers.

       plain  outline + ink-dim text     everything ordinary
       wire   .btn--accent, cyan outline  important, but not THE action here
       neon   gradient fill               the one primary action on a surface

     The .authbtn / .authbtn-in / .btn--ghost classes that used to be wired
     into these selectors are gone: renderAuthBox() was rewritten to emit
     `.btn.btn--accent` and `.avatar`, and nothing has emitted an .authbtn
     since — the rules survived as an unreferenced third spelling of tiers
     that already existed. (Checked against every class string in index.html,
     app.js and the three member pages before deleting.)

     Which elements get neon: .hero-play (the lead hero slice's Play),
     `button.lbtn` (the two real <button type=submit> gate/login actions), and
     .lplay-go (the detail modal's Play). NOT `a.lbtn` in general — that class
     is also worn by the IMDb / TMDB / Letterboxd link badges, which should
     stay plain. */
  .btn, .lbtn{
    display:inline-flex; align-items:center; justify-content:center; gap:6px;
    background:transparent; border:1px solid var(--line); color:var(--ink-dim);
    border-radius:var(--r-md); padding:8px 16px; font-size:var(--t-meta); font-weight:600;
    font-family:inherit; text-decoration:none; cursor:pointer; white-space:nowrap;
    transition:color .18s var(--ease), border-color .18s var(--ease), background .18s var(--ease), transform .18s var(--ease), box-shadow .18s var(--ease), filter .18s var(--ease);
  }
  .btn:focus-visible, .lbtn:focus-visible{
    outline:2px solid var(--focus-ring); outline-offset:2px;
  }
  .btn--sm{ padding:5px 12px; font-size:var(--t-eyebrow); border-radius:var(--r-sm); }
  .btn:hover, a.lbtn:not(.lplay-go):hover{
    color:var(--platinum-bright); border-color:var(--platinum); transform:translateY(-1px);
  }
  .btn:active, .lbtn:active{transform:translateY(0) scale(.97);}
  /* Ripple effect — radial reveal from click point */
  .btn,.lbtn{position:relative; overflow:hidden;}
  .btn::after,.lbtn::after{content:""; position:absolute; inset:0; background:radial-gradient(circle at var(--ripple-x,50%) var(--ripple-y,50%), color-mix(in srgb,var(--text-primary) 15%,transparent) 0%, transparent 70%); opacity:0; transition:opacity .4s var(--ease-exit); pointer-events:none;}
  .btn:active::after,.lbtn:active::after{opacity:1; transition:opacity 0s;}
  /* press feedback on all interactive surfaces */
  .navlink:active, .libtab:active, .iconbtn:active,
  .sheet-close:active, .menu-item:active, .railnav:active, .pv-btn:active,
  .pv-ep:active, .pv-season-btn:active{transform:scale(.96);}
  .search:active{transform:scale(.99);}
  /* disabled — universal treatment for all controls */
  .btn:disabled, .lbtn:disabled, .field:disabled, .libtab:disabled,
  .pv-btn:disabled{opacity:.35; cursor:not-allowed; pointer-events:none; filter:grayscale(.3);}
  /* accent tier — rose enamel: a tonal rose→wine fill, a champagne catch-light
     along the top edge, dark ink, and a soft rose halo. Reads as a polished
     object rather than a flat swatch. */
  .btn--accent{
    background:linear-gradient(140deg,var(--aurora-green-hover),var(--aurora-green) 45%,var(--aurora-rose));
    color:var(--accent-on); border-radius:var(--radius-pill); border:none;
    box-shadow:0 4px 18px -4px color-mix(in srgb,var(--aurora-green) 42%,transparent),
      inset 0 1px 0 rgba(255,244,230,0.30);
  }
  .btn--accent:hover{ transform:translateY(-2px); color:var(--accent-on);
    box-shadow:0 10px 34px -6px color-mix(in srgb,var(--aurora-green) 55%,transparent),
      inset 0 1px 0 rgba(255,244,230,0.42); }
  /* Enamel sheen — a band of light glides across the primary actions on hover,
     the way light catches a polished surface. isolation:isolate makes each
     button its own stacking context so the ::before band (z-index:-1) sits
     above the fill but below the label — crisp text even on the bare-text
     Sign in / Log in buttons that have no child element to lift. */
  .lplay-go, .hero-play{ position:relative; overflow:hidden; }
  .btn--accent, .lbtn, .lplay-go, .hero-play{ isolation:isolate; }
  .btn--accent::before, .lbtn::before, .lplay-go::before, .hero-play::before{
    content:""; position:absolute; top:0; bottom:0; left:-70%; width:48%; z-index:-1; pointer-events:none;
    background:linear-gradient(100deg, transparent, rgba(255,244,230,0.38), transparent);
    transform:skewX(-16deg); opacity:0;}
  @media(hover:hover){
    .btn--accent:hover::before, .lbtn:hover::before, .lplay-go:hover::before, .hero-play:hover::before{
      animation:btn-sheen .7s var(--ease-standard);}
  }
  @keyframes btn-sheen{0%{left:-70%;opacity:0} 22%{opacity:1} 100%{left:130%;opacity:0}}
  @media(prefers-reduced-motion:reduce){ .btn--accent::before,.lbtn::before,.lplay-go::before,.hero-play::before{display:none;} }
  /* plain tier — pill + subtle glass, for non-primary actions */
  .btn:not(.btn--accent){ border-radius:var(--radius-pill); background:color-mix(in srgb, var(--text-primary) 6%, transparent); border:1px solid var(--border-strong); }
  .btn:not(.btn--accent):hover{ background:color-mix(in srgb, var(--text-primary) 12%, transparent); transform:translateY(-1px); }
  /* Detail modal's Play button — the one action that surface exists for. */
  .lplay-go{ border:none; color:var(--accent-on); background:linear-gradient(140deg,var(--aurora-green-hover),var(--aurora-green) 45%,var(--aurora-rose)); font-weight:700;
    padding:11px 22px; font-size:var(--t-body);
    box-shadow:0 4px 18px -4px color-mix(in srgb,var(--aurora-green) 42%,transparent), inset 0 1px 0 rgba(255,244,230,0.30); }
  .lplay-go:hover{ filter:brightness(1.05); transform:translateY(-2px); color:var(--accent-on); border:none; background:linear-gradient(140deg,var(--aurora-green-hover),var(--aurora-green) 45%,var(--aurora-rose)); box-shadow:0 10px 34px -6px color-mix(in srgb,var(--aurora-green) 55%,transparent), inset 0 1px 0 rgba(255,244,230,0.42); }
  .lplay-go svg{ width:15px; height:15px; }
  .pv-btn.btn{ background:color-mix(in srgb, var(--text-primary) 6%, transparent); border:1px solid var(--border-strong); border-radius:50%; padding:4px; color:#fff; opacity:.9; }

  /* field — one input/select treatment for .libcontrols and the login form.
     Fixes the old sizing quirk permanently: .field never sets a flex-basis
     itself, so a column form (.loginform) can't inherit a giant row-flex
     height by accident — flex behavior is opt-in per-container instead. */
  .field{
    background:var(--bg-soft); border:1px solid var(--line); color:var(--ink);
    border-radius:var(--r-md); padding:10px 14px; font-size:var(--t-meta); font-family:inherit;
    -webkit-appearance:none; appearance:none;
  }
  .field:focus-visible{ outline:2px solid var(--accent); outline-offset:1px; }
  .field::placeholder{ color:var(--muted); }
  /* .field sets appearance:none, which on a <select> removes the native
     dropdown arrow and leaves nothing in its place — so the catalog's genre
     and sort controls rendered as plain pills that read as buttons, with no
     signal that they open a list at all. Draw the chevron back on (inline SVG
     data-URI, currentColor baked to the ink tone since a data: URI can't read
     a CSS variable) and reserve room for it. */
  select.field{ padding-right:36px; background-repeat:no-repeat;
    background-position:right 12px center; background-size:11px;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%239BB0CC' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
  select.field:hover{ border-color:var(--line-strong); }

  /* searchfield — the ONE search-input shell (design-review #1). A leading
     magnifier icon + a borderless input inside a shared rounded shell, so
     every "search" on the site reads as the same component: it mirrors the
     ⌘K palette's .p-input (icon + borderless field) and the header .search
     pill (same border, same cyan focus signal). The library filter search
     was the outlier — a flat bordered rectangle with no icon — and now
     inherits this. Same box metrics as .field (10px/14px padding) so it lines
     up with the genre/sort selects sitting beside it in .libcontrols. */
  .searchfield{ display:flex; align-items:center; gap:10px; padding:10px 14px;
    background:var(--bg-soft); border:1px solid var(--line); border-radius:var(--r-md);
    color:var(--muted); cursor:text; transition:border-color .2s var(--ease), background .2s var(--ease); }
  .searchfield:focus-within{ border-color:var(--cyan); background:color-mix(in srgb, var(--cyan) 5%, transparent); }
  .searchfield svg{ width:16px; height:16px; flex:none; opacity:.8; }
  .searchfield input{ flex:1; min-width:0; background:none; border:none; outline:none;
    color:var(--ink); font:400 var(--text-sm)/1.2 inherit; padding:0; -webkit-appearance:none; appearance:none; }
  .searchfield input::placeholder{ color:var(--muted); }
  /* the search-input clear "x" (WebKit) inherits the muted chrome */
  .searchfield input::-webkit-search-cancel-button{ filter:grayscale(1) opacity(.5); }

  /* overlay / sheet — the shared .lb backdrop recipe + card treatment, used
     by the detail modal, login modal, and player box (all --r-xl, same
     shadow token, same close-button component). */
  .overlay{position:fixed; inset:0; z-index:80;
    background:color-mix(in srgb, var(--color-bg-deepest) 72%, transparent); backdrop-filter:blur(14px) saturate(150%); -webkit-backdrop-filter:blur(14px) saturate(150%);
    display:none; align-items:center; justify-content:center; padding:28px;}
  .overlay.on{display:flex;}
  .sheet{position:relative;
    background:var(--glass-card-bg); backdrop-filter:var(--glass-card-blur); -webkit-backdrop-filter:var(--glass-card-blur);
    border:1px solid var(--border-strong); border-radius:var(--radius-xl); box-shadow:var(--shadow-modal);}
  .sheet-close{position:absolute; top:12px; right:14px; z-index:3; width:32px; height:32px; border-radius:50%;
    background:color-mix(in srgb, var(--text-primary) 6%, transparent); border:1px solid var(--border-strong); color:var(--ink-dim); cursor:pointer; font-size:var(--text-sm); line-height:1;
    display:flex; align-items:center; justify-content:center;}
  .sheet-close:hover{color:var(--ink); background:color-mix(in srgb, var(--text-primary) 12%, transparent);}

  /* ── UI pass B: split prose-width vs. wide-track containers ──────────────
     .wrap (hero/prose) stays a readable measure; .wrap--wide is the same
     centered container with a wider cap so the catalog grid and discovery
     rails gain columns for free on ultrawide (the grid already uses
     auto-fill,minmax — a wider container is the whole fix). Both flow from
     the same two width tokens, so tightening/loosening either later is a
     one-line token change, not a hunt through selectors. */
  .wrap{position:relative; z-index:2; max-width:var(--w-read); margin:0 auto; padding:0 var(--gutter);}
  .wrap--wide{max-width:var(--w-wide);}
  /* The header wrap must out-stack the hero: every .wrap shares z-index:2, and
     the hero comes later in the DOM, so a header popover (appearance/account)
     dropping down into the hero's band would otherwise be painted over by the
     hero slices. Lifting just the header's wrap fixes it without touching the
     rest of the page's stacking. */
  #topwrap{position:sticky;top:0;z-index:30;}
  /* Phones: the 24px fixed gutters eat a big share of a ~360px screen and
     make every rail/grid feel cramped. Drop to 16px to reclaim usable width
     (also pulls the hero copy, which shares .wrap, tighter to the edge). */
  @media(max-width:640px){ :root{--gutter:var(--s4);} }
  /* ── streaming-app redesign: compact top-bar ──────────────────────────────
     The page used to open with a 60vh centered hero built entirely from text
     (h1 + lede). That's now the rotating backdrop hero banner below instead —
     this bar is just the persistent brand mark + controls, present in both
     home and catalog view (unlike the hero banner, which hides in catalog). */
  header{padding:18px 0 14px;}
  #topbar{
    background:var(--glass-header-bg);backdrop-filter:var(--glass-header-blur);
    -webkit-backdrop-filter:var(--glass-header-blur);
    border:1px solid var(--border-default);border-radius:var(--radius-xl);
    box-shadow:var(--shadow-md),inset 0 1px 0 rgba(255,244,230,0.08),inset 0 -1px 0 var(--gilt-soft);
    padding:var(--sp-4) var(--sp-5);
  }
  .topline{display:flex; align-items:center; justify-content:space-between; gap:16px; margin:0; flex-wrap:wrap;}
  /* ⌘K trigger pill — mockup's own .search, unmodified. Hidden below 640px
     (mockup's own breakpoint too) since the header's already tight there;
     ⌘K/Ctrl+K itself still works from a keyboard on mobile-width desktop
     windows, this is just the visible affordance. */
  .search{flex:1; max-width:460px; display:flex; align-items:center; gap:10px; cursor:text; height:38px;
    padding:var(--sp-2) var(--sp-4); border-radius:var(--radius-pill);
    background:color-mix(in srgb, var(--text-primary) 4%, transparent); border:1px solid var(--border-default);
    color:var(--muted); font-size:var(--text-sm); font-family:inherit; transition:.2s var(--ease);}
  .search:hover{border-color:var(--aurora-teal); background:color-mix(in srgb, var(--text-primary) 6%, transparent); color:var(--ink-dim);}
  .search svg{width:15px; height:15px; opacity:.7; flex:none;}
  .search span:not(.kbd){overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left;}
  .search .kbd{margin-left:auto; display:flex; gap:3px; flex:none;}
  .kbd b{font:700 11px/1 inherit; color:var(--ink-dim); background:color-mix(in srgb, var(--line-c) 9%, transparent);
    border:1px solid var(--line); border-radius:5px; padding:3px 5px;}
  @media(max-width:640px){.search{display:none;}}
  /* ── identity cluster (design-review #5) ───────────────────────────────────
     One coherent right-side cluster, three clearly-distinct affordances in one
     visual language instead of three: (1) "Watch together" — the single
     standalone action, a wire button; (2) an appearance icon-button that opens
     a labelled Theme popover (the 6 colour swatches used to sit bare in the
     header and read like an avatar row — now they're behind a labelled control,
     no longer ambiguous); (3) an account avatar that opens a menu holding the
     username, The instrument, and Sign out (previously bare "name · Sign out"
     text). .hmenu is the shared positioned wrapper for a button + its popover. */
  .topright{display:flex; align-items:center; gap:10px;}
  /* Hidden above the phone breakpoint so the desktop bar is unchanged. */
  .btn-icon{display:none; flex:none;}
  /* Phones: this row is nowrap by default, and its children are all
     intrinsically sized, so anything added to it pushes the PAGE sideways
     rather than reflowing — which is exactly what happened when the
     notification bell stopped being display:none (44px + a 10px gap took the
     row from 309px to 363px inside 343px of usable width, and the whole
     document gained 34px of horizontal scroll).

     Wrapping instead of overflowing makes the row self-correcting: it stays on
     one line while it fits and drops to a second when it doesn't, so the next
     control added here can't reintroduce the same bug. justify-content keeps
     the wrapped remainder aligned to the right edge with the avatar. */
  @media(max-width:640px){
    /* Give the control row its OWN full-width line instead of letting it
       compete with the wordmark for the remainder of a shared one. As a
       shrink-to-fit flex item it was allocated ~282px for ~331px of content,
       so it wrapped internally to three rows and pushed the sticky header to
       164px — on the screen with the least vertical room. flex-basis:100%
       hands it the full 343px, which its content fits on a single line, and
       .topline (already flex-wrap:wrap) drops it below the wordmark. */
    .topright{flex:1 0 100%; flex-wrap:wrap; justify-content:flex-end; gap:8px; row-gap:8px;}
    /* Swap label for icon. Keeps all five controls on one row at 375px and the
       header at two lines instead of three. */
    .topright .btn-label{display:none;}
    .topright .btn-icon{display:block;}
    .topright .btn{padding-left:0; padding-right:0; width:44px;}
    /* #topbar's own 20px side padding sits INSIDE #topwrap's 16px, so a phone
       was spending 72px of a 375px screen on nested gutters before any content
       was laid out. */
    #topbar{padding:var(--sp-3);}
    /* Wrapping alone fixed the overflow but cost 52px of sticky header on a
       375px screen, which is a bad trade on the device with least room. So
       also reclaim the width: 6px off each side of the two text buttons is
       24px back, enough to keep the row on ONE line in practice. Scoped to
       .topright so buttons elsewhere keep their normal size, and only the
       horizontal padding moves — the 44px min-height touch target from the
       (pointer: coarse) block below is untouched. Wrapping stays as the
       safety net for longer labels or a future control. */
    .topright .btn{padding-left:10px; padding-right:10px;}
  }
  .authbox{display:flex; align-items:center; gap:10px;}
  .hmenu{position:relative; display:inline-flex;}
  /* quiet square icon-button — same outline language as the .btn base */
  .iconbtn{width:32px; height:32px; padding:0; display:inline-flex; align-items:center; justify-content:center;
    background:color-mix(in srgb, var(--text-primary) 3%, transparent); border:1px solid var(--border-subtle); border-radius:50%; color:var(--ink-dim);
    cursor:pointer; transition:all .2s;}
  .iconbtn:hover{color:var(--platinum-bright); background:color-mix(in srgb, var(--text-primary) 6%, transparent); border-color:var(--border-strong);}
  .iconbtn:focus-visible{outline:2px solid var(--focus-ring); outline-offset:2px;}
  .iconbtn svg{width:17px; height:17px;}
  .iconbtn[aria-expanded="true"]{color:var(--cyan); border-color:color-mix(in srgb, var(--cyan) 45%, transparent);}
  /* account avatar — same 32px footprint as the icon-button, but round + the
     click-target cyan, so it reads as "you" */
  .avatar{width:32px; height:32px; padding:0; border-radius:50%; display:inline-flex; align-items:center; justify-content:center;
    background:color-mix(in srgb, var(--cyan) 12%, transparent); border:1px solid color-mix(in srgb, var(--cyan) 40%, transparent);
    color:var(--cyan); font:700 13px/1 var(--sans); cursor:pointer; flex:none;
    transition:box-shadow .18s var(--ease), border-color .18s var(--ease);}
  .avatar:hover, .avatar[aria-expanded="true"]{border-color:var(--cyan); box-shadow:0 0 16px -5px var(--gc);}
  .avatar:focus-visible{outline:2px solid var(--focus-ring); outline-offset:2px;}
  /* dropdown popover — reuses the modal-surface gradient/border/shadow tokens */
  .menu{position:absolute; top:calc(100% + 8px); right:0; z-index:120; min-width:210px; padding:6px;
    background:var(--glass-card-bg); backdrop-filter:var(--glass-card-blur); -webkit-backdrop-filter:var(--glass-card-blur);
    border:1px solid var(--border-strong); border-radius:var(--radius-lg); box-shadow:var(--shadow-modal); animation:menu-pop .16s var(--ease);}
  .menu[hidden]{display:none;}
  @keyframes menu-pop{from{opacity:0; transform:translateY(-4px);}}
  @media(prefers-reduced-motion:reduce){.menu{animation:none;}}
  .menu-label{font-size:var(--text-3xs); letter-spacing:.2em; text-transform:uppercase; color:var(--muted); font-weight:700; padding:6px 8px 8px;}
  .menu-head{padding:6px 10px 10px; margin:0 0 6px; border-bottom:1px solid var(--line);}
  .menu-name{display:block; font-weight:700; color:var(--ink); font-size:var(--text-base); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
  .menu-sub{display:block; font-size:var(--text-2xs); color:var(--muted); margin-top:2px;}
  .menu-item{display:flex; align-items:center; gap:10px; width:100%; text-align:left; background:none; border:none;
    color:var(--ink-dim); font:600 13px/1.2 var(--sans); padding:9px 10px; border-radius:var(--r-sm); cursor:pointer;}
  .menu-item:hover{background:color-mix(in srgb, var(--line-c) 8%, transparent); color:var(--ink);}
  .menu-item svg{width:15px; height:15px; flex:none; opacity:.85;}
  .appearance-menu{min-width:0;}
  /* Theme picker — a row of two-tone swatch buttons inside the appearance
     popover. Each swatch previews a palette's accent pair; the active one wears
     a ring. Built by renderThemePicker() in app.js. */
  .themerow{display:flex; align-items:center; gap:10px; flex:none; padding:2px 8px 8px; flex-wrap:wrap;}
  .themerow button{width:22px; height:22px; padding:0; border-radius:50%; cursor:pointer; position:relative;
    border:1px solid var(--border-strong); box-shadow:var(--shadow-sm);
    transition:transform .18s var(--ease), box-shadow .18s var(--ease);}
  .themerow button:hover{transform:scale(1.15);}
  .themerow button[aria-pressed="true"]{transform:scale(1.05);}
  .themerow button[aria-pressed="true"]::after{content:""; position:absolute; inset:-4px; border-radius:50%;
    border:1.5px solid var(--gilt); box-shadow:0 0 8px var(--gilt-glow);}
  .themerow button:focus-visible{outline:2px solid var(--focus-ring); outline-offset:3px;}
  @media(prefers-reduced-motion:reduce){.themerow button{transition:none;}}
  /* Privacy section inside the appearance popover — the hot-join opt-in.
     Widens the otherwise min-width:0 appearance menu enough for the copy. */
  .pref-section{margin-top:6px; padding-top:4px; border-top:1px solid var(--line); width:min(84vw,300px);}
  .pref-row{display:flex; align-items:flex-start; gap:12px; padding:6px 8px 8px; cursor:pointer;}
  .pref-copy{display:flex; flex-direction:column; gap:3px; min-width:0;}
  .pref-title{font:600 13px/1.2 var(--sans); color:var(--ink);}
  .pref-note{font-size:var(--text-2xs); line-height:1.4; color:var(--muted);}
  /* Toggle switch — the checkbox is the real control (kept accessible, just
     visually collapsed to a dot); .pref-switch-track is the rendered pill. */
  .pref-switch{position:relative; flex:none; width:38px; height:22px; margin-top:1px;}
  .pref-switch input{position:absolute; inset:0; margin:0; opacity:0; width:100%; height:100%; cursor:pointer;}
  .pref-switch-track{position:absolute; inset:0; border-radius:999px; background:var(--line-strong);
    transition:background .18s var(--ease);}
  .pref-switch-track::after{content:""; position:absolute; top:2px; left:2px; width:18px; height:18px;
    border-radius:50%; background:var(--ink); box-shadow:var(--shadow-sm); transition:transform .18s var(--ease);}
  .pref-switch input:checked ~ .pref-switch-track{background:var(--accent);}
  .pref-switch input:checked ~ .pref-switch-track::after{transform:translateX(16px);}
  .pref-switch input:focus-visible ~ .pref-switch-track{outline:2px solid var(--accent); outline-offset:2px;}
  .pref-switch input:disabled{cursor:default;}
  .pref-switch input:disabled ~ .pref-switch-track{opacity:.5;}
  @media(prefers-reduced-motion:reduce){.pref-switch-track,.pref-switch-track::after{transition:none;}}
  /* wordmark — the ONE Media|Forest mark (design-review #6). A single locked
     treatment reused on the header, the sign-in gate, and the hero empty-state
     so the logo never drifts between solid / uppercase / gradient variants
     across screens: "Media" and "Forest" in --ink, the "|" divider in the
     brand neon gradient text-clip. Its glow comes from the theme-universal
     .bar rule up top (additive with background-clip:text). .wordmark--lg is
     the larger cut for the gate/hero, where the mark stands alone. */
  h1, .wordmark{font-family:var(--font-serif); font-weight:var(--fw-semibold); line-height:1;
    letter-spacing:-.005em; margin:0; color:var(--ink);}
  h1{font-size:var(--head-card);}
  .wordmark{display:inline-block; font-size:var(--head-card);}
  .wordmark--lg{font-size:var(--head-modal);}
  h1 .bar, .wordmark .bar{background:var(--neon); -webkit-background-clip:text; background-clip:text; color:transparent;
    font-weight:400; margin:0 .1em; opacity:.92;}
  .h1home{cursor:pointer; display:inline-flex; align-items:center;}
  .h1home:hover .bar{opacity:1;}
  .h1home:focus-visible{outline:2px solid var(--accent); outline-offset:6px; border-radius:6px;}
  .wordmark--lg{display:inline-flex; align-items:center; justify-content:center;}
  /* ── Brand emblem — a gilt faceted gem cradling an accent play-triangle
     (media held in a jewel). Two-tone and theme-reactive: the gem + facets
     follow --amber-base (leaf), the play follows --accent-base. Sized in em so
     it scales with the wordmark. */
  .brandmark{width:1.02em; height:1.02em; flex:none; margin-right:.42em; vertical-align:-.14em; overflow:visible;
    filter:drop-shadow(0 1px 3px rgba(0,0,0,.45));}
  .brandmark .bm-gem{fill:none; stroke:var(--amber-base); stroke-width:1.7; stroke-linejoin:round;}
  .brandmark .bm-facet{fill:none; stroke:var(--amber-base); stroke-width:.9; opacity:.5; stroke-linejoin:round; stroke-linecap:round;}
  .brandmark .bm-play{fill:var(--accent-base);}
  .h1home:hover .brandmark .bm-play{fill:var(--accent-hover);}
  @media(prefers-reduced-motion:no-preference){ .brandmark .bm-play{transition:fill .2s var(--ease);} }
  /* ── Gilt flourish divider — centred sparkle flanked by fading rules. */
  .flourish{display:flex; align-items:center; justify-content:center; gap:16px;
    width:min(88vw,460px); margin:var(--sp-12) auto var(--sp-6);}
  .flourish::before, .flourish::after{content:""; height:1px; flex:1 1 auto;}
  .flourish::before{background:linear-gradient(90deg,transparent,var(--gilt-line) 92%);}
  .flourish::after{background:linear-gradient(90deg,var(--gilt-line),transparent 8%);}
  .flourish i{flex:none; width:15px; height:15px;
    background:linear-gradient(135deg,var(--amber-hover),var(--amber-base));
    -webkit-mask:var(--spark-mask) center/contain no-repeat; mask:var(--spark-mask) center/contain no-repeat;
    filter:drop-shadow(0 0 7px var(--gilt-glow));}
  /* small secondary link for the demoted Jellyfin native-app entry, and the
     Books disclosure's two links (reused there — same "quiet inline link"
     treatment throughout the page) */
  .applink{font-size:var(--text-sm); color:var(--muted); margin:0; letter-spacing:.01em;}
  .applink a{color:var(--ink-dim); text-decoration:none; font-weight:600; border-bottom:1px solid var(--line); transition:color .18s var(--ease), border-color .18s var(--ease);}
  .applink a:hover{color:var(--link); border-color:var(--link);}

  /* ── Nocturne Stage 2 fidelity pass: scrollable hero strip ────────────────
     Was a fixed 3-slice grid triptych (a divergence the first Stage-2 port
     introduced); the approved mockup is a horizontally-scrollable band of
     banner slices instead, so this now uses the exact same overflow-x
     scroll + scroll-snap + edge-fade-mask recipe as .addgrid (the poster
     rails) rather than a grid — 2-3 slices visible at a time, scroll for
     the rest. loadHero()/renderHero() below fetch and render more than 3
     items again (see their own comments); heroPlay()/heroInfo()/
     renderFilmDetail() are unchanged, only what calls them changed shape.
     Scrim uses hub's own color-mix(var(--void)) convention (matching every
     other themed scrim on the page) rather than the mockup's hardcoded
     rgba(3,4,10,..), so it follows the active theme's actual base tone, not
     just Nocturne's own. */
  /* Full-bleed (it deliberately lives outside .wrap so the strip runs edge to
     edge), but the FIRST slice now starts on the same left edge as the header
     wordmark and the rails below it, via --bleed-inset. Before this the strip
     began hard against the viewport at x=0 while everything else began at the
     --w-wide inset — 252px apart on a 2560px screen, and the single loudest
     misalignment on the page. scroll-padding-inline keeps snapped slices on
     that same edge rather than snapping flush to the viewport. */
  .hero{position:relative; z-index:2; width:100%; margin:0 0 var(--section);
    display:flex; gap:14px; overflow-x:auto; overflow-y:visible;
    padding:4px var(--bleed-inset) 14px;
    scroll-padding-inline:var(--bleed-inset);
    scroll-snap-type:x proximity; scrollbar-width:thin; scrollbar-color:color-mix(in srgb, var(--text-primary) 15%, transparent) transparent;
    height:clamp(300px, 44vw, 520px);
    -webkit-mask-image:linear-gradient(90deg, transparent 0, #000 22px, #000 calc(100% - 22px), transparent 100%);
    mask-image:linear-gradient(90deg, transparent 0, #000 22px, #000 calc(100% - 22px), transparent 100%);}
  .hero::-webkit-scrollbar{height:8px}
  .hero::-webkit-scrollbar-thumb{background:color-mix(in srgb, var(--text-primary) 15%, transparent); border-radius:var(--r-sm); transition:background .2s var(--ease)}
  .hero:hover::-webkit-scrollbar-thumb{background:color-mix(in srgb, var(--accent) 38%, transparent)}
  .hero::-webkit-scrollbar-track{background:transparent}
  /* This page's own .hero{display:flex} outranks the UA [hidden]{display:none}
     default (equal 0,1,0 specificity, page stylesheet wins the tie) - same
     gotcha .pv-loading/.hero-nav already had to work around before Stage 2;
     .hero didn't need this rule before since it had no `display` override of
     its own. openCatalog()/closeCatalog() (further down) set hero.hidden to
     toggle it, so without this it silently never actually hid. */
  .hero[hidden]{display:none;}
  /* #hero-slides is still renderHero()'s existing innerHTML target (zero JS
     changes to the element-targeting) - display:contents makes the wrapper
     invisible to flex layout, so its .slice children become direct flex
     items of .hero, same as if there were no wrapper element at all. */
  #hero-slides{display:contents;}
  .slice{position:relative; overflow:hidden; border-radius:var(--radius-lg); border:1px solid var(--border-strong);
    display:flex; align-items:flex-end; isolation:isolate; cursor:pointer; background:var(--color-bg-surface);
    flex:0 0 clamp(230px,27vw,380px); scroll-snap-align:start;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.07),var(--shadow-sm);
    transition:border-color .3s var(--ease), transform .3s var(--ease), box-shadow .3s var(--ease);
    will-change:transform;}
  .slice:focus-visible{
    border-color:color-mix(in srgb, var(--aurora-green) 55%, transparent);
    box-shadow:inset 0 1px 0 rgba(255,255,255,.07),var(--shadow-lg),0 0 30px -6px var(--accent-glow);
    transform:translateY(-3px);}
  @media(hover:hover){
    .slice:hover{
      border-color:color-mix(in srgb, var(--aurora-green) 55%, transparent);
      box-shadow:inset 0 1px 0 rgba(255,255,255,.07),var(--shadow-lg),0 0 30px -6px var(--accent-glow);
      transform:translateY(-3px);}
  }
  .slice:active{transform:translateY(-2px) scale(.99);}
  .slice .bg{position:absolute; inset:0; z-index:0; background-size:cover; background-position:center 22%;
    background-color:var(--panel-bg); transform:scale(1.04); transform-origin:center; transition:transform .5s var(--ease);}
  .slice:focus-visible .bg{transform:scale(1.08);}
  @media(hover:hover){ .slice:hover .bg{transform:scale(1.08);} }
  /* Ken Burns — the featured slice's artwork drifts and breathes, giving the
     hero a slow cinematic life. Lead only, paused while hovered (so the hover
     read still feels intentional), and off entirely for reduced motion. */
  @media(hover:hover) and (prefers-reduced-motion:no-preference){
    .slice.lead .bg{animation:kenburns 28s ease-in-out infinite alternate;}
    .slice.lead:hover .bg{animation-play-state:paused;}
  }
  @keyframes kenburns{0%{transform:scale(1.05) translate(0,0)} 100%{transform:scale(1.12) translate(-1.4%,-0.9%)}}
  /* aurora light wash over the artwork — theme-tinted, screen-blended */
  .slice::before{content:""; position:absolute; inset:0; z-index:1; pointer-events:none;
    background:radial-gradient(60% 55% at 22% 18%,color-mix(in srgb,var(--aurora-green) 16%,transparent),transparent 62%),
      radial-gradient(45% 50% at 74% 10%,color-mix(in srgb,var(--aurora-teal) 13%,transparent),transparent 58%),
      radial-gradient(55% 45% at 50% 30%,color-mix(in srgb,var(--aurora-purple) 10%,transparent),transparent 62%);
    mix-blend-mode:screen;}
  .slice::after{content:""; position:absolute; inset:0; z-index:1;
    background:linear-gradient(0deg, color-mix(in srgb, var(--void) 96%, transparent) 4%, color-mix(in srgb, var(--void) 40%, transparent) 40%, transparent 74%);}
  .slice .in{position:relative; z-index:2; padding:clamp(16px,2vw,26px); width:100%;}
  .slice .hero-eyebrow{display:inline-block; margin:0 0 6px; padding:3px 11px;
    background:color-mix(in srgb,var(--amber-base) 12%,transparent);
    border:1px solid color-mix(in srgb,var(--amber-base) 34%,transparent);
    color:var(--amber-base); border-radius:var(--radius-pill); font-size:var(--text-3xs); letter-spacing:.16em; text-transform:uppercase; font-weight:var(--fw-bold); white-space:nowrap;}
  .slice.lead{flex:0 0 clamp(320px,37vw,560px);}
  .slice h2{font-family:var(--font-serif); font-weight:400; margin:0;
    line-height:1.02; letter-spacing:-.01em;
    background:linear-gradient(110deg,var(--aurora-green),var(--aurora-teal),var(--aurora-purple),var(--aurora-teal),var(--aurora-green));
    background-size:200% 100%; -webkit-background-clip:text; background-clip:text; color:transparent;
    filter:drop-shadow(0 4px 24px rgba(0,0,0,.55));
    transition:background-position .6s var(--ease-standard);}
  @media(hover:hover){ .slice:hover h2{background-position:100% 50%;} }
  .slice.lead h2{font-size:clamp(30px,3.2vw,50px);}
  .slice.lead h2::after{content:""; display:block; width:64px; height:2px; margin-top:12px;
    background:linear-gradient(90deg,var(--amber-base),var(--aurora-green) 70%,transparent);
    border-radius:var(--radius-pill); box-shadow:0 0 14px var(--gilt-glow);}
  .slice:not(.lead) h2{font-size:clamp(20px,1.8vw,28px);}
  .slice .meta{font-size:var(--text-xs); color:var(--ink-dim); margin-top:9px; display:flex; gap:10px; flex-wrap:wrap; font-variant-numeric:tabular-nums;}
  .slice .meta .r{color:var(--aurora-amber); font-weight:700; text-shadow:0 0 12px color-mix(in srgb,var(--aurora-amber) 45%,transparent);}
  .slice-syn{font-size:var(--text-sm); color:var(--ink-dim); line-height:1.5; margin:10px 0 0; max-width:44ch;
    display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;}
  .slice .go{margin-top:14px; display:inline-flex; opacity:0; transform:translateY(6px); transition:.25s var(--ease);}
  .slice:focus-visible .go{opacity:1; transform:none;}
  @media(hover:hover){ .slice:hover .go{opacity:1; transform:none;} }
  @media(prefers-reduced-motion:reduce){.slice .go{transition:none;} .slice .bg{transition:none;}}
  .hero-play{background:linear-gradient(140deg,var(--aurora-green-hover),var(--aurora-green) 45%,var(--aurora-rose)); color:var(--accent-on); border:none; font-weight:700;
    box-shadow:0 4px 18px -4px color-mix(in srgb,var(--aurora-green) 42%,transparent), inset 0 1px 0 rgba(255,244,230,0.30);}
  .hero-play:hover{filter:brightness(1.05); background:linear-gradient(140deg,var(--aurora-green-hover),var(--aurora-green) 45%,var(--aurora-rose)); border:none; color:var(--accent-on); box-shadow:0 10px 34px -6px color-mix(in srgb,var(--aurora-green) 55%,transparent), inset 0 1px 0 rgba(255,244,230,0.42);}
  .hero-play svg{width:15px; height:15px;}
  /* graceful empty state — no items yet (JF unreachable, empty local dev DB,
     still loading). Keeps the compact-topbar brand line legible on its own
     rather than leaving a jarring blank band. Unchanged from before Stage 2 -
     still its own layout, not a .slice (there's nothing to click through to). */
  .hero-empty{position:relative; z-index:2; flex:1 1 100%; height:100%; display:flex; flex-direction:column;
    align-items:flex-start; justify-content:center; padding:0 0 var(--s6);}
  .hero-title{font-family:var(--font-serif); font-weight:var(--fw-semibold); color:var(--ink);
    font-size:var(--head-page); line-height:var(--lh-tight); letter-spacing:-.01em; margin:0;}
  .hero-empty .wordmark{margin:0 0 4px;
    background:linear-gradient(120deg,var(--aurora-green),var(--aurora-teal),var(--aurora-purple)); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text;}
  /* the lede paragraph only — scoped so it never restyles the .wordmark above it */
  .hero-empty .hero-lede{font-size:var(--t-lead); color:var(--ink-dim); max-width:38ch; margin:14px 0 0; line-height:1.5;}
  @media(max-width:900px){
    .hero{height:clamp(240px, 58vw, 320px);}
    .slice{flex-basis:74vw;} .slice.lead{flex-basis:86vw;}
  }
  @media(max-width:640px){
    .hero{height:220px;}
    .slice, .slice.lead{height:220px; flex-basis:88vw;}
    .slice-syn{display:none;}
  }

  /* section heading row shared by the content rails — a .cap plus an
     optional right-aligned quiet action (see-all / request link) */
  .railhead{display:flex; align-items:baseline; justify-content:space-between; gap:14px; flex-wrap:wrap;
    margin:0 0 var(--heading-gap);}
  /* Rail headings are the page's only wayfinding, and they were rendering at
     the shared .eyebrow treatment: 10.5px, .28em tracking, --muted. That is a
     *label* size being asked to do a *heading* job — at arm's length
     "CONTINUE WATCHING" and "RECENTLY ADDED" read as grey texture rather than
     as structure, so the homepage had no visible skeleton. Promoted to a real
     section title: sentence case at 15px in the ink tone, tracking back to
     near-normal. The eyebrow treatment stays exactly as it was everywhere
     else it is genuinely a micro-label (stat labels, spec keys, badges). */
  .railhead .cap{margin:0; position:relative; font-family:var(--font-serif); font-weight:var(--fw-regular); font-size:var(--head-rail); letter-spacing:-.02em;
    text-transform:none; color:var(--ink);}
  /* Section mark — a custom gilt sparkle (mask-filled so it follows the theme's
     leaf colour), replacing the plain ◆. */
  .railhead .cap::before{content:""; display:inline-block; width:.58em; height:.58em; margin-right:.52em; vertical-align:.04em;
    background:linear-gradient(135deg,var(--amber-hover),var(--amber-base));
    -webkit-mask:var(--spark-mask) center/contain no-repeat; mask:var(--spark-mask) center/contain no-repeat;
    filter:drop-shadow(0 0 6px var(--gilt-glow));}
  /* Signature motion — a leaf-gold hairline draws out from under each rail
     heading as it enters the viewport. Scroll-driven (animation-timeline:view),
     so it runs on the compositor with no JS and no scroll listener; where that
     isn't supported the rule simply sits filled. */
  .railhead .cap::after{content:""; position:absolute; left:0; right:0; bottom:-7px; height:1px;
    background:linear-gradient(to right, var(--gilt-line), transparent 82%);
    transform:scaleX(0); transform-origin:left center;}
  @supports (animation-timeline:view()){
    .railhead .cap::after{animation:cap-rule linear both; animation-timeline:view(); animation-range:entry 8% cover 28%;}
    @keyframes cap-rule{from{transform:scaleX(0);} to{transform:scaleX(1);}}
  }
  @supports not (animation-timeline:view()){ .railhead .cap::after{transform:scaleX(1);} }
  @media(prefers-reduced-motion:reduce){ .railhead .cap::after{animation:none; transform:scaleX(1);} }
  .live{color:var(--aurora-green);}
  .live::before{content:""; display:inline-block; width:7px; height:7px; border-radius:50%; background:var(--aurora-green); margin-right:6px; animation:live-dot 2s ease-in-out infinite;}
  /* NB: this line used to end in `}}}` — one closing brace too many. A stray
     `}` at top level makes the parser drop the NEXT rule while it recovers,
     and the next rule was `.cw{margin:0; display:none;}`. Losing it meant the
     Continue Watching section had no hidden state: loadResume() removes .on
     to hide the rail, but without `display:none` the element fell back to
     display:block, so an empty "Continue watching" heading stayed on the page
     when signed out or when there was nothing to resume. `.cw.on` survived,
     which is why this looked like a JS bug rather than a syntax error. */
  @keyframes live-dot{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--aurora-green) 55%,transparent)}70%{box-shadow:0 0 0 6px transparent}}

  /* Continue Watching — same .pcard/.poster rail language as the discovery
     rails, plus a progress bar baked into the poster. Signed-in-only. */
  .cw{margin:0; display:none;} .cw.on{display:block;}
  .cw .poster{cursor:pointer;}
  .cw-bar{position:absolute; left:0; right:0; bottom:0; height:4px; background:color-mix(in srgb, var(--void) 62%, transparent); z-index:2;}
  .cw-bar i{display:block; height:100%; background:var(--grad-progress);}
  .cw-badge{position:absolute; top:8px; left:8px; z-index:2; font-size:var(--text-3xs); font-weight:700; letter-spacing:.06em;
    text-transform:uppercase; padding:3px 7px; border-radius:var(--r-pill); backdrop-filter:blur(4px);
    -webkit-backdrop-filter:blur(4px); background:color-mix(in srgb, var(--bg) 72%, transparent); border:1px solid var(--line); color:var(--ink-dim);}
  .cw-sub{font-size:var(--text-2xs); color:var(--muted); margin-top:2px; text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
  /* Landscape resume card (design pass): 16:9 still + progress bar + resume line,
     so Continue Watching stops mimicking the portrait browse rails. Flows in the
     same .addgrid rail as the poster cards; reuses .cw-bar for the progress fill. */
  .cwcard{flex:0 0 clamp(248px,30vw,340px); scroll-snap-align:start; cursor:pointer;
    border-radius:var(--card-radius); background:var(--card-bg); border:var(--card-border);
    box-shadow:var(--card-shadow-rest); overflow:hidden; contain:paint;
    transition:transform .4s var(--ease-spring), box-shadow .4s var(--ease-standard), border-color .4s var(--ease-standard);}
  @media(hover:hover){ .cwcard:hover{transform:var(--card-lift-sm); box-shadow:var(--card-shadow-hover); border-color:var(--border-accent);} }
  .cwcard:focus-visible{outline:2px solid var(--focus-ring); outline-offset:2px;}
  .cwcard:active{transform:translateY(-1px) scale(.995);}
  .cw-thumb{position:relative; aspect-ratio:16/9; overflow:hidden;
    background:linear-gradient(160deg,var(--raised-bg),var(--panel-bg));}
  .cw-thumb img{width:100%; height:100%; object-fit:cover; display:block;}
  .cw-thumb .noimg{position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    padding:12px; text-align:center; font-family:var(--font-serif); font-weight:600; color:var(--text-secondary); line-height:1.2;}
  .cw-play{position:absolute; top:50%; left:50%; transform:translate(-50%,-50%) scale(.88);
    width:46px; height:46px; border-radius:50%; display:flex; align-items:center; justify-content:center;
    color:#fff; background:color-mix(in srgb, var(--void) 52%, transparent); border:1px solid rgba(255,255,255,.32);
    opacity:0; pointer-events:none; transition:opacity .25s var(--ease), transform .25s var(--ease-spring);}
  @media(hover:hover){ .cwcard:hover .cw-play{opacity:1; transform:translate(-50%,-50%) scale(1);} }
  .cwcard:focus-visible .cw-play{opacity:1; transform:translate(-50%,-50%) scale(1);}
  .cw-info{padding:10px 13px 12px;}
  .cw-t{font-weight:600; font-size:var(--text-sm); line-height:1.25; letter-spacing:-.01em;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color:var(--text-primary);}
  .cw-meta{font-size:var(--text-2xs); color:var(--text-muted); margin-top:3px; font-variant-numeric:tabular-nums;}
  @media(prefers-reduced-motion:reduce){ .cwcard, .cw-play{transition:none;} }

  /* Nocturne Stage 2: Books moved out of its own homepage disclosure into
     two quiet dashed "cutout" links in the footer — see the HTML comment by
     #footer-books for why two, not the mockup's one. Mockup's own .cutout
     recipe, unmodified. */
  .footer-books{display:inline-flex; gap:20px; flex-wrap:wrap;}
  /* These were dashed-outline pills, which in the middle of a plain text
     footer read as two stray form controls rather than as links. The footer
     is a quiet row of text; its links should be quiet text too. */
  .cutout{display:inline-flex; align-items:center; gap:6px;
    color:var(--ink-dim); font-size:var(--text-sm); font-weight:600;
    border-bottom:1px solid transparent; transition:color .2s var(--ease), border-color .2s var(--ease);}
  .cutout:hover{color:var(--link); border-bottom-color:color-mix(in srgb, var(--link) 55%, transparent);}

  /* "You've been signed out" toast — reuses the .sheet gradient/border/shadow
     recipe (same as the modals) rather than inventing a new surface, just at
     toast scale/position. Bottom-center works at every width without its
     own breakpoint. */
  .toast{position:fixed; z-index:100; left:50%; bottom:24px; transform:translate(-50%,10px);
    display:flex; align-items:center; gap:16px; padding:13px 16px 13px 18px;
    max-width:min(92vw, 420px); border-radius:var(--radius-md);
    background:var(--glass-card-bg); backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px);
    border:1px solid var(--border-strong); box-shadow:var(--shadow-md);
    color:var(--ink-dim); font-size:var(--text-body); font-weight:500;
    opacity:0; pointer-events:none; transition:opacity .25s var(--ease), transform .25s var(--ease);}
  .toast.on{opacity:1; transform:translate(-50%,0); pointer-events:auto;}
  .toast-actions{display:flex; gap:8px; flex:none;}
  @media(prefers-reduced-motion:reduce){.toast{transition:none;}}
  @media(max-width:480px){.toast{left:12px; right:12px; bottom:14px; transform:translateY(10px); max-width:none;}
    .toast.on{transform:translateY(0);}}

  /* ── Now playing (design-review #3: right-sized) ───────────────────────────
     The old glass-in-ring centerpiece (a ~700px neon ring around one compact
     card) was pure decoration — it didn't map to real data (the card already
     carries its own progress bar) and ate a full screen of height. Removed.
     Now playing is a plain card grid under a normal railhead, exactly like
     Continue Watching, so it scales cleanly with no special-casing: 0 sessions
     hides the whole section, 1 renders a single card, 2+ fill the grid. Every
     session is the same .npc card (no primary/secondary split), so a
     family server's concurrent viewers all read identically. */
  .now{margin:var(--section) 0 0; display:none;}
  .now.on{display:block;}
  .cap{margin:0 0 var(--s4);}
  /* auto-fit + 1fr let two sessions stretch to 988px each on a 2040px track —
     a card holding a 72px poster and four short lines, blown out to a metre of
     screen. Capping the column at 520px and packing from the start keeps the
     card a card; three or four concurrent viewers still fill the row. */
  .nowgrid{display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,320px),520px));
    justify-content:start; gap:16px;}
  .npc{position:relative; border-radius:var(--card-radius);
    background:var(--card-bg); backdrop-filter:var(--card-blur); -webkit-backdrop-filter:var(--card-blur);
    border:var(--card-border);
    box-shadow:0 30px 80px -30px rgba(0,0,0,.9), inset 0 1px 0 rgba(255,255,255,.06);
    padding:var(--s4);
    transition:transform .35s var(--ease-standard), box-shadow .35s var(--ease-standard), border-color .35s var(--ease-standard);
    display:flex; gap:16px; align-items:stretch;
    will-change:transform; contain:paint;}
  @media(hover:hover){
    .npc:hover{transform:var(--card-lift-sm); box-shadow:var(--shadow-lg),0 0 30px var(--accent-glow); border-color:var(--border-accent);}
  }
  .np-poster{flex:0 0 72px; width:72px; border-radius:var(--r-md); overflow:hidden; background:var(--raised-2); border:1px solid var(--line-strong); align-self:flex-start;}
  .np-poster img{width:100%; aspect-ratio:2/3; object-fit:cover; display:block;}
  .np-body{flex:1; min-width:0; display:flex; flex-direction:column;}
  .npc.paused{opacity:.6;}
  .npc .row{display:flex; align-items:center; gap:8px;}
  .npc .who{color:var(--accent-2); font-weight:700;}
  /* type badge — Film / TV / Anime */
  .npc .badge{font-size:var(--text-3xs); letter-spacing:.14em; text-transform:uppercase; font-weight:700;
    padding:2px 7px; border-radius:var(--r-pill); border:1px solid var(--line); color:var(--muted); margin-left:auto;}
  .npc .badge.movie{color:var(--accent-2); border-color:color-mix(in srgb, var(--accent-2) 50%, transparent);}
  .npc .badge.tv{color:var(--badge-tv); border-color:color-mix(in srgb, var(--badge-tv) 50%, transparent);}
  .npc .badge.anime{color:var(--badge-anime); border-color:color-mix(in srgb, var(--badge-anime) 50%, transparent);}
  .npc .tag{font-size:var(--text-3xs); letter-spacing:.1em; text-transform:uppercase; color:var(--muted);}
  .npc .ttl{font-size:16.5px; font-weight:700; margin:6px 0 2px; letter-spacing:-.01em;}
  .npc .se{color:var(--accent-2); font-variant-numeric:tabular-nums; font-weight:700;}
  .npc .meta{font-size:var(--text-detail); color:var(--muted);}
  .npc .bar{height:4px; border-radius:4px; background:var(--track); margin-top:12px; overflow:hidden;}
  .npc .bar i{display:block; position:relative; height:100%; background:var(--grad-progress); box-shadow:0 0 12px var(--gm); transition:width .6s var(--ease); overflow:hidden;}
  /* a highlight sweeps across the fill so the bar reads as actively playing */
  .npc .bar i::after{content:""; position:absolute; inset:0;
    background:linear-gradient(90deg, transparent, color-mix(in srgb,var(--text-primary) 50%,transparent), transparent);
    transform:translateX(-100%); animation:barflow 2.6s var(--ease-standard) infinite;}
  @keyframes barflow{0%{transform:translateX(-100%)} 55%,100%{transform:translateX(240%)}}
  @media(prefers-reduced-motion:reduce){.npc .bar i::after{display:none;}}
  /* bottom row — elapsed/remaining time on the left, hot-join on the right
     (mockup's .np-row/.np-join), replacing the old plain .time-then-button
     stack so the join CTA reads as the card's one deliberate action. */
  .np-row{display:flex; align-items:center; gap:12px; margin-top:10px;}
  .np-watchwith{margin-left:auto;}
  /* live pulse next to the "Now playing" cap — the span IS the dot (green
     --ok "on air"), so the generic .live::before dot must be suppressed here
     or the heading shows two clashing dots: a rose ::before and this green
     span side by side. */
  .now .cap .live::before{content:none;}
  .now .cap .live{display:inline-block; width:7px; height:7px; border-radius:50%; background:var(--ok);
    margin-left:9px; vertical-align:middle; box-shadow:0 0 0 0 color-mix(in srgb, var(--ok) 60%, transparent); animation:pulse 2.2s infinite;}
  @keyframes pulse{0%{box-shadow:0 0 0 0 color-mix(in srgb, var(--ok) 55%, transparent);}70%{box-shadow:0 0 0 7px color-mix(in srgb, var(--ok) 0%, transparent);}100%{box-shadow:0 0 0 0 color-mix(in srgb, var(--ok) 0%, transparent);}}
  @media (prefers-reduced-motion: reduce){.now .cap .live{animation:none;}}

  .ledger{display:flex; align-items:flex-end; justify-content:space-between; gap:var(--s6); flex-wrap:wrap;
    margin:var(--section) 0 0; padding:var(--s5) 0 0; border-top:1px solid var(--line);}
  .ledger .big{font-family:var(--font-serif); font-weight:600; font-size:clamp(46px,7.4vw,72px);
    line-height:1; letter-spacing:-.02em; font-variant-numeric:tabular-nums;}
  .ledger .big .u{font-family:var(--sans); font-size:15px; font-weight:500; color:var(--muted); margin-left:12px;}
  .spark{display:block; margin-top:14px;}
  .stats{display:flex; gap:34px; flex-wrap:wrap;}
  .stat .n{font-size:22px; font-weight:600; font-variant-numeric:tabular-nums; letter-spacing:-.01em;}
  .stat .n.zero{color:var(--ink-dim); font-weight:400;}
  .stat .l{font-size:var(--text-xs); color:var(--muted); text-transform:uppercase; letter-spacing:.12em; margin-top:3px;}

  /* watch time by genre — quiet bronze bars, shown only with data */
  .genre{margin:var(--section) 0 0; display:none;}
  .genre.on{display:block;}
  .grow{display:grid; grid-template-columns:110px 1fr 52px; align-items:center; gap:14px; padding:7px 0;}
  .grow .gl{font-size:var(--text-body); color:var(--ink-dim);}
  .grow .gt{height:6px; border-radius:6px; background:var(--track); overflow:hidden;}
  .grow .gt i{display:block; height:100%; background:var(--grad-progress); opacity:.9;}
  .grow .gv{font-size:var(--text-detail); color:var(--muted); text-align:right; font-variant-numeric:tabular-nums;}

  /* The footer used to sit on --w-read (1080px) while every other block on the
     page sat on --w-wide (2040px). On a 2560px screen that put its rule and
     its text 480px inside the content above it — three different left edges on
     one page (hero at 0, content at the wide inset, footer at the read inset),
     which is the kind of thing that reads as "unfinished" without anyone being
     able to say why. Same track as the content now. */
  /* The bottom padding is a safe area, not decoration. Two controls are fixed
     to the bottom corners of the viewport — the "?" shortcuts button at
     left:20px and Back to top at right:24px — and both are ~40px tall, so they
     occupy the lowest ~60px of the screen permanently. Scrolled to the end,
     that band landed on top of the footer: the "?" button sat over "Sign in
     once at…", hiding the first word, and Back to top sat over the wordmark on
     the right. Reserving the band below the text means the buttons float over
     empty space instead of content. */
  footer{position:relative; z-index:2; max-width:var(--w-wide); margin:var(--section) auto 0;
    padding:var(--s6) var(--gutter) 76px;
    border-top:1px solid var(--border-subtle); color:var(--text-muted); font-size:var(--text-sm);}
  footer a{color:var(--text-secondary); text-decoration:none; transition:color .18s var(--ease);}
  footer a:hover{color:var(--accent-hover);}
  .foot-cols{display:grid; grid-template-columns:1.7fr 1fr 1fr; gap:30px 44px; align-items:start;}
  @media(max-width:720px){ .foot-cols{grid-template-columns:1fr 1fr;} .foot-brand{grid-column:1/-1;} }
  @media(max-width:440px){ .foot-cols{grid-template-columns:1fr;} }
  .foot-col{display:flex; flex-direction:column; gap:9px; align-items:flex-start;}
  .foot-h{font-size:var(--text-2xs); letter-spacing:.16em; text-transform:uppercase; color:var(--text-muted); font-weight:700; margin:0 0 4px;}
  .foot-brand .foot-wordmark{font-size:var(--text-lg); margin:0 0 8px; color:var(--text-primary);}
  .foot-tag{margin:0; color:var(--text-secondary); max-width:36ch; line-height:1.5; font-size:var(--text-body);}
  .foot-whisper{margin:14px 0 0; font-size:var(--text-xs); color:var(--text-muted); font-variant-numeric:tabular-nums; letter-spacing:.01em;}
  .foot-whisper b{color:var(--amber-base); font-weight:700;}
  .foot-base{display:flex; justify-content:space-between; align-items:center; gap:16px; flex-wrap:wrap;
    margin-top:30px; padding-top:18px; border-top:1px solid var(--border-subtle); font-size:var(--text-xs); color:var(--text-muted);}
  .foot-mark{font-family:var(--font-serif); letter-spacing:.02em; color:var(--text-secondary);}

  /* Gilt through-line — the poster's champagne inner-hairline, carried onto every
     modal/instrument sheet so "gilt = a considered frame" reads as one language
     across the site rather than a poster-only flourish. Theme-reactive: --gilt-soft
     derives from each theme's --aurora-amber (silver under Nightshade's chrome,
     gold under Bronze, etc.). */
  .sheet{box-shadow:var(--shadow-modal), inset 0 0 0 1px var(--gilt-soft);}
  .rise{opacity:0; transform:translateY(14px); animation:rise .9s var(--ease) forwards;}
  @keyframes rise{to{opacity:1; transform:none}}
  .d1{animation-delay:.05s}.d2{animation-delay:.16s}.d3{animation-delay:.28s}
  /* Staggered card entrance — applied via JS inline style delay per card */
  .card-enter{opacity:0; transform:translateY(20px); animation:card-enter .5s var(--ease-enter) forwards;}
  @keyframes card-enter{to{opacity:1; transform:none}}
  /* Page crossfade — applied during home↔catalog transitions */
  .view-enter{animation:view-in .3s var(--ease-enter) both;}
  .view-exit{animation:view-out .2s var(--ease-exit) both;}
  @keyframes view-in{from{opacity:0; transform:translateY(8px)} to{opacity:1; transform:none}}
  @keyframes view-out{to{opacity:0; transform:translateY(-8px)}}
  a:focus-visible{outline:2px solid var(--focus-ring); outline-offset:3px; border-radius:6px;}
  @media(prefers-reduced-motion:reduce){.rise,.card-enter,.view-enter,.view-exit{animation:none!important;opacity:1;transform:none}}
  /* server specs — quiet "for the curious" text chart + live storage bar */
  .chart{max-width:640px; margin-top:4px;}
  .crow{display:grid; grid-template-columns:104px 1fr; gap:16px; align-items:baseline;
    padding:8px 0; border-bottom:1px solid color-mix(in srgb, var(--line-c) 6%, transparent); font-size:var(--text-body);}
  .crow .ck{font-weight:700;}
  .crow .cv{color:var(--ink-dim); font-variant-numeric:tabular-nums;}
  .storage{max-width:640px; margin:16px 0 0; padding:8px 0 0;}
  .storage .top{display:flex; justify-content:space-between; align-items:baseline; gap:12px;}
  .storage .ck{font-weight:700;}
  .storage .sfree{color:var(--ink-dim); font-size:var(--text-sm); font-variant-numeric:tabular-nums;}
  .sbar{height:8px; border-radius:8px; background:var(--track); border:1px solid var(--line); overflow:hidden; margin-top:9px;}
  .sbar i{display:block; height:100%; width:0; background:var(--grad-progress); transition:width .9s var(--ease);}
  .roadmap{font-size:var(--text-sm); color:var(--muted); margin:22px 0 0; line-height:1.55;}
  .roadmap .ck{font-weight:700; margin-right:var(--s3);}
  /* service status — Statuspage-style board (banner + per-service bar timeline) */
  .uptime{margin:var(--section) 0 0}
  .uptop{display:flex; align-items:center; gap:14px; flex-wrap:wrap; margin-bottom:16px}
  .uptop .cap{margin:0}
  .chip{display:inline-flex; align-items:center; gap:7px; padding:5px 12px; border-radius:var(--r-pill);
    border:1px solid var(--line); background:var(--bg-soft); font-size:var(--text-caption); font-weight:600; letter-spacing:.02em; color:var(--ink-dim)}
  .chip .sd{width:7px; height:7px; border-radius:50%; background:var(--muted); flex:none}
  .chip.live{color:var(--ok-dim); border-color:color-mix(in srgb, var(--ok) 35%, transparent); position:relative} .chip.live .sd{background:var(--ok); animation:spulse 2.2s infinite}
  /* rotating conic ring on the live status pill — a slow bead of green light
     travels the border, reading as a heartbeat. @property makes the angle
     animatable; the two-layer mask paints only the 1px border ring (not the
     fill). Used sparingly — this single "live" pill is the one earned spot. */
  @property --ring-a{syntax:'<angle>'; inherits:false; initial-value:0deg;}
  .chip.live::before{content:""; position:absolute; inset:-1px; border-radius:inherit; padding:1px; pointer-events:none;
    background:conic-gradient(from var(--ring-a), transparent 0 62%, color-mix(in srgb, var(--ok) 85%, transparent) 80%, var(--ok-dim) 88%, transparent 96%);
    -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite:xor; mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite:exclude;
    animation:ringspin 3.6s linear infinite;}
  @keyframes ringspin{to{--ring-a:360deg;}}
  @media(prefers-reduced-motion:reduce){.chip.live::before{animation:none; opacity:0;}}
  .chip.partial{color:var(--warn-dim); border-color:color-mix(in srgb, var(--warn) 40%, transparent)} .chip.partial .sd{background:var(--warn)}
  .chip.down{color:var(--down-dim); border-color:color-mix(in srgb, var(--down) 40%, transparent)} .chip.down .sd{background:var(--down)}
  .board{background:color-mix(in srgb, var(--bg) 35%, transparent); border:1px solid var(--line); border-radius:var(--r-lg); padding:6px 18px}
  .srow{display:grid; grid-template-columns:150px 1fr 62px; align-items:center; gap:16px; padding:13px 0;
    border-bottom:1px solid color-mix(in srgb, var(--line-c) 7%, transparent)}
  .srow:last-child{border-bottom:none}
  .sname{font-size:var(--text-base); font-weight:600; color:var(--ink); display:flex; align-items:center; gap:9px; min-width:0}
  .sname .dot{width:8px; height:8px; border-radius:50%; background:var(--muted); flex:none}
  .sname .dot.up{background:var(--ok)} .sname .dot.down{background:var(--down)} .sname .dot.deg{background:var(--warn)}
  .sname .sstate{color:var(--ink-dim); letter-spacing:.04em; margin-left:2px}
  .sbars{display:flex; gap:3px; align-items:stretch; height:32px}
  .sbars i{flex:1 1 0; min-width:2px; border-radius:2px; background:color-mix(in srgb, var(--line-c) 6%, transparent)}
  .sbars i.up{background:var(--ok); opacity:.82} .sbars i.up:hover{opacity:1}
  .sbars i.deg{background:var(--warn); opacity:.9}
  .sbars i.down{background:var(--down); opacity:.95}
  .sup{font-size:var(--text-detail); color:var(--ink-dim); text-align:right; font-variant-numeric:tabular-nums; font-weight:600}
  .upaxis{display:flex; justify-content:space-between; font-size:var(--text-2xs); color:color-mix(in srgb, var(--line-c) 45%, transparent); margin-top:8px; letter-spacing:.02em}
  .upaxis .axmid{color:var(--muted)}
  @keyframes spulse{0%{box-shadow:0 0 0 0 color-mix(in srgb, var(--ok) 50%, transparent)}70%{box-shadow:0 0 0 5px color-mix(in srgb, var(--ok) 0%, transparent)}100%{box-shadow:0 0 0 0 color-mix(in srgb, var(--ok) 0%, transparent)}}
  @media(prefers-reduced-motion:reduce){.chip .sd{animation:none}}
  @media(max-width:640px){.srow{grid-template-columns:104px 1fr 50px; gap:10px} .sname .sstate{display:none} .sbars{height:24px; gap:2px}}
  /* Now a sibling of .np-watchwith inside .np-row (see above), not the only
     thing in its own row - gap instead of space-between so it doesn't try
     to claim the whole row width for itself. */
  .npc .time{display:flex; gap:10px; font-size:var(--text-2xs); color:var(--muted); font-variant-numeric:tabular-nums; letter-spacing:.02em;}
  /* just added — poster rail with aurora synopsis on hover */
  .added{margin:var(--section) 0 0; display:none;} .added.on{display:block;}
  .addgrid{display:flex; gap:22px; overflow-x:auto; overflow-y:visible;
    /* edge-fade: posters dissolve at the scroll edges instead of hard-cutting
       under the scrollbar. Horizontal-only gradient (solid full height), so
       the poster hover-lift into the top padding is never clipped.

       The mask is positioned against the SCROLLPORT, not the content, so its
       leading 22px sat permanently over the first card — the rail's first
       poster and its caption were always rendered half-faded ("The Bear" read
       as a ghosted "he Bear", every rail, at every width). The fix is to give
       the scroll container 22px of its own leading/trailing padding and pull
       it back out with an equal negative margin: the fade region now covers
       empty padding at rest, and only starts eating content once you have
       actually scrolled — which is the effect it was always meant to have.
       Net inline size is unchanged, and the negative margin is absorbed by
       .wrap's own 24px gutter, so nothing widens the page. */
    padding:22px 26px 20px; margin-inline:-26px; scroll-padding-inline:26px;
    scroll-snap-type:x proximity;
    /* Gallery feel: no scrollbar chrome on the rails. Wheel/trackpad, the arrow
       buttons, drag and keyboard all still scroll — the bar was the only thing
       that read as "utility" rather than "shelf". */
    scrollbar-width:none;
    -webkit-mask-image:linear-gradient(90deg, transparent 0, #000 26px, #000 calc(100% - 26px), transparent 100%);
    mask-image:linear-gradient(90deg, transparent 0, #000 26px, #000 calc(100% - 26px), transparent 100%);}
  .addgrid::-webkit-scrollbar{display:none;}
  /* Fluid, not a fixed 150px. In a 2040px track a four-item Continue Watching
     rail used to leave ~68% of the row empty with four postage-stamp posters
     stranded on the left; the art now grows with the track instead (--card-w
     clamps 132→208px) so a wide screen gets bigger posters rather than more
     void. The rails still scroll — this only changes how much of the track a
     card claims. */
  .pcard{flex:0 0 var(--card-w); width:var(--card-w); scroll-snap-align:start;
    border-radius:var(--card-radius); overflow:hidden;
    background:var(--card-bg); backdrop-filter:var(--card-blur); -webkit-backdrop-filter:var(--card-blur);
    border:var(--card-border);
    box-shadow:var(--card-shadow-rest);
    transition:transform .5s var(--ease-spring), box-shadow .45s var(--ease-standard), border-color .45s var(--ease-standard),
      opacity .4s var(--ease-standard);
    contain:paint;}
  /* PERF — layer promotion is scoped to the rail the pointer is actually in.
     `will-change:transform` used to sit on EVERY .pcard permanently, so a
     catalog page held one composited layer per card (100+ on a full grid),
     each costing GPU memory and a compositor entry whether or not anything
     was moving. Promoting on rail-enter is early enough that the lift is
     still smooth on the first card you touch, and an idle page now holds
     zero speculative layers. `filter` also left the transition list — see
     the spotlight rule below. */
  .addgrid:hover .pcard, .addgrid:focus-within .pcard{will-change:transform;}
  /* Gated: a latched :hover on touch leaves one tile permanently raised above
     its neighbours (it also sets z-index:5, so it overlaps them). */
  @media(hover:hover){
    .pcard:hover{transform:var(--card-lift) scale(1.045); box-shadow:var(--card-shadow-hover); border-color:var(--border-accent); z-index:5;}
    /* Spotlight — the gallery move: hovering one tile in a rail eases the rest
       back (dimmer, desaturated, a hair smaller) so the one you're looking at
       is the only thing lit. :has() scopes it to the hovered rail only. */
    /* PERF — the dim was `opacity:.5 + filter:saturate(.82) brightness(.82)`.
       The filter half forced a separate filter render pass on every non-hovered
       card, re-rastered across the whole .4s transition, and `:has()` re-runs
       that on every card boundary the pointer crosses — the dominant source of
       rail-scrub jank. Over a near-black field `opacity:.5` already reads as
       both darker and less saturated, so the filter was buying ~nothing for a
       lot. Opacity + transform are compositor-only; the look survives. */
    .addgrid:has(.pcard:hover) .pcard:not(:hover){ opacity:.5; transform:scale(.965); }
  }
  /* Glint — a single band of light crosses the card as it lifts, then settles
     into the gilt matte frame on the poster. One-shot, hover-gated. */
  .pcard::after{content:""; position:absolute; inset:0; z-index:6; pointer-events:none; border-radius:inherit;
    background:linear-gradient(105deg, transparent 42%, rgba(255,244,230,0.13) 50%, transparent 58%);
    background-size:250% 100%; background-position:150% 0; opacity:0;}
  @media(hover:hover){ .pcard:hover::after{opacity:1; animation:card-glint .85s var(--ease-standard);} }
  @keyframes card-glint{from{background-position:150% 0} to{background-position:-60% 0}}
  @media(prefers-reduced-motion:reduce){ .pcard::after{display:none;} }
  .pcard:active{transform:translateY(-2px) scale(.99);}
  @media(max-width:640px){.addgrid{gap:15px;}}
  .poster{position:relative; aspect-ratio:2/3; border-radius:var(--r-md); overflow:hidden;
    background:linear-gradient(160deg,var(--raised-bg),var(--panel-bg)); border:1px solid var(--line);
    box-shadow:var(--shadow-sm),inset 0 1px 0 rgba(255,244,230,0.07);}
  /* skeleton shimmer — a soft light sweep across the placeholder gradient
     while the poster art loads. Sits at z0 behind the image (z1), so once the
     opaque <img> paints it's covered. Bounded iteration count (not infinite)
     caps the cost on long catalog scrolls; it settles transparent after.

     This rule did nothing at all until now. The third colour stop was
     `rgba(var(--star),.06)`, and --star resolves to a hex (#AFC4E6), so the
     stop expanded to the nonsense `rgba(#AFC4E6,.06)`. That is invalid at
     computed-value time, which drops the WHOLE `background` shorthand — the
     element computed `background-image:none` while the shimmer animation
     dutifully ran on nothing. (Verified in-browser: getComputedStyle(el,
     '::before').backgroundImage === "none".) rgba() never accepted a var()
     holding a full colour; color-mix() is the construct that does. */
  .poster::before{content:""; position:absolute; inset:0; z-index:0;
    background:linear-gradient(100deg, transparent 28%, color-mix(in srgb, var(--line-c) 14%, transparent) 48%, color-mix(in srgb, var(--star) 7%, transparent) 52%, transparent 72%);
    background-size:220% 100%; animation:shimmer 1.5s ease-in-out 5 both;}
  .poster img{position:relative; z-index:1; width:100%; height:100%; object-fit:cover; display:block;}
  .poster .noimg{position:absolute; inset:0; z-index:1; display:flex; align-items:center; justify-content:center; padding:14px;
    text-align:center; font-family:var(--font-serif); font-size:var(--text-lg); font-weight:var(--fw-semibold); color:var(--text-secondary); line-height:1.18; letter-spacing:-.01em;
    background:linear-gradient(160deg,var(--color-bg-elevated),var(--color-bg-surface));}
  @keyframes shimmer{from{background-position:120% 0}to{background-position:-120% 0}}
  @media(prefers-reduced-motion:reduce){.poster::before{animation:none; opacity:0;}}
  /* Gilt matte — a champagne inner hairline that frames the art as the card
     lifts. Sits above the synopsis overlay (z3) so it reads as a frame, not a
     tint. Paired with the card's rose border, the poster mats in rose-gold. */
  .poster::after{content:""; position:absolute; inset:0; z-index:3; pointer-events:none; border-radius:inherit;
    box-shadow:inset 0 0 0 1px transparent; transition:box-shadow .32s var(--ease-standard);}
  @media(hover:hover){ .pcard:hover .poster::after{box-shadow:inset 0 0 0 1px var(--gilt-line);} }
  .poster .syn{position:absolute; inset:0; z-index:2; opacity:0; transition:opacity .28s var(--ease); display:flex; align-items:flex-end;
    background:linear-gradient(to top, var(--color-bg-deepest) 0%, color-mix(in srgb, var(--color-bg-deepest) 92%, transparent) 28%, transparent 62%);}
  /* Focus stays ungated — that's the keyboard path, and it works on any
     device. The hover half is gated because on a touch screen :hover latches
     on tap and stays latched until you tap something else, so the synopsis
     overlay would sit on top of the last poster you touched. */
  .poster:focus-within .syn{opacity:1;}
  @media(hover:hover){ .poster:hover .syn{opacity:1;} }
  .syn-in{padding:16px 14px;}
  .syn-t{font-family:var(--font-serif); font-weight:600; font-size:var(--text-lg); line-height:1.14; letter-spacing:-.01em; color:var(--text-primary);}
  .syn-g{font-weight:700; margin:6px 0 7px;}
  .syn-o{font-size:var(--text-xs); color:var(--text-secondary); line-height:1.45; margin:0;
    display:-webkit-box; -webkit-line-clamp:5; -webkit-box-orient:vertical; overflow:hidden;}
  .syn-more{font-weight:700; margin-top:var(--sp-2);}
  /* Poster caption — centered under the art, and titles wrap to two lines
     instead of clipping mid-word on a single line (the old white-space:nowrap
     + ellipsis turned "Your Friend, Nate Bargatze" into "Your Friend, Nate Ba…").
     List view (a row layout, styled below) resets this back to left-aligned. */
  .pt{font-size:var(--text-sm); font-weight:600; letter-spacing:-.01em; margin-top:10px; line-height:1.3;
    text-align:center; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;}
  .py{font-size:var(--text-2xs); color:var(--muted); margin-top:3px; text-align:center; font-variant-numeric:tabular-nums;}
  /* In the wrapping catalog grid, reserve two lines so a one-line title next to
     a two-line one doesn't stagger the rows. Rails are single rows, so they
     don't need it (and stay tighter without it). */
  .libgrid:not(.list-mode) .pt{min-height:calc(2 * 1.3em);}
  /* request prompt under the doors */
  /* the Request affordance next to the Popular rail — the whole phrase is
     now one link (was a <p> with a plain-text lead-in + inline <a>) */
  .reqline{font-size:var(--text-body); color:var(--link); text-decoration:none; font-weight:600; letter-spacing:.01em; transition:color .18s var(--ease);}
  .reqline:hover{color:var(--link-2);}
  /* ── Nocturne Stage 2: "the instrument" — the old always-on-the-homepage
     <details class="sys"> (library stats, service status, hardware spec)
     moved into a ⌘K-launched overlay (mockup's own "Open the instrument"
     command: "Live server telemetry, storage & uptime — hidden by
     default"). This is a container swap only — every id inside
     (#total/#stats/#spark.../#genre/#uptime/#statuschip/#statusboard/
     #spec-chart/#stg-txt/#stg-bar/#spec-roadmap) is unchanged and still
     targeted by the exact same loaders (loadStatic/loadHostSpec/loadStatus/
     loadUptime), which still run unconditionally at boot exactly as before
     — they populate the DOM whether or not this overlay is currently open,
     same as the old <details> populated itself while collapsed. Only the
     open/close mechanism changed, from <details>/<summary> to the shared
     .overlay/.sheet modal recipe (openInstrument()/closeInstrument()). */
  #instrumentlightbox{z-index:80;}
  .instrumentbox{max-width:860px; width:100%; max-height:88vh; padding:30px 28px 8px;
    display:flex; flex-direction:column; overflow:hidden;}
  .instrument-title{font-size:var(--head-sheet); margin:0 0 4px;}
  .instrument-sub{font-size:var(--text-sm); color:var(--muted); margin:0 0 var(--sp-5);}
  .instrument-scroll{overflow-y:auto; overflow-x:hidden; padding-bottom:26px; margin:0 -6px; padding-left:6px; padding-right:6px;}
  .instrument-scroll > .ledger{margin-top:0; padding-top:0; border-top:none;}
  .instrument-scroll .chart{margin-top:18px;}

  /* ── Nocturne Stage 2: ⌘K command palette ─────────────────────────────────
     Adapted from the mockup's .cmdk/.palette almost verbatim - hub's own
     --ease/tokens substituted for the mockup's --e/bare hex, .btn reused for
     the esc hint's sibling affordances rather than inventing new button
     styling. See openCmdK()/cmdkRender() etc. below for the real wiring
     (search reuses /api/library/items, same endpoint the catalog view's own
     search box already calls). */
  .cmdk{position:fixed; inset:0; z-index:110; display:none; align-items:flex-start; justify-content:center;
    padding-top:12vh; background:rgba(0,0,0,.55); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px);}
  .cmdk:not(.open):not(.on){display:none;}
  .cmdk.on, .cmdk.open{display:flex; animation:cmdk-fade .18s var(--ease);}
  @keyframes cmdk-fade{from{opacity:0}}
  .palette{width:min(94vw,640px); max-height:76vh; display:flex; flex-direction:column; border-radius:var(--radius-xl); overflow:hidden;
    background:var(--glass-card-bg); backdrop-filter:var(--glass-card-blur); -webkit-backdrop-filter:var(--glass-card-blur);
    border:1px solid var(--border-strong); box-shadow:var(--shadow-modal); animation:cmdk-pop .2s var(--ease);}
  @keyframes cmdk-pop{from{opacity:0; transform:translateY(8px) scale(.99)}}
  @keyframes cmdk-in{from{opacity:0; transform:translateY(4px) scale(.97)}}
  @media(prefers-reduced-motion:reduce){.cmdk.on{animation:none;} .palette{animation:none;}}
  .p-input{display:flex; align-items:center; gap:12px; padding:16px 18px; border-bottom:1px solid var(--line); flex:none;}
  .p-input svg{width:18px; height:18px; color:var(--muted); flex:none;}
  .p-input input{flex:1; min-width:0; background:none; border:none; outline:none; color:var(--ink); font:400 16px/1.2 inherit;}
  .p-input input::placeholder{color:var(--muted);}
  .p-input .esc{font-size:var(--text-2xs); color:var(--muted); border:1px solid var(--line); border-radius:5px; padding:3px 7px; flex:none;}
  .p-scroll{overflow:auto; overflow-x:hidden;}
  .p-group{padding:8px;}
  .gl{font-size:var(--text-3xs); letter-spacing:.2em; text-transform:uppercase; color:var(--muted); font-weight:700; padding:8px 10px 4px;}
  .p-item{display:flex; align-items:center; gap:12px; padding:10px; border-radius:var(--r-md); cursor:pointer;}
  .p-item:hover, .p-item.sel{background:color-mix(in srgb, var(--line-c) 8%, transparent);}
  .p-item.sel{box-shadow:inset 2px 0 0 0 var(--cyan);}
  .p-ico{width:30px; height:30px; border-radius:var(--r-sm); flex:none; display:grid; place-items:center; font-size:var(--text-base);
    background:var(--raised-2); border:1px solid var(--line); color:var(--cyan);}
  .p-ico.m{color:var(--magenta);}
  .p-main{flex:1; min-width:0;}
  .p-t{font-size:var(--text-base); color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
  .p-s{font-size:var(--text-xs); color:var(--muted);}
  .p-item .hint{font-size:var(--text-2xs); color:var(--muted); flex:none;}
  .p-empty{padding:22px 14px; text-align:center; font-size:var(--text-sm); color:var(--muted);}
  .sc{display:grid; grid-template-columns:1fr 1fr; gap:8px; padding:6px 10px 12px;}
  .sc-col{border:1px solid var(--line); border-radius:var(--r-md); padding:10px 12px; background:color-mix(in srgb, var(--line-c) 3%, transparent);}
  .sc-col h4{margin:0 0 8px; font-size:var(--text-2xs); letter-spacing:.14em; text-transform:uppercase; color:var(--ink-dim); font-weight:700;}
  .sc-row{display:flex; align-items:center; justify-content:space-between; padding:5px 0; font-size:var(--text-detail); color:var(--ink-dim);}
  .sc-row .keys{display:flex; gap:3px;}
  .sc-row .keys b{font:700 10.5px/1 inherit; background:color-mix(in srgb, var(--line-c) 10%, transparent); border:1px solid var(--line);
    border-radius:5px; padding:3px 5px; color:var(--platinum-bright);}
  @media(max-width:640px){.sc{grid-template-columns:1fr;}}

  /* discovery rail group (just-added + popular) — a touch tighter than the
     full --section rhythm since they're one conceptual "browse more" block,
     not two unrelated sections. */
  .discover{display:flex; flex-direction:column; gap:var(--s6); margin:var(--section) 0 0;}
  .discover .added{margin:0;}
  /* Personal rails share the .added rail language exactly — they differ in
     where their contents come from, not in how they look, and a member should
     not be able to tell "the server picked this for you" from "the server
     picked this for everyone" by the styling. */
  #personal{display:flex; flex-direction:column; gap:var(--s6); margin:var(--section) 0 0;}
  #personal:empty{display:none; margin:0;}
  .personal-rail{margin:0;}
  .poster{cursor:pointer;}
  .syn-more{font-weight:700; margin-top:var(--s2);}
  /* ── title detail ─────────────────────────────────────────────────────────
     A column, not a two-up card: full-bleed backdrop header, then a scrolling
     body carrying synopsis, credits, the season/episode list and a "More like
     this" rail. The old 780px poster-beside-a-paragraph layout had nowhere to
     put any of that, which is why episodes were only reachable from inside the
     player and why "what else is like this" did not exist at all. */
  .lbox{overflow:hidden; max-width:1040px; width:100%; max-height:90vh;
    display:flex; flex-direction:column;}
  /* The backdrop header. Its height is a ratio of the modal's own width so the
     art keeps a consistent crop from 640px to 1040px. */
  .lb-hero{position:relative; flex:none; isolation:isolate;
    min-height:clamp(220px, 30vw, 340px); display:flex; align-items:flex-end;}
  .lb-art{position:absolute; inset:0; z-index:0; background-size:cover; background-position:center 20%;}
  /* No backdrop on file: the poster stands in, blown up and blurred, so the
     header is never an empty band. */
  .lb-art--poster{background-position:center 30%; filter:blur(22px) saturate(1.15); transform:scale(1.15);}
  /* One scrim doing two jobs: bottom-up so the copy sits on near-solid ground,
     and a left-side wash so a bright right-hand backdrop can still carry a
     legible title. */
  .lb-hero::after{content:""; position:absolute; inset:0; z-index:1; background:
    linear-gradient(0deg, var(--modal-grad-2) 2%, color-mix(in srgb, var(--modal-grad-2) 82%, transparent) 34%, transparent 88%),
    linear-gradient(90deg, color-mix(in srgb, var(--modal-grad-2) 72%, transparent), transparent 62%);}
  .lb-heroin{position:relative; z-index:2; display:flex; align-items:flex-end; gap:var(--s5);
    padding:var(--s5) clamp(20px,3vw,32px) clamp(18px,2.4vw,26px); width:100%;}
  .lb-poster{flex:none; width:clamp(96px,11vw,132px); border-radius:var(--r-md); overflow:hidden;
    border:1px solid var(--line-strong); box-shadow:var(--shadow-md); background:var(--panel-bg);}
  .lb-poster img{width:100%; aspect-ratio:2/3; object-fit:cover; display:block;}
  .lb-herocopy{min-width:0; flex:1;}
  .lb-kicker{margin:0 0 var(--s2); color:var(--ink-dim);}
  @media(max-width:640px){
    .lbox{max-height:94vh;}
    .lb-heroin{gap:var(--s4); padding:var(--s4);}
    .lb-poster{width:84px;}
  }
  .lbody{padding:clamp(20px,2.4vw,28px) clamp(20px,3vw,32px) 30px; overflow:auto; flex:1; min-height:0;}
  .ltitle{font-family:var(--font-serif); font-weight:var(--fw-semibold); font-size:var(--head-modal); line-height:var(--lh-tight);
    letter-spacing:-.01em; margin:0 30px 0 0; color:var(--ink); text-shadow:0 3px 20px rgba(0,0,0,.55);}
  .lmeta{font-size:var(--text-sm); color:var(--muted); margin-top:8px; font-variant-numeric:tabular-nums;}
  .lgen{font-weight:700; margin-top:var(--s4);}
  .lrate{display:flex; gap:8px; flex-wrap:wrap; margin-top:14px;}
  /* section headings inside the detail body — same promoted treatment as the
     homepage rail headings, so "Episodes" and "More like this" read as
     structure rather than as micro-labels */
  .lb-caphead{font-size:15px; font-weight:600; letter-spacing:-.005em;
    text-transform:none; color:var(--ink); margin:var(--s6) 0 var(--s4);}
  /* Each section renders nothing at all when it has nothing to show, so a
     movie never gets an "Episodes" heading and an obscure title never gets an
     empty "More like this". */
  .lb-eps:empty, .lb-similar:empty{display:none;}
  .lb-eps .pv-seasons{margin-bottom:var(--s4);}
  .lb-eps .pv-eplist{max-height:none;}
  /* the similar rail is a plain scroller here — no edge mask, since it sits
     inside a padded panel rather than against the page gutter */
  .lb-similar .addgrid{margin-inline:0; padding-left:0; padding-right:0;
    -webkit-mask-image:none; mask-image:none;}
  .rbadge{display:inline-flex; align-items:center; gap:6px; border:1px solid var(--line); border-radius:var(--r-sm);
    padding:5px 11px; font-size:var(--text-sm); font-weight:700; color:var(--ink-dim); font-variant-numeric:tabular-nums;}
  .rbadge .star{color:var(--accent-2);}
  .rbadge .rl{font-size:var(--text-3xs); font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--muted);}
  .lsyn{font-size:var(--text-base); color:var(--ink-dim); line-height:1.6; margin:18px 0 0;}
  .lcredits{font-size:var(--text-detail); color:var(--muted); margin-top:16px; line-height:1.6;}
  .lcredits b{color:var(--ink-dim); font-weight:700; text-transform:uppercase; font-size:var(--text-3xs); letter-spacing:.08em; margin-right:5px;}
  .llinks{display:flex; gap:10px; flex-wrap:wrap; margin-top:22px;}
  a.pcard{text-decoration:none; color:var(--ink);}
  .pstatus{position:absolute; top:8px; right:8px; z-index:2; font-size:var(--text-3xs); font-weight:700; letter-spacing:.05em;
    padding:3px 8px; border-radius:var(--r-pill); -webkit-backdrop-filter:blur(4px); backdrop-filter:blur(4px);
    background:color-mix(in srgb, var(--bg) 72%, transparent); border:1px solid var(--line); color:var(--ink-dim);}
  .pstatus.want{color:var(--link); border-color:color-mix(in srgb, var(--link) 50%, transparent);}
  .pstatus.req{color:var(--warn-dim); border-color:color-mix(in srgb, var(--warn) 45%, transparent);}
  /* Nocturne Stage 2: request breadcrumb chip - CSS only, deliberately
     unused for now. See loadPopular()'s comment below for why: the real
     data to drive it (a genuine 4-step per-request pipeline with a live
     download %) doesn't exist anywhere in this stack today, and faking it
     would be a worse outcome than not showing it. Ready to wire the moment
     that data source exists - same class names/shape as the mockup. */
  .reqchip{display:inline-flex; align-items:center; padding:5px 12px; border-radius:var(--r-pill);
    border:1px solid var(--line-strong); background:color-mix(in srgb, var(--line-c) 5%, transparent); font-size:var(--text-caption);}
  .reqchip .lab{color:var(--ink-dim); font-weight:700; margin-right:10px;}
  .reqchip .crumb{display:flex; align-items:center; gap:7px; color:var(--muted); font-weight:600;}
  .reqchip .crumb .on{color:var(--cyan);} .reqchip .done{color:var(--ok);} .reqchip .sep{opacity:.4;} .reqchip .s{opacity:.5;}
  /* catalog view — browse/search/detail, entered via the Catalog door (?view=catalog) */
  /* Anchor offset for the sticky header, on the SCROLLER rather than per-id.
     The old rule was `#homeview, #catalogview{scroll-margin-top:72px}`: two
     hardcoded pixels values on two elements. Both parts were wrong. 72px is
     the desktop header height, but the header wraps to 112px on phones (more
     now that .topright can wrap), so every anchor jump landed underneath it;
     and naming two ids left the rails — #cw, #added, #popular, every
     personal rail — on the default 0, so openCatalog()'s scrollIntoView and
     the skip link both parked their target behind the header.

     scroll-padding-top on the scrolling element covers every anchor and every
     scrollIntoView at once. --topbar-h is measured from the real element by
     app.js and kept current through a ResizeObserver, so it tracks wrapping
     and font-size changes instead of guessing. The fallback matches the
     desktop header for the pre-JS frame. */
  html{scroll-padding-top:calc(var(--topbar-h, 72px) + 12px);}
  #catalogview{padding:var(--sp-12) 0 40px;}
  .libhead{display:flex; align-items:baseline; justify-content:space-between; gap:16px; flex-wrap:wrap; margin:0 0 26px;}
  .libtitle{font-family:var(--font-serif); font-weight:var(--fw-semibold); font-size:var(--head-page);
    line-height:var(--lh-tight); letter-spacing:-.005em; margin:0;}
  .libtitle{background:linear-gradient(120deg,var(--aurora-green),var(--aurora-teal),var(--aurora-purple),var(--aurora-green)); background-size:200% 100%; -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; animation:tagline-sheen 20s linear infinite;}
  .libback{font-size:var(--text-sm); font-weight:600; color:var(--link); text-decoration:none; white-space:nowrap; transition:color .18s var(--ease);}
  .libback:hover{color:var(--link-2);}
  /* type tabs — All / Movies / TV Shows / Anime — pill-style, not underline */
  .libtabs{display:flex; gap:var(--s2); flex-wrap:wrap; margin:0 0 var(--s4);}
  .libtab{background:none; border:1px solid var(--border-default); border-radius:var(--radius-pill); color:var(--muted);
    font-family:inherit; font-size:var(--text-base); font-weight:600; letter-spacing:.01em; padding:8px 16px;
    cursor:pointer; transition:color .2s var(--ease), border-color .2s var(--ease), background .2s var(--ease), transform .2s var(--ease);}
  .libtab:hover{color:var(--ink-dim); border-color:var(--border-strong);}
  .libtab.on{background:var(--aurora-green); color:var(--color-bg-deepest); border-color:transparent;}
  .libtab:focus-visible{outline:2px solid var(--focus-ring); outline-offset:2px;}
  /* View density toggle — Grid (default) / List / Compact */
  .view-toggle{display:flex; gap:2px; margin-left:var(--sp-2);}
  .view-toggle button{background:none; border:1px solid var(--border-default); color:var(--text-muted); border-radius:var(--radius-sm); padding:4px 8px; cursor:pointer; font-size:var(--text-xs); transition:all .15s var(--ease);}
  .view-toggle button:hover{color:var(--text-primary); border-color:var(--border-strong);}
  .view-toggle button[aria-pressed="true"]{background:var(--accent-base); color:var(--color-bg-deepest); border-color:transparent;}
  /* List mode — full-width cards with inline metadata */
  .libgrid.list-mode{grid-template-columns:1fr; gap:var(--sp-2);}
  .libgrid.list-mode .pcard{flex-direction:row; width:100%; display:flex; gap:var(--sp-4); align-items:center; padding:var(--sp-3);}
  .libgrid.list-mode .poster{width:60px; flex:none; aspect-ratio:2/3;}
  .libgrid.list-mode .pt{font-size:var(--text-base); margin:0; white-space:normal; text-align:left; display:block; -webkit-line-clamp:unset; min-height:0;}
  .libgrid.list-mode .py{font-size:var(--text-xs); text-align:left;}
  /* Compact mode — smaller cards, tighter grid */
  .libgrid.compact-mode{grid-template-columns:repeat(auto-fill,minmax(100px,1fr)); gap:var(--sp-2);}
  .libgrid.compact-mode .pt{font-size:var(--text-2xs);}
  .libgrid.compact-mode .py{display:none;}
  .libcontrols{display:flex; gap:var(--s3); flex-wrap:wrap; margin:0 0 var(--s6);}
  /* .field (defined once, above) supplies the shared input/select look;
     this context only adds the row-flex sizing behavior specific to the
     library filter bar — the login form (below) never opts into this, which
     is what permanently fixes the old sizing quirk instead of patching it
     per-form. */
  /* Capped. Unbounded flex-grow stretched this to ~1060px on a 1440 screen —
     a search box wider than any query anyone types, which reads as a layout
     accident rather than a control. */
  .libcontrols .searchfield{flex:1 1 240px; min-width:160px; max-width:460px;}
  .libcontrols select.field{flex:0 0 auto; cursor:pointer;}
  /* same fluid card measure as the rails, so a poster is the same size whether
     you meet it in Recently Added or in the catalog grid */
  .libgrid{display:grid; grid-template-columns:repeat(auto-fill,minmax(clamp(140px,12vw,240px),1fr)); gap:var(--sp-4);}
  /* pushed to the end of the filter row, so it reads as a result OF the
     filters rather than as another control */
  .libcount{margin:0 0 0 auto; align-self:center; font-size:var(--t-meta); color:var(--muted);
    font-variant-numeric:tabular-nums; white-space:nowrap;}
  @media(max-width:640px){ .libcount{margin:var(--s1) 0 0; width:100%;} }
  .libstatus{font-size:var(--text-detail); color:var(--muted); text-align:center; margin:26px 0 0; min-height:1em;
    display:flex; flex-direction:column; align-items:center; gap:var(--s2);}
  /* empty state — a statement, a reason, and a way out, rather than the bare
     "No titles match." dead end this used to be */
  .libempty-t{font-family:var(--font-serif); font-size:20px; font-weight:600; color:var(--ink); line-height:1.2;}
  .libempty-s{font-size:var(--text-body); color:var(--muted);}
  .libempty-b{margin-top:var(--s2);}
  .libsentinel{height:1px;}
  @media(max-width:640px){
    .libcontrols{gap:var(--s2);} .libcontrols .searchfield{flex:1 1 100%;} .libcontrols select.field{flex:1 1 auto;}
    .libgrid{grid-template-columns:repeat(auto-fill,minmax(112px,1fr)); gap:16px 12px;}
    /* phones: the clamp's own floor (132px) would give 2 columns with a wide
       gutter; 112px keeps three across at 375px, which is what the rail does. */
  }
  /* Tablet — 768px: restore search, wider catalog grid, more hero height */
  @media(min-width:641px) and (max-width:900px){
    .search{display:flex;}
    .libgrid{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));}
    .hero{height:clamp(280px,48vw,380px);}
  }
  /* login/logout control (Phase 2a). Actual playback is a separate, later
     phase — see Media-Forest handoff docs. */
  .lplay{margin-top:var(--s4); display:flex; align-items:center; gap:var(--s3); flex-wrap:wrap;}
  /* ── request chooser ─────────────────────────────────────────────────────
     One sheet, one job: pick what kind of thing you want and get sent to the
     right backend. Replaces five separate affordances (three door pills, the
     Popular rail's link, the footer's "Request books") pointing at two
     different apps. Reuses .sheet for the surface and the same icon-tile
     language the old doors used, at card scale. */
  .requestbox{max-width:460px; width:100%; padding:30px 28px 28px;}
  .requestbox .ltitle{font-size:var(--head-sheet); margin:0 30px 4px 0;}
  .requestbox .lsub{font-size:var(--text-sm); color:var(--muted); margin:0 0 var(--sp-5);}
  .reqgrid{display:flex; flex-direction:column; gap:var(--s2);}
  .reqcard{display:flex; align-items:center; gap:var(--s4); padding:var(--s3) var(--s4);
    border:1px solid var(--line); border-radius:var(--r-md); background:var(--bg-soft);
    color:var(--ink); text-decoration:none;
    transition:border-color .2s var(--ease), background .2s var(--ease), box-shadow .2s var(--ease), transform .2s var(--ease);}
  .reqcard:hover, .reqcard:focus-visible{outline:2px solid var(--focus-ring); outline-offset:2px; color:var(--ink);
    border-color:color-mix(in srgb, var(--cyan) 42%, transparent);
    background:color-mix(in srgb, var(--cyan) 6%, transparent);
    box-shadow:0 0 22px -12px var(--gc);}
  .reqcard:active{transform:translateY(0) scale(.98);}
  .reqcard-i{width:38px; height:38px; flex:none; display:grid; place-items:center; border-radius:var(--r-md);
    background:color-mix(in srgb, var(--accent) 13%, transparent); color:var(--accent-2);}
  .reqcard-i svg{width:20px; height:20px;}
  .reqcard-b{flex:1; min-width:0; display:flex; flex-direction:column; gap:2px;}
  .reqcard-t{font-size:var(--t-body); font-weight:600; line-height:1.2;}
  .reqcard-n{font-size:var(--t-cap); color:var(--muted);}
  .reqcard-go{flex:none; color:var(--muted); transition:color .2s var(--ease), transform .2s var(--ease);}
  .reqcard:hover .reqcard-go{color:var(--cyan); transform:translateX(3px);}
  @media(prefers-reduced-motion:reduce){.reqcard-go{transition:none;}}
  /* the Popular rail's request link is a <button> now (it opens the chooser
     rather than navigating), so it needs the anchor-ish reset the .reqline
     rule below assumed it already had */
  button.reqline{background:none; border:none; padding:0; cursor:pointer; font-family:inherit;}

  /* login modal — simpler single-column card, sibling overlay to #lightbox.
     .sheet (shared) supplies background/border/radius/shadow; this is just
     the size + internal layout. */
  .loginbox{max-width:360px; width:100%; padding:30px 28px 28px;}
  .loginbox .ltitle{font-size:var(--head-sheet); margin:0 0 4px;}
  .loginbox .lsub{font-size:var(--text-sm); color:var(--muted); margin:0 0 var(--sp-5);}
  /* Its own standalone column layout — deliberately NOT a .libcontrols
     instance, so it never inherits that context's row-flex input sizing
     (the permanent fix for the old sizing quirk, vs. patching it per-form). */
  .loginform{display:flex; flex-direction:column; align-items:stretch; gap:var(--s3);}
  .loginform .field{width:100%; min-width:0;}
  .loginform button{align-self:flex-start; margin-top:2px;}

  /* ── UI pass D: watch-together lobby ──────────────────────────────────
     Real surface (host/join, invite code, participant list) backed by the
     live SSE sync layer (projects/together.py); the shared-player row is a
     read-only mirror of the host-driven playback state. Sibling overlay to
     the other three (.lb recipe via .overlay/.sheet), so it stacks/dismisses
     the same way. */
  /* highest of the four overlays — openable from inside the player (90), so
     it needs to stack above it the same way the player stacks above the
     detail modal (80). */
  #togetherlightbox{z-index:95;}
  .togetherbox{max-width:420px; width:100%; padding:30px 28px 28px;}
  .together-title{font-size:var(--head-sheet); margin:0 0 4px;}
  .together-sub{font-size:var(--text-sm); color:var(--muted); margin:0 0 var(--sp-5);}
  .together-error{font-size:var(--text-sm); color:var(--warn); margin:0 0 16px; line-height:1.5;}
  .together-join{display:flex; gap:8px; margin:0 0 4px;}
  .together-join .field{flex:1; text-transform:uppercase; letter-spacing:.06em;}
  .together-hostbtn{width:100%; margin-top:14px;}
  .together-code-row{display:flex; gap:10px; align-items:center; margin:0 0 18px;}
  .together-code{flex:1; font-size:20px; font-weight:700; letter-spacing:.18em; color:var(--ink);
    background:var(--bg-soft); border:1px solid var(--line); border-radius:var(--r-md);
    padding:10px 14px; text-align:center; font-variant-numeric:tabular-nums;}
  .together-people{margin:0 0 18px;}
  .tp-list{display:flex; flex-wrap:wrap; gap:8px; margin-top:8px;}
  .tp-chip{display:inline-flex; align-items:center; gap:6px; padding:6px 12px; border-radius:var(--r-pill);
    border:1px solid var(--line); background:var(--bg-soft); font-size:var(--text-detail); font-weight:600; color:var(--ink-dim);}
  .tp-chip.host{color:var(--link); border-color:color-mix(in srgb, var(--link) 40%, transparent);}
  .tp-chip .host-badge{font-size:9px; letter-spacing:.08em; text-transform:uppercase; color:var(--accent-2); font-weight:700;}
  .together-empty{font-size:var(--text-detail); color:var(--muted); font-style:italic; margin:8px 0 0;}
  .together-player{border:1px solid var(--line); border-radius:var(--r-md); padding:14px;
    background:var(--panel-bg); margin:0 0 18px;}
  .tplay-title{font-size:var(--text-body); font-weight:600; color:var(--ink);}
  /* Read-only by design, not inert: sync is real (see together.py's
     playback broadcast + _pvApplyRemoteCommand()), but the actual controls
     live in the real player — only the host's ever do anything, and this
     panel is a status mirror, not a second control surface. pointer-events
     stays none so it never becomes a confusing dead click target. */
  .tplay-controls{display:flex; align-items:center; gap:10px; margin-top:10px; pointer-events:none;}
  .tplay-controls .pv-track{flex:1;}
  .together-leavebtn{width:100%;}

  /* player overlay (Phase 2b) — stacks above the detail modal (z-index 90 >
     80) so closing it returns to the still-open detail modal. .sheet
     supplies border/radius/shadow (background stays opaque black here,
     intentionally, for video letterboxing — the one deliberate deviation
     from the shared card gradient). Every control-bar color below is an
     existing CSS custom property so all 6 themes inherit with zero extra
     work. */
  #playerlightbox{z-index:90;}
  /* container-type on the box (not the overlay) so the control bar can react
     to the PLAYER'S OWN rendered width (a narrowed desktop window, not just
     small viewports) — see the @container rules below the control bar. */
  .pv-box{background:#000; max-width:960px; width:100%; max-height:92vh; overflow:hidden;
    box-shadow:var(--shadow-lg); container-type:inline-size; container-name:player;}
  .pv-stage{position:relative; width:100%; aspect-ratio:16/9; background:#000;}
  .pv-stage video{width:100%; height:100%; display:block; background:#000; object-fit:contain;}
  /* video-context override of .sheet-close: needs to stay legible over
     arbitrary video frames rather than the themed panel chip, so it keeps
     its own translucent-black treatment; radius/size/transition still
     inherit from the shared component. */
  .pv-close{position:absolute; top:10px; right:12px; z-index:6; width:32px; height:32px;
    background:rgba(0,0,0,.55); border:1px solid rgba(255,255,255,.18); color:#fff;}
  .pv-close:hover{background:rgba(0,0,0,.75); color:var(--link); border-color:rgba(255,255,255,.18);}
  .pv-backep{position:absolute; top:10px; left:12px; z-index:6; background:rgba(0,0,0,.55);
    border:1px solid rgba(255,255,255,.18); color:#fff; border-radius:var(--r-sm); padding:6px 11px;
    font-size:var(--text-caption); font-weight:600; cursor:pointer;}
  .pv-backep:hover{background:rgba(0,0,0,.75); color:var(--link);}
  .pv-title{position:absolute; top:12px; left:14px; right:56px; z-index:5; color:#fff; font-size:var(--text-base);
    font-weight:600; text-shadow:0 1px 6px rgba(0,0,0,.8); pointer-events:none;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
  .pv-stage.has-backep .pv-title{top:46px;}
  .pv-loading{position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    color:var(--ink-dim); font-size:var(--text-sm); z-index:4; background:rgba(0,0,0,.35); text-align:center; padding:20px;}
  /* the class's display:flex outranks the UA [hidden]{display:none}, so hidden
     alone never hid it — this restores the hide (was: loading stuck on-screen). */
  .pv-loading[hidden]{display:none;}
  .pv-controls{position:absolute; left:0; right:0; bottom:0; z-index:5; padding:10px 14px 12px;
    background:linear-gradient(to top, rgba(0,0,0,.82), rgba(0,0,0,0) 100%);
    display:flex; flex-direction:column; gap:8px;
    opacity:1; transition:opacity .25s var(--ease);}
  /* auto-hide (JS toggles .controls-hidden on #pv-stage during playback after
     ~3s idle, and tapping the stage — not a control — toggles it directly);
     the title/back-episode chrome hide with it so a hidden-controls state
     really reads as "just the video". */
  .pv-stage.controls-hidden .pv-controls,
  .pv-stage.controls-hidden .pv-title,
  .pv-stage.controls-hidden .pv-backep,
  .pv-stage.controls-hidden .pv-close{opacity:0; pointer-events:none;}
  .pv-close, .pv-backep, .pv-title{transition:opacity .25s var(--ease);}
  @media(prefers-reduced-motion:reduce){.pv-controls,.pv-close,.pv-backep,.pv-title{transition:none;}}
  .pv-row{display:flex; align-items:center; gap:10px;}
  /* Explicit box, not padding-derived: the old rule (icon + 4px padding, no
     width/height) shrink-wrapped to a ~26px square on desktop — under every
     usability minimum (WCAG 2.5.8 wants >=24px; Apple/Google guidance wants
     >=36-44px) and reportedly needed a precise click to register at all.
     Setting width/height directly makes the FULL box the hit target
     (border-box, so padding no longer eats into it) regardless of how the
     .btn/.pv-btn cascade order shakes out — measured via injected DOM probes
     (elementFromPoint across the box) to confirm the whole area, not just
     the icon glyph, is clickable both before and after this change. Mobile's
     44px media-query bump below still applies on top of this. */
  .pv-btn{display:flex; align-items:center; justify-content:center; width:36px; height:36px;
    background:color-mix(in srgb, var(--text-primary) 6%, transparent); border:1px solid var(--border-strong); border-radius:50%;
    transition:opacity .2s var(--ease), color .2s var(--ease), background .2s var(--ease);}
  .pv-btn:hover{opacity:1; color:var(--link); background:color-mix(in srgb, var(--text-primary) 12%, transparent);}
  /* pointer-events:none so a click always resolves to the button, never its
     inner <svg>/<path> — keeps e.target === the button for the menu open/close
     logic (see the outside-click handler in initPlayerControls). */
  .pv-btn svg{width:18px; height:18px; pointer-events:none;}
  .pv-time{font-size:var(--text-caption); color:#e8e8f0; font-variant-numeric:tabular-nums; white-space:nowrap;}
  /* generous invisible hit-area around the thin visual rail — already
     touch-friendly (16px tall) at every size; widened further on touch
     viewports below. */
  .pv-track{position:relative; flex:1; height:16px; display:flex; align-items:center; cursor:pointer; touch-action:none;}
  .pv-rail{position:relative; width:100%; height:4px; border-radius:4px; background:rgba(255,255,255,.16);}
  /* buffered-ahead indicator — neutral light grey (YouTube-style), clearly
     brighter than the rail and dimmer than the themed played bar. */
  .pv-buffered{position:absolute; left:0; top:0; height:100%; border-radius:4px; width:0;
    background:rgba(255,255,255,.42);}
  .pv-played{position:absolute; left:0; top:0; height:100%; border-radius:4px; width:0; background:var(--accent);}
  .pv-knob{position:absolute; top:50%; width:12px; height:12px; border-radius:50%; background:var(--link);
    transform:translate(-50%,-50%); left:0%; box-shadow:0 1px 4px rgba(0,0,0,.6);}
  .pv-vol{width:70px; accent-color:var(--accent); cursor:pointer;}
  .pv-spacer{flex:1;}
  .pv-menu{position:absolute; bottom:52px; right:14px; z-index:8; background:var(--panel-bg);
    border:1px solid var(--line); border-radius:var(--r-md); padding:6px; min-width:170px; display:none;
    box-shadow:var(--shadow-md); max-height:220px; overflow:auto;}
  .pv-menu.on{display:block;}
  /* the control bar reacts to the PLAYER BOX'S OWN width (container query,
     not a viewport breakpoint) — a desktop window narrowed to a sliver gets
     the same simplified bar a phone does. Volume collapses behind the mute
     button (still fully controllable — click/tap mute to toggle silence);
     the range input just stops competing for space. */
  @container player (max-width:480px){
    .pv-vol{display:none;}
    .pv-row{gap:6px;}
    .pv-time{font-size:10.5px;}
  }
  .pv-menu button{display:block; width:100%; text-align:left; background:none; border:none; padding:7px 10px;
    border-radius:var(--r-sm); color:var(--ink-dim); font-size:var(--text-detail); cursor:pointer; font-family:inherit;}
  .pv-menu button:hover{background:color-mix(in srgb, var(--line-c) 12%, transparent); color:var(--ink);}
  .pv-menu button[aria-pressed="true"]{color:var(--link); font-weight:600;}
  .pv-menu button:disabled{opacity:.5; cursor:default;}
  .pv-menu button:disabled:hover{background:none; color:var(--ink-dim);}
  /* quality-menu preset: primary label + a muted resolution/bitrate hint on
     its own line (e.g. "1080p" / "High · 8 Mbps"). */
  .pv-menu-sub{display:block; font-size:var(--text-2xs); font-weight:400; color:var(--ink-dim); margin-top:1px;}
  .pv-menu button[aria-pressed="true"] .pv-menu-sub{color:var(--ink-dim);}
  /* series episode picker — shares the catalog grid's card visual language */
  .pv-picker{padding:20px 22px 22px; max-height:92vh; overflow:auto; background:var(--panel-bg);}
  .pv-seasons{display:flex; gap:8px; flex-wrap:wrap; margin:0 0 16px;}
  .pv-season-btn{background:var(--bg-soft); border:1px solid var(--line); color:var(--ink-dim); border-radius:var(--r-pill);
    padding:6px 14px; font-size:var(--text-detail); font-weight:600; cursor:pointer; transition:.2s var(--ease); font-family:inherit;}
  .pv-season-btn:hover{color:var(--ink); border-color:color-mix(in srgb, var(--accent) 50%, transparent);}
  .pv-season-btn[aria-pressed="true"]{color:var(--bg); background:var(--accent); border-color:var(--accent);}
  .pv-eplist{display:flex; flex-direction:column; gap:10px;}
  .pv-ep{display:flex; gap:12px; padding:10px; border-radius:var(--r-md); border:1px solid var(--line);
    background:var(--bg-soft); cursor:pointer; transition:background .2s var(--ease), border-color .2s var(--ease);
    text-align:left; width:100%; font-family:inherit; color:inherit;}
  .pv-ep:hover{background:var(--raised-bg); border-color:color-mix(in srgb, var(--accent) 40%, transparent);}
  .pv-ep-thumb{flex:0 0 120px; width:120px; aspect-ratio:16/9; border-radius:var(--r-sm); overflow:hidden;
    background:var(--panel-bg); border:1px solid var(--line);}
  .pv-ep-thumb img{width:100%; height:100%; object-fit:cover; display:block;}
  .pv-ep-body{flex:1; min-width:0;}
  .pv-ep-title{font-size:var(--text-body); font-weight:600; color:var(--ink);}
  .pv-ep-title .se{color:var(--accent-2); margin-right:6px; font-variant-numeric:tabular-nums;}
  .pv-ep-meta{font-size:var(--text-caption); color:var(--muted); margin-top:3px;}
  .pv-ep-ov{font-size:var(--text-caption); color:var(--ink-dim); margin-top:5px; line-height:1.45;
    display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;}
  .pv-ep-resume{height:3px; border-radius:3px; background:var(--track); margin-top:8px; overflow:hidden;}
  .pv-ep-resume i{display:block; height:100%; background:var(--accent);}
  /* ── mobile video player (UI pass B) ──────────────────────────────────────
     The weakest responsive spot before this pass. Below 640px the player
     goes near-fullscreen: dvh/dvw so mobile browser chrome (the address bar
     that grows/shrinks on scroll) never clips it the way vh/vw would, no
     rounded corners/gutter, touch-sized (>=44px) controls, a taller scrub
     hit-area, and the subtitle menu becomes a bottom sheet instead of a
     small floating panel. Landscape phones get the same treatment — dvh
     still fills the actual usable height either orientation. */
  @media(max-width:640px){
    #playerlightbox.overlay{padding:0;}
    .pv-box{width:100dvw; height:100dvh; max-width:100dvw; max-height:100dvh;
      border-radius:0; display:flex; flex-direction:column;}
    .pv-stage{flex:1; width:100%; height:100%; aspect-ratio:unset; min-height:0;}
    .pv-picker{max-height:100dvh; height:100dvh;}
    /* every control gets a real >=44px touch target, not just a bigger icon */
    .pv-btn{width:44px; height:44px;}
    .pv-close, .pv-backep{width:44px; height:44px;}
    .pv-close{top:max(8px, env(safe-area-inset-top)); right:max(8px, env(safe-area-inset-right));}
    .pv-backep{top:max(8px, env(safe-area-inset-top)); left:max(8px, env(safe-area-inset-left));
      display:flex; align-items:center;}
    .pv-controls{padding:8px max(10px, env(safe-area-inset-right)) max(10px, env(safe-area-inset-bottom))
      max(10px, env(safe-area-inset-left));}
    /* taller invisible hit-area around the same thin visual rail */
    .pv-track{height:44px;}
    .pv-time{font-size:var(--text-detail);}
    /* subtitle menu -> bottom sheet: full-width, anchored to the bottom edge,
       rounded only on top, scrollable if the list is long. */
    .pv-menu{position:fixed; left:0; right:0; bottom:0; top:auto; z-index:12;
      border-radius:var(--r-lg) var(--r-lg) 0 0; padding:10px max(10px, env(safe-area-inset-left));
      min-width:0; max-height:min(60dvh, 420px);
      padding-bottom:max(10px, env(safe-area-inset-bottom));}
    .pv-menu button{padding:12px 12px; font-size:var(--text-base);}
    .pv-ep-thumb{flex:0 0 88px; width:88px;}
  }
  /* landscape phones specifically (short viewport, wide) — same near-
     fullscreen treatment; dvh already fills the true usable height. */
  @media(max-height:500px) and (orientation:landscape){
    #playerlightbox.overlay{padding:0;}
    .pv-box{width:100dvw; height:100dvh; max-width:100dvw; max-height:100dvh; border-radius:0;}
    .pv-btn{width:44px; height:44px;}
  }

  /* ══ envelope-pushing pass — modern-CSS flourishes ═════════════════════════
     All progressive enhancements: guarded by @supports / @media so any
     browser without the feature falls back to the plain (already-good) look,
     and every animation yields to prefers-reduced-motion. */

  /* ── rail scroll affordance ──────────────────────────────────────────────
     The hero strip and the three poster rails are horizontally scrollable and
     advertised it with nothing but a thin scrollbar and a 22px edge fade. On a
     trackpad that is fine; with a mouse there is no affordance at all, and
     from the keyboard there was no way to page a rail without first tabbing
     into a card and letting focus drag the scroll along.

     This started as ::scroll-button(), which is the right long-term answer —
     the browser generates the button, labels it, scrolls by a page and
     auto-disables at the ends, with no script at all. Chrome 148 PARSES the
     selector (CSS.supports('selector(::scroll-button(right))') is true) but
     does not generate the boxes: probing elementFromPoint at the button's own
     computed position hits the poster underneath it. So the whole block was
     styling that could never paint. Real buttons instead — ~30 lines in
     initRailNav(), genuinely focusable, genuinely labelled, and verifiable.
     Swap back to the pseudo-element when it actually ships. */
  .railnav{position:absolute; top:50%; transform:translateY(-50%); z-index:4;
    width:46px; height:46px; padding:0; border-radius:50%;
    display:flex; align-items:center; justify-content:center;
    border:1px solid var(--gilt-line); color:var(--ink);
    background:color-mix(in srgb, var(--color-bg-elevated) 78%, transparent);
    backdrop-filter:blur(16px) saturate(150%); -webkit-backdrop-filter:blur(16px) saturate(150%);
    box-shadow:var(--shadow-md), inset 0 1px 0 rgba(255,244,230,0.12); cursor:pointer;
    opacity:.5; transition:opacity .25s var(--ease), border-color .25s var(--ease), box-shadow .25s var(--ease), transform .25s var(--ease-spring);}
  .railnav svg{width:19px; height:19px; pointer-events:none;}
  /* 26px in, not flush: these rails carry a 22px edge-fade mask and anything
     inside it would be painted half-transparent. */
  .railnav--prev{left:26px;} .railnav--next{right:26px;}
  /* the hero is full-bleed, so its buttons ride the content edge instead of
     being stranded against the viewport */
  .railshell .railnav--prev{left:max(26px, calc(var(--bleed-inset) - 20px));}
  .railshell .railnav--next{right:max(26px, calc(var(--bleed-inset) - 20px));}
  /* Centre on the ARTWORK, not on the section box. `top:50%` of the section
     includes the heading above and the two caption lines below, which parks
     the arrow low over the title text. The poster's own centre is a knowable
     distance from the section top — 24px heading + 16px heading margin + 4px
     rail padding, then half a poster — and a poster is card-w × 3/2, so half
     of one is card-w × 0.75. Deterministic, and it tracks --card-w as the
     cards resize. */
  .cw .railnav, .added .railnav{top:calc(44px + var(--card-w) * .75);}
  /* Quiet, but never invisible. A hover-only reveal is the common pattern and
     the wrong one here: the arrow is the only signal that a rail HAS more in
     it, so hiding it until you already suspect that defeats the purpose (and
     it is dead weight for anyone who never hovers). Present at .55, full on
     approach or keyboard focus. */
  .railshell:hover .railnav, .cw:hover .railnav, .added:hover .railnav,
  .railnav:focus-visible, .railnav:hover, .railshell:focus-within .railnav,
  .cw:focus-within .railnav, .added:focus-within .railnav{opacity:1;}
  .railnav:hover{border-color:var(--gilt); transform:translateY(-50%) scale(1.08);
    box-shadow:var(--shadow-lg), 0 0 22px -6px var(--gilt-glow), inset 0 1px 0 rgba(255,244,230,0.2);}
  .railnav:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
  /* at a rail's end the button is genuinely inert — hide it rather than leave
     a dead control sitting there */
  .railnav[hidden]{display:none;}
  .railshell{position:relative;}
  .cw, .added{position:relative;}
  @media(prefers-reduced-motion:reduce){.railnav{transition:none;}}
  /* touch already has the gesture; arrows would only cover art */
  @media(pointer:coarse){.railnav{display:none !important;}}

  /* ── entry/exit animation for popovers and sheets ────────────────────────
     .menu had a one-way @keyframes menu-pop on open and nothing on close, so
     header popovers appeared with intent and then simply vanished. The overlay
     sheets had neither. @starting-style + transition-behavior:allow-discrete
     is what makes `display:none <-> flex` animatable at all, which is why this
     could never be done with a plain transition before. */
  @supports (transition-behavior:allow-discrete){
    @media(prefers-reduced-motion:no-preference){
      .overlay{
        transition:opacity .2s var(--ease), display .2s allow-discrete;
        opacity:0;
      }
      .overlay.on{ opacity:1; }
      @starting-style{ .overlay.on{ opacity:0; } }
      .overlay .sheet{
        transition:opacity .22s var(--ease), transform .22s var(--ease);
        opacity:1; transform:none;
      }
      @starting-style{ .overlay.on .sheet{ opacity:0; transform:translateY(10px) scale(.985); } }
    }
  }

  /* Headings set their own line breaks rather than leaving a one-word orphan
     on the last line; body copy avoids single-word last lines. Both are pure
     text-layout hints — unsupported browsers ignore them entirely. */
  h1, .wordmark, .gate-title, .libtitle, .ltitle, .slice h2, .hero-title,
  .instrument-title, .together-title{ text-wrap:balance; }
  .lsyn, .gate-sub, .hero-lede, .slice-syn, .pref-note, .roadmap{ text-wrap:pretty; }

  /* metallic sheen — a rose-gold specular band drifts across the wordmark and
     the gate headline. Built from the palette's ivory + champagne + rose, so
     the wordmark reads as ivory with a warm leaf of light passing over it.
     Child elements (the h1 .bar divider) keep their own color because
     background-clip:text only recolors the element's OWN text. */
  @supports ((-webkit-background-clip:text) or (background-clip:text)){
    .sheen{
      background:linear-gradient(100deg,
        var(--ink) 28%, var(--amber-base) 44%, var(--accent-base) 50%, var(--amber-base) 56%, var(--ink) 72%);
      background-size:220% 100%;
      -webkit-background-clip:text; background-clip:text; color:transparent;
      animation:sheen 7s ease-in-out infinite;}
    @keyframes sheen{0%{background-position:120% 0} 50%{background-position:-20% 0} 100%{background-position:120% 0}}
    @media(prefers-reduced-motion:reduce){.sheen{animation:none; background-position:50% 0;}}
  }

  /* scroll-driven reveal — sections rise + fade AS THEY ENTER the viewport
     (native scroll timeline, no JS/observer). Elements above the fold at load
     are already past the entry range, so they simply show. No-support
     browsers get no base opacity:0, so content is always visible there. */
  @supports (animation-timeline:view()){
    @media(prefers-reduced-motion:no-preference){
      .reveal{animation:reveal-rise linear both; animation-timeline:view(); animation-range:entry 4% cover 22%;}
      @keyframes reveal-rise{from{opacity:0; transform:translateY(28px)} to{opacity:1; transform:none}}
    }
  }

  /* View Transitions — the poster→detail morph and catalog cross-fade. Tune
     the shared-element timing; keep the whole-page root fade quick so opening
     the catalog reads as a soft dissolve, not a slow wipe. */
  @media(prefers-reduced-motion:no-preference){
    ::view-transition-group(film-poster){animation-duration:.42s; animation-timing-function:var(--ease);}
    ::view-transition-old(film-poster),::view-transition-new(film-poster){
      animation-duration:.42s; animation-timing-function:var(--ease); mix-blend-mode:normal;}
    ::view-transition-old(root),::view-transition-new(root){animation-duration:.28s;}
  }

  /* ── Accessibility pass ───────────────────────────────────────────────────
     1. Skip link. With the header, the three doors and several rails, a
        keyboard user otherwise tabs through ~20 controls before reaching any
        content. Visually hidden until focused, then pinned top-left over
        everything (z-index clears the gate at 1200). */
  .skip-link{position:fixed; top:-100px; left:var(--s3); z-index:1300;
    padding:10px 18px; border-radius:var(--r-md);
    background:var(--neon); color:var(--void); font-weight:700; font-size:var(--t-meta);
    text-decoration:none; box-shadow:var(--shadow-md); transition:top .16s var(--ease);}
  .skip-link:focus{top:var(--s3); color:var(--void);}
  @media(prefers-reduced-motion:reduce){.skip-link{transition:none;}}

  /* 2. Touch targets. The compact desktop sizing is deliberate and stays as
        it is — this only applies where the primary input is a finger, which
        is exactly the case the 44px guidance (iOS HIG / Material 48dp, and
        WCAG 2.5.5) is about. Audited at 375px, the offenders were the theme
        icon button (32x32), both gate form fields and the Sign in button
        (~33-39px tall), the header buttons, and the per-door "Request ->"
        links. `pointer:coarse` rather than a width breakpoint so a touch
        laptop or tablet in landscape gets the same treatment.
        min-height (not height) so nothing that is already taller shrinks. */
  @media (pointer: coarse){
    .btn, .lbtn, .field, .libtab,
    .navlink, .menu-item, .search, .reqline, .reqcard, .skip-link{
      min-height:44px;
    }
    /* Inline-flex centers the label once these grow past their padding. */
    .reqline{display:inline-flex; align-items:center;}
    .iconbtn, .avatar{width:44px; height:44px;}
    /* The icon inside keeps its original visual size — only the hit area grew. */
    .iconbtn svg{width:20px; height:20px;}
    .libtab{padding-top:10px; padding-bottom:10px;}
    /* Player transport row: same reasoning, and these sit over video where a
       mis-tap is especially annoying. */
    .pv-btn{width:44px; height:44px;}
  }

/* ── sign-in gate ──────────────────────────────────────────────────────────
   Was a second inline <style> in the body. Moved here so index.html carries no
   CSS at all; the markup it styles (#gate) is unchanged. The gate is the
   public "homepage": opaque, themed, covering the whole app while signed out
   (body.locked). Enforcement is app.py's /api/* session middleware — this is
   only the UX half. */
body.locked{overflow:hidden;}
    /* The gate is the only screen most visitors ever see before signing in, and
       it was painting an OPAQUE background over the whole app — which meant the
       decorative stage (dot grid, perspective floor, canopy silhouette) that
       carries the entire brand was hidden behind exactly the one screen that
       most needs to say what this place is. Translucent + blurred instead: the
       stage reads through, the card still sits on a solidly legible surface. */
    #gate{position:fixed; inset:0; z-index:1200; display:none;
      align-items:center; justify-content:center; padding:var(--s5,24px);
      background:
        radial-gradient(1100px 620px at 50% -12%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 68%),
        color-mix(in srgb, var(--bg) 62%, transparent);
      backdrop-filter:blur(26px) saturate(160%); -webkit-backdrop-filter:blur(26px) saturate(160%);}
    body.locked #gate{display:flex;}
    .gate-card{width:min(92vw,432px); text-align:center;
      display:flex; flex-direction:column; gap:var(--s3,12px);
      padding:clamp(26px,5vw,44px);
      border:1px solid var(--border-default); border-radius:var(--radius-xl);
      background:var(--glass-card-bg); backdrop-filter:var(--glass-card-blur); -webkit-backdrop-filter:var(--glass-card-blur);
      box-shadow:var(--shadow-modal), inset 0 1px 0 rgba(255,244,230,0.10), inset 0 -1px 0 var(--gilt-soft);}
    .gate-card .wordmark{background:linear-gradient(120deg,var(--aurora-green),var(--amber-base) 80%); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text;}
    .gate-card .wordmark--lg{font-size:clamp(19px,2.6vw,23px); opacity:.92; margin:0 0 var(--s3);}
    .gate-title{font-family:var(--font-serif); font-weight:var(--fw-regular); font-size:var(--head-modal); line-height:var(--lh-tight); margin:0; color:var(--ink);}
    .gate-sub{color:var(--ink-dim); margin:0 0 var(--s3,12px); font-size:var(--t-body,15px);}
    .gate-card .loginform{display:flex; flex-direction:column; gap:var(--s2,8px); margin-top:var(--s1,4px);}
    #gateform .field{background:color-mix(in srgb, var(--text-primary) 4%, transparent); border:1px solid var(--border-default); border-radius:var(--radius-md); color:var(--text-primary); padding:var(--sp-3) var(--sp-4); width:100%;}
    #gateform .field:focus{border-color:var(--aurora-green); box-shadow:0 0 0 3px color-mix(in srgb,var(--aurora-green) 15%,transparent);}
    #gate-user:focus{border-color:var(--aurora-green);}
    #gate-pass:focus{border-color:var(--aurora-teal);}
    .gate-card .lbtn{width:100%; padding:12px 16px; font-size:var(--t-body);}
    .lbtn{border:none; font-weight:700; background:linear-gradient(140deg,var(--aurora-green-hover),var(--aurora-green) 45%,var(--aurora-rose)); color:var(--accent-on); border-radius:var(--radius-pill); box-shadow:0 4px 18px -4px color-mix(in srgb,var(--aurora-green) 42%,transparent), inset 0 1px 0 rgba(255,244,230,0.30);}
    .lbtn:hover{filter:brightness(1.05); transform:translateY(-2px); box-shadow:0 10px 34px -6px color-mix(in srgb,var(--aurora-green) 55%,transparent), inset 0 1px 0 rgba(255,244,230,0.42);}
    .gate-status{min-height:1.1em; margin:var(--s1,4px) 0 0; font-size:var(--t-meta,13px); color:var(--muted);}
    .gate-foot{margin:var(--s5) 0 0; font-size:var(--t-cap); color:var(--muted); line-height:1.5;}
    .gate-foot a{color:var(--aurora-teal);}
    @media(max-width:720px){
      #gate{flex-direction:column; overflow-y:auto;}
      .gate-hero{min-height:160px;}
    }
    /* gate hero — decorative atmosphere beside the sign-in card */
    .gate-hero{position:relative; display:flex; align-items:center; justify-content:center;
      background:var(--color-bg-deep); overflow:hidden; min-height:320px;}
    .gh-parallax{position:absolute; inset:0;}
    .gh-orb{position:absolute; border-radius:50%; top:50%; left:50%; transform:translate(-50%,-50%);}
    .gh-orb--1{width:clamp(240px,42vw,560px); height:clamp(240px,42vw,560px);
      background:radial-gradient(circle,color-mix(in srgb,var(--aurora-green) 10%,transparent),transparent 65%);
      filter:blur(80px); animation:gh-orb-breathe 8s ease-in-out infinite alternate;}
    .gh-orb--2{width:clamp(200px,36vw,480px); height:clamp(200px,36vw,480px);
      background:radial-gradient(circle,color-mix(in srgb,var(--aurora-teal) 8%,transparent),transparent 60%);
      filter:blur(60px); animation:gh-orb-breathe 11s ease-in-out 2s infinite alternate;}
    .gh-orb--3{width:clamp(160px,30vw,400px); height:clamp(160px,30vw,400px);
      background:radial-gradient(circle,color-mix(in srgb,var(--aurora-purple) 7%,transparent),transparent 55%);
      filter:blur(60px); animation:gh-orb-breathe 14s ease-in-out 4s infinite alternate;}
    @keyframes gh-orb-breathe{0%{transform:translate(-50%,-50%) scale(1);opacity:.6}100%{transform:translate(-50%,-50%) scale(1.18);opacity:.92}}
    .gh-ring{position:absolute; border-radius:50%; top:50%; left:50%; transform:translate(-50%,-50%); border:1.5px solid transparent;}
    .gh-ring--1{width:clamp(260px,36vw,420px); height:clamp(260px,36vw,420px);
      border-color:color-mix(in srgb,var(--aurora-teal) 12%,transparent);
      animation:gh-ring-rotate 26s linear infinite,gh-ring-glow 13s ease-in-out infinite alternate;}
    .gh-ring--2{width:clamp(360px,50vw,580px); height:clamp(360px,50vw,580px);
      border-color:color-mix(in srgb,var(--aurora-green) 6%,transparent);
      animation:gh-ring-rotate 32s linear infinite reverse; transform:translate(-50%,-50%) rotate(30deg);}
    @keyframes gh-ring-rotate{to{transform:translate(-50%,-50%) rotate(360deg)}}
    @keyframes gh-ring-glow{0%{box-shadow:0 0 30px -18px color-mix(in srgb,var(--aurora-green) 12%,transparent);opacity:.7}100%{box-shadow:0 0 70px -14px color-mix(in srgb,var(--aurora-green) 28%,transparent);opacity:1}}
    .gh-center{position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:4px; height:4px; border-radius:50%;
      background:var(--aurora-green); z-index:2;
      box-shadow:0 0 20px 3px color-mix(in srgb,var(--aurora-green) 34%,transparent),0 0 56px 10px color-mix(in srgb,var(--aurora-green) 15%,transparent);
      animation:gh-dot-pulse 4s ease-in-out infinite;}
    @keyframes gh-dot-pulse{0%,100%{box-shadow:0 0 20px 3px color-mix(in srgb,var(--aurora-green) 34%,transparent),0 0 56px 10px color-mix(in srgb,var(--aurora-green) 15%,transparent)}50%{box-shadow:0 0 32px 6px color-mix(in srgb,var(--aurora-green) 50%,transparent),0 0 84px 16px color-mix(in srgb,var(--aurora-green) 24%,transparent)}}
    .gh-skyline{position:absolute; bottom:0; left:0; width:100%; height:auto; opacity:.5; pointer-events:none;}
    .gh-tagline{position:relative; z-index:3; text-align:center; padding:0 40px;}
    .gh-tagline h1{
      font-family:var(--font-serif); font-weight:400; font-size:clamp(1.5rem,3vw,2.5rem);
      background:linear-gradient(120deg,var(--aurora-green),var(--aurora-teal),var(--aurora-purple),var(--aurora-green));
      background-size:200% 100%; -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text;
      animation:tagline-sheen 16s linear infinite; margin:0;
    }
    .gh-tagline h1 em{font-style:italic; font-weight:470;
      background:linear-gradient(120deg,var(--accent-base),var(--teal-base)); -webkit-background-clip:text; background-clip:text; color:transparent;}
    @keyframes tagline-sheen{0%{background-position:0% 50%}100%{background-position:200% 50%}}
    .gh-tagline .sub{font-size:var(--text-body); color:var(--text-muted); margin-top:12px;}
    @media(max-width:1000px){
      .gate-hero{min-height:200px;}
      .gh-orb--1{width:240px; height:240px;} .gh-orb--2{width:160px; height:160px;} .gh-orb--3{display:none;}
      .gh-ring--1{width:260px; height:260px;} .gh-ring--2{display:none;}
      .gh-tagline h1{font-size:clamp(22px,5vw,32px);}
    }
/* ── Notification panel drawer — reserved for future SSE notifications ── */
.notif-panel[data-notif-panel]{position:fixed; top:0; right:0; width:360px; max-width:90vw; height:100dvh;
  z-index:60; background:var(--glass-card-bg); backdrop-filter:blur(24px) saturate(160%); -webkit-backdrop-filter:blur(24px) saturate(160%);
  border-left:1px solid var(--border-strong); box-shadow:var(--shadow-modal);
  transform:translateX(100%); transition:transform .3s var(--ease-enter),visibility .3s; display:flex; flex-direction:column; overflow:hidden;
  /* Same reasoning as .back-to-top: translated off-screen is still tabbable, so
     without this the drawer's close button sat in the tab order permanently. */
  visibility:hidden}
.notif-panel.open{transform:translateX(0); visibility:visible}
.notif-panel-header{display:flex; align-items:center; justify-content:space-between; padding:var(--sp-5); border-bottom:1px solid var(--border-subtle)}
.notif-panel-header h2{margin:0; font-size:var(--text-lg); font-weight:var(--fw-semibold)}
.notif-backdrop[data-notif-backdrop]{position:fixed; inset:0; z-index:59; background:rgba(0,0,0,.45); display:none}
.notif-backdrop.open{display:block}
/* The drawer is a flex column with the header fixed; the list takes the rest
   and scrolls on its own so a long feed never scrolls the page behind it. */
.notif-list{flex:1; min-height:0; overflow-y:auto; padding:var(--sp-3) var(--sp-5) var(--sp-5)}
.notif-empty{margin:var(--sp-5) 0 0; color:var(--text-muted); font-size:var(--text-sm); line-height:1.6}
.notif-item{padding:var(--sp-3) 0; border-bottom:1px solid var(--border-subtle)}
.notif-item:last-child{border-bottom:none}
.notif-item-t{font-size:var(--text-sm); font-weight:var(--fw-medium); color:var(--text-primary)}
.notif-item-b{margin-top:2px; font-size:var(--text-xs); color:var(--text-muted); line-height:1.5}

/* ── Keyboard help overlay ─────────────────────────────────────────────── */
.kbd-overlay{position:fixed; inset:0; z-index:130; background:rgba(0,0,0,.55); backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px); display:none; align-items:center; justify-content:center}
.kbd-overlay.open{display:flex}
.kbd-panel{width:560px; max-width:90vw; max-height:80vh; overflow-y:auto; background:var(--glass-card-bg); backdrop-filter:blur(24px) saturate(160%); -webkit-backdrop-filter:blur(24px) saturate(160%); border:1px solid var(--border-strong); border-radius:var(--radius-xl); box-shadow:var(--shadow-modal); padding:var(--sp-6)}
.kbd-row{display:flex; align-items:center; justify-content:space-between; padding:var(--sp-2) 0; border-bottom:1px solid var(--border-subtle); font-size:var(--text-sm)}
.kbd-row:last-child{border-bottom:none}
.kbd-help-btn{position:fixed; bottom:20px; left:20px; z-index:30; width:36px; height:36px; border-radius:50%; background:var(--glass-card-bg); backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px); border:1px solid var(--border-strong); color:var(--text-muted); font-family:var(--font-mono); font-size:var(--text-sm); cursor:pointer; display:flex; align-items:center; justify-content:center; transition:all .2s; box-shadow:var(--shadow-raised)}
.kbd-help-btn:hover{color:var(--text-primary); border-color:var(--border-accent); transform:scale(1.1)}

/* ── Context menu — right-click popup ──────────────────────────────────── */
.ctx-menu{position:fixed; z-index:85; min-width:200px; background:var(--glass-card-bg); backdrop-filter:blur(24px) saturate(160%); -webkit-backdrop-filter:blur(24px) saturate(160%); border:1px solid var(--border-strong); border-radius:var(--radius-md); box-shadow:var(--shadow-modal); padding:var(--sp-1); display:none; animation:cmdk-in .15s var(--ease-spring) both}
.ctx-menu.open{display:block}
/* Matched to .menu-item, the account/appearance popover row this sits beside —
   same font, padding, radius and hover wash. It previously used a parallel
   token vocabulary (--text-sm, --sp-N, --radius-xs against the menu's --sans,
   13px, --r-sm), so two menus that can open inches apart rendered at different
   sizes and weights.

   NB: do not write a "--sp-" glob with a star-slash in a CSS comment. Doing so
   closes the comment early, and the tail of the sentence then parses as a
   malformed rule that swallows the NEXT one — which silently deleted this very
   .ctx-item block until it was caught. Same failure mode as the stray brace
   fixed further up: CSS error recovery eats the following rule. */
.ctx-item{display:flex; align-items:center; gap:10px; width:100%; text-align:left;
  background:none; border:none; color:var(--ink-dim); font:600 13px/1.2 var(--sans);
  padding:9px 10px; border-radius:var(--r-sm); cursor:pointer; transition:background-color .12s;}
/* :focus, not only :focus-visible. Opening this menu moves focus to the first
   item programmatically, and after a RIGHT-CLICK that focus does not match
   :focus-visible — so the global focus ring never drew and the menu opened
   with no indication of where the arrow keys would go. The hover wash doubles
   as the roving-focus indicator, which is the correct read for a menu. */
.ctx-item:hover{background:color-mix(in srgb, var(--line-c) 8%, transparent); color:var(--ink);}
/* Focus reads STRONGER than hover here, deliberately. The shared wash resolves
   to about 4% alpha — right for a passive pointer hover, far too faint to be
   the thing telling you which row the arrow keys will activate. Tinting toward
   the accent makes the focused row read as "selected" rather than "grazed", and
   the leading bar gives it an edge that survives on top of poster artwork. */
.ctx-item:focus{background:color-mix(in srgb, var(--accent-base) 16%, transparent);
  color:var(--ink); outline:none; box-shadow:inset 2px 0 0 var(--accent-base);}
.ctx-item:focus-visible{outline:2px solid var(--focus-ring); outline-offset:-2px;}
.ctx-sep{height:1px; background:var(--border-subtle); margin:var(--sp-1) 0}

/* ── Back to top ───────────────────────────────────────────────────────── */
/* visibility, not just opacity: opacity:0 alone leaves the button in the tab
   order, so a keyboard user tabbing the page lands on an invisible control and
   has no idea where focus went. visibility:hidden removes it from the tab order
   AND still animates, as long as it is in the transition list. */
.back-to-top{position:fixed; bottom:24px; right:24px; z-index:30; width:40px; height:40px; border-radius:50%; background:var(--glass-card-bg); backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px); border:1px solid var(--border-strong); color:var(--text-muted); cursor:pointer; display:flex; align-items:center; justify-content:center; opacity:0; visibility:hidden; pointer-events:none; transition:opacity .3s,visibility .3s,transform .25s var(--ease-spring); box-shadow:var(--shadow-raised)}
.back-to-top.is-visible{opacity:1; visibility:visible; pointer-events:auto}
.back-to-top:hover{color:var(--text-primary); transform:translateY(-3px)}

/* ── Global focus ring — one source of truth ────────────────────────────── */
:focus-visible{outline:2px solid var(--focus-ring); outline-offset:2px;}

/* ── Consolidated reduced-motion ────────────────────────────────────────── */
@media(prefers-reduced-motion:reduce){
  .menu,.cmdk.on,.cmdk.open,.palette,.ctx-menu{animation:none!important}
  .cursor-glow{display:none!important}
  .pcard,.npc,.slice{transition:none!important}
  .live::before{animation:none!important}
  /* gate atmosphere (the first screen every visitor sees) + catalog title sweep */
  .gh-orb,.gh-ring,.gh-center,.gh-tagline h1,.libtitle{animation:none!important}
  .libtitle{background-position:50% 50%;}   /* hold the gradient static, no sweep */
}

/* ── Backdrop-filter fallback — solid backgrounds when blur unsupported ── */
@supports not ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))){
  #topbar{background:var(--color-bg-elevated);}
  .gate-card,.pcard,.npc,.sheet,.palette,.menu,.toast,.cmdk,.kbd-panel,.ctx-menu,.notif-panel,.back-to-top,.kbd-help-btn{
    background:var(--color-bg-elevated);}
  #gate{background:color-mix(in srgb, var(--color-bg-deepest) 85%, transparent);}
  .overlay{background:color-mix(in srgb, var(--color-bg-deepest) 80%, transparent);}
}

/* ── Notification badge ───────────────────────────────────────────────── */
.notif-badge{position:absolute; top:-3px; right:-4px; min-width:16px; height:16px; padding:0 4px;
  border-radius:var(--radius-pill); background:var(--status-danger); color:#fff;
  font-size:var(--text-3xs); font-weight:var(--fw-bold); line-height:16px; text-align:center;}
.iconbtn{position:relative;}

/* ── Offline indicator ────────────────────────────────────────────────── */
.offline-bar{position:fixed; top:0; left:0; right:0; z-index:2000; padding:6px var(--sp-4);
  background:var(--status-warning); color:var(--color-bg-deepest); font-size:var(--text-xs); font-weight:var(--fw-semibold); text-align:center;}
.offline-bar[hidden]{display:none;}

/* ── Screen-reader-only utility ───────────────────────────────────────── */
.sr-only{position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0;}



/* ── Forced-colors safety — gradient-clipped display text (hero titles, the
   catalog title, the wordmark + its divider bar, the neon eyebrow, the gate
   tagline) computes a transparent text fill. Under Windows High Contrast /
   forced-colors that transparent fill is NOT repainted, so the headline renders
   invisible. Force every clipped element back to a system colour there. ── */
@media (forced-colors: active){
  .slice h2, .libtitle, .sheen, .wordmark, .wordmark .bar,
  .eyebrow.neon, .gh-tagline h1, .gh-tagline h1 em{
    -webkit-text-fill-color:CanvasText; color:CanvasText; background:none;
  }
}

/* ── Print styles — hide decorative layers, keep content ───────────────── */
@media print{
  .aurora-sky,.cursor-glow,
  #gate,.cmdk,.overlay,.notif-panel,.toast,.kbd-overlay,.kbd-help-btn,.ctx-menu,.back-to-top{display:none!important;}
  body::after, html::after, body::before{display:none!important;}
  body{background:#fff; color:#000; font-size:12pt;}
  body.locked{overflow:visible;}
  #topbar,.hero,.railhead,.addgrid,.libgrid{break-inside:avoid;}
}

