.player-toast{
  position:fixed; left:50%; bottom:104px; transform:translateX(-50%) translateY(8px);
  background:#1a1a1a; color:#fff; border:1px solid #2e2e2e; border-radius:8px;
  padding:10px 16px; font-size:12.5px; font-weight:600; letter-spacing:.01em;
  box-shadow:0 8px 24px rgba(0,0,0,.4); opacity:0; pointer-events:none; z-index:999;
  transition:opacity .18s ease, transform .18s ease; max-width:86vw; text-align:center;
}
.player-toast.show{opacity:1; transform:translateX(-50%) translateY(0);}
@media(max-width:760px){ .player-toast{bottom:200px;} }
/* Keyboard -/+ volume readout: shares .player-toast's look but fades faster
   and sits just above the shared toast so the two never overlap if both are
   up at once. Its 1.2s (vs 2.6s) hang time lives in player.js. */
.volume-toast{ bottom:152px; transition:opacity .12s ease, transform .12s ease; }
@media(max-width:760px){ .volume-toast{bottom:248px;} }

.soft-signin-nudge{
  position:fixed; left:50%; bottom:104px; transform:translateX(-50%) translateY(10px);
  background:var(--card); color:var(--fg); border:1px solid #262626; border-radius:12px;
  padding:16px 22px 18px; box-shadow:0 12px 30px rgba(0,0,0,.5); z-index:95;
  width:calc(100vw - 32px); max-width:340px; text-align:center;
  opacity:0; pointer-events:none; transition:opacity .25s ease, transform .25s ease;
}
.soft-signin-nudge.show{opacity:1; transform:translateX(-50%) translateY(0); pointer-events:auto;}
.soft-signin-nudge .soft-signin-msg{margin:0 0 12px; font-size:13.5px; line-height:1.5; color:var(--fg);}
.soft-signin-nudge .btn-accent{padding:9px 22px; font-size:12.5px;}
#soft-signin-close{
  position:absolute; top:6px; right:8px; background:none; border:none; color:var(--muted);
  cursor:pointer; font-size:19px; line-height:1; padding:6px; z-index:2;
}
#soft-signin-close:hover{color:var(--fg);}
@media(max-width:760px){ .soft-signin-nudge{bottom:200px;} }

:root{
  --bg:#0c0c0c; --sidebar:#111111; --card:#141414; --fg:#ffffff; --muted:#9a9a9a; --accent:#2e6b45;
  --beta-red:#e0272b;
  /* Instrumental mode's blue. Already the colour the transport's shuffle
     icon turns while instrumental shuffle is running
     (#shuffle-btn.instrumental-active) — named here so the Instrumental
     Shuffle button and the state it produces can't drift apart. */
  --instrumental:#5b8dee;
}
*{box-sizing:border-box;}
/* ---- The white flash under the player bar (Raz, 2026-09-07) --------------
   "stop the page from being able to be scrolled up past the now playing bar
   and revealing white bottom… white space in screenshot should not be
   revealed after scrolling up."

   Two separate causes, both fixed here. The bar itself was never the
   problem — it is position:fixed;bottom:0 and stays there. What moves is
   the whole viewport, rubber-banding past the end of the document, and
   what showed through underneath was the browser canvas.

   1. color-scheme:dark. Without it the UA paints its canvas, its overscroll
      area and its scrollbars from the LIGHT defaults — white — no matter
      what colour <body> is. This is the white in Raz's screenshot. The
      coming-soon page in functions/_middleware.js already declared it; the
      app itself never did.
   2. An explicit background on <html>. body's background normally
      propagates to the canvas, but that propagation is exactly the thing
      that behaves inconsistently in WKWebView (this site also runs inside
      the Capacitor shell as THE BAKERY). Painting the root directly means
      there is nothing to propagate and nothing to get wrong.
   3. overscroll-behavior-y:none stops the rubber band at the root scroller
      altogether, so there is no gap to paint in the first place. It also
      stops scroll chaining out of inner panels, which is wanted here.

   NOT position:fixed on <body> — see the scroll-gesture-playbook skill:
   that was tried on this project, did not fix scrolling, and broke touch
   scroll on other position:fixed elements. Do not reintroduce it. */
:root{color-scheme:dark;}
html,body{
  margin:0; padding:0; touch-action:manipulation;
  overscroll-behavior-y:none;
}
html{background:var(--bg); height:100%;}
/* min-height on <body>, not height. This pair used to be a shared
   height:100%, which caps body's box at exactly one viewport — and
   position:sticky is clamped to its containing block, so the header added
   below would have unstuck and scrolled away the moment you passed the first
   screenful. min-height keeps the full-height guarantee the layout wanted and
   lets the box grow with the page. Raz, 2026-09-09. */
body{
  min-height:100%;
  background:var(--bg); color:var(--fg);
  font-family:'Geist','Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;
  -webkit-font-smoothing:antialiased;
}
a{color:inherit;}
/* Pinned to the top of the viewport (Raz, 2026-09-09: "fix and lock the top
   so this does not happen and it is black"). It used to scroll away, which
   left the top of the screen showing whatever happened to be behind it.

   Three things this rule needs to get right:

   1. An explicit background. The header had none and inherited body's, which
      is fine while it sits in flow and fatal once content scrolls underneath
      it. var(--bg) is the same #0c0c0c the native shells now paint (see
      bkrsclb-mobile's capacitor.config.json and Android colors.xml), so the
      header, the page and the app frame are one continuous black.
   2. z-index:60, not higher. That is deliberately the same level the profile
      dropdown was written for (see the .profile-menu-wrap block above) —
      sticky makes the header a stacking context, so the menu's own z-index
      now only orders it *within* the header, and the header's value is what
      the rest of the page sees. 60 clears the player bar (40), the
      now-playing sidebar (39) and the side tabs (50-52), and still sits
      under the overlays (65+), which should cover the header, not duck
      beneath it.
   3. Safe-area padding is ONLY applied under html.bk-edge (see the block
      right below). Plain browsers and THE BAKERY <= 1.0.2 never declare
      viewport-fit=cover, so env(safe-area-inset-*) resolves to 0 there. */
header.site{
  position:sticky; top:0; z-index:60;
  background:var(--bg);
  display:flex; align-items:center; justify-content:space-between;
  padding:16px 28px; border-bottom:1px solid #1c1c1c;
}
/* THE BAKERY edge-to-edge (2026-09-17, needs app 1.0.3+).
   Why: with ios.contentInset:"always" (app <= 1.0.2) the web view's scroll
   view draws page content inside its own top inset, so anything scrolled
   past the pinned header showed behind the clock (IMG_2166). A curtain hung
   off the sticky header (header.site::before, v=142) did NOT fix it on a
   real phone: WebKit does not paint pinned layers into that inset. So the
   fix is native: 1.0.3 sets ios.contentInset:"never", the page runs under
   the status bar, and the header's own black background covers it.
   1.0.3 adds "BKRSCLB-Edge" to its user agent; the inline script in each
   page's <head> then adds html.bk-edge and viewport-fit=cover, which is
   what makes env(safe-area-inset-*) real. Without that marker nothing below
   applies, so browsers and older installs are unchanged. Once bk-edge is on,
   the two env(safe-area-inset-bottom) calcs (player bar, .main) also become
   live, which is intended: the page now reaches the home indicator too.
   Anything else pinned to the very top edge needs its own padding here. */
html.bk-edge header.site{padding-top:calc(16px + env(safe-area-inset-top));}
html.bk-edge #my-selections-drawer,
html.bk-edge #series-info-drawer{padding-top:env(safe-area-inset-top);}
header.site .brand{display:flex; align-items:center; gap:10px; font-weight:700; font-size:19px;}
#brand-name{cursor:pointer;}
/* The red "Beta" tag that used to sit up against BKRSCLB was removed
   2026-08-27 (Raz: "remove beta from the bakery main header text as i am now
   charging for subscriptions") — a beta badge next to a paid product reads as
   a disclaimer. Markup is gone from index/community/profile; this wrapper
   stays because it is what keeps the brand row an inline-flex.
   --beta-red itself is NOT dead: the Watch tab, several close buttons and the
   join banner all still use it as the app's red. */
.brand-with-corner{position:relative; display:inline-flex; align-items:center;}
header.site .brand #brand-logo-link{display:inline-flex; align-items:center; line-height:0; cursor:pointer;}
header.site .brand #brand-logo-img{width:32px; height:32px; object-fit:contain; display:block;}
header.site .brand .dot{width:32px; height:32px; border-radius:50%; background:var(--fg); display:inline-block;}
header.site nav{display:flex; gap:22px; font-size:13px;}
header.site nav a{text-decoration:none; color:var(--muted); font-weight:600; cursor:pointer; padding-bottom:4px;}
/* A tab may carry two labels — the full one and a short one for phones
   (see NAV_SHORT_LABELS in player.js). Desktop is the default and shows
   the full one; the ≤760px block near the bottom of this file flips the
   pair. This rule MUST stay above that block: the two selectors have
   equal specificity, so whichever is written last wins, and putting the
   desktop default afterwards would silently keep "Home" hidden on the
   phones it exists for. */
header.site nav a .nav-short{display:none;}
header.site nav a.active{color:var(--fg); border-bottom:2px solid var(--accent);}
/* Watch is the one tab that goes red while you're on its page — the app's
   red (--beta-red, named for the since-removed Beta tag), so video reads as
   its own lane in the nav. */
header.site nav a.nav-watch.active{color:var(--beta-red); border-bottom-color:var(--beta-red);}

.header-right{display:flex; align-items:center; gap:18px;}

/* My Profile dropdown (2026-08-26) — the header's "My Profile" word now
   opens a two-item menu instead of the profile overlay directly, and
   "Playlists" moved out of the main nav row into it. Styled off
   .rows-dropdown-menu above so it reads as the same kind of object as
   every other menu on the site: --card panel, #262626 hairline, 8px
   radius, #1f1f1f hover.

   z-index:60 clears the player bar (40) and the now-playing sidebar (39);
   it sits below the overlays (65+) on purpose — an open overlay should
   cover this, not have it float on top. */
