/* =========================================================
   Texerlite — design
   ========================================================= */

/* Registering these as typed custom properties (instead of plain
   strings) is what makes an accent-color change animate smoothly —
   the browser can interpolate a "<color>" over time, so every
   background/border/etc. built from var(--primary) or var(--primary-grad)
   eases into the new color instead of snapping to it. Falls back to an
   instant change in browsers that don't support @property, which is
   harmless. */
@property --primary { syntax: '<color>'; inherits: true; initial-value: #3b82f6; }
@property --primary-dark { syntax: '<color>'; inherits: true; initial-value: #1d4ed8; }
/* Cycling color for the Aurora glow's "Prism · Custom colors" mode
   (Settings → Customization) — app.js periodically re-sets this to the
   next color in the user's chosen palette, and the transition below
   crossfades between them. */
@property --prism-color { syntax: '<color>'; inherits: true; initial-value: #3b82f6; }
/* Same trick for corner roundness (Settings → Customization → Corner
   roundness): registering these as typed <length> properties means a
   Sharp/Default/Pill switch eases every card, button and panel's
   border-radius to its new value instead of snapping. */
@property --radius-lg { syntax: '<length>'; inherits: true; initial-value: 18px; }
@property --radius-md { syntax: '<length>'; inherits: true; initial-value: 12px; }
@property --radius-sm { syntax: '<length>'; inherits: true; initial-value: 8px; }
/* .content's background is a gradient (a faint accent tint fading into
   --bg-elevated) — gradients themselves can't be transitioned by the
   browser, so switching it to `background: transparent` in the fully-
   Minimal layout template used to just snap. Routing both gradient stops'
   alpha through this typed <number> instead means transitioning THIS one
   property smoothly fades the whole gradient out (and back in), the same
   trick as --radius-lg/md/sm above. */
@property --content-tint { syntax: '<number>'; inherits: true; initial-value: 1; }

:root {
  --primary: #3b82f6;
  --primary-dark: #1d4ed8;
  /* Derived from the two colors above so it animates right along with
     them — app.js only ever sets --primary/--primary-dark directly. */
  --primary-grad: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
  transition:
    --primary .45s cubic-bezier(.22,1,.36,1), --primary-dark .45s cubic-bezier(.22,1,.36,1),
    --radius-lg .4s cubic-bezier(.22,1,.36,1), --radius-md .4s cubic-bezier(.22,1,.36,1), --radius-sm .4s cubic-bezier(.22,1,.36,1);

  --bg-void: #000000;
  --bg: #0a0c11;
  --bg-elevated: #12151c;
  --bg-card: #171b23;
  --bg-card-hover: #1e2330;
  --border: #232833;
  --border-soft: #1b1f28;

  --text: #f2f4f8;
  --text-dim: #9aa3b2;
  /* was #5f6779, which only reached ~3.0:1 on --bg-card and failed WCAG AA
     for the small secondary text it's used on (card artist, empty states,
     input placeholders). #828da3 clears 4.5:1 on every surface token. */
  --text-faint: #828da3;
  --focus: #7cb0ff;   /* keyboard focus ring — deliberately lighter than
                         --primary so it stays visible on primary buttons */

  --radius-lg: 18px;
  --radius-md: 12px;
  --radius-sm: 8px;
  --gap: 10px;

  --font-display: 'Space Grotesk', 'Manrope', 'Vazirmatn', sans-serif;
  --font-body: 'Manrope', sans-serif; 
}

* { box-sizing: border-box; }
/* The [hidden] attribute must always win over any component's own
   `display` rule (modals, locked-state, etc. set display:flex on their
   own class, which otherwise ties in specificity with the browser's
   built-in [hidden] rule and can end up winning the cascade). */
[hidden] { display: none !important; }
/* svh (small viewport height), not dvh: dvh keeps recalculating live as the
   mobile browser's address bar/toolbar hides and shows while the user
   scrolls the inner .content pane, and since this shell is the outer
   fixed frame that the navbar / mobile tab bar / player bar all sit
   inside, every one of those recalculations reflows them — the visible
   "icon and label jitter up/down while scrolling" bug. svh is the height
   assuming the browser chrome is fully expanded, so it's a fixed value
   that never changes mid-scroll; the couple of extra pixels the chrome
   can reclaim when it hides just show as a hairline gap under the last
   safe-area inset instead of a live-resizing shell. */
html, body { height: 100%; height: 100svh; overscroll-behavior: none; }
body {
  margin: 0;
  background: var(--bg-void);
  color: var(--text);
  font-family: var(--font-body);
  user-select: none;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  /* Settings → Customization → Corner roundness sets --radius-lg/md/sm
     via a body.radius-* class (see near the bottom of this file), i.e. the
     value actually changes on <body>, not on <html>/:root. The eased
     transition for those typed properties was only declared on :root, so
     it never applied where the change happens — the radius snapped
     instantly. Repeating it here is what makes it animate. */
  transition:
    --radius-lg .4s cubic-bezier(.22,1,.36,1), --radius-md .4s cubic-bezier(.22,1,.36,1), --radius-sm .4s cubic-bezier(.22,1,.36,1);
}
h1, h2, h3 { font-family: var(--font-display); margin: 0; letter-spacing: -0.01em; }
button, input { font-family: inherit; }
button { touch-action: manipulation; }
input:focus { outline: none; }

/* =========================================================
   Keyboard focus
   Pointer users never see these rings (:focus-visible), but every
   control stays reachable and clearly marked for keyboard users.
   ========================================================= */
:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.song-card:focus-visible, .artist-card:focus-visible, .track-row:focus-visible { outline-offset: 3px; }
/* The scroll container must not tuck a focused row under the sticky player bar
   or the section headers (WCAG 2.2 "Focus Not Obscured"). */
.content { scroll-padding-block: 90px 120px; }

/* Screen-reader-only text: present in the accessibility tree, invisible on screen. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
/* Skip link — first tab stop on the page, visible only once focused. */
.skip-link {
  position: fixed; top: 8px; left: 50%; transform: translate(-50%, -140%);
  background: var(--primary); color: #fff; font-size: 13px; font-weight: 700;
  padding: 10px 18px; border-radius: 999px; z-index: 500; text-decoration: none;
  transition: transform .2s ease;
}
.skip-link:focus-visible { transform: translate(-50%, 0); }
::-webkit-scrollbar { width: 5px; height: 10px; }
::-webkit-scrollbar-thumb { background: #262b36; border-radius: 10px; }
::-webkit-scrollbar-track { background: transparent; }

.icon svg, .icon-btn svg { width: 20px; height: 20px; display: block; }

/* =========================================================
   Layout — floating panels with real gaps (nothing touches
   the viewport edges, panels never touch each other either)
   ========================================================= */
.layout {
  height: 100vh;
  /* svh, not dvh — see the html/body rule above for why: dvh live-resizes
     the whole shell (navbar, bottom tab bar, player bar included) as the
     browser's address bar animates in and out during scroll, which is
     what was reading as the icon/label/tab jitter. */
  height: 100svh;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  padding: var(--gap);
  /* viewport-fit=cover + black-translucent status bar means the page
     really does extend under the notch and the home indicator in an
     installed PWA, so every edge has to respect its own safe-area inset
     or the navbar ends up behind the iPhone's status bar. */
  padding-top: max(var(--gap), env(safe-area-inset-top));
  padding-bottom: max(var(--gap), env(safe-area-inset-bottom));
  padding-left: max(var(--gap), env(safe-area-inset-left));
  padding-right: max(var(--gap), env(safe-area-inset-right));
}

/* ---------------- Navbar ---------------- */
.navbar {
  height: 65px;
  flex: 0 0 auto;
  background: var(--bg-elevated);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  gap: 25px;
  padding: 0 20px;
  /* Without this the navbar is an unpositioned, z-index:auto box, so the
     Settings/account dropdown inside it (.user-dropdown, position:absolute)
     stacks against the WHOLE PAGE rather than just the navbar — and since
     .content comes later in the DOM and gets its own compositing layer
     (see the -webkit-mask-image note on .content below), it was painting
     over the open dropdown and eating its clicks. Giving the navbar its
     own stacking context above .content fixes that for every template. */
  position: relative;
  z-index: 2;
  /* Baseline zero-state for box-shadow/backdrop-filter so the Layout
     template swap (below) can animate INTO these rather than popping —
     see the "Layout templates" transition note further down. */
  box-shadow: 0 8px 24px -12px rgba(0,0,0,0);
  backdrop-filter: blur(0px) saturate(100%);
  -webkit-backdrop-filter: blur(0px) saturate(100%);
  transition:
    background .4s cubic-bezier(.22,1,.36,1), border-color .4s cubic-bezier(.22,1,.36,1),
    border-radius .4s cubic-bezier(.22,1,.36,1), box-shadow .4s cubic-bezier(.22,1,.36,1),
    backdrop-filter .4s cubic-bezier(.22,1,.36,1), -webkit-backdrop-filter .4s cubic-bezier(.22,1,.36,1);
  /* Its own stable compositing layer, isolated from the .content pane's
     scroll/mask repaints below it — see the note on .content for why this
     is what actually stops the navbar's icon/text from shifting on scroll
     (earlier fix attempts only touched the vh/dvh/svh unit, which wasn't
     the real cause). */
  transform: translateZ(0);
  -webkit-transform: translateZ(0);
}
.brand { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
/* The icon and the wordmark sit on separate baselines by default (an
   inline <svg> reserves a few px below its own baseline the way an
   inline <img> does, and the display-font's line-height adds more space
   above/below the glyphs than the icon has) — flex align-items:center
   alone can't fix that mismatch. Forcing the icon into its own centered
   flex box and collapsing the wordmark's line-height to 1 puts both
   optical centers on the exact same line. */
.brand-icon {
  width: 30px; height: 30px; color: var(--primary);
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
}
.brand-icon svg { width: 30px; height: 30px; display: block; }
.brand-name { font-family: var(--font-display); font-weight: 700; font-size: 19px; letter-spacing: -0.02em; line-height: 1; }

.navbar-search {
  position: relative;
  flex: 1 1 auto;
  max-width: 480px;
  margin: 0 auto;
  background: var(--bg-card);
  border: 1px solid var(--border-soft, var(--border));
  border-radius: 999px;
  height: 42px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 14px 0 44px;
  cursor: text;
  transition: border-color .25s, background .4s cubic-bezier(.22,1,.36,1);
}
.navbar-search:hover { border-color: var(--border); }
.navbar-search:focus-within {
  border-color: var(--primary);
}
.navbar-search-icon {
  position: absolute; left: 15px; top: 50%; transform: translateY(-50%);
  width: 23px; height: 23px; color: var(--text-faint); flex: 0 0 auto; pointer-events: none;
}
.navbar-search-icon svg { width: 23px; height: 23px; }
.navbar-search-input {
  flex: 1; min-width: 0; height: 100%;
  background: transparent; border: none; outline: none; color: var(--text); font-size: 14px; font-weight: 500; width: 100%;
  appearance: none;
}
.navbar-search-input::placeholder { color: var(--text-faint); }
.navbar-search-input::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.navbar-search-kbd {
  flex: 0 0 auto; display: flex; align-items: center; justify-content: center;
  min-width: 22px; height: 22px; padding: 0 7px; border-radius: 999px;
  background: var(--bg-card-hover); border: 1px solid var(--border);
  font-family: var(--font-mono, ui-monospace, monospace); font-size: 11px; color: var(--text-dim); transition: opacity .25s;
}
@media (hover: hover) { .navbar-search:not(:focus-within):hover .navbar-search-kbd { border-color: var(--border); } }
.navbar-search:focus-within .navbar-search-kbd { opacity: .55; }

.artist-search {
  max-width: 320px;
  margin-top: 14px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 999px;
  height: 42px;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0 16px;
  transition: border-color .25s;
}
.artist-search:focus-within { border-color: var(--primary); }
.artist-search-icon { width: 23px; height: 23px; color: var(--text-faint); flex: 0 0 auto; }
.artist-search-icon svg { width: 23px; height: 23px; }
.artist-search input { background: transparent; border: none; color: var(--text); font-size: 14px; width: 100%; }
.artist-search input::placeholder { color: var(--text-faint); }

.navbar-auth { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; position: relative; }

/* ---------------- Body row (sidebar + content) ---------------- */
.body-row {
  flex: 1 1 auto;
  display: flex;
  gap: var(--gap);
  min-height: 0;
}

/* ---------------- Sidebar ---------------- */
.sidebar {
  width: 260px;
  flex: 0 0 auto;
  background: var(--bg-elevated);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  display: flex;
  flex-direction: column;
  padding: 18px 12px;
  gap: 22px;
  overflow-y: auto;
  /* Zero-state baseline so the Layout template swap can animate box-shadow
     in/out instead of popping — matches .navbar/.player-bar. */
  box-shadow: 0 10px 30px -16px rgba(0,0,0,0);
  transition:
    background .4s cubic-bezier(.22,1,.36,1), border-color .4s cubic-bezier(.22,1,.36,1),
    border-radius .4s cubic-bezier(.22,1,.36,1), box-shadow .4s cubic-bezier(.22,1,.36,1);
  /* Safari doesn't clip a scrollable box's own content/scrollbar to its
     border-radius — at the very top/bottom of the scroll, corners look
     square instead of rounded. Forcing a mask makes Safari repaint the
     element through its own layer, which clips it correctly; other
     browsers already clip fine and ignore this. */
  -webkit-mask-image: -webkit-radial-gradient(white, black);
  /* Own compositing layer up front so the mask isn't re-rasterized into
     the shared stacking context on every scroll frame — see the matching
     note on .content, which is what was actually behind the tab-bar
     jitter. */
  transform: translateZ(0);
  -webkit-transform: translateZ(0);
  overscroll-behavior: contain;
}
.sidebar-nav { display: flex; flex-direction: column; gap: 2px; }
.sidebar-link {
  display: flex; align-items: center; gap: 14px; position: relative;
  background: none; border: none; color: var(--text-dim);
  font-size: 14.5px; font-weight: 600; padding: 11px 12px; border-radius: var(--radius-sm);
  cursor: pointer; text-align: left;
  transition: background .25s, color .25s, padding .3s cubic-bezier(.22,1,.36,1), font-size .3s cubic-bezier(.22,1,.36,1);
}
.sidebar-link::before {
  content: ''; position: absolute; left: -12px; top: 50%; width: 3px; height: 0;
  border-radius: 0 3px 3px 0; background: var(--primary-grad); transform: translateY(-50%);
  transition: height .28s ease;
}
@media (hover: hover) { .sidebar-link:hover { color: var(--text); background: var(--bg-card); } }
.sidebar-link.active { color: var(--text); background: var(--bg-card); }
.sidebar-link.active::before { height: 60%; }
.sidebar-link .icon { flex: 0 0 auto; }
.sidebar-link-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.sidebar-library {
  border-top: 1px solid var(--border-soft); padding-top: 18px; display: flex; flex-direction: column; gap: 6px;
  transition: border-color .4s cubic-bezier(.22,1,.36,1), padding-top .4s cubic-bezier(.22,1,.36,1);
}
.sidebar-library-title-row { display: flex; align-items: center; justify-content: space-between; padding: 0 4px 6px 12px; }
.sidebar-library-title { font-size: 11px; text-transform: uppercase; letter-spacing: .08em; color: var(--text-faint); font-weight: 700; }
.sidebar-library-actions { display: flex; align-items: center; gap: 2px; }
.sidebar-new-playlist-btn, .sidebar-view-btn { width: 20px; height: 20px; color: var(--text-faint); }
@media (hover: hover) {
  .sidebar-new-playlist-btn:hover, .sidebar-view-btn:hover { color: var(--text); background: var(--bg-card); }
}
.sidebar-new-playlist-btn svg, .sidebar-view-btn svg { width: 16px; height: 16px; transition: all .22s cubic-bezier(.34,1.56,.64,1); }
.sidebar-view-btn[aria-expanded="true"] svg { color: var(--primary); }
.sidebar-liked { padding: 10px 12px; transition: background .25s; }
.sidebar-liked.active { background: rgba(59,130,246,.10); }
.sidebar-liked-icon { width: 30px; height: 30px; border-radius: 5px; background: var(--primary-grad); display: flex; align-items: center; justify-content: center; color: #fff; }
.sidebar-liked-icon svg { width: 23px; height: 23px; }
.sidebar-topsongs-icon {
  width: 30px; height: 30px; border-radius: 5px; flex: 0 0 auto;
  background: url('../icons/top-songs-cover.webp') center/cover no-repeat;
}
.sidebar-liked-text { display: flex; flex-direction: column; gap: 2px; }
.sidebar-liked-text strong { font-size: 14px; color: var(--text); }
.sidebar-liked-text small { font-size: 12px; color: var(--text-faint); font-weight: 500; }
#likedCount.count-bump { display: inline-block; animation: countBump .35s cubic-bezier(.34,1.56,.64,1); }

.sidebar-playlists { display: flex; flex-direction: column; gap: 2px; max-height: 260px; overflow-y: auto; }
.sidebar-playlist-link { padding: 8px 12px; transition: background .25s; }
.sidebar-playlist-link.active { background: rgba(59,130,246,.10); }
.sidebar-playlist-cover { width: 30px; height: 30px; border-radius: 5px; flex: 0 0 auto; background: var(--bg-card) center/cover no-repeat; display: flex; align-items: center; justify-content: center; color: var(--text-faint); overflow: hidden; }
.sidebar-playlist-cover.placeholder { background: linear-gradient(135deg, #1a2030, #10131a); }
.sidebar-playlist-cover svg { width: 15px; height: 15px; }
.sidebar-playlist-text { font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sidebar-playlists-empty { font-size: 12px; color: var(--text-faint); padding: 4px 12px; margin: 0; line-height: 1.5; }

/* ---------------- "View as" menu (button popover) ---------------- */
.view-as-menu { width: 200px; }
.view-as-menu-title {
  font-size: 11px; text-transform: uppercase; letter-spacing: .06em; font-weight: 700;
  color: var(--text-faint); padding: 6px 12px 8px;
}
/* The shared `.song-menu-item span:not(.song-menu-arrow):not(.song-menu-check)`
   rule (used by every other floating menu) sets `flex:1` on every plain
   <span> child, including this icon wrapper — it has higher specificity
   than a single class, so it was winning and stretching the icon to fill
   half the row (blank space around a centered glyph, label shoved to the
   right, "Compact grid" wrapping onto a second line). Pin it back down. */
.view-as-menu-item span.view-as-menu-icon {
  flex: 0 0 20px !important; width: 20px; height: 20px;
  display: flex; align-items: center; justify-content: center; color: var(--text-faint);
}
.view-as-menu-icon svg { width: 17px; height: 17px; }
.view-as-menu-item > span:not(.view-as-menu-icon):not(.song-menu-check) {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.view-as-menu-item[aria-checked="true"] { color: var(--text); }
.view-as-menu-item[aria-checked="true"] .view-as-menu-icon { color: var(--primary); }

/* ---------------- Library layout modes ---------------- */
/* Compact list: current row shape, just tighter and no subtitle line —
   the closest analogue to Spotify's "Compact list". */
.sidebar-library.view-compact-list .sidebar-liked,
.sidebar-library.view-compact-list .sidebar-playlist-link { padding: 6px 12px; }
.sidebar-library.view-compact-list .sidebar-liked-icon,
.sidebar-library.view-compact-list .sidebar-topsongs-icon,
.sidebar-library.view-compact-list .sidebar-playlist-cover { width: 24px; height: 24px; border-radius: 4px; }
.sidebar-library.view-compact-list .sidebar-liked-icon svg { width: 17px; height: 17px; }
.sidebar-library.view-compact-list .sidebar-liked-text { gap: 0; }
.sidebar-library.view-compact-list .sidebar-liked-text strong { font-size: 13px; }
.sidebar-library.view-compact-list .sidebar-liked-text small { display: none; }
.sidebar-library.view-compact-list .sidebar-playlist-text { font-size: 13px; }

/* Default list is the pre-existing look — no overrides needed, it's the
   layout the sidebar already ships with (kept as its own named class so
   the menu and the CSS agree on what "default" refers to). */

/* Compact & default grid: Liked Songs, Top Songs and every playlist become
   square tiles that flow together in one CSS grid. #sidebarPlaylists is
   collapsed with display:contents so its dynamically-rendered buttons join
   the SAME grid as the two static rows above them, instead of forming a
   separate list nested inside one cell. */
.sidebar-library.view-compact-grid,
.sidebar-library.view-default-grid {
  display: grid;
  align-items: start;
  gap: 16px 10px;
}
.sidebar-library.view-compact-grid { grid-template-columns: repeat(3, 1fr); gap: 12px 8px; }
.sidebar-library.view-default-grid { grid-template-columns: repeat(2, 1fr); }
.sidebar-library.view-compact-grid .sidebar-library-title-row,
.sidebar-library.view-default-grid .sidebar-library-title-row,
.sidebar-library.view-compact-grid .sidebar-playlists-empty,
.sidebar-library.view-default-grid .sidebar-playlists-empty { grid-column: 1 / -1; }
.sidebar-library.view-compact-grid .sidebar-playlists,
.sidebar-library.view-default-grid .sidebar-playlists {
  display: contents; /* let its children become direct items of the parent grid */
  max-height: none; overflow: visible;
}
.sidebar-library.view-compact-grid .sidebar-link,
.sidebar-library.view-default-grid .sidebar-link {
  flex-direction: column; align-items: stretch; gap: 8px; padding: 8px 6px; text-align: center;
  border-radius: var(--radius-sm);
}
/* Compact grid is cover-only (no title/subtitle at all, like Spotify's
   "Compact grid") — the tile is just the artwork, so it needs no gap or
   inner padding reserved for text. */
.sidebar-library.view-compact-grid .sidebar-link { gap: 0; padding: 3px; }
.sidebar-library.view-compact-grid .sidebar-link::before,
.sidebar-library.view-default-grid .sidebar-link::before { display: none; } /* the left active-rail doesn't read as a tile accent */
.sidebar-library.view-compact-grid .sidebar-link.active,
.sidebar-library.view-default-grid .sidebar-link.active { background: rgba(59,130,246,.10); }
.sidebar-library.view-compact-grid .sidebar-liked-icon,
.sidebar-library.view-compact-grid .sidebar-topsongs-icon,
.sidebar-library.view-compact-grid .sidebar-playlist-cover,
.sidebar-library.view-default-grid .sidebar-liked-icon,
.sidebar-library.view-default-grid .sidebar-topsongs-icon,
.sidebar-library.view-default-grid .sidebar-playlist-cover {
  width: 100%; height: auto; aspect-ratio: 1 / 1; border-radius: 6px;
}
.sidebar-library.view-compact-grid .sidebar-liked-icon svg,
.sidebar-library.view-default-grid .sidebar-liked-icon svg { width: 38%; height: 38%; }
.sidebar-library.view-compact-grid .sidebar-playlist-cover svg,
.sidebar-library.view-default-grid .sidebar-playlist-cover svg { width: 28%; height: 28%; }
/* Compact grid: cover only — title, subtitle and their layout box are
   fully removed, not just visually hidden, so no empty space is reserved
   for them under the tile. */
.sidebar-library.view-compact-grid .sidebar-liked-text,
.sidebar-library.view-compact-grid .sidebar-playlist-text { display: none; }
.sidebar-library.view-default-grid .sidebar-liked-text { align-items: center; width: 100%; gap: 1px; }
.sidebar-library.view-default-grid .sidebar-liked-text strong {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  font-size: 12px; line-height: 1.3;
}
.sidebar-library.view-default-grid .sidebar-liked-text small {
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden;
  font-size: 10.5px; white-space: normal; line-height: 1.3;
}
.sidebar-library.view-default-grid .sidebar-playlist-text {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  white-space: normal; font-size: 12px; line-height: 1.3;
}

/* Small entrance animation whenever the layout mode changes, so the switch
   reads as an intentional transition rather than a hard cut. */
.sidebar-library.library-view-anim .sidebar-liked,
.sidebar-library.library-view-anim .sidebar-playlist-link {
  animation: libraryViewItemIn .32s cubic-bezier(.22,1.2,.36,1) both;
}
.sidebar-library.library-view-anim .sidebar-playlists .sidebar-playlist-link:nth-child(2) { animation-delay: .02s; }
.sidebar-library.library-view-anim .sidebar-playlists .sidebar-playlist-link:nth-child(3) { animation-delay: .04s; }
.sidebar-library.library-view-anim .sidebar-playlists .sidebar-playlist-link:nth-child(4) { animation-delay: .06s; }
.sidebar-library.library-view-anim .sidebar-playlists .sidebar-playlist-link:nth-child(5) { animation-delay: .08s; }
.sidebar-library.library-view-anim .sidebar-playlists .sidebar-playlist-link:nth-child(n+6) { animation-delay: .1s; }
@keyframes libraryViewItemIn {
  from { opacity: 0; transform: translateY(6px) scale(.96); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}

.sidebar-footer { margin-top: auto; padding: 10px 12px; }
.sidebar-footer p { font-size: 11px; color: var(--text-faint); margin: 0; }

/* ---------------- Content ---------------- */
.content {
  flex: 1 1 auto;
  --content-tint: 1;
  background: linear-gradient(180deg, rgba(59,130,246, calc(.08 * var(--content-tint))), color-mix(in srgb, var(--bg-elevated) calc(var(--content-tint) * 100%), transparent) 340px);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  overflow-y: auto;
  padding: 26px 30px 40px;
  -webkit-mask-image: -webkit-radial-gradient(white, black);
  transition:
    --content-tint .4s cubic-bezier(.22,1,.36,1), border-color .4s cubic-bezier(.22,1,.36,1),
    border-radius .4s cubic-bezier(.22,1,.36,1);
  /* Real root cause of the long-standing "tab bar icons/labels wobble while
     scrolling" bug (previous attempts only touched the vh/dvh/svh unit,
     which was a red herring): -webkit-mask-image on a scrolling element
     forces WebKit/Blink to re-rasterize that whole masked layer on every
     scroll frame. Without its own stable compositing layer, that repaint
     was bleeding into the shared stacking context and nudging the flex
     siblings below it (.player-bar, .mobile-nav) by a subpixel each frame
     — which reads as the icons/labels "shaking". Promoting .content to its
     own layer up front (translateZ(0)) makes the browser composite it in
     isolation instead of repainting the whole stack on every frame.
     overscroll-behavior: contain stops the same jitter from a second,
     unrelated cause: scroll-chaining bounce escaping into the page. */
  transform: translateZ(0);
  -webkit-transform: translateZ(0);
  overscroll-behavior: contain;
}
.view-header { margin-bottom: 26px; }
.view-header h1 { font-size: 30px; font-weight: 700; }
.view-sub { color: var(--text-dim); font-size: 14px; margin: 6px 0 0; }

.section-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 14px; }
/* A second section heading directly under a grid had no top margin at
   all, so "Following" sat flush against the last row of playlists and
   read as part of it. */
.artist-grid + .section-head,
.song-grid + .section-head,
.track-list + .section-head,
.empty-state + .section-head { margin-top: 34px; }
.section-head h2 { font-size: 19px; font-weight: 700; }

/* Song grid (cards) */
.song-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
  align-items: start; /* without this, CSS Grid stretches every card in a row to match
                          the tallest one, which is what made cards look tall/warped */
  gap: 18px;
}
/* Invisible marker used by the incremental-rendering loader (see app.js) to
   detect when the user has scrolled near the end of a grid/list, so the next
   batch of cards/rows can be appended — instead of building the whole
   catalog's worth of DOM nodes up front. */
.incremental-sentinel { grid-column: 1 / -1; height: 1px; pointer-events: none; }
.song-card {
  background: var(--bg-card);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-sm);
  padding: 14px;
  cursor: pointer;
  position: relative;
  /* Everything inside a card (cover, eq bars, play/like/menu buttons) must
     stack against the card and nothing else. Without this the card is only
     position:relative with z-index:auto, so it is NOT a stacking context and
     a positioned child's z-index competes against the whole page — which is
     how the play button ended up painting over the "..." menu, the toast and
     the modals. */
  isolation: isolate;
  transition: background .28s, border-color .28s;
}
.song-card.card-in { animation: cardIn .38s cubic-bezier(.22,1,.36,1) both; }
@media (hover: hover) { .song-card:hover { background: var(--bg-card-hover); border-color: #2a3040; } }
.song-card-cover-wrap { position: relative; margin-bottom: 12px; overflow: hidden; border-radius: var(--radius-sm); }
.song-card-cover {
  width: 100%; aspect-ratio: 1/1; border-radius: var(--radius-sm); background: var(--bg-elevated);
  background-size: cover; background-position: center;
  display: flex; align-items: center; justify-content: center; color: var(--text-faint);
}
.song-card-cover.placeholder { background: linear-gradient(135deg, #1a2030, #10131a); }
.song-card-cover.placeholder svg { width: 34%; height: 34%; opacity: .5; }
.song-card-play {
  position: absolute; bottom: 5px; right: 5px; width: 35px; height: 35px; border-radius: 50%;
  background: var(--primary-grad); color: #fff; display: flex; align-items: center; justify-content: center;
  border: none; cursor: pointer; opacity: 0; z-index: 3; overflow: hidden;
  /* was `background 0.28s ease opacity .28s, transform ...` — two property
     names in one item makes the whole item invalid, so the browser dropped it
     and the button popped in with no fade at all. */
  transition: opacity .28s ease, filter .28s ease;
}
.song-card-play::after{
  content:''; position:absolute; inset:0; background: linear-gradient(120deg, transparent 30%, rgba(255,255,255,.25) 50%, transparent 70%);
  transform: translateX(-100%); transition: transform .5s ease;
}
@media (hover: hover) { .song-card-play:hover::after { transform: translateX(100%); } }
@media (hover: hover) { .song-card-play:hover { filter: brightness(1.1); } }
@media (hover: hover) { .song-card:hover .song-card-play { opacity: 1; } }
.song-card-play svg { width: 18px; height: 18px; }
.song-card-title { font-size: 14px; font-weight: 700; margin: 0 0 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color .25s; }
.song-card-artist { font-size: 12.5px; color: var(--text-faint); margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.song-card.playing { border-color: rgba(59,130,246,.4); }
.song-card.playing .song-card-title { color: var(--primary); }
.song-card-like {
  position: absolute; top: 5px; left: 5px; width: 30px; height: 30px; border-radius: 50%;
  background: rgba(10,12,17,.65); backdrop-filter: blur(5px); border: none; color: #fff;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  opacity: 0; z-index: 3; transition: background .25s, opacity .25s;
}
@media (hover: hover) { .song-card-like:hover { background: rgba(10,12,17,.75); } }
.song-card-like.liked { opacity: 1; }
@media (hover: hover) { .song-card:hover .song-card-like { opacity: 1; } }
.song-card-like svg { width: 15px; height: 15px; }

.song-card-menu {
  position: absolute; top: 5px; right: 5px; width: 30px; height: 30px; border-radius: 50%;
  background: rgba(10,12,17,.65); backdrop-filter: blur(4px); border: none; color: #fff;
  display: flex; align-items: center; justify-content: center; cursor: pointer;
  opacity: 0; z-index: 3; transition: opacity .25s, background .25s;
}
@media (hover: hover) { .song-card:hover .song-card-menu { opacity: 1; } }
/* While its menu is open the card keeps showing its controls, so the button
   you just clicked doesn't vanish from under the pointer. */
.song-card.menu-open .song-card-play,
.song-card.menu-open .song-card-like,
.song-card.menu-open .song-card-menu { opacity: 1; }
@media (hover: hover) { .song-card-menu:hover { background: rgba(10,12,17,.75); } }
.song-card-menu svg { width: 15px; height: 15px; }

/* "now playing" equalizer indicator, shown over the cover art */
.eq-indicator {
  position: absolute; inset: 0; z-index: 1; display: none; align-items: center; justify-content: center; background: rgba(10,12,17,.55); gap: 3px; border-radius: 8px;
}
.song-card.playing .eq-indicator { display: flex; }
.eq-indicator span { width: 3px; height: 40%; background: var(--primary); border-radius: 2px; animation: eqBounce .8s ease-in-out infinite; transition: height .25s ease; }
.eq-indicator span:nth-child(2) { animation-delay: .16s; }
.eq-indicator span:nth-child(3) { animation-delay: .32s; }
.eq-indicator span:nth-child(4) { animation-delay: .64s; }
.eq-indicator span:nth-child(5) { animation-delay: .128s; }
/* Paused: instead of freezing mid-bounce at a random height, drop every
   bar back to the same flat resting line — like the moment before the
   animation ever started — and keep it there until playback resumes. */
.song-card.playing.paused .eq-indicator span { animation: none; height: 25%; }

/* Track list (Liked Songs) */
.playlist-hero { display: flex; align-items: flex-end; gap: 24px; margin-bottom: 22px; }
.playlist-hero-info { min-width: 0; }
.playlist-hero-cover {
  width: 140px; height: 140px; border-radius: var(--radius-md); flex: 0 0 auto;
  background: var(--primary-grad); display: flex; align-items: center; justify-content: center; color: #fff;
  overflow: hidden; background-size: cover; background-position: center;
}
.playlist-hero-cover svg { width: 100px; height: 100px; }
.playlist-hero-cover.placeholder { background: linear-gradient(135deg, #263049, #12151c); }
.playlist-hero-cover.placeholder svg { width: 46px; height: 46px; opacity: .55; }
.playlist-hero-kind { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: var(--text-dim); }
.playlist-hero-info h1 { font-size: 44px; margin: 8px 0; }
.playlist-hero-sub { color: var(--text-dim); font-size: 14px; margin: 0; }
.hero-duration { color: var(--text-faint); }
.playlist-hero-owner { display: flex; align-items: center; gap: 8px; font-size: 13.5px; color: var(--text-dim); margin: 4px 0; }
.playlist-hero-owner b { color: var(--text); }
.playlist-hero-owner-avatar {
  width: 22px; height: 22px; border-radius: 50%; background: var(--bg-card) center/cover no-repeat;
  display: flex; align-items: center; justify-content: center; color: var(--text-faint); flex: 0 0 auto;
}
.playlist-hero-owner-avatar svg { width: 12px; height: 12px; }

/* ---- Artists view ---- */
.artist-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 20px; }
.artist-card {
  background: var(--bg-card); border: 1px solid transparent; border-radius: var(--radius-md); padding: 16px;
  display: flex; flex-direction: column; align-items: flex-start; gap: 4px; cursor: pointer; text-align: left;
  transition: background .22s, transform .22s, border-color .22s; opacity: 0; animation: rowIn .32s cubic-bezier(.22,1,.36,1) both;
}
@media (hover: hover) { .artist-card:hover { background: var(--bg-card-hover); transform: translateY(-2px); border-color: var(--border-soft); } }
.artist-card-cover {
  width: 100%; aspect-ratio: 1; border-radius: 50%; background: var(--bg-elevated) center/cover no-repeat;
  display: flex; align-items: center; justify-content: center; color: var(--text-faint); margin-bottom: 8px;
}
.artist-card-cover svg { width: 30%; height: 30%; opacity: .6; }

/* Playlist covers shown on a user's profile page — square with soft rounded
   corners (like every other playlist cover in the app), not the circular
   artist-avatar look of .artist-card-cover. `position: relative` +
   `overflow: hidden` is what actually keeps the auto-generated collage
   (which is absolutely positioned) from spilling out and stretching the card. */
.profile-playlist-cover {
  width: 100%; aspect-ratio: 1 / 1; border-radius: var(--radius-sm); background: var(--bg-elevated) center/cover no-repeat;
  display: flex; align-items: center; justify-content: center; color: var(--text-faint); margin-bottom: 8px;
  position: relative; overflow: hidden;
}
.profile-playlist-cover.placeholder { background: linear-gradient(135deg, #1a2030, #10131a); }
.profile-playlist-cover svg { width: 30%; height: 30%; opacity: .6; }
.artist-card-name { font-weight: 700; font-size: 14.5px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.artist-card-count { font-size: 12.5px; color: var(--text-faint); }
.artist-back-btn { display: inline-flex; align-items: center; gap: 6px; margin-bottom: 18px; }
.artist-back-btn svg { width: 14px; height: 14px; }

/* Weekly/Liked/Playlist/Profile back buttons — only shown in the mobile
   full-screen detail view (see the ≤760px block below); the desktop
   equivalent is just the sidebar, which stays visible and clickable. */
.view-back-btn {
  display: none; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: 50%; padding: 0; margin-bottom: 16px;
  background: var(--bg-card); border: 1px solid var(--border); color: var(--text);
  cursor: pointer; transition: background .25s, transform .2s;
}
@media (hover: hover) { .view-back-btn:hover { background: var(--bg-card-hover); } }
.view-back-btn svg { width: 18px; height: 18px; }
.artist-hero-cover { border-radius: 50%; background-image: linear-gradient(135deg, #334155, #0f172a); }
.artist-hero-cover.placeholder svg { width: 42px; height: 42px; }

/* ---- Weekly Mix view ---- */
.weekly-hero-cover.placeholder { background: linear-gradient(135deg, #1a2030, #10131a); }
.weekly-hero-cover.placeholder svg { width: 46px; height: 46px; opacity: .55; }
.topsongs-hero-cover { background: url('../icons/top-songs-cover.webp') center/cover no-repeat; }

/* 4-cover collage used as a playlist's auto-generated cover art */
.cover-collage { position: absolute; inset: 0; display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.cover-collage-cell { background: var(--bg-card) center/cover no-repeat; display: flex; align-items: center; justify-content: center; color: var(--text-faint); }
.cover-collage-cell svg { width: 30%; height: 30%; opacity: .5; }
.playlist-hero-cover, .sidebar-playlist-cover, .song-card-cover, .track-cover { position: relative; }

.playlist-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 22px; }
.playlist-toolbar .icon-btn:not(.play-toolbar-btn) { color: var(--text-dim); width: 38px; height: 38px; }
@media (hover: hover) { .playlist-toolbar .icon-btn:not(.play-toolbar-btn):hover { color: var(--text); background: var(--bg-card); } }
/* The primary action on a playlist / artist / mix page. It had been
   sharing the dim secondary styling of the icons next to it, so the one
   button people actually came to press was the least visible thing on
   the page. */
/* Scoped to .playlist-toolbar on purpose: a bare `.play-toolbar-btn`
   ties with `.icon-btn` on specificity, and `.icon-btn` is declared
   further down the file — so the generic 34px transparent button quietly
   won and this looked like it had no styling at all.

   Styled to match .song-card-play exactly, down to the sheen sweep on
   hover: it is the same action, so it should not be a second, different
   kind of button. No drop shadow and no hover scale — the card version
   has neither. */
.playlist-toolbar .play-toolbar-btn {
  width: 46px; height: 46px; border-radius: 50%;
  background: var(--primary-grad); color: #fff;
  position: relative; overflow: hidden;
  transition: filter .28s ease;
}
.playlist-toolbar .play-toolbar-btn::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(120deg, transparent 30%, rgba(255,255,255,.25) 50%, transparent 70%);
  transform: translateX(-100%); transition: transform .5s ease;
}
@media (hover: hover) { .playlist-toolbar .play-toolbar-btn:hover::after { transform: translateX(100%); } }
@media (hover: hover) { .playlist-toolbar .play-toolbar-btn:hover { filter: brightness(1.1); } }
.playlist-toolbar .play-toolbar-btn svg { width: 19px; height: 19px; margin-left: 2px; /* optical centring of the triangle */ }
/* Play and Follow are different kinds of action — one starts the music,
   one changes a relationship — so they should not read as a pair of
   buttons welded together. The toolbar's own 10px gap is right for the
   icon buttons that sit next to Play elsewhere; this one needs more. */
.playlist-toolbar .follow-artist-btn { margin-left: 10px; }
#playlistSaveBtn.saved { color: var(--primary); }

.track-list { display: flex; flex-direction: column; }
.track-row {
  display: grid; grid-template-columns: 32px 1fr 400px 84px; align-items: center; gap: 16px; 
  padding: 9px 12px; border-radius: var(--radius-sm); cursor: pointer; color: var(--text-dim);
  transition: background .22s; position: relative;
}
.track-row.draggable-row { padding-left: 34px; }
.track-drag-handle {
  position: absolute; left: 8px; top: 50%; transform: translateY(-50%); color: var(--text-faint);
  cursor: grab; opacity: 0; transition: opacity .2s; display: flex;
}
@media (hover: hover) { .track-row.draggable-row:hover .track-drag-handle { opacity: 1; } }
.track-drag-handle svg { width: 14px; height: 14px; }
.track-row.dragging { opacity: .4; }
.track-row.drag-over-top { box-shadow: inset 0 2px 0 var(--primary); }
.track-row.drag-over-bottom { box-shadow: inset 0 -2px 0 var(--primary); }
.track-row.row-in { animation: rowIn .32s cubic-bezier(.22,1,.36,1) both; }
@media (hover: hover) { .track-row:hover { background: var(--bg-card); } }
.track-row.playing { color: var(--primary); background: rgba(59,130,246,.06); }
/* Index cell: three states (track number / hover play icon / now-playing bars)
   cross-fade + scale into each other instead of an instant display swap. */
.track-index { position: relative; font-size: 13px; color: var(--text-faint); text-align: center; width: 100%; height: 18px; }
.track-index-num, .track-row-play-icon, .track-eq {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  opacity: 0; transform: scale(.5) rotate(-12deg);
  transition: opacity .22s cubic-bezier(.34,1.56,.64,1), transform .22s cubic-bezier(.34,1.56,.64,1);
  pointer-events: none;
}
.track-index-num { opacity: 1; transform: scale(1) rotate(0deg); }
.track-row-play-icon svg { width: 14px; height: 14px; color: var(--text); }
@media (hover: hover) { .track-row:hover:not(.playing) .track-index-num { opacity: 0; transform: scale(.5) rotate(12deg); } }
@media (hover: hover) { .track-row:hover:not(.playing) .track-row-play-icon { opacity: 1; transform: scale(1) rotate(0deg); } }
.track-row.playing .track-index-num,
.track-row.playing .track-row-play-icon { opacity: 0; transform: scale(.5) rotate(-12deg); }
.track-eq { gap: 2px; }
.track-row.playing .track-eq { opacity: 1; transform: scale(1) rotate(0deg); }
.track-eq span { width: 2.5px; height: 40%; background: var(--primary); border-radius: 2px; animation: eqBounce .7s ease-in-out infinite; transition: height .25s ease; }
.track-eq span:nth-child(2) { animation-delay: .14s; }
.track-eq span:nth-child(3) { animation-delay: .28s; }
/* Paused: instead of freezing mid-bounce at a random height, drop every
   bar back to the same flat resting line — like the moment before the
   animation ever started — and keep it there until playback resumes. */
.track-row.playing.paused .track-eq span { animation: none; height: 25%; }
.track-main { display: flex; align-items: center; gap: 12px; min-width: 0; }
.track-cover { width: 42px; height: 42px; border-radius: var(--radius-sm); background: var(--bg-elevated) center/cover no-repeat; flex: 0 0 auto; display:flex; align-items:center; justify-content:center; }
.track-cover.placeholder svg { width: 40%; height: 40%; opacity: .5; color: var(--text-faint); }
.track-text { min-width: 0; }
.track-title { font-size: 14px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.track-artist { font-size: 12.5px; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.track-album { font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.track-actions { display: flex; align-items: center; gap: 8px; justify-content: flex-end; }
.track-like-btn { position: relative; background: none; border: none; color: var(--text-faint); cursor: pointer; opacity: 0; transition: opacity .25s, color .25s; }
@media (hover: hover) { .track-like-btn:hover { color: var(--text); } }
.track-like-btn.liked { opacity: 1; }
@media (hover: hover) { .track-row:hover .track-like-btn { opacity: 1; } }
.track-like-btn.liked { color: var(--primary); }
.track-like-btn svg { width: 16px; height: 16px; }
.track-menu-btn { background: none; border: none; color: var(--text-faint); cursor: pointer; opacity: 0; transition: opacity .25s, color .25s, background .25s; }
@media (hover: hover) { .track-row:hover .track-menu-btn { opacity: 1; } }
/* Same as the song cards: reaching the open menu means leaving the row, which
   would otherwise fade out the button that opened it. */
.track-row.menu-open .track-like-btn,
.track-row.menu-open .track-menu-btn { opacity: 1; }
@media (hover: hover) { .track-menu-btn:hover { color: var(--text); } }
.track-menu-btn svg { width: 16px; height: 16px; }
.track-duration { font-size: 13px; font-weight: 600; color: var(--text-faint); min-width: 34px; text-align: right; }

.empty-state { color: var(--text-faint); font-size: 14px; padding: 40px 10px; text-align: center; }

.locked-state { display: flex; flex-direction: column; align-items: center; gap: 14px; padding: 60px 20px; text-align: center; }
.locked-icon { width: 64px; height: 64px; border-radius: 50%; background: var(--bg-card); display: flex; align-items: center; justify-content: center; color: var(--text-faint); }
.locked-icon svg { width: 26px; height: 26px; }
.locked-state h3 { font-size: 18px; }
.locked-state p { color: var(--text-dim); font-size: 13.5px; margin: 0; max-width: 320px; }

/* Artist credits under the player bar / full-screen player. Styled as
   text, not as a button — it should read as the artist's name that
   happens to be clickable, which is what it is everywhere else. */
.artist-link {
  background: none; border: none; padding: 0; margin: 0;
  font: inherit; color: inherit; cursor: pointer;
  text-decoration: none; transition: color .2s;
}
@media (hover: hover) { .artist-link:hover { color: var(--text); text-decoration: underline; } }
@media (hover: hover) { .now-playing-titles .artist-link:hover { color: #fff; } }

/* =========================================================
   Player bar
   ========================================================= */
.player-bar {
  flex: 0 0 auto;
  height: 76px;
  background: var(--bg-elevated);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 560px) minmax(0, 1fr);
  align-items: center;
  padding: 0 18px;
  gap: 16px;
  /* Zero-state baseline so the Layout template swap (see the dedicated
     section further down) can animate INTO these rather than popping. */
  box-shadow: 0 14px 34px -16px rgba(0,0,0,0);
  backdrop-filter: blur(0px) saturate(100%);
  -webkit-backdrop-filter: blur(0px) saturate(100%);
  transition:
    background .4s cubic-bezier(.22,1,.36,1), border-color .4s cubic-bezier(.22,1,.36,1),
    border-radius .4s cubic-bezier(.22,1,.36,1), box-shadow .4s cubic-bezier(.22,1,.36,1),
    backdrop-filter .4s cubic-bezier(.22,1,.36,1), -webkit-backdrop-filter .4s cubic-bezier(.22,1,.36,1);
  /* Own stable compositing layer — same reasoning as .navbar above, keeps
     this immune to the .content pane's scroll/mask repaints. */
  transform: translateZ(0);
  -webkit-transform: translateZ(0);
}
.player-now { display: flex; align-items: center; gap: 12px; min-width: 0; }
.player-cover {
  width: 50px; height: 50px; border-radius: var(--radius-sm); background: var(--bg-card) center/cover no-repeat;
  flex: 0 0 auto; display:flex; align-items:center; justify-content:center; color: var(--text-faint);
  transition: border-radius .4s cubic-bezier(.22,1,.36,1);
}
.player-cover svg { width: 40%; height: 40%; opacity: .5; }
.player-cover.pop-in { animation: coverPop .28s ease-out; }
.player-meta { display: flex; flex-direction: column; min-width: 0; gap: 2px; }
.player-title { font-size: 13.5px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 300px; }
.player-artist { font-size: 12px; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 300px; }

.player-center { display: flex; flex-direction: column; align-items: center; gap: 6px; width: 100%; }
.player-controls { display: flex; align-items: center; gap: 6px; }
.player-controls .icon-btn { color: var(--text-dim); width: 32px; height: 32px; }
@media (hover: hover) { .player-controls .icon-btn:hover { color: var(--text); background: var(--bg-card); } }
.player-controls .icon-btn.active { color: var(--primary); }
.player-controls .icon-btn.active::after { content: ''; position: absolute; bottom: 1px; left: 50%; width: 3px; height: 3px; border-radius: 50%; background: var(--primary); transform: translateX(-50%); }
.player-controls .icon-btn { position: relative; }
.play-btn {
  width: 35px; height: 35px; border-radius: 50%; background: #fff; color: #0a0c11;
  display: flex; align-items: center; justify-content: center; margin: 0 4px;
  transition: transform .25s cubic-bezier(.34,1.56,.64,1);
}
@media (hover: hover) { .play-btn:hover { background: #fff; } }
.play-btn:active { transform: scale(.95); }
.play-btn svg { width: 30px; height: 30px; }

.player-seek { display: flex; align-items: center; gap: 8px; width: 100%; }
.time { font-size: 12px; font-weight: 600; color: var(--text-faint); min-width: 30px; text-align: center; font-variant-numeric: tabular-nums; }

.player-extra { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
.player-extra .icon-btn { color: var(--text-dim); width: 30px; height: 30px; }
@media (hover: hover) { .player-extra .icon-btn:hover { color: var(--text); background: var(--bg-card); } }

.icon-btn {
  background: none; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 50%; transition: color .25s, background .25s, transform .22s;
  position: relative;
}
.like-btn { color: var(--text-faint); }
.like-btn.liked { color: var(--primary); }
.heart-pop svg { animation: heartPop .5s cubic-bezier(.34,1.56,.64,1); }
.song-card-like, .track-like-btn, .like-btn { transition: color .28s, transform .25s; }

/* range inputs — no shadows, flat gradient fill (painted via JS to match --primary exactly) */
input[type="range"] {
  -webkit-appearance: none; appearance: none; height: 5px; border-radius: 5px;
  background: linear-gradient(to right, var(--primary) 0%, var(--border) 0%);
  cursor: pointer; flex: 1;
}
@media (hover: hover) { .player-seek:hover input[type="range"] { height: 5px; } }
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #fff;
  opacity: 0; transition: opacity .25s; margin-top: 0;
}
input[type="range"]::-moz-range-thumb {
  width: 12px; height: 12px; border-radius: 50%; background: #fff; border: none; opacity: 0; transition: opacity .25s;
}
input[type="range"]::-moz-range-track { background: transparent; height: 5px; border-radius: 5px; }
@media (hover: hover) { .player-seek:hover input[type="range"]::-webkit-slider-thumb,.player-extra:hover input[type="range"]::-webkit-slider-thumb,.player-seek:hover input[type="range"]::-moz-range-thumb,.player-extra:hover input[type="range"]::-moz-range-thumb { opacity: 1; } }
#volumeBar { max-width: 200px; }

/* A small "1" over the repeat glyph, so repeat-one is distinguishable
   from repeat-all at a glance instead of both just being "lit". */
.icon-btn.repeat-one::before {
  content: '1';
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -46%);
  font-size: 8px; font-weight: 800; line-height: 1;
  color: var(--primary); pointer-events: none;
}

/* =========================================================
   Auth area (navbar)
   ========================================================= */
.btn {
  border: none; border-radius: var(--radius-sm); font-size: 14px; font-weight: 700; cursor: pointer;
  padding: 10px 18px; font-family: inherit; transition: filter .25s, box-shadow .25s, transform .2s;
  position: relative; overflow: hidden;
}
@media (hover: hover) { .btn:hover { filter: brightness(1.15); } }
.btn-primary { background: var(--primary-grad); color: #fff; }
/* Same treatment as the admin panel's primary buttons: a light sweep
   across the button plus brightness only on hover — no box-shadow, no
   scale. */
.btn-primary::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(120deg, transparent 30%, rgba(255,255,255,.25) 50%, transparent 70%);
  transform: translateX(-100%); transition: transform .5s ease;
}
@media (hover: hover) {
  .btn-primary:hover::after { transform: translateX(100%); }
  .btn-primary:hover { filter: brightness(1.08); }
}
.btn-ghost { background: var(--bg-card); color: var(--text); border: 1px solid var(--border); }
.btn-block { width: 100%; }
.btn-small { padding: 8px 14px; font-size: 12.5px; display: inline-flex; align-items: center; gap: 6px; }
.btn-small svg { width: 17px; height: 17px; }

.avatar-btn {
  width: 40px; height: 40px; border-radius: 50%; border: 1px solid var(--border); cursor: pointer;
  background: none; padding: 0; color: var(--text-dim);
  display: flex; align-items: center; justify-content: center;
  transition: transform .25s;
}
/* Inner circle holds the actual avatar image/icon and clips it — kept separate
   from .avatar-btn itself so the notification dot (positioned on the button,
   see below) doesn't get cut off by this element's rounded clipping. */
.avatar-btn-inner {
  width: 100%; height: 100%; border-radius: 50%; overflow: hidden;
  background: var(--bg-card) center/cover no-repeat;
  display: flex; align-items: center; justify-content: center;
}
.avatar-btn-inner svg { width: 20px; height: 20px; }

.user-dropdown {
  position: absolute; top: 52px; right: 0; width: 210px; max-width: calc(100vw - 24px);
  background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 8px; display: flex; flex-direction: column; gap: 2px; z-index: 40;
  opacity: 0; transform: translateY(-8px) scale(.96); transform-origin: top right;
  transition: opacity .18s ease, transform .18s cubic-bezier(.22,1.4,.36,1);
}
.user-dropdown.dropdown-show { opacity: 1; transform: translateY(0) scale(1); }
.user-dropdown-name { padding: 10px 12px 8px; font-size: 13px; color: var(--text-faint); border-bottom: 1px solid var(--border-soft); margin-bottom: 4px; display: flex; align-items: center; gap: 8px; }
.user-dropdown-name svg { width: 14px; height: 14px; color: #4ade80; flex: 0 0 auto; }
.user-dropdown button {
  background: none; border: none; color: var(--text); text-align: left; font-size: 13.5px;
  padding: 10px 12px; border-radius: var(--radius-sm); cursor: pointer; display: flex; align-items: center; gap: 10px;
  transition: background .22s, transform .22s;
}
@media (hover: hover) { .user-dropdown button:hover { background: var(--bg-card-hover); } }
.user-dropdown button svg { width: 16px; height: 16px; }

/* ---- Floating "..." context menu (song cards / track rows) ---- */
.floating-menu { position: fixed; min-width: 200px; width: max-content; z-index: 300; max-height: 320px; overflow-y: auto; top: 0; left: 0; }
.song-menu-item {
  width: 100%; background: none; border: none; color: var(--text); text-align: left; font-size: 13.5px;
  padding: 10px 12px; border-radius: var(--radius-sm); cursor: pointer; display: flex; align-items: center; gap: 10px;
  transition: background .22s;
}
.song-menu-item:disabled { opacity: .5; cursor: default; }
.song-menu-item svg { width: 16px; height: 16px; flex: 0 0 auto; }
@media (hover: hover) { .song-menu-item:hover:not(:disabled) { background: var(--bg-card-hover); } }
.song-menu-item span:not(.song-menu-arrow):not(.song-menu-check) { flex: 1; }
.song-menu-arrow svg { width: 13px; height: 13px; color: var(--text-faint); }
.song-menu-danger { color: #fca5a5; }
@media (hover: hover) { .song-menu-danger:hover:not(:disabled) { background: rgba(239,68,68,.12); } }
.song-menu-divider { border-top: 1px solid var(--border-soft); margin: 4px 6px; }
.song-menu-playlists { display: flex; flex-direction: column; gap: 1px; max-height: 180px; overflow-y: auto; }
.song-menu-playlist-item { justify-content: space-between; }
.song-menu-playlist-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.song-menu-check { color: var(--primary); flex: 0 0 auto; }
.song-menu-check svg { width: 15px; height: 15px; }
.song-menu-empty { color: var(--text-faint); font-size: 12.5px; padding: 10px 12px; margin: 0; }
.song-menu-back, .song-menu-create { font-weight: 700; }

/* ---- Install app button + dropdown ---- */
.install-app-wrap { position: relative; }
.install-app-btn { color: var(--text-dim); }
@media (hover: hover) { .install-app-btn:hover { color: var(--text); background: var(--bg-card); } }
.install-dropdown {
  top: calc(100% + 10px); right: 0; left: auto; width: 180px; max-width: calc(100vw - 24px);
  transform: translateY(-8px) scale(.97); transform-origin: top right;
}
.install-dropdown.dropdown-show { transform: translateY(0) scale(1); }
.install-dropdown { width: 260px; padding: 6px; }
.install-platform-row {
  display: flex; align-items: center; gap: 10px; padding: 9px 8px; border-radius: var(--radius-sm);
  text-decoration: none; color: inherit; transition: background .22s;
}
@media (hover: hover) { .install-platform-row:hover { background: var(--bg-card-hover); } }
.install-platform-icon {
  width: 32px; height: 32px; border-radius: 8px; background: var(--bg-card-hover);
  display: flex; align-items: center; justify-content: center; flex: 0 0 auto; color: var(--text);
}
.install-platform-icon svg { width: 20px; height: 20px; }
.install-platform-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.install-platform-name { font-size: 13.5px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.install-platform-sub { font-size: 11px; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---- Download button (the only clickable/interactive part of the row —
   the row itself is a plain <div>, so only this small pill fires a
   download). Deliberately compact so it doesn't crowd out the name/sub
   text next to it. Shows the real download progress as a fill sweeping
   left→right (driven from JS via --instbtn-progress), then flips to
   "Open" when the file has fully downloaded. */
.instbtn {
  position: relative;
  flex: 0 0 auto;
  width: 60px;
  height: 28px;
  border: 1.5px solid var(--primary);
  border-radius: 999px;
  background: none;
  padding: 0;
  cursor: pointer;
  overflow: hidden;
  transition: border-color .3s ease;
}
@media (hover: hover) { .instbtn:not(:disabled):hover { filter: brightness(1.15); } }
.instbtn-fill {
  position: absolute; inset: 0; width: var(--instbtn-progress, 0%);
  background: var(--primary);
  transition: width .15s linear;
}
.instbtn-label {
  position: relative; z-index: 1;
  display: flex; align-items: center; justify-content: center; gap: 3px;
  width: 100%; height: 100%;
  font-size: 10.5px; font-weight: 800; letter-spacing: .02em; text-transform: uppercase;
  color: #fff;
}
.instbtn--done { border-color: #4ade80; }
.instbtn--done .instbtn-fill { background: #4ade80; }

/* ---- Same control, permanently disabled ("Soon") — Android build ---- */
.instbtn--disabled { border-color: var(--border); cursor: default; }
.instbtn--disabled .instbtn-label { color: var(--text-faint); }
.instbtn-soon-icon { display: none; width: 10px; height: 10px; flex: 0 0 auto; }
.instbtn-soon-icon svg { width: 100%; height: 100%; }
@media (hover: hover) { .instbtn:hover { filter: brightness(1.08); } }

.instbtn-input:checked ~ .instbtn-circle { animation: instbtnPulse 1s forwards, instbtnCircleOut .2s ease 2.4s forwards; transform: rotate(180deg); }
.instbtn-input:checked ~ .instbtn-circle::before { animation: instbtnFill 2s ease-in-out forwards; }
.instbtn-input:checked ~ .instbtn-circle .instbtn-icon { opacity: 0; visibility: hidden; }
.instbtn-input:checked ~ .instbtn-circle .instbtn-square { opacity: 1; visibility: visible; }
.instbtn-input:checked ~ .instbtn-title:not(.instbtn-title--done) { opacity: 0; visibility: hidden; }
.instbtn-input:checked ~ .instbtn-title--done { animation: instbtnShowDone .4s ease 2.4s forwards; }
.instbtn:has(.instbtn-input:checked) { width: 43px; animation: instbtnGrow .4s ease 2.4s forwards; }
.instbtn:has(.instbtn-input:checked)::before {
  content: ''; position: absolute; inset: 0; margin: auto; width: 6px; height: 6px; border-radius: 50%;
  background: #fff; opacity: 0; visibility: hidden;
  animation: instbtnOrbit 2s ease-in-out .4s forwards;
}

@keyframes instbtnPulse {
  0%   { scale: .95; box-shadow: 0 0 0 0 rgba(255,255,255,.7); }
  70%  { scale: 1;   box-shadow: 0 0 0 11px rgba(255,255,255,0); }
  100% { scale: .95; box-shadow: 0 0 0 0 rgba(255,255,255,0); }
}
@keyframes instbtnFill   { from { height: 0; } to { height: 100%; } }
@keyframes instbtnOrbit  {
  0%   { transform: rotate(-90deg) translate(19px) rotate(0); opacity: 1; visibility: visible; }
  99%  { transform: rotate(270deg) translate(19px) rotate(270deg); opacity: 1; visibility: visible; }
  100% { opacity: 0; visibility: hidden; }
}
@keyframes instbtnGrow       { 100% { width: 113px; border-color: #4ade80; } }
@keyframes instbtnCircleOut  { 100% { opacity: 0; visibility: hidden; } }
@keyframes instbtnShowDone   { 100% { opacity: 1; visibility: visible; right: 42px; } }

/* ---- Same control, permanently disabled ("Soon") — Android build ---- */
.instbtn--disabled {
  border-color: var(--border); cursor: default; opacity: .85;
}
.instbtn--disabled .instbtn-circle { background: var(--bg-card-hover); color: var(--text-faint); }
.instbtn--disabled .instbtn-circle::before { display: none; }
.instbtn--disabled .instbtn-icon { position: static; transform: none; width: 17px; height: 17px; color: var(--text-faint); }
@media (hover: hover) { .instbtn--disabled:hover { filter: none; } }

/* =========================================================
   Modals
   ========================================================= */
.modal-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,.65); backdrop-filter: blur(3px);
  display: flex; align-items: center; justify-content: center; z-index: 490;
  opacity: 0; transition: opacity .26s ease;
  /* `backdrop-filter` recomputes every frame while this element (or its
     animating child below) is mid-transition; without its own paint/
     compositing layer that recompute can briefly read stale layout on
     the first couple of frames, which is what showed up as the whole
     modal doing a tiny "flick" toward one side right as it opened.
     Isolating it onto its own layer up front removes that dependency. */
  isolation: isolate; contain: layout style paint; will-change: opacity;
}
.modal-overlay.show { opacity: 1; }
.modal {
  background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius-lg);
  width: 380px; max-width: 90vw; padding: 32px 28px; position: relative; max-height: 88vh; overflow-y: auto;
  transform: translateY(18px) scale(.95); transform-origin: center center;
  transition: transform .32s cubic-bezier(.22,1.4,.36,1); will-change: transform;
  /* Reserves the scrollbar's width up front instead of only once content
     actually overflows. Some of these modals redraw or refont their
     content a frame or two after opening (a swapped-in webfont, a
     freshly-drawn canvas), which can nudge the card's height across the
     overflow threshold — the scrollbar then popping in or out mid-
     animation shifted the visible content sideways by its width. With
     the gutter always reserved, crossing that threshold no longer moves
     anything. */
  scrollbar-gutter: stable;
}
.modal-overlay.show .modal { transform: translateY(0) scale(1); }
.modal-close {
  position: absolute; top: 15px; left: 15px; background: var(--bg-card); border: none; color: var(--text-dim);
  width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer;
  transition: background .25s, color .25s, transform .25s; z-index: 2;
}
@media (hover: hover) { .modal-close:hover { background: rgba(239,68,68,.12); color: #fca5a5; } }
/* Absolute + transform instead of relying on flex centering: flex
   centering here was still leaving the X a couple pixels low in some
   browsers (SVGs are inline-replaced elements with their own baseline
   metrics that can throw off align-items:center). Positioning the icon
   at the button's exact midpoint sidesteps that entirely — it can't be
   off-center because it's centered on a coordinate, not on content. The
   button (.modal-close) is already `position: absolute`, so it's already
   a containing block for this. */
.modal-close span {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  display: flex; align-items: center; justify-content: center; line-height: 0;
}
.modal-close svg { width: 18px; height: 18px; display: block; }

.queue-modal-close {
  background: var(--bg-card); border: none; color: var(--text-dim);
  transition: background .25s, color .25s, transform .25s;
}
@media (hover: hover) { .queue-modal-close:hover { background: rgba(239,68,68,.12); color: #fca5a5; } }
.queue-modal-close svg { width: 18px; height: 18px; }

/* Full-bleed split, not a floating card: the brand panel carries this
   screen's one bold, characteristic visual (an audio EQ — this is a music
   app, so sound made visible is the honest hero, not a generic glow) and
   the form panel stays flat and quiet. Widens into two columns once
   there's room; collapses to a single column on narrow viewports, same
   as every other modal on the site. */
.auth-modal {
  width: 760px;
  max-width: 92vw;
  padding: 0;
  overflow: hidden;
  background: var(--bg-elevated);
}
/* The base .modal-close sits top-left (matches playlist/backup-email/jam
   modals), but the queue panel's close button reads top-right — put this
   one there too so it's unmistakably the "same" control. */
.auth-modal .modal-close { left: auto; right: 15px; z-index: 2; }
.auth-modal-grid { display: grid; grid-template-columns: 1fr 1fr; }

.auth-modal-brand-panel {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 40px 34px;
  background:
    radial-gradient(circle at 15% 12%, rgba(59,130,246,.30), transparent 52%),
    radial-gradient(circle at 88% 92%, rgba(29,78,216,.32), transparent 55%),
    #070911;
  border-right: 1px solid var(--border-soft);
}
/* Extra top clearance so the close button (top:15/right:15) doesn't sit
   right on top of the tabs below it. */
.auth-modal-form-panel { padding: 56px 36px 40px; display: flex; flex-direction: column; justify-content: center; }

/* Bigger and nudged up (translateY doesn't reserve space, so it lifts
   the wordmark without dragging the eq-bars/tagline up with it — it just
   opens a little more breathing room below). */
.auth-modal-brand { display: flex; align-items: center; gap: 11px; font-family: var(--font-display); font-weight: 700; font-size: 23px; color: var(--text); line-height: 1; transform: translateY(-16px); }
.auth-modal-brand svg { width: 30px; height: 30px; display: block; }
.auth-modal-tagline { margin: 24px 0 0; max-width: 260px; color: var(--text-dim); font-size: 13.5px; line-height: 1.6; }

/* Audio-spectrum bars — ambient/looping, not tied to :hover, and animates
   via height rather than transform so it can't run afoul of the
   "no scale/transform on hover" rule (this isn't a hover effect). */
.eq-bars { display: flex; align-items: flex-end; gap: 4px; height: 84px; margin: 26px 0 0; }
.eq-bars span {
  flex: 1; min-width: 3px; max-width: 6px; height: 14%; border-radius: 3px 3px 0 0; opacity: .9;
  background: linear-gradient(180deg, var(--focus, #7cb0ff) 0%, var(--primary) 55%, var(--primary-dark) 100%);
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}
@keyframes eqBounceA { from { height: 14%; opacity: .5; } to { height: 95%; opacity: 1; } }
@keyframes eqBounceB { from { height: 10%; opacity: .45; } to { height: 65%; opacity: .95; } }
@keyframes eqBounceC { from { height: 16%; opacity: .55; } to { height: 80%; opacity: 1; } }
.eq-bars span:nth-child(1) { animation-name: eqBounceB; animation-duration: 0.97s; animation-delay: -0.42s; }
.eq-bars span:nth-child(2) { animation-name: eqBounceC; animation-duration: 1.14s; animation-delay: -0.79s; }
.eq-bars span:nth-child(3) { animation-name: eqBounceA; animation-duration: 1.31s; animation-delay: -1.16s; }
.eq-bars span:nth-child(4) { animation-name: eqBounceB; animation-duration: 0.88s; animation-delay: -0.23s; }
.eq-bars span:nth-child(5) { animation-name: eqBounceC; animation-duration: 1.05s; animation-delay: -0.6s; }
.eq-bars span:nth-child(6) { animation-name: eqBounceA; animation-duration: 1.22s; animation-delay: -0.97s; }
.eq-bars span:nth-child(7) { animation-name: eqBounceB; animation-duration: 1.39s; animation-delay: -1.34s; }
.eq-bars span:nth-child(8) { animation-name: eqBounceC; animation-duration: 0.96s; animation-delay: -0.41s; }
.eq-bars span:nth-child(9) { animation-name: eqBounceA; animation-duration: 1.13s; animation-delay: -0.78s; }
.eq-bars span:nth-child(10) { animation-name: eqBounceB; animation-duration: 1.3s; animation-delay: -1.15s; }
.eq-bars span:nth-child(11) { animation-name: eqBounceC; animation-duration: 0.87s; animation-delay: -0.22s; }
.eq-bars span:nth-child(12) { animation-name: eqBounceA; animation-duration: 1.04s; animation-delay: -0.59s; }
.eq-bars span:nth-child(13) { animation-name: eqBounceB; animation-duration: 1.21s; animation-delay: -0.96s; }
.eq-bars span:nth-child(14) { animation-name: eqBounceC; animation-duration: 1.38s; animation-delay: -1.33s; }
.eq-bars span:nth-child(15) { animation-name: eqBounceA; animation-duration: 0.95s; animation-delay: -0.4s; }
.eq-bars span:nth-child(16) { animation-name: eqBounceB; animation-duration: 1.12s; animation-delay: -0.77s; }
.eq-bars span:nth-child(17) { animation-name: eqBounceC; animation-duration: 1.29s; animation-delay: -1.14s; }
.eq-bars span:nth-child(18) { animation-name: eqBounceA; animation-duration: 0.86s; animation-delay: -0.21s; }
.eq-bars span:nth-child(19) { animation-name: eqBounceB; animation-duration: 1.03s; animation-delay: -0.58s; }
.eq-bars span:nth-child(20) { animation-name: eqBounceC; animation-duration: 1.2s; animation-delay: -0.95s; }
.eq-bars span:nth-child(21) { animation-name: eqBounceA; animation-duration: 1.37s; animation-delay: -1.32s; }
.eq-bars span:nth-child(22) { animation-name: eqBounceB; animation-duration: 0.94s; animation-delay: -0.39s; }
.eq-bars span:nth-child(23) { animation-name: eqBounceC; animation-duration: 1.11s; animation-delay: -0.76s; }
.eq-bars span:nth-child(24) { animation-name: eqBounceA; animation-duration: 1.28s; animation-delay: -1.13s; }

/* Sliding pill behind the active tab (::before) instead of a background
   swap on the button itself — the swap now animates as motion, not a
   hard color cut. */
.auth-tabs { position: relative; display: flex; background: var(--bg-card); border-radius: var(--radius-sm); padding: 4px; margin-bottom: 22px; }
.auth-tabs::before {
  content: ""; position: absolute; top: 4px; bottom: 4px; left: 4px; width: calc(50% - 4px);
  border-radius: var(--radius-sm); background: var(--primary-grad);
  transition: transform .45s cubic-bezier(.22,1,.36,1);
  z-index: 0;
}
.auth-tabs.auth-tabs--register::before { transform: translateX(100%); }
.auth-tab {
  position: relative; z-index: 1; flex: 1; background: none; border: none; color: var(--text-dim); font-weight: 700; font-size: 13px;
  padding: 9px; border-radius: var(--radius-sm); cursor: pointer; transition: color .35s ease;
}
.auth-tab.active { color: #fff; }

/* Both forms are stacked in the same grid cell (instead of one being
   `hidden`) so the panel never collapses to zero height mid-transition.
   The outgoing form fades + blurs + shrinks out in place, the incoming
   one fades + sharpens + grows in in place — no horizontal movement, so
   fields never drift left/right, they just crossfade centered. */
.auth-form-stack { display: grid; }
.auth-form-stack > .auth-form { grid-area: 1 / 1; }
/* Kept: still used by .settings-pane and other unrelated fade-ins
   elsewhere in this file. */
@keyframes authFormIn {
  from { opacity: 0; transform: translateX(10px); }
  to   { opacity: 1; transform: translateX(0); }
}
.auth-form { display: flex; flex-direction: column; gap: 20px; }
/* The crossfade-hide/show pair below is only meant for the login/register
   pair stacked in .auth-form-stack (one is "the other tab" and must stay
   invisible + non-interactive until picked). It was previously written as
   a bare `.auth-form` rule, which matched EVERY form with this class —
   including the standalone Account-settings form, the create-playlist
   form and the backup-email form. None of those ever get an `.is-active`
   class added anywhere, so they were permanently opacity:0 + hidden +
   unclickable: the exact "fields don't load" bug in Settings → Account.
   Scoping both rules to `.auth-form-stack > .auth-form` keeps the intended
   crossfade for login/register while leaving every other form visible. */
.auth-form-stack > .auth-form {
  opacity: 0; pointer-events: none; visibility: hidden; transform: scale(.97);
  will-change: opacity, transform; backface-visibility: hidden;
  transition: opacity .4s cubic-bezier(.22,1,.36,1), transform .4s cubic-bezier(.22,1,.36,1),
              visibility 0s linear .4s;
}
.auth-form-stack > .auth-form.is-active {
  opacity: 1; pointer-events: auto; visibility: visible; transform: scale(1);
  transition: opacity .4s cubic-bezier(.22,1,.36,1), transform .4s cubic-bezier(.22,1,.36,1);
}
.auth-form label { position: relative; font-size: 12.5px; color: var(--text-dim); display: flex; flex-direction: column; gap: 9px; font-weight: 600; }
.auth-form label .opt { color: var(--text-faint); font-weight: 400; }
/* Underline field: grows on focus rather than the field lifting or
   scaling, so it reads as premium without touching the hover/scale rule. */
.auth-input-wrap { position: relative; display: flex; align-items: center; }
.auth-input-icon {
  position: absolute; left: 1px; top: 50%; transform: translateY(-50%);
  width: 16px; height: 16px; display: flex; color: var(--text-faint); pointer-events: none;
  transition: color .4s ease;
}
.auth-input-icon svg { width: 16px; height: 16px; display: block; }
.auth-form label:focus-within .auth-input-icon { color: var(--primary); }
.auth-form input {
  width: 100%; background: transparent; border: none; border-bottom: 1px solid var(--border); border-radius: 0;
  padding: 8px 1px 10px; color: var(--text); font-size: 15px; outline: none;
  transition: border-color .45s cubic-bezier(.22,1,.36,1);
}
/* Only fields wrapped with an icon (login/register) get the extra
   left padding — other .auth-form fields (playlist name, backup email,
   etc.) are untouched. */
.auth-input-wrap input { padding-left: 24px; }
.auth-form input::placeholder { color: var(--text-faint); }
.auth-form input:focus { border-bottom-color: transparent; }
.auth-form label::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 2px; width: 0;
  background: var(--primary-grad); transition: width .5s cubic-bezier(.22,1,.36,1);
}
.auth-form label:focus-within::after { width: 100%; }
/* Kill the browser's default autofill styling (a jarring yellow/blue
   fill that ignores the app's theme) by matching the field's own
   background instead of letting the UA paint over it, and pushing the
   forced color transition far enough out that it never gets to run. */
.auth-form input:-webkit-autofill,
.auth-form input:-webkit-autofill:hover,
.auth-form input:-webkit-autofill:focus,
.auth-form input:-webkit-autofill:active {
  -webkit-text-fill-color: var(--text);
  -webkit-box-shadow: 0 0 0 1000px transparent inset;
  box-shadow: 0 0 0 1000px transparent inset;
  caret-color: var(--text);
  transition: background-color 9999s ease-in-out 0s, color 9999s ease-in-out 0s;
}
.auth-form input:autofill { background: transparent; color: var(--text); }
.auth-error { color: #fca5a5; font-size: 12.5px; margin: 0; min-height: 14px; }
.auth-success { color: #4ade80; font-size: 12.5px; margin: 0; min-height: 14px; }
.modal-divider { border: none; border-top: 1px solid var(--border-soft); margin: 2px 0; }

/* Visual-only for now — no reset flow behind it yet. */
.auth-forgot-link {
  align-self: flex-end;
  margin-top: -12px;
  color: var(--text-faint);
  font-size: 12.5px;
  cursor: pointer;
  user-select: none;
  transition: color .2s ease;
}
@media (hover: hover) { .auth-forgot-link:hover { color: var(--text-dim); } }

@media (max-width: 640px) {
  .auth-modal { width: 380px; }
  .auth-modal-grid { grid-template-columns: 1fr; }
  .auth-modal-brand-panel { padding: 26px 26px 18px; border-right: none; border-bottom: 1px solid var(--border-soft); }
  .auth-modal-tagline { display: none; }
  .eq-bars { height: 46px; margin-top: 16px; }
  .auth-modal-form-panel { padding: 28px 26px 32px; }
}

/* ---- Output device permission modal — fully custom, shown BEFORE the
   browser's own native permission prompt so the site gets to explain
   itself in its own voice instead of a bare OS dialog appearing cold. ---- */
.output-perm-modal { text-align: center; padding-top: 30px; }
.output-perm-icon {
  width: 56px; height: 56px; border-radius: 50%; margin: 0 auto 18px;
  background: rgba(59, 130, 246, .14); color: var(--primary);
  display: flex; align-items: center; justify-content: center;
}
.output-perm-icon svg { width: 26px; height: 26px; }
.output-perm-modal h2 { font-size: 17px; margin: 0 0 12px; }
.output-perm-modal p { margin: 0 0 12px; font-size: 13px; color: var(--text-dim); line-height: 1.6; }
.output-perm-subnote { font-size: 11.5px !important; color: var(--text-faint) !important; }
.output-perm-actions { justify-content: center; gap: 10px; margin-top: 20px; }
.output-perm-actions .btn { flex: 1; }

.profile-modal h2 { text-align: center; margin-bottom: 22px; font-size: 20px; }
.profile-avatar-picker { display: flex; flex-direction: column; align-items: center; gap: 12px; margin-bottom: 26px; }
.profile-avatar-preview {
  width: 96px; height: 96px; border-radius: 50%; border: 1px solid var(--border); background: var(--bg-card) center/cover no-repeat;
  display: flex; align-items: center; justify-content: center; color: var(--text-faint);
}
.profile-avatar-preview svg { width: 36px; height: 36px; }
.profile-actions { display: flex; justify-content: space-between; align-items: center; }

/* =========================================================
   Settings view (Account / Privacy / Friends) — a regular page
   like the other sidebar destinations, with a vertical tab rail
   on the left (more room per tab, matches the app's own sidebar)
   and the tab content to the right.
   ========================================================= */
.settings-view { max-width: 880px; }
/* Phone-only control; the ≤760px block gives it its real styling. */
.settings-pane-back { display: none; }

.settings-layout { display: flex; gap: 28px; align-items: flex-start; }

.settings-tabs {
  display: flex; flex-direction: column; flex: 0 0 190px; background: var(--bg-card);
  border-radius: var(--radius-md); padding: 6px; gap: 2px;
  transition: background .4s cubic-bezier(.22,1,.36,1);
}
.settings-tab {
  background: none; border: none; color: var(--text-dim); font-weight: 700; font-size: 13.5px;
  padding: 8px 14px; border-radius: var(--radius-sm); cursor: pointer; display: flex; align-items: center; justify-content: flex-start; gap: 8px;
  transition: background .28s ease, color .28s ease; position: relative; text-align: left; width: 100%;
}
@media (hover: hover) { .settings-tab:hover { color: var(--text); background: var(--bg-card-hover); } }
.settings-tab.active { background: var(--primary-grad); color: #fff; }
@media (hover: hover) { .settings-tab.active:hover { background: var(--primary-grad); } }
.settings-tab-icon { display: flex; flex: 0 0 auto; }
.settings-tab-icon svg { width: 18px; height: 18px; display: block; }
.settings-tab-badge {
  background: #ef4444; color: #fff; font-size: 10px; font-weight: 800; border-radius: 999px;
  padding: 1px 6px; line-height: 1.5; min-width: 15px; text-align: center; margin-left: auto;
}

.settings-body { flex: 1; min-width: 0; }
.settings-pane { display: flex; flex-direction: column; gap: 18px; animation: authFormIn .25s cubic-bezier(.22,1,.36,1); }

/* ---- Account tab: photo on the left, fields on the right ---- */
.settings-account-grid { display: flex; gap: 32px; align-items: flex-start; }
.settings-account-grid .profile-avatar-picker { flex: 0 0 140px; margin-bottom: 0; }
.settings-account-grid .auth-form { flex: 1; min-width: 0; }

/* ---- Backup email option (Account tab) + its modal ---- */
.backup-email-option {
  display: flex; align-items: center; gap: 16px;
  padding: 16px; margin-top: 24px;
  background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-md);
}
.backup-email-option-icon {
  flex: 0 0 auto; width: 38px; height: 38px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center; color: #fff;
  background: var(--primary-grad);
}
.backup-email-option-icon svg { width: 18px; height: 18px; }
.backup-email-option .privacy-option-text { flex: 1; min-width: 0; }
.backup-email-option .privacy-option-text strong { display: flex; align-items: center; gap: 8px; }
.backup-email-set-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  margin-left: 10px;
  vertical-align: middle;
  font-size: 10.5px; font-weight: 700; color: #4ade80; background: rgba(74,222,128,.14);
  border-radius: 999px; padding: 2px 8px; letter-spacing: .01em;
}
.backup-email-set-pill svg {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
}

.backup-email-modal { text-align: center; padding-top: 36px; }
.backup-email-modal .auth-form { text-align: left; margin-top: 24px; }
.backup-email-modal-badge {
  position: relative;
  width: 64px; height: 64px; margin: 0 auto 18px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center; color: #fff;
  background: var(--primary-grad);
  box-shadow: 0 8px 26px -8px rgba(59,130,246,.65);
  animation: backupEmailBadgeIn .5s cubic-bezier(.22,1.4,.36,1);
}
.backup-email-modal-badge svg { width: 26px; height: 26px; }
.backup-email-modal-badge::before,
.backup-email-modal-badge::after {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  border: 1.5px solid var(--primary); opacity: 0;
  animation: backupEmailRing 2.6s ease-out infinite;
}
.backup-email-modal-badge::after { animation-delay: 1.3s; }
/* Was `scale(.6) rotate(-12deg) -> scale(1) rotate(0)`. The rotation
   read fine on the badge's own circular background, but the mail icon
   inside it is not symmetric, so for the first couple of frames it
   visibly tilted and drifted before straightening out — that's what
   showed up as the modal "shaking" on open. Scale-only keeps the same
   pop-in energy without the icon appearing to swing sideways. */
@keyframes backupEmailBadgeIn {
  from { opacity: 0; transform: scale(.6); }
  to   { opacity: 1; transform: scale(1); }
}
@keyframes backupEmailRing {
  0%   { transform: scale(1);    opacity: .55; }
  100% { transform: scale(1.55); opacity: 0; }
}
.backup-email-modal-title { font-family: var(--font-display); font-size: 19px; font-weight: 700; margin: 0 0 8px; }
.backup-email-modal-sub { font-size: 12.5px; color: var(--text-dim); line-height: 1.6; margin: 0 auto; max-width: 300px; }

/* ---- Privacy tab ---- */
.privacy-option {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 16px; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-md);
}
.privacy-option-text strong { font-size: 13.5px; display: block; margin-bottom: 5px; color: var(--text); }
.privacy-option-text p { margin: 0; font-size: 12px; color: var(--text-dim); line-height: 1.55; }

/* ---- Playback tab (crossfade / normalize / remember volume) ---- */
.playback-option-text strong { font-size: 13.5px; display: block; margin-bottom: 5px; color: var(--text); }
.playback-option-text p { margin: 0; font-size: 12px; color: var(--text-dim); line-height: 1.55; }
.settings-slider-row { display: flex; align-items: center; gap: 14px; margin: 12px 0 24px; }
.settings-slider-row input[type="range"] { flex: 1; }
.settings-slider-value { font-size: 12.5px; font-weight: 700; color: var(--text-dim); min-width: 34px; text-align: right; }

/* ---- Output device picker — fully custom, no native <select>. Each row
   is its own button so it can carry an icon (speaker vs. headphones,
   guessed from the device's label) and a trailing checkmark that only
   the active device shows. ---- */
.output-device-list { display: flex; flex-direction: column; gap: 8px; margin: 12px 0 26px; }
.output-device-item {
  display: flex; align-items: center; gap: 12px; width: 100%; text-align: left;
  background: var(--bg-card); border: 1px solid var(--border); color: var(--text);
  border-radius: var(--radius-sm); padding: 12px 14px; cursor: pointer;
  font-family: inherit; transition: background .2s, border-color .2s;
}
@media (hover: hover) { .output-device-item:hover { background: var(--bg-card-hover); border-color: #2f3648; } }
.output-device-item.active { border-color: var(--primary); background: rgba(59, 130, 246, .10); }
.output-device-icon {
  flex: 0 0 auto; width: 34px; height: 34px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg-card-hover); color: var(--text-dim); transition: background .2s, color .2s;
}
.output-device-item.active .output-device-icon { background: rgba(59, 130, 246, .18); color: var(--primary); }
.output-device-icon svg { width: 17px; height: 17px; display: block; }
.output-device-name { flex: 1; min-width: 0; font-size: 13.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.output-device-name small { display: block; font-size: 11.5px; font-weight: 400; color: var(--text-faint); white-space: normal; margin-top: 2px; }
/* The blue circle-check is always in the markup (every row, selected or
   not) — visibility is pure CSS, driven off .active. Keeping the element
   itself always present (rather than adding/removing it from the DOM on
   selection) is what makes the pop-in/out animate instead of just
   appearing; it also means selecting a device never has to touch
   anything but this class + the two rules below it. */
.output-device-check {
  flex: 0 0 auto; width: 20px; height: 20px; color: var(--primary); display: flex;
  opacity: 0; transform: scale(.6);
  transition: opacity .18s ease, transform .18s cubic-bezier(.34,1.56,.64,1);
}
.output-device-check svg { width: 20px; height: 20px; display: block; }
.output-device-item.active .output-device-check { opacity: 1; transform: scale(1); }
.output-device-item.output-device-ask { border-style: dashed; color: var(--text-dim); }
.output-device-item.output-device-ask .output-device-icon { background: none; border: 1.5px dashed var(--border); }
.output-device-note { margin-top: -14px; }

/* ---- Notifications tab ---- */
.notif-sub-options {
  display: flex; flex-direction: column; gap: 2px;
  max-height: 0; overflow: hidden; opacity: 0;
  transition: max-height .4s cubic-bezier(.22,1,.36,1), opacity .3s ease, margin-top .4s ease;
}
.notif-sub-options.open { max-height: 640px; opacity: 1; margin-top: 6px; }
.settings-note { font-size: 12px; color: #fca5a5; margin: 14px 0 0; line-height: 1.55; }

.switch { position: relative; display: inline-block; width: 44px; height: 26px; flex: 0 0 auto; cursor: pointer; }
.switch input { opacity: 0; width: 0; height: 0; position: absolute; }
.switch-track {
  position: absolute; inset: 0; background: var(--bg-elevated); border: 1px solid var(--border);
  border-radius: 999px; transition: background .25s, border-color .25s;
}
.switch-thumb {
  position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 50%;
  background: var(--text-faint); transition: transform .25s cubic-bezier(.22,1.4,.36,1), background .25s;
}
.switch input:checked + .switch-track { background: var(--primary-grad); border-color: transparent; }
.switch input:checked + .switch-track .switch-thumb { transform: translateX(18px); background: #fff; }

/* ---- Friends tab ---- */
.friend-search { position: relative; flex: 0 0 auto; }
.friend-search input {
  width: 100%; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 11px 14px; color: var(--text); font-size: 13.5px; outline: none; transition: border-color .25s;
}
.friend-search input:focus { border-color: var(--primary); }
.friend-search-results {
  position: absolute; top: calc(100% + 8px); left: 0; right: 0; background: var(--bg-elevated); border: 1px solid var(--border);
  border-radius: var(--radius-sm); max-height: 240px; overflow-y: auto; z-index: 20; box-shadow: 0 14px 34px rgba(0,0,0,.45);
  padding: 4px;
}
.friend-result-row, .friend-row { display: flex; align-items: center; gap: 10px; padding: 8px; border-radius: var(--radius-sm); }
@media (hover: hover) { .friend-result-row:hover { background: var(--bg-card-hover); } }
.friend-avatar {
  width: 34px; height: 34px; border-radius: 50%; background: var(--bg-card) center/cover no-repeat;
  display: flex; align-items: center; justify-content: center; color: var(--text-faint); flex: 0 0 auto; overflow: hidden;
}
.friend-avatar svg { width: 16px; height: 16px; }
.friend-name-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.friend-name-btn {
  background: none; border: none; padding: 0; text-align: left; color: var(--text); font-size: 13.5px; font-weight: 700;
  cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: var(--font-body);
}
@media (hover: hover) { .friend-name-btn:hover { text-decoration: underline; } }
.friend-activity {
  font-size: 11.5px; color: var(--text-faint); display: flex; align-items: center; gap: 5px; min-width: 0;
}
.friend-activity.is-live { color: #4ade80; }
.friend-activity .eq-dot { width: 6px; height: 6px; border-radius: 50%; background: #4ade80; flex: 0 0 auto; animation: pulseDot 1.2s ease-in-out infinite; }
.friend-activity span:last-child { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
@keyframes pulseDot { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }

.friend-action-btn {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: 50%; border: none; background: var(--bg-elevated);
  color: var(--text-dim); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background .2s, color .2s;
}
.friend-action-btn svg { width: 18px; height: 18px; }
@media (hover: hover) { .friend-action-btn:hover { background: var(--bg-card-hover); color: var(--text); } }
@media (hover: hover) { .friend-action-btn.add:hover { background: var(--bg-elevated); color: #fff; } }
@media (hover: hover) { .friend-action-btn.cancel:hover { background: var(--bg-elevated); color: #fff; } }
@media (hover: hover) { .friend-action-btn.danger:hover { background: rgba(239,68,68,.15); color: #fca5a5; } }
@media (hover: hover) { .friend-action-btn.accept:hover { background: rgba(74,222,128,.15); color: #4ade80; } }
.friend-action-btn:disabled { opacity: .55; cursor: default; }

.friends-scroll { max-height: 55vh; overflow-y: auto; padding-right: 4px; margin-top: 4px; }
.friends-section { margin-bottom: 18px; }
.friends-section:last-child { margin-bottom: 0; }
.friends-section h4 {
  font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-faint);
  margin: 0 0 8px; display: flex; align-items: center; gap: 6px;
}
.friend-request-row { justify-content: space-between; }
.friend-request-actions { display: flex; gap: 6px; flex: 0 0 auto; }
.empty-state-small { color: var(--text-faint); font-size: 12.5px; text-align: center; padding: 20px 10px; margin: 0; }

/* ---- Active Sessions tab ---- */
.sessions-list { display: flex; flex-direction: column; gap: 4px; margin-top: 4px; }
.session-row {
  display: flex; align-items: center; gap: 12px; padding: 12px 10px; border-radius: var(--radius-sm);
  border: 1px solid transparent; transition: background .2s;
}
@media (hover: hover) { .session-row:hover { background: var(--bg-card-hover); } }
.session-row.is-current { border-color: var(--border); background: var(--bg-card); }
.session-icon {
  width: 38px; height: 38px; border-radius: 50%; background: var(--bg-elevated); color: var(--text-dim);
  display: flex; align-items: center; justify-content: center; flex: 0 0 auto;
}
.session-icon svg { width: 19px; height: 19px; }
.session-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.session-title {
  font-size: 13.5px; font-weight: 700; color: var(--text); display: flex; align-items: center; gap: 8px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.session-current-pill {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  font-size: 10px; font-weight: 700; color: #4ade80; background: rgba(74,222,128,.14);
  border-radius: 999px; padding: 2px 8px; letter-spacing: .01em; text-transform: uppercase;
}
.session-sub {
  font-size: 11.5px; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.session-revoke-btn { flex: 0 0 auto; }
.sessions-footer { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); display: flex; justify-content: center; }


/* ---- Notification badges (navbar avatar + dropdown) ---- */
.avatar-btn { position: relative; }
.avatar-btn.has-notification::after {
  content: ''; position: absolute; top: -1px; right: -1px; width: 11px; height: 11px; border-radius: 50%;
  background: #ef4444; border: 2px solid var(--bg); box-sizing: border-box;
}
.dropdown-badge {
  background: #ef4444; color: #fff; font-size: 10px; font-weight: 800; border-radius: 999px;
  padding: 1px 6px; margin-left: auto;
}

/* =========================================================
   User profile view
   ========================================================= */
.profile-hero-avatar { border-radius: 50%; }
.profile-hero-avatar svg { width: 40%; height: 40%; }
.profile-hero-activity { display: flex; align-items: center; gap: 8px; margin: 10px 0 2px; font-size: 13.5px; color: #4ade80; min-width: 0; max-width: 100%; }
.profile-hero-activity .eq-dot { width: 7px; height: 7px; border-radius: 50%; background: #4ade80; animation: pulseDot 1.2s ease-in-out infinite; flex: 0 0 auto; }
.profile-hero-activity span:last-child { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
.profile-hero-actions { display: flex; gap: 10px; margin-top: 5px; }
/* Sits on the "Listening to …" line itself rather than under it: the
   button only means anything next to the track it plays. */
.profile-listen-along-btn { flex: 0 0 auto; margin-left: 4px; padding: 5px 11px; font-size: 12px; }
.profile-listen-along-btn svg { width: 14px; height: 14px; }
@media (max-width: 600px) {
  .profile-hero-activity { flex-wrap: wrap; }
  .profile-listen-along-btn { margin-left: 0; }
}
.playlist-hero-owner-link { display: flex; align-items: center; gap: 8px; background: none; border: none; padding: 0; cursor: pointer; font-family: var(--font-body); }
@media (hover: hover) { .playlist-hero-owner-link:hover span:last-child b { text-decoration: underline; } }

/* ---- Create / Edit Playlist modal ---- */
.playlist-modal h2 { text-align: center; margin-bottom: 22px; font-size: 20px; }
.playlist-cover-picker { gap: 14px; }
.playlist-cover-preview {
  width: 120px; height: 120px; border-radius: var(--radius-md); border: 1px solid var(--border);
  background: var(--bg-card) center/cover no-repeat; display: flex; align-items: center; justify-content: center;
  color: var(--text-faint); position: relative; overflow: hidden;
}
.playlist-cover-preview.placeholder { background: linear-gradient(135deg, #1a2030, #10131a); }
.playlist-cover-preview svg { width: 34px; height: 34px; opacity: .6; }
.playlist-cover-picker-actions { display: flex; align-items: center; gap: 8px; }
.playlist-cover-picker-actions label svg { width: 16px; height: 16px; }

/* ---- Generic confirm modal ---- */
.confirm-modal { width: 340px; }
.confirm-modal h2 { font-size: 18px; margin-bottom: 10px; }
.confirm-modal p { color: var(--text-dim); font-size: 13.5px; line-height: 1.6; margin: 0 0 22px; }
.confirm-modal-danger { background: linear-gradient(135deg, #ef4444, #b91c1c); }

/* =========================================================
   Toast
   ========================================================= */
.toast {
  position: fixed; bottom: 100px; left: 50%; transform: translateX(-50%) translateY(10px);
  background: var(--bg-card); border: 1px solid var(--border); color: var(--text);
  padding: 12px 20px; border-radius: 999px; font-size: 13px; font-weight: 600; z-index: 530;
  opacity: 0; transition: opacity .22s ease, transform .22s ease;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast-error { border-color: rgba(239, 68, 68, .5); color: #fecaca; }
.toast-success { border-color: rgba(74, 222, 128, .45); color: #d6f5df; }

/* =========================================================
   Queue panel — slide-in from the right
   ========================================================= */
.queue-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,.45); z-index: 480;
  opacity: 0; transition: opacity .28s ease;
}
.queue-overlay.show { opacity: 1; }
.queue-panel {
  position: fixed; top: var(--gap); right: var(--gap); bottom: calc(76px + var(--gap) * 2);
  width: 340px; max-width: calc(100vw - 24px); background: var(--bg-elevated); border: 1px solid var(--border);
  border-radius: var(--radius-md); z-index: 481; display: flex; flex-direction: column; overflow: hidden;
  transform: translateX(calc(100% + 20px)); transition: transform .32s cubic-bezier(.22,1.4,.36,1);
  box-shadow: 0 24px 60px rgba(0,0,0,.4);
}
.queue-panel.show { transform: translateX(0); }
.queue-panel-head { display: flex; align-items: center; justify-content: space-between; padding: 16px 16px 12px; border-bottom: 1px solid var(--border-soft); flex: 0 0 auto; }
.queue-panel-head h3 { font-size: 16px; }
.queue-panel-body { overflow-y: auto; padding: 8px 10px 16px; flex: 1 1 auto; }
.queue-section { margin-bottom: 14px; }
.queue-section-title { font-size: 11px; text-transform: uppercase; letter-spacing: .07em; color: var(--text-faint); font-weight: 700; padding: 8px 8px 6px; }
.queue-row { display: flex; align-items: center; gap: 10px; padding: 6px 8px; border-radius: var(--radius-sm); transition: background .2s; }
@media (hover: hover) { .queue-row:hover { background: var(--bg-card); } }
.queue-row-now { background: rgba(59,130,246,.08); }
.queue-row .track-cover { width: 38px; height: 38px; }
.queue-row .track-text { min-width: 0; flex: 1; }
.queue-row-remove { width: 26px; height: 26px; color: var(--text-faint); flex: 0 0 auto; }
.queue-row-remove svg { width: 13px; height: 13px; }
@media (hover: hover) { .queue-row-remove:hover { color: #fca5a5; background: rgba(239,68,68,.12); } }

/* =========================================================
   Update banner
   ========================================================= */
.update-banner {
  flex: 0 0 auto; height: 46px; background: var(--bg-elevated); border: 1px solid rgba(59,130,246,.35);
  border-radius: var(--radius-md); display: flex; align-items: center; gap: 12px; padding: 0 16px;
  opacity: 0; transform: translateY(-14px); transition: opacity .3s cubic-bezier(.22,1,.36,1), transform .3s cubic-bezier(.22,1,.36,1);
  box-shadow: 0 8px 30px rgba(59,130,246,.12);
}
.update-banner.show { opacity: 1; transform: translateY(0); }
.update-banner-dot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--primary); flex: 0 0 auto;
  animation: updateDotPulse 1.6s ease-in-out infinite;
}
.update-banner-text { font-size: 13px; font-weight: 600; color: var(--text); flex: 1; }
.update-banner-close { width: 26px; height: 26px; color: var(--text-faint); flex: 0 0 auto; }
.update-banner-close svg { width: 14px; height: 14px; }
@keyframes updateDotPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(59,130,246,.55); }
  50% { box-shadow: 0 0 0 6px rgba(59,130,246,0); }
}

/* Pulses a shared song's card into view (see openSharedSong in app.js) */
.shared-highlight { animation: sharedHighlight 1.6s ease; }
@keyframes sharedHighlight {
  0%, 100% { box-shadow: none; }
  15%, 55% { box-shadow: 0 0 0 3px var(--primary); }
}

/* =========================================================
   Tooltips — a single JS-positioned bubble (assets/js/app.js), appended
   directly to <body> and placed with position:fixed. This deliberately
   avoids the old ::after-on-the-element approach: that version was still
   clipped by any scrolling ancestor (e.g. .content's overflow-y:auto)
   whenever a button sat near that panel's own edge, even when there was
   plenty of room in the actual browser viewport. A fixed, body-level
   element has no such ancestor to be clipped by.
   ========================================================= */
.js-tooltip {
  /* Above the achievement-card fullscreen viewer (z-index 600) too — a
     tooltip on the viewer's own close button was rendering underneath
     the viewer's dark backdrop instead of on top of it. */
  position: fixed; z-index: 610;
  background: #1c2028; border: 1px solid var(--border); color: var(--text);
  font-size: 11px; font-weight: 600; letter-spacing: .01em; padding: 6px 10px; border-radius: 7px;
  white-space: nowrap; pointer-events: none;
  opacity: 0; transform: translateY(4px); transition: opacity .15s ease, transform .15s ease;
}
.js-tooltip.tt-flip-below { transform: translateY(-4px); }
.js-tooltip.show { opacity: 1; transform: translateY(0); }
@media (hover: none) {
  .js-tooltip { display: none; } /* tooltips add little value on touch devices */
  .navbar-search-kbd { display: none; } /* no physical "/" key to hint at on touch */
}

/* =========================================================
   Touch devices — controls that only ever appeared on :hover
   (song-card play/like/menu, track-row like/menu, drag handle)
   are invisible and unreachable with no mouse. Show them
   permanently on touch/coarse-pointer devices, and bump the
   smallest tap targets up to a comfortable size.
   ========================================================= */
@media (hover: none), (pointer: coarse) {
  .song-card-play,
  .song-card-like,
  .song-card-menu,
  .track-like-btn,
  .track-menu-btn,
  .track-drag-handle { opacity: 1; }
  .icon-btn { min-width: 38px; min-height: 38px; }
  /* These are not .icon-btn, so the rule above never reached them and
     they stayed 30x30 / 16x16 on touch — well under the 44px target
     size WCAG 2.5.8 asks for, and genuinely fiddly to hit. */
  .song-card-like,
  .song-card-menu { width: 36px; height: 36px; }
  .track-like-btn,
  .track-menu-btn {
    min-width: 38px; min-height: 38px;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .notif-center-action { min-height: 38px; }
  .mobile-nav-btn { min-height: 48px; }
  .player-controls .icon-btn,
  .now-playing-controls .icon-btn { min-width: 40px; min-height: 40px; }
  /* the seek/volume thumb only ever showed on :hover, which never fires
     on touch — with no drag handle visible, there was no indication a
     phone user could even scrub the track at all */
  input[type="range"]::-webkit-slider-thumb { opacity: 1; }
  input[type="range"]::-moz-range-thumb { opacity: 1; }
}

/* =========================================================
   Entrance / feedback animations
   ========================================================= */
.view { animation: none; }
.view.animate-in { animation: viewFadeIn .32s cubic-bezier(.22,1,.36,1); }

@keyframes viewFadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes cardIn {
  from { opacity: 0; transform: translateY(14px) scale(.97); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes rowIn {
  from { opacity: 0; transform: translateX(-8px); }
  to   { opacity: 1; transform: translateX(0); }
}
@keyframes eqBounce {
  0%, 100% { height: 25%; }
  50% { height: 70%; }
}
@keyframes countBump {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.35); color: var(--primary); }
  100% { transform: scale(1); }
}
@keyframes coverPop {
  0%   { opacity: 0; transform: translateY(3px); }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes heartPop {
  0%   { transform: scale(1) rotate(0deg); }
  30%  { transform: scale(.7) rotate(-8deg); }
  55%  { transform: scale(1.35) rotate(6deg); }
  75%  { transform: scale(.95) rotate(-2deg); }
  100% { transform: scale(1) rotate(0deg); }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
}

svg { vertical-align: middle; }

/* =========================================================
   Responsive
   Breakpoints: 1080 (small laptop) → 900 (tablet landscape) →
   760 (tablet portrait) → 600 (large phone) → 480 (phone) →
   380 (small phone)
   ========================================================= */

/* =========================================================
   Mobile bottom navigation + library sheet

   Hidden by default; the ≤760px block below turns them on. See the
   comment on .mobile-nav in index.html for why the sidebar is dropped
   entirely at that width rather than squeezed further.
   ========================================================= */
.mobile-nav {
  display: none;
  /* Own stable compositing layer, isolated from the .content pane's
     scroll/mask repaints above it — this (not the vh/dvh/svh unit, which
     earlier fixes focused on) is what was actually making the icons and
     labels here shift by a subpixel while scrolling. See the matching
     note on .content. */
  transform: translateZ(0);
  -webkit-transform: translateZ(0);
}
.mobile-nav-btn {
  flex: 1 1 0; min-width: 0; position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  background: none; border: none; cursor: pointer; padding: 8px 4px;
  color: var(--text-faint); font-family: inherit; font-size: 10.5px; font-weight: 700;
  border-radius: var(--radius-sm); transition: color .2s;
  -webkit-tap-highlight-color: transparent;
  /* Keeps each button's own layout/paint from ever being recalculated
     just because something elsewhere on the page reflows or repaints. */
  contain: layout style paint;
}
.mobile-nav-btn .mobile-nav-icon { display: flex; }
.mobile-nav-btn svg { width: 22px; height: 22px; display: block; }
.mobile-nav-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.mobile-nav-btn.active { color: var(--primary); }
.mobile-nav-btn:active { transform: scale(.94); }

.sheet-overlay {
  position: fixed; inset: 0; z-index: 430; background: rgba(0, 0, 0, .55);
  opacity: 0; transition: opacity .26s ease;
}
.sheet-overlay.show { opacity: 1; }
.mobile-sheet {
  position: fixed; z-index: 440; left: 0; right: 0; bottom: 0;
  max-height: min(78dvh, 640px);
  display: flex; flex-direction: column;
  background: var(--bg-elevated);
  border-top: 1px solid var(--border);
  border-radius: 18px 18px 0 0;
  box-shadow: 0 -18px 50px rgba(0, 0, 0, .6);
  padding-bottom: env(safe-area-inset-bottom);
  transform: translateY(100%);
  transition: transform .3s cubic-bezier(.22,1,.36,1);
}
.mobile-sheet.show { transform: translateY(0); }
.mobile-sheet-grabber {
  display: block; width: 42px; height: 4px; border-radius: 999px; border: none; padding: 0;
  background: #39404f; margin: 10px auto 4px; cursor: pointer; flex: 0 0 auto;
}
.mobile-sheet-head {
  display: flex; align-items: center; justify-content: space-between;
  padding: 6px 16px 10px; flex: 0 0 auto;
}
.mobile-sheet-head h3 { font-size: 16px; }
.mobile-sheet-head .icon-btn { color: var(--text-dim); }
.mobile-sheet-body {
  flex: 1 1 auto; overflow-y: auto; -webkit-overflow-scrolling: touch;
  padding: 0 8px 16px;
}
.library-sheet-row {
  width: 100%; display: flex; align-items: center; gap: 12px; text-align: left;
  background: none; border: none; cursor: pointer; color: var(--text);
  padding: 10px; border-radius: var(--radius-sm); font-family: inherit;
  transition: background .2s;
}
.library-sheet-row:active { background: var(--bg-card); }
@media (hover: hover) { .library-sheet-row:hover { background: var(--bg-card); } }
.library-sheet-row.active { background: rgba(59, 130, 246, .10); }
.library-sheet-art {
  width: 46px; height: 46px; border-radius: 7px; flex: 0 0 auto; overflow: hidden;
  background: var(--bg-card) center/cover no-repeat;
  display: flex; align-items: center; justify-content: center; color: var(--text-faint);
}
.library-sheet-art > span { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
.library-sheet-art svg { width: 20px; height: 20px; }
.library-sheet-art.is-liked { background: var(--primary-grad); color: #fff; }
.library-sheet-art.is-topsongs { background: url('../icons/top-songs-cover.webp') center/cover no-repeat; }
.library-sheet-art.is-weekly { background: url('../icons/weekly-mix-cover.webp') center/cover no-repeat; }
.library-sheet-playlist-cover {
  position: relative; width: 100%; height: 100%;
  background: var(--bg-card) center/cover no-repeat;
  display: flex; align-items: center; justify-content: center; color: var(--text-faint);
}
.library-sheet-playlist-cover svg { width: 40%; height: 40%; opacity: .6; }
.library-sheet-text { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.library-sheet-text strong { font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.library-sheet-text small { font-size: 12px; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.library-sheet-divider {
  font-size: 11px; text-transform: uppercase; letter-spacing: .08em; font-weight: 700;
  color: var(--text-faint); padding: 14px 10px 6px;
}
.library-sheet-note { font-size: 12.5px; color: var(--text-faint); padding: 14px 10px; margin: 0; line-height: 1.55; }
body.sheet-open { overflow: hidden; }

/* While a finger is dragging a sheet the transform is written inline on
   every frame, so the transition has to be out of the way — it is
   restored the moment the finger lifts, which is what gives the spring
   back or the rest of the way down. */
.mobile-sheet.is-dragging,
.queue-panel.is-dragging,
.notif-center.is-dragging { transition: none; }

/* Queue and Jam mirrored into the full-screen player. Only needed once
   .player-extra stops being on screen (≤900px), so they are hidden on
   wider viewports where the real controls are still in the player bar. */
.now-playing-only-mobile { display: none; }

/* =========================================================
   Responsive

   Breakpoints: 1180 (small laptop) → 1024 (tablet landscape) →
   760 (phone / tablet portrait — the sidebar-to-bottom-nav switch) →
   600 (large phone) → 420 (phone) → 360 (small phone)
   ========================================================= */

/* ---------------- ≤1180px — small laptop ---------------- */
@media (max-width: 1180px) {
  .song-grid { grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: 14px; }
  .track-row { grid-template-columns: 32px 1fr minmax(0, 200px) 84px; }
  #volumeBar { max-width: 120px; }
}

/* ---------------- ≤1024px — tablet landscape ---------------- */
@media (max-width: 1024px) {
  .sidebar { width: 84px; }
  .sidebar-liked-text, .sidebar-library-title, .sidebar-link-label, .sidebar-playlist-text, .sidebar-playlists-empty { display: none; }
  .sidebar-link { justify-content: center; }
  .sidebar-link::before { left: -12px; }
  .sidebar-liked { justify-content: center; padding: 10px; }
  .sidebar-library-title-row { justify-content: center; padding: 0 0 6px; }
  .sidebar-playlist-link { justify-content: center; padding: 8px; }
  /* The icon-only rail has no room for grid tiles — always fall back to a
     plain centered icon list here regardless of the chosen "View as" mode. */
  .sidebar-library.view-compact-grid,
  .sidebar-library.view-default-grid { display: flex; flex-direction: column; gap: 6px; }
  .sidebar-library.view-compact-grid .sidebar-playlists,
  .sidebar-library.view-default-grid .sidebar-playlists { display: flex; flex-direction: column; max-height: 260px; overflow-y: auto; }
  .sidebar-library.view-compact-grid .sidebar-link,
  .sidebar-library.view-default-grid .sidebar-link,
  .sidebar-library.view-compact-list .sidebar-link {
    flex-direction: row; justify-content: center; padding: 8px; text-align: left;
  }
  .sidebar-library.view-compact-grid .sidebar-liked-icon,
  .sidebar-library.view-compact-grid .sidebar-topsongs-icon,
  .sidebar-library.view-compact-grid .sidebar-playlist-cover,
  .sidebar-library.view-default-grid .sidebar-liked-icon,
  .sidebar-library.view-default-grid .sidebar-topsongs-icon,
  .sidebar-library.view-default-grid .sidebar-playlist-cover { width: 30px; height: 30px; aspect-ratio: auto; }
  .navbar-search { max-width: 260px; }
  .player-bar { grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr) minmax(0, 1fr); gap: 10px; padding: 0 12px; }
  /* The volume slider and the Mini Player go first: the slider needs real
     width to be usable and the full one is in Now Playing anyway, and
     Mini Player is a Picture-in-Picture feature with no touch equivalent.
     Mute, 8D, Jam and Queue all stay — they have no other home at this
     width. */
  #volumeBar, #miniPlayerBtn { display: none; }
  .player-meta { max-width: none; }
  .player-title, .player-artist { max-width: 100%; }
  .content { padding: 22px 20px 34px; }
  .track-row { grid-template-columns: 32px 1fr minmax(0, 140px) 74px; gap: 12px; }
}

/* ---------------- ≤900px — narrow tablet ---------------- */
@media (max-width: 900px) {
  .player-bar { grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) minmax(0, auto); gap: 8px; }
  .player-extra { gap: 2px; }
  .navbar-search { max-width: 200px; }
}

/* ---------------- ≤760px — phone / tablet portrait ---------------- */
@media (max-width: 760px) {
  .navbar { padding: 0 14px; gap: 12px; }
  .brand-name { display: none; }
  .navbar-search { max-width: none; flex: 1; }
  .song-grid { grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 12px; }
  .artist-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 14px; }
  .playlist-hero { gap: 16px; }
  .playlist-hero-cover { width: 110px; height: 110px; }
  .playlist-hero-info h1 { font-size: 30px; }
  .view-header h1 { font-size: 26px; }
  /* The album column has no room left once the row's other columns are
     accounted for, so it is dropped rather than squeezed further. */
  .track-row { grid-template-columns: 28px 1fr 74px; gap: 10px; }
  .track-album { display: none; }
  .modal { width: min(94vw, 460px); }

  /* The Windows desktop app has nothing to offer a phone/tablet, so the
     download button in the navbar is dropped entirely on touch-primary
     narrow viewports (see hideDesktopDownloadButtonWhereItDoesNotBelong
     in app.js for the other case this button is hidden — inside the
     desktop app itself). */
  .install-app-wrap { display: none; }

  /* ---- The sidebar becomes a bottom tab bar ---- */
  .sidebar { display: none; }
  .mobile-nav {
    display: flex; align-items: stretch; flex: 0 0 auto;
    background: var(--bg-elevated); border: 1px solid var(--border-soft);
    border-radius: var(--radius-md); padding: 2px 4px;
    /* 44px tap targets plus the home-indicator inset on iOS. */
    min-height: 56px;
  }
  .body-row { min-height: 0; }

  /* ---- Compact player bar (Spotify's phone pattern) ----
     A 3px progress line pinned to the bottom edge instead of a scrubber
     nobody can hit accurately with a thumb; the real scrubber, shuffle,
     repeat, volume and lyrics all live one tap away in Now Playing,
     which tapping the bar opens. */
  .player-bar {
    display: flex; align-items: center; gap: 10px;
    height: 62px; padding: 0 8px 0 10px; position: relative; overflow: hidden;
  }
  .player-now { flex: 1 1 auto; min-width: 0; gap: 10px; }
  .player-meta { display: flex; min-width: 0; }
  .player-title { font-size: 13px; }
  .player-artist { font-size: 11.5px; }
  .player-cover { width: 44px; height: 44px; }
  .player-center { flex: 0 0 auto; width: auto; flex-direction: row; gap: 0; }
  .player-controls { gap: 2px; }
  /* The bar is 62px tall and clipped to a rounded rectangle, so the
     progress line pinned to its bottom edge was being eaten by the
     corner radius at both ends. Squaring off just that edge lets it run
     the full width, which is the only thing it is there to do. */
  .player-bar { border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; }
  .player-now .like-btn { flex: 0 0 auto; }
  /* Give the title room: the cover, heart and two transport buttons are
     all fixed width, so this is the only element that can absorb a long
     track name. */
  .player-meta { flex: 1 1 auto; }
  .player-title, .player-artist { max-width: 100%; }
  #shuffleBtn, #repeatBtn, #prevBtn { display: none; }
  .player-seek { display: none; }
  .player-seek .time { display: none; }
  /* A readout, not a control: a 3px-tall scrubber pinned to the very
     bottom edge of the bar is impossible to hit deliberately and far too
     easy to hit by accident. Clicks fall through to the bar itself,
     which opens the full player where the real scrubber lives. */
  .player-seek input[type="range"] {
    height: 3px; border-radius: 0; pointer-events: none; cursor: default;
  }
  .player-seek input[type="range"]::-webkit-slider-thumb { opacity: 0; }
  .player-seek input[type="range"]::-moz-range-thumb { opacity: 0; }
  /* Nothing in .player-extra fits a 62px bar. Queue and Jam — the two
     that matter on touch — reappear inside the full-screen player;
     volume is the hardware keys' job on a phone. */
  .player-extra { display: none; }
  .now-playing-only-mobile { display: flex; }

  /* ---- Full-screen detail views (Spotify-style) ----
     Liked Songs / a Playlist / Weekly Mix / an artist page / a Profile
     take the whole screen: the navbar is dropped and a single back
     button replaces it. The bottom nav deliberately stays, so the user
     is never stranded without a way out. */
  body[data-view="liked"] .navbar,
  body[data-view="weekly"] .navbar,
  body[data-view="topsongs"] .navbar,
  body[data-view="playlist"] .navbar,
  body[data-view="profile"] .navbar,
  body[data-view="settings"] .navbar,
  body.artist-detail-open .navbar,
  body[data-view="liked"] .update-banner,
  body[data-view="weekly"] .update-banner,
  body[data-view="playlist"] .update-banner,
  body[data-view="profile"] .update-banner,
  body.artist-detail-open .update-banner {
    display: none;
  }
  body[data-view="liked"] .layout,
  body[data-view="weekly"] .layout,
  body[data-view="topsongs"] .layout,
  body[data-view="playlist"] .layout,
  body[data-view="profile"] .layout,
  body[data-view="settings"] .layout,
  body.artist-detail-open .layout {
    padding: 0 0 max(var(--gap), env(safe-area-inset-bottom)); gap: 0;
  }
  body[data-view="liked"] .body-row,
  body[data-view="weekly"] .body-row,
  body[data-view="topsongs"] .body-row,
  body[data-view="playlist"] .body-row,
  body[data-view="profile"] .body-row,
  body[data-view="settings"] .body-row,
  body.artist-detail-open .body-row { gap: 0; }
  body[data-view="liked"] .content,
  body[data-view="weekly"] .content,
  body[data-view="topsongs"] .content,
  body[data-view="playlist"] .content,
  body[data-view="profile"] .content,
  body[data-view="settings"] .content,
  body.artist-detail-open .content {
    border: none; border-radius: 0;
    padding: max(16px, env(safe-area-inset-top)) 16px 26px;
  }
  /* The player bar and tab bar are inside .layout, whose padding the
     rules above zero out — give them their own inline margin back so
     they don't run edge to edge under a rounded screen corner. */
  body[data-view="liked"] .player-bar, body[data-view="liked"] .mobile-nav,
  body[data-view="weekly"] .player-bar, body[data-view="weekly"] .mobile-nav,
  body[data-view="topsongs"] .player-bar, body[data-view="topsongs"] .mobile-nav,
  body[data-view="playlist"] .player-bar, body[data-view="playlist"] .mobile-nav,
  body[data-view="profile"] .player-bar, body[data-view="profile"] .mobile-nav,
  body[data-view="settings"] .player-bar, body[data-view="settings"] .mobile-nav,
  body.artist-detail-open .player-bar, body.artist-detail-open .mobile-nav {
    margin: 0 max(6px, env(safe-area-inset-left)) 0 max(6px, env(safe-area-inset-right));
  }
  body[data-view="liked"] .mobile-nav,
  body[data-view="weekly"] .mobile-nav,
  body[data-view="topsongs"] .mobile-nav,
  body[data-view="playlist"] .mobile-nav,
  body[data-view="profile"] .mobile-nav,
  body[data-view="settings"] .mobile-nav,
  body.artist-detail-open .mobile-nav { margin-top: 6px; }
  body[data-view="liked"] .player-bar,
  body[data-view="weekly"] .player-bar,
  body[data-view="topsongs"] .player-bar,
  body[data-view="playlist"] .player-bar,
  body[data-view="profile"] .player-bar,
  body[data-view="settings"] .player-bar,
  body.artist-detail-open .player-bar { margin-top: 6px; }

  .view-back-btn { display: flex; }
  .artist-back-btn .view-back-label { display: none; }

  /* ---- Panels become bottom sheets ---- */
  .queue-panel {
    top: auto; left: 0; right: 0; bottom: 0; width: auto; max-width: none;
    height: auto; max-height: 72dvh;
    border-radius: 18px 18px 0 0; border-width: 1px 0 0;
    transform: translateY(100%);
    padding-bottom: env(safe-area-inset-bottom);
  }
  .queue-panel.show { transform: translateY(0); }
  /* The notification surfaces are handled at the very bottom of this
     file, next to the rules they override — see "Notifications on
     small screens". */

  /* Floating menus must never be wider than the screen they pop up on. */
  .floating-menu { width: min(260px, calc(100vw - 24px)); max-height: 60dvh; }

  /* ---- Settings: a list of tabs, then the tab ----
     The horizontal scroller this replaces put half the tabs past the
     right edge with nothing to indicate they existed, and still left the
     settings themselves starting a third of the way down the screen.
     Now the list is one screen — every tab visible, nothing to scroll —
     and opening one replaces it, with the back button walking out. */
  .settings-layout { flex-direction: column; gap: 16px; align-items: stretch; }
  .settings-view { max-width: none; }
  .settings-view .view-header { text-align: center; }
  .settings-tabs {
    flex-direction: column; flex-wrap: nowrap; overflow: visible;
    flex: 0 0 auto; width: 100%; min-width: 0;
    gap: 8px; background: none; padding: 0;
  }
  .settings-tab {
    width: 100%; justify-content: flex-start; white-space: nowrap;
    background: var(--bg-card); border: 1px solid var(--border-soft);
    padding: 14px 16px; font-size: 14.5px;
  }
  /* A chevron, so a row reads as "goes somewhere" rather than "is
     selected" — which is what it now does on this layout. */
  .settings-mobile .settings-tab::after {
    content: ''; margin-left: auto; width: 7px; height: 7px;
    border-right: 2px solid currentColor; border-top: 2px solid currentColor;
    transform: rotate(45deg); opacity: .5;
  }
  .settings-mobile .settings-tab.active { background: var(--bg-card); color: var(--text); }
  .settings-mobile .settings-tab-badge { margin-left: 8px; }

  .settings-pane.is-hidden-by-nav { display: none; }

  .settings-pane-back {
    display: flex; align-items: center; gap: 8px; align-self: flex-start;
    background: var(--bg-card); border: 1px solid var(--border-soft); color: var(--text);
    border-radius: 999px; padding: 9px 16px 9px 12px; font-family: inherit;
    font-size: 13.5px; font-weight: 700; cursor: pointer;
    transition: background .2s, transform .2s;
  }
  .settings-pane-back:active { transform: scale(.96); }
  .settings-pane-back svg { width: 17px; height: 17px; }
  /* The whole point of the drill-down is that one tab owns the screen. */
  .settings-drilled-in .settings-tabs { display: none; }

  /* ---- Bottom sheets get a grab handle ----
     They can be dragged down to dismiss (see enableSheetDrag in app.js);
     without something that looks grabbable nobody would think to try. */
  .queue-panel::before,
  .notif-center::before {
    content: ''; display: block; flex: 0 0 auto;
    width: 42px; height: 4px; border-radius: 999px;
    background: #39404f; margin: 10px auto 2px;
  }
  .queue-panel-head, .notif-center-head { padding-top: 8px; }
}

/* ---------------- ≤600px — large phone ---------------- */
@media (max-width: 600px) {
  :root { --gap: 6px; }
  .content { padding: 16px 12px 26px; }
  .view-header { margin-bottom: 20px; }
  .view-header h1 { font-size: 23px; }
  .view-sub { font-size: 13px; }
  .section-head h2 { font-size: 17px; }
  .section-head { gap: 10px; flex-wrap: wrap; }

  /* `auto`, not a fixed 60px. The like and menu buttons are 38px each on
     a touch device (they have to be, to be hittable), so a hard-coded
     60px column could not contain them — the actions overflowed left and
     the heart ended up sitting on top of the track title. An auto column
     is sized by what is actually in it, and 1fr gives the title whatever
     is left. */
  .track-row { grid-template-columns: 22px minmax(0, 1fr) auto; gap: 8px; padding: 8px; }
  .track-row.draggable-row { padding-left: 26px; }
  .track-drag-handle { left: 4px; }
  .track-cover { width: 40px; height: 40px; }
  .track-main { min-width: 0; }
  /* The running time is the least useful of the three on a phone and the
     only one that is not interactive, so it makes way for the two that
     are. It is still on every row from 601px up. */
  .track-duration { display: none; }
  .track-actions { gap: 2px; }

  .playlist-toolbar { flex-wrap: wrap; }
  .modal { padding: 22px 16px; width: min(94vw, 420px); max-height: 86dvh; overflow-y: auto; }
  .confirm-modal, .jam-modal { width: min(94vw, 420px); }
  .jam-link-row { flex-wrap: wrap; }
  .jam-link-row .btn { width: 100%; }

  .settings-account-grid { flex-direction: column; align-items: center; text-align: center; gap: 20px; }
  .settings-account-grid .profile-avatar-picker { flex: 0 0 auto; }
  .shortcuts-grid { grid-template-columns: 1fr; grid-auto-flow: row; grid-template-rows: none; }
  .privacy-option { gap: 14px; }
  .friends-scroll { max-height: none; }

  .playlist-hero {
    flex-direction: column; align-items: flex-start; text-align: left; gap: 14px; margin-bottom: 18px;
  }
  .playlist-hero-cover { width: 100px; height: 100px; }
  .playlist-hero-info h1 { font-size: 25px; }
  .playlist-hero-info { width: 100%; }

  .friend-search-results { left: 0; right: 0; }
  .toast { bottom: calc(140px + env(safe-area-inset-bottom)); left: 12px; right: 12px; transform: translateY(12px); width: auto; text-align: center; }
  .toast.show { transform: translateY(0); }

  /* iOS Safari zooms the page in on focus of any input under 16px —
     bump every text input up to 16px only at phone widths. */
  input[type="text"], input[type="password"], input[type="search"], input[type="email"], textarea, select { font-size: 16px; }
}

/* ---------------- ≤420px — phone ---------------- */
@media (max-width: 420px) {
  .navbar { padding: 0 10px; gap: 8px; height: 56px; }
  .brand-icon, .brand-icon svg { width: 26px; height: 26px; }
  .navbar-search { height: 38px; padding: 0 12px 0 38px; }
  .navbar-search-icon { left: 12px; }
  .navbar-search-icon, .navbar-search-icon svg { width: 23px; height: 23px; }
  .navbar-search-kbd { display: none; }
  .navbar-auth { gap: 2px; }

  .song-grid { grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); gap: 10px; }
  .song-card { padding: 10px; }
  .artist-grid { grid-template-columns: repeat(auto-fill, minmax(108px, 1fr)); gap: 12px; }
  .shortcut-grid { grid-template-columns: 1fr 1fr; }

  .now-playing-controls { gap: 12px; }
  .mobile-nav-label { font-size: 10px; }
}

/* ---------------- ≤360px — small phone ---------------- */
@media (max-width: 360px) {
  .song-grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 8px; }
  .player-cover { width: 40px; height: 40px; }
  #playerLikeBtn { display: none; }
  .btn { padding: 9px 14px; font-size: 13px; }
  .playlist-hero-cover { width: 84px; height: 84px; }
  .playlist-hero-info h1 { font-size: 22px; }
  .mobile-nav-btn svg { width: 20px; height: 20px; }
}

/* ---------------- Landscape phones ----------------
   A phone on its side has ~350px of height to play with. The
   full-screen player's square artwork alone would be taller than the
   viewport, so it switches to a two-column layout. */
@media (max-width: 900px) and (max-height: 460px) and (orientation: landscape) {
  .now-playing-content {
    max-width: none; flex-direction: row; flex-wrap: wrap;
    align-items: center; gap: 8px 22px; padding: 12px 20px;
  }
  .now-playing-top { flex: 1 1 100%; }
  .now-playing-stage { flex: 0 0 34%; max-width: 220px; }
  .now-playing-art { width: 100%; }
  .now-playing-meta, .now-playing-seek, .now-playing-controls, .now-playing-volume { flex: 1 1 46%; }
  .now-playing-controls { gap: 14px; }
  .now-playing-controls .play-btn { width: 48px; height: 48px; }
  .now-playing-controls .play-btn svg { width: 40px; height: 40px; }
  .mobile-nav { min-height: 48px; }
  .mobile-nav-label { display: none; }
}

/* =========================================================
   Now Playing — full-screen "chill mode" (Spotify-style)
   ========================================================= */
.player-cover, .player-meta { cursor: pointer; }

.now-playing {
  position: fixed; inset: 0; z-index: 400; display: flex; align-items: center; justify-content: center;
  opacity: 0; transition: opacity .32s ease;
}
.now-playing.show { opacity: 1; }
/* Two identical layers stacked on each other; only one is opaque at a
   time and a track change swaps which. Fading the single layer out to
   change picture left the full-screen player with no background at all
   for the length of the fade, and the site behind it showed straight
   through. The background colour is on both layers so even the very
   first paint, before any artwork has loaded, is opaque. */
.now-playing-bg {
  position: absolute; inset: -10%; background-size: cover; background-position: center;
  filter: blur(60px) brightness(.5) saturate(1.3); transform: scale(1.15); background-color: var(--bg-elevated);
  opacity: 0; transition: opacity .55s ease;
}
.now-playing-bg.is-front { opacity: 1; }
.now-playing-scrim {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,.35), rgba(8,10,14,.86) 55%, rgba(8,10,14,.97));
}
.now-playing-content {
  position: relative; z-index: 1; width: 100%; max-width: 420px; box-sizing: border-box;
  /* dvh, not vh: on a phone `100vh` is the height *without* the browser's
     address bar, so the bottom of the panel (the volume row) sat under
     the chrome and could not be reached. Scrollable as well, because a
     short landscape viewport cannot fit artwork + controls at all. */
  max-height: 100dvh; overflow-y: auto; overscroll-behavior: contain;
  padding: 24px 28px 40px; display: flex; flex-direction: column; gap: 20px; color: #fff;
  transform: translateY(16px); transition: transform .34s cubic-bezier(.22,1,.36,1);
}
.now-playing-content::-webkit-scrollbar { width: 0; }
.now-playing.show .now-playing-content { transform: translateY(0); }
.now-playing-top { display: flex; align-items: center; justify-content: space-between; }
.now-playing-top-spacer { width: 34px; }
.now-playing-close { color: #fff; }
@media (hover: hover) { .now-playing-close:hover { background: rgba(255,255,255,.12); } }
.now-playing-kind { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .1em; color: rgba(255,255,255,.65); }
.now-playing-art-wrap { display: flex; justify-content: center; padding: 8px 0 4px; }
.now-playing-art {
  width: min(80vw, 350px); aspect-ratio: 1; border-radius: var(--radius-md); background: var(--bg-card) center/cover no-repeat;
  /* A large blur with almost no spread throws light past the cover's own
     edges on every side, not just downward — against the blurred backdrop
     that reads as a faint halo hugging the left/right/top edges rather
     than a shadow underneath. Pulling the spread in tight and leaning on
     the vertical offset keeps the shadow under the artwork instead. */
  box-shadow: 0 22px 40px -14px rgba(0,0,0,.55); display: flex; align-items: center; justify-content: center; color: rgba(255,255,255,.4);
}
.now-playing-art svg { width: 30%; height: 30%; }
/* `justify-content: space-between` with three loose children pushed the
   copy button into the gap between the title and the heart. The two
   controls are one group now, and the titles take the slack, so copy is
   always immediately beside like. */
.now-playing-meta { display: flex; align-items: center; gap: 14px; }
.now-playing-meta-actions { display: flex; align-items: center; gap: 4px; flex: 0 0 auto; margin-left: auto; }
.now-playing-titles { min-width: 0; flex: 1 1 auto; }
.now-playing-titles h1 { font-size: 22px; font-weight: 700; margin: 0 0 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.now-playing-titles p { font-size: 14.5px; color: rgba(255,255,255,.65); margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.now-playing-meta .icon-btn { color: rgba(255,255,255,.75); flex: 0 0 auto; }
.now-playing-meta .icon-btn.liked { color: var(--primary); }
.now-playing-seek input[type="range"] { width: 100%; }
.now-playing-times { display: flex; justify-content: space-between; font-size: 11.5px; color: rgba(255,255,255,.55); margin-top: 4px; }
.now-playing-controls { display: flex; align-items: center; justify-content: center; gap: 22px; }
.now-playing-controls .icon-btn { color: rgba(255,255,255,.8); }
@media (hover: hover) { .now-playing-controls .icon-btn:hover { color: #fff; background: rgba(255,255,255,.1); } }
.now-playing-controls .icon-btn.active { color: var(--primary); }
.now-playing-controls .play-btn { width: 60px; height: 60px; border-radius: 50%; background: #fff; color: #0a0c11; transition: all .2s; }
@media (hover: hover) { .now-playing-controls .play-btn:hover { background: #fff; color: #0a0c11; opacity: 0.8; } }
.now-playing-controls .play-btn:active { transform: scale(.95); }
.now-playing-controls .play-btn svg { width: 50px; height: 50px; }
.now-playing-volume { display: flex; align-items: center; justify-content: center; gap: 10px; max-width: 260px; margin: 4px auto 0; width: 100%; }
.now-playing-volume .icon-btn { color: rgba(255,255,255,.75); flex: 0 0 auto; }
@media (hover: hover) { .now-playing-volume .icon-btn:hover { color: #fff; background: rgba(255,255,255,.1); } }
.now-playing-volume input[type="range"] { flex: 1; }

body.now-playing-open { overflow: hidden; }
@media (max-width: 760px) {
  .now-playing-content {
    padding: max(14px, env(safe-area-inset-top)) 20px calc(26px + env(safe-area-inset-bottom));
    gap: 14px;
  }
  .now-playing-art { width: min(78vw, 320px); }
  .now-playing-top-actions { gap: 2px; }
  .now-playing-titles h1 { font-size: 20px; }
  /* A full-width volume slider is close to useless with a thumb and the
     hardware keys are right there, so the row is dropped and the space
     goes to the transport controls instead. */
  .now-playing-volume { display: none; }
  .now-playing-controls { gap: 18px; }
}

/* ---- Lyrics (Spotify-style synced view inside Now Playing) ---- */
.now-playing-lyrics-toggle { color: var(--text-dim); transition: color .25s ease; }
@media (hover: hover) { .now-playing-lyrics-toggle:hover { color: var(--text); } }
.now-playing-lyrics-toggle.active { color: var(--primary); }

/* Cover <-> Lyrics flip: a real 3D card-flip (like turning the record over),
   not a plain crossfade. .now-playing-stage sets the perspective; .now-playing-
   stage-inner is the card that rotates 180deg on the Y axis. Both faces sit on
   top of one another (back face is pre-rotated 180deg + absolutely positioned)
   so the size of the card always follows the front face (the art, a square). */
.now-playing-stage { perspective: 1600px; }
.now-playing-stage-inner {
  position: relative;
  transform-style: preserve-3d;
  transition: transform .68s cubic-bezier(.22,1,.36,1);
  will-change: transform;
}
.now-playing.lyrics-mode .now-playing-stage-inner { transform: rotateY(180deg); }

.now-playing-face {
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  transition: opacity .3s ease;
}
.now-playing-face--front { pointer-events: auto; }
.now-playing-face--back {
  position: absolute;
  inset: 0;
  transform: rotateY(180deg);
  pointer-events: none;
}
.now-playing.lyrics-mode .now-playing-face--front { pointer-events: none; }
.now-playing.lyrics-mode .now-playing-face--back { pointer-events: auto; }

/* Fade the artwork/glow slightly as it rotates past the edge, and bring the
   lyrics text in with a soft scale — keeps the flip from feeling stiff. */
.now-playing-stage-inner { transform-origin: center; }
.now-playing-face--front { transition: opacity .5s ease; }
.now-playing.lyrics-mode .now-playing-face--front { opacity: .35; }
.lyrics-scroll {
  transition: opacity .45s ease .12s, transform .45s cubic-bezier(.22,1,.36,1) .12s;
  opacity: 0; transform: scale(.94);
}
.now-playing.lyrics-mode .lyrics-scroll { opacity: 1; transform: scale(1); }

.now-playing-lyrics {
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin: 4px 0;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 8%, #000 88%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 8%, #000 88%, transparent);
  scrollbar-width: none;
}
.now-playing-lyrics::-webkit-scrollbar { display: none; }
.lyrics-scroll { display: flex; flex-direction: column; gap: 2px; padding: 90px 16px 120px; }
.lyrics-line {
  font-size: 17px; font-weight: 700; line-height: 1.55; margin: 0; padding: 6px 2px;
  color: rgba(255,255,255,.38); cursor: pointer; transform-origin: center;
  transition: color .3s ease, transform .3s ease;
  overflow-wrap: break-word; word-break: break-word;
}
@media (hover: hover) { .lyrics-line:hover { color: rgba(255,255,255,.85); } }
.lyrics-line.active { color: #fff; transform: scale(1.05); }
.lyrics-line--static { cursor: default; font-size: 16.5px; font-weight: 500; color: rgba(255,255,255,.7); }
@media (hover: hover) { .lyrics-line--static:hover { color: rgba(255,255,255,.7); } }
.lyrics-state-msg { color: rgba(255,255,255,.55); font-size: 14.5px; text-align: center; padding: 60px 16px; line-height: 1.6; }

/* =========================================================
   Mini Player — floating, Spotify-style compact player
   ========================================================= */
/* While Mini Player is active, the whole app shell is swapped out for the
   small floating card — playback itself keeps running because <audio> and
   the Mini Player markup both live outside .layout. */
body.mini-player-active .layout { display: none; }
body.mini-player-active { background: var(--bg-void); }

.mini-player {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 500;
  width: 280px;
  max-width: calc(100vw - 36px);
  background: var(--bg-elevated);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  box-shadow: 0 24px 48px rgba(0,0,0,.55);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  opacity: 0;
  transform: translateY(18px) scale(.94);
  pointer-events: none;
  transition: opacity .3s cubic-bezier(.22,1,.36,1), transform .3s cubic-bezier(.22,1,.36,1);
}
.mini-player.show { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }

/* When floating in a real always-on-top OS window via the Document
   Picture-in-Picture API, the card fills the whole floating window
   instead of floating inside the page itself. */
.mini-player.pip-mode {
  position: static;
  width: 100%;
  height: 100vh;
  max-width: none;
  border: none;
  border-radius: 0;
  box-shadow: none;
  opacity: 1;
  transform: none;
  pointer-events: auto;
  justify-content: center;
  box-sizing: border-box;
}

.mini-player-expand {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 28px;
  height: 28px;
  color: var(--text-faint);
}
@media (hover: hover) { .mini-player-expand:hover { color: var(--text); background: var(--bg-card); } }
.mini-player-expand svg { width: 15px; height: 15px; }

.mini-player-top { display: flex; align-items: center; gap: 10px; padding-right: 26px; }
.mini-player-cover {
  width: 48px; height: 48px; border-radius: var(--radius-sm); background: var(--bg-card) center/cover no-repeat;
  flex: 0 0 auto; display: flex; align-items: center; justify-content: center; color: var(--text-faint);
}
.mini-player-cover svg { width: 40%; height: 40%; opacity: .5; }
.mini-player-cover.pop-in { animation: coverPop .28s ease-out; }
.mini-player-meta { display: flex; flex-direction: column; min-width: 0; gap: 2px; }
.mini-player-title { font-size: 13px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mini-player-artist { font-size: 11.5px; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.mini-player-seek input[type="range"] { width: 100%; height: 4px; }

.mini-player-controls { display: flex; align-items: center; justify-content: center; gap: 10px; }
.mini-player-controls .icon-btn { color: var(--text-dim); width: 30px; height: 30px; }
@media (hover: hover) { .mini-player-controls .icon-btn:hover { color: var(--text); background: var(--bg-card); } }
.mini-player-controls .play-btn {
  width: 34px; height: 34px; border-radius: 50%; background: transparent; color: #0a0c11;
  display: flex; align-items: center; justify-content: center;
  transition: all .25s cubic-bezier(.34,1.56,.64,1);
}
@media (hover: hover) { .mini-player-controls .play-btn:hover { background: #fff; color: #0a0c11; } }
.mini-player-controls .play-btn:active { transform: scale(.95); }
.mini-player-controls .play-btn svg { width: 28px; height: 28px; }

@media (max-width: 760px) {
  .mini-player { right: 10px; bottom: calc(10px + env(safe-area-inset-bottom)); left: 10px; width: auto; }
}
/* =========================================================
   Jam — shared listening room
   ========================================================= */
#jamBtn.jam-btn-active { color: #4ade80; }
#jamBtn.jam-btn-active::after {
  content: ''; position: absolute; top: 6px; right: 6px; width: 7px; height: 7px; border-radius: 50%;
  background: #4ade80; box-shadow: 0 0 0 2px var(--bg-elevated); animation: pulseDot 1.2s ease-in-out infinite;
}
#jamBtn { position: relative; }

.jam-modal { text-align: center; }
.jam-modal-head { display: flex; flex-direction: column; align-items: center; gap: 10px; margin-bottom: 22px; }
.jam-modal-icon {
  width: 46px; height: 46px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
  background: var(--primary-grad); color: #fff;
}
.jam-modal-icon svg { width: 24px; height: 24px; }
.jam-modal-head h2 { font-size: 20px; margin: 0; }
.jam-modal-sub { font-size: 13px; color: var(--text-dim); margin: 0; line-height: 1.5; }

.jam-empty-state { display: flex; flex-direction: column; align-items: center; gap: 16px; }
.jam-empty-state p { font-size: 13.5px; color: var(--text-dim); line-height: 1.6; margin: 0; }

.jam-active-state { display: flex; flex-direction: column; gap: 14px; text-align: left; }
.jam-link-row { display: flex; gap: 8px; }
#jamLinkInput {
  flex: 1; min-width: 0; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 10px 12px; color: var(--text); font-size: 12.5px; outline: none;
}
.jam-expiry { font-size: 11.5px; color: var(--text-faint); margin: 0; }

.jam-participants { display: flex; flex-direction: column; gap: 4px; max-height: 220px; overflow-y: auto; }
.jam-participant { display: flex; align-items: center; gap: 10px; padding: 7px 8px; border-radius: var(--radius-sm); }
@media (hover: hover) { .jam-participant:hover { background: var(--bg-card-hover); } }
.jam-participant-avatar {
  width: 30px; height: 30px; border-radius: 50%; background: var(--bg-card) center/cover no-repeat;
  display: flex; align-items: center; justify-content: center; color: var(--text-faint); flex: 0 0 auto; overflow: hidden;
}
.jam-participant-avatar svg { width: 14px; height: 14px; }
.jam-participant-name { flex: 1; min-width: 0; font-size: 13px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.jam-participant-badge {
  font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--primary);
  background: rgba(59,130,246,.14); border-radius: 5px; padding: 3px 6px; flex: 0 0 auto;
}

.jam-modal-actions { display: flex; justify-content: center; padding-top: 4px; border-top: 1px solid var(--border-soft); }

/* =========================================================
   Skeletons — shown while the first payload (session + catalog)
   is still in flight, so the app never opens on a blank panel.
   They mirror the real card/row geometry exactly, which keeps
   layout shift at zero when the real data swaps in.
   ========================================================= */
.skeleton {
  /* Sits on top of a --bg-card surface, so the base tone has to be lighter
     than the card itself or the placeholder shapes are invisible. */
  background: linear-gradient(100deg, #222836 30%, #333c50 50%, #222836 70%);
  background-size: 260% 100%;
  animation: skeletonSweep 1.3s ease-in-out infinite;
  border-radius: var(--radius-sm);
}
@keyframes skeletonSweep { from { background-position: 160% 0; } to { background-position: -60% 0; } }

.skeleton-card { background: var(--bg-card); border: 1px solid var(--border-soft); border-radius: var(--radius-sm); padding: 14px; }
.skeleton-card .skeleton-cover { width: 100%; aspect-ratio: 1/1; border-radius: var(--radius-sm); margin-bottom: 12px; }
.skeleton-card .skeleton-line { height: 11px; border-radius: 5px; }
.skeleton-card .skeleton-line + .skeleton-line { margin-top: 8px; width: 60%; height: 9px; }

.skeleton-row { display: flex; align-items: center; gap: 12px; padding: 8px 10px; }
.skeleton-row .skeleton-cover { width: 44px; height: 44px; border-radius: var(--radius-sm); flex: 0 0 auto; }
.skeleton-row .skeleton-lines { flex: 1; min-width: 0; }
.skeleton-row .skeleton-line { height: 11px; border-radius: 5px; width: 42%; }
.skeleton-row .skeleton-line + .skeleton-line { margin-top: 8px; width: 26%; height: 9px; }

/* =========================================================
   Home shelves — horizontally scrolling rows (Recently played,
   New releases, Popular artists). A shelf keeps the catalog
   browsable without turning Home into a wall of 1000+ identical
   cards with no hierarchy.
   ========================================================= */
.shelf { margin-bottom: 34px; }
.shelf-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.shelf-head h2 { font-size: 19px; font-weight: 700; flex: 1; min-width: 0; }
.shelf-head-icon { width: 22px; height: 22px; color: var(--primary); flex: 0 0 auto; display: flex; }
.shelf-head-icon svg { width: 22px; height: 22px; }
.shelf-nav { display: flex; gap: 6px; flex: 0 0 auto; }
.shelf-nav-btn {
  width: 30px; height: 30px; border-radius: 50%; border: 1px solid var(--border);
  background: var(--bg-card); color: var(--text-dim); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: background .2s, color .2s, opacity .2s;
}
.shelf-nav-btn svg { width: 15px; height: 15px; }
@media (hover: hover) { .shelf-nav-btn:hover:not(:disabled) { background: var(--bg-card-hover); color: var(--text); } }
.shelf-nav-btn:disabled { opacity: .3; cursor: default; }

.shelf-track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 168px;
  gap: 18px;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-padding-inline: 4px; /* matches the track's own inline padding */
  overflow-anchor: none;
  /* breathing room for the cards' hover lift and focus ring, which the
     scroll container would otherwise clip */
  padding: 6px 4px 10px;
  margin: -6px -4px -10px;
  scrollbar-width: none;
}
.shelf-track::-webkit-scrollbar { display: none; }
.shelf-track > * { scroll-snap-align: start; }
@media (prefers-reduced-motion: reduce) { .shelf-track { scroll-behavior: auto; } }

/* ---- "Made for you" shortcut tiles ---- */
.shortcut-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 14px; margin-bottom: 34px;
}
.shortcut-tile {
  display: flex; align-items: center; gap: 14px; text-align: left;
  color: var(--text); /* <button> does not inherit color from the page */
  background: var(--bg-card); border: 1px solid var(--border-soft); border-radius: var(--radius-md);
  padding: 12px; cursor: pointer; overflow: hidden; position: relative; min-width: 0;
  transition: background .24s, transform .3s cubic-bezier(.22,1,.36,1), border-color .24s;
}
@media (hover: hover) { .shortcut-tile:hover { background: var(--bg-card-hover); transform: translateY(-3px); border-color: #2a3040; } }
.shortcut-tile:active { transform: scale(.985); }
.shortcut-tile-art {
  width: 58px; height: 58px; border-radius: var(--radius-sm); flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center; color: #fff;
  background: var(--primary-grad); background-size: cover; background-position: center;
}
.shortcut-tile-art svg { width: 26px; height: 26px; }
.shortcut-tile-art.art-weekly { background-image: url('../icons/weekly-mix-cover.webp'); }
.shortcut-tile-art.art-topsongs { background-image: url('../icons/top-songs-cover.webp'); }
.shortcut-tile-art.art-shuffle { background: linear-gradient(135deg, #f59e0b, #ef4444); }
.shortcut-tile-art.art-jam { background: linear-gradient(135deg, #8b5cf6, #d946ef); }
.shortcut-tile-text { min-width: 0; flex: 1; }
/* display:block matters here — these are <span>s, and overflow/text-overflow
   are simply ignored on inline boxes, so the labels ran past the tile edge
   instead of ellipsing. */
.shortcut-tile-title { display: block; font-family: var(--font-display); font-size: 14.5px; font-weight: 700; margin: 0 0 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Two lines rather than one line plus an ellipsis. These labels are the
   whole point of the tile — "Listen together, perfectly in sync" cut to
   "Listen together, perfect…" tells the reader nothing they didn't
   already guess from the title. */
.shortcut-tile-sub {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  font-size: 12px; color: var(--text-faint); margin: 0;
  overflow: hidden; line-height: 1.4;
}

/* =========================================================
   Sort control (All tracks)
   ========================================================= */
.sort-btn {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--bg-card); border: 1px solid var(--border); color: var(--text-dim);
  border-radius: 999px; padding: 7px 13px; font-size: 12.5px; font-weight: 600;
  cursor: pointer; transition: background .2s, color .2s, border-color .2s;
}
@media (hover: hover) { .sort-btn:hover { background: var(--bg-card-hover); color: var(--text); border-color: #2f3648; } }
.sort-btn svg { width: 14px; height: 14px; }
.sort-btn .sort-btn-caret svg { width: 11px; height: 11px; }
.sort-menu { width: 200px; }

/* =========================================================
   Rich empty states — an icon, a headline, one line of guidance
   and (where there is something useful to do) an action, instead
   of a single line of grey text.
   ========================================================= */
.empty-block {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 6px; padding: 48px 20px; margin: 0 auto; max-width: 380px;
}
.empty-block-icon {
  width: 62px; height: 62px; border-radius: 50%; margin-bottom: 6px;
  background: var(--bg-card); border: 1px solid var(--border-soft);
  display: flex; align-items: center; justify-content: center; color: var(--primary);
}
.empty-block-icon svg { width: 26px; height: 26px; }
.empty-block h3 { font-family: var(--font-display); font-size: 16.5px; font-weight: 700; margin: 0; }
.empty-block p { font-size: 13.5px; color: var(--text-faint); line-height: 1.6; margin: 0; }
.empty-block .btn { margin-top: 12px; }

/* =========================================================
   Keyboard shortcuts cheat sheet
   ========================================================= */
#settingsPaneShortcuts { overflow-y: visible; gap: 0; }
.shortcuts-intro { font-size: 12.5px; color: var(--text-faint); line-height: 1.6; margin: 0 0 12px; }
.shortcuts-grid { display: grid; grid-template-columns: 1fr 1fr; grid-auto-flow: column; grid-template-rows: repeat(7, auto); column-gap: 28px; }
.shortcut-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 9px 2px; border-bottom: 1px solid var(--border-soft); font-size: 13px; color: var(--text-dim);
}
.shortcut-row:last-child { border-bottom: none; }
.shortcut-keys { display: flex; gap: 4px; flex: 0 0 auto; }
kbd {
  font-family: var(--font-body); font-size: 11px; font-weight: 700; color: var(--text);
  background: var(--bg-card); border: 1px solid var(--border); border-bottom-width: 2px;
  border-radius: 5px; padding: 3px 7px; min-width: 22px; text-align: center; line-height: 1.3;
}

/* =========================================================
   Sleep timer
   ========================================================= */
/* The sleep menu is anchored inside the full-screen Now Playing view
   (z-index 400), so the shared .floating-menu z-index of 300 would render it
   behind the very screen it belongs to. */
.sleep-menu { width: 215px; z-index: 450; }
.sleep-btn { position: relative; }
.sleep-btn.active { color: var(--primary); }
.spatial-menu { width: 180px; z-index: 450; }
.spatial-btn { position: relative; }
.spatial-btn.active { color: var(--primary); }
.player-extra .spatial-btn.active { color: var(--primary); }
.sleep-countdown {
  position: absolute; bottom: -3px; right: -5px;
  background: var(--primary); color: #fff; font-size: 9px; font-weight: 800;
  border-radius: 999px; padding: 1px 4px; line-height: 1.5; pointer-events: none;
  /* a ring in the surface colour separates the badge from the glyph it sits on */
  box-shadow: 0 0 0 2px rgba(8, 10, 14, .92);
}

/* =========================================================
   Responsive additions for the blocks above
   ========================================================= */
/* Art above the text once a tile gets narrow. Side by side it leaves the
   label barely a third of the tile, which is where the clipping starts. */
@media (max-width: 1100px) {
  .shortcut-tile { flex-direction: column; align-items: flex-start; padding: 12px; gap: 10px; }
  .shortcut-tile-art { width: 46px; height: 46px; }
  .shortcut-tile-art svg { width: 22px; height: 22px; }
  .shortcut-tile-text { width: 100%; }
  .shortcut-tile-title { white-space: normal; overflow-wrap: anywhere; }
  .shortcut-tile-sub { overflow-wrap: anywhere; }
}

@media (max-width: 760px) {
  .shelf-track { grid-auto-columns: 138px; gap: 12px; }
  .shelf-nav { display: none; } /* touch users swipe the shelf instead */
  .shortcut-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
}
@media (max-width: 480px) {
  .shelf { margin-bottom: 26px; }
  .shelf-track { grid-auto-columns: 128px; gap: 10px; }
  .shelf-head h2 { font-size: 17px; }
  .shortcut-tile-title { font-size: 13.5px; }
  .shortcut-tile-sub { font-size: 11.5px; }
  .empty-block { padding: 36px 12px; }
}

/* Now Playing top bar gained a second action (sleep timer) beside Lyrics. */
.now-playing-top-actions { display: flex; align-items: center; gap: 8px; }
.now-playing-top-actions .icon-btn { color: #fff; }
@media (hover: hover) { .now-playing-top-actions .icon-btn:hover { background: rgba(255,255,255,.12); } }
.now-playing-top-actions .icon-btn.active { color: var(--primary); }


/* =========================================================
   Keyboard reachability for hover-revealed controls
   The play / like / "..." buttons on a card only faded in on
   :hover, so a keyboard user could tab into a control that was
   completely invisible. :focus-within reveals the same set the
   moment focus lands anywhere inside the card or row.
   ========================================================= */
.song-card:focus-within .song-card-play,
.song-card:focus-within .song-card-like,
.song-card:focus-within .song-card-menu { opacity: 1; }
.track-row:focus-within:not(.playing) .track-index-num { opacity: 0; transform: scale(.5) rotate(12deg); }
.track-row:focus-within:not(.playing) .track-row-play-icon { opacity: 1; transform: scale(1) rotate(0deg); }

/* .track-index became a real <button> (the row's keyboard-reachable play
   control) — strip the UA button chrome so it looks exactly as it did. */
button.track-index {
  background: none; border: none; padding: 0; margin: 0; font: inherit; cursor: pointer;
  display: block;
}

/* =========================================================
   Notifications

   Two surfaces (see the "Notifications" section in app.js):
     .notif-stack  — Texerlite's own notification cards, drawn whenever
                     the tab is in front. The whole reason these exist is
                     that an OS notification cannot be styled by a web
                     page at all: it arrives as a generic system toast
                     with the browser's chrome around it. These are the
                     product's own surface — brand colours, the real
                     cover art or avatar, and buttons that do the thing.
     .notif-center — the history panel behind the navbar bell.
   ========================================================= */
.notif-stack {
  position: fixed; z-index: 520;
  top: calc(var(--gap) + 12px);
  right: calc(var(--gap) + 12px);
  width: min(370px, calc(100vw - 24px));
  display: flex; flex-direction: column; gap: 10px;
  pointer-events: none; /* the gaps between cards must stay click-through */
}
.notif-card {
  pointer-events: auto;
  position: relative; overflow: hidden;
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 12px; align-items: start;
  padding: 13px 12px 15px;
  background: linear-gradient(155deg, #1a2030 0%, var(--bg-elevated) 60%);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: 0 18px 44px rgba(0, 0, 0, .55), 0 0 0 1px rgba(59, 130, 246, .07);
  cursor: pointer;
  opacity: 0; transform: translateX(18px) scale(.97);
  transition: opacity .28s cubic-bezier(.22,1,.36,1), transform .28s cubic-bezier(.22,1,.36,1);
}
.notif-card.show { opacity: 1; transform: translateX(0) scale(1); }
.notif-card.leaving { opacity: 0; transform: translateX(18px) scale(.97); }
@media (hover: hover) { .notif-card:hover { border-color: #2f3648; } }
.notif-card-art {
  width: 42px; height: 42px; border-radius: var(--radius-sm); flex: 0 0 auto;
  background: var(--bg-card) center/cover no-repeat;
  display: flex; align-items: center; justify-content: center; color: var(--primary);
}
.notif-card-art.is-round { border-radius: 50%; }
.notif-card-art svg { width: 20px; height: 20px; }
.notif-card-text { min-width: 0; display: flex; flex-direction: column; gap: 3px; padding-top: 1px; }
.notif-card-title {
  font-family: var(--font-display); font-size: 13.5px; font-weight: 700; color: var(--text);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.notif-card-body {
  font-size: 12.5px; color: var(--text-dim); line-height: 1.45;
  /* Two lines, then ellipsis: enough for "Title — Artist" without one
     long track name pushing the card to half the screen. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.notif-card-actions { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; }
.notif-card-action {
  border: 1px solid var(--border); background: var(--bg-card); color: var(--text);
  font-family: inherit; font-size: 12px; font-weight: 700; border-radius: 999px;
  padding: 6px 13px; cursor: pointer; transition: background .2s, border-color .2s, filter .2s;
}
@media (hover: hover) { .notif-card-action:hover { background: var(--bg-card-hover); border-color: #2f3648; } }
.notif-card-action.is-primary { background: var(--primary-grad); border-color: transparent; color: #fff; }
@media (hover: hover) { .notif-card-action.is-primary:hover { filter: brightness(1.12); } }
.notif-card-close {
  background: none; border: none; color: var(--text-faint); cursor: pointer; padding: 2px;
  border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center;
  transition: color .2s, background .2s; flex: 0 0 auto;
}
@media (hover: hover) { .notif-card-close:hover { color: #fca5a5; background: rgba(239, 68, 68, .12); } }
.notif-card-close svg { width: 13px; height: 13px; }
/* The auto-dismiss countdown, drawn as a shrinking bar so the card never
   disappears without warning. Hovering pauses it (.is-paused). */
.notif-card-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: rgba(255,255,255,.05); }
.notif-card-progress i { display: block; height: 100%; width: 100%; background: var(--primary-grad); transform-origin: left center; animation: notifCountdown linear forwards; }
.notif-card.is-paused .notif-card-progress i { animation-play-state: paused; }
@keyframes notifCountdown { from { transform: scaleX(1); } to { transform: scaleX(0); } }

/* ---- Navbar bell + unread badge ---- */
.notif-center-wrap { position: relative; }
.notif-bell-btn { color: var(--text-dim); }
@media (hover: hover) { .notif-bell-btn:hover { color: var(--text); background: var(--bg-card); } }
.notif-bell-btn.has-unread { color: var(--text); }
.notif-bell-btn.has-unread svg { animation: bellNudge 1.6s ease-in-out 2; transform-origin: 50% 15%; }
@keyframes bellNudge {
  0%, 70%, 100% { transform: rotate(0deg); }
  76% { transform: rotate(9deg); }
  84% { transform: rotate(-7deg); }
  92% { transform: rotate(4deg); }
}
.notif-bell-badge {
  position: absolute; top: 1px; right: 0;
  min-width: 16px; height: 16px; padding: 0 4px;
  background: var(--primary-grad); color: #fff;
  font-size: 9.5px; font-weight: 800; line-height: 16px; text-align: center;
  border-radius: 999px; box-shadow: 0 0 0 2px var(--bg-elevated); pointer-events: none;
}

/* ---- Notification centre ---- */
.notif-center-overlay {
  position: fixed; inset: 0; z-index: 460; background: rgba(0, 0, 0, .45);
  opacity: 0; transition: opacity .26s ease;
}
.notif-center-overlay.show { opacity: 1; }
.notif-center {
  position: fixed; z-index: 470;
  top: calc(var(--gap) + 74px); right: var(--gap);
  width: min(380px, calc(100vw - var(--gap) * 2));
  max-height: min(560px, calc(100dvh - 160px));
  display: flex; flex-direction: column;
  background: var(--bg-elevated); border: 1px solid var(--border);
  border-radius: var(--radius-md); box-shadow: 0 26px 60px rgba(0, 0, 0, .6);
  /* translate only — no scale. A scaling panel drags every child along
     with it by a fraction of its distance from the transform origin, so
     the close button and the heading visibly slid sideways as the panel
     settled. Sliding the whole panel moves everything together, which
     reads as one object arriving rather than a box inflating. */
  opacity: 0; transform: translateY(-10px);
  transition: opacity .2s cubic-bezier(.22,1,.36,1), transform .2s cubic-bezier(.22,1,.36,1);
}
.notif-center.show { opacity: 1; transform: translateY(0); }
/* The close glyph is centred by flex; pinning its box stops any residual
   sub-pixel drift while the panel is settling. */
.notif-center-head .queue-modal-close { width: 34px; height: 34px; flex: 0 0 34px; }
.notif-center-head .queue-modal-close svg { width: 17px; height: 17px; }
.notif-center-head {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 14px 12px 12px 16px; border-bottom: 1px solid var(--border-soft); flex: 0 0 auto;
}
.notif-center-head h3 { font-size: 15.5px; }
.notif-center-head-actions { display: flex; align-items: center; gap: 4px; }
.notif-center-action {
  background: none; border: none; color: var(--text-dim); font-family: inherit;
  font-size: 12px; font-weight: 600; cursor: pointer; padding: 6px 9px; border-radius: var(--radius-sm);
  transition: color .2s, background .2s;
}
@media (hover: hover) { .notif-center-action:hover { color: var(--text); background: var(--bg-card); } }
@media (hover: hover) { .notif-center-action-danger:hover { color: #fca5a5; background: rgba(239, 68, 68, .12); } }
.notif-center-body { flex: 1 1 auto; overflow-y: auto; padding: 6px; -webkit-overflow-scrolling: touch; }
.notif-center-foot {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px; border-top: 1px solid var(--border-soft); flex: 0 0 auto;
}
.notif-row {
  width: 100%; text-align: left; background: none; border: none; cursor: pointer;
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 11px; align-items: center;
  padding: 10px; border-radius: var(--radius-sm); transition: background .2s;
  font-family: inherit; color: var(--text);
}
@media (hover: hover) { .notif-row:hover { background: var(--bg-card); } }
.notif-row .notif-card-art { width: 38px; height: 38px; }
.notif-row-text { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.notif-row-title { font-size: 13px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.notif-row-body {
  font-size: 12px; color: var(--text-dim); line-height: 1.4;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.notif-row-time { font-size: 11px; color: var(--text-faint); font-weight: 600; margin-top: 2px; }
.notif-row-dot { width: 7px; height: 7px; border-radius: 50%; background: transparent; flex: 0 0 auto; }
.notif-row.is-unread { background: rgba(59, 130, 246, .06); }
.notif-row.is-unread .notif-row-dot { background: var(--primary); }
.notif-center-empty {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 6px; padding: 46px 20px; color: var(--text-faint);
}
.notif-center-empty-icon {
  width: 52px; height: 52px; border-radius: 50%; margin-bottom: 6px;
  background: var(--bg-card); border: 1px solid var(--border-soft);
  display: flex; align-items: center; justify-content: center; color: var(--primary);
}
.notif-center-empty-icon svg { width: 22px; height: 22px; }
.notif-center-empty h4 { font-family: var(--font-display); font-size: 15px; font-weight: 700; margin: 0; color: var(--text); }
.notif-center-empty p { font-size: 12.5px; line-height: 1.55; margin: 0; max-width: 240px; }

/* =========================================================
   Buffering state — a stalled network used to be indistinguishable
   from a dead player: the icon stayed on ❚❚ and the clock simply
   stopped. A slow ring around the transport button says "waiting on
   data" rather than "something broke".
   ========================================================= */
.player-bar .play-btn, .now-playing-controls .play-btn { position: relative; }
.player-bar.is-buffering .play-btn::after,
.now-playing.is-buffering .now-playing-controls .play-btn::after {
  content: ''; position: absolute; inset: -5px; border-radius: 50%;
  border: 2px solid transparent; border-top-color: var(--primary);
  animation: bufferSpin .9s linear infinite;
}
@keyframes bufferSpin { to { transform: rotate(360deg); } }

/* The noscript fallback is the only thing on the page for a visitor
   without JavaScript, so give it enough shape to be readable. */
.noscript-shell {
  max-width: 620px; margin: 0 auto; padding: 60px 24px;
  font-family: var(--font-body); color: var(--text); line-height: 1.65;
}
.noscript-shell h1 { font-size: 30px; margin-bottom: 14px; }
.noscript-shell h2 { font-size: 20px; margin: 22px 0 6px; }
.noscript-shell p { color: var(--text-dim); font-size: 15px; }

/* =========================================================
   Notifications on small screens

   These live at the end of the file on purpose: the notification base
   styles above are the last block in the sheet, so an override written
   up in the Responsive section would lose the cascade to them.
   ========================================================= */
@media (max-width: 760px) {
  /* Cards drop in from the top, where the eye already is on a phone,
     and span the width so a two-line body never has to be truncated to
     a sliver. */
  .notif-stack {
    top: max(10px, env(safe-area-inset-top)); left: 10px; right: 10px; width: auto;
  }
  .notif-card { transform: translateY(-14px) scale(.97); }
  .notif-card.show { transform: translateY(0) scale(1); }
  .notif-card.leaving { transform: translateY(-14px) scale(.97); }

  /* The centre becomes a bottom sheet: reachable with a thumb, and it
     lines up with the bell being in the top bar rather than trying to
     hang off it in a 375px-wide viewport. */
  .notif-center {
    top: auto; bottom: 0; left: 0; right: 0; width: auto;
    max-height: 76dvh; border-radius: 18px 18px 0 0; border-width: 1px 0 0;
    transform-origin: bottom center; transform: translateY(100%);
    padding-bottom: env(safe-area-inset-bottom);
  }
  .notif-center.show { transform: translateY(0); }
  .notif-center-head { padding-top: 16px; }
  .notif-row { padding: 12px 10px; }
}

/* =========================================================
   Playback settings — mix style, loudness target, equaliser
   ========================================================= */
.settings-slider-row.is-muted { opacity: .45; }
.settings-slider-row.is-muted input[type="range"] { cursor: not-allowed; }

.mix-mode-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
  gap: 8px; margin: 12px 0 26px;
}
.mix-mode-btn {
  display: flex; flex-direction: column; gap: 3px; text-align: left;
  background: var(--bg-card); border: 1px solid var(--border); color: var(--text);
  border-radius: var(--radius-sm); padding: 11px 13px; cursor: pointer;
  font-family: inherit; transition: background .2s, border-color .2s;
}
@media (hover: hover) { .mix-mode-btn:hover { background: var(--bg-card-hover); border-color: #2f3648; } }
.mix-mode-btn strong { font-size: 13.5px; font-weight: 700; }
.mix-mode-btn small { font-size: 11.5px; color: var(--text-faint); line-height: 1.45; }
.mix-mode-btn.active { border-color: var(--primary); background: rgba(59, 130, 246, .10); }
.mix-mode-btn.active strong { color: var(--primary); }

/* Segmented control (loudness target, and Settings → Customization's glow
   style / card style / corner roundness / density pickers). `position:
   relative` gives the sliding highlight (.seg-thumb, added by app.js) a
   containing block to be absolutely positioned within. */
.seg-control {
  /* .settings-pane and its sub-groups are `display:flex; flex-direction:
     column`, whose default `align-items: stretch` was overriding this
     element's own `inline-flex` sizing and stretching the whole dark
     background to the pane's full width — with only 2-3 short buttons
     inside, that left a lot of empty background around them. align-self
     opts this one element back out of that stretch, sizing to its
     buttons like inline-flex normally would. */
  align-self: flex-start;
  position: relative;
  display: inline-flex; background: var(--bg-card); border: 1px solid var(--border);
  border-radius: 999px; padding: 3px; gap: 2px; margin: 10px 0 4px;
}
.seg-btn {
  position: relative; z-index: 1;
  background: none; border: none; color: var(--text-dim); font-family: inherit;
  font-size: 12.5px; font-weight: 700; padding: 7px 16px; border-radius: 999px;
  cursor: pointer; transition: background .2s, color .2s;
}
@media (hover: hover) { .seg-btn:hover { color: var(--text); } }
.seg-btn.active { background: var(--primary-grad); color: #fff; }
/* The actual highlight, sized and moved to match the active button —
   app.js measures the active .seg-btn and sets --seg-thumb-w/x on the
   control, so the pill slides and resizes to it instead of two buttons'
   backgrounds instantly swapping in place. Sits behind the buttons
   (z-index 0) and takes over from the plain .seg-btn.active background
   above once JS has positioned it once (.has-seg-thumb). */
.seg-thumb {
  /* Container has padding:3px — inset the thumb by that amount so it
     matches the buttons' actual height instead of filling the padding
     box (which was making it taller than the buttons and flush against
     the container's outer border). */
  position: absolute; top: 3px; bottom: 3px; left: 0; z-index: 0;
  width: var(--seg-thumb-w, 0px); transform: translateX(var(--seg-thumb-x, 0px));
  border-radius: 999px; background: var(--primary-grad); pointer-events: none;
  transition: transform .32s cubic-bezier(.22,1,.36,1), width .32s cubic-bezier(.22,1,.36,1);
}
.seg-control.has-seg-thumb .seg-btn.active { background: none; }

.normalize-readout {
  font-size: 12px; color: var(--text-faint); margin: 8px 0 0; line-height: 1.5;
  font-variant-numeric: tabular-nums;
}

/* ---- Equaliser ---- */
.eq-presets { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0 16px; }
.eq-preset-btn {
  background: var(--bg-card); border: 1px solid var(--border); color: var(--text-dim);
  font-family: inherit; font-size: 12px; font-weight: 600; border-radius: 999px;
  padding: 6px 13px; cursor: pointer; transition: background .2s, color .2s, border-color .2s;
}
@media (hover: hover) { .eq-preset-btn:hover { background: var(--bg-card-hover); color: var(--text); } }
.eq-preset-btn.active { border-color: var(--primary); color: var(--primary); background: rgba(59, 130, 246, .10); }

.eq-bands {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
  gap: 6px; padding: 6px 0 2px;
}
.eq-band { display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 0; }
.eq-band-value {
  font-size: 11px; font-weight: 700; color: var(--text-dim);
  font-variant-numeric: tabular-nums; min-height: 14px;
}
.eq-band-label { font-size: 11px; font-weight: 600; color: var(--text-faint); }
/* Vertical sliders: `writing-mode: vertical-lr` with `direction: rtl` is
   the combination that gives a bottom-to-top range input in every current
   engine (the old `appearance: slider-vertical` is deprecated). */
/* The compound selector is deliberate: the generic `input[type="range"]`
   rule near the top of this file is (0,1,1) and would otherwise beat a
   plain `.eq-band-slider` class, pinning these to a 5px height. */
input[type="range"].eq-band-slider {
  writing-mode: vertical-lr; direction: rtl;
  width: 6px; height: 120px; flex: 0 0 auto; padding: 0;
  background: linear-gradient(to top, var(--border) 0%, var(--border) 100%);
}
input[type="range"].eq-band-slider::-webkit-slider-thumb { opacity: 1; width: 14px; height: 14px; }
input[type="range"].eq-band-slider::-moz-range-thumb { opacity: 1; width: 14px; height: 14px; }
input[type="range"].eq-band-slider::-moz-range-track { background: transparent; width: 6px; }

@media (max-width: 600px) {
  .mix-mode-grid { grid-template-columns: 1fr; margin-bottom: 20px; }
  .eq-bands { gap: 2px; }
  input[type="range"].eq-band-slider { height: 104px; }
  .eq-band-label, .eq-band-value { font-size: 10px; }
  .seg-control { width: 100%; justify-content: stretch; }
  .seg-btn { flex: 1; padding: 9px 8px; }
  /* Settings → Customization's pickers (glow style, card style, corner
     roundness, density) were inheriting the full-width stretch above,
     which left their background pill much wider than the 2-3 short
     buttons inside it — mostly empty space. These should size to their
     buttons like on desktop, not fill the row. */
  #settingsPaneCustomization .seg-control { width: auto; justify-content: flex-start; }
  #settingsPaneCustomization .seg-btn { flex: none; padding: 7px 16px; }
}

/* Lyrics-search fallback banner (Search view) */
.search-lyrics-note {
  font-size: 12.5px; color: var(--text-dim); margin: 0 0 14px;
  background: rgba(59, 130, 246, .08); border: 1px solid rgba(59, 130, 246, .22);
  border-radius: var(--radius-sm); padding: 9px 12px; line-height: 1.5;
}

/* =========================================================
   Mini player — refreshed
   The card was a grey box with a thumbnail, a title and three
   buttons. It now carries the track's own artwork as a blurred
   backdrop, a header that says what it is, the like/share actions
   the main bar has, a time readout, and shuffle/repeat — so it is a
   real player rather than a cut-down remote.
   ========================================================= */
.mini-player { padding: 0; overflow: hidden; }
.mini-player-bg {
  position: absolute; inset: -30%;
  background-size: cover; background-position: center;
  filter: blur(34px) brightness(.42) saturate(1.25);
  transform: scale(1.1);
  transition: background-image .4s ease;
  pointer-events: none;
}
/* A dark wash over the artwork so text keeps its contrast whatever
   the cover happens to be. */
.mini-player-bg::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(160deg, rgba(10, 12, 17, .72), rgba(10, 12, 17, .92));
}
.mini-player-inner {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; gap: 11px;
  padding: 12px 14px 14px;
}
.mini-player-head { display: flex; align-items: center; justify-content: space-between; }
.mini-player-kind {
  font-size: 10px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  color: rgba(255, 255, 255, .5);
}
.mini-player .mini-player-expand {
  position: static; width: 26px; height: 26px; color: rgba(255, 255, 255, .6);
}
@media (hover: hover) { .mini-player .mini-player-expand:hover { color: #fff; background: rgba(255, 255, 255, .12); } }
.mini-player-top { padding-right: 0; }
.mini-player-meta { flex: 1; min-width: 0; }
.mini-player-like { flex: 0 0 auto; width: 30px; height: 30px; color: rgba(255, 255, 255, .6); }
@media (hover: hover) { .mini-player-like:hover { color: #fff; } }
.mini-player-like.liked { color: var(--primary); }
.mini-player-like svg { width: 16px; height: 16px; }
.mini-player-cover { box-shadow: 0 8px 20px rgba(0, 0, 0, .45); }
.mini-player-title { color: #fff; }
.mini-player-artist { color: rgba(255, 255, 255, .55); }
.mini-player-times {
  display: flex; justify-content: space-between; margin-top: 5px;
  font-size: 10.5px; font-weight: 600; color: rgba(255, 255, 255, .45);
  font-variant-numeric: tabular-nums;
}
.mini-player-controls { gap: 6px; }
.mini-player-controls .icon-btn { color: rgba(255, 255, 255, .6); width: 28px; height: 28px; position: relative; }
@media (hover: hover) { .mini-player-controls .icon-btn:hover { color: #fff; background: rgba(255, 255, 255, .12); } }
.mini-player-controls .icon-btn.active { color: var(--primary); }
.mini-player-controls .icon-btn svg { width: 17px; height: 17px; }
.mini-player-controls .play-btn { width: 38px; height: 38px; }
.mini-player-controls .play-btn svg { width: 32px; height: 32px; }
.mini-player-seek input[type="range"] { height: 4px; }
.mini-player-seek input[type="range"]::-webkit-slider-thumb { opacity: 1; width: 10px; height: 10px; }
.mini-player-seek input[type="range"]::-moz-range-thumb { opacity: 1; width: 10px; height: 10px; }
/* In a Picture-in-Picture window the card fills the whole frame. */
.mini-player.pip-mode .mini-player-inner { flex: 1; justify-content: center; padding: 16px 18px; }

/* =========================================================
   Follow artist
   ========================================================= */
/* "Following" turns into "Unfollow" on hover, so the button says what
   pressing it will do rather than what the current state is.

   The labels are two real, stacked elements that crossfade. The previous
   version collapsed the label with `font-size: 0` and drew the
   replacement in a ::after — neither of which can be transitioned, so
   the swap was an instant snap while the colours around it eased, and
   catching the button with the pointer produced a visible flicker. */
.follow-artist-btn {
  min-width: 112px; justify-content: center; position: relative; overflow: hidden;
  transition: background .28s ease, border-color .28s ease, color .28s ease, filter .25s;
}
.follow-artist-btn .follow-label {
  display: block; transition: opacity .24s ease, transform .24s ease;
}
.follow-artist-btn .follow-label-hover {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  opacity: 0; transform: translateY(45%);
  color: #fca5a5;
}
.follow-artist-btn.is-following {
  background: rgba(59, 130, 246, .12);
  border-color: var(--primary);
  color: var(--primary);
}
.follow-artist-btn.is-following:focus-visible {
  background: rgba(239, 68, 68, .12);
  border-color: rgba(239, 68, 68, .5);
}
@media (hover: hover) { .follow-artist-btn.is-following:hover {
  background: rgba(239, 68, 68, .12);
  border-color: rgba(239, 68, 68, .5);
} }
.follow-artist-btn.is-following:focus-visible .follow-label-state {
  opacity: 0; transform: translateY(-45%);
}
@media (hover: hover) { .follow-artist-btn.is-following:hover .follow-label-state {
  opacity: 0; transform: translateY(-45%);
} }
.follow-artist-btn.is-following:focus-visible .follow-label-hover {
  opacity: 1; transform: translateY(0);
}
@media (hover: hover) { .follow-artist-btn.is-following:hover .follow-label-hover {
  opacity: 1; transform: translateY(0);
} }

/* =========================================================
   Entrance animations
   Detail views used to appear fully formed the instant their data
   arrived. Staggering the hero — cover, kind, title, meta — makes
   the page look like it is assembling rather than blinking.
   ========================================================= */
@keyframes heroRise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}
@keyframes heroCoverIn {
  from { opacity: 0; transform: translateY(16px) scale(.94); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
.hero-animate .playlist-hero-cover { animation: heroCoverIn .5s cubic-bezier(.22,1,.36,1) both; }
.hero-animate .playlist-hero-kind  { animation: heroRise .45s cubic-bezier(.22,1,.36,1) both; animation-delay: .06s; }
.hero-animate .playlist-hero-info h1 { animation: heroRise .5s cubic-bezier(.22,1,.36,1) both; animation-delay: .10s; }
.hero-animate .playlist-hero-owner { animation: heroRise .5s cubic-bezier(.22,1,.36,1) both; animation-delay: .14s; }
.hero-animate .playlist-hero-sub   { animation: heroRise .5s cubic-bezier(.22,1,.36,1) both; animation-delay: .17s; }
.hero-animate .playlist-toolbar    { animation: heroRise .5s cubic-bezier(.22,1,.36,1) both; animation-delay: .22s; }

/* 13) The greeting arrives a word at a time. */
@keyframes greetingWordIn {
  from { opacity: 0; transform: translateY(14px) rotate(1.5deg); filter: blur(4px); }
  to   { opacity: 1; transform: translateY(0) rotate(0); filter: blur(0); }
}
.greeting-word {
  display: inline-block;
  animation: greetingWordIn .55s cubic-bezier(.22,1,.36,1) both;
}

/* Copy-link buttons. Named for what they do — they put the track's URL
   on the clipboard; nothing is shared anywhere. */
.player-copy-btn { color: var(--text-faint); }
.now-playing-copy-btn { color: var(--text-faint); }
@media (hover: hover) { .player-copy-btn:hover { color: var(--text); background: var(--bg-card); } .now-playing-copy-btn:hover { color: var(--text); background: var(--bg-card); } }
.player-copy-btn svg { width: 17px; height: 17px; }
.now-playing-copy-btn svg { width: 19px; height: 19px; }

@media (max-width: 760px) {
  /* Room on the compact bar is already tight; copying lives in the
     full-screen player, where the same button is one tap away. */
  .player-copy-btn { display: none; }
  .mini-player-controls #miniPlayerShuffleBtn,
  .mini-player-controls #miniPlayerRepeatBtn { display: none; }
}
/* =========================================================
   Settings → Customization tab
   Accent color, ambient "Aurora" glow, card style, corner
   roundness and interface density. Everything here reacts live
   off --primary / --primary-dark / --primary-grad, the radius
   tokens, and the body classes .aurora-on / .density-compact —
   see app.js applyCustomization().
   ========================================================= */

/* "PRO" flair on the Customization tab — purely cosmetic for now
   (the feature is fully unlocked), a placeholder for a future
   subscription gate. */
.settings-tab-pro {
  margin-left: auto; flex: 0 0 auto; background: var(--primary-grad); color: #fff;
  font-size: 9.5px; font-weight: 800; letter-spacing: .06em; border-radius: 5px;
  padding: 2px 5px; line-height: 1.5;
}
.settings-mobile .settings-tab-pro { margin-left: 6px; }
.custom-section-pro { margin-left: 8px; vertical-align: middle; }

.custom-section-head { margin: 6px 0 -6px; }
.custom-section-head h3 { font-size: 15px; font-weight: 700; display: flex; align-items: center; }
.custom-section-head p { margin: 4px 0 0; font-size: 12px; color: var(--text-dim); line-height: 1.55; }
.custom-option-icon { display: inline-flex; margin-right: 7px; color: var(--primary); vertical-align: -3px; }
.custom-option-icon svg { width: 16px; height: 16px; display: block; }

/* ---- Live preview card ---- */
.custom-preview {
  position: relative; overflow: hidden; isolation: isolate;
  background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-lg);
  padding: 20px; display: flex; flex-direction: column; gap: 16px;
}
.custom-preview-glow {
  position: absolute; z-index: -1; inset: -40% -20% auto -20%; height: 220px;
  background: var(--primary-grad); opacity: .28; filter: blur(46px);
  transition: background 1.4s cubic-bezier(.22,1,.36,1);
  animation: customPreviewDrift 9s ease-in-out infinite alternate;
}
@keyframes customPreviewDrift {
  from { transform: translate(-4%, -6%) scale(1); }
  to   { transform: translate(4%, 4%) scale(1.08); }
}
.custom-preview-song {
  display: flex; align-items: center; gap: 14px; background: var(--bg-elevated);
  border: 1px solid var(--border-soft); border-radius: var(--radius-md); padding: 12px 14px;
}
.custom-preview-cover {
  flex: 0 0 auto; width: 46px; height: 46px; border-radius: var(--radius-sm); background: var(--primary-grad);
  display: flex; align-items: center; justify-content: center;
  transition: background 1.4s cubic-bezier(.22,1,.36,1);
}
.custom-preview-cover .eq-indicator { display: flex; background: none; position: static; }
.custom-preview-song-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.custom-preview-song-text strong { font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.custom-preview-song-text span { font-size: 11.5px; color: var(--text-dim); }
.custom-preview-btn { pointer-events: none; flex: 0 0 auto; transition: background 1.4s cubic-bezier(.22,1,.36,1); }

/* ---- Accent color swatch grid ---- */
.accent-swatch-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; }
.accent-swatch {
  position: relative; aspect-ratio: 1; border-radius: 50%; border: none; cursor: pointer; padding: 0;
  background: var(--swatch-grad); box-shadow: inset 0 0 0 2px rgba(255,255,255,.08);
  transition: transform .18s cubic-bezier(.34,1.56,.64,1);
}
@media (hover: hover) { .accent-swatch:hover { transform: scale(1.08); } }
.accent-swatch::after {
  content: ''; position: absolute; inset: -4px; border-radius: 50%; border: 2px solid transparent;
  transition: border-color .2s ease;
}
.accent-swatch.active::after { border-color: var(--text); }
.accent-swatch-check {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  color: #fff; opacity: 0; transform: scale(.6); filter: drop-shadow(0 1px 2px rgba(0,0,0,.5));
  transition: opacity .18s ease, transform .18s cubic-bezier(.34,1.56,.64,1);
}
.accent-swatch-check svg { width: 16px; height: 16px; }
.accent-swatch.active .accent-swatch-check { opacity: 1; transform: scale(1); }
.accent-swatch.accent-swatch-custom {
  background: var(--bg-elevated); box-shadow: inset 0 0 0 2px var(--border);
  display: flex; align-items: center; justify-content: center; color: var(--text-dim);
}
.accent-swatch.accent-swatch-custom svg { width: 17px; height: 17px; }
.accent-swatch.accent-swatch-custom.active { box-shadow: inset 0 0 0 2px var(--text); color: var(--text); }

/* ---- Prism custom-color palette (Settings → Customization → Glow style → Prism) ----
   Swatches scroll horizontally in their own row so the palette never wraps
   into a ragged multi-line block; the "+" add button and the pending-color
   confirm/cancel controls stay pinned at the end, outside the scroll area. */
.prism-palette { display: flex; align-items: center; gap: 12px; margin-top: 2px; flex-wrap: wrap; }
.prism-palette-swatches {
  display: flex; align-items: center; gap: 12px; flex: 1 1 auto; min-width: 0;
  overflow-x: auto; overflow-y: hidden; padding: 6px 2px; scrollbar-width: thin;
}
.prism-palette-swatches::-webkit-scrollbar { height: 5px; }
.prism-palette-swatches::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; }
.prism-color-chip {
  position: relative; width: 32px; height: 32px; border-radius: 50%; flex: 0 0 auto;
  box-shadow: inset 0 0 0 2px rgba(255,255,255,.12), 0 2px 6px rgba(0,0,0,.25);
  transition: transform .18s cubic-bezier(.34,1.56,.64,1);
}
@media (hover: hover) { .prism-color-chip:hover { transform: scale(1.08); } }
.prism-color-chip-remove {
  position: absolute; top: -5px; right: -5px; width: 17px; height: 17px; border-radius: 50%;
  background: var(--bg-elevated); border: 1px solid var(--border); color: var(--text-dim);
  display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0;
  transition: color .2s, background .2s, border-color .2s;
}
@media (hover: hover) { .prism-color-chip-remove:hover:not(:disabled) { color: #fff; background: #ef4444; border-color: #ef4444; } }
.prism-color-chip-remove svg { width: 9px; height: 9px; }
.prism-color-chip-remove:disabled { opacity: .3; cursor: not-allowed; }

/* Add-color button — a distinct dashed "outline" circle (rather than a
   filled swatch) so it reads immediately as "add new" instead of looking
   like just another color in the row, with a clear hover state that
   picks up the current accent to invite the click. */
.prism-palette-add {
  width: 34px; height: 34px; border-radius: 50%; flex: 0 0 auto; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: 1.5px dashed var(--border); color: var(--text-faint);
  transition: color .2s, border-color .2s, background .2s, transform .18s cubic-bezier(.34,1.56,.64,1);
}
@media (hover: hover) {
  .prism-palette-add:hover {
    color: var(--primary); border-color: var(--primary); border-style: solid;
    background: color-mix(in srgb, var(--primary) 12%, transparent); transform: scale(1.08);
  }
}
.prism-palette-add-icon { display: flex; }
.prism-palette-add-icon svg { display: block; width: 15px; height: 15px; }

/* The color you're currently picking, held here until you confirm it —
   nothing is added to the cycle above until "Add" is clicked. Redone as
   a small elevated card with a labeled swatch and clearly separated
   Add/Cancel actions, instead of a cramped unlabeled pill. */
.prism-palette-pending {
  display: flex; align-items: center; gap: 10px; flex: 0 0 auto;
  padding: 6px 8px 6px 6px; border-radius: 999px;
  background: var(--bg-elevated); box-shadow: inset 0 0 0 1px var(--border), 0 4px 14px rgba(0,0,0,.25);
  animation: authFormIn .18s cubic-bezier(.22,1,.36,1);
}
.prism-pending-chip {
  width: 28px; height: 28px; border-radius: 50%; flex: 0 0 auto;
  box-shadow: inset 0 0 0 2px rgba(255,255,255,.18), 0 0 0 3px var(--bg-elevated);
}
.prism-pending-label { font-size: 12px; font-weight: 600; color: var(--text-dim); white-space: nowrap; }
.prism-pending-actions { display: flex; align-items: center; gap: 6px; }
.prism-pending-btn {
  height: 27px; border: none; cursor: pointer; padding: 0 12px; flex: 0 0 auto;
  display: flex; align-items: center; justify-content: center; gap: 5px; border-radius: 999px;
  font-size: 12px; font-weight: 700; font-family: inherit; line-height: 1;
  transition: background .2s, color .2s, filter .2s, transform .15s;
}
/* `display: block` drops the icon out of inline text flow, which is what
   was leaving it a couple of pixels smaller/lower than the "Add" label —
   an inline <svg> keeps its font-metrics descender gap even inside a flex
   item, so it never quite matched the text's line. Block removes that gap
   so both sit on the same line, same size. */
.prism-pending-btn svg { display: block; width: 12px; height: 12px; }
.prism-pending-confirm { background: var(--primary-grad); color: #fff; }
@media (hover: hover) { .prism-pending-confirm:hover { filter: brightness(1.1); } }
.prism-pending-confirm:active { transform: scale(.96); }
/* Was missing `justify-content: center` from the base rule's perspective —
   with a fixed 27px width and a single icon child, the icon (sized well
   under 27px) sat flush left instead of centered. */
.prism-pending-cancel { width: 27px; padding: 0; background: transparent; color: var(--text-dim); }
@media (hover: hover) { .prism-pending-cancel:hover { color: var(--text); background: var(--bg-card-hover); } }

.visually-hidden-input {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}

.link-btn {
  background: none; border: none; padding: 0; color: var(--primary); font-weight: 700;
  font-size: inherit; cursor: pointer; text-decoration: underline; font-family: inherit;
}
.settings-note-muted { color: var(--text-dim); }
.custom-actions { display: flex; justify-content: center; margin-top: -4px; }

/* ---- Customization lock overlay (logged out) ---- */
#settingsPaneCustomization { position: relative; }
#settingsPaneCustomization.locked > *:not(.custom-lock-overlay) { opacity: .32; filter: saturate(.5); pointer-events: none; user-select: none; }
.custom-lock-overlay {
  position: absolute; inset: -6px; z-index: 5; border-radius: var(--radius-md);
  display: flex; align-items: center; justify-content: center; padding: 20px;
  background: rgba(8,9,13,.6); backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px);
}
.custom-lock-card {
  max-width: 320px; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 6px;
  background: var(--bg-card); border: 1px solid var(--border-soft); border-radius: var(--radius-md);
  padding: 24px 22px; box-shadow: 0 12px 32px rgba(0,0,0,.4);
}
.custom-lock-icon {
  width: 42px; height: 42px; border-radius: 50%; margin-bottom: 6px;
  display: flex; align-items: center; justify-content: center;
  background: var(--bg-elevated); color: var(--primary);
}
.custom-lock-icon svg { width: 20px; height: 20px; }
.custom-lock-card h3 { font-size: 16px; }
.custom-lock-card p { font-size: 13px; color: var(--text-dim); margin: 0 0 6px; line-height: 1.5; }
.seg-btn:disabled { opacity: .5; cursor: not-allowed; }

@media (max-width: 480px) {
  .accent-swatch-grid { grid-template-columns: repeat(5, 1fr); }
}

/* =========================================================
   Aurora glow — ambient animated color behind the whole app,
   only visible through the gaps the "floating panel" layout
   already leaves around the navbar / sidebar / content, so it
   never sits behind anything that needs to stay readable.

   Three selectable patterns (Settings → Customization → Glow
   style), all gated by body.aurora-on so the master switch still
   turns the whole effect off regardless of which is picked:
     .glow-aurora  (default) — three soft drifting blobs
     .glow-orbit               — one slow-spinning conic ring
     .glow-prism                — the blobs, cycling through hue
   ========================================================= */
.aurora-bg {
  position: fixed; inset: 0; z-index: 0; overflow: hidden; pointer-events: none;
  opacity: 0; transition: opacity 1.1s ease;
}
body.aurora-on .aurora-bg { opacity: 1; }
.aurora-bg i {
  position: absolute; display: block; border-radius: 50%; filter: blur(70px);
  background: var(--primary-grad); opacity: .5; font-style: normal;
  transition: background 1.4s cubic-bezier(.22,1,.36,1), opacity 1s ease;
}
.aurora-bg i:nth-child(1) { width: 46vmax; height: 46vmax; top: -18vmax; left: -14vmax; animation: auroraDrift1 26s ease-in-out infinite alternate; }
.aurora-bg i:nth-child(2) { width: 38vmax; height: 38vmax; bottom: -16vmax; right: -10vmax; opacity: .38; animation: auroraDrift2 32s ease-in-out infinite alternate; }
.aurora-bg i:nth-child(3) { width: 30vmax; height: 30vmax; bottom: 10vmax; left: 20vmax; opacity: .3; animation: auroraDrift3 22s ease-in-out infinite alternate; }
@keyframes auroraDrift1 { from { transform: translate(0,0) scale(1); } to { transform: translate(6vmax, 5vmax) scale(1.15); } }
@keyframes auroraDrift2 { from { transform: translate(0,0) scale(1); } to { transform: translate(-5vmax, -6vmax) scale(1.1); } }
@keyframes auroraDrift3 { from { transform: translate(0,0) scale(1); } to { transform: translate(4vmax, -4vmax) scale(1.2); } }

/* Orbit — the three blobs fade out and a single large, slowly-rotating
   conic-gradient ring fades in, blurred into a soft halo. The ring is
   always in the DOM (content: '' isn't gated behind .glow-orbit) so
   switching styles crossfades between the two instead of the ring
   popping in/out with no transition to animate. */
.aurora-bg::before {
  content: ''; position: absolute; inset: -25%;
  background: conic-gradient(from 0deg, var(--primary) 0deg, transparent 70deg, var(--primary) 160deg, transparent 250deg, var(--primary) 320deg, transparent 360deg);
  filter: blur(90px); opacity: 0;
  transition: opacity 1s ease, background 1.4s cubic-bezier(.22,1,.36,1);
  animation: auroraOrbitSpin 46s linear infinite;
}
body.glow-orbit .aurora-bg i { opacity: 0; }
body.glow-orbit .aurora-bg::before { opacity: .55; }
@keyframes auroraOrbitSpin { to { transform: rotate(360deg); } }

/* Prism — keeps the drifting blobs but cycles their hue continuously,
   so the glow sweeps through the color wheel instead of sitting still
   on one accent. */
body.glow-prism .aurora-bg { animation: auroraPrismHue 16s linear infinite; }
@keyframes auroraPrismHue { from { filter: hue-rotate(0deg) saturate(1.3); } to { filter: hue-rotate(360deg) saturate(1.3); } }

/* Prism · Custom colors — instead of an automatic rainbow sweep, the
   blobs cycle through the user's own chosen palette. app.js drives
   --prism-color on a timer; the @property transition on it (see :root)
   is what makes each switch fade instead of snap. */
body.glow-prism.prism-custom .aurora-bg { animation: none; }
body.glow-prism.prism-custom .aurora-bg i { background: var(--prism-color); }

/* Pulse — the glow locked to the tempo of whatever's currently playing.
   Unlike the other patterns this one isn't driven by CSS @keyframes at
   all: a rAF loop in app.js (pulseTick/writePulseFrame) writes each
   blob's transform + opacity directly, every frame, off the real
   playback position — each blob at its own frequency/phase (see
   PULSE_BLOBS) so across a cycle every blob visits left, right, up
   *and* down on its own, not all three sliding along one diagonal.
   Two values are shared through custom properties for the rules below
   that can't be reached directly from JS —
     --pulse-punch  0..1, a sharp attack-decay envelope: spikes right on
                    each beat, decays before the next (the "kick")
     --pulse-hue    a full hue-rotate sweep every 8 beats, so a faster
                    song's color cycles visibly faster than a slow one
   While paused, or before a track's tempo is known, app.js feeds this a
   slow, punch-free ambient clock instead of the real beat grid — see the
   three branches in pulseTick — so the glow never keeps "beating" for a
   song that isn't actually sounding. */
body.glow-pulse .aurora-bg { animation: none; }
body.glow-pulse .aurora-bg::before { opacity: 0; }
body.glow-pulse .aurora-bg i {
  animation: none;
  transition: background 1.4s cubic-bezier(.22,1,.36,1);
  filter: blur(70px) hue-rotate(var(--pulse-hue, 0deg)) saturate(calc(1.25 + var(--pulse-punch, 0) * .35));
  will-change: transform, opacity;
}
/* A soft flash that blooms behind the blobs on every beat — the one cue
   that reads as "reacting to the music" even out of the corner of your
   eye, independent of wherever the blobs themselves currently are. */
body.glow-pulse .aurora-bg::after {
  content: ''; position: absolute; inset: -10%; z-index: 0;
  background: radial-gradient(circle at 50% 42%, var(--primary) 0%, transparent 62%);
  opacity: calc(var(--pulse-punch, 0) * .3);
  transform: scale(calc(1 + var(--pulse-punch, 0) * .22));
  mix-blend-mode: screen;
  transition: background 1.4s cubic-bezier(.22,1,.36,1);
}

@media (prefers-reduced-motion: reduce) {
  .aurora-bg i, .aurora-bg::before { animation: none; }
  body.glow-prism .aurora-bg { animation: none; }
  body.glow-pulse .aurora-bg i { transform: none; filter: blur(70px); }
  body.glow-pulse .aurora-bg::after { opacity: 0; }
}
.layout { position: relative; z-index: 1; }

/* =========================================================
   Card style — "Glass" swaps the flat card/row background for a
   translucent, frosted panel so the Aurora glow (and whatever's
   behind it) shows through. Falls back cleanly to the solid look
   in browsers without color-mix()/backdrop-filter support.
   ========================================================= */
body.card-style-glass .song-card,
body.card-style-glass .artist-card,
body.card-style-glass .custom-preview-song {
  background: color-mix(in srgb, var(--bg-card) 52%, transparent);
  backdrop-filter: blur(18px) saturate(1.4);
  -webkit-backdrop-filter: blur(18px) saturate(1.4);
  border-color: rgba(255,255,255,.09);
}
body.card-style-glass .custom-preview {
  background: color-mix(in srgb, var(--bg-card) 42%, transparent);
  backdrop-filter: blur(24px) saturate(1.4);
  -webkit-backdrop-filter: blur(24px) saturate(1.4);
}
body.card-style-glass .track-row.playing {
  background: color-mix(in srgb, var(--primary) 14%, transparent);
}
@media (hover: hover) {
  body.card-style-glass .song-card:hover { background: color-mix(in srgb, var(--bg-card-hover) 65%, transparent); }
  body.card-style-glass .track-row:hover { background: color-mix(in srgb, var(--bg-card) 60%, transparent); }
}

/* =========================================================
   Corner roundness — re-points the three radius tokens every
   card/button/panel in the app already draws from, so one pick
   changes the whole app's silhouette at once.
   ========================================================= */
body.radius-sharp { --radius-lg: 6px; --radius-md: 5px; --radius-sm: 4px; }
body.radius-pill  { --radius-lg: 28px; --radius-md: 22px; --radius-sm: 18px; }

/* =========================================================
   Interface density — with "Compact":
     1. Home's "All tracks" section swaps from the card grid to a
        Liked Songs/playlist-style track list (see renderHomeGrid()
        in app.js, which toggles #homeGrid between .song-grid and
        .track-list) — and that list's rows drop their track-number
        column (the play glyph is the at-rest affordance instead of
        only appearing on hover, so a row is still obviously
        clickable without a number).
     2. The sidebar's nav tabs (Home/Search/Artists/etc.) tighten
        their padding and font size to fit more of the library on
        screen at once.
   Everything else — search results, artist/shelf grids, playlist
   heroes, every other track list in the app — always looks like
   "Comfortable" and ignores this setting entirely. Both effects
   ease in/out (see the transition on .sidebar-link and on
   .track-index-num/.track-row-play-icon) rather than snapping, to
   match the rest of Customization's live-preview feel.
   ========================================================= */
body.density-compact #homeGrid.track-list .track-index-num {
  opacity: 0; transform: scale(.5) rotate(12deg);
}
body.density-compact #homeGrid.track-list .track-row-play-icon {
  opacity: .55; transform: scale(1) rotate(0deg);
}
body.density-compact #homeGrid.track-list .track-row:hover:not(.playing) .track-row-play-icon,
body.density-compact #homeGrid.track-list .track-row:focus-within:not(.playing) .track-row-play-icon {
  opacity: 1;
}
body.density-compact .sidebar-link { padding: 7px 12px; font-size: 13.5px; }

/* The row that used to overlap the profile banner is now a plain
   avatar + name row, like any other hero (playlist/album/artist),
   just with the avatar circular instead of square.
   Previously this block re-declared its own flex-direction, gap and
   avatar size at its own private breakpoints, completely separate from
   the ones .playlist-hero / .playlist-hero-cover use — so the profile
   header drifted out of sync with every other hero: a smaller avatar,
   extra side padding that nudged it out of alignment with the
   "Playlists" heading below it, and a fixed row layout that (unlike the
   playlist hero) never switched to a stacked layout on narrow screens.
   The avatar element already carries the .playlist-hero-cover class
   too, and this row already carries .playlist-hero — so removing the
   duplicate/overriding rules here lets the avatar, gaps and breakpoints
   fall back to the exact same ones the playlist/album hero uses. This
   block now only adds what's genuinely different about a profile: the
   circular border/shadow treatment, a deliberately larger size than a
   playlist cover (a face reads fine bigger than square artwork does),
   and spacing for the extra activity/actions rows a playlist hero
   doesn't have. */
.profile-hero-row .profile-hero-avatar {
  width: 180px; height: 180px;
  border: 4px solid var(--bg); box-shadow: 0 6px 20px rgba(0,0,0,.45);
  background-color: var(--bg-elevated);
}
.profile-hero-row .playlist-hero-info h1 {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.profile-hero-row .profile-hero-activity { margin-top: 6px; }
.profile-hero-row .profile-hero-actions { margin-top: 8px; }

@media (max-width: 760px) {
  .profile-hero-row .profile-hero-avatar { width: 140px; height: 140px; border-width: 3px; }
}
@media (max-width: 600px) {
  /* Matches the playlist hero's own stacked layout at this width —
     only the size is profile-specific, kept proportionally larger
     than the playlist cover's 100px here. */
  .profile-hero-row .profile-hero-avatar { width: 128px; height: 128px; }
  .profile-hero-row .playlist-hero-info h1 { white-space: normal; }
}
@media (max-width: 360px) {
  .profile-hero-row .profile-hero-avatar { width: 108px; height: 108px; border-width: 2px; }
}
/* ---- Settings → Music Profile ---- */
.mp-stats-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
}
.mp-stat-card {
  display: flex; align-items: center; gap: 12px;
  padding: 16px; background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-md);
}
.mp-stat-icon {
  width: 40px; height: 40px; flex: 0 0 auto; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--primary-grad); color: #fff;
}
.mp-stat-icon svg { width: 19px; height: 19px; }
.mp-stat-text { min-width: 0; }
.mp-stat-text strong { display: block; font-family: var(--font-display); font-size: 19px; font-weight: 700; color: var(--text); line-height: 1.2; }
.mp-stat-text p { margin: 3px 0 0; font-size: 12px; color: var(--text-dim); }

.mp-section { display: flex; flex-direction: column; gap: 10px; }
.mp-section-title {
  display: flex; align-items: center; gap: 8px;
  margin: 0; font-family: var(--font-display); font-size: 15px; font-weight: 700; color: var(--text);
}
.mp-section-icon { display: flex; color: var(--primary); }
.mp-section-icon svg { width: 17px; height: 17px; }

.mp-list { display: flex; flex-direction: column; gap: 4px; }
.mp-song-row, .mp-artist-row {
  display: flex; align-items: center; gap: 12px;
  padding: 8px 10px; border-radius: var(--radius-sm); cursor: pointer;
  transition: background .15s;
}
.mp-song-row:hover, .mp-artist-row:hover { background: var(--bg-card); }
.mp-song-rank {
  width: 20px; flex: 0 0 auto; text-align: center;
  font-size: 13px; font-weight: 700; color: var(--text-faint);
}
.mp-song-cover, .mp-artist-photo {
  width: 42px; height: 42px; flex: 0 0 auto;
  background-color: var(--bg-elevated); background-size: cover; background-position: center;
  display: flex; align-items: center; justify-content: center; color: var(--text-faint);
}
.mp-song-cover { border-radius: 6px; }
.mp-artist-photo { border-radius: 50%; }
.mp-song-cover svg, .mp-artist-photo svg { width: 18px; height: 18px; }
.mp-song-text { flex: 1 1 auto; min-width: 0; }
.mp-song-title { font-size: 14px; font-weight: 600; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mp-song-artist { font-size: 12.5px; color: var(--text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
.mp-song-plays { flex: 0 0 auto; font-size: 12px; font-weight: 600; color: var(--text-dim); white-space: nowrap; }

@media (max-width: 620px) {
  .mp-stats-grid { grid-template-columns: 1fr; }
}

/* ---- Settings → Achievements ---- */
.ach-intro { color: var(--text-dim); font-size: 13px; line-height: 1.6; margin: 0; }

.ach-grid {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px;
}
@media (max-width: 760px) { .ach-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 460px) { .ach-grid { grid-template-columns: 1fr; } }

.ach-card {
  position: relative; display: flex; flex-direction: column; gap: 10px;
  background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: 18px 16px 16px; cursor: pointer; overflow: hidden; text-align: left;
  transition: transform .35s cubic-bezier(.22,1,.36,1), opacity .5s ease, filter .5s ease,
              border-color .3s ease, box-shadow .3s ease;
  animation: achCardIn .5s cubic-bezier(.22,1,.36,1) both;
}
@keyframes achCardIn { from { opacity: 0; transform: translateY(10px) scale(.97); } to { opacity: 1; transform: none; } }

@media (hover: hover) {
  .ach-card:not(.locked):hover {
    transform: translateY(-3px); border-color: var(--border-soft);
    box-shadow: 0 16px 34px -18px rgba(0,0,0,.55);
  }
}
.ach-card.locked { opacity: .42; filter: grayscale(.55); cursor: default; }
@media (hover: hover) { .ach-card.locked:hover { opacity: .62; } }

/* One-time pop when a card flips from locked to unlocked and jumps to
   the front of the list — see promoteUnlockedCard() in app.js. */
.ach-card.just-unlocked { animation: achCardIn .5s cubic-bezier(.22,1,.36,1) both, achUnlockPop .7s cubic-bezier(.22,1.6,.36,1) .05s both; }
@keyframes achUnlockPop {
  0% { box-shadow: 0 0 0 0 rgba(255,255,255,0); }
  35% { box-shadow: 0 0 0 8px rgba(255,255,255,.08), 0 16px 34px -18px rgba(0,0,0,.55); }
  100% { box-shadow: 0 16px 34px -18px rgba(0,0,0,.55); }
}

.ach-card-icon-wrap {
  width: 52px; height: 52px; border-radius: 50%; flex: 0 0 auto; color: #fff;
  display: flex; align-items: center; justify-content: center;
  background: var(--ach-grad, var(--primary-grad));
  box-shadow: 0 10px 22px -10px var(--ach-color2, rgba(59,130,246,.55));
}
.ach-card-icon-wrap svg { width: 30px; height: 30px; }
.ach-card.locked .ach-card-icon-wrap { background: var(--bg-elevated); color: var(--text-faint); box-shadow: none; }

.ach-card-label { font-family: var(--font-display); font-size: 15px; font-weight: 700; color: var(--text); margin: 0; }
.ach-card-desc { font-size: 12.5px; color: var(--text-dim); margin: 0; line-height: 1.5; }
.ach-card-period {
  display: inline-block; align-self: flex-start; font-size: 10.5px; font-weight: 800;
  color: var(--ach-color1, var(--primary)); text-transform: uppercase; letter-spacing: .05em;
  background: color-mix(in srgb, var(--ach-color1, var(--primary)) 16%, transparent);
  border-radius: 999px; padding: 3px 9px;
}
.ach-card-date { font-size: 11.5px; color: var(--text-faint); margin-top: -2px; }

.ach-card-progress-track { height: 5px; border-radius: 3px; background: var(--bg-elevated); overflow: hidden; margin-top: auto; }
.ach-card-progress-fill { height: 100%; border-radius: 3px; background: var(--ach-grad, var(--primary-grad)); transition: width .6s cubic-bezier(.22,1,.36,1); }
.ach-card-progress-label { font-size: 11px; color: var(--text-faint); margin: 0; }

.ach-card-lock-icon { position: absolute; top: 14px; right: 14px; color: var(--text-faint); display: flex; }
.ach-card-lock-icon svg { width: 15px; height: 15px; }

.empty-block.ach-empty { margin-top: 4px; }

/* ---- Achievement certificate modal ---- */
.ach-modal {
  text-align: center; padding-top: 38px;
  --ach-color1: var(--primary); --ach-color2: var(--primary-dark);
  --ach-grad: linear-gradient(135deg, var(--ach-color1), var(--ach-color2));
}
.ach-modal-badge {
  position: relative; width: 84px; height: 84px; margin: 6px auto 20px; border-radius: 26px;
  display: flex; align-items: center; justify-content: center; color: #fff;
  background: var(--ach-grad);
  box-shadow: 0 16px 36px -12px color-mix(in srgb, var(--ach-color1) 60%, transparent),
              inset 0 0 0 1px rgba(255,255,255,.12);
  animation: achBadgeIn .5s cubic-bezier(.22,1.4,.36,1);
}
/* Soft color-matched glow behind the badge — z-index below the badge's
   own box so it doesn't compete with the icon, and it's a plain scale
   (no rotate) after the backup-email badge's rotate turned out to read
   as a "shake" — see the .backup-email-modal-badge note above. */
.ach-modal-badge::before {
  content: ''; position: absolute; inset: -14px; z-index: -1; border-radius: 34px;
  background: var(--ach-grad); opacity: .35; filter: blur(20px);
}
.ach-modal-badge svg { width: 36px; height: 36px; }
@keyframes achBadgeIn { from { opacity: 0; transform: scale(.6); } to { opacity: 1; transform: scale(1); } }
/* The canvas is still rendered at full resolution (needed as the source
   for both the fullscreen viewer and the download), it's just never
   shown inline — the certificate has a lot of fine print that was
   simply illegible at the ~270px an inline preview has room for, and
   loading/animating it in on every open was also the thing that read
   as the modal "shaking" right after opening. The "Open card" button
   below opens the same card full-size in `.ach-lightbox-overlay`. */
.ach-modal-canvas { display: none; }
.ach-card-open-btn {
  display: inline-flex; align-items: center; gap: 3px;
}
.ach-card-open-btn[disabled] { opacity: .55; cursor: default; }
.ach-card-open-btn svg { width: 18px; height: 18px; }
.ach-modal-title { font-family: var(--font-display); font-size: 21px; font-weight: 700; margin: 0 0 8px; }
.ach-modal-desc { color: var(--text-dim); font-size: 13.5px; margin: 0 0 24px; line-height: 1.6; }
.ach-modal-actions { justify-content: center; gap: 10px; }
.ach-modal-actions .btn { border-radius: 999px; padding: 11px 22px; font-weight: 600; }
.ach-modal-actions .btn span { display: inline-flex; vertical-align: -3px; margin-right: 5px; }
.ach-modal-actions .btn svg { width: 18px; height: 18px; }

/* ---- Achievement card fullscreen viewer (Telegram-style photo view) ----
   A separate top-level overlay, not another `.modal-overlay`: it needs
   to sit above the achievement modal itself (the modal stays open
   underneath) and has no card chrome at all — just the dimmed backdrop,
   the image at its real size, and a close button pinned to the corner. */
.ach-lightbox-overlay {
  position: fixed; inset: 0; z-index: 600; background: rgba(6,7,10,.94);
  display: flex; align-items: center; justify-content: center; padding: 32px;
  opacity: 0; transition: opacity .26s ease;
  isolation: isolate; contain: layout style paint;
}
.ach-lightbox-overlay.show { opacity: 1; }
.ach-lightbox-img {
  max-width: min(92vw, 480px); max-height: 86vh; width: auto; height: auto;
  border-radius: 20px; box-shadow: 0 30px 80px rgba(0,0,0,.6);
  transform: scale(.96); transition: transform .28s cubic-bezier(.22,1,.36,1);
}
.ach-lightbox-overlay.show .ach-lightbox-img { transform: scale(1); }
.ach-lightbox-close {
  position: absolute; top: 18px; right: 18px; z-index: 2;
  width: 38px; height: 38px; border-radius: 50%; border: none; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,.1); color: #fff; transition: background .2s;
}
.ach-lightbox-close svg { width: 18px; height: 18px; }
@media (hover: hover) { .ach-lightbox-close:hover { background: rgba(255,255,255,.2); } }
@media (max-width: 600px) {
  .ach-lightbox-overlay { padding: 16px; }
  .ach-lightbox-close { top: max(14px, env(safe-area-inset-top)); right: 14px; }
}

/* ---- Floating "Achievement unlocked" toasts (independent of the single
   status/error .toast element — several can unlock at once, e.g. crossing
   two hour tiers in one flush, and each needs its own icon + label). ---- */
.ach-toast-stack {
  position: fixed; top: 18px; right: 18px; z-index: 600;
  display: flex; flex-direction: column; gap: 10px; pointer-events: none;
}
.ach-toast {
  pointer-events: auto; display: flex; align-items: center; gap: 12px;
  background: var(--bg-elevated); border: 1px solid var(--border); border-radius: var(--radius-md);
  padding: 12px 16px; min-width: 250px; max-width: 320px; cursor: pointer;
  box-shadow: 0 20px 40px -20px rgba(0,0,0,.6);
  opacity: 0; transform: translateX(30px) scale(.97);
  transition: transform .45s cubic-bezier(.22,1.4,.36,1), opacity .35s ease;
}
.ach-toast.show { opacity: 1; transform: none; }
.ach-toast-icon {
  width: 38px; height: 38px; border-radius: 50%; flex: 0 0 auto; color: #fff;
  display: flex; align-items: center; justify-content: center; background: var(--ach-grad, var(--primary-grad));
}
.ach-toast-icon svg { width: 19px; height: 19px; }
.ach-toast-text { min-width: 0; }
.ach-toast-text strong { display: block; font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-dim); font-weight: 800; }
.ach-toast-text span { display: block; font-weight: 700; font-size: 14px; color: var(--text); margin-top: 2px; }

@media (max-width: 620px) {
  .ach-toast-stack { top: auto; bottom: calc(140px + env(safe-area-inset-bottom)); left: 12px; right: 12px; align-items: stretch; }
  .ach-toast { max-width: none; }
}

/* =========================================================
   Layout templates — Header / Sidebar / Player bar
   (Settings → Customization → Layout templates)
   Toggled via body classes from applyCustomization() in app.js:
     header-template-{classic|floating|minimal}
     sidebar-template-{classic|floating|minimal}
     player-template-{classic|floating|minimal}
   "classic" is the existing look above and needs no rules here —
   only floating/minimal are overrides on top of it.
   ========================================================= */

/* All template overrides below only ever change background / border-color /
   border-radius / box-shadow / backdrop-filter — every property a base rule
   above (.navbar, .navbar-search, .sidebar, .content, .player-bar,
   .player-cover, .settings-tabs) already carries a transition for, and
   "off" states use a zero-alpha border-color / box-shadow / blur(0) instead
   of `border: none` / `box-shadow: none`, so nothing here ever has to jump —
   switching templates eases smoothly, the same as every other Customization
   control. */

/* ---- Header: Floating ---- */
body.header-template-floating .navbar {
  background: color-mix(in srgb, var(--bg-elevated) 78%, transparent);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  /* Half the navbar's own 65px height, not 999px — a border-radius that
     already exceeds half the box's height renders as a full pill no
     matter how much bigger it gets, so animating from 12px (Classic)
     straight to 999px clears that "full pill" threshold in the first
     couple % of the transition and then holds the exact same pill shape
     for the rest of it, which reads as an instant snap even though the
     value is technically still animating underneath. Stopping right at
     the threshold keeps every frame of the transition visually distinct. */
  border-radius: 33px;
  border-color: var(--border-soft);
  box-shadow: 0 8px 24px -12px rgba(0,0,0,.45);
  /* z-index now lives on the base .navbar rule (needed in every template,
     not just this one) — a z-index declared here would win over it since
     this selector is more specific, so it's intentionally left out. */
}
body.header-template-floating .navbar-search { background: var(--bg-card-hover); }

/* ---- Header: Minimal ---- */
body.header-template-minimal .navbar {
  background: transparent;
  border-color: transparent;
  border-radius: 0;
  box-shadow: 0 8px 24px -12px rgba(0,0,0,0);
}
body.header-template-minimal .navbar-search {
  background: transparent;
  border-color: var(--border-soft);
}

/* ---- Sidebar: Floating ---- */
body.sidebar-template-floating .sidebar {
  background: color-mix(in srgb, var(--bg-elevated) 85%, transparent);
  border-radius: var(--radius-lg);
  box-shadow: 0 10px 30px -16px rgba(0,0,0,.5);
}
body.sidebar-template-floating .sidebar-link.active {
  background: rgba(255,255,255,.06);
  color: #fff;
}
body.sidebar-template-floating .sidebar-liked.active,
body.sidebar-template-floating .sidebar-playlist-link.active { background: rgba(255,255,255,.06); }

/* ---- Sidebar: Minimal ---- */
body.sidebar-template-minimal .sidebar {
  background: transparent;
  border-color: transparent;
  border-radius: 0;
  box-shadow: 0 10px 30px -16px rgba(0,0,0,0);
}
body.sidebar-template-minimal .sidebar-library { border-top-color: transparent; padding-top: 0; }
body.sidebar-template-minimal .sidebar-link.active { background: rgba(255,255,255,.06); }
body.sidebar-template-minimal .sidebar-link.active::before { width: 3px; height: 60%; }
body.sidebar-template-minimal .sidebar-liked.active,
body.sidebar-template-minimal .sidebar-playlist-link.active { background: rgba(255,255,255,.06); }

/* ---- Player bar: Floating ---- */
body.player-template-floating .player-bar {
  /* Half the player bar's own 76px height (see the .navbar note above for
     why not 999px). */
  border-radius: 38px;
  background: color-mix(in srgb, var(--bg-elevated) 82%, transparent);
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  box-shadow: 0 14px 34px -16px rgba(0,0,0,.55);
}
body.player-template-floating .player-cover { border-radius: var(--radius-lg); }

/* ---- Player bar: Minimal ---- */
body.player-template-minimal .player-bar {
  background: transparent;
  border-color: transparent;
  border-radius: 0;
  box-shadow: 0 14px 34px -16px rgba(0,0,0,0);
}

/* Flattening the main content panel, the settings tabs rail and the
   scrollbar thumb only makes sense once EVERY template (header + sidebar +
   player) is Minimal at once — with e.g. just the header set to Minimal,
   the sidebar/player are still drawn as normal cards, so .content should
   stay a normal card too instead of going transparent underneath them.
   Chaining the three classes on one selector (no commas) requires all
   three to be present at the same time, rather than any single one. */
body.header-template-minimal.sidebar-template-minimal.player-template-minimal .content {
  --content-tint: 0; border-color: transparent; border-radius: 0;
}

body.header-template-minimal.sidebar-template-minimal.player-template-minimal .settings-tabs {
  background: transparent;
}

body.header-template-minimal.sidebar-template-minimal.player-template-minimal ::-webkit-scrollbar-thumb {
  background: transparent; transition: background .4s cubic-bezier(.22,1,.36,1);
}

/* ---- Layout template picker thumbnails (Settings → Customization) ---- */
.layout-template-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 22px; }
.layout-template-card {
  display: flex; flex-direction: column; align-items: stretch; gap: 8px;
  background: var(--bg-card); border: 1.5px solid var(--border-soft); border-radius: var(--radius-md);
  padding: 8px; cursor: pointer; transition: border-color .18s, background .18s;
}
.layout-template-card:hover { border-color: var(--border); }
.layout-template-card.active { border-color: var(--primary); background: var(--bg-card-hover); }
.layout-template-thumb { width: 100%; height: 54px; border-radius: var(--radius-sm); overflow: hidden; background: var(--bg-elevated); }
.layout-template-thumb svg { width: 100%; height: 100%; display: block; }
.layout-template-label { font-size: 12.5px; font-weight: 600; color: var(--text); text-align: center; }