.profile-menu-wrap{position:relative; display:inline-flex; align-items:center;}
.profile-menu-wrap #profile-nav-link{white-space:nowrap; cursor:pointer;}
.profile-menu-caret{font-size:9px; opacity:.75; margin-left:2px; display:inline-block;}
.profile-menu-wrap.open #profile-nav-link{color:var(--fg);}
.profile-menu-wrap.open .profile-menu-caret{transform:rotate(180deg);}
.profile-menu{
  position:absolute; top:calc(100% + 10px); right:0; background:var(--card);
  border:1px solid #262626; border-radius:8px; min-width:170px; display:none;
  overflow:hidden; z-index:60; box-shadow:0 12px 30px rgba(0,0,0,.5);
}
.profile-menu-wrap.open .profile-menu{display:block;}
.profile-menu button{
  display:block; width:100%; text-align:left; background:none; border:none; color:var(--fg);
  font-size:12.5px; font-weight:600; padding:11px 14px; cursor:pointer; font-family:inherit;
  white-space:nowrap;
}
.profile-menu button:hover{background:#1f1f1f;}
/* Shop is the one header link that leaves for bkrsclb.com, so it is a button
   rather than another grey word in a row of grey words.

   It was green (#3a8a58, hovering to #2e6b45) from 25 Aug so that it matched
   the "Listen free" CTA on bkrsclb.com. Raz retired that on 2026-09-07:
   "make shop button on streaming app a very dark grey instead of green."
   The Now Playing bar took the green that same day, and two greens on one
   screen — one of them chrome — was one too many.

   Dark grey on a near-black header needs an edge or it reads as a hole, so
   the pill carries a 1px inset ring. box-shadow, not border: a border would
   add 2px to the pill's width, and the mobile header row (see the 760px
   block below) has already run out of room once.

   THE HEXES ARE HARDCODED ON PURPOSE — do not "tidy" them into a theme var.
   The admin Theme page repaints --accent, --card and friends at runtime as
   inline styles on <html>; this button must not follow, or a theme change
   silently turns the one leave-the-site control back into a coloured one. */
.store-btn{
  display:inline-flex; align-items:center; gap:6px; white-space:nowrap;
  background:#1f1f1f; color:#fff; text-decoration:none;
  box-shadow:inset 0 0 0 1px #343434;
  font:700 12px/1 inherit; padding:8px 14px; border-radius:999px;
  transition:background .15s ease, box-shadow .15s ease, transform .12s ease;
}
.store-btn:hover{background:#2c2c2c; color:#fff; box-shadow:inset 0 0 0 1px #454545;}
.store-btn:active{transform:scale(.97);}
.search-wrap{position:relative; display:flex; align-items:center;}
.search-wrap input{
  background:#ffffff; border:1px solid #262626; border-radius:999px; color:#111;
  font-size:12.5px; padding:8px 30px 8px 14px; width:190px; transition:.15s; outline:none;
}
.search-wrap input:focus{border-color:var(--accent); width:220px;}
.search-wrap input::placeholder{color:#7a7a7a;}
.search-wrap #search-clear{
  position:absolute; right:8px; background:none; border:none; color:#7a7a7a; font-size:11px;
  cursor:pointer; display:none; padding:2px;
}
.search-wrap #search-clear:hover{color:#111;}
.search-wrap.has-query #search-clear{display:block;}

.wrap{display:flex; min-height:calc(100vh - 60px);}
.main{flex:1; padding:28px 34px 140px; max-width:1280px; margin:0 auto; width:100%;}
.main h1{font-size:26px; margin:0 0 4px;}
.main .sub{color:var(--muted); font-size:13px; margin-bottom:26px;}

/* Discovery Shuffle — persistent inline button beside the "Streaming"
   title (index.html only). Desktop: outline pill with icon + label.
   Mobile: collapses to an icon-only accent-filled circle (see the
   @media(max-width:760px) override below) since "Discovery Shuffle" text
   doesn't fit comfortably next to the h1 on small screens; the button's
   aria-label carries the full accessible name either way. Click handler
   (calls the same startDiscoveryShuffle() the popup's "Press Play" uses)
   lives in player.js next to the rest of the Discovery Shuffle popup code. */
.page-title-row{display:flex; align-items:center; gap:14px; flex-wrap:wrap; margin-bottom:4px;}
.page-title-row h1{margin:0;}
.discovery-shuffle-inline-btn{
  background:transparent; color:var(--fg); border:1.5px solid var(--accent); border-radius:999px;
  padding:8px 18px; font-weight:700; font-size:12.5px; cursor:pointer;
  display:inline-flex; align-items:center; gap:7px; font-family:inherit; white-space:nowrap;
}
.discovery-shuffle-inline-btn:hover{background:rgba(46,107,69,.18);}
.discovery-shuffle-inline-btn svg{flex-shrink:0;}
@media(max-width:760px){
  .discovery-shuffle-inline-btn{
    background:var(--accent); border-color:var(--accent); color:#fff;
    width:34px; height:34px; padding:0; border-radius:50%; justify-content:center;
  }
  .discovery-shuffle-inline-btn .discovery-shuffle-inline-label{display:none;}
}
/* Instrumental Shuffle — the same button shape as Discovery Shuffle, aimed
   at the instrumental-only pool instead of the whole catalog. Blue, not
   green (Raz, 2026-08-27: "make it a blue outline to match the instrumental
   mode colour"): --instrumental is the exact colour the transport's shuffle
   icon turns the moment this button fires, so the control and the state it
   produces read as one thing.

   Declared AFTER the block above on purpose — same specificity, so source
   order is what lets these override the green. */
.instrumental-shuffle-inline-btn{border-color:var(--instrumental);}
.instrumental-shuffle-inline-btn:hover{background:rgba(91,141,238,.18);}

/* Lit state — the button stays filled for as long as the mode it started is
   the one actually running (Raz, 2026-08-27). setShuffleIconClass() in
   player.js owns the .active class; see the note there for why it hangs off
   that function and not off the click handlers.

   Declared AFTER the :hover rules above deliberately. All four selectors are
   the same specificity (0,2,0), so source order decides: while a button is
   lit, the translucent hover tint no longer replaces the solid fill and wash
   it out. Hover then reads as a small brightening instead, which is what the
   .active:hover pair below provides. */
.discovery-shuffle-inline-btn{transition:background .15s ease, border-color .15s ease, color .15s ease;}
.discovery-shuffle-inline-btn.active{background:var(--accent); border-color:var(--accent); color:#fff;}
.instrumental-shuffle-inline-btn.active{background:var(--instrumental); border-color:var(--instrumental); color:#fff;}
.discovery-shuffle-inline-btn.active:hover{filter:brightness(1.14);}

@media(max-width:760px){
  /* Both buttons collapse to icon-only circles on a phone — two pills plus
     the "Streaming" h1 don't fit a 390px row. That makes the ICON the only
     thing telling them apart at a glance, so this one is equalizer bars
     rather than a second set of shuffle arrows in a different colour. */
  .instrumental-shuffle-inline-btn{background:var(--instrumental); border-color:var(--instrumental);}
  /* On a phone both buttons are ALREADY solid circles, so "filled" says
     nothing there — the lit state has to be a ring instead. Two shadows: the
     inner one punches the page background back out so the ring reads as a
     halo with a gap, rather than a thicker border. Matches both buttons —
     the instrumental one carries .discovery-shuffle-inline-btn too. */
  .discovery-shuffle-inline-btn.active{box-shadow:0 0 0 2px var(--bg), 0 0 0 4px rgba(255,255,255,.9);}
}

/* Homepage intro — "Jump back in" row for returning visitors only (see
   the note above the homepage-intro markup in index.html). Sits above
   Featured Releases. */
.homepage-intro{margin-bottom:34px;}
.jump-back-in-head{display:flex; align-items:center; gap:10px; margin-bottom:14px;}
.jump-back-in-head .grid-title{margin:0; cursor:pointer; user-select:none;}
.jump-back-in-toggle{
  background:none; border:1px solid #262626; color:var(--muted); width:22px; height:22px;
  border-radius:6px; cursor:pointer; font-size:10px; display:flex; align-items:center; justify-content:center;
  flex-shrink:0; transition:transform .18s ease, color .15s ease, border-color .15s ease; padding:0; margin:0;
}
.jump-back-in-toggle:hover{color:var(--fg); border-color:#3a3a3a;}
.jump-back-in-toggle.collapsed{transform:rotate(-90deg);}
.intro-latest-nudge{margin:14px 0 0; font-size:12.5px; color:var(--muted);}
.intro-latest-nudge a{color:var(--fg); font-weight:600; text-decoration:underline; text-underline-offset:2px;}
.intro-latest-nudge a:hover{color:var(--accent);}

.featured-section{margin-bottom:34px;}
.featured-section .featured-label{margin:0 0 14px; color:var(--muted); font-size:12px; letter-spacing:.06em; text-transform:uppercase; font-weight:700;}
.featured-wrap{display:flex; align-items:center; gap:10px;}
.featured-arrow{
  background:var(--card); border:1px solid #262626; color:var(--fg); width:38px; height:38px; border-radius:50%;
  display:flex; align-items:center; justify-content:center; cursor:pointer; flex-shrink:0; font-size:16px;
}
.featured-arrow:hover{background:#1a1a1a;}
.featured-grid{display:grid; grid-template-columns:1fr 1fr; gap:18px; flex:1; min-width:0;}
/* Featured card is deliberately HUE-FREE (2026-09-07). It used to be an
   --accent tint; the Now Playing bar took the green gradient the same week
   and two green slabs on one screen was one too many. These are literals,
   NOT var(--accent), so repointing the accent on the admin Theme page can't
   put the colour back. The only colour left on the card is the gold Play Now
   button below — deliberate, it's the one thing asking to be clicked. */
.featured-card{
  background:linear-gradient(120deg,#242424,#0c0c0c 70%);
  border:1px solid #1c1c1c; border-radius:14px; padding:22px; display:flex; gap:16px; align-items:flex-start; position:relative;
}
.featured-card .art{width:88px; height:88px; border-radius:8px; background-size:cover; background-position:center; background-color:#222; flex-shrink:0; background-image:linear-gradient(135deg,#2a2a2a,#0c0c0c); position:relative;}
.featured-card .meta{min-width:0;}
.featured-card .meta p.kicker{margin:0; color:var(--muted); font-size:11px; letter-spacing:.06em; text-transform:uppercase;}
.featured-card .meta h3{margin:4px 0 6px; font-size:17px; line-height:1.25;}
.featured-card .meta .caption{margin:0 0 12px; font-size:12px; color:#a8a8a8; line-height:1.5; font-style:italic;}
/* Red. Deliberately #c4323c, NOT the #e0454f used on the Now Playing bar:
   white on #e0454f is 4.1:1, under AA for this 13px bold label; #c4323c is
   5.5:1 and reads as the same red at a glance. */
.featured-card .play-btn{background:#c4323c; color:#ffffff; border:none; border-radius:999px; padding:10px 20px; font-weight:700; font-size:13px; cursor:pointer; display:inline-flex; align-items:center; gap:8px;}
.featured-dots{display:flex; justify-content:center; gap:7px; margin-top:16px;}
.featured-dots i{width:6px; height:6px; border-radius:50%; background:#333; display:block; transition:.2s; cursor:pointer;}
.featured-dots i.active{background:#c4323c; width:18px; border-radius:3px;}
.featured-tap-zone{display:none;}

.grid-title{font-size:15px; font-weight:700; margin:28px 0 14px;}
.grid-title-row{display:flex; align-items:center; justify-content:space-between; gap:12px;}
.grid-title-row .grid-title{margin:28px 0 14px;}
.grid-title-controls{display:flex; align-items:center; gap:10px;}
.rows-dropdown{position:relative; display:none;}
.rows-dropdown-btn{
  background:var(--card); border:1px solid #262626; border-radius:8px; color:var(--fg); font-size:12px;
  padding:7px 12px; cursor:pointer; display:flex; align-items:center; gap:6px; font-family:inherit; font-weight:600;
}
.rows-dropdown-menu{
  position:absolute; top:calc(100% + 6px); right:0; background:var(--card); border:1px solid #262626; border-radius:8px;
  min-width:110px; display:none; overflow:hidden; z-index:10; box-shadow:0 12px 30px rgba(0,0,0,.5);
}
.rows-dropdown.open .rows-dropdown-menu{display:block;}
.rows-dropdown-menu button{
  display:block; width:100%; text-align:left; background:none; border:none; color:var(--fg); font-size:12.5px;
  padding:9px 14px; cursor:pointer; font-family:inherit;
}
.rows-dropdown-menu button:hover{background:#1f1f1f;}
.rows-dropdown-menu button.active{color:var(--accent); font-weight:700;}
#sort-select{
  background:var(--card); border:1px solid #262626; border-radius:8px; color:var(--fg);
  font-size:12px; padding:7px 10px; cursor:pointer; outline:none;
}
.grid{display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:16px;}
.card{background:var(--card); border:1px solid #1f1f1f; border-radius:10px; padding:12px; cursor:pointer; transition:.15s; min-width:0;}
.card:hover{background:#1a1a1a;}
.card .art{width:100%; aspect-ratio:1/1; border-radius:6px; margin-bottom:10px; background-size:cover; background-position:center; background-color:#222; position:relative;}
/* Cover/photo art is a real <img> now (see setArtImg()/lazyBg() in
   player.js), not a CSS background-image — this absolutely-positions and
   object-fits it inside whichever placeholder container it landed in, so
   it looks pixel-identical to the old background-image treatment while
   getting native lazy-loading + a real alt attribute for screen readers.
   The dark background-color already set on each of these containers stays
   visible as the loading placeholder until the image paints in. */
.art img, .np-sidebar-photo img, .comment-avatar img, .composer-avatar img{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover; border-radius:inherit; display:block;
}
.card .art.video::after{content:"▶"; position:absolute; inset:0; display:flex; align-items:center; justify-content:center; font-size:18px; color:#fff; background:rgba(0,0,0,.25);}
.card .t{font-size:12.5px; font-weight:600; margin:0 0 2px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.card .s{font-size:11px; color:var(--muted); margin:0;}
.empty{color:var(--muted); font-size:13px; padding:20px 0;}
/* Artist thumbnails: bigger cards, fixed 3-per-row grid on desktop instead
   of the catalog's auto-fill-as-many-as-fit layout. Mobile keeps the shared
   .grid behavior (3-across via the max-width:760px override below). */
@media(min-width:761px){
  #artist-grid{grid-template-columns:repeat(3, minmax(0, 1fr)); gap:24px;}
  /* Series render as editorial blocks now (see .series-edi), not tiles. Their
     layout lives with the rest of that block further down this file — putting
     a #series-grid rule here as well meant the later one silently won and the
     two-up layout never appeared. */
}

/* ---- Now Playing bar: "green into black" (Raz, 2026-09-07) --------------
   Was background:var(--sidebar) — a flat #000000 from the theme. Raz picked
   the gradient option out of the three-way preview: the Shop pill's #3a8a58
   at the left edge, through a dark green, landing on #111111 by the right.
   The green sits under the artwork and the track title, which is the half
   of the bar worth drawing the eye to; the volume slider ends up on black,
   where it was before.

   A LITERAL, not var(--sidebar). Anything reading --sidebar follows the
   admin Theme page, and repointing "Sidebar / Bars" (which also paints the
   real sidebar) would silently flatten this back to one colour.

   Everything under "contrast on the gradient" below exists because grey
   chrome that reads fine on black does not read on #3a8a58. Each override
   is scoped to .player-bar so nothing else in the app is touched, and each
   is a literal for the same reason the background is. */
.player-bar{
  position:fixed; bottom:0; left:0; right:0; z-index:40;
  background:linear-gradient(90deg,#3a8a58 0%,#25523a 46%,#111111 100%);
  border-top:1px solid rgba(255,255,255,.09);
  display:flex; align-items:center; gap:24px; padding:12px 22px;
}
/* ---- contrast on the gradient ----
   These three beat the unscoped .progress-row / .progress / .progress i
   further down on specificity, so their position here is safe. The two
   that would only have TIED with a later rule (.player-bar .np .s and
   .player-bar .btns #shuffle-btn) are changed at their own declarations
   instead — writing them here would have looked right and done nothing. */
/* var(--muted) is theme-controlled (live value #d6d6d6, but the Theme page
   can set anything). White-at-74% holds the timestamps at 4.6:1 over the
   green end no matter what the theme does. */
.player-bar .progress-row{color:rgba(255,255,255,.74);}
/* #2c2c2c read as a dark smear on green. A black wash sits correctly on
   both ends of the gradient. */
.player-bar .progress{background:rgba(0,0,0,.30);}
/* The played portion was var(--accent) — now #2e6b45, i.e. green on green,
   invisible. Raz's call (2026-09-07): red, not white — it reads as elapsed
   time at a glance against both ends of the gradient, and the scrub handle
   is already white, so a white fill left the two indistinguishable.
   #e0454f is the red already in this file (the stop-shuffle hold ring and
   the destructive buttons), not a new colour. */
.player-bar .progress i{background:#e0454f;}
/* 260px, not 230: the transport heart lives in here now and the title needs
   to keep its room. .np-text takes the slack (min-width:0 so the title can
   still ellipsis instead of shoving the heart out of the box). */
.player-bar .np{display:flex; align-items:center; gap:12px; width:260px; flex-shrink:0; overflow:hidden;}
.player-bar .np .np-text{min-width:0; flex:1;}
/* The row hearts are hidden until their row is hovered; this one has no row
   to hover, so it is always visible. Bigger tap target than a row heart, too
   — on mobile it sits in the thumb's path. */
.player-bar .np-like{opacity:.75; flex-shrink:0; padding:8px;}
.player-bar .np-like:hover{opacity:1;}
.player-bar .np-like.liked{opacity:1;}
/* box-shadow, not border — a border would change the box size and shove the
   title. The artwork now sits on green, so it needs an edge to sit against. */
.player-bar .np .art{width:44px; height:44px; border-radius:6px; background:linear-gradient(135deg,var(--accent),#0c0c0c); background-size:cover; background-position:center; flex-shrink:0; position:relative; box-shadow:0 0 0 1px rgba(0,0,0,.35);}
.player-bar .np .t{font-size:12.5px; font-weight:600; margin:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
/* Literal, not var(--muted): this line sits over the greenest part of the
   gradient. See the contrast note on .player-bar above. */
.player-bar .np .s{font-size:11px; color:rgba(255,255,255,.74); margin:0;}
.player-bar .controls{display:flex; flex-direction:column; align-items:center; gap:6px; flex:1; max-width:560px;}
.player-bar .btns{display:flex; align-items:center; gap:18px; font-size:15px;}
.player-bar .btns button{background:none; border:none; color:var(--fg); cursor:pointer; font-size:15px; padding:4px; display:flex; align-items:center; justify-content:center;}
.player-bar .btns .play{width:32px; height:32px; border-radius:50%; background:#fff; color:#111; display:flex; align-items:center; justify-content:center; font-size:12px; border:none; cursor:pointer;}
.player-bar .btns .repeat-btn{width:18px; height:18px; min-width:18px; border-radius:50%; background:#fff; color:#111; display:flex; align-items:center; justify-content:center; border:none; cursor:pointer; padding:0; flex:none;}
.player-bar .btns .repeat-btn svg{display:block;}
.player-bar .btns .repeat-btn.repeat-off-mode{width:auto; height:16px; min-width:16px; border-radius:8px; padding:0 5px;}
.player-bar .btns .repeat-btn .repeat-off-label{font-size:8px; font-weight:800; line-height:1; letter-spacing:0.3px; color:#111; white-space:nowrap;}
/* Idle colour is a literal for the same reason .np .s is — see the contrast
   note on .player-bar. */
.player-bar .btns #shuffle-btn{color:rgba(255,255,255,.66); position:relative;}
.player-bar .btns #shuffle-btn svg{position:relative; z-index:2;}
.player-bar .btns #shuffle-btn.holding{color:#fff;}
/* Was var(--accent). The accent is #2e6b45 and the bar under this icon is
   #25523a — green on green, gone. Raz (2026-09-07): "make the shuffle
   button red instead of green when selected… so the green shuffle is not
   lost on the green now playing bar." Red also matches the played portion
   of the scrub bar, so "this is on" reads as one colour across the bar.
   The blue instrumental mode and the amber by-artist mode below already
   clear the gradient and are deliberately left alone. */
.player-bar .btns #shuffle-btn.regular-active{color:#e0454f;}
.player-bar .btns #shuffle-btn.instrumental-active{color:#5b8dee;}
.player-bar .btns #shuffle-btn.artist-active{color:#c98a2c;}
/* The hold-to-choose-a-mode ring fills with var(--accent) too, and vanishes
   into the bar for the same reason. White, matching .holding, which is what
   the icon itself turns while the ring is filling. .hold-ring.stopping
   (red) is declared after this and still wins — that one must stay red. */
.player-bar .hold-ring{
  background:conic-gradient(#ffffff calc(var(--pct,0) * 1%), transparent 0);
}

.shuffle-wrap{position:relative; display:inline-flex;}
.hold-ring{
  position:absolute; inset:0; border-radius:50%;
  background:conic-gradient(var(--accent) calc(var(--pct,0) * 1%), transparent 0);
  transition:background .05s linear; pointer-events:none;
}
.hold-ring.stopping{background:conic-gradient(#e0454f calc(var(--pct,0) * 1%), transparent 0);}
.shuffle-menu{
  position:absolute; bottom:calc(100% + 14px); left:50%; transform:translateX(-50%) translateY(6px);
  background:var(--card); border:1px solid #262626; border-radius:10px; min-width:200px;
  opacity:0; pointer-events:none; transition:opacity .16s ease, transform .16s ease;
  box-shadow:0 12px 30px rgba(0,0,0,.55); overflow:hidden; z-index:45; text-align:left;
}
.shuffle-menu.show{opacity:1; pointer-events:auto; transform:translateX(-50%) translateY(0);}
.shuffle-menu button{
  display:block; width:100%; text-align:left; background:none; border:none; color:var(--fg); font-size:12.5px;
  font-weight:600; padding:10px 14px; cursor:pointer; font-family:inherit; white-space:nowrap;
  border-bottom:1px solid #1f1f1f;
}
.shuffle-menu button:last-child{border-bottom:none;}
.shuffle-menu button:hover{background:#1f1f1f;}
.shuffle-menu button.active{color:var(--accent);}
.shuffle-menu button.active::after{content:"— current"; float:right; color:var(--accent); font-weight:400; font-size:11px;}
.shuffle-menu button.kbd-focus,
.shuffle-menu .menu-back.kbd-focus{background:var(--accent); color:#fff;}
.shuffle-menu button.kbd-focus.active,
.shuffle-menu button.kbd-focus.active::after{color:#fff;}
.shuffle-menu .menu-back{
  color:var(--muted); font-size:11.5px; font-weight:600; padding:9px 14px; border-bottom:1px solid #1f1f1f; cursor:pointer;
}
.shuffle-menu .menu-back:hover{color:var(--fg);}
.vol svg{flex-shrink:0; color:var(--muted);}
.progress-row{display:flex; align-items:center; gap:8px; width:100%; font-size:10.5px; color:var(--muted);}
.progress{flex:1; height:4px; background:#2c2c2c; border-radius:2px; position:relative; cursor:pointer; padding:8px 0; margin:-8px 0; background-clip:content-box;}
.progress i{position:absolute; left:0; top:8px; height:4px; width:0%; background:var(--accent); border-radius:2px; display:block; pointer-events:none;}
.progress-handle{
  position:absolute; top:50%; width:11px; height:11px; border-radius:50%; background:#fff; left:0%;
  transform:translate(-50%,-50%); box-shadow:0 1px 4px rgba(0,0,0,.5); pointer-events:none;
}
body.scrubbing{user-select:none;}
.vol{width:150px; display:flex; align-items:center; gap:8px; font-size:13px; color:var(--muted); flex-shrink:0;}
.vol input[type=range]{width:100%;}

.back-link{background:none; border:none; color:var(--muted); font-size:13px; cursor:pointer; padding:0 0 18px; font-weight:600;}
.back-link:hover{color:var(--fg);}

.album-header{
  display:flex; align-items:center; gap:24px; margin-bottom:30px; flex-wrap:wrap;
}
.album-header .art{
  width:180px; height:180px; border-radius:10px; background-size:cover; background-position:center;
  background-color:#222; background-image:linear-gradient(135deg,var(--accent),#0c0c0c); flex-shrink:0;
  box-shadow:0 12px 30px rgba(0,0,0,.4); position:relative;
}
.album-header .art.clickable{cursor:pointer; transition:opacity .15s;}
.album-header .art.clickable:hover{opacity:.85;}
/* ---- artist photo slideshow (migration 063) ----
   Every photo in the set is stacked in the same box and cross-faded by
   toggling .on — the same mechanic as the video image sets, and for the
   same reason: swapping one <img>'s src shows a blank frame on every
   change while the next file downloads. All of them are in the DOM from
   the start, so by the time a fade begins the image is already decoded.
   Only ever applied when there are 2+ photos; a single photo keeps the
   plain, un-transitioned .art treatment above. */
.album-header .art.artist-slides{overflow:hidden;}
.album-header .art.artist-slides img{opacity:0; transition:opacity .7s ease;}
.album-header .art.artist-slides img.on{opacity:1;}
.artist-slide-nav{
  position:absolute; top:50%; transform:translateY(-50%); z-index:3;
  width:26px; height:34px; padding:0; border:none; cursor:pointer;
  background:rgba(0,0,0,.5); color:#fff; font-size:19px; line-height:1;
  font-family:inherit; opacity:0; transition:opacity .15s ease, background .15s ease;
}
.artist-slide-nav.prev{left:0; border-radius:0 5px 5px 0;}
.artist-slide-nav.next{right:0; border-radius:5px 0 0 5px;}
.artist-slide-nav:hover{background:rgba(0,0,0,.78);}
.artist-slide-nav:focus-visible{opacity:1; outline:2px solid var(--accent); outline-offset:-2px;}
.album-header .art.artist-slides:hover .artist-slide-nav{opacity:1;}
.artist-slide-dots{
  position:absolute; left:0; right:0; bottom:8px; z-index:3;
  display:flex; justify-content:center; gap:5px;
}
.artist-slide-dot{
  width:6px; height:6px; padding:0; border-radius:50%; cursor:pointer;
  border:none; background:rgba(255,255,255,.42);
  transition:background .2s ease, transform .2s ease;
}
.artist-slide-dot.on{background:#fff; transform:scale(1.25);}
.artist-slide-dot:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
@media (hover:none){
  /* No hover on a phone, so arrows that only appear on hover would be
     unreachable. They stay visible instead — the swipe is the primary
     gesture, these are the discoverable fallback. */
  .artist-slide-nav{opacity:1; background:rgba(0,0,0,.42);}
}
/* Roster tiles cross-fade on their own timer (see wireArtistTileCycle in
   player.js) on every device, staggered so the grid ripples rather than
   flipping in unison. Hovering one pauses it. */
.card .art.cycle img{opacity:0; transition:opacity .55s ease;}
.card .art.cycle img.on{opacity:1;}
.album-header .meta p{margin:0; color:var(--muted); font-size:12px; letter-spacing:.06em; text-transform:uppercase;}
.album-header .meta h2{margin:6px 0 6px; font-size:28px;}
.album-header .meta .s{color:var(--muted); font-size:13px; margin:0 0 16px;}
.album-header .play-btn{background:var(--accent); color:#fff; border:none; border-radius:999px; padding:10px 24px; font-weight:700; font-size:13px; cursor:pointer;}
/* Artist page — round play button sitting beside the artist's name. Starts
   an artist-scoped shuffle (the same pool as the player bar's hold-shuffle →
   "By Artist" menu) from that artist's own page. Sized off the h2 rather than
   the .play-btn pill because it lives inline with the heading, not under it. */
.album-header .meta .artist-name-row{display:flex; align-items:center; gap:12px; flex-wrap:wrap;}
.artist-shuffle-btn{
  background:var(--accent); color:#fff; border:none; border-radius:50%;
  width:40px; height:40px; flex-shrink:0; cursor:pointer; padding:0;
  display:inline-flex; align-items:center; justify-content:center;
  box-shadow:0 6px 16px rgba(0,0,0,.35);
  transition:transform .15s ease, filter .15s ease;
}
.artist-shuffle-btn:hover{transform:scale(1.07); filter:brightness(1.12);}
.artist-shuffle-btn:active{transform:scale(.95);}
.artist-shuffle-btn svg{flex-shrink:0; margin-left:2px;}
/* Instrumental toggle — swaps this album's tracklist + playback to attached
   instrumental versions in place (see track-instrumental.js / migration
   034). Only shown at all when the album actually has at least one
   attached instrumental (JS toggles display:none). Black by default per
   spec, blue on hover; .active (currently showing instrumentals) pins that
   same blue permanently so the toggled state stays legible without a hover. */
.instrumental-toggle-btn{
  background:#000; color:#fff; border:1px solid #2c2c2c; border-radius:999px; padding:10px 24px;
  font-weight:700; font-size:13px; letter-spacing:.02em; cursor:pointer; margin-left:10px;
  transition:background .15s ease, border-color .15s ease;
}
.instrumental-toggle-btn:hover{background:#1d6fd6; border-color:#1d6fd6;}
.instrumental-toggle-btn.active{background:#1d6fd6; border-color:#1d6fd6;}

.tracklist{display:flex; flex-direction:column; gap:2px;}
.track-row{
  display:grid; grid-template-columns:32px 1fr auto; align-items:center; gap:14px;
  padding:10px 12px; border-radius:8px; cursor:pointer; font-size:13.5px;
}
/* Long-pressing the title text (to open the context menu — see
   wireTrackRowLongPress() in player.js) was triggering the browser's
   native text-selection/"Copy" callout instead, fighting the custom
   menu for the same gesture. user-select stops the selection itself;
   -webkit-touch-callout stops iOS Safari's copy/lookup popover, which
   fires independently of selection. tap-highlight-color also killed
   here since the gray flash briefly covering the row looked like a
   selection artifact too.
   Scoped to touch-primary devices only (round 15) — desktop macOS Safari
   ties its `contextmenu` event firing to whether the clicked element is
   selectable, so a global `user-select:none` here silently killed the
   desktop right-click context menu in Safari only (Chrome/Firefox don't
   have that coupling, so it looked fine there). Touch devices never had
   that problem — only the text-selection/callout race this rule exists
   to fix — so restricting it to touch keeps the original mobile fix
   intact while letting desktop Safari's contextmenu fire normally again. */
@media (hover: none) and (pointer: coarse) {
  .track-row{
    user-select:none; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none;
    -webkit-touch-callout:none; -webkit-tap-highlight-color:transparent;
  }
  /* Same fix, same reason, for album cards — holding one to open the
     "See Info Card" menu (wireAlbumCardLongPress() in player.js) was
     fighting the native text-selection/copy callout the same way track
     rows did. Kept in the same touch-only media query for the same
     desktop-Safari-contextmenu reason documented above. */
  .card{
    user-select:none; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none;
    -webkit-touch-callout:none; -webkit-tap-highlight-color:transparent;
  }
  /* The bottom player bar is app chrome, not content — a press-drag on the
     now-playing title was highlighting/selecting the text on phones (Raz's
     2026-08-18 report, blue selection over the title in THE BAKERY app),
     which reads as broken. Same touch-only scoping as .track-row above for
     the same desktop-Safari-contextmenu reason. touch-action:manipulation
     is set per element (it does NOT inherit) so a double-tap that lands on
     any control inside the bar can never trigger iOS double-tap zoom. */
  .player-bar, .player-bar *{
    user-select:none; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none;
    -webkit-touch-callout:none; -webkit-tap-highlight-color:transparent;
    touch-action:manipulation;
  }
}
.track-row:hover{background:var(--card);}
.track-row.playing{color:var(--accent); background:var(--card);}
.track-row .num{color:var(--muted); font-size:12px; text-align:center;}
.track-row.playing .num{color:var(--accent);}
.track-row .title{overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.track-row .title .from{color:var(--muted); font-weight:400;}
.track-row .title .featuring{color:var(--muted); font-weight:400; font-size:0.9em; margin-left:6px;}
.track-row.playing .title .featuring{color:inherit; opacity:0.85;}
.track-row .dur{color:var(--muted); font-size:12px;}

/* ---- Heart / Like button (feature spec 2026-08-15 §1) ----
   Spec calls for hover-to-reveal on desktop / tap-to-reveal on mobile;
   simplified here to "always visible, dim until hovered/liked" since
   there's no hover concept on touch anyway — a signed-in visitor can
   always tap it directly rather than needing a reveal-tap first. Liked
   state and the like/unlike click flash are RED.

   Colour history, because this has moved twice in one day: #208311 from
   15 Aug, then #2e6b45 on 2026-09-07 when Raz replaced every instance of
   the old green, then later the same day — "make the heart turn red when
   clicked not green" — #e0454f. That is the red already used by the scrub
   bar's played portion and the active shuffle icon, so red reads as "on"
   everywhere in this app; it is not a new colour.

   Deliberately literals, not var(--accent): the admin Theme page repaints
   the accent at runtime and the heart must not follow it. Hover uses the
   same red so hovering previews the state the click produces.

   The like-flash keyframe further down carries the same value — change
   both together or the pulse flashes the old colour on the way in. */
.track-like-btn{
  background:none; border:none; padding:4px; margin:0; cursor:pointer; color:var(--muted);
  display:flex; align-items:center; justify-content:center; opacity:.55;
  transition:opacity .15s ease, color .15s ease, transform .15s ease;
}
.track-row:hover .track-like-btn, .track-like-btn.liked{ opacity:1; }
.track-like-btn:hover{ color:#e0454f; }
.track-like-btn.liked{ color:#e0454f; }
.track-like-btn.like-flash{ animation:like-flash-pulse .5s ease; }
/* The album header heart (migration 064). Row hearts fade in on row hover;
   this one has no row, so it is always visible and sits inline with the
   Play All / Share buttons above the tracklist. */
.album-like-btn{ opacity:.8; vertical-align:middle; display:inline-flex; }
.album-like-btn:hover{ opacity:1; }
.album-like-btn.liked{ opacity:1; }

/* ---- "pick three" onboarding (migration 064) ----
   Shown once to a signed-in member whose library is completely empty. */
.p3-overlay{
  position:fixed; inset:0; z-index:120; display:flex; align-items:center; justify-content:center;
  background:rgba(0,0,0,.82); padding:24px; overflow-y:auto;
}
.p3-modal{
  background:var(--sidebar); border:1px solid #1f1f1f; border-radius:16px;
  padding:30px 30px 26px; max-width:640px; width:100%; margin:auto;
}
.p3-eyebrow{
  margin:0 0 8px; font-size:11px; letter-spacing:.18em; text-transform:uppercase;
  color:var(--accent); font-weight:700;
}
.p3-title{margin:0 0 8px; font-size:26px; letter-spacing:-.01em;}
.p3-sub{margin:0 0 22px; color:var(--muted); font-size:14px; line-height:1.55;}
/* minmax(0,1fr), NOT 1fr: a 1fr track has an automatic min-content floor, and
   .p3-name is white-space:nowrap, so a long album title widens its own column
   and the four tiles come out ragged (measured: 136 / 231 / 99 / 149px). */
.p3-grid{display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px;}
.p3-tile{
  background:none; border:2px solid transparent; border-radius:10px; padding:0;
  cursor:pointer; text-align:left; font-family:inherit; color:var(--fg);
  transition:border-color .15s ease, transform .15s ease;
}
.p3-tile:hover{transform:translateY(-2px);}
.p3-tile.on{border-color:var(--accent);}
.p3-art{
  display:block; width:100%; aspect-ratio:1/1; border-radius:8px;
  background:#141414 center/cover no-repeat; margin-bottom:6px;
}
.p3-name{
  display:block; font-size:11.5px; font-weight:600; line-height:1.3;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding:0 2px 4px;
}
.p3-tile.on .p3-name{color:var(--accent);}
.p3-tile:focus-visible{outline:2px solid var(--accent); outline-offset:3px;}
.p3-actions{display:flex; align-items:center; gap:14px; margin-top:24px; flex-wrap:wrap;}
.p3-save{
  padding:11px 20px; border-radius:999px; border:1px solid var(--accent);
  background:var(--accent); color:#fff; font-size:14px; font-weight:700;
  font-family:inherit; cursor:pointer; transition:opacity .15s ease;
}
.p3-save:disabled{opacity:.4; cursor:default;}
.p3-save:not(:disabled):hover{opacity:.9;}
.p3-skip{
  background:none; border:none; color:var(--muted); font-size:13.5px;
  font-family:inherit; cursor:pointer; padding:8px 4px; text-decoration:underline;
}
.p3-skip:hover{color:var(--fg);}
/* Liked Albums panel — same shell as the pick-three modal, wider, and its
   grid holds real catalog cards (entryCardEl) rather than bespoke tiles. */
.la-modal{max-width:860px;}
.la-grid{display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:14px;}
.la-grid .card{cursor:pointer;}
@media(max-width:760px){
  .p3-modal{padding:24px 20px 20px; border-radius:14px;}
  .p3-title{font-size:22px;}
  .p3-grid{grid-template-columns:repeat(3,minmax(0,1fr)); gap:10px;}
  .la-grid{grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px;}
}
@keyframes like-flash-pulse{
  0%{ transform:scale(1); }
  /* Matches .track-like-btn.liked — see the colour history note there. */
  35%{ transform:scale(1.4); color:#e0454f; }
  100%{ transform:scale(1); }
}

#np-sidebar-tab{
  position:fixed; top:50%; right:0; transform:translateY(-50%);
  width:24px; height:56px; background:#ffffff; border:1px solid #262626; border-right:none;
  border-radius:8px 0 0 8px; display:flex; align-items:center; justify-content:center;
  color:#111; cursor:pointer; z-index:41; padding:0; transition:background .15s, opacity .15s;
  box-shadow:0 4px 14px rgba(0,0,0,.35);
}
#np-sidebar-tab:hover{opacity:.85;}

#np-sidebar{
  position:fixed; top:0; right:0; bottom:0; width:340px; max-width:88vw;
  background:var(--sidebar); border-left:1px solid #1f1f1f; z-index:39;
  transform:translateX(100%); transition:transform .25s ease;
  /* Mobile fix, round 2: `-webkit-overflow-scrolling:touch` + `overflow-y:auto`
     on the SAME element that also carries a CSS `transform` is a known
     iOS/WebKit combo that can silently break touch scrolling on that
     element — even though the scroll lock added earlier stopped it from
     scrolling the page BEHIND it, the panel's own content still couldn't
     be scrolled. Every other working overlay in this file (Quick Commands'
     qc-modal→qc-frame→qc-body, the profile/community iframe modals)
     already keeps the transform on an outer wrapper and does the actual
     scrolling on a separate, non-transformed inner element — this now
     matches that pattern: transform stays here, actual scrolling moves to
     .np-sidebar-scroll below. overflow:hidden just clips content to the
     panel's own box, nothing more. */
  overflow:hidden;
}
#np-sidebar.expanded{transform:translateX(0);}
#np-sidebar-close{
  /* Red circle + white X per Raz's ask, so this close button stands out
     more against the sidebar's dark background than the plain muted-gray
     icon did before. Reuses --beta-red (the app's red; the variable is
     named for the Beta tag that used to carry it, removed 2026-08-27)
     rather than inventing a new color. */
  background:var(--beta-red); border:none; color:#fff; cursor:pointer; padding:6px;
  border-radius:50%;
  /* Was float:right — switched to absolute + a z-index so it sits pinned
     on top of .np-sidebar-scroll now that that's a separate, independently
     scrolling box (see the #np-sidebar comment above). A float here would
     otherwise fight with .np-sidebar-scroll's height:100% (floats don't
     reserve height for following siblings the way this needs), same
     positioning approach the three sibling popups' close buttons already use. */
  position:absolute; top:14px; right:14px; z-index:2;
}
#np-sidebar-close:hover{opacity:.85; color:#fff;}
.np-sidebar-scroll{
  padding:60px 22px 100px; clear:both;
  /* Mobile fix, round 7: round 6's debug badge showed the lock code's own
     "allowed" counter climbing normally while Raz dragged — proof round 4's
     touchmove lock was never the blocker. So the remaining explanation is
     that this element sits off-screen (translateX) or hidden until the
     panel opens, and some iOS/WebKit versions never re-evaluate whether a
     newly-revealed element is a real scroll container just because its
     class/transform changed later — it can stay stuck in the
     "not scrollable" state it had at layout time, matching the exact
     signature seen all along (touches arrive, content overflows, scrollTop
     never moves). Fix: scroll is no longer just a static CSS property.
     `overflow-y` defaults to `hidden` here — genuinely inert while the
     panel is closed/off-screen — and player.js's enablePanelScroll()/
     disablePanelScroll() explicitly flip it to `auto` (forcing overflow-y
     off then back on across two animation frames) exactly when
     expandNpSidebar()/collapseNpSidebar() run, so WebKit re-runs its
     scrollability check right as the panel actually becomes visible
     instead of trusting a stale one from load time.
  */
  overflow-y:hidden;
  -webkit-overflow-scrolling:touch;
  transform:translateZ(0);
}
#np-sidebar .np-sidebar-scroll{height:100%;}
.np-sidebar-photo{width:100%; aspect-ratio:1/1; border-radius:10px; background-size:cover; background-position:center; background-color:#222; margin-bottom:18px; position:relative;}
.np-sidebar-photo.clickable{cursor:pointer; transition:opacity .15s;}
.np-sidebar-photo.clickable:hover{opacity:.85;}
#np-sidebar h3{margin:0 0 3px; font-size:18px;}
.np-sidebar-artist{color:var(--muted); font-size:13px; margin:0 0 18px;}
.np-sidebar-meta{display:flex; flex-direction:column; gap:9px; margin-bottom:18px; padding-bottom:18px; border-bottom:1px solid #262626;}
.np-sidebar-meta-row{display:flex; justify-content:space-between; gap:10px; font-size:12.5px;}
.np-sidebar-meta-row span{color:var(--muted); flex-shrink:0;}
.np-sidebar-meta-row strong{text-align:right; font-weight:600;}
.np-sidebar-description{font-size:13px; line-height:1.65; color:var(--fg); white-space:pre-wrap; margin:0 0 20px;}
.np-sidebar-gallery{display:grid; grid-template-columns:1fr 1fr; gap:8px;}
.np-sidebar-gallery.single{grid-template-columns:1fr;}
.np-sidebar-gallery img{width:100%; aspect-ratio:1/1; object-fit:cover; border-radius:8px; cursor:pointer; display:block;}

/* ---- Lyrics: sidebar button (opens the focused popup below, same as the
   right-click "Read Lyrics" option) and the right-click "See Info Card"
   popup (reuses .np-sidebar-scroll wholesale). ---- */
#np-sidebar-lyrics-btn{
  background:none; border:1px solid #2c2c2c; color:var(--fg); border-radius:999px; padding:7px 16px;
  font-size:12px; font-weight:700; cursor:pointer; font-family:inherit; margin:0 0 14px; display:inline-block;
}
#np-sidebar-lyrics-btn:hover{background:#1a1a1a;}
.lyrics-scroll{max-height:280px; overflow-y:auto; font-size:13px; line-height:1.75; color:var(--fg); white-space:pre-wrap; padding-right:4px;}

#track-info-overlay, #lyrics-popup-overlay{
  position:fixed; inset:0; background:rgba(0,0,0,.7); z-index:65; opacity:0; pointer-events:none; transition:opacity .2s ease;
}
#track-info-overlay.show, #lyrics-popup-overlay.show{opacity:1; pointer-events:auto;}
#track-info-popup, #lyrics-popup, #track-comment-popup{
  position:fixed; top:50%; left:50%; transform:translate(-50%,-50%) scale(.96);
  width:380px; max-width:90vw; max-height:86vh;
  background:var(--sidebar); border:1px solid #262626; border-radius:14px; z-index:66;
  opacity:0; pointer-events:none; transition:opacity .2s ease, transform .2s ease;
  box-shadow:0 20px 60px rgba(0,0,0,.55);
  /* Mobile fix, round 2: see the long comment on #np-sidebar above — the
     transform here has to stay off the same element that scrolls, so
     scrolling moved to the inner .np-sidebar-scroll wrapper (added in the
     HTML for lyrics/comment popups, already present for the info popup).
     overflow:hidden here is just corner-clipping, not the scroll mechanism. */
  overflow:hidden;
}
#track-info-popup.show, #lyrics-popup.show, #track-comment-popup.show{opacity:1; pointer-events:auto; transform:translate(-50%,-50%) scale(1);}
/* Red circle + white X on all three of these popups (See Info Card,
   Lyrics, Comment) per Raz's ask — matches the Now Playing sidebar's
   close button. Was briefly Info-Card-only; now unified across the group. */
#track-info-close, #lyrics-popup-close, #track-comment-close{
  position:absolute; top:10px; right:10px; background:var(--beta-red); border:none;
  color:#fff; cursor:pointer; padding:6px; z-index:2; border-radius:50%;
}
#track-info-close:hover, #lyrics-popup-close:hover, #track-comment-close:hover{opacity:.85; color:#fff;}
#track-info-popup .np-sidebar-scroll, #lyrics-popup .np-sidebar-scroll, #track-comment-popup .np-sidebar-scroll{
  max-height:86vh; padding:44px 22px 26px;
}
/* Mobile fix: these popups are vertically centered (top:50% + translate)
   and capped at max-height:86vh on ALL screens — but the mobile Option-B
   player bar (see the .player-bar media-query comment above, ~200-210px
   tall, same number .main's padding-bottom uses for clearance) sits fixed
   at the very bottom of the viewport. A centered 86vh-tall popup can run
   right down into that reserved zone, so the last lines of a long
   description/comment/lyrics sheet render behind/underneath it and are
   never reachable no matter how much the inner .np-sidebar-scroll is
   scrolled — the scroll works, the BOX itself just doesn't leave room for
   the last chunk. Shrinking both the popup and its inner scroll box on
   mobile guarantees real clearance instead of relying on 86vh math that
   doesn't know the player bar exists. */
@media(max-width:760px){
  #track-info-popup, #lyrics-popup, #track-comment-popup{ max-height:calc(100vh - 242px - env(safe-area-inset-bottom)); }
  #track-info-popup .np-sidebar-scroll, #lyrics-popup .np-sidebar-scroll, #track-comment-popup .np-sidebar-scroll{ max-height:calc(100vh - 242px - env(safe-area-inset-bottom)); }
}
#lyrics-popup h3{margin:0 0 3px; font-size:18px;}

/* Clip picker bar inside the synced lyrics panel (build plan phase 5). */
.lyrics-clip-bar{
  display:none; align-items:center; gap:10px; margin:0 0 10px; flex-wrap:wrap;
}
#lyrics-popup.synced .lyrics-clip-bar{display:flex;}
#lyrics-clip-btn{
  background:none; border:1px solid #2c2c2c; color:var(--fg); border-radius:999px; padding:6px 14px;
  font-size:11.5px; font-weight:800; letter-spacing:.04em; text-transform:uppercase; cursor:pointer; font-family:inherit;
}
#lyrics-clip-btn:hover{background:#1a1a1a;}
.lyrics-clip-bar.picking #lyrics-clip-btn{border-color:var(--beta-red); color:var(--beta-red);}
#lyrics-clip-hint{font-size:11.5px; color:#888;}
.lyrics-scroll.clip-picking .ly-line{opacity:.6;}
.lyrics-scroll.clip-picking .ly-line[data-i]:hover{opacity:1; text-decoration:underline; text-underline-offset:5px;}
.lyrics-scroll.clip-picking .ly-line.clip-pick{opacity:1; color:var(--beta-red);}

/* ---- Synced lyrics (migration 068, build plan 2026-09-19) ----
   Same popup as "Read Lyrics" — one lyrics surface, no second button. When
   the track is the one playing and has approved timings, the popup gets
   .synced: wider, a tall inner scroller, and the Notes-style line-by-line
   highlight with the current word glowing inside the active line. Anything
   else falls through to the plain static text above, unchanged. */
#lyrics-popup.synced{width:520px;}
#lyrics-popup.synced .lyrics-scroll{
  position:relative; white-space:normal; font-size:20px; line-height:1.45; font-weight:700;
  max-height:min(62vh, 560px); padding:22vh 6px 30vh 0; letter-spacing:-.01em;
  -webkit-mask-image:linear-gradient(to bottom, transparent 0, #000 12%, #000 82%, transparent 100%);
          mask-image:linear-gradient(to bottom, transparent 0, #000 12%, #000 82%, transparent 100%);
}
.ly-line{margin:0 0 14px; color:#fff; opacity:.28; cursor:pointer; transition:opacity .25s ease, transform .25s ease; transform-origin:left center;}
.ly-line.past{opacity:.42;}
.ly-line.active{opacity:1; transform:scale(1.02);}
.ly-line.ly-gap{height:10px; margin:0 0 14px; cursor:default;}
.ly-line.ly-section{font-size:11px; font-weight:700; letter-spacing:.12em; text-transform:uppercase; opacity:.35; cursor:default; margin-top:22px;}
.ly-line:hover{opacity:.7;}
.ly-line.active:hover{opacity:1;}
.ly-line.active .ly-w{opacity:.55; transition:opacity .12s ease, text-shadow .12s ease;}
.ly-line.active .ly-w.sung{opacity:1;}
.ly-line.active .ly-w.now{opacity:1; text-shadow:0 0 14px rgba(255,255,255,.75), 0 0 2px rgba(255,255,255,.9);}
.ly-sync-note{font-size:11px; color:#777; margin:-2px 0 10px;}
@media(max-width:760px){
  #lyrics-popup.synced{width:94vw; max-width:94vw;}
  #lyrics-popup.synced .lyrics-scroll{font-size:19px; max-height:calc(100vh - 242px - 150px - env(safe-area-inset-bottom)); padding-top:16vh; padding-bottom:22vh;}
}
@media (prefers-reduced-motion: reduce){
  .ly-line, .ly-line.active .ly-w{transition:none;}
  .ly-line.active{transform:none;}
}

/* ---- Sign-in-required popup: shown when a signed-out visitor tries a
   members-only action (currently: creating a playlist). Same overlay/
   centered-box shell as #track-info-popup etc., just simpler content. ---- */
#signin-required-overlay{
  position:fixed; inset:0; background:rgba(0,0,0,.7); z-index:70; opacity:0; pointer-events:none; transition:opacity .2s ease;
}
#signin-required-overlay.show{opacity:1; pointer-events:auto;}
#signin-required-popup{
  position:fixed; top:50%; left:50%; transform:translate(-50%,-50%) scale(.96);
  width:340px; max-width:88vw;
  background:var(--sidebar); border:1px solid #262626; border-radius:14px; z-index:71;
  opacity:0; pointer-events:none; transition:opacity .2s ease, transform .2s ease;
  box-shadow:0 20px 60px rgba(0,0,0,.55);
  padding:34px 24px 26px; text-align:center;
}
#signin-required-popup.show{opacity:1; pointer-events:auto; transform:translate(-50%,-50%) scale(1);}
#signin-required-popup h4{margin:0 0 10px; font-size:16px; color:#fff;}
#signin-required-popup p{margin:0 0 20px; font-size:12.5px; line-height:1.6; color:var(--muted);}
#signin-required-popup .btn-accent{padding:10px 26px;}
#signin-required-close{
  position:absolute; top:10px; right:10px; background:none; border:none; color:var(--muted); cursor:pointer; padding:6px; z-index:2;
}
#signin-required-close:hover{color:var(--fg);}

/* ---- Subscribe nudge: same shell as the sign-in-required popup above, one
   step wider to fit the two plan buttons side by side on desktop. Shown 3
   minutes into a session to non-members only; see initSubscribePopup() in
   player.js for the timing and the every-3rd-visit suppression. ---- */
#subscribe-overlay, #personalize-overlay{
  position:fixed; inset:0; background:rgba(0,0,0,.7); z-index:70; opacity:0; pointer-events:none; transition:opacity .2s ease;
}
#subscribe-overlay.show, #personalize-overlay.show{opacity:1; pointer-events:auto;}
#subscribe-popup, #personalize-popup{
  position:fixed; top:50%; left:50%; transform:translate(-50%,-50%) scale(.96);
  width:380px; max-width:88vw;
  background:var(--sidebar); border:1px solid #262626; border-radius:14px; z-index:71;
  opacity:0; pointer-events:none; transition:opacity .2s ease, transform .2s ease;
  box-shadow:0 20px 60px rgba(0,0,0,.55);
  padding:34px 24px 24px; text-align:center;
}
#subscribe-popup.show, #personalize-popup.show{opacity:1; pointer-events:auto; transform:translate(-50%,-50%) scale(1);}
#subscribe-popup h4, #personalize-popup h4{margin:0 0 10px; font-size:16px; color:#fff;}
#subscribe-popup > p, #personalize-popup > p{margin:0 0 18px; font-size:12.5px; line-height:1.6; color:var(--muted);}
/* Personalize popup sits above the subscribe pair (z 70/71) so that even a
   pathological double-open renders the first-sign-in dialog on top. */
#personalize-overlay{z-index:72;}
#personalize-popup{z-index:73;}
#personalize-popup p b{color:#fff; font-weight:700;}
#personalize-popup a.sub-pop-btn--year{color:#ffffff;}
#subscribe-close{
  position:absolute; top:10px; right:10px; background:none; border:none; color:var(--muted); cursor:pointer; padding:6px; z-index:2;
}
#subscribe-close:hover{color:var(--fg);}
.sub-pop-btns{display:flex; flex-direction:column; gap:8px;}
.sub-pop-btn{
  display:flex; align-items:center; justify-content:center; gap:8px; flex-wrap:wrap;
  padding:12px 16px; border-radius:9px; font-size:13px; font-weight:700;
  text-decoration:none; line-height:1.2; border:1px solid transparent;
  transition:opacity .15s ease, border-color .15s ease;
}
.sub-pop-btn:hover{opacity:.9;}
.sub-pop-btn--month{background:#ffffff; color:#0c0c0c;}
.sub-pop-btn--year{background:var(--accent); color:#ffffff;}
/* The anchors sit inside a dark dialog where a generic `a{color:…}` rule
   would otherwise repaint the label — pin both explicitly so the white
   button never renders white-on-white. */
#subscribe-popup a.sub-pop-btn--month{color:#0c0c0c;}
#subscribe-popup a.sub-pop-btn--year{color:#ffffff;}
.sub-pop-badge{
  background:#e8c25a; color:#0c0c0c; font-size:10px; font-weight:800;
  letter-spacing:.04em; text-transform:uppercase; padding:3px 7px; border-radius:999px; white-space:nowrap;
}
.sub-pop-math{margin:14px 0 0; font-size:11px; line-height:1.5; color:var(--muted);}

/* ---- THE BAKERY join bar: slim strip under the header, matched to the one
   on razfresco.com and bkrsclb.com so all three read as one offer. Pure
   #000 against the app's --bg #0c0c0c, with a hairline under it, so it
   reads as a distinct strip rather than more header. Desktop only — the
   mobile breakpoint hides it and the subscribe popup covers that case. ---- */
.bakery-bar{
  display:flex; align-items:center; justify-content:center; gap:18px; flex-wrap:wrap;
  position:relative; background:#000000; border-bottom:1px solid #1c1c1c;
  padding:11px 56px 11px 28px;
}
.bakery-bar[hidden]{display:none;}
.bakery-bar-text{font-size:12.5px; line-height:1.4; color:var(--muted);}
.bakery-bar-text strong{color:var(--fg); font-weight:700;}
.bakery-bar-btns{display:inline-flex; align-items:center; gap:10px; flex-wrap:wrap;}
.bakery-bar-btn{
  display:inline-flex; align-items:center; gap:7px;
  padding:7px 14px; border-radius:7px; font-size:11.5px; font-weight:800;
  letter-spacing:.02em; text-transform:uppercase; text-decoration:none;
  line-height:1; white-space:nowrap; border:1px solid transparent;
  transition:opacity .15s ease, border-color .15s ease;
}
.bakery-bar-btn:hover{opacity:.88;}
.bakery-bar-btn--month{background:#ffffff; border-color:#ffffff;}
.bakery-bar-btn--year{background:transparent; border-color:#3a3a3a;}
.bakery-bar-btn--year:hover{border-color:#ffffff;}
/* Pinned by id: a generic `a{color:…}` would otherwise repaint the white
   button's label white-on-white. Same bug class as the popup above. */
#bakery-bar a.bakery-bar-btn--month{color:#0c0c0c;}
#bakery-bar a.bakery-bar-btn--year{color:#ffffff;}
.bakery-bar-badge{
  background:#e8c25a; color:#0c0c0c; font-size:9.5px; font-weight:800;
  letter-spacing:.04em; text-transform:uppercase; padding:3px 6px; border-radius:999px;
}
#bakery-bar-close{
  position:absolute; top:50%; right:16px; transform:translateY(-50%);
  background:none; border:none; color:var(--muted); cursor:pointer;
  font-size:13px; line-height:1; padding:6px;
}
#bakery-bar-close:hover{color:var(--fg);}
/* Mobile gets the popup instead — a bar this dense would eat the viewport. */
@media(max-width:760px){ .bakery-bar{display:none !important;} }

/* ---- THE BAKERY join block: sits under Featured Releases, above the
   catalog. Card surface and #262626 hairline are the same ones the rest of
   this app uses, so it reads as a section of the page rather than a banner
   dropped on top of it. Red × in the corner removes it. ---- */
.bakery-join{
  position:relative; display:flex; align-items:center; justify-content:space-between;
  gap:24px; flex-wrap:wrap;
  background:var(--card); border:1px solid #262626; border-radius:12px;
  padding:18px 52px 18px 22px; margin:0 0 26px;
}
.bakery-join[hidden]{display:none;}
.bakery-join__text{display:flex; flex-direction:column; gap:5px; min-width:0;}
.bakery-join__title{font-size:14px; font-weight:700; color:var(--fg); line-height:1.35;}
.bakery-join__sub{font-size:11.5px; color:var(--muted); line-height:1.45;}
.bakery-join__btns{display:inline-flex; align-items:center; gap:10px; flex-wrap:wrap;}
.bakery-join__btn{
  display:inline-flex; align-items:center; gap:7px;
  padding:10px 18px; border-radius:999px; font-size:12.5px; font-weight:700;
  text-decoration:none; line-height:1; white-space:nowrap;
  border:1.5px solid transparent; font-family:inherit;
  transition:opacity .15s ease, background .15s ease, border-color .15s ease;
}
.bakery-join__btn--month{background:#ffffff; border-color:#ffffff;}
.bakery-join__btn--month:hover{opacity:.88;}
.bakery-join__btn--year{background:transparent; border-color:var(--accent);}
.bakery-join__btn--year:hover{background:rgba(46,107,69,.18);}
/* Pinned: a generic `a{color:…}` would repaint the white pill's label
   white-on-white. Fourth time this guard has been needed in this project. */
#bakery-join a.bakery-join__btn--month{color:#0c0c0c;}
#bakery-join a.bakery-join__btn--year{color:var(--fg);}
.bakery-join__badge{
  background:#e8c25a; color:#0c0c0c; font-size:9.5px; font-weight:800;
  letter-spacing:.04em; text-transform:uppercase; padding:3px 6px; border-radius:999px;
}
/* Red on purpose — Raz asked for a clear one. Sized past the 44px touch
   minimum so it is not a coin-flip to hit on a phone. */
#bakery-join-close{
  position:absolute; top:8px; right:8px;
  width:30px; height:30px; display:flex; align-items:center; justify-content:center;
  background:none; border:1px solid transparent; border-radius:50%;
  color:var(--beta-red); cursor:pointer;
  font-size:14px; line-height:1; font-family:inherit; padding:0;
  transition:background .15s ease, border-color .15s ease;
}
#bakery-join-close:hover{background:rgba(224,39,43,.14); border-color:var(--beta-red);}
@media(max-width:760px){
  .bakery-join{
    padding:12px 12px 13px; margin-bottom:16px; gap:10px;
    flex-direction:column; align-items:flex-start; border-radius:10px;
  }
  /* The price maths is cut on phones, per Raz — it is reassurance, not the
     offer, and it costs a whole line of a screen that has none to spare.
     The $9.99/year button already carries the number that matters. */
  .bakery-join__sub{display:none;}
  .bakery-join__text{gap:0;}
  .bakery-join__title{font-size:12.5px; line-height:1.3; padding-right:30px;}
  /* Full-width buttons on a phone: a 50/50 split makes both labels wrap. */
  .bakery-join__btns{width:100%; flex-direction:column; align-items:stretch; gap:6px;}
  .bakery-join__btn{justify-content:center; padding:10px 14px; font-size:12px;}
  .bakery-join__badge{font-size:9px; padding:2px 5px;}
  /* Still comfortably tappable, just not oversized for a smaller card. */
  #bakery-join-close{width:30px; height:30px; top:4px; right:4px; font-size:14px;}
}

#track-context-menu, #album-context-menu{
  position:fixed; z-index:55; background:var(--card); border:1px solid #262626; border-radius:10px;
  padding:6px; box-shadow:0 12px 30px rgba(0,0,0,.55); display:none; flex-direction:column; min-width:170px;
}
#track-context-menu.show, #album-context-menu.show{display:flex;}
#track-context-menu button, #album-context-menu button{
  background:none; border:none; color:var(--fg); text-align:left; padding:9px 12px; font-size:12.5px;
  font-weight:600; cursor:pointer; border-radius:6px; font-family:inherit;
}
#track-context-menu button:hover, #album-context-menu button:hover{background:#232323;}

.photo-lightbox{
  position:fixed; inset:0; background:rgba(0,0,0,.7); z-index:60;
  display:flex; align-items:center; justify-content:center; padding:40px;
  cursor:zoom-out; animation:lightbox-fade .15s ease;
}
.photo-lightbox img{
  max-width:88vw; max-height:88vh; border-radius:10px; display:block;
  box-shadow:0 20px 60px rgba(0,0,0,.55); cursor:default;
}
@keyframes lightbox-fade{ from{opacity:0;} to{opacity:1;} }
.lightbox-arrow{
  position:fixed; top:50%; transform:translateY(-50%); z-index:61;
  background:rgba(20,20,20,.7); border:1px solid rgba(255,255,255,.18); color:#fff;
  width:44px; height:44px; border-radius:50%; font-size:20px; line-height:1;
  display:flex; align-items:center; justify-content:center; cursor:pointer;
}
.lightbox-arrow:hover{background:rgba(35,35,35,.85);}
.lightbox-arrow.hidden{display:none;}
.lightbox-prev{left:18px;}
.lightbox-next{right:18px;}
@media(max-width:760px){
  .lightbox-arrow{width:38px; height:38px; font-size:17px;}
  .lightbox-prev{left:8px;}
  .lightbox-next{right:8px;}
}

@media(max-width:760px){
  .player-bar .vol{display:none;}
  /* Mobile Now Playing "Option B": info row on top, then the scrub bar with
     elapsed/remaining timestamps, then a centered transport controls row
     below with bigger touch targets — all via a flex reflow of the same
     shared markup used on desktop. */
  .player-bar{flex-direction:column; align-items:stretch; gap:10px; padding:10px 14px calc(24px + env(safe-area-inset-bottom));}
  .player-bar .np{width:100%;}
  /* The heart sits at the end of the info row on a phone, where the thumb
     already is. 12px padding around a 17px icon clears the 44px tap target. */
  .player-bar .np-like{padding:12px; margin-right:-4px;}
  .player-bar .controls{max-width:none; width:100%; gap:8px;}
  .player-bar .progress-row{order:-1;}
  .player-bar .btns{justify-content:center; width:100%; gap:28px;}
  .player-bar .btns button{padding:8px;}
  .player-bar .btns .play{width:44px; height:44px;}
  .player-bar .btns .play svg{width:14px; height:14px;}
  header.site nav{gap:12px; font-size:12px;}
  /* 28px of side padding was costing the nav 56px of a 390px screen —
     more than the whole "Playlists" tab it was cutting in half. 16px
     also happens to be exactly what .main uses on a phone, so the header
     now lines up with the cards underneath it instead of sitting 12px
     further in. Measured at 375 / 390 / 430px, 2026-08-25. */
  header.site{flex-wrap:wrap; gap:10px; padding-left:16px; padding-right:16px;}
  .header-right{width:100%; justify-content:space-between; gap:10px;}
  /* A one-word "Shop" pill is narrower than the old "← Back to Store" text
     link, so nothing needs shrinking here — but the tighter row gap stays:
     the header-right row is the first thing to run out of width on a 390px
     phone, and it now leaves the search field wider than before. Re-measure
     at 390px before adding anything else to this row. */
  .search-wrap input{width:100%;}
  .search-wrap{flex:1;}
  .album-header{gap:16px;}
  .album-header .art{width:120px; height:120px;}
  .album-header .meta h2{font-size:20px;}
  .album-header .meta .artist-name-row{gap:10px;}
  .artist-shuffle-btn{width:34px; height:34px;}
  .artist-shuffle-btn svg{width:14px; height:14px;}
  #np-sidebar{width:100vw; max-width:100vw;}
  /* The Option B player bar is 3 rows tall on mobile instead of 1 — give the
     page enough bottom clearance so it doesn't cover the last catalog row. */
  .main{padding-left:16px; padding-right:16px; padding-bottom:calc(222px + env(safe-area-inset-bottom));}
  .grid-title-row{flex-wrap:wrap;}
  .rows-dropdown{display:inline-block;}
  .featured-arrow{display:none;}
  .featured-grid{grid-template-columns:1fr;}
  .featured-card{padding:16px; text-align:center; flex-direction:column; align-items:center;}
  .featured-card .art{width:96px; height:96px; margin:0 auto;}
  .featured-card .meta{width:100%;}
  .featured-tap-zone{
    position:absolute; top:17px; height:96px; width:34%; display:flex; align-items:center; z-index:5;
    cursor:pointer; color:#fff; background:none; border:none; padding:0;
  }
  .featured-tap-zone.left{left:0; justify-content:flex-start;}
  .featured-tap-zone.right{right:0; justify-content:flex-end;}
  .featured-tap-zone span{
    width:30px; height:30px; border-radius:50%; margin:0 8px; flex-shrink:0;
    background:rgba(0,0,0,.5); border:1px solid rgba(255,255,255,.22); box-shadow:0 4px 10px rgba(0,0,0,.35);
    display:flex; align-items:center; justify-content:center; font-size:16px; line-height:1;
  }
  /* minmax(0, 1fr), not plain 1fr — a bare 1fr track won't shrink past its
     content's natural width, which is exactly what caused the horizontal
     scroll: a fixed 3-per-row grid needs its tracks to be allowed to shrink. */
  .grid{grid-template-columns:repeat(3, minmax(0, 1fr)); gap:8px;}
  .card{padding:8px;}
  .card .t{font-size:11px;}
  .card .s{font-size:9.5px;}
  body.grid-density-2 .grid{grid-template-columns:repeat(2, minmax(0, 1fr)); gap:12px;}
  body.grid-density-2 .card{padding:12px;}
  body.grid-density-2 .card .t{font-size:12.5px;}
  body.grid-density-2 .card .s{font-size:11px;}
  body.grid-density-4 .grid{grid-template-columns:repeat(4, minmax(0, 1fr)); gap:6px;}
  body.grid-density-4 .card{padding:6px;}
  body.grid-density-4 .card .t{font-size:10px;}
  body.grid-density-4 .card .s{font-size:9px;}
}

/* ---- Series Info drawer ----
   A deliberately distinct "special edition" look (white background, serif
   headers, gold rule) so it reads like the physical vinyl packaging rather
   than another app panel — opens from the left, only while viewing a single
   series' detail page. */
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,600;1,500&family=EB+Garamond:ital,wght@0,400;0,500;1,400&display=swap');

/* ---- MY SELECTIONS — expandable library panel (feature spec 2026-08-15
   §2), replacing the old inline "Jump Back In" homepage row. Same fixed
   left-edge tab + slide-in drawer mechanism as Series Info directly below
   (confirmed with Raz as the pattern to extend — this app has no literal
   sidebar nav to hang a new entry off of), stacked above it per the
   spec's explicit "listed above it in tab/nav order" requirement.
   UPDATED 2026-08-16 (confirmed with Raz): the original spec locked this
   panel to grey background / black text, exempt from THE BAKERY's dark
   theme. That's superseded — the panel now uses the site's dark theme
   (--sidebar bg, white text), library cards use a charcoal tile with a
   white title / silver (var(--muted)) subtitle, and the launcher tab
   uses a silver gradient instead of flat light grey. Fixes a real
   contrast bug in the process: cards were pulling the dark-theme card
   background while titles inherited the panel's black text, landing
   near-black-on-near-black (~1.2:1 contrast). The top: offsets on this
   tab and on #series-info-tab below are a best estimate (both are
   rotated-text buttons whose real on-screen footprint isn't knowable
   without a live browser) — nudge them if the deployed pair overlaps or
   gaps oddly. */
#my-selections-tab{
  position:fixed; left:0; top:calc(50% - 92px); transform:translateY(-50%) rotate(-90deg) translateX(50%);
  transform-origin:left top;
  background:linear-gradient(180deg,#d4d7db,#b9bec4); color:#111111; font-weight:700; font-size:12.5px;
  letter-spacing:.06em; text-transform:uppercase; padding:10px 18px; border:none; border-radius:0 0 8px 8px;
  cursor:pointer; z-index:51;
  box-shadow:2px 0 12px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.5), inset 0 -1px 0 rgba(0,0,0,.15);
  align-items:center; gap:8px; display:flex;
  clip-path:inset(0 0 0 0); transition:clip-path .22s ease;
  /* Desktop drag (2026-08-17, widened same day per Raz — this was still
     highlighting text live even with the two properties below in place)
     drags with a held mouse button across the label text — without a
     complete no-select set the browser starts a text selection instead
     and the drag reads as a stuttering highlight. Added -moz- for
     Firefox and -webkit-user-drag to stop Safari/Chrome's separate
     native "drag this element" ghost-image behavior, which is a
     different browser feature from text selection and needs its own
     opt-out. */
  user-select:none; -webkit-user-select:none; -moz-user-select:none;
  -webkit-user-drag:none;
}
#my-selections-tab:hover{ background:linear-gradient(180deg,#c7cbcf,#a8adb3); }
/* Held-M feedback (2026-08-16): the keyboard shortcut requires holding M
   for 500ms, so the tab brightens to white over exactly that long — the
   hold reads as "filling up" instead of as a dead key. Colour only: no
   padding/size/position change, because this element's rotate math is
   derived from its own box (see the clip-path note below). The class is
   added on keydown and removed on keyup/blur/fire — see player.js. */
#my-selections-tab.ms-hold{
  background:linear-gradient(180deg,#ffffff,#e9ecef);
  box-shadow:2px 0 18px rgba(255,255,255,.45), inset 0 1px 0 rgba(255,255,255,.7), inset 0 -1px 0 rgba(0,0,0,.15);
  transition:background .5s linear, box-shadow .5s linear, clip-path .22s ease;
}
@media (prefers-reduced-motion: reduce){
  #my-selections-tab.ms-hold{ transition:none; }
}
/* Mobile default position (2026-08-16, round 2 per Raz): the shared
   "top:calc(50% - 92px)" rule above reads fine on a tall desktop viewport
   but on a short mobile viewport the fixed -92px term eats a much bigger
   share of the screen, landing the resting "tab" state up near the top
   third instead of roughly centered. Scoped to touch/coarse-pointer
   devices only (same feature test the JS state-machine below uses) so
   desktop is untouched. +248px is calibrated from Raz's marked-up
   screenshot against the app's own transform math (165x34 unrotated box,
   now retired) — it's an estimate, not something
   measured on a live device from this sandbox, so nudge it if the
   deployed position is off on his actual phone. Only applies to the
   resting default: dragging (see JS) or an in-progress touch swipe both
   write a direct inline top/style that overrides this rule as normal. */
@media (hover: none), (pointer: coarse) {
  #my-selections-tab{ top: calc(50% + 248px); }
}
/* ---- Round 6 (2026-08-17 per Raz — "i only want three states for my
   selections, hidden, the tab that says my selections and the expanded
   open window"): exactly three states now. "Collapsed" (the thin
   translucent sliver from rounds 3–4) is retired entirely, per Raz's
   explicit "remove the collapsed line state" — .ms-collapsed no longer
   exists anywhere in the CSS or JS.
   - **Hidden** (.ms-hidden below): nothing visible at all.
   - **Tab**: the full label+icons pill, unclipped — same look this
     element has always had at rest. Desktop's permanent default (no
     auto-hide, per Raz's original rule); mobile's default unless the
     "keep tab visible" checkbox inside the drawer is checked.
   - **Expanded**: the open drawer.
   The old clip-path-based constant-box technique (see the still-present
   `clip-path:inset(0 0 0 0)` on the base rule above, now permanently
   inert since nothing ever sets a different value) is no longer needed
   for a size-preserving trick — Hidden is done with opacity instead,
   which never touches layout, so there's nothing left to keep constant
   for. Left the base rule's clip-path property in place rather than
   removing it: it's fully inert now, and removing it added removal-risk
   for zero behavior change. */
#my-selections-tab.ms-hidden{
  opacity:0;
  pointer-events:none;
  transition:opacity .25s ease;
}
@media (prefers-reduced-motion: reduce){
  #my-selections-tab.ms-hidden{ transition:none; }
}
#my-selections-tab .ms-tab-icon{
  background:none; border:none; padding:0; margin:0; color:inherit; cursor:pointer;
  font-size:11px; line-height:1; display:flex; align-items:center; justify-content:center;
  position:relative;
}
/* Hit-target overlay (2026-08-17 per Raz: "the top button... does not
   expand"). Measured live on the deployed page, each icon's real
   clickable area was 11x11px — an 11px square is not a target anyone
   can reliably hit, which is the whole bug: the click handler worked
   fine whenever a click actually landed (verified with elementFromPoint
   plus a programmatic .click(), which opened the drawer every time).
   Done as an absolutely-positioned ::after rather than padding on the
   button, ON PURPOSE: padding would grow the button's layout box, which
   grows the container's box, and every position/rotate number at the
   top of this section is derived from that container box being exactly
   186x35 (see the .ms-collapsed clip-path note above for the last time
   changing the real box moved the tab ~170px off-target). A pseudo-
   element paints and hit-tests outside the button without touching
   layout at all, so the geometry is bit-for-bit unchanged.
   ~25x25 result, and it stays inside the container's border box so the
   base clip-path:inset(0) doesn't trim it. Clicks on it target the
   button itself, since pseudo-elements are never event targets. */
#my-selections-tab .ms-tab-icon::after{
  content:""; position:absolute; top:-7px; right:-7px; bottom:-7px; left:-7px;
}
#my-selections-tab .ms-tab-label{ white-space:nowrap; }
/* ---- Desktop reveal strip (2026-08-17 per Raz) — the way back from
   Hidden on a mouse. Round 6 made the left arrow a one-way trip to
   Hidden (.ms-hidden is pointer-events:none), which touch escapes with
   swipe-to-reveal; desktop has no swipe by Raz's explicit rule, so the
   only routes back were hold-M or a page reload, neither of which
   announces itself. Confirmed live on production before building this:
   after the left-arrow click, elementFromPoint over the launcher
   returns the page behind it, so there was genuinely no mouse target.
   Deliberately NOT a full-height edge strip: that would swallow clicks
   down the entire left edge of every page. player.js sizes it to the
   launcher's own footprint and only ever shows it while the launcher is
   hidden, so it occupies exactly the space the thing it restores used
   to occupy — including after a drag. Default is display:none; the JS
   is what reveals it, so on touch (where it's never wired up) it stays
   inert no matter what. */
#my-selections-reveal{
  display:none;
  position:fixed; left:0; width:5px; padding:0; border:none;
  background:linear-gradient(180deg,#d4d7db,#b9bec4);
  border-radius:0 4px 4px 0; cursor:pointer; z-index:52;
  box-shadow:2px 0 10px rgba(0,0,0,.35);
  opacity:.55; transition:opacity .2s ease, width .2s ease, box-shadow .2s ease;
  /* Draggable up/down (2026-08-17 per Raz), same as the tab — stops the
     browser starting a selection drag mid-gesture. */
  user-select:none; -webkit-user-select:none;
}
#my-selections-reveal.show{ display:block; }
#my-selections-reveal:hover, #my-selections-reveal:focus-visible{
  opacity:1; width:9px;
  box-shadow:2px 0 16px rgba(255,255,255,.35);
}
#my-selections-reveal:focus-visible{ outline:2px solid #ffffff; outline-offset:2px; }
/* Same trick as the icon buttons above: a generous invisible hit area
   without growing the visible sliver. 5px of silver is the right visual
   weight; 5px of target is not. */
#my-selections-reveal::after{
  content:""; position:absolute; top:0; bottom:0; left:-2px; right:-14px;
}
@media (prefers-reduced-motion: reduce){
  #my-selections-reveal{ transition:none; }
}
#my-selections-overlay{
  position:fixed; inset:0; background:rgba(0,0,0,.55); opacity:0; pointer-events:none; transition:opacity .3s; z-index:61;
}
#my-selections-overlay.show{ opacity:1; pointer-events:auto; }
#my-selections-drawer{
  position:fixed; top:0; bottom:0; left:0; width:420px; max-width:92vw;
  background:var(--sidebar); color:#ffffff; z-index:71;
  transform:translateX(-100%); transition:transform .35s cubic-bezier(.4,0,.2,1);
  display:flex; flex-direction:column; box-shadow:12px 0 40px rgba(0,0,0,.5);
}
#my-selections-drawer.show{ transform:translateX(0); }
.ms-head{
  padding:22px 24px 14px; border-bottom:1px solid rgba(255,255,255,.1); display:flex; align-items:center; justify-content:space-between;
}
.ms-head h3{ margin:0; font-size:22px; font-weight:800; color:#ffffff; }
#my-selections-close{ background:none; border:none; font-size:26px; color:#8a8a8a; cursor:pointer; line-height:1; padding:4px; }
/* Round 5 (2026-08-17 per Raz): view-preference setting, mobile only —
   desktop's resting default is already the always-visible "tab" state
   with no Hidden concept, so this row would be meaningless there. Placed
   right under the header per Raz's confirmed choice ("Inside the My
   Selections drawer"). */
.ms-settings{
  display:none;
  padding:10px 24px;
  border-bottom:1px solid rgba(255,255,255,.1);
}
@media (hover: none), (pointer: coarse) {
  .ms-settings{ display:block; }
}
.ms-settings-row{
  display:flex; align-items:center; gap:8px;
  font-size:12.5px; color:var(--muted);
  cursor:pointer;
}
.ms-settings-row input{ accent-color:#ffffff; cursor:pointer; width:15px; height:15px; flex:0 0 auto; }
.ms-tabs{ display:flex; padding:0 24px; border-bottom:1px solid rgba(255,255,255,.1); }
.ms-tab-btn{
  flex:1; background:none; border:none; padding:12px 6px; font-size:12.5px; font-weight:700;
  letter-spacing:.03em; text-transform:uppercase; color:#8a8a8a; cursor:pointer; font-family:inherit;
  border-bottom:2px solid transparent; margin-bottom:-1px;
}
.ms-tab-btn.active{ color:#ffffff; border-bottom-color:#ffffff; }
.ms-body{ padding:16px 24px 24px; overflow-y:hidden; -webkit-overflow-scrolling:touch; transform:translateZ(0); flex:1; }
.ms-body p.empty{ color:#8a8a8a; font-size:13px; padding:20px 0; text-align:center; }
.ms-body .grid{ grid-template-columns:repeat(2, minmax(0,1fr)); }
.ms-body .card{ background:#2a2a2a; border-color:rgba(158,158,158,.28); }
.ms-body .card:hover{ background:#333333; }
.ms-body .card .t{ color:#ffffff; font-weight:700; }
.ms-body .card .s{ color:var(--muted); }
.ms-body .track-row{ color:#ffffff; }
.ms-body .track-row:hover{ background:rgba(255,255,255,.06); }
.ms-body .track-row .num, .ms-body .track-row .dur{ color:var(--muted); }
.ms-body .track-row.playing{ color:#2e6b45; background:rgba(46,107,69,.15); }
.ms-body .track-row.playing .num{ color:#2e6b45; }
@media(max-width:760px){
  #my-selections-drawer{ width:100vw; max-width:100vw; }
}

#series-info-tab{
  position:fixed; left:0; top:calc(50% + 92px); transform:translateY(-50%) rotate(-90deg) translateX(50%);
  transform-origin:left top;
  background:var(--accent); color:#fff; font-weight:700; font-size:12.5px;
  letter-spacing:.06em; text-transform:uppercase; padding:10px 18px; border:none; border-radius:0 0 8px 8px;
  cursor:pointer; z-index:50; box-shadow:2px 0 12px rgba(0,0,0,.4);
  align-items:center; gap:8px;
}
#series-info-overlay{
  position:fixed; inset:0; background:rgba(0,0,0,.55); opacity:0; pointer-events:none; transition:opacity .3s; z-index:60;
}
#series-info-overlay.show{ opacity:1; pointer-events:auto; }
#series-info-drawer{
  position:fixed; top:0; bottom:0; left:0; width:520px; max-width:92vw;
  background:#ffffff; color:#1b1712; z-index:70;
  transform:translateX(-100%); transition:transform .35s cubic-bezier(.4,0,.2,1);
  display:flex; flex-direction:column; box-shadow:12px 0 40px rgba(0,0,0,.5);
}
#series-info-drawer.show{ transform:translateX(0); }
.si-head{
  padding:22px 26px 16px 26px; border-bottom:1px solid #ddd2ae; display:flex; align-items:flex-start; justify-content:space-between;
}
.si-eyebrow{ font-size:13.5px; letter-spacing:.26em; text-transform:uppercase; color:#9c7a2e; margin:0 0 8px 0; font-family:'EB Garamond',serif; }
.si-head h3{ font-family:'Playfair Display',serif; font-size:30px; margin:0; font-weight:600; color:#1b1712; }
#series-info-close{ background:none; border:none; font-size:26px; color:#5a5246; cursor:pointer; line-height:1; padding:4px; }
.si-body{ padding:20px 26px; overflow-y:hidden; -webkit-overflow-scrolling:touch; transform:translateZ(0); flex:1; font-family:'EB Garamond',serif; font-size:19px; line-height:1.75; }
.si-body p{ margin:0 0 17px 0; text-align:justify; color:#1b1712; }
.si-body p.empty{ color:#5a5246; text-align:left; }
.si-foot{ padding:18px 26px; border-top:1px solid #ddd2ae; display:flex; gap:10px; }
.si-foot a, .si-foot button{
  flex:1; text-align:center; padding:13px; border-radius:8px; text-decoration:none; font-weight:700; font-size:16px;
  cursor:pointer; font-family:inherit; box-sizing:border-box;
}
#series-info-pdf-link{ background:transparent; color:#1b1712; border:1.5px solid #1b1712; }
#series-info-close-2{ background:#1b1712; color:#f6f1e7; border:none; }

@media(max-width:760px){
  #series-info-drawer{ width:100vw; max-width:100vw; }
}

/* ---- PDF preview modal ----
   Deliberately mirrors the printed series dossier's own design language
   (black ground, gold rule + corner brackets, monospace "stamp" labels)
   rather than the cream/serif drawer — this is the artifact itself popping
   out, not another site panel. */
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Archivo:wght@700;800&display=swap');

#pdfp-overlay{
  position:fixed; inset:0; background:rgba(6,5,4,.82); z-index:85;
  opacity:0; pointer-events:none; transition:opacity .25s ease;
}
#pdfp-overlay.show{ opacity:1; pointer-events:auto; }
#pdfp-modal{
  position:fixed; inset:0; z-index:90; display:flex; align-items:center; justify-content:center;
  padding:4vh 3vw; opacity:0; pointer-events:none; transform:scale(.97);
  transition:opacity .25s ease, transform .25s cubic-bezier(.2,.8,.2,1);
}
#pdfp-modal.show{ opacity:1; pointer-events:auto; transform:scale(1); }
.pdfp-frame{
  position:relative; width:min(980px,94vw); height:min(90vh,1250px);
  background:#0b0a09; border:1px solid #6b5628; box-shadow:0 30px 90px rgba(0,0,0,.6);
  display:flex; flex-direction:column;
  background-image: radial-gradient(rgba(233,197,106,.05) 1px, transparent 1.2px);
  background-size:5px 5px;
}
.pdfp-corner{ position:absolute; width:22px; height:22px; border-color:#d7ac54; opacity:.9; pointer-events:none; }
.pdfp-corner.tl{ top:10px; left:10px; border-top:2px solid; border-left:2px solid; }
.pdfp-corner.tr{ top:10px; right:10px; border-top:2px solid; border-right:2px solid; }
.pdfp-corner.bl{ bottom:10px; left:10px; border-bottom:2px solid; border-left:2px solid; }
.pdfp-corner.br{ bottom:10px; right:10px; border-bottom:2px solid; border-right:2px solid; }
.pdfp-head{
  display:flex; align-items:center; justify-content:space-between; gap:16px;
  padding:22px 30px 16px; border-bottom:1px solid #4a3c1e;
}
.pdfp-eyebrow{
  margin:0 0 6px; font-family:'Space Mono',monospace; font-size:11px; letter-spacing:.25em;
  color:#d7ac54; text-transform:uppercase;
}
.pdfp-head h4{
  margin:0; font-family:'Archivo',sans-serif; font-weight:800; font-size:22px; letter-spacing:-.01em;
  color:#f4e2ac;
}
.pdfp-head-left{ min-width:0; }
.pdfp-head-right{ display:flex; align-items:center; gap:14px; flex-shrink:0; }
.pdfp-newtab{
  font-family:'Space Mono',monospace; font-size:12px; letter-spacing:.08em; text-transform:uppercase;
  color:#0b0a09; background:#d7ac54; border:1px solid #d7ac54; padding:9px 16px;
  text-decoration:none; white-space:nowrap; transition:background .15s, color .15s;
}
.pdfp-newtab:hover{ background:transparent; color:#e9c56a; }
#pdfp-close{
  background:none; border:1px solid #6b5628; color:#e9c56a; font-size:20px; line-height:1;
  width:36px; height:36px; cursor:pointer; flex-shrink:0;
}
#pdfp-close:hover{ background:#4a3c1e; }
.pdfp-body{ flex:1; padding:0; position:relative; overflow:hidden; background:#3a3226; }
.pdfp-body::before{
  content:""; position:absolute; inset:0; box-shadow:inset 0 12px 18px -14px rgba(0,0,0,.6); pointer-events:none; z-index:1;
}
.pdfp-pages{
  position:absolute; inset:0; overflow-y:auto; overflow-x:hidden;
  display:flex; flex-direction:column; align-items:center; gap:18px;
  padding:24px 0 40px;
}
.pdfp-pages canvas, .pdfp-pages img{
  max-width:calc(100% - 48px); height:auto; box-shadow:0 8px 30px rgba(0,0,0,.5); background:#fff; display:block;
}
.pdfp-loading{
  position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  transition:opacity .2s ease; pointer-events:none;
}
.pdfp-loading.hide{ opacity:0; }
.pdfp-loading-stamp{
  font-family:'Space Mono',monospace; font-size:12px; letter-spacing:.25em; text-transform:uppercase; color:#d7ac54;
}
.pdfp-error{
  position:absolute; inset:0; display:flex; align-items:center; justify-content:center; text-align:center; padding:0 40px;
  font-family:'Space Mono',monospace; font-size:13px; letter-spacing:.05em; color:#e9c56a; line-height:1.7;
}
.pdfp-foot{ padding:14px 30px 20px; border-top:1px solid #4a3c1e; flex-shrink:0; }
.pdfp-bottom-close{
  display:block; width:100%; box-sizing:border-box; padding:12px; text-align:center;
  font-family:'Space Mono',monospace; font-size:12px; letter-spacing:.08em; text-transform:uppercase;
  background:none; border:1px solid #6b5628; color:#e9c56a; cursor:pointer;
}
.pdfp-bottom-close:hover{ background:#4a3c1e; }

@media(max-width:760px){
  .pdfp-frame{ width:100vw; height:100vh; border:0; }
  .pdfp-head{ padding:18px 18px 14px; }
  .pdfp-head h4{ font-size:18px; }
  .pdfp-newtab{ padding:8px 12px; font-size:11px; }
  .pdfp-foot{ padding:12px 18px 16px; }
}

/* ---- quick commands (keyboard shortcut legend) ---- */
.qc-info-btn{
  position:fixed; right:20px; bottom:96px; z-index:60;
  width:38px; height:38px; border-radius:50%;
  background:#000; border:1px solid rgba(255,255,255,.18); color:#fff;
  font-family:Georgia,'Times New Roman',serif; font-style:italic; font-weight:700; font-size:21px;
  display:flex; align-items:center; justify-content:center; padding:0 0 2px;
  cursor:pointer; box-shadow:0 4px 16px rgba(0,0,0,.5);
  transition:transform .15s ease, background .15s ease, border-color .15s ease;
}
.qc-info-btn:hover{ transform:scale(1.08); background:#1a1a1a; border-color:rgba(255,255,255,.35); }
/* Mobile: the "i" button now stays visible (opens a shuffle-modes-only
   explainer — see openQuickCommands()'s isDesktopViewport() branch in
   player.js — since keyboard shortcuts don't apply without a keyboard).
   Lifted higher than the desktop bottom:96px offset because the mobile
   Now Playing bar stacks into 3 rows and runs taller than the single-row
   desktop bar that offset was tuned for; worth eyeballing on an actual
   phone after deploy since the bar's height can vary slightly with text
   size / long track titles wrapping. Shrunk a touch so it reads as a
   secondary control next to the transport buttons. */
@media(max-width:760px){
  .qc-info-btn{ right:14px; bottom:172px; width:34px; height:34px; font-size:18px; }
}

/* Long-press-to-hide menu for the mobile "i" button (see wireQcInfoHold()
   in player.js). Reuses #track-context-menu's look (dark card, small
   buttons) rather than inventing a new style. */
.qc-hide-menu{
  position:fixed; z-index:107; background:var(--card); border:1px solid #262626; border-radius:10px;
  padding:6px; box-shadow:0 12px 30px rgba(0,0,0,.55); display:none; flex-direction:column; min-width:160px;
}
.qc-hide-menu button{
  background:none; border:none; color:var(--fg); text-align:left; padding:9px 12px; font-size:12.5px;
  font-weight:600; cursor:pointer; border-radius:6px; font-family:inherit;
}
.qc-hide-menu button:hover{ background:#232323; }

/* ---- Cmd+S / Ctrl+S quick-search popup ----
   Reuses .qc-overlay/.qc-modal for the backdrop + centering behavior
   (same proven fixed/opacity/transform pattern as Quick Commands below);
   only the inner frame is bespoke since this holds a single search input
   rather than a tabbed list. */
.cmdsearch-frame{
  width:min(560px,92vw); background:#0f0f0f; border:1px solid #262626; border-radius:14px;
  box-shadow:0 30px 90px rgba(0,0,0,.6); padding:22px;
}
#cmdsearch-input{
  width:100%; box-sizing:border-box; background:#151515; border:1px solid #2c2c2c; border-radius:10px;
  color:#fff; font-size:18px; padding:14px 16px; font-family:inherit; outline:none;
}
#cmdsearch-input:focus{ border-color:#3a3a3a; }
.cmdsearch-hint{ margin:10px 2px 0; font-size:12px; color:#777; }

.qc-overlay{
  position:fixed; inset:0; background:rgba(0,0,0,.75); backdrop-filter:blur(2px); z-index:105;
  opacity:0; pointer-events:none; transition:opacity .22s ease;
}
.qc-overlay.show{ opacity:1; pointer-events:auto; }
.qc-modal{
  position:fixed; inset:0; z-index:106; display:flex; align-items:center; justify-content:center;
  padding:4vh 3vw; opacity:0; pointer-events:none; transform:scale(.96);
  transition:opacity .22s ease, transform .22s cubic-bezier(.2,.8,.2,1);
}
.qc-modal.show{ opacity:1; pointer-events:auto; transform:scale(1); }
.qc-frame{
  width:min(480px,92vw); max-height:86vh; overflow:hidden;
  background:#0f0f0f; border:1px solid #262626; border-radius:14px; box-shadow:0 30px 90px rgba(0,0,0,.6);
  display:flex; flex-direction:column;
}
.qc-head{
  display:flex; align-items:center; justify-content:space-between; padding:20px 22px 14px;
  border-bottom:1px solid #222;
}
.qc-head h4{ margin:0; font-size:17px; font-weight:800; color:#fff; letter-spacing:-.01em; }
#qc-close{
  background:none; border:1px solid #2c2c2c; color:#ccc; width:30px; height:30px; border-radius:7px;
  font-size:14px; cursor:pointer; display:flex; align-items:center; justify-content:center; flex-shrink:0;
}
#qc-close:hover{ background:#1a1a1a; }
.qc-foot{ padding:14px 22px 20px; border-top:1px solid #222; flex-shrink:0; }
.qc-foot .popup-bottom-close{ margin-top:0; }
.qc-tabs{ display:flex; gap:8px; padding:14px 22px 0; }
/* Mobile defaults to the Shuffle explainer (forced in openQuickCommands()
   via isDesktopViewport()), and Keyboard/List stay hidden there since they
   don't apply without a keyboard — but Shuffle and Features both work fine
   on a touchscreen, so those two tabs stay reachable on mobile. */
@media(max-width:760px){
  .qc-tabs{ display:flex; }
  .qc-tab{ display:none; }
  .qc-tab[data-view="shuffle"], .qc-tab[data-view="features"]{ display:block; }
}
.qc-tab{
  flex:1; background:#141414; border:1px solid #262626; color:#999; padding:8px 10px; border-radius:7px;
  font-size:12px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; cursor:pointer;
  transition:background .15s, color .15s, border-color .15s;
}
.qc-tab:hover{ color:#ccc; }
.qc-tab.active{ background:#173322; border-color:#2e6b45; color:#fff; }
/* flex:1 + min-height:0 are required, not decorative — without them a flex
   child defaults to min-height:auto (grows to fit its content instead of
   shrinking to the space left in .qc-frame's flex column), so overflow-y:auto
   here never actually kicks in and .qc-frame's overflow:hidden just silently
   clips whatever doesn't fit (worst on the Features tab, the longest list). */
.qc-body{ flex:1 1 auto; min-height:0; overflow-y:auto; padding-bottom:6px; -webkit-overflow-scrolling:touch; }

.qc-keyboard{ display:flex; flex-direction:column; gap:8px; align-items:center; padding:22px 16px 4px; }
.qc-row{ display:flex; gap:8px; align-items:flex-start; }
.qc-row--indent1{ margin-left:18px; }
.qc-row--indent2{ margin-left:34px; }
.qc-key{
  width:38px; height:38px; border-radius:6px; background:#161616; border:1px solid #262626;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  color:#666; font-family:'Space Mono',monospace; font-size:12px; letter-spacing:.02em;
}
.qc-key-letter{ font-weight:700; line-height:1.1; }
.qc-key-tag{ display:none; }
.qc-key--active{
  background:#173322; border-color:#2e6b45; color:#fff; box-shadow:0 0 0 1px rgba(46,107,69,.35) inset;
}
.qc-key--active .qc-key-tag{
  display:block; font-size:7.5px; letter-spacing:.05em; text-transform:uppercase; color:#7fd9a4;
  margin-top:1px; line-height:1.25; white-space:nowrap;
}
.qc-key--wide{ width:96px; }
.qc-key--space{ width:220px; }
.qc-key--dual{ height:54px; }
.qc-key--danger.qc-key--active{
  background:#3a1414; border-color:#e0454f; box-shadow:0 0 0 1px rgba(224,69,79,.35) inset;
}
.qc-key--danger.qc-key--active .qc-key-tag{ color:#f2a3a8; }
.qc-hint{
  margin:14px 22px 22px; font-size:11.5px; line-height:1.6; color:#8a8a8a; text-align:center;
}
.qc-hint strong{ color:#c9c9c9; }

.qc-list{ display:flex; flex-direction:column; gap:9px; padding:20px 22px 26px; }
.qc-list-item{
  display:flex; align-items:center; gap:14px; padding:10px 14px;
  background:#141414; border:1px solid #232323; border-radius:8px;
}
.qc-list-key{
  min-width:52px; text-align:center; padding:5px 8px; background:#173322; border:1px solid #2e6b45;
  border-radius:5px; font-family:'Space Mono',monospace; font-size:11.5px; font-weight:700; color:#7fd9a4;
  flex-shrink:0;
}
.qc-list-key--danger{ background:#3a1414; border-color:#e0454f; color:#f2a3a8; }
.qc-list-desc{ font-size:13px; color:#e6e6e6; line-height:1.4; }

@media(max-width:520px){
  .qc-frame{ width:94vw; }
  .qc-key{ width:29px; height:33px; font-size:10px; }
  .qc-key-tag{ font-size:6.5px; }
  .qc-key--wide{ width:74px; }
  .qc-key--space{ width:168px; }
  .qc-key--dual{ height:46px; }
  .qc-row--indent1{ margin-left:12px; }
  .qc-row--indent2{ margin-left:24px; }
  .qc-row{ gap:6px; }
}

/* ---- Playlists (user-created, client-side only) ---- */
.card .art.playlist-art,
.album-header .art.playlist-art{
  display:flex; align-items:center; justify-content:center; background-image:none !important;
  background:linear-gradient(135deg,#1b1b1b,#0c0c0c);
}
/* wrap: Produced By Raz now carries three pills (Shuffle, Share, Series
   Info) and three 20px-padded pills do not fit a 390px column. Same
   failure the mobile nav had when the Watch tab was added. */
.playlist-detail-actions{display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin-top:2px;}
.edit-toggle-btn{
  background:none; border:1px solid #2c2c2c; color:var(--fg); border-radius:999px; padding:9px 20px;
  font-weight:700; font-size:13px; cursor:pointer; font-family:inherit;
}
.edit-toggle-btn:hover{background:#1a1a1a;}
.edit-toggle-btn.active{background:var(--accent); border-color:var(--accent); color:#fff;}
.delete-playlist-btn{
  background:none; border:1px solid #3a1414; color:#e0454f; border-radius:999px; padding:9px 20px;
  font-weight:700; font-size:13px; cursor:pointer; font-family:inherit;
}
.delete-playlist-btn:hover{background:#3a1414; color:#f2a3a8; border-color:#e0454f;}

.playlist-card{position:relative;}
.playlist-card-delete{
  position:absolute; top:8px; right:8px; z-index:3; padding:0;
  width:24px; height:24px; border-radius:50%; border:1px solid rgba(255,255,255,.15);
  background:rgba(10,10,10,.75); color:#fff; font-size:16px; line-height:1; cursor:pointer;
  display:flex; align-items:center; justify-content:center; opacity:0;
  transition:opacity .15s ease, background .15s ease, border-color .15s ease; font-family:inherit;
}
.playlist-card:hover .playlist-card-delete{opacity:1;}
.playlist-card-delete:hover{background:#3a1414; color:#f2a3a8; border-color:#e0454f;}
@media(max-width:760px){
  .playlist-card-delete{opacity:1;}
}

.track-row.editing{grid-template-columns:32px 1fr auto auto;}
.track-edit-controls{display:flex; align-items:center; gap:4px;}
.track-edit-controls .pl-move,
.track-edit-controls .pl-remove{
  background:none; border:none; color:var(--muted); cursor:pointer; font-size:11px; line-height:1;
  padding:6px; border-radius:6px; display:flex; align-items:center; justify-content:center; font-family:inherit;
}
.track-edit-controls .pl-move:hover,
.track-edit-controls .pl-remove:hover{background:#232323; color:var(--fg);}
.track-edit-controls .pl-move:disabled{opacity:.3; cursor:default;}
.track-edit-controls .pl-move:disabled:hover{background:none; color:var(--muted);}
.track-edit-controls .pl-remove{font-size:15px; color:#e0454f;}
.track-edit-controls .pl-remove:hover{background:#3a1414; color:#f2a3a8;}
.track-edit-controls .pl-drag-handle{
  color:var(--muted); cursor:grab; padding:6px 2px; font-size:13px; line-height:1; letter-spacing:-2px;
  display:flex; align-items:center; justify-content:center; user-select:none; touch-action:none;
}
.track-edit-controls .pl-drag-handle:hover{color:var(--fg);}
.track-edit-controls .pl-drag-handle:active{cursor:grabbing;}
.track-row.editing.dragging{opacity:.4;}
.track-row.editing.drag-over{background:var(--card); box-shadow:inset 0 2px 0 var(--accent);}

.playlist-add-wrap{position:relative; display:inline-flex;}
/* Literal for the gradient, matching the shuffle icon's idle colour — see
   the contrast note on .player-bar. */
.player-bar .btns #playlist-add-btn{color:rgba(255,255,255,.66);}
.player-bar .btns #playlist-add-btn:hover{color:#ffffff;}
.playlist-add-menu{
  position:absolute; bottom:calc(100% + 14px); left:50%; transform:translateX(-50%) translateY(6px);
  background:var(--card); border:1px solid #262626; border-radius:10px; width:220px;
  opacity:0; pointer-events:none; transition:opacity .16s ease, transform .16s ease;
  box-shadow:0 12px 30px rgba(0,0,0,.55); overflow:hidden; z-index:45; text-align:left;
  display:flex; flex-direction:column;
}
.playlist-add-wrap.open .playlist-add-menu{opacity:1; pointer-events:auto; transform:translateX(-50%) translateY(0);}
#playlist-add-list{display:flex; flex-direction:column;}
.playlist-add-item{
  display:flex; align-items:center; justify-content:space-between; gap:10px; width:100%; text-align:left;
  background:none; border:none; color:var(--fg); font-size:12.5px; font-weight:600; padding:10px 14px;
  cursor:pointer; font-family:inherit; border-bottom:1px solid #1f1f1f;
}
.playlist-add-item:hover{background:#1f1f1f;}
.playlist-add-item .pl-name{overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.playlist-add-item .pl-count{color:var(--muted); font-size:11px; font-weight:400; flex-shrink:0;}
.playlist-add-create-trigger{
  display:block; width:100%; text-align:left; background:none; border:none; color:var(--accent);
  font-size:12.5px; font-weight:700; padding:11px 14px; cursor:pointer; font-family:inherit;
}
.playlist-add-create-trigger:hover{background:#1f1f1f;}
#playlist-add-list p.empty.small{padding:14px 14px 6px; font-size:12px; margin:0;}
#playlist-add-create{display:flex; flex-direction:column; gap:10px; padding:12px 14px;}
#playlist-create-name{
  background:#0c0c0c; border:1px solid #2c2c2c; border-radius:7px; color:var(--fg); font-size:12.5px;
  padding:9px 11px; outline:none; font-family:inherit; width:100%;
}
#playlist-create-name:focus{border-color:var(--accent);}
.playlist-create-actions{display:flex; align-items:center; justify-content:flex-end; gap:8px;}
.playlist-create-actions button{
  background:none; border:none; color:var(--muted); font-size:12px; font-weight:700; cursor:pointer;
  padding:8px 12px; border-radius:6px; font-family:inherit;
}
.playlist-create-actions button:hover{background:#1f1f1f; color:var(--fg);}
#playlist-create-confirm{background:var(--accent); color:#fff;}
#playlist-create-confirm:hover{background:var(--accent); opacity:.9; color:#fff;}

@media(max-width:760px){
  .playlist-add-menu{width:200px;}
}

/* The old single ".playlist-empty" bubble lived here. It was replaced by the
   per-column teaching cards below (.pl-teach) and nothing renders it any
   more, so the rules went with it rather than sitting as dead weight. */

/* ---- Playlist page — "Liked Songs" (left) / "My Playlists" (right) split ----
   One idea per column. Liked Songs is a fixed system list; MY PLAYLISTS holds
   this member's own created playlists plus anything they've saved (an
   editorial playlist or another member's — see playlist-saves.js). EDITORIAL
   is admin-curated and now runs full width UNDERNEATH the split rather than
   occupying the right column, because it is label content and does not belong
   under a heading that says "My". Stacks to one column on narrow screens. */
.playlist-columns{
  display:grid; grid-template-columns:1fr 1.1fr; gap:0; align-items:stretch;
}
.playlist-col-liked{padding-right:40px;}
.playlist-col-playlists{padding-left:40px; border-left:1px solid #1f1f1f;}
.playlist-col-liked .grid-title,
.playlist-col-playlists .grid-title{margin-top:0;}
.saved-title{margin-top:32px !important;}
#editorial-section{margin-top:38px; padding-top:30px; border-top:1px solid #1f1f1f;}
#editorial-section .grid-title{margin-top:0;}
@media(max-width:900px){
  .playlist-columns{grid-template-columns:1fr; gap:0;}
  .playlist-col-liked{padding-right:0;}
  .playlist-col-playlists{
    margin-top:12px; padding-top:28px; padding-left:0;
    border-left:none; border-top:1px solid #1f1f1f;
  }
}

/* ---- Empty-column teaching cards ----
   These replace the single grey "you haven't made any playlists yet" box.
   Each fills its column (grid-column:1/-1 — the parent is a .grid, see the
   layout conventions note) and carries an action, so an empty account has
   something to press rather than a wall of black. */
.pl-teach{
  grid-column:1/-1;
  /* margin-top must stay 0: the card in the Playlists column and the card in
     the Liked column sit at the same y, and any top margin here shifts only
     one of them (the Liked one is inside a flex row that zeroes it). */
  margin:0; padding:26px 24px;
  background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02));
  border:1px solid rgba(255,255,255,.09);
  border-radius:18px;
}
.pl-teach__icon{
  width:46px; height:46px; border-radius:50%; margin:0 0 15px;
  background:color-mix(in srgb, var(--accent) 22%, transparent);
  color:color-mix(in srgb, var(--accent) 65%, #fff 35%);
  border:1px solid color-mix(in srgb, var(--accent) 38%, transparent);
  display:flex; align-items:center; justify-content:center;
}
.pl-teach__icon svg{width:22px; height:22px; display:block;}
.pl-teach__title{
  margin:0 0 9px; font-size:19px; font-weight:700;
  color:var(--fg); letter-spacing:-.012em; line-height:1.25;
}
.pl-teach__body{margin:0; color:var(--muted); font-size:14px; line-height:1.6;}
/* The Liked column always has one tile in it (Liked Songs is a fixed system
   list), so a full-width card underneath would start lower than the card in
   the empty Playlists column and the two would not line up. Switching that
   grid to a flex row puts the card BESIDE the tile, tops aligned, which is
   the whole point of showing two of them. Wraps on narrow columns. */
#liked-grid.has-teach{display:flex; flex-wrap:wrap; gap:16px; align-items:flex-start;}
#liked-grid.has-teach > .card{flex:0 0 168px; width:168px;}
#liked-grid.has-teach > .pl-teach{flex:1 1 280px; margin-top:0;}
.pl-teach__cta{
  margin-top:18px; display:inline-block; cursor:pointer;
  padding:10px 18px; border-radius:999px;
  background:var(--accent); color:#fff; border:1px solid var(--accent);
  font-size:13.5px; font-weight:700; font-family:inherit;
  transition:opacity .15s ease;
}
.pl-teach__cta:hover{opacity:.9;}
.pl-teach__cta:focus-visible{outline:2px solid var(--accent); outline-offset:3px;}

.editorial-grid{grid-template-columns:repeat(auto-fill,minmax(190px,1fr));}
.card.editorial-card{position:relative; padding-bottom:44px;}
/* Editorial-column display pictures are HORIZONTAL 16:9 (Raz, 2026-08-17)
   — a deliberate visual distinction from the square user-playlist tiles —
   and the photo shows UNCROPPED: object-fit/background-size contain, with
   black letterbox bars when the upload isn't exactly 16:9. Applies to
   editorial playlist cards (this rule), the Produced By Raz / Unreleased
   cards in the same column, and their detail-page header art (.wide-art,
   added by player.js only on those surfaces — the Series grid and album
   pages keep square art). */
.card .art.editorial-art{
  aspect-ratio:16/9;
  background:linear-gradient(135deg,#1b1b1b,#0c0c0c);
}
.card .art.editorial-art img{width:100%; height:100%; object-fit:contain; border-radius:6px; background:#000;}
.card .art.wide-art{aspect-ratio:16/9; background-size:contain; background-repeat:no-repeat; background-position:center; background-color:#000;}
.card .art.wide-art img{object-fit:contain; background:#000;}
.album-header .art.wide-art{width:320px; height:180px; background-size:contain; background-repeat:no-repeat; background-position:center; background-color:#000;}
.album-header .art.wide-art img{object-fit:contain; background:#000;}
@media(max-width:760px){ .album-header .art.wide-art{width:213px; height:120px;} }
.editorial-desc{
  font-size:11.5px; color:var(--muted); margin:2px 0 4px; line-height:1.4;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
.playlist-save-btn{
  position:absolute; left:12px; right:12px; bottom:12px;
  background:none; border:1px solid #2c2c2c; color:var(--fg); border-radius:999px;
  padding:6px 10px; font-weight:700; font-size:11px; cursor:pointer; font-family:inherit;
  transition:background .15s ease, border-color .15s ease, color .15s ease;
}
.playlist-save-btn:hover{background:#1a1a1a;}
.playlist-save-btn.saved{background:color-mix(in srgb, var(--accent) 18%, transparent); border-color:var(--accent); color:var(--accent);}

.saved-playlist-card .s{overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}

.editorial-detail-desc{margin-top:2px; max-width:520px; line-height:1.5;}

#editorial-detail-save.active{background:var(--accent); border-color:var(--accent); color:#fff;}

/* ---- My Profile overlay ----
   Loads /profile.html inside an iframe instead of navigating there, so
   opening the profile never stops whatever's playing in this page's
   <audio> element — same "it's just a DOM overlay" principle as the photo
   lightbox and Quick Commands panel above, just applied to a full page. */
.profile-ov{
  position:fixed; inset:0; background:rgba(0,0,0,.75); backdrop-filter:blur(2px); z-index:120;
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .22s ease, visibility 0s linear .22s;
}
.profile-ov.show{
  opacity:1; visibility:visible; pointer-events:auto;
  transition:opacity .22s ease, visibility 0s;
}
/* SAFARI: `visibility:hidden` on the closed state is load-bearing, not
   decoration. These two overlays are always in the DOM and each holds a
   full-viewport <iframe> pinned to inset:0. Safari does NOT honour
   `pointer-events:none` for the `contextmenu` event over an iframe (it does
   honour it for click), so with opacity alone the invisible about:blank
   frame swallowed every right-click on the page — Raz got Safari's "Open
   Frame in New Tab / Print Frame…" menu instead of the app's cover share
   menu, while hold-left-click still worked. `visibility:hidden` takes the
   frame out of hit-testing entirely in every engine. The `visibility 0s
   linear .22s` delay keeps it visible for the full fade-OUT; the .show rule
   drops the delay so it appears instantly on the way in. */
.profile-modal{
  position:fixed; inset:0; z-index:121; display:flex; align-items:center; justify-content:center;
  padding:4vh 3vw; opacity:0; visibility:hidden; pointer-events:none; transform:scale(.97);
  transition:opacity .22s ease, transform .22s cubic-bezier(.2,.8,.2,1), visibility 0s linear .22s;
}
.profile-modal.show{
  opacity:1; visibility:visible; pointer-events:auto; transform:scale(1);
  transition:opacity .22s ease, transform .22s cubic-bezier(.2,.8,.2,1), visibility 0s;
}
.profile-modal-frame{
  /* Round 13: widened per Raz's report that the "Community" heading felt
     cropped against the frame edge — this frame is shared by both the My
     Profile and Community overlays (see the Phase 3 sub-step 5 notes), so
     the bump benefits both. */
  position:relative; width:min(1320px,97vw); height:min(94vh,1000px);
  background:#0a0a0a; border:1px solid #262626; border-radius:16px; box-shadow:0 30px 90px rgba(0,0,0,.6);
  overflow:hidden;
}
.profile-modal-close{
  /* Red circle + white X per Raz's ask, matching #np-sidebar-close /
     #track-info-close / #lyrics-popup-close / #track-comment-close —
     shared by both My Profile and Community since they both use this
     class, which is the desired look for both. */
  position:absolute; top:14px; right:14px; z-index:2;
  background:var(--beta-red); border:none; color:#fff; width:34px; height:34px; border-radius:50%;
  font-size:17px; cursor:pointer; display:flex; align-items:center; justify-content:center;
}
.profile-modal-close:hover{ opacity:.85; }

/* Reusable "reachable bottom close button" — added to every popup/overlay
   sitewide per Raz's ask, so closing never requires reaching back up to a
   small corner X. Sits as the last element of a popup's own scrollable
   content (or a dedicated foot area) rather than floating fixed, so it
   never overlaps content and needs no extra scroll-region wiring. */
.popup-bottom-close{
  display:block; width:100%; margin-top:22px; padding:13px; border-radius:10px;
  border:1px solid #2a2a2a; background:#161616; color:#fff; font-size:14px; font-weight:700;
  cursor:pointer; font-family:inherit; text-align:center; box-sizing:border-box;
}
.popup-bottom-close:hover{ border-color:var(--beta-red); }
.profile-modal-frame iframe{
  width:100%; height:100%; border:none; display:block; background:#0a0a0a;
}
@media(max-width:760px){
  .profile-modal{ padding:0; }
  .profile-modal-frame{ width:100vw; height:100vh; border-radius:0; border:none; }
}

/* ---- Comments (Phase 4) ----
   Adds on to the existing album page and track right-click menu without
   changing their layout: an inline card under the tracklist for
   album-level comments, and a compact popup (matching #track-info-popup /
   #lyrics-popup) for track-level comments reached via the right-click
   "Comment" option, plus a small count pill on tracklist rows that have
   comments. Signed-out visitors can read everything; posting requires
   sign-in via the same "Continue with Shopify" flow used across the site. */

.comment-count-pill{
  background:#1c1c1c; border:1px solid #2c2c2c; color:var(--muted);
  font-size:10.5px; font-weight:700; padding:2px 7px; border-radius:999px;
}

.comments-card{
  background:var(--card); border:1px solid #1f1f1f; border-radius:14px;
  padding:22px 24px; margin-top:24px;
}
.comments-head{display:flex; align-items:baseline; gap:8px; margin-bottom:16px;}
.comments-head h2{font-size:15px; margin:0;}
.comments-count{color:var(--muted); font-size:12.5px;}

.signin-nudge{
  background:#161616; border:1px solid #262626; border-radius:10px;
  padding:16px 18px; display:flex; align-items:center; justify-content:space-between;
  gap:14px; flex-wrap:wrap; margin-bottom:16px;
}
.signin-nudge p{margin:0; font-size:12.5px; color:var(--muted);}
.btn-accent{
  background:var(--accent); color:#fff; border:none; border-radius:999px;
  padding:9px 20px; font-weight:700; font-size:12.5px; cursor:pointer; text-decoration:none;
  display:inline-block; white-space:nowrap; font-family:inherit;
}
.btn-accent:hover{opacity:.9;}
.btn-accent:disabled{opacity:.45; cursor:default;}

.composer{display:flex; gap:12px; margin-bottom:20px;}
.composer-avatar{
  width:36px; height:36px; border-radius:50%; flex:none;
  background:#000 url('/assets/default-avatar.svg') center/cover no-repeat;
  border:1px solid #262626; position:relative;
}
.composer-body{flex:1; min-width:0;}
.composer-body textarea{
  width:100%; background:#161616; border:1px solid #262626; border-radius:10px;
  color:var(--fg); font-family:inherit; font-size:13px; padding:10px 12px; resize:vertical;
  min-height:44px; box-sizing:border-box;
}
.composer-body textarea:focus{outline:none; border-color:#3a3a3a;}
.composer-actions{display:flex; align-items:center; justify-content:flex-end; margin-top:8px;}

.comment-list{display:flex; flex-direction:column; gap:16px;}
.comment-item{display:flex; gap:12px;}
/* No photo => the BKRSCLB chef-hat default (2026-08-24). A real avatar is
   rendered as a child <img> that covers this box, so the fallback only
   ever shows when there's nothing to cover it. */
.comment-avatar{
  width:34px; height:34px; border-radius:50%; flex:none;
  background:#000 url('/assets/default-avatar.svg') center/cover no-repeat;
  border:1px solid #262626; position:relative;
}
.comment-body{flex:1; min-width:0;}
.comment-head{display:flex; align-items:baseline; gap:8px;}
.comment-name{font-weight:700; font-size:12.5px;}
.comment-time{color:var(--muted); font-size:11px;}
.comment-text{font-size:13px; line-height:1.55; color:var(--fg); white-space:pre-wrap; margin:3px 0 0; word-break:break-word;}
.comment-actions{display:flex; margin-top:4px;}
.comment-delete{
  background:none; border:none; color:var(--muted); font-size:11px; cursor:pointer;
  padding:0; font-family:inherit;
}
.comment-delete:hover{color:#e05252;}
.comment-report{
  background:none; border:none; color:var(--muted); font-size:11px; cursor:pointer;
  padding:0; font-family:inherit; margin-left:10px;
}
.comment-report:hover{color:var(--fg);}
.comment-report:disabled{color:#555; cursor:default;}

.load-more{
  display:block; margin:16px auto 0; background:none; border:1px solid #2c2c2c; color:var(--fg);
  border-radius:999px; padding:8px 18px; font-size:12px; font-weight:700; cursor:pointer; font-family:inherit;
}
.load-more:hover{background:#1a1a1a;}
.comments-empty{color:var(--muted); font-size:12.5px; margin:0;}

/* Track-level comment popup — mirrors #track-info-popup / #lyrics-popup
   exactly (Style A, compact) so it reads as a sibling of the existing
   "Read Lyrics"/"See Info Card" popups rather than a new pattern. Its
   shared position/size/transform/overflow rules now live in the combined
   #track-info-popup, #lyrics-popup, #track-comment-popup selector above —
   only what's unique to the comment popup stays here. */
#track-comment-overlay{
  position:fixed; inset:0; background:rgba(0,0,0,.7); z-index:65; opacity:0; pointer-events:none; transition:opacity .2s ease;
}
#track-comment-overlay.show{opacity:1; pointer-events:auto;}
#track-comment-popup h3{margin:0 0 3px; font-size:18px;}
#track-comment-popup .comments-count{display:block; margin-bottom:14px;}
#track-comment-popup .comment-list{max-height:260px; overflow-y:hidden; -webkit-overflow-scrolling:touch; transform:translateZ(0); padding-right:2px;}

@media(max-width:760px){
  .comment-item, .composer{gap:10px;}
}


/* ---- Discovery Shuffle invite popup (5s after first-ever visit, or
   after 2 minutes with nothing playing — see the "discovery popup"
   section in player.js). Deliberately built as a normal in-page overlay
   (same pattern as every other modal in this file), not a native
   window/alert, so it reads as part of the site rather than a spam
   popup. Sized to feel big — most of the viewport on mobile, a large
   centered card on desktop. */
.discovery-overlay{
  position:fixed; inset:0; background:rgba(0,0,0,.82); backdrop-filter:blur(3px); z-index:130;
  opacity:0; pointer-events:none; transition:opacity .25s ease;
}
.discovery-overlay.show{ opacity:1; pointer-events:auto; }
.discovery-modal{
  position:fixed; inset:0; z-index:131; display:flex; align-items:center; justify-content:center;
  padding:5vh 5vw; opacity:0; pointer-events:none; transform:scale(.96);
  transition:opacity .25s ease, transform .25s cubic-bezier(.2,.8,.2,1);
}
.discovery-modal.show{ opacity:1; pointer-events:auto; transform:scale(1); }
.discovery-frame{
  background:var(--card); border:1px solid #262626; border-radius:18px;
  width:min(640px, 100%); max-height:86vh; overflow-y:auto;
  padding:56px 48px; text-align:center; box-shadow:0 30px 80px rgba(0,0,0,.6);
}
.discovery-eyebrow{ color:var(--accent); font-size:12px; font-weight:800; letter-spacing:.14em; text-transform:uppercase; margin:0 0 14px; }
.discovery-frame h2{ font-size:30px; margin:0 0 18px; line-height:1.2; }
.discovery-copy{ color:var(--muted); font-size:14.5px; line-height:1.6; margin:0 0 32px; }
.discovery-actions{ display:flex; gap:14px; justify-content:center; flex-wrap:wrap; }
.discovery-btn{
  padding:14px 28px; border-radius:999px; font-size:14px; font-weight:800; cursor:pointer;
  font-family:inherit; letter-spacing:.01em; border:1px solid transparent;
}
.discovery-btn-ghost{ background:none; border-color:#333; color:#ccc; }
.discovery-btn-ghost:hover{ background:#1a1a1a; }
.discovery-btn-accent{ background:var(--accent); color:#fff; }
.discovery-btn-accent:hover{ opacity:.9; }
@media(max-width:760px){
  /* "Take up most of the screen" on mobile: near-fullscreen card instead
     of a centered floating one. */
  .discovery-modal{ padding:0; }
  .discovery-frame{
    width:100%; height:100%; max-height:none; border-radius:0;
    display:flex; flex-direction:column; justify-content:center; padding:32px 26px;
  }
  .discovery-frame h2{ font-size:24px; }
}

/* ── Trust / transparency copy ──────────────────────────────────────────
   Added 2026-08-25 in response to a Google Safe Browsing "deceptive pages"
   flag on bkrsclb.com. Nothing here changes behaviour — it exists so that
   both people and automated scanners can see, without clicking, that:
     • the "Sign in" button is our own store account (OAuth via Shopify),
       not a request for someone else's credentials, and
     • leaving for razfresco.com at checkout is an intentional, related-brand
       handoff rather than an off-site redirect to an unrelated payment page.
   Keep this copy visible. Hiding it re-creates exactly the pattern that
   tripped the classifier in the first place. */
.auth-note{
  display:block;
  max-width:420px;
  margin:10px auto 0;
  font-size:11px;
  line-height:1.5;
  color:var(--muted);
  text-align:center;
}
.checkout-note{
  color:var(--muted);
  font-size:11px;
  line-height:1.5;
}
.bakery-bar-text .checkout-note{
  display:inline;
  opacity:.75;
  margin-left:6px;
}
@media(max-width:760px){
  .bakery-bar-text .checkout-note{display:none;}
}

/* Small legal/contact footer shown at the bottom of every public page. */
.site-legal{
  max-width:900px;
  margin:56px auto 28px;
  padding:22px 20px 0;
  border-top:1px solid #1f1f1f;
  text-align:center;
  font-size:11px;
  line-height:1.7;
  color:var(--muted);
}
.site-legal a{color:var(--muted);text-decoration:underline;}
.site-legal a:hover{color:#fff;}
.site-legal .site-legal__sep{opacity:.4;margin:0 8px;}

/* Standalone legal/contact page body. */
.legal-page{
  max-width:760px;
  margin:0 auto;
  padding:40px 20px 80px;
  line-height:1.7;
}
.legal-page h1{font-size:26px;margin:0 0 6px;}
.legal-page h2{font-size:15px;margin:34px 0 8px;}
.legal-page p,.legal-page li{color:var(--muted);font-size:13px;}
.legal-page ul{padding-left:20px;}
.legal-page .legal-updated{font-size:11px;opacity:.7;margin-bottom:28px;}

/* Keep the legal footer clear of the fixed playback bar on the main app. */
body .site-legal{padding-bottom:130px;}
.legal-page ~ .site-legal{padding-bottom:0;}


/* ============================================================
   SERIES DOSSIER TEASE (Series grid)
   ============================================================
   A series with an uploaded dossier PDF gets a cue strip on its card; the
   cue expands a full-width panel whose backdrop is the dossier's own first
   pages, faded and blurred back. The blur is doing real work — the point is
   to show HOW MUCH there is without making any of it readable, so the pages
   sell the depth while the copy sells the story and the button sells the door.

   Palette is the existing dossier one used by #series-info-drawer and
   #pdfp-modal above: gold #9c7a2e, ink #1b1712, cream #f6f1e7, with
   'EB Garamond'/'Playfair Display' — so the tease, the drawer and the case
   file all read as one document rather than three components.
   ============================================================ */

.series-dossier-cue {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  margin-top: 10px;
  padding: 7px 8px;
  background: transparent;
  border: 1px solid #2a2419;
  border-radius: 6px;
  color: #9c7a2e;
  font-family: 'EB Garamond', serif;
  font-size: 11.5px;
  letter-spacing: .10em;
  text-transform: uppercase;
  cursor: pointer;
  text-align: center;
  justify-content: center;
  white-space: nowrap;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.series-dossier-cue:hover,
.series-dossier-cue:focus-visible {
  background: rgba(156, 122, 46, .10);
  border-color: #9c7a2e;
  color: #c9a227;
}
.series-dossier-cue .sdc-mark { font-size: 13px; line-height: 1; }
.series-dossier-cue[aria-expanded="true"] {
  background: rgba(156, 122, 46, .14);
  border-color: #9c7a2e;
  color: #c9a227;
}

/* ---- Series page: editorial blocks ----
   #series-grid holds .series-edi blocks, not tiles — one per row, all equal.
   Everything visual is inherited from .series-dossier-panel below; this only
   changes size, layout and how much of the artwork shows, because the panel
   design was already right and only its placement was wrong. */
.series-standfirst{
  margin:-6px 0 20px; max-width:62ch;
  color:var(--muted); font-size:14.5px; line-height:1.6;
}
/* One block per row, all the same size (Raz, 2026-08-27). These are three
   flagship bodies of work — half-width cards made two of them read as
   also-rans, and at that width there was no spare side for the artwork. */
#series-grid{display:grid; grid-template-columns:1fr; gap:18px; align-items:stretch;}
/* Scoped through #series-grid deliberately. .series-dossier-panel is declared
   LATER in this file with the same specificity, so a bare .series-edi rule
   loses every property they both set — min-height silently stayed 240px and
   the blocks never got their full height. The id settles it. */
#series-grid .series-edi{margin:0; min-height:330px; grid-column:1/-1;}
/* The inherited panel hid its contact sheet almost completely (opacity .22
   under a 97%-to-45% veil) because it was a tease that opened UNDER a card
   that already carried the artwork. Here the block IS the card, so the sheet
   has to read as the picture: lifted well up, and the veil pulled back hard on
   the right. The blur stays — at this scale the dossier pages read as volume
   and structure, never as sentences, which is the point. Text sits over the
   opaque left end, so its contrast is untouched by any of this. */
.series-edi .sdp-sheet{opacity:.72; filter:grayscale(.08) blur(1px);}
/* Five stops, not four. The text column ends at 58%, so the veil holds nearly
   opaque to there and only then releases — but it has to release SLOWLY: a
   short ramp reads as a hard vertical seam down the middle of the artwork,
   which is what a four-stop version did. */
.series-edi .sdp-veil{
  background:linear-gradient(90deg,
    rgba(9,9,9,.96) 0%,
    rgba(9,9,9,.94) 46%,
    rgba(9,9,9,.8) 60%,
    rgba(9,9,9,.34) 82%,
    rgba(9,9,9,.08) 100%);
}
.series-edi .sdp-body{padding:36px 40px; max-width:58%;}
.series-edi .sdp-title{font-size:36px;}
.sde-meta{
  margin:6px 0 0; color:var(--muted);
  font-size:11px; letter-spacing:.14em; text-transform:uppercase;
}
.sde-actions{display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin-top:20px;}
.sde-play{
  cursor:pointer; padding:10px 18px; border-radius:999px;
  background:var(--accent); color:#fff; border:1px solid var(--accent);
  font-size:13.5px; font-weight:700; font-family:inherit;
  transition:opacity .15s ease;
}
.sde-play:hover{opacity:.9;}
.sde-play:focus-visible,.series-edi .sdp-open:focus-visible{outline:2px solid var(--accent); outline-offset:3px;}
/* The note is a whole-row aside under both buttons, not a third button. */
.sde-actions .sdp-note{flex-basis:100%; margin:2px 0 0;}
/* Colour artwork layer — painted on phones only. Hidden by default so it
   costs nothing on desktop, where the contact sheet is the backdrop and is
   supposed to be. Sits between .sdp-sheet and .sdp-veil in the DOM, so it
   covers the sheet and the veil still covers both. */
.sde-art{display:none;}
@media(max-width:760px){
  .series-edi{min-height:0;}
  /* On a phone the text needs the full width, so there is no clear side left
     for the artwork — the veil has to cover the whole block rather than step
     aside to one side of it. */
  .series-edi .sdp-body{padding:22px 20px; max-width:none;}
  .series-edi .sdp-title{font-size:27px;}

  /* Raz, 2026-08-27: "on mobile i want artwork showed behind the series text
     so that part of the site isn't so dead… faded out but visible enough to
     add some kind of colour, otherwise its just a bunch of text."

     The previous pass ran the contact sheet at .3 under a flat .90–.94 veil,
     which composites to roughly 2% visible — the artwork was, in practice,
     not there. And the sheet is pages of TYPE, so even at full strength it
     answers "too much text" with more text. Two changes:

     1. The colour artwork (series photo, else a release cover) paints
        instead — see .sde-art and seriesEditorialEl() in player.js. When a
        series has no separate artwork the sheet is already that artwork, so
        the same numbers are applied to it and nothing is lost.
     2. Blur does the legibility work instead of darkness. At 6px there is no
        detail left to compete with a headline — only colour and shape — so
        the veil can come back from .90 to .52 at the top and the block reads
        as a lit surface rather than a text box. Saturation is pushed because
        blurring toward the local average pulls colour out.

     Numbers were set against the composite, not guessed: the first pass at
     opacity .5 / veil .62 measured legible but barely coloured, so both were
     pushed one step. Sampled off a rendered 390px screenshot, the lightest
     background any of the three blocks puts behind its text measures
     relative luminance .045 — 11:1 against white, past AA's 4.5:1 and past
     AAA's 7:1, with the other two blocks at 13:1 and 19:1. The veil still
     ramps DOWNWARD in strength because the block's own text does the same:
     a 27px headline can sit over a lighter ground than the 15px excerpt
     under it. Re-measure if these numbers are ever raised again. */
  .series-edi .sdp-sheet,
  .series-edi .sde-art{
    opacity:.62; filter:saturate(1.5) blur(6px); transform:scale(1.12);
  }
  .series-edi .sde-art{display:block; position:absolute; inset:0;}
  /* Where the art layer paints, the contact sheet underneath is not a
     backdrop any more, just a half-visible second image muddying the first.
     JS adds .series-edi--art to the block only when both exist. */
  .series-edi--art .sdp-sheet{display:none;}
  .series-edi .sdp-veil{background:linear-gradient(180deg,
    rgba(9,9,9,.52) 0%, rgba(9,9,9,.66) 42%, rgba(9,9,9,.8) 100%);}
  /* The art layer is an <img> inserted by lazyBg(), same as the sheet — it
     needs the same cover/fit or it paints at its natural size in the corner. */
  .series-edi .sde-art img{
    display:block; width:100%; height:100%; object-fit:cover; object-position:center;
  }
}

/* grid-column:1/-1 — own full-width row inside .grid's auto-fill columns.
   Deliberately NOT `margin:… auto`: that would centre the panel against the
   whole grid width and read as broken next to left-flush cards. */
.series-dossier-panel {
  grid-column: 1 / -1;
  position: relative;
  overflow: hidden;
  display: flex;
  align-items: center;
  min-height: 240px;
  margin: 2px 0 10px;
  border: 1px solid #1f1f1f;
  border-radius: 10px;
  background: #0d0d0d;
}

.sdp-sheet {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center top;
  opacity: .22;
  /* Blur is the paywall here, not decoration: at this scale the pages read as
     volume and structure, never as sentences. */
  filter: grayscale(.2) blur(1.5px);
  transform: scale(1.04); /* hides the blur's soft edge against the border */
}
/* lazyBg()/setArtImg() inserts an <img> child rather than setting
   background-image, so the background-* rules above never apply to the sheet.
   Without this the contact sheet renders at its natural 800px width in the
   top-left corner of the panel. Keep both: the img is what actually paints. */
.sdp-sheet img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
}
.sdp-sheet--none {
  background: radial-gradient(120% 90% at 80% 20%, #1a1712 0%, #0d0d0d 70%);
  opacity: 1;
  filter: none;
}
.sdp-veil {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    rgba(9, 9, 9, .97) 0%,
    rgba(9, 9, 9, .93) 38%,
    rgba(9, 9, 9, .62) 72%,
    rgba(9, 9, 9, .45) 100%
  );
}

.sdp-body {
  position: relative;
  padding: 30px 34px;
  max-width: 660px;
}
.sdp-eyebrow {
  margin: 0 0 8px;
  font-family: 'EB Garamond', serif;
  font-size: 12.5px;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: #9c7a2e;
}
.sdp-title {
  margin: 0 0 12px;
  font-family: 'Playfair Display', serif;
  font-size: 28px;
  font-weight: 600;
  color: #ffffff;
  line-height: 1.15;
}
.sdp-excerpt {
  margin: 0 0 18px;
  max-width: 58ch;
  font-family: 'EB Garamond', serif;
  font-size: 16px;
  line-height: 1.6;
  color: #d8d2c6;
}
.sdp-actions { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.sdp-open {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 20px;
  border-radius: 6px;
  border: 1.5px solid #9c7a2e;
  background: transparent;
  color: #f6f1e7;
  font-family: 'EB Garamond', serif;
  font-size: 14px;
  letter-spacing: .06em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background .15s ease, color .15s ease;
}
.sdp-open:hover,
.sdp-open:focus-visible { background: #9c7a2e; color: #1b1712; }
.sdp-open--locked { border-color: #4a4034; color: #c9c2b4; }
.sdp-open--locked:hover,
.sdp-open--locked:focus-visible { background: #9c7a2e; border-color: #9c7a2e; color: #1b1712; }
.sdp-note {
  margin: 0;
  font-family: 'EB Garamond', serif;
  font-size: 13px;
  color: #8a8378;
}

.sdp-close {
  position: absolute;
  top: 10px;
  right: 12px;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 1px solid transparent;
  border-radius: 50%;
  color: #8a8378;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  transition: color .15s ease, border-color .15s ease;
}
.sdp-close:hover,
.sdp-close:focus-visible { color: #f6f1e7; border-color: #4a4034; }

/* ---- dossier membership gate ---- */
/* The withheld page renders blurred beneath the join panel: the reader can
   see the document keeps going and roughly how dense it is, with nothing
   legible. Pages past this one are never requested, so there is no cleaner
   copy sitting in the DOM to un-blur. */
.pdfp-locked {
  position: relative;
  display: block;
}
.pdfp-locked__page {
  filter: blur(7px) grayscale(.35) brightness(.55);
  /* Fades the blurred page out downward so it reads as the document
     receding rather than as a broken image. */
  -webkit-mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, .35) 62%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 0%, rgba(0, 0, 0, .35) 62%, transparent 100%);
  pointer-events: none;
  -webkit-user-select: none;
  user-select: none;
}
.pdfp-gate {
  position: absolute;
  left: 50%;
  top: 34%;
  transform: translate(-50%, -34%);
  width: min(460px, 86%);
  padding: 26px 26px 24px;
  text-align: center;
  background: rgba(9, 9, 9, .93);
  border: 1px solid #2a2419;
  border-radius: 12px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .6);
}
.pdfp-gate__eyebrow {
  margin: 0 0 8px;
  color: #9c7a2e;
  font-family: 'EB Garamond', serif;
  font-size: 11.5px;
  letter-spacing: .18em;
  text-transform: uppercase;
}
.pdfp-gate__title {
  margin: 0 0 10px;
  color: #f6f1e7;
  font-family: 'EB Garamond', serif;
  font-size: 24px;
  line-height: 1.15;
}
.pdfp-gate__copy {
  margin: 0 0 18px;
  color: #b9b2a4;
  font-family: 'EB Garamond', serif;
  font-size: 15px;
  line-height: 1.5;
}
.pdfp-gate__join {
  display: inline-block;
  padding: 11px 22px;
  background: #c9a227;
  border: 0;
  border-radius: 6px;
  color: #0d0d0d;
  font-family: 'EB Garamond', serif;
  font-size: 14px;
  letter-spacing: .10em;
  text-transform: uppercase;
  /* Now an <a> to the login page, not a <button> — kill the default
     underline and keep the gold-on-black chip look in both states. */
  text-decoration: none;
  cursor: pointer;
  transition: background .15s ease, transform .15s ease;
}
.pdfp-gate__join:hover,
.pdfp-gate__join:focus-visible {
  background: #e0bb3c;
  transform: translateY(-1px);
}

@media (max-width: 749px) {
  .pdfp-gate {
    top: 26%;
    transform: translate(-50%, -26%);
    width: 90%;
    padding: 20px 18px 18px;
  }
  .pdfp-gate__title { font-size: 20px; }
  .pdfp-gate__copy { font-size: 14px; margin-bottom: 14px; }

  .series-dossier-panel { min-height: 0; }
  .sdp-body { padding: 22px 18px; }
  .sdp-title { font-size: 22px; }
  .sdp-excerpt { font-size: 15px; margin-bottom: 16px; }
  /* Vertical veil on narrow screens: the copy sits over the sheet rather than
     beside it, so the gradient has to darken top-down instead of left-right.
     The first pass ran .90-.97 here, which multiplied against the sheet's own
     .22 to roughly 2% visible — the pages disappeared and the tease with them.
     Lighter veil + heavier sheet still measures ~15:1 against the cream body
     copy, well clear of WCAG 4.5:1, because the sheet sits on a #0d0d0d panel. */
  .sdp-sheet { opacity: .34; }
  .sdp-veil {
    background: linear-gradient(
      180deg,
      rgba(9, 9, 9, .72) 0%,
      rgba(9, 9, 9, .86) 58%,
      rgba(9, 9, 9, .93) 100%
    );
  }
  .sdp-actions { gap: 10px; }
  .sdp-open { padding: 9px 16px; font-size: 13px; }
}

/* ============================================================
   /videos — the Netflix-style video page (2026-08-25, Raz's call:
   "more of a mock version of netflix then youtube").

   Three pieces: one full-bleed hero banner, then horizontal rows of 16:9
   tiles that scroll with arrow buttons, with a hover-expand panel on each
   tile. Everything below is scoped under #videos-view or a .v- prefix so
   none of it can leak into the music catalog's square-art grids.

   THE ONE NON-OBVIOUS THING: .vrow-scroller sets overflow-x, and CSS
   forces overflow-y to match — so a tile that grows on hover WOULD be
   clipped. The fix is the deliberately fat vertical padding on the
   scroller (room for the scaled tile plus its info panel) pulled back
   with a negative margin. Don't "tidy" that padding away.
   ============================================================ */

#videos-view{ padding-bottom:10px; }

/* ---- hero banner ---- */
.vhero{
  position:relative; overflow:hidden;
  /* Full-bleed across .main, padding included. calc + explicit negative
     margins rather than width:auto: the first cut used aspect-ratio:16/9
     with max-height:62vh, and on a tall window the max-height fed BACK
     through the aspect ratio and shrank the WIDTH to 1047px instead of
     1280 — a banner with 230px of dead page beside it. A definite width
     with an explicit height can't do that. */
  width:calc(100% + 68px); margin:0 -34px 10px;
  /* Cinematic, not 16:9 — a 16:9 banner at this width is 720px tall and
     pushes every row below the fold. The image inside is object-fit:cover,
     so it crops rather than letterboxes. */
  /* 560 looked right in isolation and put the first row almost entirely
     below the fold; 480 leaves a strip of Latest showing, which is the
     "there's more down here" cue the whole layout depends on. */
  height:clamp(330px, 34vw, 480px);
  background:#000; display:flex; align-items:flex-end;
}
.vhero-img{
  position:absolute; inset:0; width:100%; height:100%;
  object-fit:cover; object-position:center 28%;
  opacity:0; transition:opacity 1.1s ease;
}
.vhero-img.on{opacity:1;}
/* The hero's fade is slower than a tile's on purpose — it is a large,
   ambient background that should drift, where a tile is reacting to the
   cursor and needs to feel immediate. */
.vhero-imgs{position:absolute; inset:0;}
/* Small dots under the hero copy showing how many images are in the set
   and which is up. Only rendered when there is more than one. */
.vhero-dots{display:flex; gap:6px; margin:0 0 16px;}
.vhero-dot{
  width:22px; height:3px; border-radius:2px; background:rgba(255,255,255,.28);
  transition:background .3s ease;
}
.vhero-dot.on{background:var(--beta-red);}
/* Two scrims, not one: the horizontal pass keeps the title legible over a
   bright frame, the vertical pass melts the bottom edge into the page so
   the banner reads as part of the page rather than a pasted-in rectangle. */
.vhero-scrim{
  position:absolute; inset:0;
  background:
    linear-gradient(90deg, rgba(0,0,0,.94) 0%, rgba(0,0,0,.74) 34%, rgba(0,0,0,.12) 68%, rgba(0,0,0,0) 100%),
    linear-gradient(0deg, var(--bg) 0%, rgba(12,12,12,.72) 16%, rgba(12,12,12,0) 48%);
}
.vhero-body{
  position:relative; z-index:2; padding:0 34px 40px; max-width:640px;
}
.vhero-kicker{
  margin:0 0 10px; font-size:11px; font-weight:800; letter-spacing:.18em;
  text-transform:uppercase; color:#fff; display:flex; align-items:center; gap:9px;
}
.vhero-kicker .vhero-mark{
  color:var(--accent); font-size:15px; letter-spacing:0; font-weight:900;
}
.vhero-title{
  margin:0 0 12px; font-size:clamp(30px, 5.2vw, 58px); line-height:1.02;
  font-weight:800; letter-spacing:-.028em; color:#fff;
  text-shadow:0 2px 26px rgba(0,0,0,.7);
}
.vhero-meta{
  margin:0 0 12px; font-size:12.5px; font-weight:700; color:#d7d7d7;
  display:flex; align-items:center; gap:9px; flex-wrap:wrap; letter-spacing:.01em;
}
.vhero-meta .dot{color:#6b6b6b;}
.vhero-badge{
  font-size:10px; font-weight:800; letter-spacing:.09em; text-transform:uppercase;
  padding:3px 8px; border-radius:4px; border:1px solid rgba(255,255,255,.34); color:#fff;
}
.vhero-badge.members{border-color:var(--accent); color:#9fe0b6;}
.vhero-desc{
  margin:0 0 20px; font-size:13.5px; line-height:1.6; color:#cfcfcf;
  display:-webkit-box; -webkit-line-clamp:3; -webkit-box-orient:vertical; overflow:hidden;
  text-shadow:0 1px 14px rgba(0,0,0,.8);
}
.vhero-btns{display:flex; gap:12px; flex-wrap:wrap;}
.vhero-btn{
  display:inline-flex; align-items:center; gap:8px; border:0; cursor:pointer;
  border-radius:999px; padding:12px 26px; font:700 14px/1 inherit; letter-spacing:.01em;
  transition:transform .12s ease, background .15s ease, opacity .15s ease;
}
.vhero-btn:active{transform:scale(.97);}
.vhero-btn.play{background:var(--accent); color:#fff;}
/* Hover is DERIVED from --accent rather than hardcoded. It used to be a
   literal #3a8a58, which survived the 2026-09-07 #208311 -> #2e6b45 accent
   swap untouched and would have gone stale again on the next one. The base
   above is var(--accent), so the hover has to lift off whatever that is. */
.vhero-btn.play:hover{background:color-mix(in srgb, var(--accent) 84%, #ffffff);}
.vhero-btn.ghost{background:rgba(255,255,255,.16); color:#fff; backdrop-filter:blur(6px);}
.vhero-btn.ghost:hover{background:rgba(255,255,255,.26);}

/* ---- rows ---- */
.vrow{margin:0 0 4px;}
.vrow-head{
  display:flex; align-items:baseline; gap:12px;
  margin:26px 0 10px; padding:0;
}
.vrow-title{
  margin:0; font-size:16px; font-weight:800; letter-spacing:-.01em; color:var(--fg);
}
/* A category heading is a link into that category's own page. It is a
   <button> styled as a heading rather than an <a>, because the route is
   pushed client-side — but it must still look and behave like something
   you can click, hence the arrow that slides on hover. */
button.vrow-title{
  background:none; border:0; padding:0; cursor:pointer; font-family:inherit;
  display:inline-flex; align-items:center; gap:6px;
}
button.vrow-title .chev{
  color:var(--beta-red); font-size:18px; opacity:0; transform:translateX(-4px);
  transition:opacity .16s ease, transform .16s ease;
}
button.vrow-title:hover .chev{opacity:1; transform:translateX(0);}
.vrow-count{font-size:11.5px; color:var(--muted); font-weight:600;}
/* The horizontal padding is the arrows' GUTTER. The first version had the
   arrows overlaying the row's left and right edges, sitting on top of the
   first and last tile — Raz: "the next/previous buttons should not overlap
   the video thumbnail because they are hard to click that way". He's
   right: an arrow on top of a tile means every press is a coin flip
   between scrolling the row and opening a video. They now live in their
   own space beside the row and can never steal a tile's click. */
.vrow-wrap{position:relative; padding:0 52px;}

/* See the header comment: this padding is what stops overflow-y from
   clipping a hovered tile and its panel. The negative margin gives the
   space back to the page so rows don't drift apart. */
.vrow-scroller{
  display:flex; gap:10px; overflow-x:auto; overflow-y:hidden;
  scroll-behavior:smooth; scroll-snap-type:x proximity;
  /* The bottom padding is measured, not guessed: a 300x169 tile scaled
     1.11 hangs ~9px below its own box, and the hover panel below that is
     ~100px scaled. 116px was 6px short and clipped the panel's last row
     of pixels (caught by the puppeteer harness, 2026-08-25). 136 leaves
     real headroom. The negative margin hands the unused space back to the
     page so rows don't drift apart. */
  padding:18px 4px 136px; margin:-18px -4px -104px;
  scrollbar-width:none; -ms-overflow-style:none;
  -webkit-overflow-scrolling:touch;
}
.vrow-scroller::-webkit-scrollbar{display:none;}

/* Always on screen, but quiet: mostly black with a small chevron, filling
   to dark grey on hover (Raz's call — the first cut was red-on-red and
   shouted louder than the artwork it sits beside).
   Still ALWAYS rendered, unlike the original opacity:0-until-hover pair,
   which was invisible on touch and undiscoverable to anyone who didn't
   happen to mouse over the row. A control you have to find by accident is
   not a control. */
.vrow-arrow{
  /* top/bottom track the scroller's own padding so the button spans the
     artwork and not the empty hover-panel gutter beneath it. */
  position:absolute; top:18px; bottom:104px; width:44px; z-index:6;
  display:flex; align-items:center; justify-content:center;
  cursor:pointer; font-size:15px; font-weight:700; line-height:1;
  border:0; border-radius:8px;
  background:#000; color:#7c7c7c;
  transition:background .15s ease, color .15s ease, transform .1s ease, opacity .15s ease;
}
.vrow-arrow.left{left:0;}
.vrow-arrow.right{right:0;}
.vrow-arrow:hover{background:#2b2b2b; color:#fff;}
.vrow-arrow:active{transform:scale(.94);}
/* Dimmed rather than removed at the ends of a row — a control that
   vanishes and reappears under the cursor is worse than one that greys
   out, and keeping it in place stops the row twitching. */
.vrow-arrow:disabled{opacity:.3; cursor:default; background:#000; color:#3a3a3a;}
.vrow-arrow:disabled:hover{background:#000; color:#3a3a3a;}
/* Only fully hidden when the row doesn't scroll at all, where the pair
   would be pure decoration. */
.vrow-arrow[hidden]{display:none;}

/* ---- tiles ---- */
.vtile{
  flex:0 0 300px; max-width:300px; position:relative; cursor:pointer;
  scroll-snap-align:start; border-radius:9px;
  transition:transform .26s cubic-bezier(.2,.7,.3,1);
}
.vtile-art{
  position:relative; width:100%; aspect-ratio:16/9; border-radius:9px; overflow:hidden;
  background:#151515; border:1px solid #1f1f1f;
}
/* Cycling image sets (migration 059). Every image in a set is stacked in
   the same box and cross-faded by toggling .on — NOT swapped by changing
   one <img>'s src, which would show a blank frame on every change while
   the next file downloads. All of them are in the DOM from the start, so
   by the time a fade begins the image is already decoded. */
.vtile-art img, .vhero-img{
  width:100%; height:100%; object-fit:cover; display:block;
}
.vtile-art img{
  position:absolute; inset:0; opacity:0;
  transition:opacity .55s ease;
}
.vtile-art img.on{opacity:1;}
/* A single-image set never animates: no transition to interrupt, and it
   is painted immediately rather than fading in on first render. */
.vtile-art img.solo{opacity:1; transition:none;}
.vtile-art::after{
  /* Sits over the art only, so the panel below never dims. */
  content:""; position:absolute; inset:0; border-radius:9px;
  background:linear-gradient(0deg, rgba(0,0,0,.5), rgba(0,0,0,0) 52%);
  opacity:.85; transition:opacity .2s ease;
}
.vtile-fallback{
  position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  color:#3d3d3d; font-size:30px; font-weight:900; letter-spacing:-.03em;
}
.vtile-dur{
  position:absolute; right:8px; bottom:8px; z-index:2;
  background:rgba(0,0,0,.82); color:#fff; font-size:10.5px; font-weight:700;
  padding:2px 6px; border-radius:4px; letter-spacing:.02em;
}
.vtile-lock{
  position:absolute; left:8px; top:8px; z-index:2;
  background:rgba(0,0,0,.78); border:1px solid var(--accent); color:#9fe0b6;
  font-size:9.5px; font-weight:800; letter-spacing:.08em; text-transform:uppercase;
  padding:3px 7px; border-radius:4px;
}
.vtile-play{
  position:absolute; left:50%; top:50%; z-index:2; transform:translate(-50%,-50%) scale(.8);
  width:52px; height:52px; border-radius:50%; background:rgba(0,0,0,.55);
  border:2px solid #fff; color:#fff; display:flex; align-items:center; justify-content:center;
  font-size:17px; opacity:0; transition:opacity .2s ease, transform .2s ease; padding-left:3px;
}
/* Title always visible under the tile on touch; on desktop it lives in the
   hover panel instead, so the row reads as pure artwork until you point at
   something — which is the whole Netflix effect. */
.vtile-caption{
  margin:8px 2px 0; font-size:12.5px; font-weight:700; color:#d2d2d2; line-height:1.35;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}

/* ---- hover-expand panel (pointer devices only) ---- */
.vtile-panel{
  position:absolute; left:0; right:0; top:100%; z-index:1;
  background:#161616; border:1px solid #242424; border-top:0;
  border-radius:0 0 9px 9px; padding:11px 13px 13px;
  opacity:0; transform:translateY(-8px); pointer-events:none;
  transition:opacity .2s ease, transform .2s ease;
  box-shadow:0 22px 44px rgba(0,0,0,.6);
}
.vtile-panel-title{
  margin:0 0 6px; font-size:13.5px; font-weight:800; color:#fff; line-height:1.3;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
}
.vtile-panel-meta{
  margin:0 0 10px; font-size:11px; font-weight:700; color:var(--muted);
  display:flex; align-items:center; gap:7px; flex-wrap:wrap;
}
.vtile-panel-meta .dot{color:#4a4a4a;}
.vtile-panel-meta .yt{color:#e0272b;}
.vtile-panel-btns{display:flex; gap:8px;}
.vtile-panel-btn{
  border:0; cursor:pointer; border-radius:999px; padding:7px 15px;
  font:700 11.5px/1 inherit; background:var(--accent); color:#fff;
  display:inline-flex; align-items:center; gap:6px;
}
.vtile-panel-btn.ghost{background:rgba(255,255,255,.14); color:#fff;}
.vtile-panel-btn:hover{filter:brightness(1.12);}

@media (hover:hover) and (pointer:fine){
  /* Desktop only. The caption is hidden because the panel carries the
     title — showing both would double it up mid-hover. */
  .vtile-caption{display:none;}
  .vtile:hover{transform:scale(1.11); z-index:8;}
  .vtile:hover .vtile-art{border-color:#333;}
  .vtile:hover .vtile-art::after{opacity:.35;}
  .vtile:hover .vtile-play{opacity:1; transform:translate(-50%,-50%) scale(1);}
  .vtile:hover .vtile-panel{opacity:1; transform:translateY(0); pointer-events:auto;}
}
@media (hover:none){
  /* Touch: no hover state exists, so the panel would be unreachable and
     the scale would just make the row jitter under a finger. Tap plays. */
  .vtile-panel{display:none;}
  .vtile-play{opacity:1; transform:translate(-50%,-50%) scale(.86);}
  /* A finger swipes the row; arrows would only eat screen width. The
     gutter that holds them goes with them. */
  .vrow-arrow{display:none;}
  .vrow-wrap{padding:0;}
  /* PROXIMITY, not mandatory (2026-08-25). wireRowTouch() in player.js
     falls back to driving scrollLeft by hand when WebKit refuses to
     engage its native pan on this row — and mandatory snapping yanks
     every hand-assigned scrollLeft straight back to the nearest tile,
     so the row fought the finger. Proximity still settles on a tile
     when a drag ends near one and leaves a deliberate mid-row position
     alone. The manual path also turns snapping off inline for the
     duration of a drag; this is the belt to that pair of braces. */
  .vrow-scroller{padding-bottom:16px; margin-bottom:0; scroll-snap-type:x proximity;}
}

/* ---- homepage teaser row ---- */
.vrow-link{
  background:none; border:0; padding:0; cursor:pointer; color:var(--muted);
  font:700 12px/1 inherit; letter-spacing:.02em; margin-left:auto;
}
.vrow-link:hover{color:var(--fg);}

/* ---- video modal: YouTube iframe lives here too ---- */
#video-frame-wrap{
  position:relative; width:100%; aspect-ratio:16/9; border-radius:10px;
  overflow:hidden; background:#000;
}
#video-frame-wrap iframe{position:absolute; inset:0; width:100%; height:100%; border:0;}

@media(max-width:900px){
  .vtile{flex-basis:246px; max-width:246px;}
}
@media(max-width:760px){
  .vhero{width:calc(100% + 32px); margin:0 -16px 8px; height:clamp(300px, 76vw, 430px);}
  .vhero-body{padding:0 16px 22px; max-width:none;}
  .vhero-desc{-webkit-line-clamp:2; font-size:12.5px; margin-bottom:15px;}
  .vhero-btn{padding:11px 20px; font-size:13px;}
  .vtile{flex-basis:60vw; max-width:60vw;}
  .vrow-head{margin:20px 0 8px;}
  .vrow-title{font-size:14.5px;}
}

/* While /videos is open, the page's normal chrome steps aside so the hero
   banner is the first thing on screen — that's the whole Netflix effect.
   Done with ONE body class rather than by setting display on each element,
   because every one of these has its own visibility logic (a dismissed
   join bar, a collapsed Jump back in, the Featured rule in
   syncFeaturedVisibility). Setting display here and "restoring" it later
   would silently overrule decisions this page never made; removing the
   class puts them all back exactly as they were. */
body.videos-page .page-title-row,
body.videos-page .main > .sub,
body.videos-page #homepage-intro,
body.videos-page #bakery-join,
body.videos-page .featured-section{ display:none !important; }
/* .main's top padding would push the "full-bleed" hero down off the top
   edge; the hero's own negative margin already accounts for it. */
body.videos-page .main{ padding-top:20px; }
body.videos-page #videos-view .back-link{ padding-bottom:12px; }

/* Adding the Videos tab made the mobile nav the seventh item in a flex row
   with no wrap, and it pushed the whole PAGE 29px wider than the viewport
   at 390px — a horizontal scroll on every screen of the site, not just the
   video page (caught by the puppeteer harness, 2026-08-25).
   The nav now scrolls inside its own box instead of stretching the page.
   min-width:0 is the load-bearing half: without it a flex item refuses to
   shrink below its content width and overflow-x never engages. This also
   means the next tab added here is free. */
@media(max-width:760px){
  header.site nav{
    overflow-x:auto; min-width:0; -webkit-overflow-scrolling:touch;
    scrollbar-width:none; -ms-overflow-style:none;
  }
  header.site nav::-webkit-scrollbar{display:none;}
  header.site nav a{white-space:nowrap; flex:0 0 auto;}
  /* "All Releases" becomes "Home" below 760px (Raz, 2026-08-25). Both
     labels are in the markup and only the visible one changes, so a
     rotate or a resize picks the right one with no re-render — see
     NAV_SHORT_LABELS in player.js. Desktop keeps the full wording. */
  header.site nav a .nav-full{display:none;}
  header.site nav a .nav-short{display:inline;}
}

/* Seven tabs, one line, 390px of phone. Three things pay for it, and it
   takes all three: "Home" instead of "All Releases" (~70px), the header's
   side padding dropping 28→16 in the block above (24px), and this last
   trim for the narrowest phones.
   MEASURED on the deployed page in a 390px clone, 2026-08-25:
     390px — needs 330, has 358. Fits, 28px spare.
     375px — needs 330, has 343. Fits.
     360px — needs 330, has 328. Scrolls by 2px, which is what the edge
             fade is for.
   Re-measure at 390px before adding an eighth tab; there is no room
   left to find after this. */
@media(max-width:400px){
  header.site nav{gap:10px; font-size:11.5px;}
}

/* Scroll-edge fades. The classes are toggled by syncNavFade() in
   player.js and are only ever present while the row actually has more
   behind that edge, so nothing is dimmed once you reach the end. The
   two-stop mask keeps the fade a fixed 26px regardless of nav width. */
header.site nav.fade-right{
  -webkit-mask-image:linear-gradient(to right,#000 calc(100% - 26px),transparent 100%);
  mask-image:linear-gradient(to right,#000 calc(100% - 26px),transparent 100%);
}
header.site nav.fade-left{
  -webkit-mask-image:linear-gradient(to right,transparent 0,#000 26px);
  mask-image:linear-gradient(to right,transparent 0,#000 26px);
}
header.site nav.fade-left.fade-right{
  -webkit-mask-image:linear-gradient(to right,transparent 0,#000 26px,#000 calc(100% - 26px),transparent 100%);
  mask-image:linear-gradient(to right,transparent 0,#000 26px,#000 calc(100% - 26px),transparent 100%);
}

/* ---- Videos: admin-only PREVIEW state (migration 057) ----
   Only ever rendered for a signed-in admin looking at a section the
   public can't see. It is deliberately loud: the failure mode this
   guards against is Raz walking the live site, seeing the Videos tab,
   and assuming it already shipped. */
.vpreview-bar{
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  margin:0 0 16px; padding:11px 15px; border-radius:10px;
  background:rgba(224,39,43,.09); border:1px solid rgba(224,39,43,.42);
}
.vpreview-text{font-size:12.5px; line-height:1.5; color:#e6e6e6;}
.vpreview-text b{color:#fff;}
.vpreview-text a{color:var(--accent); font-weight:700;}
.vpreview-tag{
  flex:0 0 auto; font-size:9.5px; font-weight:900; letter-spacing:.13em;
  text-transform:uppercase; padding:4px 9px; border-radius:4px;
  background:var(--beta-red); color:#fff;
}
.vrow-head .vpreview-tag{align-self:center;}
/* A dot on the nav tab, so the state is legible from any page without
   another banner competing with the site's own chrome. */
header.site nav a.nav-preview::after{
  content:""; display:inline-block; width:6px; height:6px; margin-left:6px;
  border-radius:50%; background:var(--beta-red); vertical-align:middle;
}

/* ==== MEMBER PERKS (Raz, 2026-09-11) =======================================
   A signed-in listener used to get exactly the page a visitor gets.
   applyMemberPerks() in player.js stamps two classes on <body> once
   /api/auth/shopify/status answers, and everything below keys off them:
     body.bk-signed-in  any signed-in account (free or paid) → SILVER
     body.bk-member     an active Club Member                → GOLD
   Gold is new to THE BAKERY and is reserved for member status only (ring,
   rim, drawer tab, exclusive ribbon, vault). Keep it off ordinary buttons,
   or it stops meaning anything. The metal hexes are LITERALS on purpose, the
   same rule as .store-btn: the admin Theme page repaints --accent and
   friends at runtime, and member status must not follow a theme change.
   Previewed and approved in the "Bakery Member Perks" artifact. */

/* 1. The 1.5px rim under the whole header. It REPLACES the 1px #1c1c1c
   hairline rather than stacking under it. bottom:-1px lays it over the
   border's own strip; header.site is sticky, so it is already positioned. */
body.bk-signed-in header.site{border-bottom-color:transparent;}
body.bk-signed-in header.site::after{
  content:""; position:absolute; left:0; right:0; bottom:-1px; height:1.5px; pointer-events:none;
  background:linear-gradient(90deg,#3e4145 0%,#8b8f94 16%,#d4d7db 32%,#7b7f84 50%,#c9ccd0 68%,#8b8f94 84%,#3e4145 100%);
}
body.bk-member header.site::after{
  background:linear-gradient(90deg,#5e4519 0%,#c9a24f 14%,#f4dc98 30%,#a57f36 48%,#f7e3a8 66%,#b38c40 82%,#5e4519 100%);
  box-shadow:0 0 8px rgba(233,200,119,.28);
}

/* 2. Avatar bubble in place of the "My Profile" text. The link keeps its
   real /profile.html href and its dropdown — only its contents change.
   !important on colour because the link carries an inline muted colour. */
#profile-nav-link.has-avatar{display:inline-flex; align-items:center; gap:8px; color:var(--fg) !important; font-weight:600;}
.hdr-avatar-ring{
  display:inline-grid; place-items:center; width:30px; height:30px; flex-shrink:0;
  border-radius:50%; padding:2px; background:#5f6368;
}
.hdr-avatar{
  width:100%; height:100%; border-radius:50%; overflow:hidden; position:relative;
  border:2px solid var(--bg);
  /* Chef hat as the no-photo fallback. Deliberately NOT default-avatar.svg —
     that file is a 6-byte stub in the repo (commit b5ccc24) and renders
     nothing. The logo PNG is white on transparent, so it needs the #111. */
  background:#111111 url(/assets/bkrsclb-logo.png) center/62% no-repeat;
}
.hdr-avatar img{position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block;}
.hdr-handle{max-width:140px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
body.bk-member .hdr-avatar-ring{
  background:conic-gradient(from 200deg,#8c6a2b,#f6e0a0,#b58e40,#fbe9b6,#9a7632,#e9c877,#8c6a2b);
  box-shadow:0 0 0 1px rgba(233,200,119,.25), 0 0 12px rgba(233,200,119,.42);
  animation:bk-member-glow 4.5s ease-in-out infinite;
}
@keyframes bk-member-glow{50%{box-shadow:0 0 0 1px rgba(233,200,119,.35), 0 0 18px rgba(233,200,119,.6);}}
body.bk-member .hdr-handle{color:#e4c171;}
/* The phone header row is already full (see the 760px block) — avatar only. */
@media(max-width:760px){ .hdr-handle{display:none;} }

/* 3. "Welcome back, <name>" in place of the platform strapline. */
#home-sub.is-greeting{display:flex; align-items:center; gap:10px; flex-wrap:wrap;}
#home-sub .home-greet{font-size:15px; color:#cfcfcf;}
#home-sub .home-greet b{color:var(--fg); font-weight:700;}
.member-chip{
  display:inline-block; font-size:9.5px; font-weight:800; line-height:1; letter-spacing:.1em;
  text-transform:uppercase; color:#2a1d05; padding:5px 8px; border-radius:999px; white-space:nowrap;
  background:linear-gradient(115deg,#7d5f24 0%,#f0d58e 22%,#b48d3d 44%,#fbe8b2 62%,#9c7730 84%,#e9c877 100%);
}

/* 4. Unreleased Vault — the paid third shuffle. Gold border drawn with the
   padding-box/border-box trick so the pill keeps the exact 1.5px geometry of
   its green and blue siblings. [hidden] needs restating: the shared button
   rule sets display:inline-flex, which beats the UA's [hidden]. */
.vault-shuffle-inline-btn[hidden]{display:none;}
.vault-shuffle-inline-btn{
  border-color:transparent; color:#e4c171;
  background:linear-gradient(var(--bg),var(--bg)) padding-box,
             linear-gradient(90deg,#8a6a2a,#f0d58e 30%,#b48d3d 55%,#fbe8b2 78%,#9c7730) border-box;
}
.vault-shuffle-inline-btn:hover{
  background:linear-gradient(#1b160c,#1b160c) padding-box,
             linear-gradient(90deg,#8a6a2a,#f0d58e 30%,#b48d3d 55%,#fbe8b2 78%,#9c7730) border-box;
}
.vault-shuffle-inline-btn.active{
  color:#2a1d05; border-color:transparent;
  background:linear-gradient(115deg,#7d5f24 0%,#f0d58e 22%,#b48d3d 44%,#fbe8b2 62%,#9c7730 84%,#e9c877 100%) border-box;
}
.vault-star{font-size:13px; line-height:1;}
/* Transport icon while the vault is shuffling — gold, like the button. */
.player-bar .btns #shuffle-btn.vault-active{color:#e9c877;}
@media(max-width:760px){
  /* Icon-only filled circle, like its siblings; the star tells it apart. */
  .vault-shuffle-inline-btn,
  .vault-shuffle-inline-btn:hover{
    color:#2a1d05; border-color:transparent;
    background:linear-gradient(115deg,#7d5f24 0%,#f0d58e 22%,#b48d3d 44%,#fbe8b2 62%,#9c7730 84%,#e9c877 100%) border-box;
  }
  .vault-star{font-size:15px;}
}

/* 5. MEMBER EXCLUSIVE ribbon + stamp on members-only covers. The wrapper
   exists because setArtImg() empties .art when the lazy cover arrives. The
   .art's own margin-bottom collapses through the wrapper, so bottom:6px on
   the stamp measures from the artwork's edge, not the card's. */
.mx-art-wrap{position:relative;}
.mx-ribbon{
  position:absolute; top:0; left:0; z-index:2; pointer-events:none;
  padding:5px 8px 5px 7px; border-radius:6px 0 8px 0;
  font-size:8.5px; font-weight:800; line-height:1; letter-spacing:.09em; text-transform:uppercase;
  color:#2a1d05; white-space:nowrap;
  background:linear-gradient(115deg,#7d5f24 0%,#f0d58e 22%,#b48d3d 44%,#fbe8b2 62%,#9c7730 84%,#e9c877 100%);
  box-shadow:0 1px 3px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.55);
}
.mx-ribbon .mx-short{display:none;}
.mx-stamp{
  position:absolute; right:6px; bottom:6px; z-index:2; pointer-events:none;
  width:24px; height:24px; border-radius:50%; display:grid; place-items:center;
}
.mx-lock{background:rgba(0,0,0,.74); border:1px solid rgba(255,255,255,.2); color:#fff;}
.mx-open{display:none; background:rgba(12,12,12,.88); box-shadow:inset 0 0 0 1.5px #d9b45f; color:#e4c171;}
body.bk-member .mx-lock{display:none;}
body.bk-member .mx-open{display:grid;}
@media(max-width:760px){
  .mx-ribbon{font-size:7.5px; padding:4px 6px;}
  .mx-ribbon .mx-long{display:none;}
  .mx-ribbon .mx-short{display:inline;}
  .mx-stamp{width:20px; height:20px; right:4px; bottom:4px;}
  .mx-stamp svg{width:10px; height:10px;}
}

/* 6. Vault Selections — the My Selections tab and drawer, gold for members. */
body.bk-member #my-selections-tab{
  background:linear-gradient(180deg,#f3dc9c,#c9a24f); color:#2a1d05;
  box-shadow:2px 0 14px rgba(233,200,119,.25), inset 0 1px 0 rgba(255,255,255,.6), inset 0 -1px 0 rgba(0,0,0,.15);
}
body.bk-member #my-selections-tab:hover{background:linear-gradient(180deg,#f7e3a8,#bf9644);}
/* Keep the hold-M "filling up" feedback — the gold rule above outranks it. */
body.bk-member #my-selections-tab.ms-hold{background:linear-gradient(180deg,#ffffff,#fbf0d2);}
body.bk-member #my-selections-reveal{background:linear-gradient(180deg,#f3dc9c,#c9a24f);}
body.bk-member #my-selections-drawer .ms-head{position:relative; border-bottom-color:transparent;}
body.bk-member #my-selections-drawer .ms-head::after{
  content:""; position:absolute; left:0; right:0; bottom:-1px; height:1.5px;
  background:linear-gradient(90deg,#5e4519 0%,#c9a24f 14%,#f4dc98 30%,#a57f36 48%,#f7e3a8 66%,#b38c40 82%,#5e4519 100%);
}
body.bk-member #my-selections-drawer .ms-tab-btn.active{color:#e4c171; border-bottom-color:#d9b45f;}
/* Three tabs in a 420px drawer (and a 390px phone): tighten so
   "RECENTLY PLAYED" never wraps. */
.ms-tabs.ms-tabs-3{padding:0 16px;}
.ms-tabs.ms-tabs-3 .ms-tab-btn{font-size:11px; letter-spacing:.02em; padding:12px 2px; white-space:nowrap;}

.vault-collections{display:flex; flex-direction:column; gap:12px;}
.vault-new-collection{display:flex; gap:8px;}
.vault-new-collection input{
  flex:1; min-width:0; background:#1a1a1a; border:1px solid #2e2e2e; border-radius:999px;
  color:#fff; font:inherit; font-size:13px; padding:10px 14px; outline:none;
}
.vault-new-collection input:focus{border-color:#d9b45f;}
.vault-new-collection input::placeholder{color:#7a7a7a;}
.vault-new-collection button{
  flex-shrink:0; background:transparent; color:#e4c171; border:1.5px solid #b48d3d;
  border-radius:999px; font:inherit; font-size:12.5px; font-weight:700; padding:0 16px; cursor:pointer;
}
.vault-new-collection button:hover{background:rgba(233,200,119,.12);}
.vault-collections-count{margin:0; font-size:11px; color:var(--muted); letter-spacing:.02em;}

@media (prefers-reduced-motion: reduce){
  body.bk-member .hdr-avatar-ring{animation:none;}
}

/* ==== THE BAKERY join block: phones only (Raz, 2026-09-11) =================
   On desktop the slim join bar under the header does this job now ("a
   better slicker, sleeker CTA"), so the block under Featured Releases is
   hidden above 760px. On phones it stays, and the price maths line was
   already cut there (see .bakery-join__sub in the 760px block above).
   !important because initBakeryJoin() un-hides it with the hidden attribute
   and the base rule sets display:flex. */
@media(min-width:761px){ #bakery-join{display:none !important;} }

/* ---- Listening badges: album-complete pop-up + Messages inbox (2026-09-16)
   Same frame as the subscribe/personalize pair. The pop-up stays neutral;
   gold only appears on an unlocked badge, which is the member-status colour
   (see the bk-member block) and is earned here. ---- */
#album-complete-overlay, #messages-overlay{
  position:fixed; inset:0; background:rgba(0,0,0,.72); z-index:74; opacity:0; pointer-events:none; transition:opacity .2s ease;
}
#album-complete-overlay.show, #messages-overlay.show{opacity:1; pointer-events:auto;}
#album-complete-popup, #messages-popup{
  position:fixed; top:50%; left:50%; transform:translate(-50%,-50%) scale(.96);
  width:380px; max-width:88vw; max-height:86vh; overflow-y:auto; overscroll-behavior:contain;
  background:var(--sidebar); border:1px solid #262626; border-radius:14px; z-index:75;
  opacity:0; pointer-events:none; transition:opacity .2s ease, transform .2s ease;
  box-shadow:0 20px 60px rgba(0,0,0,.55); padding:30px 24px 22px; text-align:center;
}
#album-complete-popup.show, #messages-popup.show{opacity:1; pointer-events:auto; transform:translate(-50%,-50%) scale(1);}
#album-complete-popup.has-badge{border-color:rgba(233,200,119,.45);}
.ac-close{
  position:absolute; top:8px; right:10px; background:none; border:none; color:var(--muted);
  font-size:22px; line-height:1; cursor:pointer; padding:6px; font-family:inherit;
}
.ac-close:hover{color:var(--fg);}
.ac-cover{width:132px; height:132px; object-fit:cover; border-radius:8px; display:block; margin:0 auto 16px; box-shadow:0 10px 30px rgba(0,0,0,.5);}
.ac-kicker{font-size:10px; font-weight:800; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); margin:0 0 6px;}
#album-complete-popup h4{margin:0 0 10px; font-size:20px; color:#fff; letter-spacing:.02em;}
#album-complete-popup p{margin:0 0 12px; font-size:13px; line-height:1.6; color:var(--muted);}
#album-complete-popup p b{color:#fff;}
#album-complete-popup .ac-count, #album-complete-popup .ac-tally, #album-complete-popup .ac-next{font-size:12px; margin-bottom:8px;}
.ac-badge{
  display:flex; align-items:center; gap:12px; text-align:left; margin:14px 0;
  padding:10px 12px; border-radius:10px; background:#0c0c0c; border:1px solid rgba(233,200,119,.35);
}
.ac-badge-art{
  flex:0 0 48px; width:48px; height:48px; border:2px solid #e9c877; border-radius:6px; background:#000; overflow:hidden;
}
.ac-badge-art img{width:100%; height:100%; object-fit:contain; display:block;}
.ac-badge-txt{display:flex; flex-direction:column; gap:2px;}
.ac-badge-txt small{font-size:9.5px; font-weight:800; letter-spacing:.12em; text-transform:uppercase; color:#e9c877;}
.ac-badge-txt b{font-size:14px; color:#fff; letter-spacing:.04em;}
#album-complete-popup .sub-pop-btns{margin-top:16px;}
#album-complete-popup button.sub-pop-btn{font-family:inherit; cursor:pointer; border:1px solid #2a2a2a; background:#1a1a1a; color:var(--fg);}
#album-complete-popup .sub-pop-btn--year{color:#fff;}

#messages-popup{width:440px; text-align:left; padding:18px 18px 14px;}
.msg-head{display:flex; align-items:center; justify-content:space-between; margin:0 0 10px;}
.msg-head h4{margin:0; font-size:16px; color:#fff;}
.msg-head .ac-close{position:static;}
.msg-empty{font-size:13px; color:var(--muted); line-height:1.6; margin:8px 0 12px;}
.msg-row{display:flex; gap:12px; padding:12px 4px; border-top:1px solid #1f1f1f;}
.msg-row:first-child{border-top:none;}
.msg-art{flex:0 0 44px; width:44px; height:44px; border-radius:6px; background:#000 url('/assets/default-avatar.svg') center/cover no-repeat; overflow:hidden;}
.msg-row.is-badge .msg-art{background:#000; border:2px solid #e9c877;}
.msg-art img{width:100%; height:100%; object-fit:cover; display:block;}
.msg-row.is-badge .msg-art img{object-fit:contain;}
.msg-txt{display:flex; flex-direction:column; gap:3px; min-width:0;}
.msg-txt b{font-size:13px; color:#fff;}
.msg-txt span{font-size:12.5px; line-height:1.55; color:var(--muted);}
.msg-txt small{font-size:11px; color:#6d6d6d;}
.msg-row.unread .msg-txt b::before{content:""; display:inline-block; width:7px; height:7px; border-radius:50%; background:#fff; margin-right:7px; vertical-align:middle;}
.profile-menu .pm-count{
  display:inline-block; min-width:16px; padding:2px 5px; margin-left:6px; border-radius:999px;
  background:#fff; color:#000; font-size:10px; font-weight:800; line-height:1.2; text-align:center;
}
.profile-menu .pm-count[hidden]{display:none;}
#profile-nav-link.has-unread .hdr-avatar-ring{position:relative;}
#profile-nav-link.has-unread .hdr-avatar-ring::after{
  content:""; position:absolute; top:-1px; right:-1px; width:8px; height:8px; border-radius:50%;
  background:#fff; box-shadow:0 0 0 2px var(--bg);
}
.profile-menu button[hidden]{display:none;}
