/* ============================================================
   APP CONTENT STYLES
   Shared between the desktop phone viewer and the mobile prototype
   ============================================================ */

:root {
  /* ── Fonts ── */
  --font-main: 'DM Sans', system-ui, -apple-system, sans-serif;
  --font-head: 'DM Sans', system-ui, -apple-system, sans-serif;
  --font-sub:  'DM Sans', system-ui, -apple-system, sans-serif;
  --font-mono: 'SUSE Mono', 'Courier New', monospace;

  /* ── Dark charcoal theme ── */
  --accent:   #c8c2b8;
  --accent2:  #a09890;
  --bg:       #0c0b0a;
  --bg2:      #0f0e0d;
  --surface:  #161514;
  --surface2: #1e1c1b;
  --border:   #252320;
  --text:     #e8e4dc;
  --text2:    #706860;
  --text3:    #302e2c;
  --star:     #e8a83c;

  /* Vinyl record silhouette (disc with a center hole) — used to mask rating units */
  --vinyl-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M12 1a11 11 0 1 0 0 22 11 11 0 0 0 0-22Zm0 8.6a2.4 2.4 0 1 0 0 4.8 2.4 2.4 0 0 0 0-4.8Z'/%3E%3C/svg%3E");

  /* Tab bar */
  --tab-bg:   #0a0908;
  --tab-h:    83px;
}

/* ============================================================
   SCREEN WRAPPER
   ============================================================ */
.app-screen {
  width: 100%;
  min-height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-main);
  -webkit-font-smoothing: antialiased;
}

/* ── Theme scopes for the older .app-screen pages (auth / onboarding / song) ──
   Re-point the palette tokens to the current Spindeck dark (#111116) and light
   (#f0ece3) themes so those screens render as a Float·Dark / Float·Light pair,
   matching Home / Wall / Playlists. All their components already read these
   vars, so overriding them here is enough. */
.app-screen.sd-theme-dark {
  --bg:       #111116;
  --bg2:      #16161c;
  --surface:  #1c1c23;
  --surface2: #24242d;
  --border:   rgba(232,226,214,0.12);
  --text:     #e8e2d6;
  --text2:    rgba(232,226,214,0.52);
  --text3:    rgba(232,226,214,0.22);
  --accent:   #e6b866;
  --accent2:  #cfa24f;
}
/* Selected genre chip: give the light theme a warm tint (the dark default's
   translucent off-white is invisible on cream). */
.app-screen.sd-theme-light .chip.selected { background: rgba(168,116,42,0.10); }
.app-screen.sd-theme-light {
  --bg:       #f0ece3;
  --bg2:      #ece7dc;
  --surface:  #ffffff;
  --surface2: #e7e0d3;
  --border:   rgba(26,18,8,0.14);
  --text:     #1a1208;
  --text2:    rgba(26,18,8,0.55);
  --text3:    rgba(26,18,8,0.32);
  --accent:   #a8742a;
  --accent2:  #8a5f1f;
}
/* Hide all scrollbars inside the phone UI */
.screen-content *::-webkit-scrollbar { display: none; }
.screen-content * { scrollbar-width: none; }

/* ============================================================
   COMMON COMPONENTS
   ============================================================ */

/* Top nav bar (in-app, not OS) */
.app-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 20px 10px;
  background: var(--bg);
  flex-shrink: 0;
}
.app-nav-title {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.3px;
  font-family: var(--font-sub);
}
.app-nav-btn {
  background: none;
  border: none;
  color: var(--accent);
  font-size: 17px;
  cursor: pointer;
  padding: 4px 2px;
  display: flex;
  align-items: center;
  gap: 4px;
  font-family: inherit;
}
.app-nav-back { font-size: 22px; line-height: 1; }
.app-nav-btn svg { flex-shrink: 0; }

/* Tab bar */
.tab-bar {
  flex-shrink: 0;
  height: var(--tab-h);
  background: var(--tab-bg);
  border-top: 1px solid var(--border);
  display: flex;
  align-items: flex-start;
  padding-top: 10px;
  margin-top: auto;
}
.tab-item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  color: var(--text3);
  cursor: pointer;
  border: none;
  background: none;
  font-family: inherit;
  padding: 0;
  transition: color 0.15s;
}
.tab-item.active { color: var(--accent); }
.tab-item span   { font-size: 10px; font-weight: 500; }
.tab-item svg    { width: 24px; height: 24px; }

/* Segment tabs (Following / For You / etc) */
.seg-tabs {
  display: flex;
  padding: 0 20px;
  gap: 0;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
  flex-shrink: 0;
}
.seg-tab {
  padding: 10px 16px;
  font-size: 14px;
  font-weight: 500;
  color: var(--text2);
  cursor: pointer;
  border: none;
  background: none;
  font-family: inherit;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color 0.15s, border-color 0.15s;
}
.seg-tab.active { color: var(--text); border-bottom-color: var(--text); }

/* Buttons */
.btn-primary {
  width: 100%;
  padding: 15px;
  background: var(--text);
  color: var(--bg);
  border: none;
  border-radius: 12px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  font-family: inherit;
  transition: opacity 0.15s;
}
.btn-primary:active { opacity: 0.85; }

.btn-outline {
  width: 100%;
  padding: 15px;
  background: none;
  color: var(--text);
  border: 1.5px solid var(--border);
  border-radius: 12px;
  font-size: 16px;
  font-weight: 500;
  cursor: pointer;
  font-family: inherit;
  transition: border-color 0.15s;
}
.btn-outline:active { border-color: var(--accent); color: var(--accent); }

.btn-ghost {
  width: 100%;
  padding: 12px;
  background: none;
  color: var(--text2);
  border: none;
  font-size: 15px;
  cursor: pointer;
  font-family: inherit;
}

.btn-small {
  padding: 8px 16px;
  border-radius: 8px;
  border: 1.5px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  font-family: inherit;
}

/* Form fields */
.field-group { display: flex; flex-direction: column; gap: 16px; }
.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.field label { font-size: 13px; color: var(--text2); font-weight: 500; }
.field input, .field textarea {
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: 10px;
  color: var(--text);
  font-size: 16px;
  font-family: inherit;
  padding: 13px 14px;
  outline: none;
  transition: border-color 0.15s;
  -webkit-appearance: none;
}
.field input::placeholder, .field textarea::placeholder { color: var(--text3); }
.field input:focus, .field textarea:focus { border-color: var(--accent2); }
.field textarea { resize: none; min-height: 120px; line-height: 1.5; }

/* Divider */
.divider {
  display: flex;
  align-items: center;
  gap: 12px;
  color: var(--text3);
  font-size: 13px;
}
.divider::before, .divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: var(--border);
}

/* ── Monospace subtext (all metadata, labels, counts) ── */
.album-meta, .artist-meta, .rc-time, .rc-action, .rc-artist,
.trending-artist, .trending-num, .album-stat-lbl, .pstat-lbl,
.ob-step, .ff-label, .was-artist, .live-artist, .live-badge,
.live-now, .song-album, .char-count, .write-section-label,
.tb-idx, .genre-card, .swipe-meta, .swipe-artist-name,
.swipe-rating-label, .swipe-score, .swipe-up-hint,
.profile-handle, .thumb-label, .mr-text, .rc-excerpt,
.chat-text, .chat-user, .pl-meta, .field label {
  font-family: var(--font-mono);
}

/* Stars */
.stars { display: flex; gap: 4px; }
.star { color: var(--star); font-size: 18px; }
.star.empty { color: var(--text3); }

/* Rating row */
.rating-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text2);
}
.rating-score { font-weight: 700; font-size: 16px; color: var(--text); }

/* Avatar */
.avatar {
  border-radius: 50%;
  background: var(--surface2);
  overflow: hidden;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.avatar-placeholder {
  width: 100%;
  height: 100%;
  background: linear-gradient(135deg, var(--accent) 0%, #1a3a8f 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-weight: 700;
}

/* Album art placeholder */
.album-art {
  border-radius: 8px;
  background: linear-gradient(135deg, #1e3a5f 0%, #0f2027 100%);
  overflow: hidden;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Scrollable feed */
.feed {
  flex: 1;
  overflow-y: auto;
  padding: 12px 16px 16px;
  display: flex;
  flex-direction: column;
  gap: 1px;
  scrollbar-width: none;
}
.feed::-webkit-scrollbar { display: none; }

/* ============================================================
   AUTH SCREEN
   ============================================================ */
.s-auth {
  background: var(--bg);
  flex: 1;
  display: flex;
  flex-direction: column;
}

.auth-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 44px 0 28px;
  gap: 10px;
}

/* Brand lockup: turntable icon over the wordmark. The icon sits on the screen
   bg with no plate — the mark carries its own outlined silhouette. */
.auth-logo-mark {
  width: 84px;            /* asset is 484×426 (≈1.14:1) */
  height: auto;
  display: block;
}

.auth-appname {
  width: 148px;
  height: 22px;
  background-color: var(--text);
  -webkit-mask: url('images/spindeck-wordmark.png') center / contain no-repeat;
  mask: url('images/spindeck-wordmark.png') center / contain no-repeat;
}

.auth-body {
  flex: 1;
  padding: 8px 24px 32px;
  display: flex;
  flex-direction: column;
}

/* ============================================================
   ONBOARDING SCREEN
   ============================================================ */
.s-onboarding {
  background: var(--bg);
  flex: 1;
  display: flex;
  flex-direction: column;
}

.ob-header { padding: 20px 24px 16px; }
.ob-progress { height: 3px; background: var(--border); border-radius: 99px; margin-bottom: 16px; }
.ob-prog-bar { height: 100%; background: var(--accent); border-radius: 99px; transition: width 0.3s; }
.ob-step  { font-size: 12px; color: var(--text2); margin-bottom: 8px; }
.ob-title { font-size: 22px; font-weight: 700; letter-spacing: -0.4px; margin-bottom: 5px; font-family: var(--font-head); }
.ob-sub   { font-size: 14px; color: var(--text2); }

.ob-body { flex: 1; overflow-y: auto; padding: 8px 24px; scrollbar-width: none; }
.ob-body::-webkit-scrollbar { display: none; }

.genre-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  padding: 8px 14px;
  border-radius: 99px;
  border: 1.5px solid var(--border);
  background: none;
  color: var(--text2);
  font-size: 13px;
  font-weight: 500;
  cursor: pointer;
  font-family: inherit;
  transition: border-color .15s, color .15s, background .15s;
}
.chip.selected { border-color: var(--text); color: var(--text); background: rgba(232,228,220,0.08); }
.chip:active { opacity: 0.7; }

.ob-footer { padding: 16px 24px 24px; display: flex; flex-direction: column; gap: 8px; }

/* ============================================================
   WELCOME SCREEN (kept for backward compat)
   ============================================================ */
.s-welcome {
  background: var(--bg);
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 40px 28px 28px;
}

.welcome-hero {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  padding-bottom: 30px;
}

.welcome-logo-mark {
  width: 80px;
  height: 80px;
  border-radius: 22px;
  background: linear-gradient(135deg, var(--accent) 0%, #1a3a8f 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 12px 40px rgba(77, 127, 255, 0.35);
}

.welcome-wordmark {
  font-size: 34px;
  font-weight: 800;
  letter-spacing: -1.5px;
  font-family: var(--font-head);
  background: linear-gradient(135deg, var(--text) 0%, var(--text2) 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}

.welcome-tag {
  font-size: 16px;
  color: var(--text2);
  text-align: center;
  line-height: 1.5;
}

.welcome-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.welcome-footer {
  text-align: center;
  font-size: 11px;
  color: var(--text3);
  margin-top: 16px;
  line-height: 1.5;
}

/* ============================================================
   LOGIN SCREEN
   ============================================================ */
.s-login {
  background: var(--bg);
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 0 24px 32px;
}

.login-header {
  padding: 24px 0 8px;
}
.login-title { font-size: 26px; font-weight: 700; letter-spacing: -0.5px; margin-bottom: 4px; font-family: var(--font-head); }
.login-sub   { font-size: 14px; color: var(--text2); }

.login-body {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 20px;
  padding: 10px 0;
}

.login-footer {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.link-btn {
  background: none;
  border: none;
  color: var(--accent);
  font-size: 14px;
  cursor: pointer;
  font-family: inherit;
  text-align: center;
  padding: 4px;
}

/* ============================================================
   HOME / FEED SCREEN
   ============================================================ */
.s-home {
  background: var(--bg);
  flex: 1;
  display: flex;
  flex-direction: column;
}

.home-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 20px 8px;
}

.home-logo {
  font-size: 20px;
  font-weight: 800;
  letter-spacing: -0.8px;
  font-family: var(--font-head);
}

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

.icon-btn {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--surface);
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text2);
}

/* Review card in feed */
.review-card {
  background: var(--bg);
  padding: 16px 0;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.review-card:last-child { border-bottom: none; }

.rc-user {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
}
.rc-user .avatar { width: 32px; height: 32px; }
.rc-user-info { flex: 1; min-width: 0; }
.rc-username { font-size: 13px; font-weight: 600; }
.rc-action   { font-size: 12px; color: var(--text2); }
.rc-time     { font-size: 11px; color: var(--text3); flex-shrink: 0; }

.rc-body {
  display: flex;
  gap: 12px;
  align-items: flex-start;
}
.rc-art { width: 56px; height: 56px; flex-shrink: 0; border-radius: 6px; }
.rc-info { flex: 1; min-width: 0; }
.rc-album  { font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rc-artist { font-size: 12px; color: var(--text2); margin-bottom: 5px; }
.rc-excerpt { font-size: 13px; color: var(--text2); line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

.rc-footer {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}
.rc-action-btn {
  background: none;
  border: none;
  color: var(--text3);
  font-size: 12px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 4px;
  font-family: inherit;
}

/* ============================================================
   SEARCH SCREEN
   ============================================================ */
.s-search {
  background: var(--bg);
  flex: 1;
  display: flex;
  flex-direction: column;
}

.search-bar-wrap {
  padding: 12px 16px;
  flex-shrink: 0;
}

.search-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: 12px;
  padding: 11px 14px;
  color: var(--text3);
}

.search-bar input {
  flex: 1;
  background: none;
  border: none;
  outline: none;
  color: var(--text);
  font-size: 15px;
  font-family: inherit;
}
.search-bar input::placeholder { color: var(--text3); }

.search-body {
  flex: 1;
  overflow-y: auto;
  scrollbar-width: none;
  padding: 4px 16px 16px;
}
.search-body::-webkit-scrollbar { display: none; }

.section-title {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.3px;
  margin: 16px 0 10px;
  font-family: var(--font-sub);
}

/* Genre grid */
.genre-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.genre-card {
  height: 72px;
  border-radius: 10px;
  display: flex;
  align-items: flex-end;
  padding: 10px 12px;
  font-size: 13px;
  font-weight: 700;
  overflow: hidden;
  position: relative;
  cursor: pointer;
}

/* Trending row */
.trending-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.trending-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}
.trending-item:last-child { border-bottom: none; }

.trending-num {
  font-size: 13px;
  font-weight: 700;
  color: var(--text3);
  width: 18px;
  text-align: center;
}
.trending-art { width: 46px; height: 46px; flex-shrink: 0; border-radius: 6px; }
.trending-info { flex: 1; min-width: 0; }
.trending-album  { font-size: 14px; font-weight: 400; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.trending-artist { font-size: 12px; font-weight: 700; color: var(--text2); }

/* ============================================================
   ALBUM DETAIL SCREEN
   ============================================================ */
.s-album {
  background: var(--bg);
  flex: 1;
  display: flex;
  flex-direction: column;
}

.album-hero {
  position: relative;
  flex-shrink: 0;
}

.album-cover-full {
  width: 100%;
  height: 220px;
  display: block;
}

.album-hero-overlay {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 80px;
  background: linear-gradient(to bottom, transparent, var(--bg));
}

.album-back-btn {
  position: absolute;
  top: 12px;
  left: 16px;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: rgba(0,0,0,0.5);
  border: none;
  color: #fff;
  font-size: 20px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  backdrop-filter: blur(8px);
}

.album-info {
  padding: 0 20px 16px;
}

.album-title  { font-size: 22px; font-weight: 400; letter-spacing: -0.5px; line-height: 1.1; font-family: var(--font-head); }
.album-artist { font-size: 16px; color: var(--accent); font-weight: 700; margin: 3px 0 6px; }
.album-meta   { font-size: 13px; color: var(--text2); }

.album-stats {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 20px;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

.album-stat { display: flex; flex-direction: column; align-items: center; gap: 2px; flex: 1; }
.album-stat-val { font-size: 17px; font-weight: 700; }
.album-stat-lbl { font-size: 11px; color: var(--text2); }

.album-actions {
  padding: 14px 20px;
  display: flex;
  gap: 10px;
  flex-shrink: 0;
}
.album-actions .btn-primary { flex: 1; padding: 12px; font-size: 14px; border-radius: 10px; }
.album-actions .btn-small   { flex-shrink: 0; border-radius: 10px; }

.reviews-section {
  flex: 1;
  overflow-y: auto;
  padding: 0 20px 16px;
  scrollbar-width: none;
}
.reviews-section::-webkit-scrollbar { display: none; }

.reviews-title { font-size: 15px; font-weight: 700; margin-bottom: 12px; font-family: var(--font-sub); }

.mini-review {
  padding: 14px 0;
  border-bottom: 1px solid var(--border);
}
.mini-review:last-child { border-bottom: none; }
.mr-header { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
.mr-avatar { width: 26px; height: 26px; }
.mr-username { font-size: 13px; font-weight: 600; flex: 1; }
.mr-text { font-size: 13px; color: var(--text2); line-height: 1.4; }

/* ============================================================
   WRITE REVIEW SCREEN
   ============================================================ */
.s-write {
  background: var(--bg);
  flex: 1;
  display: flex;
  flex-direction: column;
}

.write-album-strip {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 20px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.was-art { width: 48px; height: 48px; border-radius: 6px; flex-shrink: 0; }
.was-name   { font-size: 14px; font-weight: 600; }
.was-artist { font-size: 12px; color: var(--text2); }

.write-body {
  flex: 1;
  overflow-y: auto;
  padding: 20px;
  display: flex;
  flex-direction: column;
  gap: 20px;
  scrollbar-width: none;
}
.write-body::-webkit-scrollbar { display: none; }

.write-section-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--text2);
  margin-bottom: 8px;
}

.star-picker {
  display: flex;
  gap: 8px;
}
.star-pick {
  font-size: 32px;
  cursor: pointer;
  color: var(--text3);
  transition: color 0.1s, transform 0.1s;
  -webkit-tap-highlight-color: transparent;
}
.star-pick.filled { color: var(--star); }
.star-pick:active { transform: scale(1.2); }

.char-count {
  text-align: right;
  font-size: 12px;
  color: var(--text3);
  margin-top: 4px;
}

.write-footer {
  padding: 14px 20px 16px;
  border-top: 1px solid var(--border);
  flex-shrink: 0;
}

/* ============================================================
   FEED FEATURE CARD
   ============================================================ */
.feed-feature {
  flex-shrink: 0;
  display: flex;
  align-items: flex-end;
  margin: 10px 16px 4px;
  height: 140px;
  border-radius: 14px;
  overflow: hidden;
  position: relative;
  cursor: pointer;
  background: linear-gradient(135deg, #030412, #0e1e50);
}
.ff-art {
  position: absolute; inset: 0;
  background: linear-gradient(to bottom, transparent 30%, rgba(0,0,0,0.85) 100%);
}
.ff-info { position: relative; z-index: 1; padding: 14px; flex: 1; }
.ff-label  { font-size: 10px; color: rgba(255,255,255,0.6); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 3px; }
.ff-album  { font-size: 15px; font-weight: 700; color: #fff; line-height: 1.2; }
.ff-artist { font-size: 12px; color: rgba(255,255,255,0.7); margin-bottom: 4px; }

/* ============================================================
   ARTIST SCREEN
   ============================================================ */
.s-artist { background: var(--bg); flex: 1; display: flex; flex-direction: column; }

.artist-hero {
  position: relative;
  flex-shrink: 0;
  height: 220px;
  background: linear-gradient(135deg, #030412, #0a1840);
  overflow: hidden;
}
.artist-hero-img {
  width: 100%; height: 100%;
  background: radial-gradient(ellipse at 50% 30%, rgba(255,255,255,0.08) 0%, transparent 70%);
}
.artist-hero-overlay {
  position: absolute; bottom: 0; left: 0; right: 0;
  height: 80px;
  background: linear-gradient(to bottom, transparent, var(--bg));
}
.artist-hero-name {
  position: absolute;
  bottom: 16px; left: 20px;
  font-size: 26px; font-weight: 800; letter-spacing: -0.5px;
  color: #fff;
  font-family: var(--font-head);
}
.artist-meta { padding: 0 20px 12px; font-size: 13px; color: var(--text2); flex-shrink: 0; }
.artist-stats {
  display: flex;
  padding: 12px 20px 16px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.artist-albums-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding-bottom: 20px; }
.artist-album-item { cursor: pointer; }

/* ============================================================
   SONG / TRACK SCREEN
   ============================================================ */
.s-song { background: var(--bg); flex: 1; display: flex; flex-direction: column; overflow-y: auto; }
.s-song::-webkit-scrollbar { display: none; }

.song-header {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 12px 20px 20px;
}
.song-art {
  width: 100px; height: 100px;
  border-radius: 10px;
  flex-shrink: 0;
  background: linear-gradient(135deg, #0f172a, #4338ca);
}
.song-title  { font-size: 18px; font-weight: 700; letter-spacing: -0.3px; font-family: var(--font-sub); }
.song-album  { font-size: 13px; font-weight: 400; color: var(--text2); margin: 3px 0 0; cursor: pointer; }
.song-artist { font-size: 12px; font-weight: 700; color: var(--accent); margin: 2px 0 0; cursor: pointer; }
.song-stats  {
  display: flex;
  padding: 12px 20px;
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

/* ============================================================
   PLAYLISTS SCREEN
   ============================================================ */
.s-playlists { background: var(--bg); flex: 1; display: flex; flex-direction: column; }

.playlist-card {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 0;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}
.playlist-card:last-child { border-bottom: none; }
.pl-art-stack { display: flex; gap: -8px; flex-shrink: 0; }
.pl-art-stack > * + * { margin-left: -18px; box-shadow: -2px 0 6px rgba(0,0,0,0.4); }
.pl-info { flex: 1; min-width: 0; }
.pl-name { font-size: 14px; font-weight: 600; }
.pl-meta { font-size: 12px; color: var(--text2); margin-top: 2px; }
.pl-export-btn {
  background: none; border: none; cursor: pointer; padding: 6px; flex-shrink: 0;
  opacity: 0.7; transition: opacity .15s;
}
.pl-export-btn:hover { opacity: 1; }

/* ============================================================
   LIVE STREAM SCREEN
   ============================================================ */
.s-live { background: var(--bg); flex: 1; display: flex; flex-direction: column; }

.live-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 16px;
  flex-shrink: 0;
}
.live-badge {
  background: var(--accent);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  padding: 2px 7px;
  border-radius: 3px;
  letter-spacing: 0.5px;
}
.live-player {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 14px 20px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.live-art {
  width: 64px; height: 64px; border-radius: 8px; flex-shrink: 0;
  background: linear-gradient(135deg, #0f172a, #4338ca);
}
.live-info { flex: 1; min-width: 0; }
.live-now   { font-size: 10px; color: var(--accent); font-weight: 700; letter-spacing: 0.5px; margin-bottom: 3px; }
.live-track { font-size: 15px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.live-artist{ font-size: 12px; color: var(--text2); }
.live-controls { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.live-ctrl {
  background: none; border: none; color: var(--text2); cursor: pointer;
  width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
  border-radius: 50%; transition: background .1s;
}
.live-ctrl:hover { background: var(--surface); }
.live-ctrl-main { color: var(--text); }

.live-chat-header {
  display: flex; justify-content: space-between; align-items: center;
  padding: 10px 16px 6px;
  font-size: 13px; font-weight: 600;
  flex-shrink: 0;
}
.live-chat {
  flex: 1; overflow-y: auto; padding: 4px 16px;
  display: flex; flex-direction: column; gap: 8px;
  scrollbar-width: none;
}
.live-chat::-webkit-scrollbar { display: none; }
.chat-msg { font-size: 13px; line-height: 1.4; }
.chat-user { font-weight: 600; margin-right: 6px; color: var(--text); }
.chat-text { color: var(--text2); }

.live-chat-input {
  display: flex; gap: 8px; padding: 10px 16px 16px; flex-shrink: 0;
  border-top: 1px solid var(--border);
}
.live-chat-input input {
  flex: 1;
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: 10px;
  color: var(--text);
  font-size: 14px;
  font-family: inherit;
  padding: 10px 14px;
  outline: none;
}
.live-chat-input input::placeholder { color: var(--text3); }

/* ============================================================
   PROFILE SCREEN
   ============================================================ */
.s-profile {
  background: var(--bg);
  flex: 1;
  display: flex;
  flex-direction: column;
}

.profile-header {
  display: flex;
  align-items: flex-start;
  padding: 20px 20px 0;
  gap: 16px;
}

.profile-avatar {
  width: 72px;
  height: 72px;
  flex-shrink: 0;
}

.profile-meta { flex: 1; min-width: 0; padding-top: 4px; }
.profile-username { font-size: 20px; font-weight: 700; letter-spacing: -0.3px; font-family: var(--font-sub); }
.profile-handle   { font-size: 14px; color: var(--text2); margin-top: 2px; }
.profile-bio      { font-size: 13px; color: var(--text2); margin-top: 6px; line-height: 1.4; }

.profile-stats {
  display: flex;
  padding: 16px 20px;
  gap: 0;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.pstat { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; }
.pstat-val { font-size: 18px; font-weight: 700; }
.pstat-lbl { font-size: 11px; color: var(--text2); }
.pstat + .pstat { border-left: 1px solid var(--border); }

.profile-tabs {
  display: flex;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.ptab {
  flex: 1;
  padding: 11px 0;
  font-size: 13px;
  font-weight: 500;
  color: var(--text2);
  border: none;
  background: none;
  font-family: inherit;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color 0.15s, border-color 0.15s;
}
.ptab.active { color: var(--text); border-bottom-color: var(--accent); }

.reviews-grid {
  flex: 1;
  overflow-y: auto;
  scrollbar-width: none;
  padding: 12px 16px;
}
.reviews-grid::-webkit-scrollbar { display: none; }

.rg-row {
  display: flex;
  gap: 8px;
  margin-bottom: 8px;
}

.rg-item {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 4px;
  cursor: pointer;
}
.rg-art { width: 100%; aspect-ratio: 1; border-radius: 6px; }
.rg-title  { font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rg-rating { display: flex; gap: 1px; }
.rg-rating .star { font-size: 10px; }

/* ============================================================
   SWIPE REVIEW SCREEN  (TikTok / Letterboxd style)
   ============================================================ */
.s-swipe {
  flex: 1;
  overflow: hidden;
  position: relative;
  background: #000;
}

.swipe-feed {
  height: 100%;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  scrollbar-width: none;
}
.swipe-feed::-webkit-scrollbar { display: none; }

.swipe-card {
  height: 100%;
  scroll-snap-align: start;
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.swipe-bg {
  position: absolute;
  inset: -24px;
  background-size: cover;
  background-position: center;
  filter: blur(55px) brightness(0.2) saturate(1.8);
  transform: scale(1.05);
}

.swipe-overlay {
  position: absolute; inset: 0;
  background: linear-gradient(
    to bottom,
    rgba(6,10,21,0.2) 0%,
    rgba(6,10,21,0.1) 30%,
    rgba(6,10,21,0.6) 65%,
    rgba(6,10,21,0.97) 100%
  );
}

.swipe-content {
  position: relative;
  z-index: 2;
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 10px 22px 14px;
}

.swipe-cover-wrap {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  padding: 8px 0 12px;
}

.swipe-cover {
  width: 62%;
  aspect-ratio: 1;
  border-radius: 14px;
  background-size: cover;
  background-position: center;
  background-color: var(--surface);
  box-shadow:
    0 28px 80px rgba(0,0,0,0.8),
    0 6px 20px rgba(0,0,0,0.5),
    0 0 0 1px rgba(255,255,255,0.06);
}

.swipe-info {
  text-align: center;
  margin-bottom: 18px;
  width: 100%;
}

.swipe-album-name {
  font-family: var(--font-head);
  font-size: 24px;
  font-weight: 800;
  letter-spacing: -0.6px;
  color: #fff;
  line-height: 1.1;
  margin-bottom: 5px;
}

.swipe-artist-name {
  font-size: 13px;
  color: rgba(255,255,255,0.65);
  margin-bottom: 3px;
}

.swipe-meta {
  font-size: 11px;
  color: rgba(255,255,255,0.35);
  letter-spacing: 0.3px;
}

.swipe-rating-section {
  width: 100%;
  background: rgba(255,255,255,0.05);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid rgba(255,255,255,0.09);
  border-radius: 18px;
  padding: 16px 18px 14px;
  margin-bottom: 12px;
}

.swipe-rating-label {
  font-size: 9px;
  color: rgba(255,255,255,0.4);
  letter-spacing: 1.5px;
  text-transform: uppercase;
  text-align: center;
  margin-bottom: 14px;
}

.swipe-stars {
  display: flex;
  justify-content: center;
  gap: 2px;
  margin-bottom: 10px;
}

.swipe-star-wrap {
  position: relative;
  width: 44px;
  height: 44px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.swipe-star-bg,
.swipe-star-fill {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 36px;
  line-height: 1;
  user-select: none;
}

.swipe-star-bg { color: rgba(255,255,255,0.1); }
.swipe-star-fill { color: var(--star); clip-path: inset(0 100% 0 0); transition: clip-path 0.1s; }
.swipe-star-fill.half { clip-path: inset(0 50% 0 0); }
.swipe-star-fill.full { clip-path: inset(0 0% 0 0); }

.swipe-score {
  text-align: center;
  font-size: 11px;
  color: rgba(255,255,255,0.4);
  letter-spacing: 0.5px;
}

.swipe-actions {
  display: flex;
  gap: 10px;
  width: 100%;
}

.swipe-skip {
  flex: 1;
  padding: 14px;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.12);
  border-radius: 14px;
  color: rgba(255,255,255,0.6);
  font-size: 14px;
  font-weight: 600;
  font-family: var(--font-main);
  cursor: pointer;
  transition: background .15s;
}
.swipe-skip:active { background: rgba(255,255,255,0.1); }

.swipe-log {
  flex: 2;
  padding: 14px;
  background: var(--accent);
  border: none;
  border-radius: 14px;
  color: #fff;
  font-size: 14px;
  font-weight: 800;
  font-family: var(--font-main);
  cursor: pointer;
  letter-spacing: -0.2px;
  transition: opacity .15s;
}
.swipe-log:active { opacity: 0.85; }

.swipe-up-hint {
  text-align: center;
  font-size: 10px;
  color: rgba(255,255,255,0.22);
  padding: 10px 0 2px;
  letter-spacing: 1px;
}

/* ============================================================
   TOP NAV (3-tab persistent nav)
   ============================================================ */
.top-nav {
  display: flex;
  align-items: stretch;
  height: 44px;
  flex-shrink: 0;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  position: relative;
}
.tn-tab {
  flex: 1;
  background: none;
  border: none;
  color: var(--text2);
  font-size: 12px;
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  position: relative;
  transition: color .15s;
  letter-spacing: 0.1px;
}
.tn-tab.active { color: var(--text); }
.tn-tab.active::after {
  content: '';
  position: absolute;
  bottom: 0; left: 20%; width: 60%; height: 1.5px;
  background: var(--text);
  border-radius: 1px;
}
.tn-center { font-weight: 700; font-size: 13px; }

/* ============================================================
   HALF STARS
   ============================================================ */
/* `vertical-align: baseline` (not middle) sits the discs ON the text baseline,
   so a rating's vinyls line up with the number beside it. An inline-flex takes
   its baseline from its first item, and these items are empty inline-blocks —
   whose baseline is their bottom edge — so the discs bottom out exactly on it. */
/* Whole-pixel gap — 1.5px pushed each successive disc onto a different
   sub-pixel boundary, which is half of why they rendered at different weights.
   The row already aligns the discs' BOX bottoms exactly on the number's text
   baseline (measured: 0.00px off). But a disc is taller than the digits' cap
   height, so sitting it on the baseline makes it loom above the number. The
   nudge drops it by ~10% of its own size, which lines the disc's optical centre
   up with the digits' — scale-relative, so it holds at every rating size. */
.hstars {
  display: inline-flex; gap: 2px; align-items: center; vertical-align: baseline;
  /* ⚠️ The optical drop is `top`, NOT `transform: translateY(10%)` (2026-09-18).
     A transform on a masked element puts it on its own compositing layer,
     which the scaled phone wrap (and WebKit on the real phone) then
     resamples — the discs read BLURRY. `top` moves it in layout, no layer,
     pixel-snapped. 1px ≈ the old 10% at the 9–13px sizes the discs mostly
     are; the big hero row loses a fraction of a pixel of drop, nothing more. */
  position: relative; top: 1px;
}
/* Each rating unit is a vinyl record: a CSS mask clips the background to the
   disc shape, so full/empty/half coloring (incl. the 50/50 split) still works. */
/* The disc is drawn by a radial-gradient mask, NOT the --vinyl-mask SVG.
   ⚠️ Why: an SVG mask-image is rasterised once and then sampled. These discs are
   ~10px and land on fractional x-positions (fractional size × 1.5px gap × the
   viewer's 0.88 phone scale), so every disc in a row sampled that bitmap at a
   different sub-pixel offset and came out a slightly different weight — the
   first one read as visibly LARGER than the other four. A gradient is resolved
   at paint time at device resolution, so all five rasterise identically.
   Stops are relative to `closest-side` = half the box, so they map 1:1 onto the
   old artwork: hole radius 10% of the box, outer edge 45.5% (was 45.8%). */
.hstar {
  display: inline-block; width: 14px; height: 14px; flex: 0 0 auto;
  background: var(--vinyl-empty, var(--text3));
  -webkit-mask: radial-gradient(circle closest-side,
                  transparent 0 20%, #000 21% 91%, transparent 92%);
          mask: radial-gradient(circle closest-side,
                  transparent 0 20%, #000 21% 91%, transparent 92%);
}
.hstar.full  { background: var(--vinyl-gold, var(--star)); }
.hstar.empty { background: var(--vinyl-empty, var(--text3)); }
.hstar.half  {
  background: linear-gradient(90deg, var(--vinyl-gold, var(--star)) 50%, var(--vinyl-empty, var(--text3)) 50%);
}
/* Unfilled vinyls read as part of the rating, not as background texture, so they
   get their own token rather than the very faint --text3 (which stays as the
   fallback everywhere else that uses it).
   ⚠️ The value follows the SURFACE, not the theme. In light mode most of the app
   is cream, so the default flips to dark ink — but the bento stats block and the
   review panel sit on the album's PROCEDURAL colour, which is dark in BOTH
   themes (see applyColorVars). Theming those by variant put dark ink on a dark
   album and made the empty vinyls disappear, which looked like "the light theme
   didn't change". */
.s-home-v3        { --vinyl-empty: rgba(255,255,255,0.85); }
.s-home-v3--light { --vinyl-empty: rgba(26,18,8,0.62); }
.s-home-v3--light .v3-blue-stars-row,
.s-home-v3--light .v3-rev-card,
.s-home-v3--light .v3-rev-hist { --vinyl-empty: rgba(255,255,255,0.85); }

/* ── The rating gold follows the album ────────────────────────────────────
   `--v3-accent` is extracted from the current cover by applyColorVars (set
   inline on the screen), so the vinyls and the histogram bars re-tint on every
   album switch instead of sitting at one fixed gold. The chain degrades:
   album accent → the active persona's accent → the default gold. Screens with
   no bento (wall, playlists, settings…) never get --v3-accent, so they simply
   fall through to the persona/default. */
.s-home-v3        { --star: var(--v3-star, var(--persona-accent, #e8a83c)); }
.s-home-v3--light { --star: var(--v3-star, var(--persona-accent, #c08818)); }
/* csharpuser (2026-09-23): THE VINYLS ARE ONE GOLD, EVERYWHERE (Eric: "a
   consistent yellow, easier to see what the exact score is"). The discs
   read `--vinyl-gold` ahead of `--star`, and it is pinned here on the shell
   — so neither the album's extracted accent (`--v3-star`) nor a feed card's
   inline per-cover `--star` (tintFeedRecords) re-tints a rating any more.
   The hearts and the histogram still follow the album. */
.s-home-v3        { --vinyl-gold: #e8a83c; }
/* YOURS vs THE ALBUM'S, on a tracklist row (0.14): the gold --star above is
   the aggregate, and a song you have reviewed yourself reads GREEN. Light
   needs its own, deeper green — the dark one on paper is barely a colour. */
.s-home-v3        { --mine: #4ec97a; }
.s-home-v3--light { --mine: #2f9c5c; }
.s-home-v3--light { --vinyl-gold: #c08818; }

/* ============================================================
   BENTO HOME
   ============================================================ */
.s-home-bento {
  background: var(--bg);
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.home-bento-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 14px;
  height: 40px;
  flex-shrink: 0;
}
.hbb-logo {
  font-size: 16px;
  font-weight: 800;
  letter-spacing: -1px;
  font-family: var(--font-head);
}
.hbb-icons { display: flex; gap: 6px; }

.bento-wrap {
  flex: 1;
  overflow-y: auto;
  scrollbar-width: none;
  padding: 4px 5px 8px;
}
.bento-wrap::-webkit-scrollbar { display: none; }

.bento-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
}

.bento-card {
  background: var(--surface);
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid var(--border);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: opacity .12s;
}
.bento-card:active { opacity: 0.85; }
.bento-card.span2 { grid-column: span 2; }

/* Hero social card */
.bc-hero { display: flex; flex-direction: column; }
.bc-hero-art { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; background-size: cover; background-position: center; }
.bc-hero-body { padding: 9px 10px 10px; }
.bc-hero-who { display: flex; align-items: center; gap: 5px; margin-bottom: 5px; }
.bc-hero-av {
  width: 18px; height: 18px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 8px; font-weight: 700; color: #fff; flex-shrink: 0;
}
.bc-hero-name { font-size: 10px; font-weight: 600; }
.bc-hero-verb { font-size: 10px; color: var(--text2); }
.bc-hero-album { font-size: 14px; font-weight: 800; letter-spacing: -0.4px; line-height: 1.15; margin-bottom: 3px; }
.bc-hero-quote { font-size: 10px; color: var(--text2); line-height: 1.35; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }

/* Mini album tile */
.bc-mini { display: flex; flex-direction: column; }
.bc-mini-art { width: 100%; aspect-ratio: 1; background-size: cover; background-position: center; }
.bc-mini-body { padding: 6px 8px 8px; }
.bc-mini-user { font-size: 9px; color: var(--text2); font-family: var(--font-mono); margin-bottom: 1px; }
.bc-mini-title { font-size: 11px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Stat tile */
.bc-stat {
  display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  padding: 14px 8px; text-align: center;
  gap: 2px;
}
.bc-stat-num { font-size: 40px; font-weight: 800; letter-spacing: -3px; line-height: 1; color: var(--text); }
.bc-stat-label { font-size: 10px; color: var(--text2); font-family: var(--font-mono); }
.bc-stat-sub { font-size: 9px; color: var(--text3); font-family: var(--font-mono); }

/* People you may know */
.bc-pymk { padding: 11px 12px 13px; }
.pymk-hd { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px; }
.pymk-title { font-size: 12px; font-weight: 700; }
.pymk-sub { font-size: 9px; color: var(--text2); font-family: var(--font-mono); }
.pymk-row { display: flex; gap: 12px; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
.pymk-row::-webkit-scrollbar { display: none; }
.pymk-person { display: flex; flex-direction: column; align-items: center; gap: 4px; flex-shrink: 0; cursor: pointer; }
.pymk-av {
  width: 40px; height: 40px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 14px; font-weight: 700; color: #fff;
}
.pymk-name { font-size: 9px; color: var(--text2); font-family: var(--font-mono); white-space: nowrap; }
.pymk-count { font-size: 8px; color: var(--text3); font-family: var(--font-mono); }
.pymk-add-btn {
  width: 40px; height: 40px; border-radius: 50%;
  border: 1.5px dashed var(--border);
  display: flex; align-items: center; justify-content: center;
  color: var(--text3); font-size: 18px; flex-shrink: 0;
}

/* Trending review */
.bc-trend { padding: 13px 14px 15px; position: relative; overflow: hidden; }
.bc-tr-label { font-size: 9px; color: var(--text2); font-family: var(--font-mono); letter-spacing: 1.5px; text-transform: uppercase; margin-bottom: 7px; }
.bc-tr-quote { font-size: 15px; font-weight: 700; letter-spacing: -0.3px; line-height: 1.3; margin-bottom: 9px; }
.bc-tr-by { font-size: 10px; color: var(--text2); display: flex; align-items: center; gap: 6px; font-family: var(--font-mono); }
.bc-tr-ghost-art {
  position: absolute; right: -14px; bottom: -14px;
  width: 80px; height: 80px; border-radius: 8px;
  background-size: cover; background-position: center;
  opacity: 0.12;
}

/* Discover / wall link */
.bc-disc { display: flex; align-items: center; gap: 10px; padding: 10px 12px; }
.bc-disc-thumbs { display: flex; gap: 3px; flex-shrink: 0; }
.bc-disc-thumb { width: 34px; height: 34px; border-radius: 5px; background-size: cover; background-position: center; flex-shrink: 0; }
.bc-disc-text { flex: 1; }
.bc-disc-label { font-size: 13px; font-weight: 700; letter-spacing: -0.2px; }
.bc-disc-sub { font-size: 10px; color: var(--text2); font-family: var(--font-mono); margin-top: 1px; }
.bc-disc-arr { font-size: 18px; color: var(--text2); flex-shrink: 0; }

/* ============================================================
   LIVE FEED (TikTok-style)
   ============================================================ */
.s-livefeed {
  background: #000;
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.lfeed-toggle {
  display: flex;
  flex-shrink: 0;
  background: rgba(0,0,0,0.7);
  border-bottom: 1px solid rgba(255,255,255,0.06);
}
.lft-btn {
  flex: 1;
  padding: 9px 0;
  font-size: 13px;
  font-weight: 500;
  color: rgba(255,255,255,0.35);
  border: none;
  background: none;
  font-family: inherit;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color .15s, border-color .15s;
}
.lft-btn.active { color: #fff; border-bottom-color: rgba(255,255,255,0.8); }

.lfeed-scroll {
  flex: 1;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  scrollbar-width: none;
  min-height: 0;
}
.lfeed-scroll::-webkit-scrollbar { display: none; }

.lfeed-card {
  height: 100%;
  scroll-snap-align: start;
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
}

.lfeed-bg {
  position: absolute; inset: -30px;
  background-size: cover; background-position: center;
  filter: blur(60px) brightness(0.18) saturate(1.6);
  transform: scale(1.05);
}
.lfeed-overlay {
  position: absolute; inset: 0;
  background: linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.05) 40%, rgba(0,0,0,0.7) 75%, rgba(0,0,0,0.95) 100%);
}
.lfeed-content {
  position: relative; z-index: 2;
  flex: 1;
  display: flex; flex-direction: column;
  align-items: center;
  padding: 12px 24px 10px;
}
.lfeed-art-wrap {
  flex: 1; display: flex; align-items: center; justify-content: center;
  width: 100%; padding: 4px 0 8px;
}
.lfeed-art {
  width: 58%; aspect-ratio: 1;
  border-radius: 12px;
  background-size: cover; background-position: center; background-color: var(--surface);
  box-shadow: 0 24px 70px rgba(0,0,0,0.8), 0 0 0 1px rgba(255,255,255,0.06);
}
.lfeed-meta { text-align: center; width: 100%; margin-bottom: 14px; }
.lfeed-artist { font-size: 11px; color: rgba(255,255,255,0.5); font-family: var(--font-mono); margin-bottom: 5px; letter-spacing: 0.3px; }
.lfeed-title { font-size: 22px; font-weight: 800; color: #fff; letter-spacing: -0.6px; line-height: 1.1; margin-bottom: 4px; font-family: var(--font-head); }
.lfeed-genre { font-size: 11px; color: rgba(255,255,255,0.3); }
.lfeed-rating { display: flex; align-items: baseline; justify-content: center; gap: 8px; margin-bottom: 6px; }
.lfeed-score { font-size: 28px; font-weight: 800; color: #fff; letter-spacing: -1px; line-height: 1; }
.lfeed-score-sub { font-size: 11px; color: rgba(255,255,255,0.3); }
.lfeed-rc {
  text-align: center;
  font-size: 12px; font-family: var(--font-mono);
  color: rgba(255,255,255,0.45);
  margin-bottom: 6px;
}
.lfeed-rc strong { color: rgba(255,255,255,0.85); font-weight: 600; }
.lfeed-today {
  text-align: center; font-size: 11px; font-family: var(--font-mono);
  color: rgba(255,255,255,0.3); margin-bottom: 14px;
  letter-spacing: 0.3px;
}
.lfeed-actions { display: flex; gap: 8px; width: 100%; margin-bottom: 10px; }
.lfeed-skip {
  flex: 1; padding: 13px;
  background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1);
  border-radius: 12px; color: rgba(255,255,255,0.6);
  font-size: 13px; font-weight: 600; font-family: inherit; cursor: pointer;
}
.lfeed-log {
  flex: 2; padding: 13px;
  background: var(--text); border: none;
  border-radius: 12px; color: var(--bg);
  font-size: 13px; font-weight: 800; font-family: inherit; cursor: pointer; letter-spacing: -0.2px;
}
.lfeed-hint { font-size: 9px; color: rgba(255,255,255,0.18); text-align: center; letter-spacing: 1px; font-family: var(--font-mono); }

/* ============================================================
   ALBUM WALL
   ============================================================ */
.s-wall {
  background: #000; flex: 1;
  display: flex; flex-direction: column; overflow: hidden;
}
.wall-header {
  display: flex; align-items: center; padding: 0 14px; height: 46px; flex-shrink: 0;
  border-bottom: 1px solid var(--border); background: var(--bg);
}
.wall-title { font-size: 15px; font-weight: 700; letter-spacing: -0.3px; flex: 1; }
.wall-cats-wrap {
  flex-shrink: 0; overflow-x: auto; scrollbar-width: none;
  padding: 8px 10px; display: flex; gap: 5px;
  border-bottom: 1px solid rgba(255,255,255,0.06);
  background: #000;
}
.wall-cats-wrap::-webkit-scrollbar { display: none; }
.wall-cat {
  flex-shrink: 0; padding: 5px 12px; border-radius: 99px;
  font-size: 11px; font-weight: 500; background: none;
  border: 1px solid rgba(255,255,255,0.1); color: rgba(255,255,255,0.4);
  font-family: inherit; cursor: pointer; white-space: nowrap; transition: all .15s;
}
.wall-cat.active { background: #fff; color: #000; border-color: #fff; }

.wall-grid {
  flex: 1; overflow-y: auto; scrollbar-width: none;
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: 2px; align-content: flex-start; background: #000;
}
.wall-grid::-webkit-scrollbar { display: none; }
.wall-cell {
  position: relative; aspect-ratio: 1; overflow: hidden;
  cursor: pointer; background: var(--surface);
}
.wall-cell-art {
  width: 100%; height: 100%; object-fit: cover;
  display: block; background-size: cover; background-position: center;
}
.wall-cell-info {
  position: absolute; bottom: 0; left: 0; right: 0;
  padding: 4px 4px 3px;
  background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, transparent 100%);
}
.wall-cell-rating {
  font-size: 7px; font-family: var(--font-mono);
  color: var(--star); line-height: 1;
}
.wall-cell-name {
  font-size: 7px; font-family: var(--font-mono);
  color: rgba(255,255,255,0.6); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* ============================================================
   PLAYLISTS EXPANDED
   ============================================================ */
.pl-exp-tabs {
  display: flex; flex-shrink: 0;
  overflow-x: auto; scrollbar-width: none;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}
.pl-exp-tabs::-webkit-scrollbar { display: none; }
.pl-exp-tab {
  flex-shrink: 0;
  padding: 11px 14px; font-size: 12px; font-weight: 500;
  color: var(--text2); border: none; background: none;
  font-family: inherit; cursor: pointer;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  white-space: nowrap; transition: color .15s, border-color .15s;
}
.pl-exp-tab.active { color: var(--text); border-bottom-color: var(--text); }

/* Artist row */
.pl-artist-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 0; border-bottom: 1px solid var(--border);
  cursor: pointer;
}
.pl-artist-row:last-child { border-bottom: none; }
.pla-art {
  width: 52px; height: 52px; border-radius: 8px; flex-shrink: 0;
  background-size: cover; background-position: center;
}
.pla-info { flex: 1; min-width: 0; }
.pla-name { font-size: 14px; font-weight: 700; letter-spacing: -0.2px; margin-bottom: 2px; }
.pla-genre { font-size: 11px; color: var(--text2); font-family: var(--font-mono); margin-bottom: 4px; }
.pla-star-row { display: flex; align-items: center; gap: 6px; }
.pla-rc { font-size: 10px; color: var(--text2); font-family: var(--font-mono); }

/* Song row */
.pl-song-row {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 0; border-bottom: 1px solid var(--border);
}
.pl-song-row:last-child { border-bottom: none; }
.pls-num { font-size: 12px; color: var(--text3); font-family: var(--font-mono); width: 18px; text-align: center; flex-shrink: 0; }
.pls-art { width: 42px; height: 42px; border-radius: 5px; flex-shrink: 0; background-size: cover; background-position: center; }
.pls-info { flex: 1; min-width: 0; }
.pls-title { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pls-artist { font-size: 11px; color: var(--text2); font-family: var(--font-mono); }
.pls-right { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; flex-shrink: 0; }
.pls-dur { font-size: 11px; color: var(--text2); font-family: var(--font-mono); }
.pls-rating { font-size: 11px; color: var(--star); font-family: var(--font-mono); }

/* Genre card */
.pl-genre-card {
  border-radius: 10px; overflow: hidden; margin-bottom: 8px;
  padding: 14px 14px 16px; position: relative; cursor: pointer;
  border: 1px solid rgba(255,255,255,0.06);
}
.plg-name { font-size: 18px; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 3px; }
.plg-stats { font-size: 10px; color: rgba(255,255,255,0.5); font-family: var(--font-mono); margin-bottom: 12px; }
.plg-tr-label { font-size: 8px; color: rgba(255,255,255,0.4); letter-spacing: 1.5px; text-transform: uppercase; font-family: var(--font-mono); margin-bottom: 4px; }
.plg-tr-text { font-size: 13px; font-weight: 600; color: rgba(255,255,255,0.85); line-height: 1.3; margin-bottom: 4px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.plg-tr-by { font-size: 10px; color: rgba(255,255,255,0.4); font-family: var(--font-mono); }

/* Album grid in playlists */
.pl-album-grid {
  display: grid; grid-template-columns: 1fr 1fr 1fr;
  gap: 10px; padding: 4px 0 12px;
}
.pl-album-item { cursor: pointer; }
.pl-album-art { width: 100%; aspect-ratio: 1; border-radius: 7px; background-size: cover; background-position: center; margin-bottom: 5px; }
.pl-album-title { font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pl-album-rc { font-size: 9px; color: var(--text2); font-family: var(--font-mono); }

/* Trending review highlight */
.pl-trending-review {
  background: var(--surface2); border: 1px solid var(--border);
  border-radius: 10px; padding: 14px; margin-bottom: 12px; cursor: pointer;
}
.pl-tr-badge { font-size: 9px; color: var(--text2); font-family: var(--font-mono); letter-spacing: 1.5px; text-transform: uppercase; margin-bottom: 7px; }
.pl-tr-quote { font-size: 15px; font-weight: 700; letter-spacing: -0.3px; line-height: 1.3; margin-bottom: 8px; }
.pl-tr-meta { display: flex; align-items: center; gap: 8px; }
.pl-tr-av { width: 22px; height: 22px; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 9px; font-weight: 700; color: #fff; }
.pl-tr-by-text { font-size: 10px; color: var(--text2); font-family: var(--font-mono); }

/* ============================================================
   HOME — TIKTOK SWIPE (Editorial look)
   ============================================================ */
.s-home-tiktok {
  background: #000;
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.htok-scroll {
  flex: 1;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  scrollbar-width: none;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.htok-scroll::-webkit-scrollbar { display: none; }
.htok-card {
  flex: 0 0 100%;
  scroll-snap-align: start;
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.htok-bg {
  position: absolute; inset: -30px;
  background-size: cover; background-position: center;
  filter: blur(60px) brightness(0.15) saturate(1.8);
  transform: scale(1.05);
}
.htok-overlay {
  position: absolute; inset: 0;
  background: linear-gradient(to bottom,
    rgba(0,0,0,0.45) 0%,
    rgba(0,0,0,0.05) 30%,
    rgba(0,0,0,0.72) 75%,
    rgba(0,0,0,0.97) 100%);
}
.htok-content {
  position: relative; z-index: 2;
  flex: 1;
  display: flex; flex-direction: column;
  padding: 8px 22px 10px;
}
.htok-who {
  display: flex; align-items: center; gap: 7px;
  padding: 2px 0 10px;
}
.htok-av {
  width: 28px; height: 28px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 700; color: #fff; flex-shrink: 0;
}
.htok-name { font-size: 13px; font-weight: 600; color: #fff; }
.htok-verb { font-size: 11px; color: rgba(255,255,255,0.45); font-family: var(--font-mono); margin-left: 2px; }
.htok-art-wrap {
  flex: 1; display: flex; align-items: center; justify-content: center;
  padding: 4px 0 12px;
}
.htok-art {
  width: 62%; aspect-ratio: 1;
  border-radius: 14px;
  background-size: cover; background-position: center; background-color: var(--surface);
  box-shadow: 0 24px 70px rgba(0,0,0,0.8), 0 0 0 1px rgba(255,255,255,0.06);
}
.htok-info { text-align: center; margin-bottom: 14px; }
.htok-album {
  font-family: var(--font-head);
  font-size: 24px; font-weight: 700;
  color: #fff; line-height: 1.1; margin-bottom: 4px;
}
.htok-artist {
  font-size: 11px; color: rgba(255,255,255,0.45);
  font-family: var(--font-mono); letter-spacing: 0.5px; margin-bottom: 8px;
}
.htok-quote {
  font-size: 13px; color: rgba(255,255,255,0.65);
  line-height: 1.5; font-style: italic; margin-top: 6px;
}
.htok-actions { display: flex; gap: 8px; width: 100%; margin-bottom: 10px; }
.htok-skip {
  flex: 1; padding: 13px;
  background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1);
  border-radius: 12px; color: rgba(255,255,255,0.6);
  font-size: 13px; font-weight: 600; font-family: inherit; cursor: pointer;
}
.htok-log {
  flex: 2; padding: 13px;
  background: var(--accent); border: none;
  border-radius: 12px; color: var(--bg);
  font-size: 13px; font-weight: 800; font-family: inherit; cursor: pointer;
}
.htok-hint {
  text-align: center; font-size: 9px; color: rgba(255,255,255,0.2);
  font-family: var(--font-mono); letter-spacing: 1px;
}

/* ============================================================
   HOME — DORFIC / FRUTIGER AERO ORANGE (Dorfic look)
   ============================================================ */
.s-home-dorf {
  background: var(--bg);
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.dorf-topbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 16px 6px; flex-shrink: 0; z-index: 2;
}
.dorf-wordmark {
  font-family: var(--font-head);
  font-size: 15px; font-weight: 800; color: var(--accent); letter-spacing: -1px;
}
.dorf-scroll {
  flex: 1; overflow-y: auto; scrollbar-width: none;
  padding-bottom: 16px; min-height: 0;
}
.dorf-scroll::-webkit-scrollbar { display: none; }

/* ── BIG GLOWING PLAYER HERO ── */
.dorf-player {
  position: relative;
  display: flex; flex-direction: column; align-items: center;
  padding: 18px 20px 16px;
  overflow: visible;
}
.dorf-player-aura {
  position: absolute;
  width: 220px; height: 220px; border-radius: 50%;
  background: radial-gradient(circle, rgba(255,138,48,0.5) 0%, rgba(200,80,0,0.18) 55%, transparent 75%);
  top: 30px; left: 50%; transform: translateX(-50%);
  pointer-events: none;
}
.dorf-player-art {
  width: 130px; height: 130px; border-radius: 18px;
  background-size: cover; background-position: center;
  box-shadow:
    0 0 0 1px rgba(255,200,100,0.18),
    0 0 50px 14px rgba(255,110,20,0.32),
    0 20px 48px rgba(0,0,0,0.75);
  position: relative; z-index: 1;
  margin-bottom: 14px;
}
.dorf-player-art::after {
  content: '';
  position: absolute; top: 0; left: 0; right: 0; height: 50%;
  background: linear-gradient(to bottom, rgba(255,255,255,0.14) 0%, transparent 100%);
  border-radius: 18px 18px 0 0; pointer-events: none;
}
.dorf-player-label {
  font-family: var(--font-mono); font-size: 8px; color: var(--accent);
  letter-spacing: 3px; text-transform: uppercase; margin-bottom: 5px;
}
.dorf-player-title {
  font-family: var(--font-head);
  font-size: 22px; font-weight: 700; color: var(--text);
  text-align: center; line-height: 1.1; margin-bottom: 3px;
}
.dorf-player-artist {
  font-family: var(--font-mono); font-size: 10px; color: var(--text2);
  text-align: center; margin-bottom: 8px; letter-spacing: 0.5px;
}
.dorf-player-stars { margin-bottom: 14px; }
.dorf-player-btn {
  padding: 11px 32px;
  background: linear-gradient(to bottom, #ffaa50 0%, #e06000 100%);
  border: none; border-radius: 99px;
  color: #1a0800; font-size: 12px; font-weight: 800;
  font-family: var(--font-head); cursor: pointer; letter-spacing: 1px;
  box-shadow: 0 4px 18px rgba(255,138,48,0.55), inset 0 1px 0 rgba(255,255,255,0.35);
}

/* ── PILL TABS ── */
.dorf-tabs {
  display: flex; gap: 6px; padding: 4px 14px 12px;
  overflow-x: auto; scrollbar-width: none;
}
.dorf-tabs::-webkit-scrollbar { display: none; }
.dorf-tab {
  flex-shrink: 0; padding: 7px 16px; border-radius: 99px;
  font-size: 11px; font-weight: 700; font-family: var(--font-mono);
  border: 1px solid var(--border);
  background: var(--surface); color: var(--text2);
  cursor: pointer; letter-spacing: 0.3px; transition: none;
}
.dorf-tab.active {
  background: var(--accent); color: #1a0800;
  border-color: var(--accent);
  box-shadow: 0 2px 12px rgba(255,138,48,0.45);
}

/* ── HORIZONTAL CAROUSEL ── */
.dorf-carousel-row {
  display: flex; gap: 10px; padding: 0 14px 16px;
  overflow-x: auto; scrollbar-width: none;
}
.dorf-carousel-row::-webkit-scrollbar { display: none; }
.dorf-bubble {
  flex-shrink: 0; width: 110px; cursor: pointer;
  border-radius: 18px; overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border); position: relative;
  box-shadow: 0 4px 14px rgba(0,0,0,0.45);
}
.dorf-bubble::before {
  content: '';
  position: absolute; top: 0; left: 0; right: 0; height: 50%;
  background: linear-gradient(to bottom, rgba(255,255,255,0.08) 0%, transparent 100%);
  pointer-events: none; z-index: 1;
}
.dorf-bubble-art {
  width: 110px; height: 110px;
  background-size: cover; background-position: center;
}
.dorf-bubble-body { padding: 6px 8px 9px; }
.dorf-bubble-title {
  font-family: var(--font-mono); font-size: 9px; font-weight: 700;
  color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  margin-bottom: 2px;
}
.dorf-bubble-meta { font-family: var(--font-mono); font-size: 8px; color: var(--accent); }

/* ── FRIEND ACTIVITY — notification pills ── */
.dorf-act-hd {
  display: flex; align-items: center; justify-content: space-between;
  padding: 0 14px 8px;
}
.dorf-act-label {
  font-family: var(--font-mono); font-size: 8px; color: var(--text2);
  letter-spacing: 2px; text-transform: uppercase;
}
.dorf-act-seeall {
  font-family: var(--font-mono); font-size: 9px; color: var(--accent);
  background: none; border: none; cursor: pointer;
}
.dorf-act-list { display: flex; flex-direction: column; gap: 6px; padding: 0 10px; }
.dorf-act-pill {
  display: flex; align-items: center; gap: 10px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 99px; padding: 7px 14px 7px 7px;
  cursor: pointer; position: relative; overflow: hidden;
}
.dorf-act-pill::before {
  content: '';
  position: absolute; top: 0; left: 0; right: 0; height: 50%;
  background: linear-gradient(to bottom, rgba(255,255,255,0.05) 0%, transparent 100%);
  pointer-events: none;
}
.dorf-act-av {
  width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 700; color: #fff;
}
.dorf-act-text { flex: 1; min-width: 0; }
.dorf-act-who { font-size: 11px; font-weight: 700; color: var(--text); line-height: 1.2; }
.dorf-act-album {
  font-family: var(--font-mono); font-size: 9px; color: var(--text2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block;
}
.dorf-act-rating { font-family: var(--font-mono); font-size: 9px; color: var(--accent); flex-shrink: 0; }

/* ============================================================
   VERSION 2 — FLOATING BENTO
   Two themes: Float·Light and Float·Dark
   ============================================================ */

/* ── Shared structure ── */
.s-home-v2 {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* ── Light theme tokens ── */
.s-home-v2--light {
  background: #f0ece3;
  color: #1a1714;
  --v2-surface: #ffffff;
  --v2-text: #1a1714;
  --v2-text2: #8a8078;
  --v2-text3: #c8c2ba;
  --v2-border: rgba(0,0,0,0.06);
  --v2-shadow: 0 2px 6px rgba(30,20,10,0.07), 0 10px 22px rgba(30,20,10,0.12);
  --v2-shadow-sm: 0 1px 3px rgba(30,20,10,0.06), 0 4px 10px rgba(30,20,10,0.09);
  --star: #c08818;
  --text3: #d4cfc8;
}

/* ── Dark theme tokens ── */
.s-home-v2--dark {
  background: #131318;
  color: #e8e2d6;
  --v2-surface: #1e1d24;
  --v2-text: #e8e2d6;
  --v2-text2: #6b625a;
  --v2-text3: #353230;
  --v2-border: rgba(255,255,255,0.05);
  --v2-shadow: 0 8px 28px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.07);
  --v2-shadow-sm: 0 4px 14px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.05);
}

/* ── Top bar ── */
.v2-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 18px 10px;
  flex-shrink: 0;
}
.v2-wordmark {
  font-size: 18px;
  font-weight: 800;
  letter-spacing: -1px;
  color: var(--v2-text);
}
.v2-icons { display: flex; gap: 8px; }
.v2-icon-btn {
  width: 32px; height: 32px;
  border-radius: 50%;
  border: 1px solid var(--v2-border);
  background: var(--v2-surface);
  box-shadow: var(--v2-shadow-sm);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  color: var(--v2-text2);
}

/* ── Scroll + Grid ── */
.v2-scroll {
  flex: 1;
  overflow-y: auto;
  padding: 4px 14px 20px;
  scrollbar-width: none;
}
.v2-scroll::-webkit-scrollbar { display: none; }
.v2-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

/* ── Base card ── */
.v2-card {
  background: var(--v2-surface);
  border-radius: 16px;
  overflow: hidden;
  box-shadow: var(--v2-shadow);
  cursor: pointer;
  position: relative;
  -webkit-tap-highlight-color: transparent;
  transition: transform 0.12s, opacity 0.12s;
}
.v2-card:active { transform: scale(0.98); opacity: 0.9; }
.v2-span2 { grid-column: span 2; }

/* ── Hero card (left col, spans 2 rows) ── */
.v2-hero { grid-row: span 2; display: flex; flex-direction: column; }
.v2-hero-art {
  flex: 1;
  min-height: 150px;
  background-size: cover;
  background-position: center;
}
.v2-hero-body { padding: 10px 12px 13px; }
.v2-who {
  display: flex; align-items: center; gap: 5px;
  margin-bottom: 7px;
  font-size: 10px;
  color: var(--v2-text2);
}
.v2-av {
  width: 20px; height: 20px; border-radius: 50%;
  background: linear-gradient(135deg,#1c1c3e,#3b1fa8);
  display: flex; align-items: center; justify-content: center;
  font-size: 8px; font-weight: 700; color: #fff; flex-shrink: 0;
}
.v2-hero-album {
  font-size: 14px; font-weight: 800;
  letter-spacing: -0.4px; line-height: 1.15;
  color: var(--v2-text);
  margin-bottom: 4px;
}
.v2-hero-quote {
  font-size: 10px; color: var(--v2-text2);
  line-height: 1.45; margin-top: 5px;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}

/* ── Mini review (top right) ── */
.v2-mini { display: flex; flex-direction: column; }
.v2-mini-art { height: 80px; background-size: cover; background-position: center; }
.v2-mini-body { padding: 8px 10px 11px; }
.v2-mini-user { font-size: 9px; color: var(--v2-text2); margin-bottom: 2px; font-family: var(--font-mono); }
.v2-mini-title { font-size: 12px; font-weight: 700; color: var(--v2-text); letter-spacing: -0.2px; }

/* ── Stat card (bottom right) ── */
.v2-stat {
  display: flex; flex-direction: column;
  justify-content: center; align-items: center;
  padding: 16px 10px; text-align: center; cursor: default;
}
.v2-stat-num {
  font-size: 34px; font-weight: 800;
  letter-spacing: -2px; line-height: 1;
  color: var(--v2-text);
}
.v2-stat-label { font-size: 10px; font-weight: 500; color: var(--v2-text2); margin-top: 5px; }
.v2-stat-sub { font-size: 9px; color: var(--v2-text3); margin-top: 2px; }

/* ── People You May Know ── */
.v2-pymk { padding: 13px 14px 15px; cursor: default; }
.v2-pymk-hd {
  display: flex; justify-content: space-between; align-items: baseline;
  margin-bottom: 13px;
}
.v2-pymk-title { font-size: 12px; font-weight: 700; color: var(--v2-text); }
.v2-pymk-sub { font-size: 10px; color: var(--v2-text2); }
.v2-pymk-row { display: flex; gap: 8px; justify-content: space-between; }
.v2-person { display: flex; flex-direction: column; align-items: center; gap: 5px; flex: 1; }
.v2-person-av {
  width: 36px; height: 36px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700; color: #fff; flex-shrink: 0;
}
.v2-person-name { font-size: 9px; font-weight: 600; color: var(--v2-text); text-align: center; line-height: 1.2; }
.v2-person-count { font-size: 8px; color: var(--v2-text2); }

/* ── Trending Review ── */
.v2-trend { padding: 14px; overflow: hidden; }
.v2-trend-ghost {
  position: absolute; right: -24px; top: -20px;
  width: 90px; height: 90px; border-radius: 14px;
  background: linear-gradient(135deg,#042a10,#6aff3a 120%);
  opacity: 0.12; transform: rotate(18deg);
  pointer-events: none;
}
.v2-trend-label {
  font-size: 9px; font-weight: 700;
  letter-spacing: 0.8px; text-transform: uppercase;
  color: var(--v2-text2); margin-bottom: 8px;
}
.v2-trend-quote {
  font-size: 12px; font-weight: 500; line-height: 1.45;
  color: var(--v2-text); margin-bottom: 10px; font-style: italic;
}
.v2-trend-by {
  font-size: 10px; color: var(--v2-text2);
  display: flex; justify-content: space-between; align-items: center;
}

/* ── Discover / Popular Albums ── */
.v2-disc {
  display: flex; align-items: center;
  gap: 12px; padding: 13px 14px;
}
.v2-disc-thumbs { display: flex; gap: 5px; }
.v2-disc-thumb {
  width: 36px; height: 36px; border-radius: 8px;
  background-size: cover; background-position: center;
}
.v2-disc-text { flex: 1; }
.v2-disc-label { font-size: 13px; font-weight: 700; color: var(--v2-text); }
.v2-disc-sub { font-size: 10px; color: var(--v2-text2); margin-top: 2px; }
.v2-disc-arr { font-size: 22px; color: var(--v2-text3); }


/* ═══════════════════════════════════════════════════════════ */
/* VERSION 3 — BENTO HERO (v3.0)                               */
/* ═══════════════════════════════════════════════════════════ */

.s-home-v3 {
  /* ⚠ `var(--sd-bg)`, not the literal, and the fallback is the same colour it
     always was. `--sd-bg` is declared further down as "screen bg — badge rings
     punch through to it" and was ALREADY exactly this value in both themes, so
     this unifies two numbers that could never legally disagree. It is also what
     lets the profile skin recolour the page: `profSkinCss` writes `--sd-bg`
     inline on `.s-prof2` / `.s-pfedit` only, so no other screen moves. */
  background: var(--sd-bg, #111116);
  color: #e8e2d6;
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  height: 100%;
  overflow: hidden;
  position: relative;
  font-family: var(--font-main);
  --text3: rgba(232,226,214,0.14);  /* grey empty stars, overrides global */
}

/* TOP HEADER — bubbles · wordmark · bubble ─────────────────────
   3-col grid keeps the wordmark centered regardless of side widths.
   Fixed height creates the ~100px drop of the bento + body below it. */
.v3-header {
  flex-shrink: 0;
  position: relative;   /* anchor for the left-aligned nav username (Profile) */
  height: 46px;   /* reduced 20px, then 4, 5 and 5 more (csharpuser 2026-09-23: the home deck closer to the top) */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 0 10px;   /* match the bento's 10px side margin */
}
.v3-header-bubbles {
  display: flex;
  align-items: center;
  gap: 6px;
  justify-self: start;
  align-self: start;   /* pin bubbles into the top-left corner */
  margin-top: 10px;
}
.v3-header-right {
  display: flex;
  align-items: center;
  gap: 6px;
  justify-self: end;
  align-self: start;   /* pin bubbles into the top-right corner */
  margin-top: 10px;
}
/* Wordmark PNG painted through a mask so it picks up the theme ink colour from
   one asset (the mark is single-tone, unlike the 2-tone turntable icon).
   Asset is 1749×249 (≈7.02:1); `contain` in a 106×30 box lands it at ~106×15,
   vertically centred on the bubble row. */
/* Wordmark + handle stack, centred in the header's middle grid column. The
   stack (not the logo) now carries the top band alignment, so the wordmark
   stays exactly where it was and the handle hangs beneath it. */
.v3-header-brand {
  align-self: start;
  margin-top: 10px;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
}
.v3-header-handle {
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.3px;
  color: rgba(232,226,214,0.42);
  line-height: 1; white-space: nowrap;
}
.s-home-v3--light .v3-header-handle { color: rgba(26,18,8,0.42); }
/* 0.12 (2026-10-02): the handle under the wordmark is OFF (Eric: "cleaner
   without it"). populateHomeData still fills it; the stack is top-aligned, so
   the wordmark doesn't move. Delete this line to bring it back. */
.v3-header-handle { display: none; }

.v3-header-logo {
  width: 106px;
  height: 30px;           /* = bubble height, so the mark centers on bubble centers */
  background-color: rgba(232,226,214,0.92);
  -webkit-mask: url('images/spindeck-wordmark.png') center / contain no-repeat;
  mask: url('images/spindeck-wordmark.png') center / contain no-repeat;
}
/* Left-aligned username in the nav (Profile). Absolutely placed so it never
   shifts the logo/bubbles or the card below — sits low-left, under the row. */
.v3-header-user {
  position: absolute; left: 25px; top: 60px;
  font-family: var(--font-main); font-size: 17px; font-weight: 500; letter-spacing: -0.2px;
  color: rgba(232,226,214,0.62); max-width: 72%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.v3-header-user .v3-hsub-nick { font-weight: 800; color: rgba(232,226,214,0.94); }
.v3-header-user .v3-hsub-handle { opacity: .7; }
.s-home-v3--light .v3-header-user { color: rgba(26,18,8,0.55); }
.s-home-v3--light .v3-header-user .v3-hsub-nick { color: rgba(26,18,8,0.9); }
/* ⚠ 34, up from 30. These three (notifications · settings · search) are the
   only always-present controls on the shell, and at 30 they were the smallest
   tap targets in the app — under every guideline's floor and noticeably fiddly
   next to the bento. Kept modest rather than pushed to 44: the header is a thin
   strip above the bento and the brand block sits between them, so growing these
   much further starts crowding the wordmark. */
.v3-bubble {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: rgba(255,255,255,0.07);
  border: 1px solid rgba(255,255,255,0.12);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.08);
  color: rgba(232,226,214,0.72);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex-shrink: 0;
  transition: background 0.15s, border-color 0.15s;
}
.v3-bubble svg { width: 17px; height: 17px; }

/* Notification pill — expands right & turns blue when there are unread notifications */
.v3-bubble--notif {
  overflow: hidden;
  transition: background 0.2s, border-color 0.2s, color 0.2s, padding 0.25s ease;
}
.v3-bubble-count {
  display: none;
  font-family: var(--font-main);
  font-size: 12.5px;
  font-weight: 700;
  line-height: 1;
  white-space: nowrap;
}
/* ⚠ `min-width` tracks `.v3-bubble`'s width. The expanded pill grows past it,
   but with a small `+5` count it can be narrower than the circle it grew from —
   so this is what stops it shrinking when it lights up. */
.v3-bubble--notif.has-notif {
  width: auto;
  min-width: 34px;
  border-radius: 999px;
  padding: 0 9px 0 8px;
  gap: 5px;
  /* keep the normal bubble fill; only the outline / stroke / count go colored */
  border-color: #3b82f6;
  color: #3b82f6;
}
.v3-bubble--notif.has-notif svg { color: #3b82f6; }
.v3-bubble--notif.has-notif .v3-bubble-count { display: block; color: #3b82f6; }
.v3-bubble:hover {
  background: rgba(255,255,255,0.13);
  border-color: rgba(255,255,255,0.2);
}
/* Light theme */
.s-home-v3--light .v3-header-logo { background-color: rgba(26,18,8,0.9); }
.s-home-v3--light .v3-bubble {
  background: rgba(26,18,8,0.06);
  border: 1px solid rgba(26,18,8,0.18);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.5);
  color: rgba(26,18,8,0.62);
}
.s-home-v3--light .v3-bubble:hover {
  background: rgba(26,18,8,0.12);
  border-color: rgba(26,18,8,0.28);
}

/* TOP BAR (search only) ────────────────────────────────────── */
.v3-topbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 16px 8px;
  flex-shrink: 0;
}
.v3-topbar-wordmark {
  font-size: 13px; font-weight: 800; letter-spacing: 0.5px;
  color: rgba(232,226,214,0.9); font-family: var(--font-main);
}
.v3-icon-btn {
  width: 30px; height: 30px; border-radius: 50%;
  background: rgba(255,255,255,0.07); border: none;
  color: rgba(232,226,214,0.7);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
}
.v3-search-btn {
  width: 88%;
  height: 34px;
  border-radius: 999px;
  background: rgba(255,255,255,0.09);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
  backdrop-filter: blur(14px) saturate(160%);
  border: 1px solid rgba(255,255,255,0.14);
  box-shadow: 0 2px 10px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.09);
  color: rgba(232,226,214,0.75);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.v3-search-btn:hover {
  background: rgba(255,255,255,0.15);
  border-color: rgba(255,255,255,0.22);
}
.s-home-v3--light .v3-search-btn {
  background: rgba(255,255,255,0.5);
  border: 1px solid rgba(255,255,255,0.65);
  box-shadow: 0 2px 10px rgba(30,20,10,0.1), inset 0 1px 0 rgba(255,255,255,0.85);
  color: rgba(26,18,8,0.65);
}

/* Scrollable body wrapper: bento + feed scroll together */
.v3-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;   /* the page is the phone's width, always — `overflow-y: auto` alone makes x scrollable too */
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  padding-top: 20px;   /* clearance so the bento's top drop shadow isn't clipped by the header edge */
  /* fade the scrolling content out toward the bottom, before it reaches the nav/toggle */
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 74px), transparent 100%);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 74px), transparent 100%);
}
.v3-body::-webkit-scrollbar { display: none; }

/* NOW-PLAYING TICKER — glossy "bubble" pill above the bento ──── */
/* The now-playing / friends ticker fills the bump. Its inset matches the
   plateau in BOTTOM_NAV_FULL_INDENT.svg — x 98.5→480.7 of 576 is 17.1%→83.5% — plus a
   little breathing room each side. `bottom` centres it in the plateau band: the
   bar is 68px tall docked at 0 and its shoulder is at 47.3%, so that band runs
   from 36px to 68px up and its middle is ~52px. */
.v3-nowbar {
  position: absolute;
  /* In the floating bubble's hump: see the geometry note on BOTTOM NAV. */
  left: 23%; right: 23%;
  bottom: 55px;
  z-index: 6;
  height: 22px;
  display: flex;
  align-items: center;
  /* It opens whoever is on it — see `renderNowBar`. The press is the only
     affordance it gets: the bar sits in the nav's bump and anything more
     (a border, a hover fill) would turn a piece of ambient chrome into a
     control competing with the six nav icons beside it. */
  cursor: pointer;
  transition: transform .12s, opacity .12s;
  /* Text + waveform ride together, centred in the bump. `.v3-now-text` is
     `flex: 0 1 auto` (not `1`) so the pair stays glued instead of the text
     eating the bar and pushing the bars to the far edge — centring only reads
     as centred because of that. Padding is symmetric for the same reason. */
  justify-content: center;
  gap: 7px;
  padding: 0 8px;
  overflow: hidden;
  /* no background of its own — it sits inside the glass console indent */
  background: none;
  border: none;
}
/* The press. Scale only — the bar has a live waveform in it, so a colour or
   opacity change on tap reads as the audio doing something. */
.v3-nowbar:active { transform: scale(0.975); }


/* ==========================================================================
   THE NAV CONSOLE - the hump grows to hold what a CD tap has to say
   ==========================================================================
   Tapping a CD used to raise a POPUP over the screen. It raises the nav's own
   hump instead: the bump grows, the friends ticker gives way to the album you
   tapped, and the room that opens up holds the four services.
   ⚠️ SAME PRINCIPLE ON THE FLOATING BUBBLE as on the docked bar it was built
   for (2026-09-03): the bubble has a hump too. What did NOT come across from
   the docked version: the unmasked `.v3-nav-blur`, the `.v3-nav-nest` plug and
   the `.v3-bottom-fade` band — those existed for a full-bleed bar with a scoop
   cut out of it, and this bar is a self-contained pill.

   WARNING: THE GEOMETRY LIVES IN THESE THREE VARIABLES AND NOWHERE ELSE. The
   nav's box, the glass mask and the items' band all read them, so the console
   state is ONE override block. The outline is the one exception -- an inline
   <svg> whose viewBox CSS cannot reach -- so `bottomNav()` ships both contours
   and this toggles which is visible.
   WARNING: the tall contour is the short one with every point below the hump
   pushed down 110 units and a straight wall inserted between the hump's fillet
   and the shoulder's, where the tangent is already vertical. The fillets are
   untouched, which is why nothing distorts under preserveAspectRatio="none":
   `--nav-ar` changes in step so it never has to stretch.
   WARNING: the items' band starts a little under the shoulder -- 40% of 126
   normally, (44.79+110+5.7)/236 = 68% when grown. (It was 88 units / 214;
   Eric wanted the panel to come out higher, and the panel was already at the
   hump's ceiling, so the ceiling went up.) The same line in absolute
   terms; a different fraction of a taller box. */
/* csharpuser (2026-09-23): THE PILL. No hump, no contour, no mask — the bar
   is a rounded box (`--nav-r`) at a fixed aspect (`--nav-ar`). The console
   state grows it upward into a rounded rect and drops the radius so the
   corners don't balloon; the icon band (`--nav-items-top`) keeps the bottom
   82 units of whichever box is showing, so the icons never move on screen.
   ⚠️ 240 units tall, not the 205 it opened at (Eric, 2026-09-25: the service
   marks sat on top of the nav icons and the album line sat on the marks).
   82 / 240 = 34.2% → the band starts at 65.8%; the extra 35 units are all
   air inside the panel — see `.v3-console` below for where it goes. */
.s-home-v3 {
  --nav-ar:        553 / 82;
  --nav-r:         999px;
  --nav-items-top: 0%;
}
.s-home-v3--console {
  --nav-ar:        553 / 240;
  --nav-r:         30px;
  --nav-items-top: 65.8%;
}

/* -- The console panel ---------------------------------------------------
   Sits in the hump the state above opened up. WARNING: anchored in px from
   the BOTTOM of the screen, not as a % of the nav -- the nav's height changes
   with the state, so a percentage would move this twice. The plateau's floor
   (the shoulder) is ~40px up at the 393 frame and its ceiling is the nav's top
   at ~113px, so 46 -> 108 sits inside it with air at both ends. */
.v3-console {
  /* All four numbers are % of the NAV, whose child this is — the nav is
     aspect-ratio driven, so only a % tracks the box at every phone scale.
     csharpuser (2026-09-23): the pill has no hump walls, so the panel takes
     nearly the whole width. Vertically it runs from under the top edge to
     well short of the icon band at 65.8%: bottom 42% puts the marks' bottom
     at 58%, ~19 units (13px) of air over the band, where there were 6 before
     (Eric, 2026-09-25). `space-between` then opens the album line and the
     marks ~24px apart inside the 132-unit panel, instead of ~9. */
  position: absolute;
  left: 7%; right: 7%;
  top: 6%; bottom: 42%;
  z-index: 6;
  display: none;
  flex-direction: column; justify-content: space-between; gap: 10px;
}
.s-home-v3--console .v3-console { display: flex; }
/* WARNING: the ticker and the console are never both up, and this is
   `visibility` rather than `display` on purpose -- `renderNowBar`'s swap timer
   keeps writing to the ticker while the console is open, and a `display: none`
   element has no box to measure. */
.s-home-v3--console .v3-nowbar,
.s-home-v3--friends .v3-nowbar { visibility: hidden; pointer-events: none; }

/* -- The friends panel ---------------------------------------------------
   In the hump the --friends state opens: a chevron to close, a label, then a
   list that scrolls inside itself when there are more friends than fit, fading
   out at its bottom so the last row reads as "more below" rather than a cut.
   Placed as a % of the NAV like the console: floor is the grown shoulder at
   744.79/826 = 90.2%, so `bottom: 10.5%` keeps the list off it. */
.v3-friends {
  position: absolute;
  left: 14%; right: 14%;
  top: 1.5%; bottom: 10.5%;
  z-index: 6;
  display: none; flex-direction: column;
  opacity: 0; transform: translateY(7px);
  transition: opacity .18s ease .09s, transform .22s cubic-bezier(.22, 1, .36, 1) .07s;
}
.s-home-v3--friends .v3-friends { display: flex; opacity: 1; transform: translateY(0); }
.v3-fr-close {
  align-self: center; flex-shrink: 0;
  width: 40px; height: 22px; padding: 0;
  background: none; border: 0; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  color: var(--sd-ink3);
  transition: color .15s, transform .1s;
}
.v3-fr-close svg { width: 16px; height: 16px; }
.v3-fr-close:hover  { color: var(--sd-ink); }
.v3-fr-close:active { transform: translateY(1px); }
.v3-fr-hd {
  flex-shrink: 0; padding: 0 4px 7px;
  font-family: var(--font-mono); font-size: 8.5px;
  text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--sd-ink3);
}
.v3-fr-list {
  flex: 1; min-height: 0;
  overflow-y: auto; scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  padding-bottom: 22px;
  -webkit-mask-image: linear-gradient(to bottom, #000 76%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 76%, transparent 100%);
}
.v3-fr-list::-webkit-scrollbar { display: none; }
.v3-fr-row {
  display: flex; align-items: center; gap: 9px;
  width: 100%; box-sizing: border-box;
  padding: 8px 4px; text-align: left;
  background: none; border: 0; cursor: pointer; color: inherit;
  border-top: 1px solid rgba(255,255,255,0.07);
  -webkit-tap-highlight-color: transparent;
}
.v3-fr-row:first-child { border-top: none; }
.v3-fr-row:hover  { background: rgba(255,255,255,0.05); }
.v3-fr-row:active { opacity: .7; }
.v3-fr-av {
  flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 10px; font-weight: 700; color: #fff;
}
.v3-fr-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.v3-fr-name { font-size: 11.5px; font-weight: 700; color: var(--star); line-height: 1.1; }
.v3-fr-line {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: 10.5px; line-height: 1.2; color: var(--sd-ink2);
}
.v3-fr-song   { font-weight: 600; color: var(--sd-ink); }
.v3-fr-album  { color: var(--sd-ink2); }
.v3-fr-artist { font-weight: 600; color: var(--sd-ink); }
.v3-fr-sep    { color: var(--sd-ink3); margin: 0 3px; }
/* The ticker's own waveform, one per row, so a row reads as "playing now". */
.v3-fr-wave { height: 9px; margin-left: 2px; }
.v3-fr-wave .v3-now-bar { width: 2px; }
.s-home-v3--light .v3-fr-name { color: #c8891f; }
.s-home-v3--light .v3-fr-row { border-top-color: rgba(26,18,8,0.1); }
.s-home-v3--light .v3-fr-row:hover { background: rgba(26,18,8,0.05); }

/* ONE line: art, album, year, artist. WARNING: the album is the only part that
   may shrink (`min-width: 0` + ellipsis on it alone). Let the year or the artist
   flex and a long title pushes them off the end instead of truncating itself,
   which loses the two facts that are always short. */
.v3-nc-row { display: flex; align-items: center; gap: 7px; min-width: 0; }
.v3-nc-art {
  flex-shrink: 0; width: 22px; height: 22px; border-radius: 4px;
  background-size: cover; background-position: center; background-color: rgba(0,0,0,.35);
}
.v3-nc-line { display: flex; align-items: baseline; gap: 5px; min-width: 0; flex: 1; }
.v3-nc-alb {
  min-width: 0; flex-shrink: 1;
  font-size: 11.5px; font-weight: 700; color: var(--sd-ink);
  font-family: var(--font-head); letter-spacing: -.2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.v3-nc-yr {
  flex-shrink: 0; font-size: 9.5px; color: var(--sd-ink3);
  font-family: var(--font-mono);
}
.v3-nc-artist {
  flex-shrink: 0; max-width: 42%;
  font-size: 9.5px; color: var(--sd-ink2); font-family: var(--font-mono);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The four services. WARNING: `space-between` over fixed-size marks rather than
   stretched buttons -- these are brand logos, and a logo whose width follows its
   container stops reading as a logo. */
.v3-nc-svcs { display: flex; align-items: center; justify-content: space-between; gap: 6px; }
.v3-nc-svc {
  flex: 0 0 auto; padding: 0; border: 0; background: none; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: transform .14s, opacity .14s;
}
.v3-nc-svc:active { transform: scale(0.9); }
.v3-nc-ico {
  position: relative; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  width: 38px; height: 38px; border-radius: 11px;
  box-shadow: 0 3px 9px rgba(0,0,0,.38);
}
.v3-nc-ico > svg { width: 62%; height: 62%; }

/* The two states `serviceGo` puts a button in: waiting on a lookup, and a
   service that has not got the record. The same language the menu rows spoke. */
.v3-nc-svc.is-wait { opacity: .45; }
.v3-nc-svc.none    { opacity: .45; }

/* -- Growing and shrinking -------------------------------------------------
   WARNING: what animates is `aspect-ratio` on the nav. Height is not settable
   here (it IS the aspect ratio), and a custom property cannot be transitioned
   -- but the property that READS the property can, so the swap of `--nav-ar`
   drives a real interpolation. The bar is anchored at `bottom: 14px`, so it
   grows UPWARD out of its own hump.
   WARNING: the mask and the outline swap DISCRETELY -- a data-URI mask cannot
   tween. For the ~260ms of the growth the tall contour is squashed into a
   shorter box, and because everything here is `preserveAspectRatio="none"`
   that reads as the hump unfolding rather than as a glitch. It is the reason
   the easing is an ease-out overshoot: the squash resolves fastest at the
   start, where it is largest.
   WARNING: the CONTENT is not carried by that -- it fades and rises on its own,
   one beat behind, or it appears fully drawn inside a hump that has not
   finished opening. */
.v3-bottom-nav { transition: aspect-ratio .26s cubic-bezier(.22, 1, .36, 1); }
.v3-console {
  opacity: 0;
  transform: translateY(7px);
  transition: opacity .18s ease .07s, transform .22s cubic-bezier(.22, 1, .36, 1) .05s;
}
.s-home-v3--console .v3-console { opacity: 1; transform: translateY(0); }
/* Anyone who would rather it just appear. */
@media (prefers-reduced-motion: reduce) {
  .v3-bottom-nav, .v3-console, .v3-friends { transition: none; }
}


/* ⚠ A real app icon, when one has been dropped in, covers the whole tile —
   drawing AND brand colour. The <img> removes itself `onerror`, so a missing
   file falls back to the vector rather than leaving an empty square, and the
   drawings stay the fallback rather than the goal. */
.sd-svc-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
  border-radius: inherit;
}
/* The two states `serviceGo` puts a button in: waiting on a lookup, and a
   service that has not got the record. The same language the menu rows spoke. */
/* ── The scene — the face cradled in the nav's scoop ────────────
   Positioned in the notch cut out of the bar's bottom edge (the notch exists to
   cradle it). Absolute against `.v3-bottom-nav` rather than being a flex item,
   so it lands on the scoop's own centre — which is 49.1%, NOT 50%: the notch in
   BOTTOM_NAV_FULL_INDENT.svg sits a few units left of centre.
   Holds the smile / lofi-kid frames painted by paintScene() (app.js). Takes the
   accent, so it re-tints with the album like every other dot asset. */
.sd-scene {
  position: absolute;
  left: 49.1%; transform: translateX(-50%);
  bottom: 2px;   /* the notch is only ~33px deep and the grid is 30 — barely any slack */
  z-index: 7;    /* above .v3-nav-emboss (6), which would otherwise cover it */
  /* 21×10 cells at ~3px pitch. ⚠️ 30px of the scoop's ~33px depth and 63 of its
     68px FLAT TOP (not its 123px opening — the notch narrows as it rises, so
     anything filling the depth has to fit the narrow end). Wide and short is
     the shape available, which is why the smile's mouth runs nearly the full
     width and the kid gets room for his headphones and a couple of notes. */
  width: 63px; height: 30px;
  pointer-events: none;
  color: var(--star, #e8a83c);
}
/* ══ The pet — six dots, one formation per reaction ═══════════════════════
   The same six the live pill's arrow is made of. Formation classes below move
   each dot; `.sd-face-dot`'s transition does the rest, so a reaction MORPHS out
   of the smile and settles back rather than cutting to a new sprite.

   ⚠️ Every offset is a multiple of the 20×20 ring the numbers were authored in,
   then scaled by `--sd-face-k` on the host — so the face can be resized without
   retyping 60 transforms. Keep new formations in those same units.
   ⚠️ Six dots is the budget. Reactions that need a seventh get cut, not
   squeezed: this box is 63×30px and detail is what killed the two mascots
   before it. Dots 1–2 are the eyes, 3–6 the mouth arc, left to right. */
.sd-face {
  /* ⚠️ 2.4, measured, not guessed: the formation's ink is 12.3 × 9.7px at k=1
     and the scoop gives it 55 × 26, so HEIGHT is the binding constraint
     (2.4 fills it; fitting the width instead would want 4.1 and overflow the
     notch). The first pass shipped 1.35 and the face read as a small ring
     floating in the bar rather than a character sitting in it. */
  /* ⚠️ Sized against the WORST formation, not the resting face. Measured at
     k=2.1 the smile was a comfortable 25.9×20.3 in a 55.4×26.4 box, but the
     equaliser hit 34.3 tall and broke 7.9px out the top of the scoop. Shrinking
     far enough to contain THAT would have left the face tiny, so the bars were
     re-cut as well (see `--music` below) and this came down a step. */
  --sd-face-k: 1.85;
  position: absolute; inset: 0;
  transform: scale(var(--sd-face-k));   /* offsets are authored at the 20×20 ring */
  transform-origin: center center;
}
.sd-face-dot {
  position: absolute; top: 50%; left: 50%;
  /* ⚠️ 3px, and it must scale WITH the face — do not counter-scale it to keep
     the dots small. The ring's look is a proportion, not a size: 3px of ink
     across an 11px mouth span, ~27%. Holding the dot at 4.2px while the offsets
     grew 2.4× dropped that to 16% and the face came out spindly and wrong even
     though every coordinate was byte-identical to the original. */
  width: 3px; height: 3px; margin: -1.5px;
  /* ⚠️ ROUNDED SQUARE, not a circle — 14% of the dot, which is SD_DOTS'
     `cornerFrac` and therefore the brand's dot. This shipped as `50%` because
     it was copied from `.v3-ring-dot`, and a circle reads as a different
     material next to every other dot asset in the app. */
  border-radius: 14%;
  background: currentColor;
  /* The same spring the pill's dots morph on — this IS the brand's motion. */
  transition: transform 0.4s cubic-bezier(0.34, 1.45, 0.5, 1), opacity 0.25s ease;
}
@media (prefers-reduced-motion: reduce) {
  .sd-face-dot { transition: none; }
}

/* Rest — two eyes over a four-dot smile. Recovered verbatim from the retired
   `.v3-ring--smile`, so the pet is the face the app already had. */
.sd-face--smile .sd-face-dot:nth-child(1) { transform: translate(-3.5px, -4px); }
.sd-face--smile .sd-face-dot:nth-child(2) { transform: translate( 3.5px, -4px); }
.sd-face--smile .sd-face-dot:nth-child(3) { transform: translate(-5.5px, 1.5px); }
.sd-face--smile .sd-face-dot:nth-child(4) { transform: translate(-2px,   4px); }
.sd-face--smile .sd-face-dot:nth-child(5) { transform: translate( 2px,   4px); }
.sd-face--smile .sd-face-dot:nth-child(6) { transform: translate( 5.5px, 1.5px); }

/* Blink — the eyes drop onto the brow line. Two dots move, four hold. */
.sd-face--blink .sd-face-dot:nth-child(1) { transform: translate(-3.5px, -2.4px) scaleY(0.4); }
.sd-face--blink .sd-face-dot:nth-child(2) { transform: translate( 3.5px, -2.4px) scaleY(0.4); }
.sd-face--blink .sd-face-dot:nth-child(3) { transform: translate(-5.5px, 1.5px); }
.sd-face--blink .sd-face-dot:nth-child(4) { transform: translate(-2px,   4px); }
.sd-face--blink .sd-face-dot:nth-child(5) { transform: translate( 2px,   4px); }
.sd-face--blink .sd-face-dot:nth-child(6) { transform: translate( 5.5px, 1.5px); }

/* Wink — one eye only, so the idle isn't a two-state flicker. */
.sd-face--wink .sd-face-dot:nth-child(1) { transform: translate(-3.5px, -4px); }
.sd-face--wink .sd-face-dot:nth-child(2) { transform: translate( 3.5px, -2.4px) scaleY(0.4); }
.sd-face--wink .sd-face-dot:nth-child(3) { transform: translate(-5.5px, 1.5px); }
.sd-face--wink .sd-face-dot:nth-child(4) { transform: translate(-2px,   4px); }
.sd-face--wink .sd-face-dot:nth-child(5) { transform: translate( 2px,   4px); }
.sd-face--wink .sd-face-dot:nth-child(6) { transform: translate( 5.5px, 1.5px); }

/* ⚠️ These were a heart outline and a six-point star, and NEITHER READ. Six
   4px dots cannot describe a shape you have to recognise — at 63×30px they are
   just a ring of dots, which is the identical mistake that killed the cat
   mascot and the landscape before it. What six dots CAN say is an EXPRESSION
   (the mouth arc bends, the eyes change size) and BARS. So every reaction below
   is the face emoting, and the equaliser is the one object formation that
   survives. Judge a new formation on a contact sheet at real size before
   wiring it up — on paper a heart is obviously fine, and it isn't.

   FAVOURITE — beaming: eyes squeezed shut with joy over a deep wide smile… */
.sd-face--love .sd-face-dot:nth-child(1) { transform: translate(-3.6px, -3.4px) scale(1.5, 0.4); }
.sd-face--love .sd-face-dot:nth-child(2) { transform: translate( 3.6px, -3.4px) scale(1.5, 0.4); }
.sd-face--love .sd-face-dot:nth-child(3) { transform: translate(-6.2px,  0.6px); }
.sd-face--love .sd-face-dot:nth-child(4) { transform: translate(-2.2px,  5px); }
.sd-face--love .sd-face-dot:nth-child(5) { transform: translate( 2.2px,  5px); }
.sd-face--love .sd-face-dot:nth-child(6) { transform: translate( 6.2px,  0.6px); }
/* …and open again, still grinning. Alternating the two is the delight. */
.sd-face--love2 .sd-face-dot:nth-child(1) { transform: translate(-3.6px, -4.6px) scale(1.4); }
.sd-face--love2 .sd-face-dot:nth-child(2) { transform: translate( 3.6px, -4.6px) scale(1.4); }
.sd-face--love2 .sd-face-dot:nth-child(3) { transform: translate(-6.2px,  0.6px); }
.sd-face--love2 .sd-face-dot:nth-child(4) { transform: translate(-2.2px,  5px); }
.sd-face--love2 .sd-face-dot:nth-child(5) { transform: translate( 2.2px,  5px); }
.sd-face--love2 .sd-face-dot:nth-child(6) { transform: translate( 6.2px,  0.6px); }

/* RATED — impressed: eyes go wide and the mouth drops to a small open O. */
.sd-face--star .sd-face-dot:nth-child(1) { transform: translate(-3.6px, -4.4px) scale(1.55); }
.sd-face--star .sd-face-dot:nth-child(2) { transform: translate( 3.6px, -4.4px) scale(1.55); }
.sd-face--star .sd-face-dot:nth-child(3) { transform: translate(-3px,   2.2px); }
.sd-face--star .sd-face-dot:nth-child(4) { transform: translate(-1px,   5px); }
.sd-face--star .sd-face-dot:nth-child(5) { transform: translate( 1px,   5px); }
.sd-face--star .sd-face-dot:nth-child(6) { transform: translate( 3px,   2.2px); }
/* …then it breaks into the widest grin it has. */
.sd-face--star2 .sd-face-dot:nth-child(1) { transform: translate(-4px, -4.2px) scale(1.2); }
.sd-face--star2 .sd-face-dot:nth-child(2) { transform: translate( 4px, -4.2px) scale(1.2); }
.sd-face--star2 .sd-face-dot:nth-child(3) { transform: translate(-6.8px, 0.4px); }
.sd-face--star2 .sd-face-dot:nth-child(4) { transform: translate(-2.4px, 5px); }
.sd-face--star2 .sd-face-dot:nth-child(5) { transform: translate( 2.4px, 5px); }
.sd-face--star2 .sd-face-dot:nth-child(6) { transform: translate( 6.8px, 0.4px); }

/* LISTEN LATER — the face droops asleep: eyes shut, mouth flattens, and the
   whole thing sinks a couple of px. Sleep is the absence of expression. */
.sd-face--sleep .sd-face-dot:nth-child(1) { transform: translate(-3.5px, -1.6px) scaleY(0.4); }
.sd-face--sleep .sd-face-dot:nth-child(2) { transform: translate( 3.5px, -1.6px) scaleY(0.4); }
.sd-face--sleep .sd-face-dot:nth-child(3) { transform: translate(-4.5px,  4.2px); }
.sd-face--sleep .sd-face-dot:nth-child(4) { transform: translate(-1.5px,  4.6px); }
.sd-face--sleep .sd-face-dot:nth-child(5) { transform: translate( 1.5px,  4.6px); }
.sd-face--sleep .sd-face-dot:nth-child(6) { transform: translate( 4.5px,  4.2px); }

/* LISTENED / anything musical — the six stand up as an equaliser and bounce.
   `music2` is the same bars at different heights; alternating the two is the
   bounce, with no keyframes to keep in sync with the JS clock.
   ⚠️ `transform-origin: center bottom` — the ONE formation that overrides the
   default centre. Bars have to grow up from a shared floor; scaled about their
   centres they grew in both directions at once, which is what put the tallest
   one 7.9px out of the scoop and read as a bar chart rather than a meter.
   ⚠️ Every bar sits at the same y (the floor) and differs ONLY in scaleY, so
   the ceiling is one number to reason about: floor 4.5 + half-dot 1.5, minus
   3px × scaleY. At the cap of 4.0 that spans ±6 units, ≈22px at k=1.85, inside
   a 26.4px box. Raise a bar past 4.0 and it leaves the notch again. */
.sd-face--music .sd-face-dot,
.sd-face--music2 .sd-face-dot { transform-origin: center bottom; }
.sd-face--music .sd-face-dot:nth-child(1) { transform: translate(-7.5px, 4.5px) scaleY(2); }
.sd-face--music .sd-face-dot:nth-child(2) { transform: translate(-4.5px, 4.5px) scaleY(3.4); }
.sd-face--music .sd-face-dot:nth-child(3) { transform: translate(-1.5px, 4.5px) scaleY(1.3); }
.sd-face--music .sd-face-dot:nth-child(4) { transform: translate( 1.5px, 4.5px) scaleY(4); }
.sd-face--music .sd-face-dot:nth-child(5) { transform: translate( 4.5px, 4.5px) scaleY(2.2); }
.sd-face--music .sd-face-dot:nth-child(6) { transform: translate( 7.5px, 4.5px) scaleY(3.1); }
.sd-face--music2 .sd-face-dot:nth-child(1) { transform: translate(-7.5px, 4.5px) scaleY(3.4); }
.sd-face--music2 .sd-face-dot:nth-child(2) { transform: translate(-4.5px, 4.5px) scaleY(1.4); }
.sd-face--music2 .sd-face-dot:nth-child(3) { transform: translate(-1.5px, 4.5px) scaleY(3.9); }
.sd-face--music2 .sd-face-dot:nth-child(4) { transform: translate( 1.5px, 4.5px) scaleY(1.7); }
.sd-face--music2 .sd-face-dot:nth-child(5) { transform: translate( 4.5px, 4.5px) scaleY(3.6); }
.sd-face--music2 .sd-face-dot:nth-child(6) { transform: translate( 7.5px, 4.5px) scaleY(1.5); }

/* LIKED / FOLLOWED / PLAYLISTED — the generic yes: a tick. */
.sd-face--yes .sd-face-dot:nth-child(1) { transform: translate(-6px,  -0.5px); }
.sd-face--yes .sd-face-dot:nth-child(2) { transform: translate(-3.5px, 2px); }
.sd-face--yes .sd-face-dot:nth-child(3) { transform: translate(-1px,   4.4px); }
.sd-face--yes .sd-face-dot:nth-child(4) { transform: translate( 1.6px, 1.4px); }
.sd-face--yes .sd-face-dot:nth-child(5) { transform: translate( 4.2px,-1.8px); }
.sd-face--yes .sd-face-dot:nth-child(6) { transform: translate( 6.8px,-5px); }

/* now shown in every state, including the fullscreen review (album / artist) view —
   the friends-listening ticker stays visible above the nav at all times */
.v3-now-text {
  /* `flex: 1` made this eat the whole bar and shove the waveform to the far
     right; `0 1 auto` lets it take only what it needs so the bars sit with the
     text, and it still shrinks + ellipses on a long line. */
  flex: 0 1 auto;
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: 4px;
  white-space: nowrap;
  overflow: hidden;
  font-size: 11.5px;
  /* Roboto Flex, width axis at its minimum (25) to pack in more info; opsz raised */
  font-family: 'Roboto Flex', var(--font-main);
  font-variation-settings: 'wdth' 25, 'opsz' 25;
  /* fade the right edge so long lines don't hard-clip against the wave */
  -webkit-mask-image: linear-gradient(90deg, #000 84%, transparent);
  mask-image: linear-gradient(90deg, #000 84%, transparent);
  transition: opacity 0.28s ease, transform 0.28s ease;
}
.v3-nowbar.is-swapping .v3-now-text { opacity: 0; transform: translateY(-4px); }
.v3-now-name   { font-weight: 700; color: var(--star); flex-shrink: 0; }
.v3-now-sep    { color: rgba(232,226,214,0.35); }
.v3-now-song   { font-weight: 600; color: rgba(232,226,214,0.92); }
.v3-now-album  { font-weight: 400; color: rgba(232,226,214,0.7); }
.v3-now-artist { font-weight: 700; color: rgba(232,226,214,0.92); }
/* colorful animated waveform */
.v3-now-wave {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 1.5px;
  height: 11px;
  padding-right: 2px;
}
.v3-now-bar {
  width: 2.5px;
  height: 100%;
  border-radius: 2px;
  background: #fff;
  transform: scaleY(0.28);
  transform-origin: center;
  opacity: 0.92;
  animation-name: nowWave;
  animation-iteration-count: infinite;
  animation-timing-function: ease-in-out;
}
@keyframes nowWave {
  0%, 100% { transform: scaleY(0.25); }
  50%      { transform: scaleY(1); }
}
/* Light theme */
.s-home-v3--light .v3-now-name   { color: #c8891f; }
.s-home-v3--light .v3-now-sep    { color: rgba(26,18,8,0.35); }
.s-home-v3--light .v3-now-song,
.s-home-v3--light .v3-now-artist { color: rgba(26,18,8,0.92); }
.s-home-v3--light .v3-now-album  { color: rgba(26,18,8,0.6); }
.s-home-v3--light .v3-now-bar    { background: rgba(26,18,8,0.8); }

/* SCROLL AREA ──────────────────────────────────────────────── */
.v3-scroll-area {
  /* Matches .ntf-scroll — the feed inside is the inbox's row list, and the old
     30px top gap left the first sticky group header floating well below the
     bento. Bottom clears the nav/nowbar overlay so nothing is cut off. */
  padding: 10px 12px 75px;
}
/* ⚠️ The HOME feed runs to the BENTO's edges (Eric, 2026-09-15): the bento
   sits 10px in from the body, and the cards want every px of width the
   record row can get, so the sides here are the bento's 10px and the cards
   below carry 3px of their own. */
.s-home-v3 .v3-scroll-area { padding-left: 10px; padding-right: 10px; }

/* ── Review upvote pill (thumbs up + count) ───────────────────
   Replaces the old static "♥ n" on review cards and friend-feed cards.
   State lives in REV_VOTES (app.js) so it survives re-renders. */
/* The like / comment pill. ⚠️ Every caller uses `--sm`, so THAT is the size
   that ships — the base is the reference. Both were bumped a step: at 9px with
   a 10px glyph they read as decoration rather than as controls you can hit. */
.v3-up {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 4px 10px 4px 8px; border-radius: 999px;
  border: none; background: rgba(255,255,255,0.06);
  color: rgba(232,226,214,0.5);
  font-family: var(--font-mono); font-size: 11px; font-weight: 700;
  line-height: 1; cursor: pointer;
  transition: background .14s, color .14s;
}
.v3-up svg { width: 14px; height: 14px; flex-shrink: 0; }
.v3-up--sm { font-size: 10.5px; padding: 4px 9px 4px 7px; }
.v3-up--sm svg { width: 13px; height: 13px; }
/* ⚠️ The FEED's pills run bigger than `.v3-up--sm` elsewhere. They are the
   only tap target on a row you are meant to scan past quickly, and at the
   shared size they were the smallest thing on screen. Scoped to --feed so
   the review cards' pills, which sit in a denser column, keep theirs. */
.v3-up--sm.v3-up--feed { font-size: 11.5px; padding: 5.5px 12px 5.5px 10px; }
.v3-up--sm.v3-up--feed svg { width: 14.5px; height: 14.5px; }
.v3-up:hover { background: rgba(255,255,255,0.11); color: #e8e2d6; }
.v3-up.is-on { background: rgba(232,168,60,0.16); color: var(--star); }
/* The comment pill is the same object as the upvote pill, so the pair reads as
   one control. `is-open` means its thread is showing below. */
.v3-cmt-btn.is-open { background: rgba(232,168,60,0.16); color: var(--star); }
/* THE BUBBLE'S DOTS (Eric, 2026-09-23, "to match the heart"): hidden at
   rest, and every three seconds they come up one after another — dot, dot,
   dot — hold, and go. Once YOU have commented (`.is-mine`, cmtHasMine) the
   bubble turns blue, filled, and the dots sit solid in the screen's colour. */
.v3-cmt-btn .cmt-dot { opacity: 0; animation: cmt-dot 4s ease-in-out infinite; }   /* once every 4s, quick (Eric) */
.v3-cmt-btn .cmt-dot:nth-of-type(2) { animation-delay: .12s; }
.v3-cmt-btn .cmt-dot:nth-of-type(3) { animation-delay: .24s; }
@keyframes cmt-dot {              /* the same ~half-second blink, on a 4s clock */
  0%, 100%  { opacity: 0; }
  2.5%, 9.5% { opacity: 1; }
  13%       { opacity: 0; }
}
.v3-cmt-btn.is-mine { color: #3b82f6; }
.v3-cmt-btn.is-mine svg path { fill: #3b82f6; stroke: #3b82f6; }
.v3-cmt-btn.is-mine .cmt-dot { opacity: 1; animation: none; fill: var(--sd-bg, #111116); stroke: none; }
@media (prefers-reduced-motion: reduce) { .v3-cmt-btn .cmt-dot { animation: none; opacity: 1; } }
/* On a CARD the open state means nothing any more (the thread lives on the
   review page, and CMT_OPEN stays true once you have been there), so the pill
   keeps its resting look. On the page it lights as before. */
.v3-rev-card .v3-cmt-btn.is-open { background: rgba(255,255,255,0.06); color: rgba(232,226,214,0.5); }

/* ⚠️ `.v3-up`'s own colours are hard-coded for a DARK surface, because every
   other place it appears — the review cards — sits on the album's procedural
   colour, which is dark in BOTH themes. The home feed sits on the SCREEN bg,
   which is cream in the light theme, so its pills re-declare themselves off the
   shared --sd-* tokens. Same trap as --vinyl-empty: the value follows the
   surface, not the theme. */
.v3-up--feed {
  background: var(--sd-pill);
  color: var(--sd-ink3);
}
.v3-up--feed:hover { background: var(--sd-pill-hi); color: var(--sd-ink); }
.v3-up--feed.is-on { background: rgba(232,168,60,0.18); color: var(--star); }
.v3-up.is-on svg { fill: rgba(232,168,60,0.22); }
.v3-brand-row {
  display: flex;
  align-items: baseline;
  gap: 7px;
  padding: 10px 0 8px;
}
.v3-brand-name {
  font-size: 20px;
  font-weight: 800;
  letter-spacing: -1px;
  color: rgba(232,226,214,0.9);
  font-family: var(--font-main);
}
.v3-brand-sub {
  font-size: 11px;
  color: rgba(232,226,214,0.28);
  letter-spacing: 0.2px;
}
.v3-feed-hd {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 6px;
}
.v3-feed-title {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.6px;
  text-transform: uppercase;
  color: rgba(232,226,214,0.35);
}
.v3-feed-seeall {
  font-size: 11px;
  background: none;
  border: none;
  color: var(--v3-accent, #c8a060);
  cursor: pointer;
  font-family: var(--font-main);
  padding: 0;
}
/* ── Home activity feed ───────────────────────────────────────
   Deliberately NOT styled here. The feed IS the notifications component —
   `.ntf-group` / `.ntf-row` / `.ntf-ava` / … , further down — and the home shell
   now carries the same `--sd-*` tokens, so it inherits the inbox's look whole.
   Adding home-only rules here is what made the two screens diverge the first
   time; if a row needs to change, change `.ntf-row` and let both screens move
   together. (The old `.v3-friend-card` feed this replaced is gone.) */

/* BOTTOM NAV — the floating glass console ─────────────────────────
   A wide rounded bar with a raised centre hump (the now-playing ticker sits in
   it) and FIVE buttons in the lower bar: Home · Trending · Shop · Playlists ·
   Profile. Floats centred, 14px off the bottom, over the content; the frosted
   fill obscures whatever scrolls behind it. Silhouette viewBox 553×126 — the
   original BOTTOM_NAV shape — ~63px tall at 72% of the 385px mockup.

   ⚠️ BACK BY DECISION (2026-09-03). It was docked from 2026-08-20: full-bleed,
   flat-bottomed, a scoop cut from its bottom edge for the pet and then the
   shop button, a plateau that grew into the CD console, and four helper boxes
   sharing its geometry (`.v3-nav-blur`, `.v3-nav-nest`, `.v3-nav-emboss`,
   `.v3-bottom-fade`). Eric brought the bubble back and asked for the shop as
   a plain nav icon — so the scoop and its furniture went, along with
   `.v3-nav-gap` and the nth-child nudges that kept icons off it. ⚠️ THE CD
   CONSOLE STAYED: the bubble has a hump, so the same principle applies — see
   THE NAV CONSOLE above.
   `images/BOTTOM_NAV_FULL_INDENT.svg` stays on disk as the docked silhouette.

   ⚠️ The hump's interior is x 67→485 of 553 = 12.1%→87.7% of the bar, and the
   bar is 72% of the screen starting at 14% — so on SCREEN the hump spans
   ~22.7%→77.2%, which is where `.v3-nowbar` (left/right 23%) comes from. Its
   floor, the shoulder, is y=44.79 of 126 = 35.5%; at 63px tall and 14px off
   the bottom that line sits ~54.6px up, which is the ticker's `bottom: 55px`.
   Change the width and every one of those moves with it. */
.v3-bottom-nav {
  position: absolute;
  left: 50%; transform: translateX(-50%);   /* centred */
  bottom: 27px;                              /* floating off the bottom — 14 → 19 (Eric, 0.12, 2026-10-02: "raise the nav bar by like 5px") → 27 (0.20, 2026-10-13: "push the bottom nav up by like 8 px") */
  width: 72%;                                /* the only scale lever — keeps proportions */
  /* Keeps the fillet curves undistorted — height follows the width. Don't swap
     this for a fixed height: the SVG is preserveAspectRatio="none" and WILL
     stretch. `--nav-ar` so the console state can grow it (see THE NAV CONSOLE). */
  aspect-ratio: var(--nav-ar);
  z-index: 5;
}
/* Frosted-glass fill, clipped to the console shape */
.v3-nav-glass {
  position: absolute; inset: 0;
  z-index: 0; pointer-events: none;
  background: rgba(26,26,32,0.42);
  -webkit-backdrop-filter: blur(22px) saturate(175%);
          backdrop-filter: blur(22px) saturate(175%);
  box-shadow: 0 10px 34px rgba(0,0,0,0.42);
  /* csharpuser (2026-09-23): the pill. The hairline that used to be a stroked
     <svg> contour is a border now — same 1px, same alpha. */
  border-radius: var(--nav-r);
  border: 1px solid rgba(255,255,255,0.2);
  transition: border-radius .26s cubic-bezier(.22, 1, .36, 1);
}
/* 5 buttons — spread across the lower bar (below the centre hump). The lower
   band starts a touch under the shoulder (35.5%) so the icons sit centred in
   the bar's body rather than on the shoulder line. */
.v3-nav-items {
  /* WARNING: `--nav-items-top`, not a literal: where the band starts changes
     when the hump grows for the console. */
  position: absolute; left: 0; right: 0; top: var(--nav-items-top); bottom: 0;
  display: flex; align-items: center; justify-content: space-around;
  z-index: 2;
}
.v3-nav-item {
  flex: 1; height: 100%;
  background: none; border: none; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  color: rgba(232,226,214,0.45);
  transition: color .18s, transform .1s;
}
.v3-nav-item.active { color: #e8e2d6; }
.v3-nav-item svg { width: 21px; height: 21px; transition: transform .18s; }
/* The shop's bag is a dot icon (SD_ICONS.bag, a 5×5 sd-dot-ico) — same 21px
   box as the stroked icons beside it, and it fills with currentColor so it
   lights and dims with them. */
.v3-nav-item .sd-dot-ico { width: 21px; height: 21px; }
.v3-nav-item:hover { color: #e8e2d6; }
.v3-nav-item:hover svg { transform: scale(1.1); }
.v3-nav-item:active { transform: scale(0.92); }

/* ── SCROLLED (csharpuser, 2026-09-24) ──────────────────────────────────
   `.is-scrolled` on the shell (sdScrollWatch): the wordmark and handle go,
   the bubbles either side stay; and "back 2 top" rises out of the nav pill
   where the hump was. The tab is the pill's own glass, a size down. */
.v3-header-brand { transition: opacity .22s ease, transform .22s ease; }
.s-home-v3.is-scrolled .v3-header-brand { opacity: 0; transform: translateY(-6px); pointer-events: none; }
.v3-nav-top {
  position: absolute; left: 50%; top: -14px;
  transform: translate(-50%, 10px); opacity: 0; pointer-events: none;
  z-index: 3;
  padding: 5px 13px 6px; border-radius: 999px;
  background: rgba(26,26,32,0.55);
  -webkit-backdrop-filter: blur(22px) saturate(175%); backdrop-filter: blur(22px) saturate(175%);
  border: 1px solid rgba(255,255,255,0.2);
  box-shadow: 0 6px 18px rgba(0,0,0,0.35);
  color: #e8e2d6;
  font-family: var(--font-main); font-size: 10.5px; font-weight: 700; letter-spacing: 0.02em; line-height: 1;
  cursor: pointer; white-space: nowrap;
  transition: opacity .2s ease, transform .26s cubic-bezier(.22, 1, .36, 1);
}
.s-home-v3.is-scrolled .v3-nav-top { opacity: 1; transform: translate(-50%, -100%); pointer-events: auto; }
.v3-nav-top:active { transform: translate(-50%, -100%) scale(0.96); }
.s-home-v3--light .v3-nav-top { background: rgba(255,255,255,0.6); border-color: rgba(26,18,8,0.16); color: #1a1208; box-shadow: 0 6px 18px rgba(30,20,10,0.16); }
@media (prefers-reduced-motion: reduce) { .v3-header-brand, .v3-nav-top { transition: none; } }

/* Registered property so --v3-accent can be CSS-transitioned */
@property --v3-accent {
  syntax: '<color>';
  inherits: true;
  initial-value: #c8a060;
}
.s-home-v3 { transition: --v3-accent 0.55s ease, background-color 0.4s ease; }

/* Master SVG frame overlay — no strokes, purely structural */
.v3-master-frame {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: auto;
  pointer-events: none;
  z-index: 5;
  overflow: visible;
  color: transparent;
}
.v3-master-frame path,
.v3-master-frame circle { stroke: none; }
.s-home-v3--light .v3-master-frame { color: transparent; }

/* BENTO GRID ──────────────────────────────────────────────── */
/* Bento: aspect-ratio matches SVG viewBox 690×670 so children can be % positioned */
.v3-bento {
  position: relative;
  /* ⚠ This ratio exists a SECOND time, as the number `--v3-bento-hw` on
     `.s-home-v3` — the shop's scale model has to compute a height from it and
     `aspect-ratio` is a ratio type, not something `calc()` can divide. Change
     one, change the other. (730 ÷ 689 = 1.0595.) */
  aspect-ratio: 689 / 730;
  overflow: visible;
  flex-shrink: 0;
  margin: 10px 10px 22px;
}
/* Hand swap: the only cell that slides is the For You box (it has a left transition) —
   freeze it so it snaps instead of floating across. The arrow dots morph visibly. */
.v3-hand-swapping .v3-for-single { transition: none; }

/* ── Bento skins (see bentoSkinBackHtml / bentoSkinFrontHtml in screens.js) ──
   BACK is a <g> inside the fill's svg: the svg is `overflow: visible`, so the
   ears above y=0 and the curl below y=730 still draw, and the svg's one
   drop-shadow is cast by the union. FRONT is two flat <svg>s in the bento's
   box: the ears band above the top edge, the curl band below the bottom one.
   Colours read the same `--v3-box1-color` the fill does, so the skin follows
   the album's procedural colour. Hand mode mirrors both about the centre. */
.v3-skin-back, .v3-skin-front { display: none; }
body.sd-skin-furry .v3-skin--furry { display: block; }
.v3-bg-fill { overflow: visible; }
.v3-skin-front {
  position: absolute; left: 0; width: 100%;
  pointer-events: none;
  z-index: 4;                 /* over the cells' shadow and the feed's sticky header (2); under the frame (5) */
}
.v3-skin-front--ears { top: -11.91%; height: 11.91%; }     /* 86.945 / 730 */
.v3-skin-front--tail { top: 99.88%;  height: 10.26%; }     /* 729.147 / 730 · 74.908 / 730 */
/* Mirror with the hand toggle. The back <g> keeps its -86.945 lift; the
   front svgs flip about their own centre, which is the bento's.
   ⚠️ Furry was drawn over the LEFT-hand bento (tail under the For You column,
   not the cover), so it is the RIGHT-hand default that mirrors, not --left. */
.s-home-v3:not(.s-home-v3--left) .v3-skin-back  { transform: matrix(-1, 0, 0, 1, 689, -86.945); }
.s-home-v3:not(.s-home-v3--left) .v3-skin-front { transform: scaleX(-1); }
.v3-skin-outer { fill: var(--v3-box1-color, #3a1e2e); }
.v3-skin-inner { fill: color-mix(in srgb, var(--v3-box1-color, #3a1e2e) 75%, #fff); }
.s-home-v3--light .v3-skin-outer { fill: var(--v3-box1-color, #f0ece3); }
.s-home-v3--light .v3-skin-inner { fill: color-mix(in srgb, var(--v3-box1-color, #f0ece3) 75%, #fff); }
/* The shop tile's preview: the ears, cropped to the field. */
/* Furry's tile in the shop's Themes shelf (it was `.shop-field--skin`, a row
   of its own, until 2026-09-17): the ears on the theme's swatch. */
.shop-field--furry { color: #d9a066; }
.shop-field--furry svg { width: 62%; height: auto; }

/* ── FURRY on the PROFILE CARD: dog ears over your photo (Eric, 2026-09-17) ──
   Drawn INSIDE .prof-base (profEarsHtml in screens.js), standing on the name
   banner's flat top above the picture pane, so the card's own emboss
   drop-shadow shapes them and they take the card's colour — the same idea as
   the bento's ears. Only YOUR card emits them; the body class shows them.
   They stand ~52 units (≈28px at the 393 frame) above the card. The skin
   already pads every .v3-body to 40px when worn (for the bento's ears), so
   the canvas only adds a little air on top of that. */
.prof-ears { display: none; }
body.sd-skin-furry .prof-ears { display: block; }
.prof-ears-outer { fill: var(--pf-base); }
.prof-ears-inner { fill: color-mix(in srgb, var(--pf-base) 75%, #fff); }
body.sd-skin-furry .prof-canvas--ears { margin-top: 14px; }
/* The ears stand ~12% of the bento above its top edge (~47px at the 393
   frame), and the body's 20px clearance plus the bento's 10px margin only
   give them 30 — the scroller would clip the tips. Room for them when worn. */
body.sd-skin-furry .v3-body { padding-top: 40px; }

/* Background fill SVG — fills only inside the outer bento frame shape */
.v3-bg-fill {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: auto;
  pointer-events: none;
  z-index: 0;
  color: var(--v3-box1-color, #3a1e2e);
  filter:
    drop-shadow(0 6px 14px rgba(0,0,0,0.5))
    drop-shadow(0 16px 34px rgba(0,0,0,0.55));
}
.v3-bg-fill path { stroke: none; }
.s-home-v3--light .v3-bg-fill {
  color: var(--v3-box1-color, #f0ece3);   /* procedural light tint (falls back to cream) */
  filter:
    drop-shadow(0 8px 16px rgba(30,20,10,0.18))
    drop-shadow(0 20px 40px rgba(30,20,10,0.26));
}

/* All bento cells absolutely positioned from SVG coordinate space */
/* z-index hierarchy: 0 bg-fill · 1 album/blue · 2 for-single · 3 cd · 5 master-frame */
.v3-album {
  position: absolute;
  /* Derived, not eyeballed. The shell's album region is x 0.5→538.5,
     y 0.5→534.52 (`.v3-bg-fill`'s bg-right path; the bottom is where
     `.v3-blue` starts at 83.78%). So: 0.5/689, 0.5/638, 538/689, 534.02/638.
     The old 0.07 / 0.08 / 78.01 / 83.70 left the cover ~0.5 units short on the
     right, i.e. a half-pixel crescent of shell down that edge. */
  left: 0.0726%; top: 0.0685%; width: 78.0842%; height: 73.1534%;
  background-size: cover;
  background-position: center;
  cursor: pointer;
  /* ⚠️ The radius has to be a PERCENTAGE, not px. Every other number on this
     element is a % of the 689×638 viewBox, so the box rescales with the phone
     while a px radius stays put — it can only be correct at one width. The
     frame's corner is 20 units; at the 365px bento that's 10.60px, and the 10px
     this used to carry left a 0.6px crescent of the box1 fill showing at the
     top-left, top-right and bottom-right. That is the corner "gap".
     20 units ÷ 537.49 wide = 3.721%, ÷ 534.01 tall = 3.7451% — the `/` form is
     what keeps the corner CIRCULAR on a box that isn't square (one percentage
     resolves against width on one axis and height on the other, i.e. an
     ellipse). Bottom-left stays square: it's the step junction. */
  border-radius: 3.71747% 3.71747% 3.71747% 0 / 3.74499% 3.74499% 3.74499% 0;
  overflow: hidden;
  z-index: 1;
  transform: translate(var(--album-dx, 0%), var(--album-dy, 0%));
}
/* ── Optical nudge ────────────────────────────────────────────
   The box above is DERIVED — it lands on the shell's edges to the unit. This
   is the eyeball correction on top, because the arithmetic and the eye don't
   agree at this size: the two shapes are antialiased curves painted by
   different rasterisers (an SVG path fill vs. a CSS border-radius), so where
   they *look* flush isn't where they *are* flush.

   ⚠️ **Percentages, not px** — and that is the whole point of this block.
   Settled by eye at -0.5px / -0.2px on the 385px mockup, then converted, so
   the correction is a PROPORTION of the cover and holds at every phone width
   instead of only that one. Same rule as the radii above: a px offset here is
   correct at exactly one size and wrong everywhere else.
   ⚠️ **`translate()` percentages resolve against the ELEMENT, not the parent**
   — so these are % of the cover's own 538.0 × 534.02 units, not of the 689×638
   viewBox. That's what makes the conversion asymmetric.

   Turning it by eye — at the 385px mockup 1 phone px is:
     horizontal  0.35087%   (1.8877 units ÷ 538.00)
     vertical    0.35348%   (1.8877 units ÷ 534.02)
   so the current values are exactly -0.5px and -0.2px. Negative = left / up.

   It stays a transform rather than being folded into `left`/`top` so the box
   above remains a clean derivation and this remains the one thing to turn. */
.s-home-v3 {
  --album-dx: -0.175434%;   /* -0.5px */
  --album-dy: -0.070697%;   /* -0.2px */
  /* The bento's height ÷ its width, i.e. its `aspect-ratio` as a number the
     shop can multiply by. See the ⚠ on `.v3-bento` — these two are one number. */
  --v3-bento-hw: 1.0595;
}
/* Fullscreen cover butts against nothing, so it wants no correction. */
.s-home-v3--review .v3-album { --album-dx: 0%; --album-dy: 0%; }

/* ── Who gets the finger on the cover ─────────────────────────────
   `touch-action` is decided BEFORE any JS runs — the browser latches it at
   touch-start — so it has to be declarative state, not a flag written by
   whichever gesture happened to wire itself last. It used to be exactly that:
   two inline writes racing (`setupAlbumSwipe` wrote `pan-y`, `proWheelInit`
   wrote `none`), and inline beat every rule in this file, so the CSS could not
   have the last word even when it was right.

     .v3-album          the swipe reads horizontal drags; vertical stays the
                        page's, so the home screen still scrolls under a finger
                        that lands on the cover
     --wheel            Pro's shelf wheel drags DOWNWARD, so it has to claim the
                        axis the swipe gave back (added by `proWheelInit`)
     --review           the album page. Both gestures are off here (see
                        `bentoGesturesOn` in app.js) — the cover is a header, so
                        every drag belongs to the page, and the cover is the
                        biggest thing on screen to start a scroll on.
   ⚠ Specificity is what orders these, not source order: `--review` is (0,2,0)
   and beats `--wheel`'s (0,1,0) wherever the two meet. */
.v3-album { touch-action: pan-y; }
/* A HOLD opens the wheel, so the phone's own long-press (callout, selection
   loupe) must stay out of it — it cancels the pointer mid-hold. */
.v3-album--wheel { touch-action: none; -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
/* Nothing here answers a tap either — `enterAlbumPage` early-returns when the
   shell is already fullscreen — so stop advertising one. */
.s-home-v3--review .v3-album { touch-action: pan-y; cursor: default; }

/* Right column wrapper: search corner on top, red box below */
.v3-right-col {
  display: flex;
  flex-direction: column;
}
.v3-search-corner {
  flex-shrink: 0;
  height: 46px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  background: #111116;
  border-radius: 0 15px 0 0;
  position: relative;
}

/* Fillets: filter on outer div, mask on ::after — keeps drop-shadow following the arc */
.v3-fillet {
  position: absolute;
  width: 20px;
  height: 20px;
  pointer-events: none;
  z-index: 1;
}
.v3-fillet::after {
  content: '';
  position: absolute;
  inset: 0;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}
.v3-fillet-bl {
  bottom: -1px;
  left: -0.5px;
}
.v3-fillet-bl::after {
  background: var(--v3-box1-bg, linear-gradient(155deg, #1c0a0a, #260e0e));
}
.v3-fillet-tl {
  top: -1px;
  left: -1px;
}
.v3-fillet-tl::after {
  background: var(--v3-box2-bg, linear-gradient(155deg, #09101d, #0c1628));
}

/* Red box: Trending (shortened — below search corner) */
.v3-red {
  background: var(--v3-box1-bg, linear-gradient(155deg, #1c0a0a, #260e0e));
  display: flex;
  flex-direction: column;
  flex: 1;
  overflow: hidden;
  border-radius: 0 15px 15px 0;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.06);
  position: relative;
}
.v3-red-title {
  font-size: 7.5px; font-weight: 700;
  letter-spacing: 1.1px; text-transform: uppercase;
  color: var(--v3-accent, #9a2828);
  padding: 10px 10px 6px; flex-shrink: 0;
}
.v3-red-thumbs {
  flex-shrink: 0;
  display: flex;
  gap: 5px;
  margin: 9px 9px 5px;
}
.v3-red-thumb {
  flex: 1;
  aspect-ratio: 1;
  border-radius: 8px;
  background-size: cover;
  background-position: center;
  cursor: pointer;
}
.v3-red-next-img {
  flex: 1;
  min-height: 0;
  margin: 0 9px 9px;
  border-radius: 11px;
  background-size: cover;
  background-position: center;
  cursor: pointer;
}
/* ForYou single panel */
.v3-for-single {
  position: absolute;
  /* Exact sizing from NewBentoSvgs For You box: x557–670, y105–520 in 689×638 space */
  left: 80.84%; top: 14.3836%; width: 16.40%; height: 56.8493%;
  background-size: cover; background-position: center;
  background-color: #2a2520;
  /* ⚠️ Percentage, same reason as .v3-album — and this one was the loud gap:
     the For-You panels round at 15 units (7.95px at the 365px bento) and this
     carried 11px, so the art curved away 3px before the frame did on all four
     corners. 15 ÷ 112.99 wide = 13.2748%, 15 ÷ 415.02 tall = 3.6143%; the two
     axes differ this much because the box is tall and narrow, and the `/` form
     is the only way to get a circular corner out of it. */
  border-radius: 13.2748% / 3.6143%;
  overflow: hidden;
  cursor: pointer;
  z-index: 2;
  transition: filter 0.2s, width 0.4s ease, height 0.4s ease, left 0.4s ease, top 0.4s ease;
}
.v3-for-single::after {
  content: '';
  position: absolute;
  inset: 0;
  box-shadow: inset 3px 3px 7px rgba(0,0,0,0.5), inset -3px -3px 6px rgba(255,255,255,0.07);
  pointer-events: none;
  z-index: 3;
}
.v3-for-single:active { filter: brightness(1.15); }
.s-home-v3--light .v3-for-single {
  background-color: #c4bbb0;
}
.s-home-v3--light .v3-for-single::after {
  box-shadow: none;   /* emboss read badly on cream — flat edge instead */
}
.v3-search-pill {
  position: absolute;
  top: 10px; right: 10px; width: 72px; height: 34px;
  z-index: 10;
  border-radius: 999px;
  background: #111116;
  border: none;
  box-shadow: -4px -4px 8px 0 rgba(255,255,255,0.07), 4px 4px 6px -1px rgba(0,0,0,0.5);
  display: flex; align-items: center; justify-content: center;
  cursor: pointer;
  color: rgba(232,226,214,0.85);
}

/* Candy-stripe album covers */
.v3-stripe-wrap {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column;
  overflow: hidden;
}
.v3-stripe-item {
  flex: 1; min-height: 0;
  background-size: cover; background-position: center;
  clip-path: polygon(0 0, 100% 10%, 100% 90%, 0 100%);
  margin-top: -8px;
  position: relative; cursor: pointer;
  transition: filter .2s;
}
.v3-stripe-item:hover { filter: brightness(1.12); }
.v3-stripe-item:nth-child(1) {
  flex: 1.35; margin-top: 0;
  clip-path: polygon(0 0, 100% 0, 100% 90%, 0 100%);
  z-index: 5;
}
.v3-stripe-item:nth-child(2) { z-index: 4; }
.v3-stripe-item:nth-child(3) { z-index: 3; }
.v3-stripe-item:nth-child(4) { z-index: 2; }
.v3-stripe-item:last-child {
  flex: 1.35;
  clip-path: polygon(0 0, 100% 10%, 100% 100%, 0 100%);
  z-index: 1;
}
.v3-more-btn {
  background: none; border: none;
  color: var(--v3-accent, #7a2828); font-size: 9px; font-weight: 700;
  cursor: pointer; font-family: var(--font-main);
  padding: 6px 10px 8px; text-align: left; letter-spacing: 0.3px;
  flex-shrink: 0;
}

/* Blue box: Reviews strip */
.v3-blue {
  position: absolute;
  left: 0.07%; top: 73.2192%; width: 78.08%; height: 26.664%;   /* was 16.14% — dips into the CD-gap below */
  background: transparent;
  padding: 9px 12px 8px;   /* bottom was 4px — extra clip room so the quote's descenders show */
  display: flex; flex-direction: column; align-items: flex-start; justify-content: space-between;
  gap: 0;
  cursor: pointer;
  border-radius: 0 0 10px 10px;
  overflow: hidden;
}
.v3-blue-info-row {
  display: flex;
  align-items: baseline;
  gap: 4px;
  width: 100%;
  position: relative; top: -2.5px;   /* nudge the album/artist/date line up */
  overflow: visible;   /* was hidden — clipped artist descenders (g/y/p). .v3-blue still clips. */
  line-height: 1.05;
  font-family: 'Roboto Flex', var(--font-main);
  font-size: 10.5px;
}
.v3-blue-artist {
  font-weight: 700;
  color: rgba(232,226,214,0.88);
  white-space: nowrap;
  flex-shrink: 0;
  font-size: 10.5px;
  transition: font-size 0.4s cubic-bezier(0.4,0,0.2,1);
}
.v3-blue-sep {
  font-size: 8px;
  color: rgba(232,226,214,0.22);
  flex-shrink: 0;
}
.v3-blue-album {
  font-weight: 400;
  color: rgba(232,226,214,0.55);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 0 1 auto;
  min-width: 0;
  font-size: 10.5px;
  /* Extend the ellipsis clip box downward so descenders (g/y/p) aren't shaved,
     then pull it back out of layout so the row height is unchanged (quote stays put). */
  padding-bottom: 3px;
  margin-bottom: -3px;
  transition: font-size 0.4s cubic-bezier(0.4,0,0.2,1), opacity 0.18s ease;
}
/* Compact bento only: flip the pair so the ALBUM leads and the artist recedes.
   The app-wide convention (album 400 / artist 700) still holds everywhere else —
   review, album page, artist page all keep the base rules above. */
.s-home-v3:not(.s-home-v3--review) .v3-blue-album {
  font-weight: 700;
  color: rgba(232,226,214,0.88);
}
.s-home-v3:not(.s-home-v3--review) .v3-blue-artist {
  font-weight: 400;
  color: rgba(232,226,214,0.55);
}
/* Compact bento info box — block metrics (the dev box's Gap / Pad T / Pad L).
   ⚠️ Mirrored by DEVBOX_FIELDS in app.js; change both or the panel silently
   emits the old numbers.
   ⚠️ The X / Y / Size knobs are ZEROED here, and the Size one has to stay that
   way: a `transform: scale()` on a grid item is a PAINT-time effect, so the
   grid sizes the track from the item's untransformed box and the scaled result
   spills outside it — measured at 11.6px of the album title hanging past the
   right edge of `.v3-blue`, which clips. The old single-line layout had no
   tracks to overflow, which is why the same numbers were fine there. Tune the
   type sizes below instead. */
.s-home-v3:not(.s-home-v3--review) .v3-blue {
  padding: 9px 12px 8px;
  column-gap: 10px;
}

/* ══ Compact bento info box — TWO COLUMNS ═══════════════════════════════════
   ┌──────────────────────────┬──────────┐
   │ Album · 2019             │          │
   │                          │     4.4  │   ← score, ~1.5 text lines tall
   │ Artist                   │  ●●●●◐   │
   └──────────────────────────┴──────────┘
   ⚠️ CSS only, and deliberately so. `.v3-blue`'s children are SHARED with the
   fullscreen review state, which stacks the very same elements as artist /
   album+year / stars in a single column — so re-nesting them in screens.js to
   make two columns here would mean re-deriving that state as well, along with
   the artist page that sits on top of it. Everything below is scoped
   `:not(--review)` and both rows stay real elements, which also keeps the dev
   box's X / Y / Size knobs pointed at something.
   ⚠️ Specificity, not order, is what makes these win: `.v3-blue { gap: 3px }`
   further down the file is (0,1,0) against this block's (0,3,0). Don't "tidy"
   these into the plain `.v3-blue` rules. */
.s-home-v3:not(.s-home-v3--review) .v3-blue {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto;
  align-items: center;
  /* ⚠️ The strip is ~195 viewBox units tall since the shell grew (was ~103).
     `space-between` pins the title/rating row to the TOP and the credits row to
     the BOTTOM, with the new room as one gap between them — rather than packing
     both to the top and leaving the slack dangling underneath.
     ⚠️ It degrades correctly: the credits row is `hidden` (display: none) on
     albums with no data, and with a single row `space-between` places it at the
     start, so the box looks exactly as it did before credits existed. */
  align-content: space-between;
  gap: 0 10px;
}
/* ⚠️ `grid-row: 1` is load-bearing, not decoration. With only a column named,
   auto-placement runs in sparse mode: the info row is first in the DOM, takes
   column 2 in the mirrored layout, and leaves the cursor past column 1 — so the
   stars row, asking for a column BEHIND the cursor, gets pushed to a second row.
   That is what "two rows, 24px and 35px" was: the rating stacked under the
   title instead of beside it, and the box overflowed by the difference. */
.s-home-v3:not(.s-home-v3--review) .v3-blue-info-row,
.s-home-v3:not(.s-home-v3--review) .v3-blue-stars-row {
  grid-row: 1;
  transform: none;   /* see the note on the dev box's Size knob above */
  top: 0; left: 0;
}
.s-home-v3:not(.s-home-v3--review) .v3-blue-info-row  { grid-column: 1; }
.s-home-v3:not(.s-home-v3--review) .v3-blue-stars-row { grid-column: 2; }

/* Left column — album · year over the artist. A grid rather than two flex rows
   so the year keeps its own track: the album can then ellipsize against
   `minmax(0, auto)` without the year being pushed out of the box with it. */
/* ⚠️ THREE tracks, and the third is the point: album, year, then an empty `1fr`
   that eats the leftover width. Two `auto`/`max-content` tracks across a
   `width: 100%` row both stretch instead, which parked the year at the far edge
   with a hole between it and the title. The flexible track also absorbs the
   artist's contribution — it spans all three, and without somewhere flexible to
   put that excess it would widen the title/year tracks and reopen the hole. */
/* ⚠️ The info row reaches INTO the rating column and the artist gives the room
   back. The rating column's track is sized by its WIDEST row — the count plus
   the vinyls — but the title only ever collides with the compact score sitting
   above them, so a fixed strip beside the title was always dead: measured at a
   71px column against a 28px score, 42.7px wasted, on a title that was only
   119.6px. The row claims that strip with a negative margin and
   `.v3-blue-artist` re-reserves it with padding, so line 2 still clears the
   count and the discs. `--sd-title-extra` is measured per album in
   `sizeTitleExtra()` — the count's width moves with the number ("6k" vs
   "156k"), so it can't be a constant. */
.s-home-v3:not(.s-home-v3--review) .v3-blue-info-row {
  display: grid;
  grid-template-columns: minmax(0, max-content) max-content 1fr;
  align-items: baseline;
  column-gap: 0;      /* the year's ::before dot carries its own margin */
  /* ⚠️ `width: auto`, overriding the base rule's `width: 100%`. An explicit
     100% resolves against the GRID AREA, so it pins the row to its column and
     the negative margin below changes nothing at all — the row simply refuses
     to grow and the title gains 0px. `auto` lets `justify-self: stretch` size
     it from the area, which the margin can then extend. */
  width: auto;
  margin-right: calc(-1 * var(--sd-title-extra, 0px));
}
.s-home-v3:not(.s-home-v3--review) .v3-blue-title        { grid-area: 1 / 1; min-width: 0; }
.s-home-v3:not(.s-home-v3--review) .v3-blue-date--inline { grid-area: 1 / 2; }
/* Spans every track so a long name uses the full column width, not just the
   album's. nowrap + ellipsis because it can no longer share a line. */
.s-home-v3:not(.s-home-v3--review) .v3-blue-artist {
  grid-area: 2 / 1 / 3 / 4;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  /* Hands back exactly what the row above claimed — line 2 sits beside the
     count and the vinyls, which are the wide part of the rating column. */
  padding-right: var(--sd-title-extra, 0px);
}
/* The album/artist separator only made sense while they shared a line. */
.s-home-v3:not(.s-home-v3--review) .v3-blue-sep { display: none; }
/* ⚠️ The year's dot lives in a ::before, so a yearless album left it dangling
   after the title. That is not an edge case here: albums pulled from Deezer at
   runtime arrive `_lite` with no year at all (see the recs section in
   CLAUDE.md), and setMainAlbum writes `album.year || ''` into this span. */
.v3-blue-date:empty { display: none; }

/* Right column — the score, with the review count and the vinyls sharing the
   line beneath it. Two rows of a 2-track grid rather than a flex column,
   because the count has to sit BESIDE the discs, not above or below them, and
   the DOM order is score → vinyls → count. Grid placement reorders them
   without touching screens.js (and so without touching the review state, which
   keeps all three on one flex line). */
.s-home-v3:not(.s-home-v3--review) .v3-blue-stars-row {
  display: grid;
  grid-template-columns: auto auto;
  justify-content: end;     /* both tracks hug the outer edge */
  align-items: baseline;
}
.s-home-v3:not(.s-home-v3--review) .v3-blue-score {
  grid-area: 1 / 1 / 2 / 3;   /* spans the count + vinyls beneath it */
  justify-self: end;
  letter-spacing: calc(-0.02em + 0.1px);    /* big digits open up; pull them back together — then 0.1px back out (Eric, 2026-09-18) */
  line-height: 1;
}
.s-home-v3:not(.s-home-v3--review) .v3-blue-count  {
  grid-area: 2 / 1;
  position: relative; top: 2.5px;   /* follows the discs, which ride 2.5px low here (Eric, 2026-09-18). `top`, not a transform — see .hstars */
}
/* Bare "12.5k" here; "12.5k reviews" on the album page. The word is a span the
   state hides — see the note in setMainAlbum for why this can't be a JS branch. */
.s-home-v3:not(.s-home-v3--review) .v3-rc-long { display: none; }

/* ── Credits, in the room the taller shell opened up ──────────────────────
   Produced by / Mixed by, from MusicBrainz (see `creditsFor` in app.js).
   ⚠️ Spans both columns on a second grid row — the title and the rating share
   row 1, and this belongs under BOTH, not beside either.
   ⚠️ THREE fixed rows, always drawn (see `creditRowsHtml`): a missing value
   leaves its label in place, faded. Coverage is patchy — MusicBrainz is
   volunteer-entered — so the alternative was a block that changes height on
   every swipe, which reads worse than a blank in a standing form.
   ⚠️ Compact bento only; the review state lays this box out differently. */
.s-home-v3:not(.s-home-v3--review) .v3-blue-credits {
  grid-column: 1 / -1;
  grid-row: 2;
  align-self: end;
  min-width: 0;
  padding-top: 7px;
}
.v3-blue-credits[hidden] { display: none; }
.v3-cred-row {
  display: flex; align-items: baseline; gap: 5px;
  min-width: 0; line-height: 1.28;
}
/* ⚠️ A row with no value keeps its LABEL but fades it, so the three lines read
   as a standing form with a blank in it rather than as three broken rows. Do
   not hide it — the strip is a fixed height and a block that changes shape
   album to album is what this arrangement exists to avoid. */
.v3-cred-row.is-empty .v3-cred-lbl { opacity: 0.45; }
.v3-cred-lbl {
  flex-shrink: 0;
  font-family: var(--font-mono); font-size: 8px;
  text-transform: uppercase; letter-spacing: 0.06em;
  color: rgba(232,226,214,0.35);
}
.v3-cred-names {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--font-main); font-size: 10px; font-weight: 500;
  color: rgba(232,226,214,0.62);
}
/* ⚠️ An EMPTY names span has no line box, so an empty row used to collapse to
   the 8px label's height while filled rows stood at the 10px name's — the
   three lines changed pitch album to album. A no-break space gives the empty
   span the same line box (and baseline) as a filled one, so every row is the
   same height whether or not MusicBrainz had anything for it. */
.v3-cred-names:empty::before { content: '\00a0'; }
.s-home-v3:not(.s-home-v3--review) .v3-blue-stars-row .hstars {
  grid-area: 2 / 2;
  top: 3.5px;   /* the base 1px optical drop, plus 2.5px down in the bento (Eric, 2026-09-18). `top`, not a transform — see .hstars */
}
/* ⚠️ The two children take `1em` rather than a number so the ROW's font-size is
   the single lever, which is what the dev box's Line 1 Size slider turns.
   Their base rules declare 10.5px outright, so this has to override them. */
.s-home-v3:not(.s-home-v3--review) .v3-blue-album,
.s-home-v3:not(.s-home-v3--review) .v3-blue-artist { font-size: 1em; }

/* ── RIGHT-hand mode: the whole box mirrors ────────────────────────────────
   Rating to the left, text to the right and right-aligned. ⚠️ And the YEAR
   leads the album: it's the shorter, lighter token, so putting it on the
   outside edge keeps the ragged edge next to the rating instead of a hard
   column of two flush-right titles. It inverts the app's usual
   album-before-year order — a deliberate, layout-only exception.
   ⚠️ The rating always sits on the side AWAY FROM THE CD, which is why this
   arrangement rides `:not(--left)` while the base block above is the left-hand
   one. The CD is bottom-right in right-hand mode and bottom-left in left-hand
   mode, and parking the big number next to it read as crowded. If the CD ever
   moves, these two blocks swap selectors — nothing else about them changes. */
.s-home-v3:not(.s-home-v3--left):not(.s-home-v3--review) .v3-blue {
  grid-template-columns: auto minmax(0, 1fr);
}
.s-home-v3:not(.s-home-v3--left):not(.s-home-v3--review) .v3-blue-info-row  { grid-column: 2; }
.s-home-v3:not(.s-home-v3--left):not(.s-home-v3--review) .v3-blue-stars-row { grid-column: 1; }

/* Same three tracks, reversed: the `1fr` leads so the pair is pushed against
   the right edge, with the year on the outside. */
.s-home-v3:not(.s-home-v3--left):not(.s-home-v3--review) .v3-blue-info-row {
  grid-template-columns: 1fr max-content minmax(0, max-content);
  justify-items: end;
  text-align: right;
  /* The rating column is on the LEFT here, so the dead strip above the score is
     too — the overhang and the artist's reservation both swap sides. */
  margin-right: 0;
  margin-left: calc(-1 * var(--sd-title-extra, 0px));
}
.s-home-v3:not(.s-home-v3--left):not(.s-home-v3--review) .v3-blue-artist {
  padding-right: 0;
  padding-left: var(--sd-title-extra, 0px);
}
.s-home-v3:not(.s-home-v3--left):not(.s-home-v3--review) .v3-blue-date--inline { grid-area: 1 / 2; }
/* ⚠️ `justify-self: stretch` overrides the row's `justify-items: end` for this
   one item, and it is what makes the title ELLIPSISE instead of overflowing.
   An `end`-aligned grid item is sized to its content, so a long album ran left
   out of its track and straight over the year — "2009 · Man On The Moon…"
   rendered as "2Man On The Moon…". Stretched, it fills the track and the
   ellipsis inside `.v3-blue-album` finally has a box to bite on. */
.s-home-v3:not(.s-home-v3--left):not(.s-home-v3--review) .v3-blue-title {
  grid-area: 1 / 3;
  justify-self: stretch;
}
/* The dot has to follow the year now instead of leading it. */
.s-home-v3:not(.s-home-v3--left):not(.s-home-v3--review) .v3-blue-date--inline::before { content: none; }
.s-home-v3:not(.s-home-v3--left):not(.s-home-v3--review) .v3-blue-date--inline::after {
  content: '·'; margin-left: 4px;
  font-size: 8px; color: rgba(232,226,214,0.22);
}
/* The rating column sits on the LEFT here, so everything in it flips: the
   tracks hug the inner edge and the count moves to the far side of the discs,
   keeping it against the text column rather than the screen edge. */
.s-home-v3:not(.s-home-v3--left):not(.s-home-v3--review) .v3-blue-stars-row {
  justify-content: start;
}
.s-home-v3:not(.s-home-v3--left):not(.s-home-v3--review) .v3-blue-score { justify-self: start; }
.s-home-v3:not(.s-home-v3--left):not(.s-home-v3--review) .v3-blue-count { grid-area: 2 / 2; }
.s-home-v3:not(.s-home-v3--left):not(.s-home-v3--review) .v3-blue-stars-row .hstars { grid-area: 2 / 1; }

/* ══ Compact bento info box — tuned in the dev box, pasted verbatim ═════════
   ⚠️ Mirrored by DEVBOX_FIELDS in app.js; change both or the panel silently
   emits the old numbers — and since #devbox-live is injected at load and wins
   ties, a mismatch means the panel overrides the stylesheet on every page view.
   Kept LAST in this section so it wins the (0,3,0) ties against the layout
   rules above, which is what makes it safe to paste straight from Copy CSS. */
.s-home-v3:not(.s-home-v3--review) .v3-blue {
  column-gap: 10px;
  padding: 9px 12px 8px;
}
.s-home-v3:not(.s-home-v3--review) .v3-blue-info-row {
  left: 0px;
  top: 4.5px;
  font-size: 14px;
  row-gap: 5.5px;
}
/* ⚠️ One token changed from the pasted output: `gap: 1px` → `row-gap: 1px`.
   The slider is labelled Row gap, but `gap` sets BOTH axes, which crushed the
   count-to-vinyls spacing to 1px along with it. devBoxCss now emits the two
   axes separately, so the next Copy CSS matches this as written. */
.s-home-v3:not(.s-home-v3--review) .v3-blue-stars-row {
  left: 0px;
  top: 2px;
  row-gap: 1px;
  column-gap: 5px;
}
.s-home-v3:not(.s-home-v3--review) .v3-blue-score {
  font-size: 22.5px;
}

/* Album title + (fullscreen) release-year wrapper.
   NOTE: no overflow:hidden here — on an inline-flex it moves the baseline to the box's
   bottom edge, which clips the album's descenders. The album ellipsizes itself. */
.v3-blue-title {
  display: inline-flex; align-items: baseline; gap: 6px;
  min-width: 0; flex: 0 1 auto;
}
.v3-blue-date { font-weight: 400; color: rgba(232,226,214,0.42); flex-shrink: 0; }
.v3-blue-date--fs { display: none; }   /* fullscreen-only; hidden in the bento */
.v3-blue-date--inline::before {
  content: '·'; margin-right: 4px;
  font-size: 8px; color: rgba(232,226,214,0.22);
}
.v3-blue-stars-row {
  display: flex; align-items: baseline; gap: 7px;
  position: relative; top: -0.5px;   /* was 2.5px — nudged the rating row up 3px */
}
/* ⚠️ DM Sans 800, DELIBERATELY not the mono the count beside it uses. The two
   numbers on this line do different jobs: the score is the headline and wants
   the sans's tighter, heavier digits, while the count is a quiet piece of
   metadata and reads as one because it's mono. Matching them was tried and
   flattened the line into a single undifferentiated string of numbers.
   It's stated explicitly because `--font-main` here is inherited, not declared,
   which makes it look like an oversight next to the count's explicit mono.
   ⚠️ share.js draws this line too — `f2` and its MAIN font follow. */
.v3-blue-score {
  font-size: 13px; font-weight: 800;
  font-family: var(--font-main);
  color: rgba(232,226,214,0.82);
  letter-spacing: 0; line-height: 1;
}
.v3-blue-count {
  font-size: 9.5px; color: rgba(232,226,214,0.35);
  font-family: var(--font-mono);
  letter-spacing: 0.2px;
}
/* The sample review is gone from the compact bento (it was already hidden in
   review mode, so this retires it everywhere). The markup and the typewriter in
   app.js stay — they just paint into a hidden node — so it can be brought back
   by deleting this one rule. With it gone the stats block holds TWO lines, and
   .v3-blue switches from space-between (which would push them to the far top
   and bottom of the box) to a centred stack the dev box can tune. */
.v3-blue-quote { display: none; }
.v3-blue { justify-content: center; gap: 3px; }
.v3-blue-quote {
  overflow: hidden;
  white-space: nowrap;
  width: 100%;
  position: relative;
  bottom: 6px;   /* nudged the sample review up */
  padding-bottom: 3px;   /* extend the clip box so the quote's own descenders aren't cut */
  -webkit-mask-image: linear-gradient(to right, black 72%, transparent 100%);
  mask-image: linear-gradient(to right, black 72%, transparent 100%);
}
.v3-blue-quote-text {
  font-size: 9.5px;
  color: rgba(232,226,214,0.38);
  font-family: var(--font-mono);
  font-style: italic;
  display: inline-block;
  white-space: nowrap;
}
.v3-blue-quote--scroll .v3-blue-quote-text {
  animation: v3-quote-scroll 16s ease-in-out infinite;
}
@keyframes v3-quote-scroll {
  0%, 18%  { transform: translateX(0); }
  78%, 100% { transform: translateX(var(--quote-scroll, -60%)); }
}

/* Corner gap: spinning CD (bottom-right, no box bg) */
.v3-corner {
  background: #111116;
  position: relative;
}

/* Spinning CD — positioned in bento coordinate space from SVG cx=615.5 cy=614 r=55 */
.v3-cd {
  position: absolute;
  left: 81.35%; top: 76.5855%; width: 15.97%; height: 15.0685%;
  border-radius: 50%;
  overflow: hidden;
  background-size: cover; background-position: center;
  animation: v3spin 12s linear infinite;
  transition: filter 0.3s ease;
  cursor: pointer; z-index: 0;
  box-shadow:
    0 4px 12px rgba(0,0,0,0.3),
    0 1px 3px rgba(0,0,0,0.2),
    0 0 0 1px rgba(255,255,255,0.06);
}
.v3-cd-hole {
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%,-50%);
  width: 10px; height: 10px; border-radius: 50%;
  background: #111116;
  box-shadow: inset 0 1px 2px rgba(0,0,0,0.9);
  z-index: 1;   /* over the .sd-pix block canvas while a cover is still loading */
}

/* Progressive cover load — the block canvas that covers a box while its picture
   is still on the wire (`sdCover` in app.js).
   ⚠️ `image-rendering: pixelated` IS the effect: the backing store is as small
   as 5×5 and CSS blows it up to the whole box, so the browser must not
   interpolate or you get a blur instead of blocks.
   ⚠️ `border-radius: inherit` — the album's corner is a derived, non-uniform,
   two-axis radius, and this canvas is used on three elements with three
   different corners, so any literal value here would be wrong on at least two. */
.sd-pix {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border-radius: inherit;
  image-rendering: pixelated;
  opacity: 0;
  transition: opacity .14s ease;
  pointer-events: none;
  z-index: 0;
}
@keyframes v3spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}
/* CD is the preview play/pause control: spins while a preview plays, freezes + dims when paused */
.v3-cd-paused .v3-cd { animation-play-state: paused; filter: brightness(0.68) saturate(0.85); }

/* Preview autoplay toggle (speaker). Positioned in the bento's coordinate space. */
.v3-preview-btn {
  position: absolute;
  width: 29px; height: 29px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: rgba(232,226,214,0.06);
  border: 1px solid rgba(232,226,214,0.16);
  color: rgba(232,226,214,0.5);
  cursor: pointer; z-index: 6; padding: 0;
  transition: color 0.15s, background 0.15s, border-color 0.15s;
  -webkit-tap-highlight-color: transparent;
}
.v3-preview-btn svg { width: 16px; height: 16px; }
.v3-preview-btn:hover { color: rgba(232,226,214,0.85); border-color: rgba(232,226,214,0.3); }
.v3-spk-wave { display: none; }                 /* off (muted) shows the X */
.v3-preview-on .v3-preview-btn {
  color: var(--v3-accent, #e8a83c);
  border-color: var(--v3-accent, #e8a83c);
  background: rgba(232,168,60,0.16);
}
.v3-preview-on .v3-preview-btn .v3-spk-x { display: none; }
.v3-preview-on .v3-preview-btn .v3-spk-wave { display: inline; }
.s-home-v3--light .v3-preview-btn { background: rgba(26,18,8,0.05); border-color: rgba(26,18,8,0.16); color: rgba(26,18,8,0.5); }
/* Left-hand: album is on the right — align the speaker's right edge with the album's (same inset from the wall) */
.s-home-v3--left .v3-preview-btn { right: 0.07%; left: auto; top: 102%; }
/* Right-hand: album is on the left — align the speaker's right edge with the album's right side */
.s-home-v3:not(.s-home-v3--left) .v3-preview-btn { right: 21.92%; left: auto; top: 102%; }

/* ══════════════════════════════════════════════════════════════
   LEFT / RIGHT HAND LAYOUT  (one-hand reachability)
   Right hand = default (matches original bento).
   .s-home-v3--left mirrors every cell + swaps the bg silhouette.
   ════════════════════════════════════════════════════════════ */

/* Background silhouette: show right by default, left when mirrored */
.v3-bg-fill .bg-left { display: none; }
.s-home-v3--left .v3-bg-fill .bg-right { display: none; }
.s-home-v3--left .v3-bg-fill .bg-left  { display: block; }

/* Album art — mirror to the right side; notch corner flips to bottom-right */
.s-home-v3--left .v3-album {
  /* bg-left puts the album region's left edge at x=150.5 → 150.5/689. The old
     21.92% resolved to 151.03, half a unit inboard, so the shell showed as a
     hairline down the left edge and thickened into the corners. */
  left: 21.84325%;
  /* Same radii as the right-hand rule, square corner flipped to bottom-right. */
  border-radius: 3.71747% 3.71747% 0 3.71747% / 3.74499% 3.74499% 0 3.74499%;
}
/* Stats strip — mirror under the album; text stays LEFT-aligned (like album title) */
.s-home-v3--left .v3-blue { left: 21.85%; }
/* For You box — mirror to the left column */
.s-home-v3--left .v3-for-single { left: 2.76%; }
/* Spinning CD — mirror to bottom-left */
.s-home-v3--left .v3-cd { left: 2.68%; }

/* ── "Live" corner pill (was search) ─────────────────────────── */
/* Positioned inside the bento to match the SVG corner pill exactly:
   x556.5–688.5, y0.5–65.5 in the 689×638 coordinate space */
.v3-live-pill {
  position: absolute;
  left: 80.77%; top: 0.437%; right: auto;
  width: 19.16%; height: 8.9041%;
  min-width: 0;
  gap: 6px; padding: 0;
  border-radius: 999px;
  font-family: var(--font-main);
  font-size: 12.5px; font-weight: 700; letter-spacing: 0.2px;
  z-index: 6;
}
.s-home-v3--left .v3-live-pill { left: 0.07%; right: auto; }
/* Hand-layout switch — now lives inside the Live pill (bento state); Back replaces it in review */
.v3-live-pill .v3-nav-switch {
  position: relative;
  width: 30px; height: 16px; border-radius: 999px;
  background: #34343c; transition: background .2s;
}
.v3-live-pill .v3-nav-knob {
  position: absolute; top: 2px; left: 2px;
  width: 12px; height: 12px; border-radius: 50%;
  background: #e8e2d6; transition: transform .2s;
  transform: translateX(14px);            /* right-hand (default off) */
}
.s-home-v3--left .v3-live-pill .v3-nav-knob { transform: translateX(0); }   /* left-hand */
.s-home-v3--left .v3-live-pill .v3-nav-switch { background: #56565f; }
.s-home-v3--light .v3-live-pill .v3-nav-switch { background: #cfc9be; }
.s-home-v3--light .v3-live-pill .v3-nav-knob { background: #1a1208; }
.s-home-v3--light.s-home-v3--left .v3-live-pill .v3-nav-switch { background: #b3ab9d; }
.v3-live-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: #e6484d; flex-shrink: 0;
  box-shadow: 0 0 6px rgba(230,72,77,0.85);
  animation: v3livepulse 1.8s ease-in-out infinite;
}
.v3-live-label { color: rgba(232,226,214,0.9); }
.s-home-v3--light .v3-live-label { color: rgba(26,18,8,0.85); }

/* Live pill ring — 6 dots in a ring. Idle = slow spin; reacts to swipe / For You / CD. */
.v3-ring {
  position: relative;
  display: inline-block;   /* keeps the 20×20 box explicit (span would otherwise be inline) */
  width: 20px; height: 20px;
  color: #e8e2d6;
}
.s-home-v3--light .v3-ring { color: #1a1208; }
.v3-live-pill .v3-ring { top: 2px; }   /* dots sat a touch high in the pill */
.v3-ring-spin {
  position: absolute; inset: 0;
  animation: none;   /* static idle — the reaction classes (swipe/foryou/cd) drive motion */
}
.v3-ring-dot {
  position: absolute; top: 50%; left: 50%;
  width: 3px; height: 3px; margin: -1.5px;   /* slightly smaller */
  /* ⚠️ Rounded SQUARE — 14% of the dot, matching SD_DOTS' `cornerFrac`, so the
     pill/back-pill/follow rings are the same dot as every generated asset and
     the nav pet. These were circles; that made the one dot shape the user sees
     most often the only one that wasn't the brand's. */
  border-radius: 14%;
  background: currentColor;
  transition: transform 0.4s cubic-bezier(0.34, 1.45, 0.5, 1);   /* the dots MORPH between formations */
  /* Idle emphasis: each dot eases OUTWARD from the arrow's centre, holds briefly, then
     settles back — once every 5s. Rides on the independent `translate` property so it
     COMPOSES with the formation `transform` above instead of overwriting it. Per-dot
     outward vector comes from --sx/--sy (set below); centre-ish dots barely move. */
  animation: v3-dot-sep 5s ease-in-out infinite;
}
@keyframes v3-dot-sep {
  0%, 55%, 100% { translate: 0 0; }
  70%, 80%      { translate: var(--sx, 0) var(--sy, 0); }
}
/* Pause the emphasis while the dots are in a reaction formation (swipe / For You / CD),
   so the pulse never fights the morph; it resumes once they reform the arrow. */
.v3-ring--swipe  .v3-ring-dot,
.v3-ring--foryou .v3-ring-dot,
.v3-ring--cd     .v3-ring-dot { animation: none; translate: 0 0; }
@media (prefers-reduced-motion: reduce) {
  .v3-ring-dot { animation: none; }
}
/* Outward separation vectors — right-hand arrow (points left). Tail pushes right, tip
   pushes left, the head arms fan out diagonally, the shaft (centre) barely stirs. */
.v3-ring-dot:nth-child(1) { --sx: 2.6px;  --sy: 0; }     /* tail → out (right) */
.v3-ring-dot:nth-child(2) { --sx: 0.8px;  --sy: 0; }     /* shaft → tiny */
.v3-ring-dot:nth-child(3) { --sx: -0.8px; --sy: 0; }     /* shaft → tiny */
.v3-ring-dot:nth-child(4) { --sx: -2.6px; --sy: 0; }     /* tip → out (left) */
.v3-ring-dot:nth-child(5) { --sx: -1px;   --sy: -2.4px; }/* upper arm → diagonal out */
.v3-ring-dot:nth-child(6) { --sx: -1px;   --sy: 2.4px; } /* lower arm → diagonal out */
/* left-hand arrow (points right) → mirror the x component */
.s-home-v3--left .v3-ring-dot:nth-child(1) { --sx: -2.6px; }
.s-home-v3--left .v3-ring-dot:nth-child(2) { --sx: -0.8px; }
.s-home-v3--left .v3-ring-dot:nth-child(3) { --sx: 0.8px; }
.s-home-v3--left .v3-ring-dot:nth-child(4) { --sx: 2.6px; }
.s-home-v3--left .v3-ring-dot:nth-child(5) { --sx: 1px; }
.s-home-v3--left .v3-ring-dot:nth-child(6) { --sx: 1px; }
/* For You bloom is snappier than the others */
.v3-ring--foryou .v3-ring-dot { transition-duration: 0.2s; }
/* ── Formation: ARROW (idle) — points left. Tail dots slid 2px toward the front. ── */
.v3-ring-dot:nth-child(1) { transform: translate(5px, 0); }    /* tail  (was 7) */
.v3-ring-dot:nth-child(2) { transform: translate(1px, 0); }    /* shaft (was 3) */
.v3-ring-dot:nth-child(3) { transform: translate(-3px, 0); }   /* shaft (was -1) */
.v3-ring-dot:nth-child(4) { transform: translate(-7px, 0); }   /* tip */
.v3-ring-dot:nth-child(5) { transform: translate(-5px, -4px); } /* upper head arm — 2px toward the tip */
.v3-ring-dot:nth-child(6) { transform: translate(-5px, 4px); }  /* lower head arm — 2px toward the tip */
/* left-hand → the dots slide to the MIRRORED arrow (points right) */
.s-home-v3--left .v3-ring-dot:nth-child(1) { transform: translate(-5px, 0); }
.s-home-v3--left .v3-ring-dot:nth-child(2) { transform: translate(-1px, 0); }
.s-home-v3--left .v3-ring-dot:nth-child(3) { transform: translate(3px,  0); }
.s-home-v3--left .v3-ring-dot:nth-child(4) { transform: translate(7px,  0); }
.s-home-v3--left .v3-ring-dot:nth-child(5) { transform: translate(5px, -4px); }
.s-home-v3--left .v3-ring-dot:nth-child(6) { transform: translate(5px,  4px); }
/* Playlist-page back pill — arrow always points LEFT (back), regardless of hand
   mode (these out-order the --left mirror). Sits before the reaction/smile
   formations so those still win. */
.plp-back-pill .v3-ring-dot:nth-child(1) { transform: translate(5px,  0);    --sx: 2.6px; }
.plp-back-pill .v3-ring-dot:nth-child(2) { transform: translate(1px,  0);    --sx: 0.8px; }
.plp-back-pill .v3-ring-dot:nth-child(3) { transform: translate(-3px, 0);    --sx: -0.8px; }
.plp-back-pill .v3-ring-dot:nth-child(4) { transform: translate(-7px, 0);    --sx: -2.6px; }
.plp-back-pill .v3-ring-dot:nth-child(5) { transform: translate(-5px, -4px); --sx: -1px; }
.plp-back-pill .v3-ring-dot:nth-child(6) { transform: translate(-5px, 4px);  --sx: -1px; }
/* ── Formation: SWIPE — the dots separate into a ring (hexagon) ── */
.v3-ring--swipe .v3-ring-dot:nth-child(1) { transform: translate(0, -7px); }
.v3-ring--swipe .v3-ring-dot:nth-child(2) { transform: translate(6px, -3.5px); }
.v3-ring--swipe .v3-ring-dot:nth-child(3) { transform: translate(6px, 3.5px); }
.v3-ring--swipe .v3-ring-dot:nth-child(4) { transform: translate(0, 7px); }
.v3-ring--swipe .v3-ring-dot:nth-child(5) { transform: translate(-6px, 3.5px); }
.v3-ring--swipe .v3-ring-dot:nth-child(6) { transform: translate(-6px, -3.5px); }
/* ── Formation: FOR YOU — the dots scatter outward (a bloom), distinct from the ring ── */
.v3-ring--foryou .v3-ring-dot:nth-child(1) { transform: translate(-9px, -3px); }
.v3-ring--foryou .v3-ring-dot:nth-child(2) { transform: translate(-4px, 8px); }
.v3-ring--foryou .v3-ring-dot:nth-child(3) { transform: translate(4px, -8px); }
.v3-ring--foryou .v3-ring-dot:nth-child(4) { transform: translate(9px, 3px); }
.v3-ring--foryou .v3-ring-dot:nth-child(5) { transform: translate(-8px, 6px); }
.v3-ring--foryou .v3-ring-dot:nth-child(6) { transform: translate(8px, -6px); }
/* ── Formation: CD — the dots snap into a tight 2×3 grid ── */
.v3-ring--cd .v3-ring-dot:nth-child(1) { transform: translate(-3px, -4px); }
.v3-ring--cd .v3-ring-dot:nth-child(2) { transform: translate(-3px, 0); }
.v3-ring--cd .v3-ring-dot:nth-child(3) { transform: translate(-3px, 4px); }
.v3-ring--cd .v3-ring-dot:nth-child(4) { transform: translate(3px, -4px); }
.v3-ring--cd .v3-ring-dot:nth-child(5) { transform: translate(3px, 0); }
.v3-ring--cd .v3-ring-dot:nth-child(6) { transform: translate(3px, 4px); }

/* The dot face is retired — no more smile/wink formations or their keyframes.
   The nav's pet carries the app's personality now; these dots keep only the
   arrow and the swipe / For-You / CD reactions. */

/* Swipe rotation is driven live from the drag (see setupAlbumSwipe): partial turn while
   dragging, completing a full spin on commit — so it feels tactile. */
.v3-ring-spin { will-change: transform; }
@keyframes v3livepulse {
  0%,100% { opacity: 1; }
  50%     { opacity: 0.45; }
}

/* ── DEV hand-toggle button (temporary) ──────────────────────── */
.v3-hand-toggle {
  position: absolute;
  left: 50%; transform: translateX(-50%);
  bottom: 64px; z-index: 60;
  display: flex; align-items: center; gap: 6px;
  padding: 5px 12px;
  border: none; border-radius: 999px;
  background: rgba(18,18,24,0.82);
  -webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px);
  box-shadow: 0 3px 12px rgba(0,0,0,0.45), inset 0 0 0 1px rgba(255,255,255,0.09);
  color: rgba(232,226,214,0.85);
  font-family: var(--font-mono, monospace);
  font-size: 10.5px; font-weight: 600; letter-spacing: 0.4px;
  cursor: pointer;
}
.v3-hand-toggle:active { filter: brightness(1.2); }
.v3-hand-ico { font-size: 12px; line-height: 1; }
.s-home-v3--left .v3-hand-ico { transform: scaleX(-1); }
.s-home-v3--light .v3-hand-toggle {
  background: rgba(248,244,238,0.85);
  color: rgba(26,18,8,0.75);
  box-shadow: 0 3px 12px rgba(116,150,179,0.25), inset 0 0 0 1px rgba(26,18,8,0.1);
}

/* ══════════════════════════════════════════════════════════════
   FULLSCREEN REVIEW MODE  (tap the stats box to open)
   The bento color floods the whole screen (seamless takeover),
   then the content fades in. Minimal, typographic — no grey boxes.
   ════════════════════════════════════════════════════════════ */
/* Panel padding matches the bento's 10px margin so % columns align exactly */
.v3-review-panel { display: none; padding: 4px 10px 18px; }
.s-home-v3--review .v3-scroll-area { display: none; }
/* 0.14b (2026-10-12): home's feed tabs go with the feed. Album and artist
   pages are this same shell with --review on (artist adds --artist), so the
   bar sat on top of them. Eric: "that bar of categories is also on the album
   page for some reason and the artist page". */
.s-home-v3--review .v3-feed-tabs { display: none; }
/* ⚠️ `backwards`, NOT `both` (2026-09-18) — the third place this bit. A
   fill-forwards animation keeps the panel on a compositing layer for good
   (this one even ends on `transform: none`, which still counts), and the
   scaled phone wrap / WebKit resample that layer: the whole review list read
   BLURRY after opening an album from the feed. The end state is the natural
   one, so `backwards` loses nothing. See *Review cards* in CLAUDE.md. */
.s-home-v3--review .v3-review-panel { display: block; animation: v3revin 0.5s ease 0.2s backwards; }

/* Top row: links (thumb/CD side) + your review (album/stats side).
   Uses the bento's 78/22 split; flips to row-reverse in right-hand mode. */
.v3-rev-top { display: flex; align-items: flex-start; gap: 0; margin-top: -20px; margin-bottom: 2px; }
.s-home-v3:not(.s-home-v3--left) .v3-rev-top { flex-direction: row-reverse; }

/* Action grid — 2 cols × 3 rows in the CD column.
   Col 1 (top→bottom): Spotify · Apple · YouTube.  Col 2: Listen later · Favorite · Shop.
   padding-top clears the CD spinning above this column. */
.v3-rev-actions {
  flex-shrink: 0;
  width: 58px;                   /* reserve the CD-column width so the review box never shifts,
                                    regardless of how many streaming icons are inside */
  display: grid;
  grid-auto-flow: row;
  grid-template-columns: 27px;   /* single column of streaming links now that fav/later/shop moved to the log sheet */
  grid-template-rows: repeat(3, 27px);
  justify-content: start;        /* icons on the left side of the reserved column, under the CD like before */
  gap: 4px;
  padding-top: 30px;
  pointer-events: none;   /* empty top area doesn't block the CD overlapping above */
}
/* Align the grid's outer edge with the CD's inset from the screen edge.
   The bento (and CD) are inset 10px, and the CD sits ~10px further in;
   the panel already gives 10px, so add ~10px more on the edge side.
   The CD mirrors sides with hand mode, so match it. */
.s-home-v3--left .v3-rev-actions { margin-left: 10px; }
.s-home-v3:not(.s-home-v3--left) .v3-rev-actions { margin-right: 10px; }
.v3-rev-btn {
  pointer-events: auto;
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid rgba(232,226,214,0.12);
  border-radius: 6px;
  background: rgba(232,226,214,0.04);
  color: rgba(232,226,214,0.38); cursor: pointer;
  transition: color 0.15s, background 0.15s, border-color 0.15s, transform 0.1s;
  -webkit-tap-highlight-color: transparent;
}
.v3-rev-btn svg { width: 16px; height: 16px; }
.v3-rev-btn:hover { transform: translateY(-1px); border-color: rgba(232,226,214,0.28); background: rgba(232,226,214,0.08); }
.v3-rev-btn:active { transform: scale(0.94); }
/* Streaming — brand-color hovers */
.v3-rev-btn--spotify:hover { color: #1DB954; border-color: rgba(29,185,84,0.5); }
.v3-rev-btn--apple:hover   { color: #fa5a78; border-color: rgba(250,90,120,0.5); }
.v3-rev-btn--yt:hover      { color: #ff2d2d; border-color: rgba(255,45,45,0.5); }
/* Actions — hover + toggled-on states */
.v3-rev-btn--later:hover { color: var(--v3-accent, #e8a83c); }
.v3-rev-btn--fav:hover   { color: #ff5a6e; }
.v3-rev-btn--shop:hover  { color: var(--v3-accent, #e8a83c); }
.v3-rev-btn--later.on {
  color: var(--v3-accent, #e8a83c); border-color: var(--v3-accent, #e8a83c);
  background: rgba(232,168,60,0.14);
}
.v3-rev-btn--fav.on {
  color: #ff5a6e; border-color: #ff5a6e; background: rgba(255,90,110,0.14);
}
.v3-rev-btn--fav.on svg { fill: #ff5a6e; }
/* Light theme */
.s-home-v3--light .v3-rev-btn {
  color: rgba(232,226,214,0.38); border-color: rgba(232,226,214,0.12); background: rgba(232,226,214,0.03);
}
.s-home-v3--light .v3-rev-btn:hover { border-color: rgba(232,226,214,0.26); background: rgba(232,226,214,0.06); }
.s-home-v3--review .v3-hand-toggle { display: none; }   /* dev button out of the way */
@keyframes v3revin {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* Flood: screen bg becomes the bento color; the bento melts into it */
.s-home-v3--review { background: var(--v3-box1-color, #241820); }
.s-home-v3--light.s-home-v3--review { background: var(--v3-box1-color, #f0ece3); }
.v3-bg-fill { transition: filter 0.35s ease; }
.s-home-v3--review .v3-bg-fill { filter: none; }

/* In review mode the reviews live below, so drop the sample quote in the stats box
   and keep the remaining stats pinned to the top (frees the space beneath) */
.s-home-v3--review .v3-blue-quote { display: none; }
.s-home-v3--review .v3-blue {
  justify-content: flex-start;
  height: auto;          /* grow to fit the taller 2-line title */
  overflow: visible;
  transform: translateY(3px);   /* nudge all 3 lines (album/date, artist, stars) down in fullscreen */
}
/* Title stacks: artist (14px) on top, album (18px) below; separator drops out */
.s-home-v3--review .v3-blue-info-row {
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  overflow: visible;
}
.s-home-v3--review .v3-blue-sep { display: none; }
/* Larger since the rating line came out from under it — the info box is down to
   two lines here, so the title pair can take the room the row used to hold. */
.s-home-v3--review .v3-blue-artist { font-size: 14.5px; position: relative; top: 3.5px; }
.s-home-v3--review .v3-blue-album {
  font-size: 22px;
  display: inline;              /* flow inline with the date + allow wrapping */
  white-space: normal;
  overflow: visible;
  color: rgba(232,226,214,0.92);
  line-height: 1.12;
  font-variation-settings: 'wdth' 85;   /* condense the variable font so long titles fit */
}
.s-home-v3--light.s-home-v3--review .v3-blue-album { color: rgba(232,226,214,0.9); }
/* Release year beside the album title — smaller + lighter than the album name */
.s-home-v3--review .v3-blue-title {
  position: relative; top: 2px;
  max-width: 100%;
  display: -webkit-box;                 /* clamp long titles to 2 lines */
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  line-height: 1.12;
}
.s-home-v3--review .v3-blue-date--inline { display: none; }
.s-home-v3--review .v3-blue-date--fs {
  display: inline; margin-left: 6px;    /* was a flex gap; title is no longer flex */
  font-size: 14px; font-weight: 300;    /* tracks the album title's step up */
  color: rgba(232,226,214,0.5);
}
.s-home-v3--light.s-home-v3--review .v3-blue-date--fs { color: rgba(232,226,214,0.5); }
/* Push the rest of the stats down ~1 line to make room */
.s-home-v3--review .v3-blue-stars-row { margin-top: 7px; }
/* ⚠️ The whole rating line is GONE from the album page — the headline score
   below the CTA says all of it, and printing the number twice made the page
   read as having two different ratings. The info box here is now just
   album · year over the artist.
   ⚠️ `:not(--artist)` is load-bearing. The artist page has no score, so this
   row is where its review COUNT lives — "the one stat" (see the `--artist`
   rules below, which hide the score and the discs but keep the count).
   Dropping the `:not()` blanks that out.
   ⚠️ The compact bento is untouched: it's the app's primary rating display and
   keeps its number, discs and count. */
.s-home-v3--review:not(.s-home-v3--artist) .v3-blue-stars-row { display: none; }

/* Flood removes the bento margin visually, leaving dead space between the For You
   box and the screen edge. Grow it outward so its edge inset matches the back button. */
.s-home-v3--review .v3-for-single { width: 19.1%; top: 15.23%; height: 68.55%; }                      /* right-hand: lowered so its bottom (83.78%) aligns with the album cover bottom */
.s-home-v3--left.s-home-v3--review .v3-for-single { left: 0.07%; width: 19.1%; top: 15.23%; height: 68.55%; }  /* left-hand: lowered so its bottom aligns with the album cover bottom */

/* ══ Album view — the fullscreen review state with a BIGGER cover ══
   Identical to fullscreen review, only the album art swells to a full-width square,
   which grows the bento taller and pushes the stats/CD + the whole panel below down.
   Nothing else about the layout changes — the CD stays in its fullscreen spot (and
   follows hand mode), and the panel content keeps the fullscreen arrangement. */
.s-home-v3--review.s-home-v3--album .v3-bento { aspect-ratio: 689 / 862; }
/* The pill height is a % of the bento; the album bento is taller (862 vs 638), so counter
   it here to keep the Back button the same size as in the fullscreen review state. */
.s-home-v3--review.s-home-v3--album .v3-live-pill { height: 7.54%; }
/* Nudge the Back pill away from whichever edge it sits on (mirrors with hand layout) */
.s-home-v3--review.s-home-v3--album.s-home-v3--left .v3-live-pill { transform: translate(4px, 4px); }
.s-home-v3--review.s-home-v3--album:not(.s-home-v3--left) .v3-live-pill { transform: translate(-4px, 4px); }

/* ── ARTIST PAGE — an --artist variation of the album page ────────────────
   Banner (full width, fading top+bottom into the bg) instead of the square cover;
   no CD / tracklist / media; a grid of the artist's albums below the info. */
.s-home-v3--review.s-home-v3--album.s-home-v3--artist .v3-album {
  left: -10px; width: calc(100% + 20px);   /* edge-to-edge, past the bento's 10px margins */
  top: 0;                                  /* top edge sits on the bento's top, exactly where the
                                              album cover starts — a clean edge, not a soft crop */
  border-radius: 0;
  box-shadow: none;
  /* as tall as the album cover (inherits aspect-ratio:1). Only the BOTTOM fades
     into the bg; a top fade read as a harsh cut-off floating under the header. */
  -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
}
.s-home-v3--artist .v3-cd,
.s-home-v3--artist .v3-preview-btn,
.s-home-v3--artist .v3-nav-toggle,
.s-home-v3--artist .v3-rev-songs,
.s-home-v3--artist .v3-rev-media,
.s-home-v3--artist .v3-blue-date { display: none; }
/* No artist reviews (2026-09-04) — an artist isn't reviewed, rated or logged;
   their ALBUMS are, and an artist review would only restate those. The whole
   CTA row goes (Review/rate/log + Listened / Later / Favorite). FAVORITE lives
   on as `.v3-artist-fav`, a corner pill up by the Back button — see below. */
.s-home-v3--artist .v3-rev-cta-row { display: none; }
/* ── Favorite pill — the Back pill's twin, in the OTHER top corner ─────────
   Same box as `.v3-live-pill` on the album page (19.16% × 7.54%, 0.437% down,
   nudged 4px in from the edge) so the two sit level; the side mirrors with hand
   mode, always opposite Back. Every value is declared outright: the button
   carries `.v3-search-pill` (which places it top-right in px) and `.v3-rev-q`
   (a 50px square in a cascade), and either would otherwise leak through. */
/* ⚠️ Two classes, not one. `.v3-rev-q { display: inline-flex }` and
   `.v3-search-pill { display: flex }` both sit later in this file at (0,1,0),
   so a bare `.v3-artist-fav` lost to them and the pill showed on every page. */
.s-home-v3 .v3-artist-fav { display: none; }
.s-home-v3--review.s-home-v3--album.s-home-v3--artist .v3-artist-fav {
  display: flex; flex-direction: row; align-items: center; justify-content: center;
  position: absolute;
  /* Lowered (2026-09-11): it was the Back pill's twin in the top corner. It now
     sits on the info box's LAST line — the artist-info line, whose centre is
     ~28px up from the bento's bottom edge once the page has settled (measure
     AFTER the entry animation: mid-flight the box overhangs and the numbers
     lie) — flush with the box's 12px right padding. px, deliberately: that
     line is px type, not a % of the bento. Same corner in both hand modes. */
  left: auto; right: 12px; top: auto; bottom: 13px;
  width: 19.16%; height: 7.54%;
  min-width: 0; margin: 0; padding: 0;
  border: none; border-radius: 999px;
  background: #111116;
  box-shadow: -4px -4px 8px 0 rgba(255,255,255,0.07), 4px 4px 6px -1px rgba(0,0,0,0.5);
  color: rgba(232,226,214,0.85);
  transform: none;
  z-index: 6;
  transition: color .15s, transform .06s;
}
.s-home-v3--review.s-home-v3--album.s-home-v3--artist .v3-artist-fav .sd-dot-ico { width: 17px; height: 17px; }
.s-home-v3--review.s-home-v3--album.s-home-v3--artist .v3-artist-fav:hover { filter: brightness(1.14); }
.s-home-v3--review.s-home-v3--album.s-home-v3--artist .v3-artist-fav.on { color: #ff5c8a; }
.s-home-v3--light.s-home-v3--review.s-home-v3--album.s-home-v3--artist .v3-artist-fav {
  background: #f0ece3;
  box-shadow: -4px -4px 8px 0 #d8d2c8, 4px 4px 6px -1px rgba(116,150,179,0.27);
  color: rgba(26,18,8,0.6);
}
.s-home-v3--light.s-home-v3--review.s-home-v3--album.s-home-v3--artist .v3-artist-fav.on { color: #ff5c8a; }
/* ══ Album page — the headline score above the histogram ═══════════════════
   Big, left-aligned, DM Sans. It heads the ratings section (score → discs →
   distribution → tracklist), which is why it takes the histogram's left edge
   rather than the stats strip's.
   ⚠️ `--font-main`, declared outright. The number under the artist is DM Sans
   by inheritance and the count beside it is mono, so "which family is this?"
   is a live question in this box — say it rather than leave it to the cascade.
   ⚠️ Review state only. `.v3-rev-score` lives in the review panel, which the
   compact bento never renders, but the scope keeps it honest. */
/* ⚠️ These six declarations are mirrored by the dev box's "Album score" tab
   (DEVBOX_TABS in app.js). Change one here and the panel's defaults have to
   follow, or its live <style> — which is injected at load and wins ties —
   silently reverts them on every page view. */
.s-home-v3--review .v3-rev-score {
  display: flex; align-items: baseline;
  position: relative;
  left: 15.5px;
  top: 5.5px;
  gap: 9px;
  padding: 23px 0 2px;
  /* ⚠️ Sized to its content, NOT the row. As a full-width block the `left`
     nudge pushed its box 15.5px past the panel's right edge, and a relative
     offset still counts as scrollable overflow — the whole album page slid
     sideways by 6px on a phone (Eric, 2026-09-18). */
  width: fit-content; max-width: calc(100% - 15.5px);
}
.s-home-v3--review .v3-rev-score-n {
  font-family: var(--font-main);
  font-size: 51px; font-weight: 800;
  line-height: 1;
  letter-spacing: -0.03em;   /* big digits open up; pull them back together */
  color: rgba(232,226,214,0.92);
}
.s-home-v3--light.s-home-v3--review .v3-rev-score-n { color: rgba(232,226,214,0.92); }
/* The discs sit on the number's baseline so the pair reads as one object. */
.s-home-v3--review .v3-rev-score-sub { position: relative; top: -2px; }
/* Sized here rather than inline (populateBigScore passes halfStars' `cssSized`)
   so the dev box can turn it — an inline width would need !important to beat. */
.s-home-v3--review .v3-rev-score-sub .hstar { width: 18px; height: 18px; }
/* The review count, right of the discs (Eric, 2026-09-18): DM Sans, grey,
   on the number's baseline. It replaces the one-liner's count, which this
   page hides. Light ink in both themes — the surface is the album flood. */
.v3-rev-score-count {
  margin-left: 9px; vertical-align: baseline;
  font-family: var(--font-main); font-size: 12px; font-weight: 500;
  color: rgba(232,226,214,0.45); white-space: nowrap;
}
/* An artist has no score — same rule as the one-liner's. */
.s-home-v3--artist .v3-rev-score { display: none; }
/* No YOUR REVIEW band and no COMMUNITY REVIEW heading either (Eric,
   2026-09-25). Both arrived on 09-24 as parts of the ALBUM page's ratings
   section, and the artist page inherited them: the band still read "Tap to
   rate" over an artist, and the community label sat over a score this rule
   already hides. Same reason as the CTA row above — an artist isn't reviewed,
   their albums are. The reviews list under the albums grid stays. */
.s-home-v3--artist .v3-rev-top,
.s-home-v3--artist .v3-rev-community { display: none; }

/* No artist rating — an artist isn't a thing you score; their ALBUMS carry the
   ratings, in the grid below. The review count stays as the one stat.
   ⚠️ `.hstars`, the WRAPPER, not just the discs. Hiding the five `.hstar`s left
   an empty inline-flex span in the row, and the row's 7px gap on either side of
   it is what was indenting the count off the artist name's left edge. */
.s-home-v3--artist .v3-blue-score,
.s-home-v3--artist .v3-blue-stars-row .hstars,
.s-home-v3--artist .v3-rev-hist { display: none; }
/* Bigger type — with no score, discs or genre, the name and the count were the
   only two things on a full-width strip and read small for it. */
.s-home-v3--artist .v3-blue-album { font-size: 26px; }
.s-home-v3--artist .v3-blue-count { font-size: 12px; }
/* ⚠️ Lower than the album page's 81%. The banner is `calc(100% + 20px)` wide
   and square, so its bottom lands ~85% down the 689×862 bento (not the cover's
   80%), and at 81% the name sat inside the fading edge of the photo. */
.s-home-v3--review.s-home-v3--album.s-home-v3--artist .v3-blue { top: 87%; }
/* ── Artist info: LABEL · MEMBERS · DESCRIPTION, instead of producer credits ──
   The credits box is the bento's (producer / mix / engineer for the album that
   was showing), and it was simply still visible under the artist name. Its own
   node here, painted by populateArtistPage. The description wraps and is capped
   at four lines; a row with nothing in it (a solo act's Members) drops out.
   ⚠️ `--rev-cred-h` is the strip's overhang below the bento (see the album
   page's rule): name + count + three rows, the last up to four lines, is
   ~150px past the box's top at 87%, so the CTA row below clears that much. */
.s-home-v3--artist .v3-blue-credits { display: none; }
.v3-artist-info { display: none; }
.s-home-v3--artist .v3-artist-info {
  display: flex; flex-direction: column; gap: 5px;
  width: 100%; margin-top: 12px;
  /* The favourite pill sits on this block's last line, at the right — keep the
     rows out from under it (its 19.16% width plus a gutter). */
  box-sizing: border-box; padding-right: calc(19.16% + 18px);
}
.s-home-v3--review.s-home-v3--album.s-home-v3--artist { --rev-cred-h: 125px; }
/* Without a description the block is two short rows, so most of that reserve
   would be a hole between the strip and the Favorite button. Same `:has()`
   device as the album page's hidden-credits rule above. */
.s-home-v3--review.s-home-v3--album.s-home-v3--artist:has(.v3-ai-row--description.is-empty) { --rev-cred-h: 55px; }
.v3-ai-row { display: flex; align-items: baseline; gap: 7px; min-width: 0; }
.v3-ai-row.is-empty { display: none; }
.v3-ai-row--description { flex-direction: column; align-items: flex-start; gap: 2px; margin-top: 3px; }
.v3-ai-lbl {
  flex-shrink: 0;
  font-family: var(--font-mono); font-size: 9px;
  text-transform: uppercase; letter-spacing: 0.08em;
  color: rgba(232,226,214,0.38);
}
.v3-ai-val {
  min-width: 0;
  font-family: var(--font-main); font-size: 12px; font-weight: 500;
  color: rgba(232,226,214,0.7);
}
.v3-ai-row--description .v3-ai-val {
  font-weight: 400; line-height: 1.4; color: rgba(232,226,214,0.6);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 4; overflow: hidden;
}
.s-home-v3--light.s-home-v3--album .v3-ai-lbl { color: rgba(26,18,8,0.4); }
.s-home-v3--light.s-home-v3--album .v3-ai-val { color: rgba(26,18,8,0.7); }
.s-home-v3--light.s-home-v3--album .v3-ai-row--description .v3-ai-val { color: rgba(26,18,8,0.6); }
/* Left-align the artist name / genre / stars */
.s-home-v3--artist .v3-blue {
  left: 0; width: 100%;
  align-items: flex-start; text-align: left;
}
.s-home-v3--artist .v3-blue-info-row,
.s-home-v3--artist .v3-blue-stars-row,
.s-home-v3--artist .v3-blue-title { align-items: flex-start; justify-content: flex-start; text-align: left; }
/* Drop the CD-column offset; centring comes from the --album rule above */
.s-home-v3--artist .v3-rev-mine {
  margin-left: 0 !important; margin-right: 0 !important;
}
/* Artist page uses the default theme background, not the album's procedural color */
.s-home-v3--review.s-home-v3--artist { background: #111116; }
.s-home-v3--light.s-home-v3--review.s-home-v3--artist { background: #f0ece3; }
.s-home-v3--artist .v3-bg-fill { display: none; }
/* 0.12 (2026-10-02): the album page keeps the theme background too — no more
   flood from the cover behind the page (Eric). The flood is the cover-derived
   --v3-box1-color that applyColorVars sets INLINE, and the page isn't the only
   thing painted with it — the bento fill behind the cover is too, and it
   showed as a stray rectangle once only the page went dark. So pin the var
   itself here (!important beats the inline style) and everything that reads
   it lands on the theme colour together. */
/* …and it is the HOMEPAGE's bg, not a fixed #111116 (Eric): --sd-bg, which a
   persona skin re-points per screen, so home and the album page match. */
.s-home-v3--review.s-home-v3--album { --v3-box1-color: var(--sd-bg, #111116) !important; background: var(--sd-bg, #111116); }
/* Light theme: the album page goes cream like the artist page (Eric,
   2026-10-14 — it used to stay dark). The artist page's `--light.--artist`
   re-inks are scoped `--light.--album` now, so they cover both; the album-only
   bits are re-inked in the ALBUM PAGE, LIGHT block at the end of the file. */
.s-home-v3--light.s-home-v3--review.s-home-v3--album { --v3-box1-color: #f0ece3 !important; background: #f0ece3; }
/* 0.12 (2026-10-02): ONE YELLOW on the album page (Eric) — the histogram
   bars, the discs and every rating read the house gold instead of the
   cover-derived colour. applyColorVars sets --v3-star / --v3-accent INLINE,
   so they're pinned here with !important; --star resolves off --v3-star on
   .s-home-v3, so the bars and discs follow. (Also the artist page, which
   shares the --album class.) */
.s-home-v3--review.s-home-v3--album { --v3-star: #e8a83c !important; --v3-accent: #e8a83c !important; }
/* 0.12 (2026-10-02): the artist page's favorite heart is just the heart now,
   like the reviews' like — no embossed pill behind it (Eric). Same spot. */
.s-home-v3--review.s-home-v3--album.s-home-v3--artist .v3-artist-fav,
.s-home-v3--light.s-home-v3--review.s-home-v3--album.s-home-v3--artist .v3-artist-fav {
  background: none; box-shadow: none; width: auto; padding: 4px;
}
/* Match the trending page's 12px side margins (drop the review panel's extra 10px inset) */
.s-home-v3--artist .v3-review-panel { padding-left: 0; padding-right: 0; }
/* No genre line — the artist name stands alone under the banner. (populateArtistPage
   still writes the genre into .v3-blue-artist; it's just not shown here.) */
.s-home-v3--artist .v3-blue-artist,
.s-home-v3--artist .v3-blue-sep { display: none; }
/* ── Artist page, LIGHT — the one screen in this state with a cream page ──
   The album page keeps the album's dark procedural colour behind the review
   panel in BOTH themes, which is why every `.s-home-v3--light .v3-rev-*` rule
   is light ink. The artist page forces the cream bg instead, so that same ink
   lands invisible — these re-ink the panel dark. Scoped `--light.--artist`
   (0,3,0) so it beats the generic `--light` rules without !important. */
.s-home-v3--light.s-home-v3--album .v3-blue-album { color: #1a1208; }
.s-home-v3--light.s-home-v3--album .v3-blue-count,
.s-home-v3--light.s-home-v3--album .v3-rev-count { color: rgba(26,18,8,0.4); }
.s-home-v3--light.s-home-v3--album .v3-rev-cta {
  background: rgba(26,18,8,0.06); border-color: rgba(26,18,8,0.12); color: #1a1208;
}
.s-home-v3--light.s-home-v3--album .v3-rev-rec {
  background: rgba(26,18,8,0.05); border-color: rgba(26,18,8,0.1); color: rgba(26,18,8,0.7);
}
.s-home-v3--light.s-home-v3--album .v3-rev-rec-name { color: #1a1208; }
.s-home-v3--light.s-home-v3--album .v3-rev-filters { border-top-color: rgba(26,18,8,0.12); }
.s-home-v3--light.s-home-v3--album .v3-rev-filter { color: rgba(26,18,8,0.4); }
.s-home-v3--light.s-home-v3--album .v3-rev-filter.active { color: #b06a2a; }
.s-home-v3--light.s-home-v3--album .v3-rev-card { border-bottom-color: rgba(26,18,8,0.1); }
.s-home-v3--light.s-home-v3--album .v3-rev-name { color: rgba(26,18,8,0.85); }
.s-home-v3--light.s-home-v3--album .v3-rev-text { color: rgba(26,18,8,0.55); }
.s-home-v3--light.s-home-v3--album .v3-rev-score { color: rgba(26,18,8,0.5); }
.s-home-v3--light.s-home-v3--album .v3-rev-time,
.s-home-v3--light.s-home-v3--album .v3-rev-likes { color: rgba(26,18,8,0.3); }
.s-home-v3--light.s-home-v3--album .v3-rev-empty { color: rgba(26,18,8,0.4); }
/* Comment thread, re-inked for the artist page's cream panel (see the ⚠️ on
   `.v3-cmt-*` — every rule there needs one of these). */
.s-home-v3--light.s-home-v3--album .v3-cmt-kids { border-left-color: rgba(26,18,8,0.14); }
.s-home-v3--light.s-home-v3--album .v3-cmt-av { background-color: rgba(26,18,8,0.08); }
.s-home-v3--light.s-home-v3--album .v3-cmt-user { color: rgba(26,18,8,0.8); }
.s-home-v3--light.s-home-v3--album .v3-cmt-text { color: rgba(26,18,8,0.55); }
.s-home-v3--light.s-home-v3--album .v3-cmt { border-top-color: rgba(26,18,8,0.08); }
.s-home-v3--light.s-home-v3--album .v3-cmt-handle { color: rgba(26,18,8,0.42); }
.s-home-v3--light.s-home-v3--album .v3-cmt-ago,
.s-home-v3--light.s-home-v3--album .v3-cmt-like,
.s-home-v3--light.s-home-v3--album .v3-cmt-reply { color: rgba(26,18,8,0.32); }
.s-home-v3--light.s-home-v3--album .v3-cmt-like:hover,
.s-home-v3--light.s-home-v3--album .v3-cmt-reply:hover { color: rgba(26,18,8,0.75); }
.s-home-v3--light.s-home-v3--album .v3-cmt-like.is-on { color: #b06a2a; }
.s-home-v3--light.s-home-v3--album .v3-cmt-none { color: rgba(26,18,8,0.4); }
.s-home-v3--light.s-home-v3--album .v3-cmt-input {
  color: #1a1208; background: rgba(26,18,8,0.05); border-color: rgba(26,18,8,0.14);
}
.s-home-v3--light.s-home-v3--album .v3-cmt-input::placeholder { color: rgba(26,18,8,0.3); }
.s-home-v3--light.s-home-v3--album .v3-cmt-to { background: rgba(176,106,42,0.16); color: #b06a2a; }

/* ── Artist albums — only shown on the artist page ────────────────
   Replaces the rating histogram. A section head with a row/grid toggle, then
   either a horizontal rail or the trending grid (both reuse .wall2-* styling,
   so a cover looks the same wherever it appears). Built by populateArtistPage. */
.v3-artist-albums { display: none; }
/* ⚠️ The top padding is the CLEARANCE for the info box's overhang, moved here
   (Eric, 2026-09-25). It used to ride on `.v3-rev-mine` (`26px +
   --rev-cred-h`, see the album page's rule) and this grid sat under that band;
   with the YOUR REVIEW / COMMUNITY REVIEW block hidden on the artist page the
   grid became the panel's first visible child and slid up under the artist
   info and the Favorite pill. Same `--rev-cred-h` (125px, or 55px with no
   description) plus the air; the 24px is the one number to turn. */
.s-home-v3--artist .v3-artist-albums {
  display: block;
  padding: calc(24px + var(--rev-cred-h, 0px)) 12px 0;
}
.v3-aa-hd {
  display: flex; align-items: center; gap: 8px;
  padding: 0 0 9px;
}
.v3-aa-title {
  font-size: 13px; font-weight: 800; letter-spacing: -0.2px;
  color: #e8e2d6;
}
.v3-aa-n {
  font-family: var(--font-mono); font-size: 9.5px;
  color: rgba(232,226,214,0.32);
}
/* Row/grid switch — a two-cell segmented control, mirrors .set-seg's language */
.v3-aa-seg {
  margin-left: auto; display: flex; gap: 2px; padding: 2px;
  border-radius: 999px; background: rgba(255,255,255,0.05);
}
.v3-aa-seg button {
  display: flex; align-items: center; justify-content: center;
  width: 24px; height: 20px; padding: 0;
  border: none; border-radius: 999px; background: none; cursor: pointer;
  color: rgba(232,226,214,0.4);
  transition: background .15s, color .15s;
}
.v3-aa-seg button:hover { color: #e8e2d6; }
.v3-aa-seg button.active { background: rgba(232,168,60,0.16); color: var(--star); }
.v3-aa-seg svg { width: 13px; height: 13px; }
/* Row mode — a horizontal rail; the cell width is fixed so covers line up */
.v3-aa-row {
  display: flex; gap: 10px; overflow-x: auto; scroll-snap-type: x proximity;
  padding-bottom: 4px; margin: 0 -12px; padding-left: 12px; padding-right: 12px;
  /* snap-align:start snaps to the SCROLLPORT edge, which would scroll the
     padding away and leave the first cover flush to the frame — match it */
  scroll-padding-left: 12px;
  scrollbar-width: none;
}
.v3-aa-row::-webkit-scrollbar { display: none; }
/* A row cover is the SAME size as a grid cover (Eric, 2026-09-18 — it was a
   fixed 96px, a step smaller): a third of the row's content width less the
   grid's two 8px column gaps, i.e. exactly what `repeat(3, 1fr)` with
   `gap: … 8px` gives the grid. The row's own gap is 8px too, so the first
   three sit where the grid's would and the fourth peeks. ⚠️ The row's content
   box IS the container's width (its -12px margins are paid back as padding),
   so the 100% here and the grid's are the same measure. */
.v3-aa-row { gap: 8px; }
.v3-aa-row .wall2-cell { flex: 0 0 calc((100% - 16px) / 3); scroll-snap-align: start; }
/* The reviews heading is emitted as the last block of .v3-artist-albums (it sits
   directly above .v3-rev-filters in the panel), so it hides with the container
   when the page leaves the artist state. */
.v3-aa-hd--rev { padding-top: 16px; }
/* Artist page — light theme */
.s-home-v3--light .v3-aa-title { color: #1a1208; }
.s-home-v3--light .v3-aa-n { color: rgba(26,18,8,0.4); }
.s-home-v3--light .v3-aa-seg { background: rgba(26,18,8,0.05); }
.s-home-v3--light .v3-aa-seg button { color: rgba(26,18,8,0.4); }
.s-home-v3--light .v3-aa-seg button:hover { color: #1a1208; }
.s-home-v3--light .v3-aa-seg button.active { background: rgba(176,106,42,0.14); color: #b06a2a; }
.s-home-v3--review.s-home-v3--album .v3-album {
  left: 0; top: 0; width: 100%; height: auto; aspect-ratio: 1;
  border-radius: 12px;
}
/* For-You strip drops away (no room beside a full-width cover). The Back pill stays,
   upper-left — in album view it steps back to the fullscreen state (see onLivePill). */
.s-home-v3--review.s-home-v3--album .v3-for-single { display: none; }
/* Stats fall just below the enlarged cover (cover bottom ≈ 80%). */
.s-home-v3--review.s-home-v3--album .v3-blue { top: 81%; }
/* CD: raised so its top lines up with the top of the album name; kept circular so the
   taller bento can't stretch it into an ellipse (hand mode still controls the side). */
.s-home-v3--review.s-home-v3--album .v3-cd { top: 84%; height: auto; aspect-ratio: 1; }
/* Pull the whole review panel (friend pill + CTA + histogram + tracklist) up so it sits
   ~15px below the album's review-numbers row instead of way down the taller bento. */
.s-home-v3--review.s-home-v3--album .v3-review-panel { margin-top: -28px; }

/* Seamless grow: toggling into album view swells the cover to fill the width while
   the bento (and its %-positioned stats + CD) glide to the taller layout. Scoped to
   review mode so it only animates the album toggle, never the home render. */
.s-home-v3--review .v3-bento { transition: aspect-ratio 0.42s cubic-bezier(0.4,0,0.2,1); }
.s-home-v3--review .v3-album {
  transition: left 0.42s cubic-bezier(0.4,0,0.2,1), top 0.42s cubic-bezier(0.4,0,0.2,1),
              width 0.42s cubic-bezier(0.4,0,0.2,1), border-radius 0.42s ease;
}
.s-home-v3--review .v3-blue { transition: top 0.42s cubic-bezier(0.4,0,0.2,1); }

/* Live pill ⇄ Back arrow */
.v3-back-content { display: none; align-items: center; gap: 3px; }
.s-home-v3--review .v3-live-content { display: none; }
.s-home-v3--review .v3-back-content { display: inline-flex; }

/* …and again while the mix dial is up. The dial has no close button of its
   own: this pill is the app's ONE dedicated back button, so it is what gets you
   out of a gesture that is still "held" — the cover-hold opened the dial and
   does not resolve until you commit a mix or leave.
   ⚠ No z-index lift is needed and none should be added back. It WAS `201`,
   because the dial used to be a bottom sheet whose scrim (`.sd-log-overlay`,
   `inset: 0; z-index: 200`) lay over the whole bento and swallowed the tap
   while the pill still looked perfectly clickable. The dial now lives inside
   `.v3-album`, which is `z-index: 1` with `overflow: hidden` — it cannot reach
   the pill at 6, let alone cover it. */
.s-home-v3--mixing .v3-live-content { display: none; }
.s-home-v3--mixing .v3-back-content { display: inline-flex; }

/* Your review — album/stats-side column; 12px left pad aligns with stats text.
   flex column so the header/stars/textarea stack tight and Post can go lower-right. */
.v3-rev-mine {
  flex: 1; min-width: 0;
  margin-top: 26px;   /* clears the taller stacked title above (was -5px) */
  padding: 0 0 6px 12px;
  display: flex; flex-direction: column;
}
/* Long album titles wrap to two lines in review mode — drop the CTA (and everything
   below it, which follows in flow) down an extra line to clear the taller title. */
.v3-rev-title-2line .v3-rev-mine { margin-top: 48px; }
.v3-rev-mine-hd {
  font-size: 8px; font-weight: 700; letter-spacing: 0.8px;
  text-transform: uppercase; color: rgba(232,226,214,0.42);
  margin-bottom: 2px; display: flex; gap: 7px; align-items: baseline;
  font-family: var(--font-main);
}
.v3-rev-album-name {
  color: rgba(232,226,214,0.7); text-transform: none;
  letter-spacing: 0; font-weight: 600; font-size: 9px;
}
.v3-rev-stars { display: flex; gap: 3px; margin-bottom: 4px; }
.v3-rev-star {
  font-size: 33px; line-height: 1; cursor: pointer;
  color: rgba(232,226,214,0.18); border-radius: 5px;
  transition: color 0.12s, transform 0.12s;
}
.v3-rev-star:hover { transform: scale(1.14); }
.v3-rev-star.on { color: var(--star, #e8a83c); }
.v3-rev-star.half {
  background: linear-gradient(90deg, var(--star, #e8a83c) 50%, rgba(232,226,214,0.18) 50%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}
.v3-rev-write {
  display: block; width: 100%; box-sizing: border-box; resize: none;
  overflow: hidden; min-height: 22px;
  background: transparent; border: none; padding: 0;
  color: #e8e2d6; font-family: var(--font-main); font-size: 13px; line-height: 1.5;   /* DM Sans, no serif (Eric, 2026-09-16) — reviews are sans everywhere now */
}
.v3-rev-write::placeholder { color: transparent; }   /* custom hint overlay used instead */
.v3-rev-write:focus { outline: none; }

/* Custom "Write a review…" hint with a blinking caret (shown only when empty & unfocused) */
.v3-rev-write-wrap { position: relative; }
.v3-rev-ph {
  display: none;
  position: absolute; top: 0; left: 0;
  font-family: var(--font-main); font-size: 13px; line-height: 1.5;
  color: rgba(232,226,214,0.3);
  pointer-events: none; white-space: nowrap;
}
.v3-rev-write:placeholder-shown + .v3-rev-ph { display: block; }
.v3-rev-write:focus + .v3-rev-ph { display: none; }
.v3-rev-caret {
  display: inline-block; width: 1px; height: 0.95em; margin-left: 2px;
  vertical-align: -0.12em; background: rgba(232,226,214,0.4);
  animation: v3caret 2.8s ease-in-out infinite;
}
@keyframes v3caret { 0%, 100% { opacity: 1; } 50% { opacity: 0.12; } }
.s-home-v3--light .v3-rev-ph { color: rgba(232,226,214,0.3); }
.s-home-v3--light .v3-rev-caret { background: rgba(232,226,214,0.4); }
.v3-rev-submit {
  margin-top: 11px; align-self: flex-end; display: none;   /* shown by JS only once there's text */
  padding: 4px 12px; border-radius: 999px;
  border: 1px solid var(--v3-accent, #e8a83c);
  background: transparent; color: var(--v3-accent, #e8a83c);
  font-family: var(--font-main); font-size: 10.5px; font-weight: 700; letter-spacing: 0.2px;
  cursor: pointer; transition: background 0.15s, color 0.15s, opacity 0.15s;
}
.v3-rev-submit:hover { background: var(--v3-accent, #e8a83c); color: #16130d; }
.v3-rev-submit:disabled { opacity: 0.55; cursor: default; background: transparent; }

/* Photo / media strip — small, horizontally scrollable; right edge fades to hint swipe */
.v3-rev-media {
  display: flex; flex-direction: column; gap: 8px;
}
.v3-rev-media::-webkit-scrollbar { display: none; }
.v3-rev-photo {
  flex: 0 0 auto;
  width: 46px; height: 46px;
  border-radius: 7px;
  background-size: cover; background-position: center;
  background-color: rgba(232,226,214,0.06);
  position: relative;
}
/* Music-video thumbnail — play badge */
.v3-rev-play {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
}
.v3-rev-play::before {
  content: '';
  position: absolute;
  width: 22px; height: 22px; border-radius: 50%;
  background: rgba(0,0,0,0.5);
  box-shadow: 0 1px 4px rgba(0,0,0,0.4);
}
.v3-rev-play::after {
  content: '';
  position: relative;
  border-style: solid;
  border-width: 5px 0 5px 8px;
  border-color: transparent transparent transparent #fff;
  margin-left: 2px;
}
.s-home-v3--light .v3-rev-photo { background-color: rgba(232,226,214,0.06); }

/* Tracklist — full-width, rate individual songs. Scrolls after ~8 rows. */
.v3-rev-songs {
  width: 100%; box-sizing: border-box; margin: 10px 0 2px;
  /* Tracklist column widths — read by both the rows and the header labels. */
  --song-dur-w: 54px;   /* 0.14: was 42, widened for the "COMMENTS" label */
  --song-rate-w: 42px;
}

/* Review-spread histogram — 10 buckets (½★ … 5★), between the CTA and the tracklist */
/* Negative top pulls the histogram (and the tracklist below it) up beside the taller
   streaming-icon column, so "the review" sits ~10px under the Review/rate/log button. */
.v3-rev-hist { box-sizing: border-box; padding-left: 12px; padding-right: 12px; margin: -36px 0 4px; }
.v3-rev-hist-hd {
  display: flex; align-items: baseline; gap: 8px; margin-bottom: 7px;
  font-size: 8px; font-weight: 700; letter-spacing: 0.8px; text-transform: uppercase;
  color: rgba(232,226,214,0.42);
}
.v3-rev-hist-sub { font-size: 8px; font-weight: 500; letter-spacing: 0.4px; color: rgba(232,226,214,0.3); }
.v3-rev-hist-bars {
  display: flex; align-items: flex-end; gap: 3px; height: 32px;
  padding-bottom: 5px; border-bottom: 1px solid rgba(232,226,214,0.1);
}
.v3-rev-hist-bar { flex: 1; height: 100%; display: flex; align-items: flex-end; }
.v3-rev-hist-bar > span {
  display: block; width: 100%; min-height: 2px; border-radius: 2px 2px 0 0;
  /* both stops ride --star, so the bars re-tint with the album like the vinyls */
  background: linear-gradient(to top,
    color-mix(in srgb, var(--star, #e8a83c) 40%, transparent),
    var(--star, #e8a83c));
}
.v3-rev-hist-axis {
  display: flex; justify-content: space-between; margin-top: 4px;
  font-size: 9px; color: rgba(232,226,214,0.4); font-family: var(--font-mono);
}
.s-home-v3--light .v3-rev-hist-hd { color: rgba(232,226,214,0.42); }
.s-home-v3--light .v3-rev-hist-sub { color: rgba(232,226,214,0.32); }
.s-home-v3--light .v3-rev-hist-bars { border-bottom-color: rgba(232,226,214,0.12); }
.s-home-v3--light .v3-rev-hist-axis { color: rgba(232,226,214,0.4); }

/* ── Review section = the bento's column grid, continued BELOW the bento ──
   CD column → streaming icons (10px under the CD). Content column → CTA, then the
   reviews (10px under the CTA). The icon column is taken OUT OF FLOW (absolute) so
   its height can never drag the reviews down — that was the broken spacing. */
.s-home-v3--review .v3-review-panel { position: relative; }
/* Media column — rides the CD side in review, flowing straight down and fading out. */
.s-home-v3--review .v3-rev-media {
  position: absolute; top: 11px;   /* lowered 4px */
  width: 46px; max-height: 360px;
  margin: 0; padding: 0;
  overflow: hidden;
  /* fade only near the bottom, matching the tracklist (which fades at 82%) */
  -webkit-mask-image: linear-gradient(to bottom, #000 80%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 80%, transparent 100%);
}
.s-home-v3--review.s-home-v3--left    .v3-rev-media { left: 25px; right: auto; }
.s-home-v3--review:not(.s-home-v3--left) .v3-rev-media { right: 25px; left: auto; }
/* Album view: icons ride up with the raised CD (and the pulled-up panel). */
.s-home-v3--review.s-home-v3--album .v3-rev-actions { top: 3px; }
/* Reviews sit exactly 10px below the CTA button (icon column no longer in the way). */
.s-home-v3--review .v3-rev-mine { padding-bottom: 0; }
/* Album + artist pages: the CTA is centred under the full-width art rather than
   hung off the stats column (there's no CD column beside it in these states).
   --artist inherits this — it always carries --album too. */
/* ⚠️ `stretch`, not `center`. Centring left the control floating inside its own
   column — measured 268.2px of button inside 321.2px of album, i.e. 26.5px of
   air on each side, while the histogram, the tracklist and the cover all sat
   flush at the album's edges. The row is the only thing on this page that
   wasn't on that grid. It now spans the album exactly; `.v3-rev-cta` grows to
   fill whatever the three fixed squares don't take. */
.s-home-v3--review.s-home-v3--album .v3-rev-mine {
  align-items: stretch;
  padding-left: 0; padding-right: 0;
  /* ⚠️ Clears the CREDITS. `.v3-blue` is absolutely positioned, and in this
     state it is `height: auto; overflow: visible` — so the three credit rows
     (Produced by / Mixed by / Label) grow it DOWNWARD out of its own box, and
     nothing in normal flow below knows they are there. The CTA row was landing
     on top of them. `--rev-cred-h` is that overhang; see the screen-level rule
     below for why it is declared there and not here. */
  margin-top: calc(26px + var(--rev-cred-h, 0px));
}
/* ⚠️ Declared on the SCREEN, not on `.v3-rev-mine`. Custom properties inherit
   DOWN, and `.v3-blue-credits` is a sibling of `.v3-rev-mine`, not a child — so
   a var set on the column is invisible to the block it is measuring. The screen
   is the nearest ancestor of both.
   The number: 3 rows × 10px names at line-height 1.28 ≈ 38px, so 40 leaves a
   hair of air. Turn this if the credit rows are ever restyled. */
.s-home-v3--review.s-home-v3--album { --rev-cred-h: 50px; }   /* 40 + the 10px of air over the credits below */
/* 10px of air between the artist and the credits on the album page (Eric,
   2026-09-18). The compact bento has its own 7px in its `:not(--review)` rule;
   this is the review state's. ⚠️ It is overhang too — see --rev-cred-h above,
   which carries it so the CTA row still clears the block. */
.s-home-v3--review .v3-blue-credits { padding-top: 10px; }
/* Coverage is patchy (MusicBrainz is volunteer-entered) and the whole block is
   `hidden` when nothing came back — in which case there is no overhang to clear
   and the reserved space would just be a hole. */
.s-home-v3--review.s-home-v3--album:has(.v3-blue-credits[hidden]) { --rev-cred-h: 0px; }
/* ⚠️ 80%, not 100%. Full width put the row on the album's own grid, which
   read as correct next to the histogram and the tracklist — but it also made
   the page's one ACTION the widest element on it, so it stopped reading as a
   button and became another band of layout. The 20% it gives up is what makes
   it look pressable again.
   ⚠️ CENTRED, via `margin-inline: auto`. `.v3-rev-mine` is a flex COLUMN, so
   the cross axis is horizontal and `align-items: stretch` is what positions
   this — once the row has an explicit width, stretch has nothing to stretch and
   drops it on the left edge. `margin-inline: auto` is what re-centres a
   fixed-width item on a flex cross axis; `justify-content` here would move the
   column's children vertically instead. */
.s-home-v3--review.s-home-v3--album .v3-rev-cta-row {
  width: 80%;
  margin-inline: auto;
}

/* ══ THE RATE GROUP (Eric's RateGroup.svg, 2026-09-18) ═══════════════════
   Replaces the CTA + quick-log strip: `rateGroupHtml()` in screens.js. The
   box is the drawing's 1633×1321 (RateGroupnew2.svg) at 83% of the column —
   the drawing grew wider round the same disc, so 83% keeps the disc the size
   it was at 80% of the old 1575 — centred like the row it replaces; every control is placed in % of that viewBox, so it scales
   with the phone. The SVG is the surfaces (wings + disc), pointer-events
   off; the five buttons are transparent boxes over the zones:
       circle   x 329→1300 · y 0→972          RATE
       TL lobe  x 0→306    · y 604→923        Listened
       BL lobe  x 432→793  · y ~1024→1320     Later     (a near-circle, r 146 about 647,1174)
       TR / BR  mirrored                      Favorite / Share
   Ink is the album flood's (light in both themes), like everything on this
   panel. `.on` (the same class the old squares carried; toggleRevAction and
   syncQuickLog paint it) lights a lobe's glyph and label in the album gold. */
.v3-rate-group {
  position: relative; width: 83%; margin-inline: auto;
  aspect-ratio: 1633 / 1321;
  margin-bottom: 19px;   /* room for the lower lobes' labels, which hang below the box (8px gap + the 8px word + air) */
  color: rgba(232,226,214,0.82);
}
.v3-rg-shape { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
.v3-rg-wing, .v3-rg-disc {
  fill: rgba(232,226,214,0.06);
  stroke: rgba(232,226,214,0.14); stroke-width: 1px; vector-effect: non-scaling-stroke;
}
/* The circle fills with the album gold once you've rated (syncRevCta →
   .is-rated on the button; the disc follows through :has). */
.v3-rate-group:has(.v3-rg-rate.is-rated) .v3-rg-disc { fill: var(--star, #e8a83c); stroke: transparent; }
.v3-rg-rate, .v3-rg-btn {
  position: absolute; margin: 0; padding: 0;
  background: none; border: none; color: inherit; font-family: var(--font-main);
  cursor: pointer; -webkit-tap-highlight-color: transparent;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.v3-rg-rate {
  left: 20.13%; top: 0.04%; width: 59.47%; height: 73.52%;
  border-radius: 50%; gap: 4px;
  transition: color .15s;
}
.v3-rg-word--sm { font-size: 14px; font-weight: 700; color: rgba(232,226,214,0.92); }
/* "RATE · REVIEW · LOG" along the top of the disc, in viewBox units (the box
   is ~5.25 units per px at phone size, so 58 ≈ 11px). */
/* ⚠️ No `fill` here in the resting state — the markup's fill attribute is
   the sweeping-glow gradient (see rateGroupHtml), and a CSS fill would beat
   it. Hover brightens by opacity for the same reason. */
.v3-rg-arc {
  font-family: var(--font-main); font-size: 58px; font-weight: 700; letter-spacing: 0.14em;
  opacity: 0.9;
}
.v3-rate-group:hover .v3-rg-arc { opacity: 1; }
/* "_._": the rating's shape, before there is one. The number's face, dim. */
.v3-rg-blank {
  font-family: var(--font-main); font-size: 180px; font-weight: 800; letter-spacing: -0.03em;
  fill: rgba(232,226,214,0.28);
}
/* Rated or written: the middle belongs to the number / the word. The arc
   stays as the button's label, in the gold disc's dark ink when rated — a
   CSS fill, which wins over the gradient attribute. */
.v3-rate-group:has(.v3-rg-rate.is-rated) .v3-rg-blank,
.v3-rate-group:has(.v3-rg-rate.is-written) .v3-rg-blank { display: none; }
.v3-rate-group:has(.v3-rg-rate.is-rated) .v3-rg-arc { fill: rgba(22,19,13,0.6); }
/* Rated: dark ink on the gold disc — number over the discs. */
.v3-rg-rate.is-rated { color: #16130d; --vinyl-empty: rgba(22,19,13,0.28); --star: #16130d; }
.v3-rg-num { font-size: 34px; font-weight: 800; line-height: 1; letter-spacing: -0.03em; }
.v3-rg-rate.is-rated .hstars { gap: 2px; }
.v3-rg-btn { gap: 5px; transition: color .15s; }
.v3-rg-btn svg { width: 17px; height: 17px; }
/* The lobes' labels sit OUTSIDE the bubbles (Eric, 2026-09-18): above the two
   upper lobes, below the two lower ones, so each bubble holds its glyph alone.
   Absolutely placed off the button, so the glyph stays centred in the lobe;
   they are children of the button, so they still take its hover / .on colour
   and a tap on the word is a tap on the lobe. The lower pair hang past the
   drawing's box — .v3-rate-group's margin-bottom is their room. */
.v3-rg-lbl {
  position: absolute; left: 50%; transform: translateX(-50%);
  font-size: 8px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;
  white-space: nowrap; line-height: 1;
}
/* 8px off the bubble (Eric, 2026-09-18: was 5 — the words sat too close to the shape). */
.v3-rg-btn--tl .v3-rg-lbl, .v3-rg-btn--tr .v3-rg-lbl { bottom: calc(100% + 8px); }
.v3-rg-btn--bl .v3-rg-lbl, .v3-rg-btn--br .v3-rg-lbl { top: calc(100% + 8px); }
.v3-rg-btn:hover { color: #fff; }
.v3-rg-btn.on { color: var(--star, #e8a83c); }
.v3-rg-btn--tl { left: 0;      top: 45.73%; width: 18.76%; height: 24.17%; }
.v3-rg-btn--tr { left: 81.24%; top: 45.73%; width: 18.76%; height: 24.17%; }
.v3-rg-btn--bl { left: 26.47%; top: 77.5%;  width: 22.09%; height: 22.4%; }  /* the lower lobe, read off RateGroupnew2.svg */
.v3-rg-btn--br { left: 51.40%; top: 77.5%;  width: 22.09%; height: 22.4%; }
/* The artist page has no log control here (its corner Favorite pill is it). */
.s-home-v3--artist .v3-rate-group { display: none; }
.s-home-v3--review.s-home-v3--album .v3-rev-cta {
  flex: 1 1 auto;
  width: auto;              /* was fit-content — that's what kept it text-sized */
  justify-content: flex-start;
  /* The base -2px optically nudged a floating button; flush against the album
     it just hangs 2px past the edge the row is now aligned to. */
  margin-left: 0;
}
/* ── Quick-log squares UNDER the CTA (2026-09-11) ─────────────────────────
   The row was CTA + three squares side by side; it is now two rows — the
   button full width on top, the three toggles as one joined strip beneath it
   spanning the same width. Cleaner, and the CTA gets the whole line for its
   label. The strip keeps the cascade (shared borders, outer corners only) so
   it still reads as one control; `--sd-q` no longer sets a width here — the
   cells share the row — only the strip's height. */
.s-home-v3--review.s-home-v3--album .v3-rev-cta-row {
  flex-direction: column; align-items: stretch; gap: 6px;
  min-height: 0;
}
.s-home-v3--review.s-home-v3--album .v3-rev-cta-row .v3-rev-cta { border-radius: 7px; justify-content: center; }
.s-home-v3--review.s-home-v3--album .v3-rev-quick { height: 42px; }
.s-home-v3--review.s-home-v3--album .v3-rev-q { flex: 1 1 0; width: auto; }
.s-home-v3--review.s-home-v3--album .v3-rev-q:first-child { margin-left: 0; border-radius: 7px 0 0 7px; }
/* ── The CTA once you have rated (syncRevCta) ─────────────────────────────
   Your discs + "Your rating 4.5" on the left, an "Edit rating?" pill on the
   right — the button asks rather than repeats the invitation. Same box, same
   height, same handler. */
.s-home-v3--review.s-home-v3--album .v3-rev-cta-row .v3-rev-cta--rated { justify-content: flex-start; }
.v3-rev-cta--rated .hstars { display: inline-flex; gap: 3px; flex-shrink: 0; }
.v3-rev-cta-you { flex: 1; min-width: 0; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.v3-rev-cta-you b { color: var(--star, #e8a83c); font-weight: 800; }
.v3-rev-cta-edit {
  flex-shrink: 0; padding: 4px 9px; border-radius: 999px;
  border: 1px solid rgba(255,255,255,0.16);
  font-size: 10.5px; font-weight: 600; letter-spacing: 0.01em;
  color: rgba(232,226,214,0.75);
}
.v3-rev-cta--rated:hover .v3-rev-cta-edit { background: rgba(255,255,255,0.08); color: #fff; }
.s-home-v3--review .v3-rev-hist { margin-top: 10px; }
/* The full tracklist, in flow — it no longer caps its height and scrolls inside
   itself (the page is the scroller; a nested one hid the rest of the album). */
.v3-rev-songs-scroll { display: flex; flex-direction: column; }
.v3-song-row {
  position: relative;
  display: flex; align-items: center; gap: 10px;
  width: 100%; box-sizing: border-box;
  /* vertical rhythm matches the playlist page's .plp-song (9px breathing) */
  padding: 9px 12px; text-align: left;   /* title starts at 12px → aligns with album name, artist & the CTA */
  background: none; border: none; cursor: pointer;
  border-top: 1px solid rgba(232,226,214,0.06);
  color: #e8e2d6; font-family: var(--font-main); font-size: 12px;
}
.v3-song-row:first-child { border-top: none; }
.v3-song-row:hover { background: rgba(232,226,214,0.045); }
/* Track number — a fixed-width cell at the head of the row (2026-09-04). It
   used to hang absolutely in the 12px padding gutter so the title stayed on the
   album-name line, but "10" at this size is the whole gutter and touched the
   title. A flex cell costs the title 24px of indent and reads as a numbered
   list. The header carries an empty twin so "SONG" still sits over the titles. */
.v3-song-num {
  flex-shrink: 0; width: 14px; text-align: left;   /* flush with the 12px gutter, like the title above */
  color: rgba(232,226,214,0.4); font-size: 9.5px; font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.v3-song-title { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The two right-hand columns are a FIXED width, shared with the header labels
   above them (--song-dur-w / --song-rate-w on .v3-rev-songs). Sized to the
   labels, which are wider than the values they head — left to size themselves,
   "LENGTH" and "RATING" would each be wider than the column and the header
   would sit off its own list. */
.v3-song-dur {
  flex-shrink: 0; width: var(--song-dur-w); text-align: right;
  color: rgba(232,226,214,0.4);
  font-size: 10px; font-family: var(--font-mono);
}
.v3-song-rate {
  flex-shrink: 0; display: inline-flex; align-items: center;
  width: var(--song-rate-w); justify-content: flex-end;
  color: var(--star); font-size: 11.5px; font-weight: 700; font-family: var(--font-mono);
}
/* YOUR rating, green, with the gold aggregate one tap away (0.14). The cell
   gets a little padding so the tap target is not just the glyphs, pulled back
   out on the right so the column still ends where the header label does. */
.v3-song-rate.is-mine { color: var(--mine, #4ec97a); }
.v3-song-rate--tap { cursor: pointer; padding: 2px 5px; margin-right: -5px; border-radius: 5px; }
.v3-song-rate--tap:hover { background: rgba(232,226,214,0.09); }
.v3-song-rate--tap:active { background: rgba(232,226,214,0.14); }
/* A favourited song's TRACK NUMBER is a heart (Eric, 2026-09-18), in the
   number cell, so the columns hold. */
/* THE HEART'S HOLE APPEARS WHEN YOU LIKE (Eric, 2026-09-23). RVP_HEART ships
   two paths — the outline alone and the outline with the pulley hole — and
   the state picks: plain until `.is-on`, then the full one, which the
   existing `.is-on svg { fill }` rules fill with the hole left open. The
   song's favourite mark is always on, so it always shows the hole. */
.rvp-full { display: none; }
.is-on .rvp-full, .v3-song-fav .rvp-full { display: block; }
.is-on .rvp-out,  .v3-song-fav .rvp-out  { display: none; }
/* THE BEAT (Eric, 2026-09-24): on an unliked like-button heart, the pulley
   hole shows for a moment every five seconds and beats — in, a squeeze, and
   gone. Only on like pills; the song's favourite mark and a liked heart
   already carry the hole in their full path. */
.rvp-hole { display: none; }
.v3-up--heart:not(.is-on) .rvp-hole {
  display: block; opacity: 0;
  transform-box: fill-box; transform-origin: center;
  animation: rvp-beat 5s ease-in-out infinite;
}
@keyframes rvp-beat {
  0%, 100% { opacity: 0; transform: scale(.55); }
  5%       { opacity: 1; transform: scale(1); }
  9%       { transform: scale(.82); }
  13%      { transform: scale(1); }
  20%      { opacity: 0; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) { .v3-up--heart:not(.is-on) .rvp-hole { animation: none; opacity: 0; } }
.v3-song-fav { display: inline-flex; align-items: center; justify-content: center; color: #ff5c8a; }
.v3-song-fav svg { width: 11px; height: 11px; fill: currentColor; }
/* "View all N songs" — under the eighth row of a longer album. Text, not a
   pill: it belongs to the list, same gutter and hairline as the rows above. */
.v3-songs-more {
  display: block; width: 100%; box-sizing: border-box;
  padding: 10px 12px; text-align: left;
  background: none; border: none; cursor: pointer;
  border-top: 1px solid rgba(232,226,214,0.06);
  font-family: var(--font-main); font-size: 11.5px; font-weight: 600;
  color: var(--v3-accent, #e8a83c);
  -webkit-tap-highlight-color: transparent;
}
.v3-songs-more:hover { background: rgba(232,226,214,0.045); }
.v3-songs-more:active { opacity: 0.7; }
/* Column labels. Same flex row as .v3-song-row, reusing its three cell classes,
   so the labels track the columns automatically — only the type is restyled. */
.v3-song-head {
  display: flex; align-items: center; gap: 10px;
  padding: 0 12px 7px;
  font-family: var(--font-mono); font-size: 8.5px;
  text-transform: uppercase; letter-spacing: 0.12em;
  color: rgba(232,226,214,0.3);
}
.v3-song-head .v3-song-dur,
.v3-song-head .v3-song-rate {
  font-size: inherit; font-weight: 400; color: inherit;
}
/* Friend-rec tag — social proof, so it sits with the REVIEWS (below the
   tracklist, directly above the filter tabs), not up by the CTA. It aligns to
   the review list's 12px gutter and is hand-mode agnostic there. */
/* No bubble (Eric, 2026-09-18) — the photo and the line, bare, on the list's
   gutter; it was a pill with a fill and a hairline. */
.v3-rev-rec {
  display: flex; align-items: center; gap: 8px; width: fit-content;
  margin: 14px 12px 0; padding: 0;
  font-size: 11.5px; color: rgba(232,226,214,0.75);
}
.v3-rev-rec[hidden] { display: none; }
/* The pill supplies the gap above the filters when it shows */
.v3-rev-rec:not([hidden]) + .v3-rev-filters { margin-top: 8px; }
.v3-rev-rec-av {
  width: 22px; height: 22px; border-radius: 50%; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 9px; font-weight: 700; color: #fff;
}
.v3-rev-rec-name { color: #e8e2d6; font-weight: 700; }
.s-home-v3--light .v3-rev-rec { background: rgba(232,226,214,0.05); border-color: rgba(232,226,214,0.1); color: rgba(232,226,214,0.7); }
.s-home-v3--light .v3-rev-rec-name { color: #e8e2d6; }
.s-home-v3--light .v3-song-row { color: #e8e2d6; border-top-color: rgba(232,226,214,0.07); }
.s-home-v3--light .v3-song-row:hover { background: rgba(232,226,214,0.04); }
/* Light theme keeps LIGHT ink here: the album page floods with the album's
   procedural colour, which is dark in both variants. */
.s-home-v3--light .v3-song-num,
.s-home-v3--light .v3-song-dur { color: rgba(232,226,214,0.45); }
.s-home-v3--light .v3-song-head { color: rgba(232,226,214,0.36); }

/* Filters — text tabs, not pills; full-width with a divider above */
.v3-rev-filters {
  display: flex; align-items: baseline; gap: 16px;
  padding: 15px 0 5px; margin: 16px 12px 0;
  border-top: 1px solid rgba(232,226,214,0.1);
}
.s-home-v3--light .v3-rev-filters { border-top-color: rgba(232,226,214,0.1); }
.v3-rev-filter {
  padding: 0; border: none; background: none;
  color: rgba(232,226,214,0.4);
  font-family: var(--font-main); font-size: 12px; font-weight: 700; letter-spacing: 0.2px;
  cursor: pointer; transition: color 0.15s;
}
.v3-rev-filter.active { color: var(--v3-accent, #e8a83c); }
.v3-rev-count {
  margin-left: auto; font-size: 9.5px; font-family: var(--font-mono);
  color: rgba(232,226,214,0.32);
}

/* Review cards — styled to match the homepage friend-feed cards (compact) */
.v3-rev-list { display: flex; flex-direction: column; margin: 0 12px; }
/* The whole card is the "read the comments" target — see cmtCardTap. The pill
   in its corner is for WRITING one, and stops propagation so the two don't
   fight; so does `.v3-cmt-wrap`, or reading a thread would collapse it. */
.v3-rev-card { padding: 9px 0; border-bottom: 1px solid rgba(232,226,214,0.08); cursor: pointer; }
.v3-rev-card:last-child { border-bottom: none; }
/* The review you tapped in the home feed, pinned atop the list.
   ⚠️ It is a HIGHLIGHTED ROW, not a card in a box. The outlined, inset,
   rounded version read as stifled — a panel sitting on top of the list rather
   than the first item in it. So the tint bleeds past the list's 12px gutter
   while the padding puts the text back on the same left edge as every other
   review: it reads as one continuous list with one row lit up. Its own
   `.v3-rev-pin-chip` says why it's lit, so it needs no border to explain
   itself.
   ⚠️ EDGE TO EDGE (Eric, 2026-09-15): the band runs to the SCREEN's sides,
   not just past the list's gutter, so `--rev-bleed` is the list's 12px plus
   the panel's 10px on the album page. The artist page's panel has no side
   padding, so there it is the 12 alone. Change a gutter, change this.
   ⚠️ `.v3-rev-card.v3-rev-card--pinned`, two classes: the grid rule for
   `.v3-rev-card` below sets `padding: 11px 0` and comes later in the file, so
   a one-class rule lost its side padding and the tint began on the text's
   own edge — which is how this shipped looking like a box hugging the words. */
.v3-rev-card.v3-rev-card--pinned {
  --rev-bleed: 12px;
  margin-left: calc(-1 * var(--rev-bleed)); margin-right: calc(-1 * var(--rev-bleed));
  padding-left: var(--rev-bleed); padding-right: var(--rev-bleed);
  background: rgba(232,168,60,0.07);
}
.s-home-v3--album .v3-rev-card.v3-rev-card--pinned { --rev-bleed: 22px; }
.v3-rev-pin-chip {
  font-family: var(--font-mono); font-size: 7.5px; letter-spacing: 0.3px;
  color: #16130d; background: var(--star);
  padding: 2px 6px; border-radius: 999px;
  flex-shrink: 0;
}
/* ── The card is the review page's hero at list scale (2026-09-11) ────────
   photo · name over @handle · when | big score with the small records under
   it, right-aligned. Then the text, then the pills at the foot. `.v3-rev-meta`
   (the "rated" row) is no longer emitted. */
/* ⚠️ A GRID, two columns: the left holds the top row (photo · name) and the
   text under it; the right is the score column (number, records, like) spanning
   both rows. That is what keeps the text up against the name — as a flex row
   the score column's height (three things tall) pushed the text down with it. */
.v3-rev-card {
  padding: 11px 0;
  display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 12px;
  grid-template-areas: "top big" "rec big" "text big" "foot foot";
  /* ⚠️ `auto 1fr`: the score column spans rows 1–3 and is taller than a short
     review, and with all-auto rows the grid split its excess between them —
     which pushed the text down and read as "centred". The text row takes all
     of it, so the text sits under the name whatever its length.
     `rec` is the record line (home feed cards only); with nothing in it the
     row is 0 tall and there is no row-gap, so the album page's cards are
     laid out exactly as before. */
  grid-template-rows: auto auto 1fr auto;
  align-items: start;
}
/* The record line — a feed card's object, under its subject: a square cover
   beneath the round photo (the app's shape rule), with album (regular, a step
   larger) over artist (bold) over year (mono) beside it. Taps to the album.
   ⚠️ THE COVER IS THE LIKE BUTTON'S TWIN (Eric, 2026-09-14): the like is as
   wide as the records row — five 9px discs (halfStars(r, 13) → round(13 ×
   .72)) with four 2px gaps = 53px — and square, so the cover is 53px. Where
   the score column sits relative to it is the feed card's business (see the
   `.v3-rev-card--feed` object-row block below the light overrides). */
.v3-rev-record {
  grid-area: rec; display: flex; align-items: center; gap: 10px;
  margin-top: 0; min-width: 0; cursor: pointer;
}
.v3-rev-record-art {
  width: 53px; height: 53px; border-radius: 7px; flex-shrink: 0;
  background: #333 center/cover no-repeat;
  box-shadow: 0 0 0 1px rgba(255,255,255,0.08);
}
.v3-rev-record-who { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* YEAR · ALBUM on one line, the artist under it (Eric, 2026-09-16). All DM
   Sans — the year left its mono. The album name is the only elastic thing:
   it shrinks and its overflow FADES at the right edge (`.is-long`, measured
   by markLongReviews) instead of ending in an ellipsis. */
.v3-rev-record-line { display: flex; align-items: baseline; gap: 5px; min-width: 0; max-width: 100%; }
.v3-rev-record-album, .v3-rev-record-artist, .v3-rev-record-year {
  font-family: var(--font-main); white-space: nowrap; overflow: hidden; line-height: 1.2;
}
.v3-rev-record-artist { text-overflow: ellipsis; }
.v3-rev-record-album  { font-size: 13px; font-weight: 400; color: rgba(232,226,214,0.92); min-width: 0; flex-shrink: 1; }
.v3-rev-record-album.is-long {
  -webkit-mask-image: linear-gradient(to right, #000 62%, transparent 100%);
          mask-image: linear-gradient(to right, #000 62%, transparent 100%);
}
.v3-rev-record-artist { font-size: 11px; font-weight: 700; color: rgba(232,226,214,0.6); }
.v3-rev-record-year   { font-size: 11px; font-weight: 500; flex-shrink: 0; color: rgba(232,226,214,0.45); }
/* The year LEFT of the album name (Eric, 2026-09-17 — it went on the right
   for an hour first). It never shrinks; the album does — so when the album
   has had to give way, markLongReviews drops the year instead (`.no-year`)
   and only then lets the name fade. A long title beats a year. */
.v3-rev-record-line.no-year .v3-rev-record-year--auto { display: none; }
.v3-rev-record:hover .v3-rev-record-album { text-decoration: underline; text-underline-offset: 2px; }
/* A record line above the text wants a little more air than the name did. */
.v3-rev-record + .v3-rev-text { margin-top: 8px; }
.v3-rev-card-top { grid-area: top; display: flex; align-items: center; gap: 9px; margin-bottom: 0; }
.v3-rev-text { grid-area: text; }
.v3-rev-foot { grid-area: foot; }
/* ONE LINE (Eric, 2026-09-14): name · @handle · when run along a single
   baseline instead of stacking — cleaner on the feed card, where the byline
   sits over the record row, and on the album page's cards the same way. The
   name is the only elastic thing: it ellipses, the handle and time never
   wrap. The review page's hero keeps the stack (its 56px photo wants two
   lines beside it). */
.v3-rev-who { flex: 1; min-width: 0; display: flex; flex-direction: row; align-items: baseline; gap: 6px; }
.v3-rev-name-row { display: flex; align-items: center; gap: 6px; min-width: 0; flex-shrink: 1; }
.v3-rev-sub { flex-shrink: 0; }
/* The byline is DM Sans end to end (Eric, 2026-09-17): the name used to
   INHERIT — so Hank's skin set it in Crimson — beside a mono @handle. Both are
   pinned now so no persona font can leak in. */
.v3-rev-sub {
  font-family: var(--font-main); font-size: 10.5px; font-weight: 500;
  color: rgba(232,226,214,0.4);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 100%;
}
.v3-rev-big { grid-area: big; display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
/* The like: as wide as the column (the records set it, ~53px) and squarish,
   centred, a size up — a real button under the score, not a chip. */
/* A SQUARE (2026-09-11): as wide as the records row (the column), and as tall
   as it is wide — the heart over the count, stacked. */
.v3-rev-big .v3-up--like {
  align-self: stretch; margin-top: 3px; box-sizing: border-box; aspect-ratio: 1;
  display: inline-flex; flex-direction: column; justify-content: center; align-items: center; gap: 3px;
  padding: 4px; font-size: 11px; border-radius: 10px;
}
.v3-rev-big .v3-up--like svg { width: 20px; height: 20px; }
/* The comment count sits under the like, same width, same shape (2026-09-11).
   The foot row only exists on your own card now, for Share. */
.v3-rev-big .v3-up--cmtcol {
  align-self: stretch; margin-top: 4px; box-sizing: border-box;
  justify-content: center; padding: 7px 4px; gap: 4px;
  font-size: 11px; border-radius: 9px;
}
.v3-rev-big .v3-up--cmtcol svg { width: 14px; height: 14px; }
.v3-rev-big-n {
  font-family: var(--font-main); font-size: 21px; font-weight: 800;
  line-height: 1; letter-spacing: -0.02em;
  color: rgba(232,226,214,0.92);
}
.v3-rev-big .hstars { display: inline-flex; gap: 2px; }
.v3-rev-foot { display: flex; align-items: center; margin-top: 8px; }
.v3-rev-foot .v3-rev-acts { margin-left: 0; }
/* A PHOTO now (2026-09-11), not initials on a gradient: `feedFace(name)` —
   the same face the person's comments wear — so the list reads the way it
   will once there are real accounts. */
.v3-rev-av {
  width: 36px; height: 36px; border-radius: 50%; flex-shrink: 0;
  background: #333 center/cover no-repeat;
  box-shadow: 0 0 0 1px rgba(255,255,255,0.08);
}
.v3-rev-name {
  font-family: var(--font-main); font-size: 12.5px; font-weight: 700;
  color: rgba(232,226,214,0.9);
  /* ⚠️ The name is the only elastic thing in this row. The pills grew a step,
     and on the PINNED card they share the line with the "from your feed" chip
     and the timestamp — without this a long handle pushes the timestamp off the
     card instead of ellipsing itself. */
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Upvote + comments ride in the card's TOP-RIGHT, with the timestamp hard right.
   They used to sit in the meta row beside the rating vinyls, crowding them. */
.v3-rev-acts { display: flex; align-items: center; gap: 6px; margin-left: auto; flex-shrink: 0; }
.v3-rev-time {
  font-size: 10px; color: rgba(232,226,214,0.22);
  flex-shrink: 0;   /* .v3-rev-acts carries the auto margin now — time stays last */
}
/* The meta row is the rating alone: vinyls + the score, the app's usual pairing */
/* Reads DOWN as one sentence: the name is the subject, this row is the verb and
   its object, the text below is what they said. The verb is what turns a bare
   row of vinyls into something you read rather than decode. */
.v3-rev-meta { display: flex; align-items: center; gap: 5px; margin-bottom: 3px; }
.v3-rev-verb {
  font-size: 9.5px; color: rgba(232,226,214,0.38);
  margin-right: 1px;
}
.s-home-v3--light .v3-rev-verb { color: rgba(26,18,8,0.42); }
.v3-rev-score {
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;
  color: rgba(232,226,214,0.5);
}
.v3-rev-likes { font-size: 9.5px; color: rgba(232,226,214,0.25); }
/* Not full width on the card — a measure, not a slab — so a paragraph reads
   as one. The page lets the text run the full width. Six lines, then the
   ellipsis: long reviews should LOOK long here, without being endless. */
/* Five lines, then a long review FADES OUT rather than stopping at an ellipsis
   — the fade says "there is more" the way a cut-off line does, without the
   glyph. `.is-long` is set by populateReviewList only on cards that really
   overflow, so a short review is never dimmed. */
.v3-rev-text {
  white-space: pre-line;                              /* reviews can be LISTS — the pool carries real newlines */
  margin-top: 5px;                                    /* the score column already sets the measure */
  font-size: 11.5px; line-height: 1.4; color: rgba(232,226,214,0.68);
  max-height: calc(1.4em * 5); overflow: hidden;              /* five lines (was six) — a long review fades sooner */
  text-wrap: pretty;                                          /* no widows (revNoWidow is the guarantee) */
}
.v3-rev-text.is-long {
  -webkit-mask-image: linear-gradient(to bottom, #000 30%, rgba(0,0,0,0.5) 65%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 30%, rgba(0,0,0,0.5) 65%, transparent 100%);
}
.v3-rev-empty {
  padding: 34px 0; text-align: center;
  font-size: 12px; color: rgba(232,226,214,0.35);
}

/* ── Review comments — the threaded replies under a review ─────
   ⚠️ The indent is REAL NESTING, not a depth class: `.v3-cmt-kids` wraps a
   comment's children and carries the offset and the rail, so a child of a child
   indents twice because it sits inside two wrappers. Nothing computes a depth,
   and adding a level needs no new rule.
   These sit on the album page's review panel, which keeps the album's dark
   procedural colour in BOTH themes — hence hard-coded warm-white ink, like
   every `.v3-rev-*` around them. ⚠️ The ARTIST page forces the cream bg
   instead, so each of these needs a `--light.--artist` counterpart below. */
.v3-cmt-wrap:empty { display: none; }

/* ═══ THE REVIEW PAGE (.s-rvp — reviewPageHtml) ══════════════════════════
   One review, pronounced. Dark ink like the album page's review panel in
   BOTH themes; the light shell floods with the album colour the way the
   light album page does, so the page reads as the album's. The reviewer's
   photo is ringed in the album accent — the one place on the page the person
   and the record share a colour. The text is the app's review voice
   (DM Sans — the serif came off 2026-09-16) at reading size, and it is NEVER clamped here: the card
   was the taste, this is the thing. */
/* A CROSS-FADE, both ways (2026-09-11 — the flying card was jarring): only the
   CONTENT fades (the .v3-body), never the shell, so the album colour behind
   it holds the whole way. sdFadeOut fades the outgoing content, the incoming
   screen's body fades in here. */
:is(.s-rvp, .s-home-v3--rvp) { color: var(--sd-ink, #e8e2d6); }
/* ── The review IN PLACE (.s-home-v3--rvp on the album page's shell) ───────
   The bento and the review panel step aside, the review panel takes the body.
   --rvp-out is the 200ms fade of whatever is leaving. ⚠️ .v3-review-panel's
   entry animation fills forwards and would pin its opacity at 1, so it is
   switched off before the fade. */
.v3-rvp-panel { display: none; }
.s-home-v3--rvp .v3-rvp-panel { display: block; }
.s-home-v3--rvp .v3-bento, .s-home-v3--rvp .v3-review-panel { display: none; }
.s-home-v3--rvp-out:not(.s-home-v3--rvp) .v3-bento,
.s-home-v3--rvp-out:not(.s-home-v3--rvp) .v3-review-panel { animation: none; opacity: 0; transition: opacity .2s ease-out; }
.s-home-v3--rvp.s-home-v3--rvp-out .v3-rvp-panel { opacity: 0; transition: opacity .2s ease-out; animation: none; }
.s-home-v3--rvp .v3-rvp-panel { animation: rvpIn .3s ease-out backwards; }   /* `backwards`, NOT `both` — see the blur note just below; `both` crept back here and blurred the in-place page (2026-09-18) */
/* ⚠️ `backwards`, not `both` (2026-09-16): the page read BLURRY on the phone.
   A fill-forwards animation on the scrolling body — an element that already
   carries a mask — keeps it on a compositing layer after the fade, and WebKit
   rasterises that layer's text at the un-scaled size inside the scaled phone
   wrap. The end state is opacity 1, the natural value, so `backwards` loses
   nothing and releases the layer when the fade ends. */
:is(.s-rvp, .s-home-v3--rvp) .v3-body { animation: rvpIn .34s ease-out backwards; }
@keyframes rvpIn { from { opacity: 0; } to { opacity: 1; } }
body.sd-return .s-home-v3--review .v3-body { animation: rvpIn .34s ease-out backwards; }
@media (prefers-reduced-motion: reduce) { :is(.s-rvp, .s-home-v3--rvp) .v3-body, body.sd-return .s-home-v3--review .v3-body { animation: none; } }
/* ⚠️ The fallback is DARK. The ink on this page is hard-coded light (like the
   album page's review panel), and the palette arrives a beat after the page
   does — a cream fallback meant white text on cream until then, and for good
   if the cover had never been extracted. The page is legible from the first
   frame now, and the album colour floods in on top. */
/* 2026-09-25: the page wears the REVIEW SHEET's look (Eric), so it is the
   screen's own background in both themes — cream in light, with the light
   theme's dark ink — not the album flood, and nothing below re-inks it. */
:is(.s-rvp, .s-home-v3--rvp).s-home-v3--light { background: var(--sd-bg, #f0ece3); color: #1a1208; }
.s-home-v3--rvp .v3-body { background: var(--sd-bg, #111116); }   /* in place on the album page: the body covers the flood */
/* …and re-ink what the light theme darkened, since the page is dark in both
   themes: the shell's inherited ink, the header's masked logo and bubbles,
   the handle, and the upvote/comment pills. Verified in the viewer. */
:is(.s-rvp, .s-home-v3--rvp) .rvp-name { color: #e8e2d6; }
.rvp-scroll { padding: 16px 12px 96px; }   /* the sheet's body adds 6 a side inside this → the sheet's 18 */                      /* the back pill sat hard under the header (2026-09-11) */
.rvp-scroll > .plp-back-pill { margin-bottom: 16px; }
.rvp-cmts-hd { margin: 14px 0 2px; font-family: var(--font-mono); font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; color: rgba(232,226,214,0.45); }
.rvp-cmts-n { margin-left: 4px; color: rgba(232,226,214,0.7); }

.v3-cmt-thread { padding: 5px 0 2px; }
.v3-cmt-kids {
  margin-left: 7px;
  padding-left: 10px;
  border-left: 1px solid rgba(232,226,214,0.13);
}
/* ⚠️ Type here runs a step LARGER than the review card above it. The thread
   started at the card's own scale (10px body, 8.5px actions) and once you are
   reading a conversation rather than scanning a list, that is too small to
   follow — the card is a summary, the thread is the thing you came for. */
/* THE REVIEW CARD'S SHAPE (Eric, 2026-09-17 — the face-beside-a-column row
   with "♥ n" under the text read as Reddit): a top row of photo · name over
   @handle · heart · time, then the text FULL WIDTH underneath — the page
   card minus the score and the share, a step smaller than the hero above it.
   A hairline between rows, like the review list. */
.v3-cmt { padding: 13px 0 14px; border-top: 1px solid rgba(232,226,214,0.07); }
.v3-cmt-add + .v3-cmt { border-top: none; }
.v3-cmt-top { display: flex; align-items: center; gap: 9px; min-width: 0; }
.v3-cmt-av {
  width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0;
  background-size: cover; background-position: center;
  background-color: rgba(255,255,255,0.08);
  box-shadow: 0 0 0 1px rgba(255,255,255,0.08);
}
.v3-cmt-who { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.v3-cmt-user {
  font-family: var(--font-main); font-size: 12px; font-weight: 700; color: rgba(232,226,214,0.9);
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.v3-cmt-handle {
  font-family: var(--font-main); font-size: 10.5px; font-weight: 500; color: rgba(232,226,214,0.4);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.v3-cmt-ago {
  flex-shrink: 0; min-width: 20px; text-align: right;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.04em;
  color: rgba(232,226,214,0.4); white-space: nowrap;
}
.v3-cmt-text {
  font-family: var(--font-main); font-size: 12.5px; line-height: 1.45;   /* pinned: DM Sans, never a serif (Eric, 2026-09-16) */
  color: rgba(232,226,214,0.78);
  margin: 9px 0 0;
  overflow-wrap: anywhere;   /* a pasted handle shouldn't widen the card */
}
.v3-cmt--mine .v3-cmt-user { color: var(--star); }
.v3-cmt-acts { display: flex; align-items: center; gap: 13px; margin-top: 6px; }
.v3-cmt-like, .v3-cmt-reply {
  border: none; background: none; padding: 0; cursor: pointer;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .02em;
  color: rgba(232,226,214,0.32);
  transition: color .14s;
}
/* The card's bare heart-and-number, in the byline's ink; pink when on. */
/* The like goes LAST on the row, to the right of the time (Eric, 2026-09-24:
   "put the like button to the right, next to the time ago"): `order` flips
   the two, the markup (cmtNodeHtml) is unchanged. */
.v3-cmt-ago { order: 1; }
.v3-cmt-like { order: 2; }
.v3-cmt-like {
  flex-shrink: 0; display: inline-flex; align-items: center; gap: 5px; padding: 3px 0; margin-left: 4px;
  font-family: var(--font-main); font-size: 12px; letter-spacing: 0; color: rgba(232,226,214,0.4);
}
.v3-cmt-like svg { width: 17px; height: 17px; }
.v3-cmt-like span { min-width: 14px; text-align: left; }
.v3-cmt-like:hover, .v3-cmt-reply:hover { color: rgba(232,226,214,0.9); }
.v3-cmt-like.is-on { color: #ff5c8a; }
.v3-cmt-like.is-on svg { fill: currentColor; }
/* Your own comment's trash — between the time and the heart (2026-10-12). Same
   order as .v3-cmt-ago, so DOM order puts it right after the time. */
.v3-cmt-del {
  order: 1; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
  border: none; background: none; padding: 3px; margin: 0 -2px 0 1px; cursor: pointer;
  color: rgba(232,226,214,0.4); transition: color .14s;
}
.v3-cmt-del svg { width: 16px; height: 16px; }
.v3-cmt-del:hover { color: #ff6b6b; }
.s-home-v3--light .v3-cmt-del { color: rgba(26,18,8,0.42); }
.s-home-v3--light .v3-cmt-del:hover { color: #d43c3c; }

/* The first CMT_DEFAULT threads render; cmtAutoMore pages the rest in on
   scroll. ("View n more" and its .v3-cmt-more styles came off on 2026-10-09 —
   see .v3-cmt-loading, which is what marks a thread with more to come.) */
.v3-cmt-none { font-size: 11px; color: rgba(232,226,214,0.32); padding: 3px 0; }

/* Column, not a row: the reply chip stacks above the input line. */
.v3-cmt-add { display: flex; flex-direction: column; gap: 5px; margin-top: 8px; }
/* The Post button sits INSIDE the box, pinned to its top-right corner, and
   stays there as the box grows down (Eric, 2026-09-24); the textarea keeps
   a right padding so the text never runs under it. */
.v3-cmt-add-row { position: relative; display: block; }
.v3-cmt-add-row .v3-cmt-input { width: 100%; padding-right: 58px; }
.v3-cmt-add-row .v3-cmt-post { position: absolute; top: 4px; right: 4px; padding: 4px 10px; }
/* Says where the next post will land — and is the way back out of reply mode. */
.v3-cmt-to {
  align-self: flex-start;
  display: inline-flex; align-items: center; gap: 7px;
  border: none; border-radius: 999px; cursor: pointer;
  padding: 3px 9px;
  background: rgba(232,168,60,0.14); color: var(--star);
  font-family: var(--font-mono); font-size: 9.5px;
  transition: background .14s;
}
.v3-cmt-to span { opacity: .65; font-size: 10px; }
.v3-cmt-to:hover { background: rgba(232,168,60,0.24); }
.v3-cmt-input {
  flex: 1; min-width: 0;
  padding: 6px 11px; border-radius: 12px;   /* the record box's corner, not a pill (Eric, 2026-09-24) */
  border: 1px solid rgba(232,226,214,0.13);
  background: rgba(0,0,0,0.2);
  font-family: var(--font-main); font-size: 11px;
  color: rgba(232,226,214,0.88);
  /* A TEXTAREA that grows DOWN (Eric, 2026-09-24: "when it's active it
     becomes larger, not the font, it grows downwards"): one line at rest,
     three when it has focus, and taller still as you type (cmtGrow sets the
     height to the content, up to eight lines). Same size type as before. */
  display: block; resize: none; line-height: 1.45;
  height: auto; min-height: calc(1.45em + 14px); max-height: calc(1.45em * 12 + 14px);
  overflow-y: auto; box-sizing: border-box;
  transition: min-height .18s ease, border-radius .18s ease;
}
.v3-cmt-input:focus { min-height: calc(1.45em * 5 + 14px); }   /* 3 → 5 lines open (Eric, 2026-09-24: "grow more") */
.v3-cmt-input::placeholder { color: rgba(232,226,214,0.3); }
.v3-cmt-input:focus { outline: none; border-color: var(--star); }
.v3-cmt-post {
  flex-shrink: 0;
  border: none; border-radius: 999px; cursor: pointer;
  padding: 6px 12px;
  background: rgba(232,168,60,0.16); color: var(--star);
  font-family: var(--font-mono); font-size: 10px; font-weight: 700;
  transition: background .14s, color .14s;
}
.v3-cmt-post:hover { background: var(--star); color: #16130d; }

/* Light theme */
.s-home-v3--light .v3-rev-mine-hd { color: rgba(232,226,214,0.42); }
.s-home-v3--light .v3-rev-album-name { color: rgba(232,226,214,0.7); }
.s-home-v3--light .v3-rev-star { color: rgba(232,226,214,0.16); }
.s-home-v3--light .v3-rev-star.half {
  background: linear-gradient(90deg, var(--star, #e8a83c) 50%, rgba(232,226,214,0.16) 50%);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent;
}
.s-home-v3--light .v3-rev-write { color: #e8e2d6; }
.s-home-v3--light .v3-rev-write::placeholder { color: transparent; }
.s-home-v3--light .v3-rev-filter { color: rgba(232,226,214,0.4); }
.s-home-v3--light .v3-rev-filter.active { color: var(--v3-accent, #b06a2a); }
.s-home-v3--light .v3-rev-count { color: rgba(232,226,214,0.35); }
.s-home-v3--light .v3-rev-card { border-bottom-color: rgba(232,226,214,0.09); }
.s-home-v3--light .v3-rev-name { color: rgba(232,226,214,0.8); }
.s-home-v3--light .v3-rev-time { color: rgba(232,226,214,0.22); }
.s-home-v3--light .v3-rev-likes { color: rgba(232,226,214,0.28); }
.s-home-v3--light .v3-rev-text { color: rgba(232,226,214,0.5); }
.s-home-v3--light .v3-rev-empty { color: rgba(232,226,214,0.4); }

/* ── The card on the HOME FEED (2026-09-14) ──────────────────────────────
   Review and rating rows in the home feed are this same card (renderFriendFeed
   → revCardHtml, `.v3-rev-card--feed`). Every `.v3-rev-*` colour above is
   hard-coded for the album page's surface — the album's procedural colour,
   dark in both themes, which is why the `--light` rules just above are still
   light ink. The feed sits on the SCREEN bg, cream in the light theme, so
   this card re-inks itself off the shared --sd-* tokens, the way `.v3-up--feed`
   and `--vinyl-empty` already do. Same trap, same fix: the value follows the
   surface, not the theme. Specificity (0,3,0) beats the `--light` overrides
   in both directions.
   ⚠️ 3px sides (Eric, 2026-09-15): the record row now carries cover ·
   titles · score · like across it and needs the width, so the card sits a
   hair inside the bento's edges — `.s-home-v3 .v3-scroll-area` sets those.
   Flush with the bento was tried and read as too much.
   Top and bottom went up 5px each (Eric, 2026-09-15): with the time
   headers gone the cards sit straight on each other and wanted the air. */
.s-home-v3 .v3-rev-card.v3-rev-card--feed {
  /* 20 top (was 17; Eric, 2026-09-18): the byline moved 3px DOWN toward the
     record row — 3 more here, 3 less in --rev-obj-gap — so it sits closer to
     its content and everything below it stays exactly where it was.
     19 bottom: the divider is a pseudo now, not a border, so the px it took
     comes back as padding. */
  padding: 20px 9px 19px;   /* 3 → 15 a side (Eric, 2026-09-24): the content lines up with the deck review's 334px column (≈25px in), not the full width; the divider still runs edge to edge */
  position: relative;
}
/* The divider runs the FULL width of the screen (Eric, 2026-09-17), not the
   card's content width: a ::after hung 10px past each side of the card, which
   is exactly `.v3-scroll-area`'s side padding — so it ends on the screen
   edge and, staying inside the scroller's padding box, adds no sideways
   scroll. Change that padding and change this. */
.s-home-v3 .v3-rev-card.v3-rev-card--feed::after {
  content: ''; position: absolute; left: -10px; right: -10px; bottom: 0; height: 1px;
  background: var(--sd-line); pointer-events: none;
}
.s-home-v3 .v3-rev-card.v3-rev-card--feed:last-child::after { display: none; }
.s-home-v3 .v3-rev-card--feed .v3-rev-name  { color: var(--sd-ink); }

.s-home-v3 .v3-rev-card--feed .v3-rev-sub   { color: var(--sd-ink3); }
.s-home-v3 .v3-rev-card--feed .v3-rev-big-n { color: var(--sd-ink); }
.s-home-v3 .v3-rev-card--feed .v3-rev-text  { color: var(--sd-ink2); }
/* csharpuser (2026-09-24): just the one name on the feed's cards — no @handle (Eric). */
.s-home-v3 .v3-rev-card--feed .v3-rev-sub { display: none; }
.s-home-v3--light .v3-rev-card--feed .v3-rev-text { color: rgba(26,18,8,0.9); }   /* 90% ink on cream (Eric, 2026-09-18) — --sd-ink2 read too faint for a paragraph */
.s-home-v3 .v3-rev-card--feed .v3-rev-av,
.s-home-v3 .v3-rev-card--feed .v3-rev-record-art { box-shadow: 0 0 0 1px var(--sd-line); background-color: var(--sd-card); }

/* ── YOUR JUST-POSTED REVIEW: THE GOLD RING (0.14, 2026-10-09) ─────────────
   Eric: "when it takes you to your posted review, can we have it so that your
   profile image has a yellow ring around it". `.v3-rev-card--posted` is set by
   myFeedCardHtml on the one card _myFeedPost builds, so the ring marks the
   card the post-review exit brought you to — not every card of yours, and not
   the album page's. It STAYS while that post leads the feed; only its draw-in
   is motion.

   ⚠⚠ THIS MUST SIT AFTER THE `--feed` AVATAR RULE DIRECTLY ABOVE, AND CARRY
   `.v3-rev-card--feed` ITSELF. The first cut was
   `.s-home-v3 .v3-rev-card--posted .v3-rev-av` — three classes, exactly the
   same as `.s-home-v3 .v3-rev-card--feed .v3-rev-av`, and it sat EARLIER in
   the file, so the hairline above won the tie on source order and there was no
   ring. It only ever showed through the ANIMATION (animations outrank normal
   declarations), which meant it vanished entirely under
   `prefers-reduced-motion`. Four classes now, and placed after: the ring wins
   on its own and the animation is decoration, not the mechanism.

   ⚠ An explicit GOLD, not `var(--star)`. Inside a home shell `--star` is
   `var(--v3-star, var(--persona-accent, #e8a83c))` — the FEATURED ALBUM's
   extracted colour — so on the bento it is whatever that cover happened to
   yield, which is often not yellow at all. #e8a83c / #c08818 are the app's own
   two golds (see :root and the --star light override).

   ⚠ The pulse is a separate ::after ring animating TRANSFORM and OPACITY.
   The first cut animated `box-shadow` through `var()` in the keyframes, which
   leans on custom-property substitution inside @keyframes and on box-shadow
   interpolation; the steady ring must not depend on either. */
.s-home-v3 .v3-rev-card--feed.v3-rev-card--posted .v3-rev-av {
  position: relative;
  box-shadow: 0 0 0 2px #e8a83c;
}
.s-home-v3--light .v3-rev-card--feed.v3-rev-card--posted .v3-rev-av {
  box-shadow: 0 0 0 2px #c08818;
}
/* The draw-in: a second ring that swells out of the photo once and goes. */
.s-home-v3 .v3-rev-card--feed.v3-rev-card--posted .v3-rev-av::after {
  content: ''; position: absolute; inset: -5px;
  border-radius: 50%; border: 2px solid #e8a83c;
  pointer-events: none;
  animation: v3PostedRing .62s ease-out both;
}
.s-home-v3--light .v3-rev-card--feed.v3-rev-card--posted .v3-rev-av::after { border-color: #c08818; }
@keyframes v3PostedRing {
  from { opacity: .95; transform: scale(.78); }
  to   { opacity: 0;   transform: scale(1.22); }
}
@media (prefers-reduced-motion: reduce) {
  /* The ring stays — it is the message. Only the swell goes. */
  .s-home-v3 .v3-rev-card--feed.v3-rev-card--posted .v3-rev-av::after { display: none; }
}
.s-home-v3 .v3-rev-card--feed .v3-rev-record-album  { color: var(--sd-ink); }
.s-home-v3 .v3-rev-card--feed .v3-rev-record-artist { color: var(--sd-ink2); }
.s-home-v3 .v3-rev-card--feed .v3-rev-record-year   { color: var(--sd-ink3); }
/* ── ACTIVITY CARDS (Eric, 2026-09-15): the feed's other verbs, in the card's
   clothes — see `act` in renderFriendFeed. The photo carries the inbox's kind
   badge, a step down from the inbox's 20px so it reads as a corner mark on
   the 36px photo, not a second avatar. The verb sits where the review card's
   number and discs sit (rows 2–3, column 2), glyph over a mono label, in the
   byline's ink so it reads as a caption, not a button. */
.s-home-v3 .v3-rev-card--act .v3-rev-av { position: relative; }
.s-home-v3 .v3-rev-card--act .ntf-badge { width: 17px; height: 17px; right: -2px; bottom: -2px; }
.s-home-v3 .v3-rev-card--act .ntf-badge svg { width: 9.5px; height: 9.5px; }
.s-home-v3 .v3-rev-card--act .v3-rev-record-art--round { border-radius: 50%; }
.s-home-v3 .v3-rev-card--act .v3-rev-sub i { font-style: normal; color: var(--sd-ink2); }
/* ⚠️ On a playlist card the byline's long token is the LIST's name, in the
   sub — so here the sub is the elastic one and the person's name holds, the
   reverse of the review card (where the sub never wraps and the name gives). */
.s-home-v3 .v3-rev-card--act .v3-rev-name-row { flex-shrink: 0; }
.s-home-v3 .v3-rev-card--act .v3-rev-sub { flex-shrink: 1; min-width: 0; }
/* ⚠️ Since the flip (2026-09-16) the record is on the RIGHT, so the verb sits
   where the review card's SCORE sits — the `score` area, left, under the
   photo — not in a hard-coded row/column that would now land on the cover. */
.s-home-v3 .v3-rev-card--act .v3-rev-act {
  grid-area: score; justify-self: start; align-self: center;
  margin-top: var(--rev-obj-gap);
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  min-width: 53px; padding: 0 2px;
  /* DM Sans, not mono (Eric, 2026-09-18) — same family as the score it
     stands in for. Still small caps-ish: uppercase, tracked. */
  font-family: var(--font-main); font-size: 8.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--sd-ink3);
  /* 3px down (Eric, 2026-09-18): centred on the two tracks it sat a hair
     above the album/artist pair's centre — the same correction the review
     card's score got (8px there, since the number is taller). `top`, not a
     transform — see .hstars for why. */
  position: relative; top: 3px;
}
.s-home-v3 .v3-rev-card--act .v3-rev-act svg { width: 20px; height: 20px; color: var(--sd-ink2); }
/* The pills: `.v3-up` is dark-surface ink too (see the ⚠️ above `.v3-up--feed`). */
.s-home-v3 .v3-rev-card--feed .v3-up { background: var(--sd-pill); color: var(--sd-ink3); }
.s-home-v3 .v3-rev-card--feed .v3-up:hover { background: var(--sd-pill-hi); color: var(--sd-ink); }
.s-home-v3 .v3-rev-card--feed .v3-up.is-on { background: rgba(232,168,60,0.18); color: var(--star); }
/* `is-open` on a card means nothing (the thread lives on the album page /
   review page) — keep the resting look, in the feed's own colours. */
.s-home-v3 .v3-rev-card--feed .v3-cmt-btn.is-open { background: var(--sd-pill); color: var(--sd-ink3); }
/* Empty records follow the surface: cream here, so dark ink (the album-page
   card forces the light value because ITS surface is dark). */
.s-home-v3--light .v3-rev-card.v3-rev-card--feed { --vinyl-empty: rgba(26,18,8,0.62); }

/* ── The feed card's SCORE SITS ON THE RECORD'S ROW (Eric, 2026-09-14) ──
   With the score column starting beside the photo the card read "drumkid
   4.5" — the number paired with the person, and what was being rated was a
   row further down. On the album page that pairing is right (the record is
   the page); on the feed the record is the news. So here the person is a
   byline — photo · name · @handle · when, the full width of the card — and
   the score sits on the record row.
   ⚠️ A CLEAN REVIEW UNDERNEATH (Eric, 2026-09-15): the score used to stack
   DOWN the right edge — number, discs, like, comment — three things tall
   beside a one-line review, which left the card mostly air. Now the LIKE and
   the COMMENT COUNT ride the top row (revCardInner `actsTop`), between the
   byline and the time, as a bare heart-and-number and bubble-and-number. The
   record row is cover · album/artist/year with NUMBER OVER DISCS on the
   right, centred on the cover's 53px. The TEXT then runs the full width of
   the card under the row with nothing beside it. `.v3-rev-big` is
   `display: contents` so the number and discs are the card's own grid items
   (revCardInner is shared with the album page, whose card is untouched).

       top   top
       rec   n
       rec   stars
       text  text
       foot  foot
*/
/* ⚠️ FLIPPED (Eric, 2026-09-16), after the album page's card landed with its
   score on the left and he liked it: the record row is now SCORE on the left
   — the 26px number with the discs beside it, one line, under the photo, the
   same row the album page's card has — and the RECORD on the right, mirrored:
   the cover flush with the card's right edge (under the time), album /
   artist / year to the LEFT of it, right-aligned. The two cards now share
   one skeleton; the feed's just has the record filling the row's right half.

       top    top
       score  rec
       text   text
       foot   foot
*/
/* ⚠️ BASELINES ACROSS THE ROW (Eric, 2026-09-17): the album name sits on the
   big number's baseline and the artist on the discs' — so the record row is
   TWO grid rows (`score`/`rec` repeated, which makes each area span both),
   and the score column and the record are SUBGRIDS in the row axis: number
   and album name in the first track, discs and artist in the second, every
   one of them `align-self: baseline`, which a subgrid shares with the parent
   grid's row. Measured exact in Chrome (album = number to the hundredth,
   artist = disc bottom). The old one-row version — two flex columns each
   centred on the cover — was 3–5px off and could only ever be nudged.
   ⚠️ The cover is 53px and the two text lines come to ~44px, so it would
   have grown both rows and thrown the pairs apart. It is taken OUT of track
   sizing with a symmetric negative margin (its margin box is under 0px tall,
   so it contributes nothing) and `align-self: center` still centres its
   border box on the two rows. The ~4.5px it overhangs each side is given
   back in `--rev-obj-gap` (11 → 15.5) and the text's margin (13 → 17.5), so
   byline → cover → text read at the same distances as before. */
.s-home-v3 .v3-rev-card.v3-rev-card--feed {
  --rev-obj-gap: 12.5px;                      /* air between the byline and the record row (15.5 − 3, see the padding note above) */
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas:
    "top top"
    "score rec"
    "score rec"
    "text text"
    "foot foot";
  grid-template-rows: auto auto auto 1fr auto;
  column-gap: 12px;
}
/* The score: the number over the discs, left, each in its own shared row.
   ⚠️ `transform: none` on the discs — `.hstars` carries a `translateY(10%)`
   optical nudge for sitting BESIDE a number on one line; under one, aligned
   to the artist's baseline, it just drops them a pixel below it. */
.s-home-v3 .v3-rev-card--feed .v3-rev-big {
  grid-area: score; display: grid; grid-template-rows: subgrid; grid-template-columns: auto;
  align-items: baseline; justify-items: start; row-gap: 5px;
  margin-top: var(--rev-obj-gap);
  /* 8px DOWN (Eric, 2026-09-18): the number + discs block is taller than the
     two title lines, so on shared baselines its centre sat above theirs. This
     brings the two blocks' centres level. Paint-only — the baselines the grid
     computes are untouched underneath, so the titles and the cover stay put.
     → 9.75 once the number went 26 → 22.5: the number's baseline stays put,
     so its top came down 3.5 and the block's centre with it by half that.
     `top`, not a transform — a transform blurred the discs (see .hstars). */
  position: relative; top: 9.75px;
}
.s-home-v3 .v3-rev-card--feed .v3-rev-big-n { font-size: 22.5px; grid-row: 1; }   /* the bento's size (was 26; Eric, 2026-09-18) */
.s-home-v3 .v3-rev-card--feed .v3-rev-big .hstars { grid-row: 2; align-self: baseline; top: -0.5px; }   /* was 1px down (Eric, 2026-09-17), then 1.5 back up toward the number (2026-09-18). `top` (relative from .hstars), not a transform — the artist's baseline stays where it is */
/* The record, mirrored: the cover in the second column (hard right, spanning
   both rows, centred — see the note above on its margin), the titles in the
   first, right-aligned so they hang off the cover's edge. `.v3-rev-record-who`
   is `display: contents` so the album line and the artist are the subgrid's
   own items; both stretch their track (`min-width: 0`) so a long album name
   fades and a long artist ellipses instead of pushing the cover off the card. */
.s-home-v3 .v3-rev-card--feed .v3-rev-record {
  grid-area: rec; display: grid; grid-template-rows: subgrid;
  grid-template-columns: minmax(0, 1fr) 53px; column-gap: 10px; row-gap: 5px;
  align-items: baseline; justify-self: stretch; max-width: none;
  margin-top: var(--rev-obj-gap); text-align: right;
}
.s-home-v3 .v3-rev-card--feed .v3-rev-record-art { grid-column: 2; grid-row: 1 / 3; align-self: center; margin: -60px 0; position: relative; top: 2px; }   /* 2px down (Eric, 2026-09-17) so the titles read centred on it; `top` (not a transform — see .hstars), nothing else moves */
.s-home-v3 .v3-rev-card--feed .v3-rev-record-who { display: contents; }
/* ⚠️ OPTICAL LIFTS (Eric, 2026-09-17), on top of the metric alignment above.
   The grid puts both baselines exactly on their partners and it still read a
   touch low: a row of circles wants to overshoot the baseline the way a round
   letter does, so with the discs' bottom sitting dead on it the artist looks
   sunk. So the artist comes up to meet the discs — Eric's call, not a
   metric. The album line was lifted 1.5px too for an hour and came back
   down (Eric, same day): it sits ON the number's baseline, `top: 0`. `top`
   on a relative box moves the paint only; the tracks and the baseline groups
   underneath are untouched, so these are the ONLY two numbers to turn. */
.s-home-v3 .v3-rev-card--feed .v3-rev-record-line { grid-column: 1; grid-row: 1; justify-self: stretch; justify-content: flex-end; min-width: 0; position: relative; top: 0; }
.s-home-v3 .v3-rev-card--feed .v3-rev-record-artist { grid-column: 1; grid-row: 2; justify-self: stretch; min-width: 0; max-width: 100%; position: relative; top: -1px; }
/* ── The ACTIVITY cards wear the same geometry (Eric, 2026-09-18) ──────────
   They share the record row with the review card, but have no 26px number in
   the first track — so their two title lines sat ~10px closer together, the
   cover overhung them by ~10px a side instead of ~4.5, and the byline and the
   divider crowded it. Fixes, in order:
   - the first track is held at the height it has on a review card, and the
     album line sits at the FOOT of it (`align-self: end`), which is where the
     review card's album baseline lands beside the number. So the album /
     artist pair and the cover sit exactly as they do on a review.
     ⚠️ That height is NOT the number's 22.5px: a subgrid's top margin
     (`--rev-obj-gap`) and half its 5px row-gap (the parent has none, so the
     difference lands as margin on the items) both live INSIDE the track —
     measured 41px on a review card when the number was 26 = 26 + 12.5 + 2.5.
     Written as that sum so it follows the number's size and the gap.
   - a record with NO second line (a followed artist: "Artist" + name) spans
     both tracks and centres on its round photo, instead of hanging off the
     top of the pair.
   - more room under the record: these cards end at the cover, where a review
     goes on into its text, so the divider sat tight against it. */
.s-home-v3 .v3-rev-card.v3-rev-card--feed.v3-rev-card--act {
  grid-template-rows: auto minmax(calc(22.5px + var(--rev-obj-gap) + 2.5px), auto) auto 1fr auto;
  padding-bottom: 26px;
}
.s-home-v3 .v3-rev-card--act .v3-rev-record-line { align-self: end; }
.s-home-v3 .v3-rev-card--act .v3-rev-record-who > .v3-rev-record-line:only-child { grid-row: 1 / 3; align-self: center; }
/* The top row's like and comment count: bare, in the byline's ink, the like
   still a live button (it goes gold when it's yours), the comment a COUNT
   only — taps fall through to the card, which opens the thread. Nothing to
   say when the count is zero. */
.s-home-v3 .v3-rev-card--feed .v3-rev-top-acts {
  margin-left: auto; margin-right: 6px; flex-shrink: 0;
  display: flex; align-items: center; gap: 9px;
}
/* ⚠️ THE HEART IS ALWAYS IN THE SAME SPOT (Eric, 2026-09-15). Comment count
   first, then the like, so a card with no thread doesn't shift the heart; the
   time is a fixed-width box, right-aligned, so "2h" and "11h" don't either;
   and the like's own count sits in a fixed box AFTER the heart, so "8" and
   "1.2k" leave the heart where it is. */
/* The two boxes are sized to their usual contents — "11h", a two-digit count —
   not their rare ones, so the heart sits close to the time; a "1.2k" like
   count is what would push it, and that is fine. */
.s-home-v3 .v3-rev-card--feed .v3-rev-top-acts + .v3-rev-time { margin-left: 0; min-width: 20px; text-align: right; }
.s-home-v3 .v3-rev-card--feed .v3-rev-top-acts .v3-up--like .v3-up-n { min-width: 16px; text-align: left; }
/* Two sizes up (Eric, 2026-09-15) so the heart is a real tap target: 19px
   glyph, 13.5px count, and the 3px of vertical padding is hit area, not look. */
.s-home-v3 .v3-rev-card--feed .v3-rev-top-acts .v3-up {
  padding: 3px 0; border-radius: 0; gap: 5px;
  background: transparent; color: var(--sd-ink3);
  font-size: 13.5px;
}
.s-home-v3 .v3-rev-card--feed .v3-rev-top-acts .v3-up svg { width: 19px; height: 19px; }
.s-home-v3 .v3-rev-card--feed .v3-rev-top-acts .v3-up--like:hover { background: transparent; color: var(--sd-ink); }
/* The heart (RVP_HEART, the hero's), pink and filled when it's yours. */
.s-home-v3 .v3-rev-card--feed .v3-rev-top-acts .v3-up--heart.is-on { background: transparent; color: #ff5c8a; }
.s-home-v3 .v3-rev-card--feed .v3-rev-top-acts .v3-up--heart.is-on svg { fill: currentColor; }
/* LIVE since 2026-09-18 (Eric): the comment pill opens the REVIEW PAGE
   (feedOpenReview), where the card tap opens the album page — so it is a
   real button now, shown even at zero, and no longer falls through to the
   card. Its cursor and hover come from `.v3-up`. */
.s-home-v3 .v3-rev-card--feed .v3-rev-top-acts .v3-up--cmtcol:hover { background: transparent; color: var(--sd-ink); }
/* The TIME sits hard right on the top row (revCardInner `timeRight`), in the
   byline's mono, on the name's line. */
.s-home-v3 .v3-rev-card--feed .v3-rev-time {
  margin-left: auto; align-self: center;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.04em;
  color: var(--sd-ink3); white-space: nowrap;
}
/* The text measure is narrower here (the comment runs beside it), and
   `text-wrap: pretty` was reshuffling the last four lines into a ragged block
   to avoid a short last line. Plain wrapping; revNoWidow's no-break space
   still keeps the last line to two words at least.
   FOUR LINES on the feed (Eric, 2026-09-15), a size up from the album
   page's 11.5px: a longer review trails off into the fade, so you know there
   is more, and a card is never taller than four lines of text. The fade is
   steeper than the album page's five-line one because there is one line to
   fade, not two.
   RAGGED RIGHT. Justified was tried (2026-09-15) and, at this measure and
   size, the word gaps read as holes — Eric's verdict, and it stands. Plain
   wrapping, no `pretty` (it reshuffled the last lines of a ragged block), no
   hyphenation; revNoWidow's no-break space still keeps the last line to two
   words at least. */
.s-home-v3 .v3-rev-card--feed .v3-rev-text {
  font-family: var(--font-main);
  font-size: 12.5px; line-height: 1.55; max-height: calc(1.55em * 4);   /* the deck review's type (Eric, 2026-09-24): same size and leading, its own width and place. 1.75 → 1.55 = 2.5px less a line (Eric, 2026-09-25) */
  text-align: left; text-wrap: wrap;
}
.s-home-v3 .v3-rev-card--feed .v3-rev-text.is-long {
  -webkit-mask-image: linear-gradient(to bottom, #000 55%, rgba(0,0,0,0.55) 76%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 55%, rgba(0,0,0,0.55) 76%, transparent 100%);
}
/* More air between the record row and the review (Eric, 2026-09-15). */
.s-home-v3 .v3-rev-card--feed .v3-rev-record + .v3-rev-text { margin-top: 21.5px; }   /* 13 + the cover's overhang (see the record row) + 4 more air under the lowered score (Eric, 2026-09-18) */

/* ── The FEED'S CARD on the ALBUM / ARTIST page (Eric, 2026-09-16) ─────────
   The home feed's review card is the look, so the album page's list wears
   it: `.v3-rev-card--page` (populateReviewList passes it with the feed's
   `timeRight` + `actsTop`). Same skeleton as `.v3-rev-card--feed` —

       top    top        photo · name · @handle · [comments · heart] · time
       score  score      4.5 ●●●●○  — the record's row, minus the record
       text   text       the review, full width
       foot   foot

   — minus the record line: the album IS the page. With nothing on the left of
   that row the score sits on the LEFT, under the photo, number and discs on
   one line, so the card still reads down as who · what they gave · what they
   said. (Right-aligned it hung under the time like a stray number.)
   Ink stays the album surface's hard-coded warm white — this card sits on the
   album's dark colour in both themes, so nothing here reads the --sd-* tokens;
   the artist page's cream panel gets its `--light.--artist` re-inks below,
   the way every other `.v3-rev-*` rule does.
   ⚠️ Top and bottom padding only: `.v3-rev-card.v3-rev-card--pinned` owns the
   sides (its edge-to-edge bleed), and this rule is later in the file at the
   same specificity. */
/* ⚠️ THE FEED'S CARD, MINUS THE RECORD (Eric, 2026-09-18): every measure
   here is read off the home feed's review card as it finally landed —
   20px over the byline, the number at the bento's 22.5px, the discs 4.5px
   under it, the text 14.75px under the discs, 19px under the card — so the
   two surfaces read as one card. The feed gets there with a subgrid, a cover
   and two paint-only shifts; with no album to centre against, this one
   just stacks, and the numbers below are what those shifts add up to
   (number top = 12.5 gap + 2.5 half row-gap + 9.75 shift = 24.75 under the
   byline; text top = 21.5 − 9.75 + 2.5 + 0.5 = 14.75 under the discs).
   Change the feed's and change these. */
.v3-rev-card.v3-rev-card--page {
  --rev-obj-gap: 24.75px;
  padding-top: 20px; padding-bottom: 19px;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "top top"
    "score score"
    "text text"
    "cmts cmts"
    "foot foot";
  grid-template-rows: auto auto 1fr auto auto;
}
/* STACKED (Eric, 2026-09-16): the number on top, the discs under it, both on
   the card's left edge. */
.v3-rev-card--page .v3-rev-big {
  grid-area: score; justify-self: start;
  flex-direction: column; align-items: flex-start; gap: 4.5px;
  margin-top: var(--rev-obj-gap);
}
.v3-rev-card--page .v3-rev-big-n { font-size: 22.5px; }
.v3-rev-card--page .v3-rev-big .hstars { align-self: flex-start; }
.v3-rev-card--page .v3-rev-text {
  margin-top: 14.75px;
  font-family: var(--font-main);                    /* the feed's text, exactly: 12.5px, four lines, left */
  font-size: 12.5px; line-height: 1.4; max-height: calc(1.4em * 4);
  text-align: left; text-wrap: wrap;
}
.v3-rev-card--page .v3-rev-text.is-long {
  -webkit-mask-image: linear-gradient(to bottom, #000 55%, rgba(0,0,0,0.55) 76%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 55%, rgba(0,0,0,0.55) 76%, transparent 100%);
}
/* The top row's like and comment: the feed's bare heart-and-number and
   bubble-and-number, in the byline's ink. Unlike the feed the comment pill is
   LIVE here — this is the page with the thread, and the pill is how you
   write one (cmtCompose). The heart is always in the same spot: comments
   first, then the like, both with fixed count boxes, the time a fixed box
   after. */
.v3-rev-card--page .v3-rev-top-acts {
  margin-left: auto; margin-right: 6px; flex-shrink: 0;
  display: flex; align-items: center; gap: 9px;
}
.v3-rev-card--page .v3-rev-top-acts + .v3-rev-time { margin-left: 0; min-width: 20px; text-align: right; }
.v3-rev-card--page .v3-rev-top-acts .v3-up--like .v3-up-n { min-width: 16px; text-align: left; }
.v3-rev-card--page .v3-rev-top-acts .v3-up {
  padding: 3px 0; border-radius: 0; gap: 5px;
  background: transparent; color: rgba(232,226,214,0.4);
  font-size: 13.5px;
}
.v3-rev-card--page .v3-rev-top-acts .v3-up svg { width: 19px; height: 19px; }
.v3-rev-card--page .v3-rev-top-acts .v3-up:hover { background: transparent; color: rgba(232,226,214,0.9); }
.v3-rev-card--page .v3-rev-top-acts .v3-up--heart.is-on { background: transparent; color: #ff5c8a; }
.v3-rev-card--page .v3-rev-top-acts .v3-up--heart.is-on svg { fill: currentColor; }
.v3-rev-card--page .v3-rev-top-acts .v3-cmt-btn.is-open { background: transparent; color: rgba(232,226,214,0.9); }
.v3-rev-card--page .v3-rev-top-acts .v3-up--cmtcol[data-n="0"] { display: none; }
.v3-rev-card--page .v3-rev-time {
  margin-left: auto; align-self: center;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.04em;
  color: rgba(232,226,214,0.4); white-space: nowrap;
}
/* The artist page's cream panel: dark ink for the top row's acts and time. */
.s-home-v3--light.s-home-v3--album .v3-rev-card--page .v3-rev-top-acts .v3-up { color: rgba(26,18,8,0.42); }
.s-home-v3--light.s-home-v3--album .v3-rev-card--page .v3-rev-top-acts .v3-up:hover,
.s-home-v3--light.s-home-v3--album .v3-rev-card--page .v3-rev-top-acts .v3-cmt-btn.is-open { color: rgba(26,18,8,0.85); }
.s-home-v3--light.s-home-v3--album .v3-rev-card--page .v3-rev-top-acts .v3-up--heart.is-on { color: #e0396e; }
.s-home-v3--light.s-home-v3--album .v3-rev-card--page .v3-rev-time { color: rgba(26,18,8,0.42); }

/* ── The first two comments, on the card (Eric, 2026-09-16) ─────────────
   `preview` on revCardInner: two root comments under the text — face · name ·
   text, clamped to two lines — and "View all n comments" when there are more.
   A taste of the thread before the tap that opens it; the rows aren't
   tappable on their own, the card is. Album-surface ink, artist re-inks below. */
.v3-rev-cmts { grid-area: cmts; margin-top: 10px; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.v3-rev-cmt { display: flex; align-items: flex-start; gap: 7px; min-width: 0; }
.v3-rev-cmt-av {
  width: 18px; height: 18px; border-radius: 50%; flex-shrink: 0; margin-top: 1px;
  background: rgba(255,255,255,0.08) center/cover no-repeat;
}
/* TWO LINES, wrapped (`.is-long` set by markLongReviews, like the review
   text). DM Sans pinned explicitly — no serif on any comment, ever. */
.v3-rev-cmt-body {
  min-width: 0; flex: 1; font-family: var(--font-main); font-size: 11.5px; line-height: 1.4; color: rgba(232,226,214,0.6);
  max-height: calc(1.4em * 2); overflow: hidden; overflow-wrap: anywhere;
}
/* Fades at the BOTTOM now, like the review text above it (Eric, 2026-09-17 —
   it was one line fading off the right edge): two lines, the second dimming
   out when there's more. */
.v3-rev-cmt-body.is-long {
  -webkit-mask-image: linear-gradient(to bottom, #000 50%, rgba(0,0,0,0.5) 78%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 50%, rgba(0,0,0,0.5) 78%, transparent 100%);
}
.v3-rev-cmt-body b { font-weight: 700; color: rgba(232,226,214,0.85); margin-right: 2px; }
.v3-rev-cmt-more { font-family: var(--font-main); font-size: 11px; color: rgba(232,226,214,0.38); }
.s-home-v3--light.s-home-v3--album .v3-rev-cmt-av { background-color: rgba(26,18,8,0.08); }
.s-home-v3--light.s-home-v3--album .v3-rev-cmt-body { color: rgba(26,18,8,0.55); }
.s-home-v3--light.s-home-v3--album .v3-rev-cmt-body b { color: rgba(26,18,8,0.85); }
.s-home-v3--light.s-home-v3--album .v3-rev-cmt-more { color: rgba(26,18,8,0.4); }

/* ── The review page's hero is the CARD, larger (2026-09-11; rebuilt
   2026-09-16 on the page card's skeleton) ──────────────────────────────────
   Built by revCardHtml with `big`, `.v3-rev-card--page` and this class; the
   heart and the share button ride the top row (heart first), the time hard
   right, the score is 40px with the discs stacked under it on the left, and
   the text runs full width at reading size, unclamped. Not tappable — it is
   already the page (cmtCardTap ignores it). After the page block so its
   measures win at equal specificity. */
.v3-rev-card.v3-rev-card--hero { padding: 4px 0 16px; cursor: default; }
.v3-rev-card--hero .v3-rev-av { width: 44px; height: 44px; }
.v3-rev-card--hero .v3-rev-card-top { gap: 11px; }
.v3-rev-card--hero .v3-rev-name { font-size: 15px; }
.v3-rev-card--hero .v3-rev-sub { font-size: 11.5px; }
.v3-rev-card--hero .v3-rev-big { gap: 6px; margin-top: 14px; }
.v3-rev-card--hero .v3-rev-big-n { font-size: 32px; }      /* was 40 (Eric, 2026-09-17); discs 22 → 17 in revCardInner */
.v3-rev-card--hero .v3-rev-big .hstars { gap: 3px; }
/* The heart left the top row for the SCORE row's right end (Eric, 2026-09-17):
   same grid area as .v3-rev-big, hung right, and exactly as tall as the
   number (32px, line-height 1) so the two centre on one line. Share and the
   time keep the top row. */
.v3-rev-card--hero .v3-rev-score-like {
  grid-area: score; justify-self: end; align-self: start;
  height: 32px; margin: 14px 0 0;
}
.v3-rev-card--hero .v3-rev-top-acts { gap: 12px; margin-right: 8px; }
.v3-rev-card--hero .v3-rev-top-acts .v3-up { font-size: 14.5px; }
.v3-rev-card--hero .v3-rev-top-acts .v3-up svg { width: 22px; height: 22px; }
/* The share button, bare like the heart beside it (sd-share-btn's ring and
   fill come off), lighting up in the star colour on hover like everywhere. */
.v3-rev-card--hero .v3-rev-top-acts .sd-share-btn--hero {
  width: auto; height: auto; padding: 3px 0; border: none; border-radius: 0;
  background: transparent; color: rgba(232,226,214,0.4);
}
.v3-rev-card--hero .v3-rev-top-acts .sd-share-btn--hero:hover { background: transparent; color: var(--star, #e8a83c); }
.v3-rev-card--hero .v3-rev-top-acts .sd-share-btn--hero svg { width: 20px; height: 20px; }
.v3-rev-card--hero .v3-rev-text,
.v3-rev-card--hero .v3-rev-text.is-long {
  margin-top: 14px; max-height: none;
  font-size: 13.5px; line-height: 1.5; color: rgba(232,226,214,0.92);
  white-space: pre-wrap; overflow-wrap: anywhere;
  -webkit-mask-image: none; mask-image: none;
}
.v3-rev-card--hero .v3-rev-foot { margin-top: 12px; }

/* Streaming service sheet */
.v3-stream-overlay {
  position: absolute; inset: 0;
  background: rgba(0,0,0,0.6);
  display: flex; align-items: flex-end;
  z-index: 100;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.v3-stream-sheet {
  width: 100%; background: #1a1921;
  border-radius: 20px 20px 0 0;
  padding: 20px 20px 36px;
  display: flex; flex-direction: column; gap: 8px;
}
.v3-stream-handle {
  width: 36px; height: 4px;
  background: rgba(255,255,255,0.13); border-radius: 99px;
  margin: 0 auto 14px;
}
/* Listen-to-preview button (accent) at the top of the sheet */
.v3-stream-preview {
  width: 100%; padding: 12px 16px; margin-bottom: 8px;
  background: rgba(232,168,60,0.13);
  border: 1px solid rgba(232,168,60,0.4);
  border-radius: 14px;
  color: var(--star, #e8a83c); font-size: 14px; font-weight: 700;
  cursor: pointer; font-family: var(--font-main);
  display: flex; align-items: center; gap: 12px; text-align: left;
  transition: background .15s, opacity .15s;
}
.v3-stream-preview.none { opacity: 0.5; }
.v3-stream-preview-ico {
  width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0;
  background: var(--star, #e8a83c); color: #16130d;
  display: flex; align-items: center; justify-content: center; gap: 3px;
}
.v3-stream-preview-ico svg { width: 15px; height: 15px; margin-left: 2px; }
.v3-stream-preview-txt { flex: 1; }
.v3-stream-preview-dur { font-family: var(--font-mono); font-size: 11px; opacity: 0.85; }
/* Playing → swap the play triangle for a pause glyph */
.v3-stream-preview.playing .v3-stream-preview-ico svg { display: none; }
.v3-stream-preview.playing .v3-stream-preview-ico::before,
.v3-stream-preview.playing .v3-stream-preview-ico::after {
  content: ''; width: 3px; height: 12px; background: #16130d; border-radius: 1px;
}
.v3-stream-label {
  font-size: 9.5px; font-weight: 700;
  color: rgba(232,226,214,0.4);
  text-align: center; margin-bottom: 8px;
  letter-spacing: 1px; text-transform: uppercase;
}
.v3-stream-app {
  width: 100%; padding: 13px 16px;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: 14px;
  color: #e8e2d6; font-size: 15px; font-weight: 600;
  cursor: pointer; font-family: var(--font-main);
  display: flex; align-items: center; gap: 12px;
  text-align: left;
}
.v3-stream-icon {
  width: 32px; height: 32px; border-radius: 8px;
  flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}
.v3-stream-cancel {
  width: 100%; padding: 14px;
  background: none; border: none;
  color: rgba(232,226,214,0.3);
  font-size: 14px; font-weight: 500;
  cursor: pointer; font-family: var(--font-main);
  margin-top: 2px;
}
/* Save row — Listened / Listen later / Add to playlist (toggle on tap) */
.v3-stream-save {
  width: 100%; padding: 11px 16px;
  background: rgba(255,255,255,0.04);
  border: 1px solid rgba(255,255,255,0.07);
  border-radius: 14px;
  color: #e8e2d6; font-size: 14px; font-weight: 600;
  cursor: pointer; font-family: var(--font-main);
  display: flex; align-items: center; gap: 12px; text-align: left;
  transition: background .15s, border-color .15s;
}
.v3-stream-sico {
  width: 32px; height: 32px; border-radius: 8px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.06); color: rgba(232,226,214,0.75);
}
.v3-stream-sico svg { width: 17px; height: 17px; }
.v3-stream-stext { flex: 1; }
.v3-stream-check {
  width: 20px; height: 20px; border-radius: 50%; flex-shrink: 0;
  border: 1.5px solid rgba(255,255,255,0.2);
  display: flex; align-items: center; justify-content: center;
}
.v3-stream-save.on { background: rgba(232,168,60,0.12); border-color: rgba(232,168,60,0.4); }
.v3-stream-save.on .v3-stream-sico { background: var(--star, #e8a83c); color: #16130d; }
.v3-stream-save.on .v3-stream-check { border-color: var(--star, #e8a83c); background: var(--star, #e8a83c); }
.v3-stream-save.on .v3-stream-check::after { content: '✓'; color: #16130d; font-size: 12px; font-weight: 800; }
/* Light theme */
.s-home-v3--light .v3-stream-save { background: rgba(26,18,8,0.04); border-color: rgba(26,18,8,0.08); color: #1a1208; }
.s-home-v3--light .v3-stream-sico { background: rgba(26,18,8,0.06); color: rgba(26,18,8,0.7); }
.s-home-v3--light .v3-stream-check { border-color: rgba(26,18,8,0.2); }


/* ── v3 Light theme overrides (s-home-v3--light) ──────────── */
.s-home-v3--light {
  background: var(--sd-bg, #f0ece3);
  color: #1a1208;
  --text3: rgba(26,18,8,0.15);
}
.s-home-v3--light .v3-topbar-wordmark { color: rgba(26,18,8,0.9); }
/* Upvote pill — light theme. (The activity feed needs nothing here: it is the
   `.ntf-*` row, which re-themes through the one `--sd-*` block.) */
.s-home-v3--light .v3-up { background: rgba(26,18,8,0.07); color: rgba(26,18,8,0.5); }
.s-home-v3--light .v3-up:hover { background: rgba(26,18,8,0.12); color: #1a1208; }
.s-home-v3--light .v3-up.is-on { background: rgba(176,106,42,0.15); color: #b06a2a; }
.s-home-v3--light .v3-up.is-on svg { fill: rgba(176,106,42,0.2); }
.s-home-v3--light .v3-brand-name { color: rgba(26,18,8,0.88); }
.s-home-v3--light .v3-brand-sub { color: rgba(26,18,8,0.28); }
.s-home-v3--light .v3-feed-title { color: rgba(26,18,8,0.32); }
.s-home-v3--light .v3-nav-item {
  color: rgba(26,18,8,0.4);
}
.s-home-v3--light .v3-nav-item.active {
  color: #1a1208;
}
.s-home-v3--light .v3-icon-btn {
  background: rgba(26,18,8,0.18);
  color: rgba(26,18,8,0.85);
}
/* Glass console — light theme (frosted cream) */
.s-home-v3--light .v3-nav-glass { background: rgba(255,255,255,0.5); box-shadow: 0 10px 34px rgba(30,20,10,0.16); border-color: rgba(26,18,8,0.16); }
/* Blue box - light */
.s-home-v3--light .v3-blue {
  background: transparent;
}
/* Bento stats text: the bento is the same dark album color in both themes now, so its
   text stays the dark-theme off-white in light mode too (inherits the base .v3-blue-* rules).
   Only the feed BELOW the bento keeps black light-theme text. */
/* CD hole - light */
.s-home-v3--light .v3-cd-hole { background: #f0ece3; }
.s-home-v3--light .v3-search-pill {
  background: #f0ece3;
  box-shadow: -4px -4px 8px 0 #d8d2c8, 4px 4px 6px -1px rgba(116,150,179,0.27);
  color: rgba(26,18,8,0.6);
}
/* Stream sheet - light */
.s-home-v3--light .v3-stream-overlay { background: rgba(0,0,0,0.18); }
.s-home-v3--light .v3-stream-sheet { background: #f8f4ee; }
.s-home-v3--light .v3-stream-handle { background: rgba(26,18,8,0.15); }
.s-home-v3--light .v3-stream-label { color: rgba(26,18,8,0.4); }
.s-home-v3--light .v3-stream-app {
  background: rgba(26,18,8,0.06);
  border-color: rgba(26,18,8,0.1);
  color: #1a1208;
}
.s-home-v3--light .v3-stream-cancel { color: rgba(26,18,8,0.3); }

/* ══════════════════════════════════════════════════════════════
   LOG / REVIEW CTA + BOTTOM SHEET  (Letterboxd-style)
   ══════════════════════════════════════════════════════════════ */

/* The single call-to-action that replaces the old inline compose block */
.v3-rev-cta {
  display: inline-flex; align-items: center; gap: 8px;
  width: fit-content; max-width: 100%;
  white-space: nowrap;   /* "Review, rate, log" always stays on one line */
  margin-left: -2px;
  padding: 10px 16px;
  background: var(--v3-box1-color, rgba(232,226,214,0.08));
  color: #e8e2d6;
  border: 1px solid rgba(255,255,255,0.10);
  border-radius: 7px;
  font-family: var(--font-main); font-size: 13px; font-weight: 600;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.08);
  transition: filter .15s, transform .06s;
}
.v3-rev-cta svg { width: 16px; height: 16px; flex-shrink: 0; }
/* ── The log control's shared identity ────────────────────────────────────
   One dot-icon size and one type scale across the CTA and the three squares,
   so the four read as one control rather than a button with three tag-alongs.
   csharpuser (2026-09-25): the `.sd-dot-ico` icons are plain 24×24 LINE
   glyphs now (SD_LINE_ICONS, screens.js) — stroke: currentColor, no fill —
   so they still inherit hover/`.on` colour; the class name stayed so these
   size rules did not have to move. */
.sd-dot-ico { display: block; }
.v3-rev-q   .sd-dot-ico { width: 15px; height: 15px; }
/* The box icon is 7×5 cells, not 5×5, so it takes its own size — a square rule
   would squash it. Wider on screen too: it carries three dots inside a frame
   and needs the room to stay legible. */
.v3-rev-cta .sd-dot-ico--box { width: 17px; height: 17px; }   /* 21×15 → 17×17 (2026-09-25): a plain pencil, not the 7×5 box */

/* ⚠️ The three dots inside the box are the only thing on this page that moves
   at rest — that is the point, it marks the button you're meant to press. Slow
   and shallow on purpose: anything faster reads as a spinner (i.e. "busy"),
   which is the opposite of an invitation.
   ⚠️ `transform-box: fill-box` is required. Without it an SVG child transforms
   about the VIEWBOX origin, so `scale()` throws the dot across the icon instead
   of growing it in place. */
@keyframes sd-ico-breathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.4); }
}
.sd-ico-live {
  transform-box: fill-box;
  transform-origin: center;
  animation: sd-ico-breathe 2.8s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .sd-ico-live { animation: none; }
}

/* ── One type treatment across all four ───────────────────────────────────
   The CTA was DM Sans 600 and the three captions were mono 400, so the row read
   as one bold button with three tag-alongs in a different voice. All four are
   now `--font-main` 600 with the same tracking; only SIZE and case separate the
   CTA from its captions, which is what makes them one control. */
.v3-rev-cta {
  font-family: var(--font-main);
  font-size: 13px; font-weight: 600; letter-spacing: 0.01em;
}
.v3-rev-cta:hover  { filter: brightness(1.12); }
.v3-rev-cta:active { transform: scale(0.985); }

/* ── CTA + quick-log squares ──────────────────────────────────
   One cascading control: the squares carry a -1px left margin so they share a
   border with the CTA and with each other instead of drawing two, and only the
   outer corners are rounded. `align-items: stretch` is what makes them square —
   they take their height from the CTA rather than hard-coding one, so changing
   the button's padding can't leave the squares short.
   The margins here are the breathing room above and below the whole row; the
   histogram below adds its own 10px on top of the bottom one. */
/* ⚠️ `--sd-q` is ONE number doing two jobs: the squares' width and the row's
   height. `align-items: stretch` gives every child the row's height, so the
   three buttons are only square while those agree — two literals would drift. */
.v3-rev-cta-row {
  --sd-q: 50px;
  display: flex; align-items: stretch;
  max-width: 100%;
  min-height: var(--sd-q);
  margin: 16px 0 20px;
}
.v3-rev-cta-row .v3-rev-cta { border-radius: 7px 0 0 7px; }
.v3-rev-quick { display: flex; align-items: stretch; }
/* Icon stacked over a tiny label, the same arrangement as the log sheet's own
   `.sd-log-opt` — the glyphs alone were guesswork at this size. The label is
   what sets the width: "LISTENED" / "FAVORITE" are 8 mono characters, so the
   width is whatever those need at the label size (50px at 7.5px type) — the
   whole row is still only ~250px of the ~310px available, so this is sized for
   legibility rather than squeezed. All three share that width rather than
   sizing to content, so the control stays an even cascade. */
.v3-rev-q {
  flex-shrink: 0; width: var(--sd-q, 50px); padding: 0 3px;
  display: inline-flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 3px;
  margin-left: -1px;                 /* collapse the shared border */
  background: var(--v3-box1-color, rgba(232,226,214,0.08));
  color: rgba(232,226,214,0.6);
  border: 1px solid rgba(255,255,255,0.10);
  border-radius: 0;
  cursor: pointer;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.08);
  transition: color .15s, filter .15s, transform .06s;
}
.v3-rev-q:last-child { border-radius: 0 7px 7px 0; }
.v3-rev-q svg { width: 14px; height: 14px; }
/* ⚠️ `--font-main` 600, matching the CTA. These were mono 400, so the row read
   as one bold button with three tag-alongs speaking in a different voice. Only
   SIZE and case separate the CTA from its captions now. */
.v3-rev-q-lbl {
  font-family: var(--font-main); font-size: 8px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.04em;
  line-height: 1; white-space: nowrap;
  opacity: 0.9;
}
.v3-rev-q:hover  { filter: brightness(1.14); color: #e8e2d6; }
.v3-rev-q:active { transform: scale(0.94); }
/* On-state matches the log sheet's options: accent for listened/later, pink for
   favourite (`.sd-log-opt.on` / `[data-k="fav"].on`). */
.v3-rev-q.on { color: var(--star, #e8a83c); }
.v3-rev-q[data-k="fav"].on { color: #ff5c8a; }
.s-home-v3--light .v3-rev-cta,
.s-home-v3--light .v3-rev-q {
  background: rgba(232,226,214,0.05);   /* fixed neutral — don't track the album's procedural tint */
  color: #e8e2d6;
  border-color: rgba(232,226,214,0.14);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.08);
}
/* The square's resting ink is dimmer than the CTA's label, and the on-states
   still win (they come later in the cascade at equal specificity). */
.s-home-v3--light .v3-rev-q { color: rgba(232,226,214,0.6); }
.s-home-v3--light .v3-rev-q.on { color: var(--star, #e8a83c); }
.s-home-v3--light .v3-rev-q[data-k="fav"].on { color: #ff5c8a; }

/* Overlay: scoped to the phone screen it's mounted in (.app-screen is relative) */
.sd-log-overlay {
  position: absolute; inset: 0; z-index: 200;
  display: flex; align-items: flex-end; justify-content: center;
  background: rgba(0,0,0,0);
  visibility: hidden;
  transition: background .25s ease, visibility 0s .25s;
}
.sd-log-overlay.open {
  visibility: visible;
  background: rgba(0,0,0,0.55);
  transition: background .25s ease;
}
.sd-log-sheet {
  width: calc(100% - 20px);        /* 10px side margins — matches the bento */
  margin-bottom: 10px;             /* float off the bottom edge too */
  background: #1c1c22;
  color: #e8e2d6;
  border-radius: 20px;
  /* ⚠️ 12px all round (2026-09-11, was 9/14/14). 12 is the album page's gutter:
     its review panel is 10px in and its song rows pad 12px, so song text sits
     22px from the screen edge — the sheet is 10px in + 12px = the same 22px.
     Top = left so the cover sits an even distance from the corner; the nub is
     taken out of flow (absolute) so it does not push the header down. */
  padding: 12px 12px 14px;
  position: relative;
  box-shadow: 0 -10px 34px rgba(0,0,0,0.5);
  transform: translateY(130%);
  transition: transform .3s cubic-bezier(.22,1,.36,1);
  font-family: var(--font-main);
  /* Almost fullscreen (2026-09-11, was 75%): 5% of the screen stays clear at
     the top so the page behind still shows through the scrim. The 10px float
     off the bottom is taken out of the 95%, so the top gap is a true 5%. */
  height: calc(95% - 10px);
  max-height: calc(95% - 10px);
  overflow-y: auto;
  scrollbar-width: none;
}
.sd-log-sheet::-webkit-scrollbar { display: none; }
.sd-log-overlay.open .sd-log-sheet { transform: translateY(0); }
/* A song's sheet has no review box and no song list: it ends under the three
   buttons, and the space above it is the page (2026-09-11). */
/* A SONG's sheet is capped at 60% (Eric, 2026-10-09: "lets keep the songs
   review window like 60% vh max"). ⚠️ PER CENT, NOT vh. The overlay is
   `position: absolute; inset: 0` inside `.app-screen`, so a percentage here
   resolves against the PHONE SCREEN — which is what 60% of the window means
   in this app. `60vh` would measure the desktop viewer's browser window, and
   the phone frame is a small box inside it, so the sheet would be sized off
   something the user never sees. Every other height in this block is a % for
   the same reason (the base sheet is `95% - 10px`). */
.sd-log-sheet.sd-log-sheet--song { height: auto; max-height: 60%; }
/* A SONG's sheet is compact (Eric, 2026-09-18): the song's name alone up top
   — no cover, no album, no year — then the discs and Favourite. You tapped
   it from the album's own tracklist; the record is behind the sheet. */
.sd-log-sheet--song .sd-log-cover,
.sd-log-sheet--song .sd-log-artist,
.sd-log-sheet--song .sd-log-year { display: none; }
.sd-log-sheet--song .sd-log-head { margin-top: 24px; gap: 0; }
.sd-log-sheet--song .sd-log-actions { margin-bottom: 14px; }

/* The nub is a CONTROL, not decoration: tap it or drag it down to close
   (wireSheetGrab in app.js, on every bottom sheet). The pseudo-element is the
   hit area — a 4px bar is not a target on a phone. */
.sd-log-grab {
  position: relative;
  width: 38px; height: 4px; border-radius: 999px;
  background: rgba(232,226,214,0.25);
  margin: 0 auto 15px;
  cursor: grab; touch-action: none;
  transition: background .15s;
}
.sd-log-grab::before { content: ''; position: absolute; inset: -14px -40px; }
.sd-log-grab:hover, .sd-log-grab:active { background: rgba(232,226,214,0.5); }

/* ── Pull to refresh (sdPtr* in app.js) ──────────────────────────────────
   The body itself is translated while you pull — the indicator is a sibling
   parked at the body's top edge, under the header, with z-index so it paints
   above the (transformed) body. `--p` is the pull as a fraction of the
   threshold: the disc fades in, grows and turns with it; at 1 it lands — the
   accent colour and a bump — and spins while the feed re-deals. */
.v3-body { overscroll-behavior-y: contain; }         /* not the browser's pull-to-refresh, ours */
.v3-body.ptr-settle { transition: transform .42s cubic-bezier(.22,1,.36,1); }
.v3-body.is-pulling { transition: none; }
.sd-ptr {
  position: absolute; left: 0; right: 0; height: 0; z-index: 1;
  display: flex; justify-content: center; pointer-events: none;
}
/* The disc is the album's accent in both themes and never changes size
   (2026-09-11). Two copies: a faint GHOST underneath, and the FILL on top,
   revealed by a conic mask that sweeps clockwise with --p — so the record
   fills like a clock face as you pull and is whole at the threshold. The
   whole thing also rises in opacity with the pull, from nothing. On release
   past the threshold the mask drops and the record spins. */
.sd-ptr-disc {
  position: relative; display: block; width: 34px; height: 34px; margin-top: 14px;
  color: var(--star, #e8a83c);
  opacity: min(1, var(--p, 0));
  transition: opacity .25s ease;
}
.sd-ptr-ghost, .sd-ptr-fill { position: absolute; inset: 0; display: block; }
.sd-ptr-ghost { opacity: 0.22; }
.sd-ptr-fill {
  -webkit-mask: conic-gradient(#000 calc(var(--p, 0) * 360deg), transparent 0);
          mask: conic-gradient(#000 calc(var(--p, 0) * 360deg), transparent 0);
}
.sd-ptr-disc svg { display: block; width: 100%; height: 100%; }
/* Landed: the sweep is complete, the ghost is gone — one solid record. */
.sd-ptr.is-armed .sd-ptr-ghost { opacity: 0; }
.sd-ptr.is-refreshing .sd-ptr-disc { opacity: 1; }
.sd-ptr.is-refreshing .sd-ptr-fill { -webkit-mask: none; mask: none; animation: sdPtrSpin .6s linear infinite; }
@keyframes sdPtrSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .sd-ptr.is-refreshing .sd-ptr-fill { animation: none; }
}
/* Log sheet only: the nub floats over the header's top edge instead of taking
   a row of its own, so the cover can sit 12px from the top like it does from
   the left. (Badges + delete sheets keep it in flow — their heads are short.) */
.sd-log-sheet .sd-log-grab { position: absolute; top: 5px; left: 50%; margin: 0; transform: translateX(-50%); }
/* With no ✕ the swipe down IS the way out, so here the nub's hit area runs the
   sheet's whole top edge (it stops 1px short of Save / Share below it). */
#sd-log .sd-log-grab::before { inset: -5px -170px -14px; }
.sd-log-sheet.is-dragging, .plc-sheet.is-dragging, .pldel-sheet.is-dragging { transition: none; }
/* ── The header IS the album page's info box, restated ───────────────────
   A centred column (2026-09-11): the cover, then the text under it, full
   width, so nothing is ever cut off — the album page wraps its title and so
   does this. Cover = exactly TWO option buttons wide (two thirds of the row
   plus the one gap between them — the same sum two .sd-log-opt's flex:1
   resolve to), square, with the options' 12px radius, so cover / vinyls /
   options read as one centred stack. The type is the review-state .v3-blue
   rules verbatim: Roboto Flex, title 22px/400 at 92% ink condensed to
   wdth 85, the year inline after it at 14px/300 and 50%, the artist
   14.5px/700 at 88%. If those rules move on the album page, move these too.
   Save and Share are the head's first row, centred under the nub — the head
   starts 24px down to clear it. */
.sd-log-head {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  margin: 24px 0 4px; text-align: center;
}
.sd-log-cover {
  width: calc((200% - 8px) / 3); aspect-ratio: 1; flex-shrink: 0;
  border-radius: 12px;
  background: #333 center/cover no-repeat;
  box-shadow: 0 4px 16px rgba(0,0,0,0.5);
}
.sd-log-meta { width: 100%; min-width: 0; font-family: 'Roboto Flex', var(--font-main); }
.sd-log-title { line-height: 1.12; }
.sd-log-album {
  display: inline; font-size: 22px; font-weight: 400;
  color: rgba(232,226,214,0.92);
  font-variation-settings: 'wdth' 85;
}
.sd-log-year { display: inline; margin-left: 6px; font-size: 14px; font-weight: 300; color: rgba(232,226,214,0.5); }
.sd-log-year:empty { display: none; }
.sd-log-artist {
  margin-top: 5px;
  font-size: 14.5px; font-weight: 700; color: rgba(232,226,214,0.88);
}
/* Save + Share — one centred pair over the cover (Eric, 2026-09-18). They are
   the head's first row, in flow, so the cover simply starts under them. There is
   no ✕ on this sheet: the nub (tap / drag down) and a tap outside dismiss it. */
.sd-log-actions { display: flex; justify-content: center; align-items: center; gap: 8px; }
.sd-log-actions .sd-log-save, .sd-log-actions .sd-log-share { min-width: 84px; padding: 8px 16px; font-size: 12.5px; }
.sd-log-x {
  background: none; border: none; color: rgba(232,226,214,0.5);
  font-size: 15px; cursor: pointer; padding: 4px; line-height: 1;
}

/* Rate — the vinyl is the hero: large, centered, near the top.
   ⚠️ The five records are sized here, not on .sd-rec, because the per-song rows
   reuse the same glyph at 14px. 5 × 54 + 4 × 7 = 298px — about 76% of the
   393px frame, so the row stops ~12% short of each screen edge (Eric,
   2026-09-11: "12% off the sides"; then "larger, smaller gaps, same sides").
   The pointer maths in the sheet reads the track's rendered width and divides
   it into five equal cells, so the size is free to move.
   ⚠️ The track is the HIT AREA and is meant to be five ~60px squares: ::before
   pads it 3px above and below so each cell is as tall as it is wide, and
   `touch-action: none` keeps a slide on it from scrolling the sheet. */
.sd-log-rate {
  /* ONE ROW (Eric, 2026-09-25): the number on the left, as tall as a disc,
     the five discs to its right and closer together; the number follows the
     discs as you rate (paintLogRating writes it). */
  display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 14px;
  padding: 12px 0 13px;
  /* ⚠️ Read by recFillWidth (app.js) — the fill is measured in these units.
     Change the disc size or the gap HERE and nowhere else. */
  --rec: 46px; --gap: 4px;   /* 54/7 → 46/4: room for the number beside them */
}
.sd-log-num {
  flex: 0 0 auto; min-width: 2.2ch; text-align: right;
  font-family: var(--font-main); font-size: var(--rec); line-height: 1; font-weight: 800; letter-spacing: -0.03em;
  color: var(--sd-ink, #e8e2d6);
  font-variant-numeric: tabular-nums;
}
.sd-log-num.is-blank { color: var(--sd-ink3, rgba(232,226,214,0.32)); }
.sd-log-stars-track {
  position: relative; display: inline-flex; cursor: pointer;
  touch-action: none; user-select: none; -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
  outline: none;
}
.sd-log-stars-track::before { content: ''; position: absolute; inset: -3px 0; }
.sd-log-stars-track:focus-visible { box-shadow: 0 0 0 2px rgba(232,168,60,0.5); border-radius: 8px; }
.sd-log-stars-empty, .sd-log-stars-fill { display: flex; gap: var(--gap); }
.sd-log-rate .sd-rec { width: var(--rec); height: var(--rec); }
/* The fill follows the finger; a short ease makes a slide read as one motion
   rather than five jumps, and is short enough (120ms) that a tap still lands. */
.sd-log-stars-fill { transition: width .12s ease-out; }
.sd-log-stars-track.is-rating .sd-log-stars-fill { transition: width .06s linear; }
.sd-log-stars-track.is-rating .sd-rec { transform: scale(1.04); }
.sd-log-rate .sd-rec { transition: transform .12s ease-out; }
@media (prefers-reduced-motion: reduce) {
  .sd-log-stars-fill, .sd-log-rate .sd-rec { transition: none; }
  .sd-log-stars-track.is-rating .sd-rec { transform: none; }
}
.sd-log-stars-empty { color: rgba(232,226,214,0.20); }
.sd-log-stars-fill {
  position: absolute; top: 0; left: 0; width: 0;
  overflow: hidden;
  color: var(--star, #e8a83c);
}
.sd-rec { width: 32px; height: 32px; flex: 0 0 auto; display: block; }
/* No number under the discs (2026-09-11) — the fill IS the readout. */

/* Options — one line, three equal cells */
.sd-log-opts { display: flex; gap: 8px; }
/* A SONG's sheet offers the rating and Favourite only (Eric, 2026-09-18) —
   Listened / Listen later are album things. The draft keys stay, so a song
   marked listened before this still remembers it; there's just no button. */
.sd-log-sheet--song .sd-log-opt[data-k="listened"],
.sd-log-sheet--song .sd-log-opt[data-k="later"] { display: none; }
.sd-log-opt {
  flex: 1;
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  padding: 12px 6px;
  background: rgba(232,226,214,0.05);
  border: 1px solid rgba(255,255,255,0.08);
  border-radius: 12px;
  color: rgba(232,226,214,0.7); font-family: inherit; font-size: 11px; font-weight: 600;
  cursor: pointer; transition: color .15s, background .15s, border-color .15s;
}
.sd-log-opt-ico { display: inline-flex; width: 22px; height: 22px; color: rgba(232,226,214,0.55); transition: color .15s; }
.sd-log-opt-ico svg { width: 22px; height: 22px; }
/* No box behind them (Eric, 2026-09-25): bare icon + word; on, the word
   brightens and the icon takes its colour. */
.sd-log-opt, .sd-log-opt.on { background: none; border-color: transparent; }
.sd-log-opt.on { color: #fff; }
.sd-log-opt.on .sd-log-opt-ico { color: var(--star, #e8a83c); }
.sd-log-opt[data-k="fav"].on .sd-log-opt-ico { color: #ff5c8a; }

.sd-log-review { position: relative; padding: 12px 0 0; }
/* The box grows ~50% downward the moment you tap into it (88 → 132px), and
   stays grown while it holds text — a saved draft should come back readable,
   not folded into three lines. It is a min-height so a long review still
   scrolls inside the sheet instead of the sheet clipping it. */
.sd-log-write {
  width: 100%; box-sizing: border-box; resize: none;
  min-height: 88px;
  transition: min-height .28s cubic-bezier(.22,1,.36,1), border-color .15s;
  background: rgba(232,226,214,0.06);
  border: 1px solid rgba(232,226,214,0.1);
  border-radius: 12px;
  padding: 12px;
  color: #e8e2d6; font-family: inherit; font-size: 13px; line-height: 1.5;
}
.sd-log-write::placeholder { color: rgba(232,226,214,0.35); }
.sd-log-write:focus { outline: none; border-color: rgba(232,226,214,0.25); }
.sd-log-write:focus, .sd-log-write.has-text { min-height: 132px; }
@media (prefers-reduced-motion: reduce) { .sd-log-write { transition: border-color .15s; } }

/* Per-song rating + review list (album log sheet only) */
.sd-log-songs { padding-top: 14px; }
.sd-log-songs-hd {
  display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px;
  font-size: 12px; font-weight: 700; color: #e8e2d6;
}
.sd-log-songs-sub { font-size: 10px; font-weight: 500; color: rgba(232,226,214,0.4); }
/* The one button at the top: Post (lit while there is something to post),
   Revise once it's posted. Share left with it (Eric, 2026-09-25). */
.sd-log-actions .sd-log-post { min-width: 96px; }
.sd-log-songs-list { display: flex; flex-direction: column; }
/* ⚠️ These rows are the ALBUM PAGE'S tracklist rows (.v3-song-row and its
   cells), restated for the sheet (2026-09-11): same 9px vertical rhythm, 10px
   cell gap, 12px type, hairline between rows, and the mono track number at
   the head. If you retune one, retune the other — the sheet is meant to read
   as the same list you just tapped on the album page.
   The 12px side padding is pulled back out with a negative margin so the
   title still starts at the sheet's own 12px margin (flush with the cover,
   heading and review box above — and 22px from the screen edge, exactly
   where the album page puts it), and the hover/logged tint bleeds to the
   sheet's edge the way it bleeds to the album's. */
.sd-log-song {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 0 -12px; padding: 9px 12px;
  border-top: 1px solid rgba(232,226,214,0.06);
  font-size: 12px; color: #e8e2d6;
  transition: background .15s, box-shadow .15s;
}
.sd-log-song:first-child { border-top: none; }
.sd-log-song:hover { background: rgba(232,226,214,0.045); }
/* The logged rail sits INSIDE the 12px gutter (inset shadow), so it never
   shifts the text the way the old border + padding swap did. */
.sd-log-song.is-logged {
  background: rgba(232,168,60,0.08);
  box-shadow: inset 2px 0 0 var(--star, #e8a83c);
}
.sd-log-song-num {
  flex-shrink: 0; width: 14px; text-align: left;
  color: rgba(232,226,214,0.4); font-size: 9.5px; font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.sd-log-song.is-logged .sd-log-song-num { color: var(--star, #e8a83c); }
.sd-log-song-title {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sd-log-song-val {
  flex-shrink: 0; min-width: 12px; text-align: right;
  font-size: 11.5px; font-weight: 700; font-family: var(--font-mono); color: var(--star, #e8a83c);
}
/* The quick favourite, left of the discs (Eric, 2026-09-18): a bare heart,
   pink and filled when on. The 26px box is the tap target; the glyph is 14. */
.sd-log-song-fav {
  flex-shrink: 0; width: 26px; height: 26px; padding: 0; margin-right: -2px;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: none; cursor: pointer;
  color: rgba(232,226,214,0.28); transition: color .15s;
}
.sd-log-song-fav svg { width: 14px; height: 14px; }
.sd-log-song-fav:hover { color: rgba(232,226,214,0.6); }
.sd-log-song-fav.on { color: #ff5c8a; }
.sd-log-song-fav.on svg { fill: currentColor; }
.sd-log-song-rate-track { position: relative; display: inline-flex; cursor: pointer; flex-shrink: 0; --rec: 14px; --gap: 4px; }
.sd-log-song-empty, .sd-log-song-fill { display: flex; gap: var(--gap); }
.sd-log-song-empty { color: rgba(232,226,214,0.20); }
.sd-log-song-fill { position: absolute; top: 0; left: 0; width: 0; overflow: hidden; color: var(--star, #e8a83c); }
.sd-log-song .sd-rec { width: var(--rec); height: var(--rec); }
.sd-log-song-note-btn {
  flex-shrink: 0; width: 26px; height: 26px; padding: 5px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(232,226,214,0.06); border: 1px solid rgba(255,255,255,0.08);
  border-radius: 8px; color: rgba(232,226,214,0.55); cursor: pointer;
  transition: color .15s, background .15s;
}
.sd-log-song-note-btn svg { width: 15px; height: 15px; }
.sd-log-song.open-note .sd-log-song-note-btn { color: var(--star, #e8a83c); background: rgba(232,168,60,0.12); }
.sd-log-song-note {
  display: none; flex: 0 0 100%; box-sizing: border-box; margin: 6px 0 2px;
  background: rgba(232,226,214,0.06); border: 1px solid rgba(232,226,214,0.1);
  border-radius: 8px; padding: 7px 10px;
  color: #e8e2d6; font-family: inherit; font-size: 12px;
}
.sd-log-song-note::placeholder { color: rgba(232,226,214,0.35); }
.sd-log-song-note:focus { outline: none; border-color: rgba(232,226,214,0.25); }
.sd-log-song.open-note .sd-log-song-note { display: block; }

/* Save — always available, in the sheet footer */
/* No Save button — the sheet autosaves, and this line is the only feedback that
   it did. It reads "Saves as you type" at rest and swaps to a green "Draft
   saved" for ~1.5s on each write, so the reassurance is there without a
   toast. Both labels are stacked in the same grid cell so the swap can't
   reflow the footer. */
/* The footer ("Saves as you type" / "Draft saved") is gone (2026-09-11) —
   the "Updated …" stamp at the top of the sheet carries that job now. */

/* ── Light theme — the sheet goes light with the shell it is mounted in ──
   The sheet is a child of the .app-screen that opened it, so the light shell's
   class is an ancestor and one override block re-inks it. The page is #f0ece3;
   the sheet is a shade lighter than the page for the same reason the dark
   sheet (#1c1c22) is a shade lighter than the dark page (#111116). Anything
   that already reads a --sd-* token (the Share pill) needs nothing here. */
.s-home-v3--light .sd-log-overlay.open { background: rgba(26,18,8,0.35); }
.s-home-v3--light .sd-log-sheet { background: #f7f4ee; color: #1a1208; box-shadow: 0 -10px 34px rgba(26,18,8,0.22); }
.s-home-v3--light .sd-log-grab { background: rgba(26,18,8,0.22); }
.s-home-v3--light .sd-log-grab:hover, .s-home-v3--light .sd-log-grab:active { background: rgba(26,18,8,0.42); }
.s-home-v3--light .sd-log-album  { color: rgba(26,18,8,0.92); }
.s-home-v3--light .sd-log-year   { color: rgba(26,18,8,0.5); }
.s-home-v3--light .sd-log-artist { color: rgba(26,18,8,0.88); }
.s-home-v3--light .sd-log-x { color: rgba(26,18,8,0.5); }
.s-home-v3--light .sd-log-stars-empty, .s-home-v3--light .sd-log-song-empty { color: rgba(26,18,8,0.14); }
.s-home-v3--light .sd-log-opt {
  background: rgba(26,18,8,0.05); border-color: rgba(26,18,8,0.1); color: rgba(26,18,8,0.7);
}
.s-home-v3--light .sd-log-opt-ico { color: rgba(26,18,8,0.5); }
.s-home-v3--light .sd-log-opt.on { color: #1a1208; background: rgba(26,18,8,0.09); border-color: rgba(26,18,8,0.18); }
.s-home-v3--light .sd-log-write, .s-home-v3--light .sd-log-song-note {
  background: rgba(26,18,8,0.05); border-color: rgba(26,18,8,0.1); color: #1a1208;
}
.s-home-v3--light .sd-log-write::placeholder, .s-home-v3--light .sd-log-song-note::placeholder { color: rgba(26,18,8,0.4); }
.s-home-v3--light .sd-log-write:focus, .s-home-v3--light .sd-log-song-note:focus { border-color: rgba(26,18,8,0.3); }
.s-home-v3--light .sd-log-songs-hd { color: #1a1208; }
.s-home-v3--light .sd-log-songs-sub { color: rgba(26,18,8,0.45); }
.s-home-v3--light .sd-log-song { border-top-color: rgba(26,18,8,0.08); color: #1a1208; }
.s-home-v3--light .sd-log-song:hover { background: rgba(26,18,8,0.045); }
.s-home-v3--light .sd-log-song.is-logged { background: rgba(232,168,60,0.16); }
.s-home-v3--light .sd-log-song-num { color: rgba(26,18,8,0.4); }
.s-home-v3--light .sd-log-song-note-btn { background: rgba(26,18,8,0.05); border-color: rgba(26,18,8,0.1); color: rgba(26,18,8,0.55); }

/* ── Quick share ──────────────────────────────────────────────
   The log sheet's footer button, the share control on YOUR review card, and the
   preview sheet. The sheet reuses `.sd-log-overlay` / `.sd-log-sheet` so it
   inherits the log sheet's geometry and open/close transition. */
/* Sits in the sheet's HEADER, beside the close button — sharing is a top-level
   action on the record, not a footnote after the form. `flex-shrink: 0` so the
   album title's ellipsis gives way before this does. */
.sd-log-share {
  flex-shrink: 0;
  padding: 6px 13px; border-radius: 999px;
  border: 1px solid var(--sd-line); background: none;
  font-family: var(--font-main); font-size: 11.5px; font-weight: 700;
  color: var(--sd-ink); cursor: pointer;
  transition: background .15s, border-color .15s, color .15s;
}
.sd-log-share:hover { background: var(--star); border-color: var(--star); color: #16130d; }
.sd-log-share[hidden] { display: none; }
/* Save is Share's twin, filled with the album's accent while there is something
   to save; with nothing pending it reads "Saved" and goes quiet. */
.sd-log-save {
  flex-shrink: 0;
  border-radius: 999px; border: 1px solid var(--star, #e8a83c);
  background: var(--star, #e8a83c); color: #16130d;
  font-family: var(--font-main); font-weight: 700; cursor: pointer;
  transition: background .15s, border-color .15s, color .15s, opacity .15s;
}
.sd-log-save:disabled { background: none; border-color: var(--sd-line); color: var(--sd-ink); opacity: 0.45; cursor: default; }

/* On the review card the share sits in `.v3-rev-acts`, where the upvote pill
   lives on everyone else's cards — same slot, so the row doesn't reflow. */
.v3-rev-share {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 9px 2px 6px; border-radius: 999px;
  border: none; background: rgba(255,255,255,0.07);
  color: rgba(232,226,214,0.62);
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;
  line-height: 1; cursor: pointer;
  transition: background .14s, color .14s;
}
.v3-rev-share svg { width: 12px; height: 12px; }
.v3-rev-share:hover { background: rgba(232,168,60,0.18); color: var(--star); }

/* ═══ SHARE (share.js) ═══════════════════════════════════════════════════
   `.sd-share-btn` is THE share control — a 28px round icon that sits on any
   surface (neutral greys, `currentColor` glyph). The sheet rides
   `.sd-log-sheet` for its 95% height and light-theme re-ink; everything
   inside reads the `--sd-*` tokens so it follows the shell it opened from. */
.sd-share-btn {
  width: 28px; height: 28px; border-radius: 999px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid rgba(128,128,128,0.28); background: rgba(128,128,128,0.14);
  color: inherit; cursor: pointer; padding: 0;
  transition: background .15s, color .15s;
}
.sd-share-btn svg { width: 15px; height: 15px; }
.sd-share-btn:hover { background: var(--star, #e8a83c); border-color: var(--star, #e8a83c); color: #1a1208; }
.sd-share-btn--lg { width: 34px; height: 34px; }
.sd-share-btn--lg svg { width: 17px; height: 17px; }
.sd-share-btn--cta { margin-left: auto; margin-right: 8px; color: rgba(232,226,214,0.8); }
.v3-rev-cta--rated .sd-share-btn--cta + .v3-rev-cta-edit { margin-left: 0; }
.v3-rev-share-lbl { margin-left: 8px; font-size: 10.5px; color: rgba(232,226,214,0.5); }
.plp-toprow { display: flex; align-items: center; justify-content: space-between; }
.plp-topbtns { display: inline-flex; align-items: center; gap: 8px; }
.prof-sec-hd--row { display: flex; align-items: center; justify-content: space-between; }
.prof-sec-hd--row .sd-share-btn { color: var(--pf-fg2); }

/* Card + buttons, and nothing else: the sheet is a column, the card takes
   every pixel the buttons leave, the buttons sit on the bottom edge. */
.sd-share-sheet { max-width: none; display: flex; flex-direction: column; padding-bottom: 12px; overflow: hidden; }
.sd-share-x { position: absolute; top: 10px; right: 10px; z-index: 2; }
.sd-share-preview {
  flex: 1; min-height: 0; margin: 22px 0 12px; position: relative;
  display: flex; align-items: center; justify-content: center;   /* no box behind it: the card floats, no letterbox */
}
/* The canvas keeps its 4:5 and grows to whichever edge it meets first. */
.sd-share-preview canvas { display: block; width: auto; height: auto; max-width: 100%; max-height: 100%; border-radius: 12px; box-shadow: 0 8px 28px rgba(0,0,0,0.45); }
.sd-share-note {
  position: absolute; left: 50%; bottom: 10px; transform: translateX(-50%);
  max-width: 88%; padding: 6px 11px; border-radius: 999px;
  background: rgba(17,17,22,0.82); color: rgba(232,226,214,0.9);
  font-family: var(--font-mono); font-size: 9.5px; line-height: 1.4; text-align: center;
  opacity: 0; pointer-events: none; transition: opacity .2s;
}
.sd-share-note.is-on { opacity: 1; }
.sd-share-apps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; flex-shrink: 0; }
/* Save image: the biggest thing on the sheet, full width on the bottom edge. */
.sd-share-app--save {
  flex-direction: row; justify-content: center; gap: 9px; flex-shrink: 0;
  margin-top: 8px; padding: 14px;
  border: none; background: var(--star, #e8a83c); color: #1a1208;
  font-size: 13px; font-weight: 800;
}
.sd-share-app--save svg { width: 20px; height: 20px; }
.sd-share-app--save:hover:not(:disabled) { background: var(--star, #e8a83c); color: #1a1208; filter: brightness(1.07); }
.sd-share-app {
  min-width: 0; padding: 9px 4px 7px; border-radius: 12px;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  border: 1px solid var(--sd-line); background: var(--sd-card);
  color: var(--sd-ink2); font-family: var(--font-main); font-size: 9.5px; font-weight: 600;
  cursor: pointer; transition: background .15s, color .15s;
}
.sd-share-app svg { width: 22px; height: 22px; }
.sd-share-app span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.sd-share-app:hover:not(:disabled) { background: var(--sd-hover); color: var(--sd-ink); }
.sd-share-app:disabled { opacity: .4; cursor: default; }

/* Previews disabled for now — hide the speaker arm/disarm control */
.v3-preview-btn { display: none !important; }

/* ============================================================
   ALBUM WALL v2 — trending page adapted to the home shell.
   Uses the shared .s-home-v3 shell (header · nowbar · bottom nav);
   only the scroll content below is wall-specific.
   ============================================================ */
.wall2-scroll {
  padding: 4px 12px 96px;   /* .v3-body is the scroller; bottom clears the floating nav (63px + 24px gap) */
}
.wall2-hd {
  display: flex; align-items: baseline; gap: 8px;
  padding: 2px 0 12px;
}
.wall2-title {
  font-family: var(--font-main); font-size: 22px; font-weight: 800;
  letter-spacing: -0.6px; color: #e8e2d6;
}
.wall2-sub {
  font-family: var(--font-mono); font-size: 10px;
  color: rgba(232,226,214,0.34); letter-spacing: 0.2px;
}
/* ⚠️ FOUR chips share this row now — Popular · Controversial · Genres · Week —
   so the type and padding are sized to fit them all across a ~310px screen
   rather than to look comfortable on their own. "Controversial" is the one
   that sets the budget; shortening the others buys nothing. `min-width: 0` +
   the overflow guard keep a longer label from pushing Week off the edge. */
.wall2-bar {
  display: flex; align-items: center; gap: 6px;
  padding-bottom: 12px;
  min-width: 0;
}
.wall2-cat {
  flex-shrink: 0;
  padding: 6px 10px; border-radius: 999px;
  background: #111116;
  border: none;
  box-shadow: -4px -4px 8px 0 rgba(255,255,255,0.07), 4px 4px 6px -1px rgba(0,0,0,0.5);
  color: rgba(232,226,214,0.6);
  font-family: var(--font-main); font-size: 11.5px; font-weight: 600;
  cursor: pointer; white-space: nowrap;
  display: inline-flex; align-items: center; gap: 4px;
  transition: box-shadow .18s, color .18s, background .18s;
}
.wall2-cat:hover { background: #17171d; color: #e8e2d6; }
/* Sized down from 11.5px to fit the fourth chip. */
.wall2-cat { font-size: 10.5px; letter-spacing: -0.01em; }
.wall2-cat.active {
  color: var(--star);
  box-shadow: inset 3px 3px 6px rgba(0,0,0,0.55), inset -3px -3px 6px rgba(255,255,255,0.05);
}
.wall2-chev { font-size: 8px; opacity: 0.75; }
.wall2-drop-btn.open {
  color: #e8e2d6;
  box-shadow: inset 3px 3px 6px rgba(0,0,0,0.55), inset -3px -3px 6px rgba(255,255,255,0.05);
}
.wall2-menuwrap { position: relative; display: inline-flex; }
/* ⚠️ The time filter used to be pinned right with `margin-left: auto`. It sits
   in the row with the other three now — they read as one set of controls, and a
   lone chip across the gap read as a different kind of thing. The rule stays
   because the class is gone from the markup, not because it's wanted. */
/* Compact dropdown popup anchored to its button (left for genres, right for time) */
.wall2-menu {
  position: absolute; top: calc(100% + 6px); z-index: 40;
  min-width: 132px; padding: 5px;
  display: flex; flex-direction: column; gap: 1px;
  background: #1d1d24;
  border-radius: 11px;
  box-shadow: 0 12px 30px rgba(0,0,0,0.55), inset 0 1px 0 rgba(255,255,255,0.06);
}
.wall2-menu[hidden] { display: none; }
.wall2-menu--genres { left: 0; }
/* ⚠️ Anchored LEFT, like the genres menu. It was `right: 0` while its button
   was the last thing on the row; now that Week sits mid-row, right-anchoring
   threw the popup back across the screen, away from the chip that opened it. */
.wall2-menu--time { left: 0; }
.wall2-menu-item {
  padding: 8px 12px; border-radius: 7px; border: none; background: none;
  color: rgba(232,226,214,0.8);
  font-family: var(--font-main); font-size: 11.5px; font-weight: 600;
  cursor: pointer; text-align: left; white-space: nowrap;
  transition: background .12s, color .12s;
}
.wall2-menu-item:hover { background: rgba(255,255,255,0.08); color: #e8e2d6; }
.wall2-menu-item.active { color: var(--star); }
/* Light theme */
.s-home-v3--light .wall2-drop-btn.open {
  color: #1a1208;
  box-shadow: inset 3px 3px 6px rgba(116,150,179,0.3), inset -3px -3px 6px rgba(255,255,255,0.7);
}
.s-home-v3--light .wall2-menu { background: #f4efe6; box-shadow: 0 12px 30px rgba(30,20,10,0.2), inset 0 1px 0 rgba(255,255,255,0.7); }
.s-home-v3--light .wall2-menu-item { color: rgba(26,18,8,0.75); }
.s-home-v3--light .wall2-menu-item:hover { background: rgba(26,18,8,0.07); color: #1a1208; }
.s-home-v3--light .wall2-menu-item.active { color: #b06a2a; }
/* NOTE: .wall2-grid / -cell / -art / -meta are shared — the artist page's
   album grid (renderArtistPage in app.js) reuses them, so keep them generic. */
.wall2-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px 8px;
}
.wall2-cell { cursor: pointer; min-width: 0; }
/* ⚠️ 3px, deliberately tighter than the app's usual corner. This is a WALL —
   the cells read as one dense surface, and a soft radius at 3-up made each one
   float as a separate card. Shared with the artist page's album grid. */
.wall2-art {
  position: relative;
  width: 100%; aspect-ratio: 1;
  border-radius: 3px;
  background-size: cover; background-position: center;
  box-shadow: 0 6px 14px rgba(0,0,0,0.45), 0 2px 5px rgba(0,0,0,0.4);
  transition: transform .16s;
}
.wall2-cell:hover .wall2-art { transform: translateY(-2px); }
.wall2-rank {
  position: absolute; top: -6px; left: -6px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--star); color: #16130d;
  font-family: var(--font-main); font-size: 11px; font-weight: 800;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 2px 6px rgba(0,0,0,0.5);
}
.wall2-meta { margin-top: 6px; display: flex; flex-direction: column; min-width: 0; }
.wall2-album {
  font-size: 11px; font-weight: 400; color: #e8e2d6;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wall2-artist {
  font-size: 9px; font-weight: 700; color: rgba(232,226,214,0.45);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wall2-rating { margin-top: 1px; display: flex; align-items: center; gap: 4px; }
.wall2-score {
  font-size: 10px; font-weight: 700; color: rgba(232,226,214,0.6);
  font-family: var(--font-mono);
}
/* Wall — light theme */
.s-home-v3--light .wall2-title  { color: #1a1208; }
.s-home-v3--light .wall2-sub    { color: rgba(26,18,8,0.4); }
.s-home-v3--light .wall2-cat {
  background: #f0ece3;
  border: none;
  box-shadow: -4px -4px 8px 0 #d8d2c8, 4px 4px 6px -1px rgba(116,150,179,0.27);
  color: rgba(26,18,8,0.6);
}
.s-home-v3--light .wall2-cat:hover { background: #f4f0e8; color: #1a1208; }
.s-home-v3--light .wall2-cat.active {
  color: #b06a2a;
  box-shadow: inset 3px 3px 6px rgba(116,150,179,0.3), inset -3px -3px 6px rgba(255,255,255,0.7);
}
.s-home-v3--light .wall2-album  { color: #1a1208; }
.s-home-v3--light .wall2-artist { color: rgba(26,18,8,0.5); }
.s-home-v3--light .wall2-score  { color: rgba(26,18,8,0.55); }
.s-home-v3--light .wall2-art {
  box-shadow: 0 6px 14px rgba(30,20,10,0.18), 0 2px 5px rgba(30,20,10,0.15);
}

/* ── Trending wall grid ──────────────────────────────────────────
   The wall shares the generic .wall2-grid with the artist page: inset from
   the frame, gutters between the covers, rounded + shadowed tiles, and the
   rank badge hung off the top-left corner. (An edge-to-edge flush mosaic —
   no side inset, no column gutter, square tiles — was tried and dropped:
   the artwork ran into the frame and read as a solid slab.) */
.s-wall2 .wall2-cell { position: relative; }

/* ============================================================
   PLAYLISTS / LIBRARY v2 — adapted to the home shell like the wall.
   Pill tabs (reuses .wall2-bar/.wall2-cat) switch the .pl2-sec
   sections client-side via plTab(); all data comes from ARCHIVE.
   ============================================================ */
.pl2-scroll {
  padding: 4px 12px 96px;   /* .v3-body is the scroller; bottom clears the floating nav (63px + 24px gap) */
}
/* Top bar — category pills + the "+" (new playlist) button on the right.
   No page title; the pills ARE the header. Pills scroll horizontally and
   fade out at the right edge when there isn't enough room. */
.pl2-topbar { display: flex; align-items: center; gap: 10px; padding: 2px 0 12px; }
.pl2-bar {
  flex: 1; min-width: 0;
  overflow-x: auto; scrollbar-width: none;
  /* Internal padding gives the raised pill emboss room; negative margins
     cancel it so the row keeps its original alignment. */
  padding: 10px 8px;
  margin: -10px -8px;
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 34px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 34px), transparent);
}
.pl2-bar::-webkit-scrollbar { display: none; }
.pl2-bar .wall2-cat { font-size: 12px; padding: 7px 14px; }
/* Discover — community playlists; embossed like the "+", acts as a tab */
.pl2-discover {
  flex-shrink: 0;
  height: 34px; padding: 0 14px; border-radius: 999px;
  background: #111116; border: none;
  box-shadow: -4px -4px 8px 0 rgba(255,255,255,0.07), 4px 4px 6px -1px rgba(0,0,0,0.5);
  color: rgba(232,226,214,0.85);
  font-family: var(--font-main); font-size: 12px; font-weight: 600;
  cursor: pointer;
  transition: background .15s, color .15s;
}
.pl2-discover:hover { background: #17171d; }
.pl2-discover.active { background: var(--star); color: #16130d; box-shadow: 4px 4px 6px -1px rgba(0,0,0,0.5); }
.s-home-v3--light .pl2-discover {
  background: #f0ece3;
  box-shadow: -4px -4px 8px 0 #d8d2c8, 4px 4px 6px -1px rgba(116,150,179,0.27);
  color: rgba(26,18,8,0.6);
}
.s-home-v3--light .pl2-discover:hover { background: #f4f0e8; }
.s-home-v3--light .pl2-discover.active { background: var(--star); color: #16130d; }
/* "+" — same emboss as the home live pill (.v3-search-pill) */
.pl2-add {
  flex-shrink: 0;
  width: 34px; height: 34px; border-radius: 999px;
  background: #111116; border: none;
  box-shadow: -4px -4px 8px 0 rgba(255,255,255,0.07), 4px 4px 6px -1px rgba(0,0,0,0.5);
  display: inline-flex; align-items: center; justify-content: center;
  color: rgba(232,226,214,0.85);
  cursor: pointer;
  transition: background .15s;
}
.pl2-add:hover { background: #17171d; }
.pl2-add svg { width: 14px; height: 14px; }
.s-home-v3--light .pl2-add {
  background: #f0ece3;
  box-shadow: -4px -4px 8px 0 #d8d2c8, 4px 4px 6px -1px rgba(116,150,179,0.27);
  color: rgba(26,18,8,0.6);
}
.s-home-v3--light .pl2-add:hover { background: #f4f0e8; }
.pl2-sec[hidden] { display: none; }
/* The library tabs (plLibrary in screens.js). Albums are the trending wall's
   grid as-is; a favourite song is the playlist page's row with a cover where
   the track number was and a heart on the end. */
.pl2-lib-song-art {
  width: 30px; height: 30px; border-radius: 3px; flex-shrink: 0;
  background-size: cover; background-position: center;
  box-shadow: 0 2px 6px rgba(0,0,0,0.4);
}
.pl2-lib-song-heart { flex-shrink: 0; font-size: 12px; color: var(--star, #e8a83c); }
.pl2-sec .plp-empty { padding-top: 48px; }
/* "Your rating" — under the discs when the score shown is yours, not the crowd's. */
.pl2-lib-mine {
  display: block; margin-top: 2px;
  font-family: var(--font-mono); font-size: 8px; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--star, #e8a83c);
}

/* ═══════════════════════════════════════════════════════════════
   THE PLAYLIST WALL — one card, done properly
   ═══════════════════════════════════════════════════════════════
   Two things were tried and dropped on the way here, and both are worth knowing
   before changing this:

     1. A wall of 80px split ROWS. Too small to be anything — a playlist is the
        one object in the app the user actually made, and a row gave it nothing.
     2. FIVE user-pickable themes (poster / polaroid / tape / index / classic).
        Killed on purpose: a wall where every card is a different shape reads as
        a mess, and most people never open a picker — so the wall ends up being
        judged on the default anyway. ⚠️ Do not reintroduce per-card shapes.

   What is left is ONE card, and the variety comes from where it should: the
   ARTWORK, which is different for every playlist because the user chose it, and
   the badges. The frame stays constant so ten of them stack into a wall instead
   of a pile.

   ⚠️ `aspect-ratio`, not a fixed height. The phone frame is not one width (the
   viewer zooms, and the mobile prototype is the real device), and a card locked
   to 230px goes squat on a wide frame and cramped on a narrow one. 3:2 keeps
   the crop and the type in proportion at any size. */
.pl2-card {
  position: relative;
  aspect-ratio: 3 / 2;
  margin-bottom: 14px;
  border-radius: 14px;
  overflow: hidden;
  cursor: pointer;
  background: rgba(255,255,255,0.05);
  /* A hairline INSIDE the radius, so the card reads as an object with an edge
     rather than a photograph bleeding into the page. `inset` because an outer
     ring would sit outside `overflow: hidden` and fight the corner. */
  box-shadow: inset 0 0 0 1px rgba(232,226,214,0.10);
  transition: transform .18s cubic-bezier(.22,1,.36,1);
}
.pl2-card:active { transform: scale(0.985); }

/* ⚠️ The art is scaled 1.04 and animates on press. The card is mostly picture,
   so the press has to happen to the PICTURE or it reads as the text moving. */
.pl2-card-art {
  position: absolute; inset: 0;
  background-size: cover; background-position: center;
  background-color: rgba(255,255,255,0.05);
  transform: scale(1.04);
  transition: transform .5s cubic-bezier(.22,1,.36,1);
}
.pl2-card:active .pl2-card-art { transform: scale(1.07); }
/* A video cover fills the same box a still would. ⚠️ `object-fit: cover` is the
   video equivalent of `background-size: cover` — without it the frame is letter-
   boxed inside the card and the crop stops matching every other cover on the
   wall. Playback is driven by `plVideoWatch` (app.js), not by an autoplay
   attribute; see the note there. */
.pl2-art-vid { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The scrim. ⚠️ A gradient LAYER, never a `filter` on the art: darkening the
   whole image to make the type readable throws away the picture the card exists
   to show. Two stops rather than one — a single linear ramp leaves a visible
   band across the middle of the card at this height. */
.pl2-card::after {
  content: ''; position: absolute; inset: 0; z-index: 1;
  background:
    linear-gradient(to top, rgba(8,7,6,0.94) 0%, rgba(8,7,6,0.72) 22%,
                    rgba(8,7,6,0.30) 48%, rgba(8,7,6,0) 74%);
}

.pl2-card-body {
  position: absolute; z-index: 2; left: 0; right: 0; bottom: 0;
  padding: 0 16px 15px;
  display: flex; flex-direction: column;
}
.pl2-card-name {
  margin: 0;
  font-family: var(--font-main);
  font-size: 24px; font-weight: 800; letter-spacing: -0.7px; line-height: 1.1;
  color: #fff;
  /* Two lines, then clamp. These titles are user-typed and some run long on
     purpose ("crying in the club (derogatory)") — one line ellipsised away is
     the joke lost, three lines is a paragraph sitting on someone's artwork. */
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  overflow: hidden;
  /* The titles carry emoji and stray symbols, which land on unpredictable parts
     of a photograph. A soft shadow keeps them legible over a blown-out sky
     without darkening the whole scrim for the sake of one card. */
  text-shadow: 0 1px 12px rgba(0,0,0,0.45);
}
.pl2-card-by {
  font-size: 10.5px; margin-top: 6px;
  color: rgba(255,255,255,0.6);
  text-shadow: 0 1px 8px rgba(0,0,0,0.4);
}
.pl2-card-by b { font-weight: 700; color: rgba(255,255,255,0.92); }
/* ⚠️ `·` separators come from CSS, not the markup — a leading dot before an
   empty first item is the classic version of this bug, and `:not(:first-child)`
   cannot happen in a template string. */
.pl2-card-meta {
  display: flex; margin-top: 9px;
  font-family: var(--font-mono); font-size: 9.5px;
  color: rgba(255,255,255,0.55);
  text-shadow: 0 1px 8px rgba(0,0,0,0.4);
}
.pl2-card-meta span + span::before { content: '\00b7'; margin: 0 6px; opacity: 0.55; }

/* ── Badges ── the emblems you pin on. Drawn in the DOT SYSTEM like every other
   icon in the app, so a badge is the same material as the nav and the log
   buttons rather than a sticker imported from somewhere else. Now that the card
   is fixed, these and the artwork are the ONLY things that vary — which is why
   they each get their own colour: a card wearing three should read as three
   things, not as a row of identical chips. */
.pl2-badges { position: absolute; z-index: 3; top: 12px; right: 12px; display: flex; gap: 6px; }
.pl2-badge {
  width: 24px; height: 24px; border-radius: 999px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(12,11,10,0.5);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.18);
  color: #e8e2d6;
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
.pl2-badge .sd-dot-ico { width: 12px; height: 12px; }
/* The badge editor, riding in the badge row. ⚠️ Same footprint as a badge but
   DASHED and uncoloured — it has to read as an empty slot rather than as a
   seventh badge, which is also what makes it legible on a card wearing none. */
.pl2-badge-add {
  border: 0; padding: 0; cursor: pointer;
  background: rgba(12,11,10,0.35);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.28);
  color: rgba(255,255,255,0.85);
  transition: background .14s, color .14s;
}
.pl2-badge-add:hover { background: rgba(12,11,10,0.62); color: #fff; }
.pl2-badge-add svg { width: 12px; height: 12px; }

.pl2-badge--gem   { color: #9fd0e8; }
.pl2-badge--flame { color: #ff8a4c; }
.pl2-badge--moon  { color: #a9a5e8; }
.pl2-badge--bolt  { color: var(--star, #e8a83c); }
.pl2-badge--drop  { color: #7fb3e8; }
.pl2-badge--sun   { color: #ffd166; }

/* ── The editorial tag ── crown = community favourite (favs > 25), candle =
   staff pick; staff wins if both apply.
   ⚠️ The ONE mark on a card its owner cannot choose, so it is deliberately a
   different object from a badge — square not round, top-LEFT not top-right. A
   badge is self-expression; this is a verdict from outside, and the two must not
   be mistaken for each other. */
.pl2-card-tag {
  position: absolute; top: 12px; left: 12px; z-index: 3;
  height: 24px; padding: 0 8px; border-radius: 7px;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(12,11,10,0.5);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.14);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
}
.pl2-card-tag svg { width: 14px; height: 14px; }
.pl2-card-tag--fav   { color: var(--star, #e8a83c); }
.pl2-card-tag--staff { color: #6f9fe8; }

/* The edit handle — only on playlists you made. Sits opposite the title
   block, on the one corner nothing else claims. ⚠️ Always visible: it used to
   fade in on hover, and on the desktop viewer nobody found it. */
.pl2-card-edit {
  position: absolute; right: 12px; bottom: 12px; z-index: 4;
  width: 28px; height: 28px; border-radius: 999px;
  border: 0; padding: 0; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(12,11,10,0.5);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.14);
  color: rgba(255,255,255,0.8);
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
  transition: background .16s;
}
.pl2-card:hover .pl2-card-edit,
.pl2-card:focus-within .pl2-card-edit { background: rgba(12,11,10,0.72); color: #fff; }
.pl2-card-edit svg { width: 15px; height: 15px; }

/* ⚠️ NO light-theme overrides, and that is deliberate. Every piece of type on
   this card sits on a PHOTOGRAPH, not on the page — so it stays white in both
   themes, exactly as album art does everywhere else in the app. The only thing
   the light theme touches is the empty state behind an image that has not
   loaded. */
.s-home-v3--light .pl2-card { background: rgba(26,18,8,0.06); }
.s-home-v3--light .pl2-card-art { background-color: rgba(26,18,8,0.06); }

@media (prefers-reduced-motion: reduce) {
  .pl2-card, .pl2-card-art, .pl2-card-edit, .pl2-badge-add { transition: none; }
  .pl2-card:active .pl2-card-art { transform: scale(1.04); }
}

/* ═══════════════════════════════════════════════════════════════
   THE CUSTOMISE SHEET — behind the ⋯ on your own cards
   ═══════════════════════════════════════════════════════════════
   Rides `.sd-log-overlay` for the scrim and the mounting so there is one
   bottom-sheet behaviour in the app. ⚠️ It sizes to its CONTENT rather than
   taking a fixed 75% like the log sheet — this one is a header and a single row
   of badges, and a three-quarter-height sheet holding one row reads as something
   that failed to load. */
.plc-sheet {
  width: calc(100% - 20px); margin-bottom: 10px;
  background: #1c1c22; color: #e8e2d6;
  border-radius: 20px;
  padding: 9px 14px 14px;
  box-shadow: 0 -10px 34px rgba(0,0,0,0.5);
  transform: translateY(130%);
  transition: transform .3s cubic-bezier(.22,1,.36,1);
  font-family: var(--font-main);
  max-height: 82%; overflow-y: auto; scrollbar-width: none;
}
.plc-sheet::-webkit-scrollbar { display: none; }
.plc-overlay.open .plc-sheet { transform: translateY(0); }

.plc-head { display: flex; align-items: center; gap: 11px; margin-bottom: 14px; }
.plc-cover {
  width: 42px; height: 42px; border-radius: 7px; flex-shrink: 0;
  background: #333 center/cover no-repeat;
  box-shadow: 0 2px 8px rgba(0,0,0,0.45);
}
.plc-head-txt { flex: 1; min-width: 0; }
.plc-title {
  font-size: 15px; font-weight: 800; letter-spacing: -0.3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.plc-sub { font-family: var(--font-mono); font-size: 9px; color: rgba(232,226,214,0.42); margin-top: 2px; }
.plc-x {
  flex-shrink: 0; width: 26px; height: 26px; border-radius: 999px;
  border: 0; cursor: pointer;
  background: rgba(232,226,214,0.09); color: rgba(232,226,214,0.7);
  font-size: 12px; line-height: 1;
}

.plc-sec-hd {
  display: flex; align-items: baseline; gap: 7px; margin: 0 0 9px;
  font-size: 12px; font-weight: 700; letter-spacing: -0.2px;
}
.plc-sec-hd span {
  font-family: var(--font-mono); font-size: 8.5px; font-weight: 400;
  color: rgba(232,226,214,0.38);
}

/* Badges — a grid, because they are peers and a rail would imply an order. */
.plc-badges { display: grid; grid-template-columns: repeat(6, 1fr); gap: 7px; }
.plc-badge {
  border: 0; padding: 8px 2px 6px; border-radius: 10px; cursor: pointer;
  background: rgba(232,226,214,0.06);
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  color: inherit; font-family: inherit;
  transition: background .14s, box-shadow .14s;
}
.plc-badge .pl2-badge { width: 26px; height: 26px; }
.plc-badge .sd-dot-ico { width: 13px; height: 13px; }
.plc-badge-name {
  font-family: var(--font-mono); font-size: 7.5px;
  color: rgba(232,226,214,0.45);
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.plc-badge.is-on { box-shadow: inset 0 0 0 1.5px var(--star, #e8a83c); }
.plc-badge.is-on .plc-badge-name { color: var(--star, #e8a83c); font-weight: 700; }


.plc-note {
  margin: 14px 0 0;
  font-family: var(--font-mono); font-size: 8px; line-height: 1.5;
  color: rgba(232,226,214,0.3);
}

@media (prefers-reduced-motion: reduce) {
  .plc-sheet, .plc-badge { transition: none; }
}

/* — Artists — */
.pl2-artist {
  display: flex; align-items: center; gap: 11px;
  padding: 9px 2px; cursor: pointer;
  border-bottom: 1px solid rgba(255,255,255,0.05);
}
.pl2-artist-av {
  width: 42px; height: 42px; border-radius: 50%;
  background-size: cover; background-position: center;
  box-shadow: 0 3px 8px rgba(0,0,0,0.45);
  flex-shrink: 0;
}
.pl2-artist-info { flex: 1; min-width: 0; }
.pl2-artist-name { font-size: 12.5px; font-weight: 700; color: #e8e2d6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pl2-artist-genre { font-size: 9.5px; color: rgba(232,226,214,0.42); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pl2-artist-rating { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.pl2-score { font-family: var(--font-mono); font-size: 10px; font-weight: 700; color: rgba(232,226,214,0.55); }

/* — Songs — */
.pl2-song {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 2px; cursor: pointer;
  border-bottom: 1px solid rgba(255,255,255,0.05);
}
.pl2-song-num { width: 14px; font-family: var(--font-mono); font-size: 10px; color: rgba(232,226,214,0.32); text-align: center; flex-shrink: 0; }
.pl2-song-art { width: 36px; height: 36px; border-radius: 6px; background-size: cover; background-position: center; box-shadow: 0 2px 6px rgba(0,0,0,0.4); flex-shrink: 0; }
.pl2-song-info { flex: 1; min-width: 0; }
.pl2-song-title { font-size: 12px; font-weight: 600; color: #e8e2d6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pl2-song-sub { font-size: 9px; color: rgba(232,226,214,0.42); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pl2-song-album { font-weight: 400; }
.pl2-song-artist { font-weight: 700; }
.pl2-song-right { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; flex-shrink: 0; }
.pl2-song-dur { font-family: var(--font-mono); font-size: 9px; color: rgba(232,226,214,0.38); }

/* — Genres — */
.pl2-genre {
  display: flex; align-items: center; gap: 12px;
  padding: 12px; margin-bottom: 9px; cursor: pointer;
  background: rgba(255,255,255,0.05);
  border-radius: 13px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.06);
  transition: background .15s;
}
.pl2-genre:hover { background: rgba(255,255,255,0.09); }
.pl2-genre-info { flex: 1; min-width: 0; }
.pl2-genre-name { font-size: 14px; font-weight: 800; letter-spacing: -0.3px; color: #e8e2d6; }
.pl2-genre-stats { font-family: var(--font-mono); font-size: 9px; color: rgba(232,226,214,0.4); margin-top: 3px; }
.pl2-genre-covers { display: flex; flex-shrink: 0; }
.pl2-genre-cover {
  width: 38px; height: 38px; border-radius: 6px;
  background-size: cover; background-position: center;
  box-shadow: 0 3px 8px rgba(0,0,0,0.5);
  border: 1.5px solid #1d1d24;
}
.pl2-genre-cover + .pl2-genre-cover { margin-left: -14px; }

/* Playlists — light theme. The card itself has no light overrides on purpose:
   all its type sits on a photograph. See the card block above. */
.s-home-v3--light .pl2-genre { background: rgba(26,18,8,0.05); box-shadow: inset 0 1px 0 rgba(255,255,255,0.5); }
.s-home-v3--light .pl2-genre:hover { background: rgba(26,18,8,0.09); }
.s-home-v3--light .pl2-artist-name,
.s-home-v3--light .pl2-song-title,
.s-home-v3--light .pl2-genre-name { color: #1a1208; }
.s-home-v3--light .pl2-artist-genre,
.s-home-v3--light .pl2-song-sub,
.s-home-v3--light .pl2-genre-stats { color: rgba(26,18,8,0.45); }
.s-home-v3--light .pl2-song-num,
.s-home-v3--light .pl2-song-dur { color: rgba(26,18,8,0.4); }
.s-home-v3--light .pl2-score { color: rgba(26,18,8,0.55); }
.s-home-v3--light .pl2-artist,
.s-home-v3--light .pl2-song { border-bottom-color: rgba(26,18,8,0.08); }
.s-home-v3--light .pl2-genre-cover { border-color: #f0ece3; box-shadow: 0 3px 8px rgba(30,20,10,0.2); }
.s-home-v3--light .pl2-artist-av,
.s-home-v3--light .pl2-song-art { box-shadow: 0 2px 6px rgba(30,20,10,0.2); }

/* ============================================================
   PLAYLIST PAGE — geometry from PlaylistPageBox.svg (688×303).
   The hero is an aspect-ratio box: image panel + CD are positioned
   divs (percent coords straight from the SVG); the info panel and
   Popular dog-ear are Eric's exact paths in an inline SVG, filled
   via CSS so both themes work. The CD seats in the panel's carved
   swoop and overflows below it, like the drawing.
   ============================================================ */
.plp-scroll { padding: 4px 12px 75px; }
/* Back pill — same dot-ring as the home live pill; the arrow (idle formation)
   points left = back. */
/* Same emboss as the home live pill (.v3-search-pill) — solid bg + neu shadow */
.plp-back-pill {
  display: inline-flex; align-items: center; justify-content: center;
  width: 72px; height: 34px; margin-bottom: 10px;
  border-radius: 999px;
  background: #111116;
  border: none;
  box-shadow: -4px -4px 8px 0 rgba(255,255,255,0.07), 4px 4px 6px -1px rgba(0,0,0,0.5);
  cursor: pointer;
  transition: background .15s;
}
.plp-back-pill:hover { background: #17171d; }

/* — hero — */
.plp-hero { position: relative; aspect-ratio: 688 / 303; margin-bottom: 14px; }
.plp-hero-img {
  position: absolute; left: 0; top: 0;
  width: 36.77%; height: 85.47%;          /* 253×259 of 688×303 */
  border-radius: 5px 0 0 5px;
  background-size: cover; background-position: center;
  background-color: rgba(255,255,255,0.05);
  /* A video cover renders as a <video> with this same class (`plArtHtml`), so
     the box has to crop the frame the way the background-size above crops a
     still — and clip it, or the video's own corners square off the panel. */
  object-fit: cover; overflow: hidden;
}
.plp-hero-shape { position: absolute; inset: 0; width: 100%; height: 100%; }
.plp-shape-panel { fill: rgba(255,255,255,0.05); }
.plp-shape-tag   { fill: var(--star); }
.plp-info {
  position: absolute; left: 40.5%; top: 8%; right: 5.5%; height: 76%;
  display: flex; flex-direction: column; align-items: flex-start;
}
.plp-name {
  font-size: 16px; font-weight: 800; letter-spacing: -0.3px; color: #e8e2d6;
  padding-right: 26px;                    /* clear the dog-ear */
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.plp-by { font-size: 10px; color: rgba(232,226,214,0.42); margin-top: 3px; }
.plp-by b { font-weight: 700; color: rgba(232,226,214,0.7); }
.plp-meta { font-family: var(--font-mono); font-size: 9.5px; color: rgba(232,226,214,0.4); margin-top: 6px; }
/* Majority genres of the tracklist — light, airy letters */
.plp-genres {
  font-size: 9px; font-weight: 500; letter-spacing: 0.4px;
  color: rgba(232,226,214,0.3);
  margin-top: 3px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 100%;
}
.plp-fav {
  margin-top: auto; margin-bottom: 10px;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; border-radius: 999px;
  background: rgba(255,255,255,0.06);
  border: 1px solid rgba(255,255,255,0.1);
  color: rgba(232,226,214,0.6);
  font-family: var(--font-mono); font-size: 10.5px; font-weight: 700;
  cursor: pointer; transition: background .15s, color .15s, border-color .15s;
}
.plp-fav svg { width: 13px; height: 13px; fill: none; }
.plp-fav:hover { background: rgba(255,255,255,0.1); color: #e8e2d6; }
.plp-fav.on { background: var(--star); border-color: var(--star); color: #16130d; }
.plp-fav.on svg { fill: #16130d; }
/* CD — center (613.66, 247.47) r 55 in SVG coords; spins like the home CD */
.plp-cd {
  position: absolute;
  left: 81.2%; top: 63.52%; width: 15.99%; aspect-ratio: 1;
  border-radius: 50%; overflow: hidden;
  background-size: cover; background-position: center;
  animation: v3spin 12s linear infinite;
  cursor: pointer;
  box-shadow:
    0 4px 12px rgba(0,0,0,0.3),
    0 1px 3px rgba(0,0,0,0.2),
    0 0 0 1px rgba(255,255,255,0.06);
}
.plp-plat { position: absolute; right: 0; top: 100%; margin-top: -4px; z-index: 50; }
.plp-plat-item { display: flex; align-items: center; gap: 8px; }
/* A service row while its link is being resolved, and when there isn't one.
   Same two states the preview button has (`.v3-stream-preview.none`) and the
   same language: the row dips rather than throwing an error at you. Most taps
   see neither — the menu warms all three links as it opens. */
.plp-plat-item.is-wait { opacity: .5; pointer-events: none; }
.plp-plat-item.none { opacity: .45; }
/* Same two-layer tile as the console's, at menu-row size — see `svcMarkHtml`. */
.plp-plat-ico {
  position: relative; overflow: hidden;
  width: 18px; height: 18px; border-radius: 5px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
}
.plp-plat-ico > svg { width: 64%; height: 64%; }

/* — songs — one line per row: song name boldest · album mid · artist lightest.
   Overflow fades out on the right (mask) instead of ellipsizing. */
.plp-song {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 2px; cursor: pointer;
  border-bottom: 1px solid rgba(255,255,255,0.05);
}
.plp-song-num { width: 16px; font-family: var(--font-mono); font-size: 10px; color: rgba(232,226,214,0.32); text-align: center; flex-shrink: 0; }
.plp-song-line {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden;
  font-size: 11px;
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
          mask-image: linear-gradient(90deg, #000 calc(100% - 26px), transparent);
}
.plp-song-title  { font-size: 12px; font-weight: 700; color: #e8e2d6; margin-right: 7px; }
.plp-song-album  { font-weight: 500; color: rgba(232,226,214,0.5); }
.plp-song-artist { font-weight: 400; color: rgba(232,226,214,0.32); }
.plp-song-rate { font-family: var(--font-mono); font-size: 10px; font-weight: 700; color: rgba(232,226,214,0.6); flex-shrink: 0; }
.plp-song-dur { font-family: var(--font-mono); font-size: 9px; color: rgba(232,226,214,0.38); flex-shrink: 0; width: 24px; text-align: right; }

/* ── Playlist page tracklist, in the album page's clothes ──────────────────
   Every number here is the album tracklist's (.v3-song-row / -num / -dur /
   -rate / -head in the ALBUM PAGE section): 9px 12px rows, 10px gap, 14px
   left-aligned number, 42px fixed right columns sized to their header labels,
   0.06 hairlines, the same header type. Change one and change the other.
   The title cell keeps the playlist's own three-part line (song · album ·
   artist) and its right-edge fade. Scoped to `--album` so the New Playlist
   page, which reuses the row classes, is untouched. */
.plp-songs--album { margin-top: 10px; --song-dur-w: 42px; --song-rate-w: 42px; }
.plp-songs--album .plp-song {
  padding: 9px 12px; gap: 10px;
  border-bottom: none; border-top: 1px solid rgba(232,226,214,0.06);
}
.plp-songs--album .plp-song:first-of-type { border-top: none; }
.plp-songs--album .plp-song:hover { background: rgba(232,226,214,0.045); }
.plp-songs--album .plp-song-num {
  width: 14px; text-align: left;
  font-size: 9.5px; color: rgba(232,226,214,0.4);
}
.plp-songs--album .plp-song-line { font-size: 11px; }
.plp-songs--album .plp-song-title { font-size: 12px; font-weight: 500; margin-right: 8px; }
.plp-songs--album .plp-song-dur {
  width: var(--song-dur-w); text-align: right;
  font-size: 10px; color: rgba(232,226,214,0.4);
}
.plp-songs--album .plp-song-rate {
  display: inline-flex; align-items: center; justify-content: flex-end;
  width: var(--song-rate-w);
  font-size: 11.5px; font-weight: 700; color: var(--star);
}
.plp-song-head {
  display: flex; align-items: center; gap: 10px;
  padding: 0 12px 7px;
  font-family: var(--font-mono); font-size: 8.5px;
  text-transform: uppercase; letter-spacing: 0.12em;
  color: rgba(232,226,214,0.3);
}
.plp-song-head .plp-song-line,
.plp-song-head .plp-song-dur,
.plp-song-head .plp-song-rate {
  font-family: inherit; font-size: inherit; font-weight: 400; color: inherit;
  -webkit-mask-image: none; mask-image: none;
}
.plp-song-head .plp-song-num { width: 14px; }
.s-home-v3--light .plp-songs--album .plp-song { border-top-color: rgba(26,18,8,0.08); }
.s-home-v3--light .plp-songs--album .plp-song:hover { background: rgba(26,18,8,0.04); }
.s-home-v3--light .plp-song-head { color: rgba(26,18,8,0.36); }
.s-home-v3--light .plp-songs--album .plp-song-rate { color: var(--star); }

/* Playlist page — light theme */
.s-home-v3--light .plp-back-pill {
  background: #f0ece3;
  box-shadow: -4px -4px 8px 0 #d8d2c8, 4px 4px 6px -1px rgba(116,150,179,0.27);
}
.s-home-v3--light .plp-back-pill:hover { background: #f4f0e8; }
.s-home-v3--light .plp-hero-img { background-color: rgba(26,18,8,0.05); }
.s-home-v3--light .plp-shape-panel { fill: rgba(26,18,8,0.05); }
.s-home-v3--light .plp-name { color: #1a1208; }
.s-home-v3--light .plp-by { color: rgba(26,18,8,0.45); }
.s-home-v3--light .plp-by b { color: rgba(26,18,8,0.7); }
.s-home-v3--light .plp-meta { color: rgba(26,18,8,0.45); }
.s-home-v3--light .plp-genres { color: rgba(26,18,8,0.32); }
.s-home-v3--light .plp-fav {
  background: rgba(26,18,8,0.05);
  border-color: rgba(26,18,8,0.12);
  color: rgba(26,18,8,0.6);
}
.s-home-v3--light .plp-fav:hover { background: rgba(26,18,8,0.1); color: #1a1208; }
.s-home-v3--light .plp-fav.on { background: var(--star); border-color: var(--star); color: #16130d; }
.s-home-v3--light .plp-cd { box-shadow: 0 4px 12px rgba(30,20,10,0.25), 0 1px 3px rgba(30,20,10,0.15); }
.plp-empty {
  padding: 22px 4px; text-align: center;
  font-family: var(--font-mono); font-size: 10.5px; color: rgba(232,226,214,0.35);
}
.s-home-v3--light .plp-empty { color: rgba(26,18,8,0.4); }

/* ============================================================
   NEW PLAYLIST (.s-plnew) — the creation page behind the "+".
   Reuses the playlist page's hero (.plp-hero / .plp-hero-img /
   .plp-shape-panel / .plp-cd) so the form IS a preview of the page being
   built; only the editable bits and the pickers below are new here.
   ============================================================ */
.plnew-scroll { padding: 4px 12px 75px; }

/* — cover well: the detail page's image panel as a real upload target. It's a
   <label> wrapping a hidden file input, so the whole panel opens the picker. — */
.plnew-cover {
  display: flex; align-items: center; justify-content: center;
  border: 1px dashed rgba(232,226,214,0.22);
  cursor: pointer;
  transition: border-color .15s, background-color .15s;
}
.plnew-cover:hover { border-color: var(--star); }
.plnew-cover--set { border-color: transparent; }
.plnew-file { display: none; }
/* The well's video preview — created by `plnewSyncOne` only when the chosen
   cover is a video, and removed again when it is not. ⚠️ `position: absolute`
   over the well rather than in its flow: the well is a centring flex box for
   the upload hint, and a video in that flow would push the hint around instead
   of covering it. */
.plnew-cover-vid {
  position: absolute; inset: 0; z-index: 1;
  width: 100%; height: 100%; object-fit: cover;
  border-radius: inherit;
}
.plnew-cover-hint {
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  font-family: var(--font-mono); font-size: 8px; letter-spacing: .06em;
  color: rgba(232,226,214,0.4); text-align: center; padding: 0 6px;
}
.plnew-cover-hint svg { width: 17px; height: 17px; }
.plnew-cover-hint i { font-style: normal; }
.plnew-cover:hover .plnew-cover-hint { color: var(--star); }
.plnew-cover--set .plnew-cover-hint { display: none; }
/* the CD is empty until a cover is chosen — give it a disc-coloured blank
   instead of letting the page show through the hole */
.plnew-cd { background-color: rgba(232,226,214,0.09); }
.s-home-v3--light .plnew-cd { background-color: rgba(26,18,8,0.1); }
.s-home-v3--light .plnew-cover { border-color: rgba(26,18,8,0.22); }
.s-home-v3--light .plnew-cover-hint { color: rgba(26,18,8,0.42); }
.s-home-v3--light .plnew-cover--set { border-color: transparent; }

/* — name field: sits where .plp-name sits, styled as an editable version — */
.plnew-name {
  width: 100%; padding: 0 26px 2px 0;                 /* right pad clears the dog-ear */
  background: none; border: none; border-bottom: 1px solid rgba(232,226,214,0.16);
  font-family: var(--font-main); font-size: 15px; font-weight: 800; letter-spacing: -0.3px;
  color: #e8e2d6; outline: none;
}
.plnew-name::placeholder { color: rgba(232,226,214,0.26); font-weight: 700; }
.plnew-name:focus { border-bottom-color: var(--star); }
.s-home-v3--light .plnew-name { color: #1a1208; border-bottom-color: rgba(26,18,8,0.18); }
.s-home-v3--light .plnew-name::placeholder { color: rgba(26,18,8,0.3); }

/* — public / private, in the slot the detail page gives the heart — */
.plnew-priv { margin-top: auto; margin-bottom: 10px; display: flex; gap: 5px; }
.plnew-priv-btn {
  padding: 5px 10px; border-radius: 999px;
  background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1);
  color: rgba(232,226,214,0.55);
  font-family: var(--font-mono); font-size: 9px; font-weight: 700; letter-spacing: .04em;
  cursor: pointer; transition: background .15s, color .15s, border-color .15s;
}
.plnew-priv-btn:hover { background: rgba(255,255,255,0.1); color: #e8e2d6; }
.plnew-priv-btn.active { background: var(--star); border-color: var(--star); color: #16130d; }
.s-home-v3--light .plnew-priv-btn {
  background: rgba(26,18,8,0.05); border-color: rgba(26,18,8,0.12); color: rgba(26,18,8,0.5);
}
.s-home-v3--light .plnew-priv-btn.active { background: var(--star); border-color: var(--star); color: #16130d; }

/* — rows: the detail page's song row, plus an add / remove control — */
.plnew-row { cursor: default; }
.plnew-res-art {
  width: 26px; height: 26px; border-radius: 5px; flex-shrink: 0;
  background-size: cover; background-position: center;
  background-color: rgba(255,255,255,0.05);
}
.plnew-plus, .plnew-x {
  flex-shrink: 0; width: 24px; height: 24px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.07); border: none;
  color: rgba(232,226,214,0.7);
  font-size: 15px; line-height: 1; cursor: pointer;
  transition: background .15s, color .15s;
}
.plnew-plus:hover, .plnew-x:hover { background: rgba(255,255,255,0.14); color: #e8e2d6; }
.plnew-res--on .plnew-plus { background: var(--star); color: #16130d; }
.s-home-v3--light .plnew-plus, .s-home-v3--light .plnew-x {
  background: rgba(26,18,8,0.07); color: rgba(26,18,8,0.6);
}
.s-home-v3--light .plnew-res--on .plnew-plus { background: var(--star); color: #16130d; }

/* — find row: search + the "Library" toggle that browses your own playlists — */
.plnew-find { display: flex; align-items: center; gap: 8px; margin: 0 0 6px; }
.plnew-libbtn {
  flex-shrink: 0; height: 35px; padding: 0 12px; border-radius: 10px;
  display: inline-flex; align-items: center; gap: 6px;
  background: rgba(255,255,255,0.05); border: none;
  color: rgba(232,226,214,0.7);
  font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: .04em;
  cursor: pointer; transition: background .15s, color .15s;
}
.plnew-libbtn svg { width: 13px; height: 13px; }
.plnew-libbtn:hover { background: rgba(255,255,255,0.1); color: #e8e2d6; }
.plnew-libbtn.active { background: var(--star); color: #16130d; }
.s-home-v3--light .plnew-libbtn { background: rgba(26,18,8,0.05); color: rgba(26,18,8,0.6); }
.s-home-v3--light .plnew-libbtn:hover { background: rgba(26,18,8,0.1); color: #1a1208; }
.s-home-v3--light .plnew-libbtn.active { background: var(--star); color: #16130d; }

/* — library browser: your playlists, then one opened to its tracks — */
.plnew-liblist {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px 2px; background: none; border: none;
  border-bottom: 1px solid rgba(255,255,255,0.05);
  cursor: pointer; text-align: left;
}
.plnew-liblist:hover { background: rgba(255,255,255,0.03); }
.plnew-lib-art {
  width: 34px; height: 34px; border-radius: 6px; flex-shrink: 0;
  background-size: cover; background-position: center;
  background-color: rgba(255,255,255,0.05);
}
.plnew-lib-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.plnew-lib-name {
  font-family: var(--font-main); font-size: 12px; font-weight: 700; color: #e8e2d6;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.plnew-lib-sub { font-family: var(--font-mono); font-size: 9px; color: rgba(232,226,214,0.38); }
.plnew-lib-chev { flex-shrink: 0; font-size: 16px; color: rgba(232,226,214,0.35); }
.plnew-libhead {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 2px 8px; border-bottom: 1px solid rgba(255,255,255,0.08); margin-bottom: 2px;
}
.plnew-libback {
  flex-shrink: 0; width: 22px; height: 22px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.07); border: none;
  color: rgba(232,226,214,0.8); font-size: 15px; line-height: 1; cursor: pointer;
}
.plnew-libback:hover { background: rgba(255,255,255,0.14); }
.plnew-libhead-t {
  flex: 1; min-width: 0;
  font-family: var(--font-main); font-size: 11.5px; font-weight: 700; color: #e8e2d6;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.plnew-liball {
  flex-shrink: 0; padding: 5px 10px; border-radius: 999px;
  background: var(--star); border: none; color: #16130d;
  font-family: var(--font-mono); font-size: 9px; font-weight: 700;
  cursor: pointer;
}
.plnew-liball:disabled {
  background: rgba(255,255,255,0.07); color: rgba(232,226,214,0.35); cursor: default;
}
.s-home-v3--light .plnew-liblist { border-bottom-color: rgba(26,18,8,0.08); }
.s-home-v3--light .plnew-liblist:hover { background: rgba(26,18,8,0.03); }
.s-home-v3--light .plnew-lib-name { color: #1a1208; }
.s-home-v3--light .plnew-lib-sub { color: rgba(26,18,8,0.42); }
.s-home-v3--light .plnew-lib-chev { color: rgba(26,18,8,0.35); }
.s-home-v3--light .plnew-libhead { border-bottom-color: rgba(26,18,8,0.12); }
.s-home-v3--light .plnew-libback { background: rgba(26,18,8,0.07); color: rgba(26,18,8,0.7); }
.s-home-v3--light .plnew-libhead-t { color: #1a1208; }
.s-home-v3--light .plnew-liball:disabled { background: rgba(26,18,8,0.07); color: rgba(26,18,8,0.35); }

/* — search — */
.plnew-searchbar {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: 8px;
  padding: 9px 12px; border-radius: 10px;
  background: rgba(255,255,255,0.05);
}
.plnew-searchbar svg { width: 15px; height: 15px; flex-shrink: 0; color: rgba(232,226,214,0.45); }
.plnew-searchbar input {
  flex: 1; min-width: 0; background: none; border: none; outline: none;
  font-family: var(--font-main); font-size: 12px; color: #e8e2d6;
}
.plnew-searchbar input::placeholder { color: rgba(232,226,214,0.32); }
.s-home-v3--light .plnew-searchbar { background: rgba(26,18,8,0.05); }
.s-home-v3--light .plnew-searchbar svg { color: rgba(26,18,8,0.45); }
.s-home-v3--light .plnew-searchbar input { color: #1a1208; }
.s-home-v3--light .plnew-searchbar input::placeholder { color: rgba(26,18,8,0.35); }

.plnew-results { max-height: 268px; overflow-y: auto; scrollbar-width: none; }
.plnew-results::-webkit-scrollbar { display: none; }

.plnew-empty {
  padding: 16px 4px; font-family: var(--font-mono); font-size: 10px;
  color: rgba(232,226,214,0.34); text-align: center;
}
.plnew-empty b { color: rgba(232,226,214,0.6); }
.s-home-v3--light .plnew-empty { color: rgba(26,18,8,0.4); }
.s-home-v3--light .plnew-empty b { color: rgba(26,18,8,0.65); }

/* The chosen list sits between the search and the results, so songs stack right
   under the search as you add them. It renders empty (no placeholder) until
   there's something in it — collapse the gap in that case. */
.plnew-chosen { margin-bottom: 10px; }
.plnew-chosen:empty { display: none; }

/* — the commit — sits at the TOP of the form, under the hero, so it's reachable
   without scrolling past however many songs have been added — */
.plnew-create {
  width: 100%; margin: 4px 0 16px; padding: 13px;
  border-radius: 12px; border: none;
  background: var(--star); color: #16130d;
  font-family: var(--font-main); font-size: 13px; font-weight: 800; letter-spacing: -0.1px;
  cursor: pointer; transition: opacity .15s, filter .15s;
}
.plnew-create:hover:not(:disabled) { filter: brightness(1.07); }
.plnew-create:disabled {
  background: rgba(255,255,255,0.07); color: rgba(232,226,214,0.3); cursor: not-allowed;
}
.s-home-v3--light .plnew-create:disabled { background: rgba(26,18,8,0.07); color: rgba(26,18,8,0.32); }

/* — delete — edit mode only. A quiet ghost under Save, red only in the type, so
   it reads as an option rather than a second call to action. It never acts on
   its own: it opens the confirm sheet below. */
/* One line: Save fills it, the trash is a SQUARE at its right end (2026-09-11). */
.plnew-actions { display: flex; align-items: stretch; gap: 8px; margin: 4px 0 16px; }
.plnew-actions .plnew-create { flex: 1; margin: 0; }
.plnew-delete {
  flex: 0 0 auto; width: 44px; margin: 0; padding: 0; align-self: stretch;
  border-radius: 12px; border: 1px solid rgba(232,86,86,0.32);
  background: transparent; color: #e85656;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; transition: background .15s, border-color .15s;
}
.plnew-delete svg { width: 17px; height: 17px; }
.plnew-delete:hover { background: rgba(232,86,86,0.08); border-color: rgba(232,86,86,0.5); }
.s-home-v3--light .plnew-delete { color: #c93b3b; border-color: rgba(201,59,59,0.3); }
.s-home-v3--light .plnew-delete:hover { background: rgba(201,59,59,0.07); }

/* — the confirm sheet — same material as the badges sheet (.plc-sheet): dark in
   both themes, sized to its content. Cancel is the wide, easy target; Delete is
   the filled red one, and it is on the right so a thumb resting on the sheet's
   natural spot lands on Cancel. */
.pldel-sheet {
  width: calc(100% - 20px); margin-bottom: 10px;
  background: #1c1c22; color: #e8e2d6;
  border-radius: 20px;
  padding: 9px 16px 16px;
  box-shadow: 0 -10px 34px rgba(0,0,0,0.5);
  transform: translateY(130%);
  transition: transform .3s cubic-bezier(.22,1,.36,1);
  font-family: var(--font-main);
}
.pldel-overlay.open .pldel-sheet { transform: translateY(0); }
.pldel-title {
  font-size: 15px; font-weight: 800; letter-spacing: -0.3px; margin-top: 6px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pldel-sub {
  margin: 5px 0 14px;
  font-family: var(--font-mono); font-size: 9px; line-height: 1.5;
  color: rgba(232,226,214,0.5);
}
.pldel-btns { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.pldel-btns button {
  padding: 12px; border-radius: 12px; border: 0; cursor: pointer;
  font-family: var(--font-main); font-size: 13px; font-weight: 800; letter-spacing: -0.1px;
  transition: filter .15s, background .15s;
}
.pldel-cancel { background: rgba(232,226,214,0.09); color: rgba(232,226,214,0.85); }
.pldel-cancel:hover { background: rgba(232,226,214,0.14); }
.pldel-go { background: #e85656; color: #fff; }
.pldel-go:hover { filter: brightness(1.08); }
@media (prefers-reduced-motion: reduce) { .pldel-sheet { transition: none; } }

.s-home-v3--light .plp-song { border-bottom-color: rgba(26,18,8,0.08); }
.s-home-v3--light .plp-song-title  { color: #1a1208; }
.s-home-v3--light .plp-song-album  { color: rgba(26,18,8,0.55); }
.s-home-v3--light .plp-song-artist { color: rgba(26,18,8,0.35); }
.s-home-v3--light .plp-song-num,
.s-home-v3--light .plp-song-dur { color: rgba(26,18,8,0.4); }
.s-home-v3--light .plp-song-rate { color: rgba(26,18,8,0.6); }

/* ============================================================
   SEARCH OVERLAY (#sd-search) — fullscreen takeover in the phone
   frame. Field + tabs pinned at top; suggestions + categorized
   results scroll below. Built by ensureSearchOverlay() in app.js.
   ============================================================ */
.sds-overlay {
  position: absolute; inset: 0; z-index: 210;
  background: #111116;
  visibility: hidden; opacity: 0;
  transition: opacity .2s ease, visibility 0s .2s;
  font-family: var(--font-main);
  display: flex; flex-direction: column;
}
.sds-overlay.open { visibility: visible; opacity: 1; transition: opacity .2s ease; }
/* THE NAV STAYS UP while you search (Eric, 2026-09-24): the screen wears
   `.is-searching` (openSearch), which lifts the bottom nav over the overlay;
   the results scroll clears it. A nav tap closes the search first. */
.s-home-v3.is-searching .v3-bottom-nav { z-index: 215; }
.s-home-v3.is-searching .sds-body { padding-bottom: 100px; }
.sds-panel {
  flex: 1; min-height: 0; display: flex; flex-direction: column;
  transform: translateY(10px);
  transition: transform .3s cubic-bezier(.22,1,.36,1);
}
.sds-overlay.open .sds-panel { transform: translateY(0); }

/* Top row — back arrow + search field */
.sds-top { display: flex; align-items: center; gap: 6px; padding: 14px 12px 9px; }
.sds-back {
  flex-shrink: 0; width: 32px; height: 32px; border: none; background: none;
  color: rgba(232,226,214,0.85); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
}
.sds-back svg { width: 22px; height: 22px; }
.sds-inputwrap {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 8px;
  height: 38px; padding: 0 12px; border-radius: 999px;
  background: rgba(255,255,255,0.06);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08);
}
.sds-input-ico { width: 15px; height: 15px; color: rgba(232,226,214,0.5); flex-shrink: 0; }
.sds-input {
  flex: 1; min-width: 0; border: none; background: none; outline: none;
  color: #e8e2d6; font-family: var(--font-main); font-size: 14px;
}
.sds-input::placeholder { color: rgba(232,226,214,0.4); }
.sds-clear {
  flex-shrink: 0; width: 20px; height: 20px; border-radius: 50%; border: none;
  background: rgba(232,226,214,0.2); color: #111116;
  font-size: 9px; line-height: 1; cursor: pointer; display: none;
  align-items: center; justify-content: center;
}
.sds-clear.show { display: inline-flex; }

/* Category tabs — funnel results into one type */
.sds-tabs {
  display: flex; gap: 2px; padding: 0 10px;
  border-bottom: 1px solid rgba(255,255,255,0.07);
}
.sds-tab {
  position: relative; border: none; background: none; cursor: pointer;
  padding: 9px 12px 11px;
  font-family: var(--font-main); font-size: 12.5px; font-weight: 600;
  color: rgba(232,226,214,0.45); transition: color .15s;
}
.sds-tab.active { color: #e8e2d6; }
.sds-tab.active::after {
  content: ''; position: absolute; left: 8px; right: 8px; bottom: -1px; height: 2px;
  background: var(--star); border-radius: 2px;
}

/* Scrolling body */
.sds-body { flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: none; padding: 4px 0 24px; }
.sds-body::-webkit-scrollbar { display: none; }

/* Autocomplete suggestions */
.sds-suggest:not(:empty) { padding: 4px 0; border-bottom: 1px solid rgba(255,255,255,0.06); }
.sds-sug {
  width: 100%; display: flex; align-items: center; gap: 12px;
  padding: 9px 16px; border: none; background: none; cursor: pointer;
  color: rgba(232,226,214,0.55); text-align: left;
}
.sds-sug:hover { background: rgba(255,255,255,0.04); }
.sds-sug > svg:first-child { width: 15px; height: 15px; flex-shrink: 0; color: rgba(232,226,214,0.38); }
.sds-sug-t { flex: 1; min-width: 0; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sds-sug-t b { color: #e8e2d6; font-weight: 700; }
.sds-sug-go { width: 14px; height: 14px; flex-shrink: 0; color: rgba(232,226,214,0.26); }

/* Result sections + rows */
.sds-sec { padding: 4px 0 6px; }
.sds-sec-hd {
  padding: 10px 16px 5px; font-family: var(--font-mono);
  font-size: 10px; font-weight: 700; letter-spacing: 0.8px; text-transform: uppercase;
  color: rgba(232,226,214,0.4);
}
.sds-row {
  width: 100%; display: flex; align-items: center; gap: 12px;
  padding: 7px 14px; border: none; background: none; cursor: pointer; text-align: left;
}
.sds-row:hover { background: rgba(255,255,255,0.04); }
.sds-thumb {
  width: 44px; height: 44px; border-radius: 7px; flex-shrink: 0;
  background: rgba(255,255,255,0.06) center/cover no-repeat;
  box-shadow: 0 2px 6px rgba(0,0,0,0.4);
}
.sds-thumb--round { border-radius: 50%; }
.sds-row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.sds-row-t { font-size: 13.5px; font-weight: 500; color: #e8e2d6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sds-row-t b { font-weight: 800; }
.sds-row-s { font-size: 10.5px; color: rgba(232,226,214,0.42); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sds-row-s b { font-weight: 700; color: rgba(232,226,214,0.6); }
.sds-row-dur { flex-shrink: 0; font-family: var(--font-mono); font-size: 10px; color: rgba(232,226,214,0.38); }
.sds-empty { padding: 44px 34px; text-align: center; font-size: 12.5px; color: rgba(232,226,214,0.4); line-height: 1.6; }

/* Zero state — trending searches chart + a rail of trending covers.
   Rendered by sdsZeroHtml() whenever the field is empty; the moment there's
   text, runSearch() overwrites .sds-results with matches instead. */
.sds-trend {
  width: 100%; display: flex; align-items: center; gap: 12px;
  padding: 7px 16px; border: none; background: none; cursor: pointer; text-align: left;
}
.sds-trend:hover { background: rgba(255,255,255,0.04); }
.sds-trend-n {
  flex-shrink: 0; width: 18px; text-align: center;
  font-family: var(--font-mono); font-size: 12px; font-weight: 700;
  color: rgba(232,226,214,0.3);
}
.sds-trend--top .sds-trend-n { color: var(--star); }   /* top 3 = chart gold */
.sds-trend-ct {
  flex-shrink: 0;
  font-family: var(--font-mono); font-size: 10px;
  color: rgba(232,226,214,0.34);
}
/* Horizontal cover rail, fading out at the right edge to signal overflow
   (same mask idiom as the playlists pill bar) */
.sds-rail {
  display: flex; gap: 10px;
  overflow-x: auto; scrollbar-width: none;
  padding: 4px 16px 6px;
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
}
.sds-rail::-webkit-scrollbar { display: none; }
.sds-tcard {
  flex-shrink: 0; width: 96px;
  display: flex; flex-direction: column; gap: 1px;
  padding: 0; border: none; background: none; cursor: pointer; text-align: left;
}
.sds-tcard-art {
  width: 96px; height: 96px; border-radius: 8px; margin-bottom: 5px;
  background: rgba(255,255,255,0.06) center/cover no-repeat;
  box-shadow: 0 4px 12px rgba(0,0,0,0.45);
  transition: transform .16s;
}
.sds-tcard:hover .sds-tcard-art { transform: translateY(-2px); }
/* album = regular, artist = bold — the app-wide album/artist convention */
.sds-tcard-t {
  font-size: 11px; font-weight: 400; color: #e8e2d6;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sds-tcard-s {
  font-size: 9px; font-weight: 700; color: rgba(232,226,214,0.45);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* Search overlay — light theme */
.sds-overlay--light { background: #f0ece3; }
.sds-overlay--light .sds-back { color: rgba(26,18,8,0.75); }
.sds-overlay--light .sds-inputwrap { background: rgba(26,18,8,0.05); box-shadow: inset 0 0 0 1px rgba(26,18,8,0.1); }
.sds-overlay--light .sds-input-ico { color: rgba(26,18,8,0.45); }
.sds-overlay--light .sds-input { color: #1a1208; }
.sds-overlay--light .sds-input::placeholder { color: rgba(26,18,8,0.4); }
.sds-overlay--light .sds-clear { background: rgba(26,18,8,0.28); color: #f0ece3; }
.sds-overlay--light .sds-tabs { border-bottom-color: rgba(26,18,8,0.1); }
.sds-overlay--light .sds-tab { color: rgba(26,18,8,0.45); }
.sds-overlay--light .sds-tab.active { color: #1a1208; }
.sds-overlay--light .sds-suggest:not(:empty) { border-bottom-color: rgba(26,18,8,0.08); }
.sds-overlay--light .sds-sug { color: rgba(26,18,8,0.55); }
.sds-overlay--light .sds-sug:hover { background: rgba(26,18,8,0.04); }
.sds-overlay--light .sds-sug > svg:first-child { color: rgba(26,18,8,0.4); }
.sds-overlay--light .sds-sug-t b { color: #1a1208; }
.sds-overlay--light .sds-sug-go { color: rgba(26,18,8,0.26); }
.sds-overlay--light .sds-sec-hd { color: rgba(26,18,8,0.45); }
.sds-overlay--light .sds-row:hover { background: rgba(26,18,8,0.04); }
.sds-overlay--light .sds-thumb { background-color: rgba(26,18,8,0.06); box-shadow: 0 2px 6px rgba(30,20,10,0.14); }
.sds-overlay--light .sds-row-t { color: #1a1208; }
.sds-overlay--light .sds-row-s { color: rgba(26,18,8,0.45); }
.sds-overlay--light .sds-row-s b { color: rgba(26,18,8,0.65); }
.sds-overlay--light .sds-row-dur { color: rgba(26,18,8,0.4); }
.sds-overlay--light .sds-empty { color: rgba(26,18,8,0.42); }
.sds-overlay--light .sds-trend:hover { background: rgba(26,18,8,0.04); }
.sds-overlay--light .sds-trend-n { color: rgba(26,18,8,0.3); }
.sds-overlay--light .sds-trend--top .sds-trend-n { color: #b06a2a; }
.sds-overlay--light .sds-trend-ct { color: rgba(26,18,8,0.38); }
.sds-overlay--light .sds-tcard-art { background-color: rgba(26,18,8,0.06); box-shadow: 0 4px 12px rgba(30,20,10,0.16); }
.sds-overlay--light .sds-tcard-t { color: #1a1208; }
.sds-overlay--light .sds-tcard-s { color: rgba(26,18,8,0.5); }

/* ============================================================
   HOME CD POPUP (.v3-cd-menu) — compact preview + streaming menu
   anchored above the CD, replacing the fullscreen stream sheet.
   Reuses .wall2-menu (container) + .v3-stream-preview (preview row)
   + .plp-plat-item / .plp-plat-ico (platform rows). Placed after
   .wall2-menu so these positioning overrides win. Built in the home
   markup (screens.js); toggled by onCdTap.
   ============================================================ */
.v3-cd-menu {
  position: absolute;
  right: 2.7%; bottom: 13%; left: auto; top: auto;
  z-index: 60; min-width: 152px;
}
.s-home-v3--left .v3-cd-menu { left: 2.7%; right: auto; }
/* Compact the reused preview row for the small popup */
.v3-cd-menu .v3-stream-preview {
  padding: 8px 10px; margin: 2px 0 4px; border-radius: 10px;
  font-size: 12px; gap: 9px;
}
.v3-cd-menu .v3-stream-preview-ico { width: 22px; height: 22px; }
.v3-cd-menu .v3-stream-preview-ico svg { width: 11px; height: 11px; margin-left: 1px; }
.v3-cd-menu .v3-stream-preview.playing .v3-stream-preview-ico::before,
.v3-cd-menu .v3-stream-preview.playing .v3-stream-preview-ico::after { width: 2.5px; height: 9px; }
.v3-cd-menu .v3-stream-preview-dur { font-size: 9px; }
.v3-cd-menu-sep { height: 1px; margin: 3px 6px 4px; background: rgba(255,255,255,0.08); }
.s-home-v3--light .v3-cd-menu-sep { background: rgba(26,18,8,0.1); }

/* ============================================================
   ONBOARDING WIZARD  (multi-step, themed via sd-theme-* tokens)
   ============================================================ */
/* Two tokens the whole wizard's mechanism reads from. The belt fills WHITE and
   its slack is grey — but white on cream is invisible, so the light variant
   swaps the fill for ink and keeps the same grey. */
.s-onboarding {
  position: relative;
  /* ⚠ EXACTLY the frame, not min-height. `.app-screen` is `min-height: 100%`,
     which let a tall step grow the screen and carry the footer off the bottom
     of the phone — Continue below the fold, on the one screen where it is
     the only way forward. Pinned, the stage (`flex: 1; min-height: 0;
     overflow-y: auto`) does the scrolling and the footer stays put. */
  height: 100%;
  --ob-fill:  #ffffff;
  --ob-track: rgba(255,255,255,0.14);
  --ob-grid:  8px;
}
.s-onboarding.sd-theme-light { --ob-fill: #1a1208; --ob-track: rgba(26,18,8,0.13); }

/* ── The progress BELT ──────────────────────────────────────────────────────
   23px off the top (it was 8px — lowered 15px once the phone chrome stopped
   being a black strip and the belt found itself hard against the status bar),
   8px off both sides, 12px thick, stadium caps. It is the
   same object as the handle rig on step 0 — a belt wrapped round two pulleys —
   drawn at the smallest scale the system has, so the page reads as one machine.
   ⚠ It used to be a 4px hairline sharing a flex row with the back button and the
   counter. Those moved to `.ob-rail` below so the belt could run the full width
   on the 8px margin; don't fold them back in. */
.ob-top { padding: 23px 8px 0; flex-shrink: 0; }
.ob-top .ob-progress {
  height: 12px; margin: 0; border-radius: 99px;
  background: var(--ob-track); overflow: hidden;
}
.ob-prog-bar {
  height: 100%; width: 12.5%; border-radius: 99px;
  background: var(--ob-fill);
  transition: width .45s cubic-bezier(.4,0,.2,1);
}

/* Back + counter, on the 24px content column the panels use. */
.ob-rail {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 24px 0; flex-shrink: 0;
}

.ob-back {
  width: 32px; height: 32px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1.5px solid var(--border); border-radius: 50%; cursor: pointer;
  background: none; color: var(--text);
  transition: border-color .15s, background .15s;
}
.ob-back:hover { border-color: var(--text2); background: var(--surface); }
.ob-back svg { width: 16px; height: 16px; }
.ob-stepcount {
  font-size: 11px; font-family: var(--font-mono); color: var(--text2);
  flex-shrink: 0; letter-spacing: 1px; font-variant-numeric: tabular-nums;
}
.ob-stepcount i { font-style: normal; color: var(--text3); margin: 0 2px; }

/* Sliding stage + panels */
.ob-stage { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; position: relative; }
/* ⚠ THE WALL RUNS UNDER THE BUTTONS. On the scrolling steps (`obSyncDock` sets
   `.ob-stage-fade`) the stage reaches the bottom edge of the phone and
   `.ob-foot` — dock + footer — floats over it; the stage's MASK fades the
   wall across exactly the foot's height (`--ob-foot`, measured), opaque just
   above it and gone at the bottom edge. So the cards dissolve INTO the picks
   and the buttons and reach obscurity at the very bottom — not at a hard line
   a quarter of the way up the phone, which is what a stage that stopped
   above a stacked dock and footer produced. A mask on the scroller, so it is
   the screen's own colour whatever the theme.
   ⚠ The other steps keep the stacked layout: step 7's profile has a note that
   sits on the footer by `margin-top: auto`, and the wheel is sized to fit. */
.ob-foot { flex-shrink: 0; }
.ob-stage-fade .ob-foot { position: absolute; left: 0; right: 0; bottom: 0; z-index: 3; }
.ob-stage-fade .ob-stage {
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - var(--ob-foot, 80px) - 10px), transparent calc(100% - 12px));
          mask-image: linear-gradient(to bottom, #000 calc(100% - var(--ob-foot, 80px) - 10px), transparent calc(100% - 12px));
}
/* So the LAST row can scroll up out of the fade and be read. */
.ob-stage-fade .ob-panel { padding-bottom: calc(var(--ob-foot, 80px) + 16px); }
.ob-panel { display: none; padding: 8px 24px 24px; animation: obFade .32s ease; }
.ob-panel--on { display: block; }
.ob-panel--profile { display: none; padding: 0; }
.ob-panel--profile.ob-panel--on { display: flex; flex-direction: column; min-height: 100%; }
@keyframes obFade { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

.ob-h { margin: 4px 0 20px; }
.ob-h .ob-title { font-size: 24px; font-weight: 800; letter-spacing: -0.5px; margin-bottom: 6px; font-family: var(--font-head); }
.ob-h .ob-sub { font-size: 13.5px; color: var(--text2); line-height: 1.45; }
.ob-h .ob-sub b { color: var(--text); font-weight: 700; }

/* ══════════════════════════════════════════════════════════════
   THE MIX DIAL — Pro's multi-genre shelf (the logo as the input)
   ══════════════════════════════════════════════════════════════
   Built by `mixDialSvg` in app.js; the geometry is all there, so this file only
   says what the parts are MADE of. The one rule to keep: an unpicked genre is a
   HOLE (an outline, a gap in the plate) and a picked one is a PULLEY (solid,
   heavier, on the belt). The whole read depends on those two being obviously
   different objects, not two states of the same dot.
   (It briefly lived in onboarding — see the note on `mixDialBuild`.) */
/* ── The mix dial, INSIDE the bento's album cell ─────────────────────────
   ⚠ This was a bottom sheet (`.mix-overlay` / `.mix-sheet`) and must not go
   back to being one. The dial chooses what the bento shows, so it belongs in
   the bento: the ring covers `.v3-album` — the same square the shelf wheel
   covers — and the commit sits in `.v3-blue` right under it. A window in front
   of the object hid the thing you were deciding about.
   Same three layers as `.shop-pick`, for the reasons documented there: a
   PAINTED blur of the cover, a wash over it, then the content. */
.mix-inline {
  position: absolute; inset: 0; z-index: 5;
  overflow: hidden; border-radius: inherit;
  opacity: 0; pointer-events: none;
  transition: opacity .2s;
  /* ⚠ Carries its own DARK tokens, exactly as the sheet did. The dial draws in
     `var(--text)` and friends, and on a light home it would otherwise come out
     as near-black ink on a dark, tinted cover — invisible. */
  --bg: #1c1c22; --surface: #24242d; --surface2: #2b2b35;
  --border: rgba(232,226,214,0.16);
  --text: #e8e2d6; --text2: rgba(232,226,214,0.62); --text3: rgba(232,226,214,0.30);
  /* ⚠ Tinted by the ALBUM, not by gold — the same rule the shelf wheel follows.
     `--v3-accent` is extracted from the cover by `applyAlbumColors` and set on
     the screen, so it inherits down here and the belt re-tints with the art. */
  --accent: var(--v3-accent, #e6b866);
}
.s-home-v3--mixing .mix-inline { opacity: 1; pointer-events: auto; }
/* `scale` overscans so the blur's own soft edge is pushed outside the box's
   `overflow: hidden` instead of showing as a pale rim inside it. */
.mix-inline-bg {
  position: absolute; inset: 0;
  background-size: cover; background-position: center;
  filter: blur(11px); transform: scale(1.14);
}
/* A LAYER, not a background on `.mix-inline`: a parent's own background paints
   beneath its children, i.e. under the very thing it is meant to darken. */
.mix-inline-tint { position: absolute; inset: 0; background: rgba(10,9,8,.62); }
/* Fills the cell. The viewBox is square and the album cell is 291×289 at the
   393px frame, so `meet` letterboxes by about a pixel — which is why the label
   budget in `DIAL` is measured against 158, not 160. */
.mix-inline .ob-dial {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  touch-action: manipulation;   /* taps, not a drag — nothing to claim */
}

/* The belt. Never filled — it is a strap, and a fill would make it a blob the
   moment it wraps three or more wheels. */
.ob-dial-belt {
  fill: none;
  stroke: var(--accent);
  stroke-width: 3;
  stroke-linejoin: round;
  opacity: .92;
  transition: stroke .22s;
}

/* ⚠ The ring no longer TURNS, so it has no transform-origin and no spring — the
   gesture is a tap. It stays a group because the belt has to live inside it
   with the holes it wraps. */

/* A genre's tap target: the 18° wedge it owns, hole and label inside it.
   Invisible, and the only thing in the group that takes a pointer. */
.ob-hole { cursor: pointer; }
.ob-hole-hit { fill: transparent; }
.ob-hole-c {
  fill: var(--bg);
  stroke: var(--text3);
  stroke-width: 1.5;
  pointer-events: none;
  transition: r .22s cubic-bezier(.2,.9,.25,1), fill .22s, stroke .22s;
}
/* A pulley: solid, and it now belongs to the belt. */
.ob-hole.is-on .ob-hole-c {
  fill: var(--accent);
  stroke: var(--accent);
}
/* ⚠ EVERY label, always. An earlier cut faded them in only once a genre was
   picked — fine with a cursor, useless on a phone, where there is no hover and
   you would be choosing blind. They fit because they RADIATE (see
   `mixDialSvg`): radial text costs ANGLE, not arc, so a label subtends about 4°
   where each genre owns 18°. Tangential text is what could never fit.

   ⚠ ROBOTO FLEX, and specifically for its `wdth` axis — the same variable face
   the album title uses (`.v3-blue-info-row`), already loaded in index.html with
   all three axes at full range (opsz 8..144, wdth 25..151, wght 100..900).
   The labels are the one thing on this dial with no room to spare: they radiate
   outward from r=90 into a viewBox that ends at 160, so every genre gets the
   same ~68 units whatever its name is. Half of them are short — Pop, Jazz, R&B,
   Folk, Soul — and a few are not: Alternative, Electronic. A fixed face has to
   be small enough for the LONGEST name, which throws away the space the short
   ones aren't using. A variable one is set big and CONDENSED PER LABEL by
   `mixDialFitLabels`, so only the names that actually overflow pay for it, and
   they pay in width rather than in size. That is what took this off SUSE Mono,
   which has no width axis to give.
   ⚠ `opsz` is PINNED to 9 rather than left on `auto`. The SVG is scaled by its
   viewBox (~0.9px per unit), so the used font-size `auto` would read is the
   unscaled 12 — an optical size for text half again as big as what lands.
   ⚠ The axes arrive as CUSTOM PROPERTIES because `font-variation-settings`
   replaces the whole tuple: a plain `font-weight` on the picked state is
   ignored while this property is set, and the JS fit pass would wipe the weight
   if it wrote the tuple itself. One variable each, so they compose. */
.ob-hole-t {
  --lwdth: 100;   /* per label, set by mixDialFitLabels() */
  --lwght: 420;
  font-family: 'Roboto Flex', var(--font-main);
  /* ⚠ 13 units, set for the SHORT names and condensed down for the long ones —
     that is the whole point of using a variable face here. A fixed face had to
     sit at 10 so "Alternative" would fit, which left Pop, Jazz and R&B three
     sizes smaller than they needed to be. This is the one dial to turn if the
     labels read too small or too tight: raise it and more names get condensed,
     lower it and fewer do.
     ⚠ 14 rather than 13 because the long names now sit in the CORNERS
     (`dialSeating`), where a label has ~88 units instead of ~70 — the extra
     room went into size for everybody rather than slack for a few.
     ⚠ `--lsize` is the LAST of the three fit steps (see mixDialFitLabels) and
     is 1 unless a name cannot be condensed into its slice. */
  font-size: calc(14px * var(--lsize, 1));
  font-variation-settings: 'opsz' 9, 'wdth' var(--lwdth), 'wght' var(--lwght);
  letter-spacing: .005em;
  fill: var(--text2);
  dominant-baseline: middle;
  pointer-events: none;
  transition: fill .22s;
}
.ob-hole.is-on .ob-hole-t { --lwght: 680; fill: var(--text); }

/* The hub: the record. Groove + spindle hole, and nothing else — the readout it
   used to carry moved to the info box, where there is room to say it in words.
   ⚠ Not a target. Every tap belongs to a wedge, and a disc swallowing the
   middle would be a dead spot in the centre of the dial. */
.ob-dial-hub { pointer-events: none; }
.ob-hub-disc   { fill: var(--surface2); stroke: var(--border); stroke-width: 1; }
.ob-hub-groove { fill: none; stroke: var(--border); stroke-width: 1; opacity: .7; }
.ob-hub-hole   { fill: var(--bg); }

/* ── The commit, in the INFO BOX ─────────────────────────────────────────
   The box that normally tells you what this album is tells you what the mix is
   instead. ⚠ It covers `.v3-blue` COMPLETELY and stops the bubble in JS, which
   is also what takes that box's tap-to-open-album off the table while you are
   choosing — otherwise committing a mix could navigate away from the screen it
   was committed for. */
.v3-blue-mix {
  position: absolute; inset: 0; z-index: 3;
  display: none;
  /* A column (Eric, 2026-09-18): the PILLS take the top of the box at its full
     width, and the count + New deck sit on the floor — where every other line
     in this bento sits. The pills' area is whatever the row leaves, and
     `mixFitChips` shrinks them to stay inside it. */
  flex-direction: column; justify-content: space-between; gap: 5px;
  padding: 9px 12px 10px;
}
.v3-blue-mix-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; flex-shrink: 0; }
.s-home-v3--mixing .v3-blue-mix { display: flex; }
/* Everything the box normally says is about ONE album, and while you are
   building a mix there isn't one yet. `visibility` rather than `display` so the
   box keeps the height its own content gives it and nothing below shifts. */
.s-home-v3--mixing .v3-blue > *:not(.v3-blue-mix) { visibility: hidden; }
.v3-blue-mix-n {
  min-width: 0; padding-bottom: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-family: var(--font-head); font-size: 15px; font-weight: 800;
  letter-spacing: -.2px; color: var(--text);
}
/* ── What you have added so far ───────────────────────────────────────────
   ⚠ This replaced a count ("3 genres"). With picks gathered across several
   rings, the ring in front of you can only ever show one main's worth, so this
   row is the only place the whole mix is visible — and each chip removes its
   own pick, so the thing you can see is the thing you can undo.
   ⚠ It WRAPS and never scrolls (it was a sideways scroller until 2026-09-18).
   `flex: 1; min-height: 0` makes it exactly the space above the bottom row, and
   `overflow: hidden` is only the safety net — `mixFitChips` (app.js) steps
   `--chip` down until the rows fit, then folds the tail into a "+N". */
.v3-blue-mix-picks {
  flex: 1 1 0; min-height: 0;
  display: flex; flex-wrap: wrap; align-content: flex-start; align-items: flex-start;
  gap: calc(var(--chip, 9px) * .42);
  overflow: hidden;
}
/* Nothing picked yet — an instruction, in the mono the app uses for readouts.
   ⚠ It was the shelf wheel's serif italic, borrowed as an "annotation" voice.
   That voice is for a COUNT sitting beside something it describes; this line
   is a direction telling you what to do next, and set in italic serif it read
   as a caption on the dial rather than as the dial talking to you. The chips
   that replace it are mono too, so the row no longer changes typeface the
   moment you pick something. */
.v3-blue-mix-picks.is-empty {
  font-family: var(--font-mono);
  font-size: 9.5px; letter-spacing: .3px; color: var(--text2); white-space: nowrap;
}
/* ⚠ EVERY measure is in em off `--chip`, so the one property scales the pill
   whole — type, padding, gap and the × together. */
.v3-mix-chip {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: .33em;
  border: 0; border-radius: 999px; padding: .33em .78em .33em .89em;
  background: color-mix(in srgb, var(--v3-accent, #e6b866) 22%, transparent);
  color: var(--text);
  font-family: var(--font-mono); font-size: var(--chip, 9px); line-height: 1.25; letter-spacing: .03em;
  white-space: nowrap; cursor: pointer;
}
.v3-mix-chip[hidden] { display: none; }
.v3-mix-more { cursor: default; opacity: .7; }
.v3-mix-chip i { font-style: normal; opacity: .5; font-size: 1.1em; }
.v3-mix-chip:hover i { opacity: 1; }
.v3-blue-mix-go {
  flex-shrink: 0; border: 0; border-radius: 999px;
  padding: 9px 15px;
  background: var(--v3-accent, #e6b866); color: #1a1208;
  font-family: var(--font-head); font-size: 12px; font-weight: 800;
  cursor: pointer; transition: opacity .18s;
}
/* ⚠ ONE genre is a valid mix — there is no minimum pick count, and don't put
   one back: the dial is an alternative way to cut the catalogue, not a
   combining puzzle. The only thing that disables this is the floor the QUEUE
   needs (`commitShelf` refuses a shelf under two albums), and the label names
   which case you are in rather than failing silently. */
.v3-blue-mix-go:disabled { opacity: .34; cursor: default; }

/* ⚠ A MAIN is a door, a SUB is a switch, and they must not look alike. A lit
   main only means "there is something picked in here" — tapping it takes you
   in, it does not toggle — so it gets a ring rather than the solid fill a real
   pick gets. The "All X" hole at the head of a sub ring IS a pick and keeps the
   solid one. */
.ob-hole--main.is-on .ob-hole-c { fill: none; stroke-width: 3.2; }

/* ── Stepping between rings: IN, THEN OUT. Never both. ────────────────────
   The genres shrink into the record and vanish. Only once they are gone do the
   new ones come back out of it. One ring on screen at a time, always.

   ⚠ THE OVERLAP WAS THE WHOLE PROBLEM. Four versions failed before this and
   every one had the two rings alive together for part of the move — an outward
   zoom, a one-notch tick, a collapse with a .2s head start, and a scale-2.8
   "nyoom" with a .14s handover. However they were eased, the result was sixteen
   names sitting on top of another sixteen, which reads as a stacking bug rather
   than as motion. There is no timing that makes two rings of text legible at
   once, so they are SEQUENCED with a real gap: out by .2s, in from .22s.

   ⚠ No perspective, no translateZ, no rotation. Depth is scale down to the size
   of the record and back, which is enough because the record is a fixed anchor
   the eye is already on.

   ⚠ Both dials share the box for the length of the exit, so the outgoing one is
   out of flow — and `mixDialRender` deletes any ghost still in flight before
   starting a new one, because the removal timer alone loses to fast taps. */
.mix-inline .ob-dial {
  transform-origin: 50% 50%;
  /* Coming back out of the record — and only after the old ring has gone. */
  transition: transform .26s cubic-bezier(.16, .84, .3, 1) .22s,
              opacity .12s ease .22s;
}
.mix-inline .ob-dial.is-ghost {
  /* Going into the record. ⚠ THE FADE IS HELD, THEN QUICK. Fading across the
     whole shrink (.16s against a .2s move) left the ring half gone before it
     had visibly moved at all, so it read as a plain dissolve and the trip into
     the record was invisible — which is exactly what it was reported as. It
     stays solid for the first half now and winks out at the small end. */
  transition: transform .2s cubic-bezier(.5, 0, .82, .4),
              opacity .1s ease .1s;
}

/* ⚠ THE START STATE MUST NOT TRANSITION, and that stopped being optional the
   moment the arrival took a .22s delay. `mixDialRender` sets the small state,
   forces a reflow, then clears it. With a delay in force, adding the class
   starts a transition that is still inside its OWN delay when the class comes
   off, and the reversal resolves to a ZERO-duration transform — so the new
   ring just appeared at full size, no growth at all. Measured with
   getAnimations(): "transform 0ms +220ms". Only the ENTER states take this;
   the ghost has to animate.
   ⚠ SCOPED WITH .mix-inline, and it has to be. Unscoped this is (0,1,0)
   against the (0,2,0) of the rule it is overriding, so it lost the cascade
   outright and the arrival stayed on the delayed transition — reading
   identically to having made no change at all. */
.mix-inline .ob-dial--enter-in,
.mix-inline .ob-dial--enter-out { transition: none; }

/* The rings still go into the record and come back out of it (.18 is about the
   hub's own radius), but they now TURN as they do (Eric, 2026-09-18): going
   INTO a genre everything turns to the RIGHT — the old ring winds clockwise
   down into the record, the new one unwinds clockwise out of it — and coming
   back OUT it all turns to the LEFT. Same travel both ways, mirrored, so the
   direction itself says deeper / shallower.
   ⚠ This WAS one state for all four cases: directional variants had been tried
   (opposite spins, opposite Z) and read as inconsistency. What is different
   here is that both rings of a step turn the SAME way — it is one motion
   handed from ring to ring, not two rings disagreeing.
   ⚠ Keep it small. The labels are readable through 50°; much more and the
   arriving ring is a smear for most of its .26s. The record does not turn —
   it is still the fixed thing the rings are read against — and the held hole
   (`.ob-dial--stay`) stands still through all of it, as before. */
.ob-dial--leave-in  { transform: rotate(50deg)  scale(.18); opacity: 0; }   /* in: old ring winds right, into the record */
.ob-dial--enter-in  { transform: rotate(-50deg) scale(.18); opacity: 0; }   /* in: new ring arrives still turning right */
.ob-dial--leave-out { transform: rotate(-50deg) scale(.18); opacity: 0; }   /* out: mirrored — everything turns left */
.ob-dial--enter-out { transform: rotate(50deg)  scale(.18); opacity: 0; }

/* The held hole (see mixStayHole in app.js): a copy of the genre you tapped,
   on its own layer, standing still while the rings swap under it. The real
   hole is `is-held` (hidden) on both rings for the duration. ⚠ No transition
   and no transform on this layer — that is its whole job. */
.mix-inline .ob-dial--stay {
  transition: none !important; transform: none !important; opacity: 1 !important;
  pointer-events: none; z-index: 3;
}
.ob-hole.is-held { visibility: hidden; }

@media (prefers-reduced-motion: reduce) {
  .mix-inline .ob-dial, .mix-inline .ob-dial.is-ghost { transition: opacity .15s ease; }
  .mix-inline .ob-dial--leave-in, .mix-inline .ob-dial--enter-in,
  .mix-inline .ob-dial--leave-out, .mix-inline .ob-dial--enter-out { transform: none; }
}

/* ── Opening: the dial spins into place ──────────────────────────────────
   The ring and the record arrive together, turning up out of the middle of the
   cover. It is the only time the two move as one object — stepping between
   rings deliberately holds the record still, because it is the fixed thing the
   rings are read against, and on arrival there is nothing to be fixed against
   yet.
   ⚠️ A KEYFRAME, not a transition. Both svgs are fresh elements at this point
   and a transition needs a previous value to run from, so it would simply
   never fire — the same reason `.ob-hub-t` uses one.
   ⚠️ The turn is anticlockwise into place and lands with a long decelerating
   tail: a record reaching speed, not a spinner. Keep it under one turn — past
   that the labels blur into an unreadable band and it reads as loading. */
@keyframes mixSpinIn {
  from { transform: rotate(-34deg) scale(.58); opacity: 0; }
  55%  { opacity: 1; }
  to   { transform: rotate(0deg) scale(1); opacity: 1; }
}
.mix-inline.is-opening .ob-dial,
.mix-inline.is-opening .ob-hub-svg {
  transform-origin: 50% 50%;
  animation: mixSpinIn .5s cubic-bezier(.17, .84, .3, 1) both;
}
@media (prefers-reduced-motion: reduce) {
  .mix-inline.is-opening .ob-dial,
  .mix-inline.is-opening .ob-hub-svg { animation: none; }
}

/* ── The record ───────────────────────────────────────────────────────────
   Its own layer, above the flying ring and never animated with it. */
.ob-hub-svg {
  position: absolute; inset: 0; width: 100%; height: 100%;
  pointer-events: none; z-index: 2;
}
.ob-hub-disc, .ob-hub-groove { transition: stroke .28s ease, stroke-width .28s ease, opacity .28s ease; }
/* ⚠ Lit whenever the mix has anything in it — the record is the only part of
   the dial that is always on screen, so it is where "you have something"
   belongs. Album accent, like the rest of the dial. */
.ob-hub-svg.is-lit .ob-hub-groove {
  stroke: var(--accent); stroke-width: 2.4; opacity: 1;
  filter: drop-shadow(0 0 5px color-mix(in srgb, var(--accent) 70%, transparent));
}
.ob-hub-svg.is-lit .ob-hub-disc {
  stroke: color-mix(in srgb, var(--accent) 55%, var(--border));
}

/* ── The main you are inside, still on the ring ──────────────────────────
   The genre you chose is IN the ring it opened, lit, in the hole it already
   occupied — so the one thing that does not move between the two rings is the
   name you pressed.

   ⚠ THIS REPLACED A FLIGHT. The chosen label used to be held lit for 190ms and
   then travel into the middle of the disc to become the record's label. It
   answered the same question and was rejected for how it read — a name
   crawling into the centre is unsettling to watch — and it charged 490ms for
   an answer that costs nothing when it simply waits for you at the other end.

   ⚠ AN OUTLINE, NOT A FILL, because it is not necessarily picked. Here the
   ring means "this is where you are"; the solid fill means "this is in your
   mix". Tapping it does pick the whole main, and .ob-hole.is-on is (0,3,0)
   against this (0,2,0), so it takes the solid fill on top and the two states
   read as one control rather than two. ⚠ Do NOT reuse .ob-hole--main.is-on's
   ring for it: that ring already means "something in here is picked" on the
   ring above, and the same mark cannot mean two things one step apart. */
.ob-hole--all .ob-hole-c { stroke: var(--accent); stroke-width: 2.4; }
.ob-hole--all .ob-hole-t { fill: var(--text); --lwght: 700; }

/* The record's label arrives WITH the ring, not ahead of it. */
.ob-hub-t {
  font-family: 'Roboto Flex', var(--font-main);
  font-size: calc(9px * var(--lsize, 1));
  font-variation-settings: 'wdth' var(--lwdth, 100), 'wght' 600;
  letter-spacing: .02em; fill: var(--text2);
  text-anchor: middle; dominant-baseline: central;
  transition: opacity .22s ease .2s;
}
/* ⚠ An ANIMATION, not a transition. The record is rebuilt by `mixHubSync` on
   every step, so the label is a brand-new element each time and has no previous
   value to transition from — a transition simply never fires. A keyframe on
   insert does, and the delay lets the ring get moving first so the genre name
   does not announce itself into an empty dial. */
@keyframes obHubIn { from { opacity: 0; } to { opacity: 1; } }
.ob-hub-t { animation: obHubIn .28s ease .12s both; }




/* ══════════════════════════════════════════════════════════════════════════
   0 | THE PLATE — the layout drawn as construction geometry
   ══════════════════════════════════════════════════════════════════════════
   All the numbers live in `OB_PLATE` / `OB_RIG` in screens.js; this file only
   says what the layers are made of. Three of them, faintest first, plus holes:

     .obp-rule   the grid the circles guide — the two verticals through the top
                 corner hubs. x=44 is the text column AND the deck pulley's axis
     .obp-cons   the construction circles whole, not just the arcs they lent
     .obp-edge   the cut edge itself, the only line at full weight
     .obp-bolt   the holes, at the convex hubs only

   ⚠ ONE circle per corner. There was an offset "ghost" layer that drew each of
     them again 8px outward, and it is the single thing that made the screen
     look busy: it doubled every line, and the ghosts collided with the
     neighbours their originals cleared. With one layer no two circles here
     overlap — that is the property to re-check if you move a vertex.

   ⚠ The panel breaks its own 24px padding (`margin: 0 -16px`) so the plate is
     369 wide on a 385 screen — 8px margins, the same as the progress belt, and
     one viewBox unit per pixel. Change the padding and this has to follow.
   ⚠ Nothing here is fill. A plate you can see through is what lets the circles
     cross the edge; give it a background and the whole idea collapses into a
     card with rounded corners. */
/* The plate is shorter than the stage, so centre it rather than letting a
   block of dead air pile up under it. */
.ob-panel--user.ob-panel--on {
  display: flex; flex-direction: column; justify-content: center;
  min-height: 100%; padding-top: 0;
}
/* ⚠ NO `width: 100%` here. As a flex item that resolves against the padded
   337px column and the negative margins merely overflow it; auto width plus the
   default stretch is what actually makes the box 337 + 32 = 369. */
.ob-plate { position: relative; flex: 0 0 auto; aspect-ratio: 369 / 464; margin: 0 -16px; }
.obp { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }

.obp-rule  line   { stroke: var(--text2); stroke-width: .5;  opacity: .16; }
.obp-cons  circle { fill: none; stroke: var(--text2); stroke-width: .6;  opacity: .38; }
.obp-bolt  circle { fill: none; stroke: var(--text2); stroke-width: 1;   opacity: .6; }
.obp-edge         { fill: none; stroke: var(--text2); stroke-width: 1.25; opacity: .75; }

/* The field, one level down the same idea: a belt over two EQUAL pulleys, so
   both its runs are exactly horizontal and its edges are grid lines too. */
/* ⚠ NOTHING in the field is filled either — not the belt, not the wheels.
   Filling the belt made the input an opaque slab that cut the construction
   circles and the x=325 rule in half; the drawing stopped passing behind the UI
   and the whole thing read as a rounded card sitting on a diagram. */
.obp-belt   { fill: none; stroke: var(--text2); stroke-width: 1.5; opacity: .8; transition: stroke .15s, opacity .15s; }
.obp-wheel  { fill: none; stroke: var(--text2); stroke-width: .8; opacity: .5; }
.obp-groove { fill: none; stroke: var(--text2); stroke-width: .8; opacity: .28; }
.obp-hub    { fill: none; stroke: var(--text2); stroke-width: 1; opacity: .6; transition: fill .22s, stroke .22s; }
.ob-plate:focus-within .obp-belt { stroke: var(--accent); opacity: 1; }

/* Valid handle → the belt tightens (goes to full weight) and the idler's bolt
   seats. ⚠ The idler used to be a three-hole motor that spun on valid. It read
   as a second, louder logo inside the field and fought the plate it sits on;
   two matching pulleys is the calmer, more correct machine. */
.ob-plate--ok .obp-belt { stroke: var(--ob-fill); opacity: 1; }
.ob-plate--ok .obp-hub  { fill: var(--ob-fill); stroke: var(--ob-fill); }

/* ── Content, positioned in the viewBox's own percentages so it rides the
      geometry at any width. Left edge 44/369 = the x=44 rule. ── */
/* ⚠ EVERY TOP HERE IS A % OF THE 464-TALL PLATE, and they were re-derived when
   the rig replaced the two-pulley field. The belt now owns y 213 → 383 (top run
   to the bottom of the r50 wheel), so the handle line sits ABOVE it and the
   readout BELOW it — the meter used to sit at y=316, which the big wheel now
   occupies. Plate y ÷ 464 = the number to type. */
/* ══════════════════════════════════════════════════════════════
   STEP 0 — the handle. Plain flow, on the shared `.ob-h` head.
   ══════════════════════════════════════════════════════════════
   ⚠ It used to be absolutely positioned in percentages of a 369×464 plate,
   because it was laid over a technical drawing that had to line up with it.
   With the drawing gone there is nothing to align to, and the percentages were
   pure cost — every change meant re-deriving six `top`s. It is ordinary flow
   now, which is also what lets it share a head and a surface language with
   every other panel. */

.ob-eyebrow {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 1.6px;
  text-transform: uppercase; color: var(--text3); margin-bottom: 10px;
}

/* ── The slot ─────────────────────────────────────────────────────────────
   The bento's neu-emboss (.v3-search-pill) pressed IN rather than raised —
   same offsets, same colours, sign flipped — so it reads as a hole to put
   something in rather than a button to press. Solid `--bg`, so it is the
   surface itself deformed, which is the whole trick.
   ⚠ It is a <label>, so the whole slot focuses the field. */
.ob-user-well {
  display: flex; align-items: center; gap: 10px;
  height: 52px; padding: 0 22px;
  border-radius: 999px; background: var(--bg); cursor: text;
  box-shadow: inset 4px 4px 8px rgba(0,0,0,0.5),
              inset -4px -4px 8px rgba(255,255,255,0.06);
  transition: box-shadow .18s;
}
.sd-theme-light .ob-user-well {
  box-shadow: inset 4px 4px 8px rgba(116,150,179,0.27),
              inset -4px -4px 8px #ffffff;
}
/* Lit when the handle is good — the same signal the meter and the hint give. */
.ob-plate--ok .ob-user-well {
  box-shadow: inset 4px 4px 8px rgba(0,0,0,0.5),
              inset -4px -4px 8px rgba(255,255,255,0.06),
              inset 0 0 0 1px var(--ob-fill);
}
.sd-theme-light .ob-plate--ok .ob-user-well {
  box-shadow: inset 4px 4px 8px rgba(116,150,179,0.27),
              inset -4px -4px 8px #ffffff,
              inset 0 0 0 1px var(--ob-fill);
}
.ob-at { font-size: 19px; font-weight: 700; color: var(--text2); flex-shrink: 0; }
.ob-user-input {
  flex: 1; min-width: 0;
  background: none; border: none; outline: none; padding: 0;
  font-family: inherit; font-size: 21px; font-weight: 700; color: var(--text);
  letter-spacing: -0.3px;
}
.ob-user-input::placeholder { color: var(--text3); }

/* ── The readout: budget, rule, reassurance — one centred block ──────────── */
.ob-user-read { margin-top: 18px; text-align: center; }

/* ⚠ The meter keeps its 141px width and is centred with `margin: auto`, not by
   going full-width with `justify-content: center` — its `::before` rule is
   `inset 0`, so a full-width element draws that underline right across the
   panel instead of under the dots. 18 dots, 5px on an 8px pitch; the tick under
   the 4th is the minimum `obUserValid` puts on Continue. */
.ob-user-meter {
  position: relative; margin: 0 auto;
  display: flex; align-items: center; gap: 3px;
  width: 141px; padding-bottom: 6px;
}
.ob-user-meter::before {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0;
  height: 1px; background: var(--border);
}
.ob-user-meter i {
  position: relative; width: 5px; height: 5px; border-radius: 50%;
  box-shadow: inset 0 0 0 1px var(--text3);
  transition: background .18s, box-shadow .18s;
}
.ob-user-meter i.on { background: var(--ob-fill); box-shadow: inset 0 0 0 1px var(--ob-fill); }
.ob-user-meter i.ob-gate::after {
  content: ''; position: absolute; left: 50%; top: 100%; width: 1px; height: 6px;
  background: var(--text3); transform: translateX(-50%);
}

/* The rule, in the drawing's voice — mono, because it is a constraint. */
.ob-user-hint {
  margin-top: 12px;
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .6px;
  text-transform: uppercase; color: var(--text3);
}
.ob-user-hint--ok { color: #3fbf6a; }

/* ⚠ Sans and sentence case against the hint's mono uppercase, and placed
   directly AFTER the constraint: "4–18 chars · a–z 0–9 _" is the line that
   makes someone think but I want a real name, so the reassurance answers it in
   place rather than pre-empting a worry they have not had yet. */
.ob-user-note {
  margin: 10px auto 0; max-width: 84%;
  font-size: 11.5px; line-height: 1.45; color: var(--text3);
}

/* 1 | Connect service */
.ob-services { display: flex; flex-direction: column; gap: 11px; }
/* ⚠ Same emboss as step 0's slot, RAISED instead of pressed — these are things
   you press, that one is a hole you put something in. No border: the shadow
   pair is the edge, which is what makes the two pages read as one surface
   language rather than "a drawing" and then "a form". */
.ob-svc {
  display: flex; align-items: center; gap: 13px;
  background: var(--bg); border: none;
  border-radius: 15px; padding: 15px 16px; cursor: pointer;
  font-family: inherit; color: var(--text);
  box-shadow: -4px -4px 8px 0 rgba(255,255,255,0.07), 4px 4px 6px -1px rgba(0,0,0,0.5);
  transition: box-shadow .18s, transform .12s;
}
.sd-theme-light .ob-svc {
  box-shadow: -4px -4px 8px 0 #d8d2c8, 4px 4px 6px -1px rgba(116,150,179,0.27);
}
/* Pressing one presses it IN — the slot's own shadow, so the two states are
   the same object seen from either side. */
.ob-svc:active { transform: translateY(1px); }
.ob-svc--on {
  box-shadow: inset 4px 4px 8px rgba(0,0,0,0.5),
              inset -4px -4px 8px rgba(255,255,255,0.06),
              inset 0 0 0 1px var(--accent);
}
.sd-theme-light .ob-svc--on {
  box-shadow: inset 4px 4px 8px rgba(116,150,179,0.27),
              inset -4px -4px 8px #ffffff,
              inset 0 0 0 1px var(--accent);
}
.ob-svc-ico {
  width: 40px; height: 40px; border-radius: 11px; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center; color: #fff;
}
.ob-svc-ico svg { width: 22px; height: 22px; }
.ob-svc-name { flex: 1; text-align: left; font-size: 16px; font-weight: 700; }
.ob-svc-cta { font-size: 12.5px; font-weight: 700; color: var(--text2); font-family: var(--font-mono); }
.ob-svc-check {
  width: 24px; height: 24px; border-radius: 50%; display: none;
  align-items: center; justify-content: center;
  background: var(--accent); color: var(--bg);
}
.ob-svc-check svg { width: 14px; height: 14px; }
.ob-svc--on .ob-svc-cta { display: none; }
.ob-svc--on .ob-svc-check { display: flex; }
/* Same voice and size as step 0's `.ob-user-note` — the quiet line that tells
   you the step is not a trap. */
.ob-note { font-size: 11.5px; line-height: 1.45; color: var(--text3); text-align: center; margin: 18px auto 0; max-width: 84%; }

/* 2 | Tracking */
.ob-track-card {
  display: flex; align-items: center; gap: 14px;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 16px; padding: 16px; margin-bottom: 18px;
}
.ob-track-wave { display: flex; align-items: flex-end; gap: 3px; height: 30px; flex-shrink: 0; }
.ob-track-wave i { width: 4px; border-radius: 2px; background: var(--accent); animation: obWave 1s ease-in-out infinite; }
.ob-track-wave i:nth-child(1) { height: 40%; animation-delay: 0s; }
.ob-track-wave i:nth-child(2) { height: 90%; animation-delay: .15s; }
.ob-track-wave i:nth-child(3) { height: 60%; animation-delay: .3s; }
.ob-track-wave i:nth-child(4) { height: 100%; animation-delay: .45s; }
.ob-track-wave i:nth-child(5) { height: 50%; animation-delay: .6s; }
@keyframes obWave { 0%,100% { transform: scaleY(.4); } 50% { transform: scaleY(1); } }
.ob-track-now { font-size: 11px; text-transform: uppercase; letter-spacing: .1em; color: var(--accent); font-weight: 700; font-family: var(--font-mono); }
.ob-track-song { font-size: 13px; color: var(--text2); margin-top: 3px; }
.ob-track-opts { display: flex; flex-direction: column; gap: 10px; }
.ob-track-opt {
  padding: 15px; border-radius: 13px; cursor: pointer; font-family: inherit;
  font-size: 14.5px; font-weight: 600; color: var(--text);
  background: var(--surface); border: 1.5px solid var(--border); transition: border-color .15s, background .15s;
}
.ob-track-opt--on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--surface)); }

/* ══════════════════════════════════════════════════════════════════════════
   3 | GENRES — Pro's mix dial, and a list of the same tree
   ══════════════════════════════════════════════════════════════════════════
   The dial is the bento's, unchanged: same svg, same `.mix-inline` wrap, same
   ring transitions and spin-in. Only `.mix-inline--ob` differs, and only in
   the ways a cover-less host has to — it is in flow rather than over a cover,
   always visible, and wears the SCREEN's palette rather than the dark tokens
   the bento version carries for sitting on album art. */

/* One row above the views: the DIAL's back on the left, the switch on the
   right, level with each other. The back is the wheel's own — one ring up —
   and shows only inside a main (`.ob-panel--in`); it keeps its space when
   hidden so the switch never shifts. ⚠ It is not the rail's Back and must not
   be folded into it: a Back that sometimes means "up a ring" and sometimes
   "previous step" is two buttons in one coat, and it was rejected as such. */
.ob-view-row { display: flex; align-items: center; justify-content: space-between; margin: -4px 0 10px; }
.ob-mix-back {
  visibility: hidden;
  display: inline-flex; align-items: center; gap: 2px;
  border: 0; border-radius: 999px; padding: 6px 11px 6px 7px;
  background: var(--surface2); color: var(--text);
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .3px;
  cursor: pointer;
  box-shadow: -2px -2px 5px rgba(255,255,255,0.06), 2px 2px 4px rgba(0,0,0,0.45);
}
.sd-theme-light .ob-mix-back { box-shadow: -2px -2px 5px #ffffff, 2px 2px 4px rgba(116,150,179,0.25); }
.ob-mix-back svg { width: 13px; height: 13px; }
.ob-panel--in .ob-mix-back { visibility: visible; }

/* The switch. The service rows' emboss at pill scale: the track pressed IN,
   the live segment sitting proud of it. */
.ob-view {
  display: inline-flex; gap: 2px; padding: 3px;
  border-radius: 999px; background: var(--bg);
  box-shadow: inset 3px 3px 6px rgba(0,0,0,0.45), inset -3px -3px 6px rgba(255,255,255,0.05);
}
.sd-theme-light .ob-view {
  box-shadow: inset 3px 3px 6px rgba(116,150,179,0.22), inset -3px -3px 6px #ffffff;
}
.ob-view-btn {
  border: 0; border-radius: 999px; padding: 6px 14px; background: none;
  color: var(--text2); font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: .4px; text-transform: uppercase; cursor: pointer;
  transition: background .15s, color .15s, box-shadow .15s;
}
.ob-view-btn.is-on {
  color: var(--text); background: var(--surface2);
  box-shadow: -2px -2px 5px rgba(255,255,255,0.06), 2px 2px 4px rgba(0,0,0,0.45);
}
.sd-theme-light .ob-view-btn.is-on {
  box-shadow: -2px -2px 5px #ffffff, 2px 2px 4px rgba(116,150,179,0.25);
}

/* One view at a time. ⚠ `display`, not `visibility`: the wheel is a square the
   height of the panel and the list is longer still, and a hidden one must not
   hold its height. The cost is that labels in a hidden wheel cannot be
   measured — `obSyncGenres` refits them whenever the wheel is showing. */
.ob-genres { display: none; }
.ob-genres.is-on { display: block; animation: obFade .32s ease; }

/* THE DOCK — this step's picks, between the stage and the footer, so they sit
   against Continue, stay in reach however far the stage has scrolled, and
   (being outside the stage) can never move the wheel or the walls. Steps 3-6
   (`.ob-has-dock`); genre chips on 3, the walls' image chips on 4/5, people
   on 6. TWO ROWS, then it fades; it scrolls inside the cap so a chip past the
   fold can still be reached and removed.
   ⚠ The fade is a mask, and only ONCE THERE IS OVERFLOW (`is-over`, measured in
   obSyncDock): a permanent bottom fade would dim the second row of a two-row
   set that fits perfectly. The cap is two rows of whichever chip the step
   uses: 23px genre chips → 56px; 30px image chips → 70px. */
.ob-picks-dock {
  display: none; flex-wrap: wrap; align-items: center; align-content: flex-start; gap: 7px;
  flex-shrink: 0; min-height: 26px; max-height: 70px; overflow-y: auto;
  margin: 0 24px 2px;
  scrollbar-width: none; -ms-overflow-style: none;
}
.s-onboarding[data-ob-step="3"] .ob-picks-dock { max-height: 56px; }
.ob-has-dock .ob-picks-dock { display: flex; }
.ob-picks-dock::-webkit-scrollbar { display: none; }
.ob-picks-dock.is-over {
  -webkit-mask-image: linear-gradient(#000 58%, transparent);
          mask-image: linear-gradient(#000 58%, transparent);
}
/* ⚠ EMPTY, IT IS GONE — not a mono hint line. The hint ("Tap artists to
   follow them") cost a row between the wall and the buttons on every step
   before you had picked anything, which is exactly when the wall wants the
   room; the head already says what to do. The stage runs to the footer until
   the first pick, and the dock opens under it then. */
.ob-picks-dock.is-empty { display: none; }
.ob-mix-chip {
  display: inline-flex; align-items: center; gap: 4px;
  border: 0; border-radius: 999px; padding: 5px 9px 5px 11px;
  background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--text);
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .3px;
  cursor: pointer;
}
.ob-mix-chip i { font-style: normal; opacity: .5; font-size: 12px; line-height: 1; }
.ob-mix-chip:hover i { opacity: 1; }

/* The wheel's box: a square out to the BELT's margin, not the text column's.
   The panel pads 24px a side and the progress belt sits 8px in, so the wheel
   bleeds 16px past the column each way — 369px in the 385px frame. The 320-unit
   viewBox lands at ~1.15px a unit here against ~0.91 in the bento, so every
   label budget the dial was tuned to is met with room over.
   ⚠ It does not move. The switch above it is one fixed row and the chips are
   BELOW it, capped — the wheel's top edge is the same pixel whatever you pick. */
.ob-mix {
  position: relative; aspect-ratio: 1 / 1;
  width: calc(100% + 32px); margin: 2px -16px 6px;
}
.mix-inline--ob {
  position: absolute; inset: 0; z-index: 1;
  opacity: 1; pointer-events: auto;
  border-radius: 0; overflow: visible;
  /* ⚠ The bento's wrap pins DARK tokens so the dial survives a light home over
     a tinted cover. There is no cover here — the dial sits on the screen, so
     it takes the screen's own palette, light or dark, back off the theme. */
  --bg: inherit; --surface: inherit; --surface2: inherit; --border: inherit;
  --text: inherit; --text2: inherit; --text3: inherit; --accent: inherit;
}
/* The head sits a little tighter on this step: the wheel below it is the
   tallest thing in the wizard and every row above it is a row it pays for. */
.ob-panel--genres .ob-h { margin-bottom: 10px; }

/* The list: BUBBLES — the chips this step had before the dial, in the wheel's
   margins (bled 16px past the column, to the belt) and the step's emboss. A
   main is a raised pill; picked, it is pressed IN with the accent rim, exactly
   as a service row is (`.ob-svc--on`), and its subs unfold in a well beneath.
   States are the dial's: pressed = picked, an accent RIM on a raised bubble =
   something under it is picked (the lit-main ring), plain = nothing. */
.ob-glist {
  display: flex; flex-wrap: wrap; gap: 9px;
  width: calc(100% + 32px); margin: 2px -16px 6px;
}
.ob-gbub {
  border: 0; border-radius: 999px; padding: 9px 15px;
  background: var(--bg); color: var(--text2);
  font-family: inherit; font-size: 13px; font-weight: 600; letter-spacing: -.1px;
  cursor: pointer;
  box-shadow: -3px -3px 6px 0 rgba(255,255,255,0.07), 3px 3px 5px -1px rgba(0,0,0,0.5);
  transition: box-shadow .18s, color .15s, transform .12s;
}
.sd-theme-light .ob-gbub { box-shadow: -3px -3px 6px 0 #d8d2c8, 3px 3px 5px -1px rgba(116,150,179,0.27); }
.ob-gbub:active { transform: translateY(1px); }
.ob-gbub.is-lit {
  color: var(--text);
  box-shadow: -3px -3px 6px 0 rgba(255,255,255,0.07), 3px 3px 5px -1px rgba(0,0,0,0.5),
              inset 0 0 0 1.5px var(--accent);
}
.sd-theme-light .ob-gbub.is-lit {
  box-shadow: -3px -3px 6px 0 #d8d2c8, 3px 3px 5px -1px rgba(116,150,179,0.27),
              inset 0 0 0 1.5px var(--accent);
}
.ob-gbub.is-on {
  color: var(--text);
  box-shadow: inset 3px 3px 6px rgba(0,0,0,0.5), inset -3px -3px 6px rgba(255,255,255,0.06),
              inset 0 0 0 1px var(--accent);
}
.sd-theme-light .ob-gbub.is-on {
  box-shadow: inset 3px 3px 6px rgba(116,150,179,0.27), inset -3px -3px 6px #ffffff,
              inset 0 0 0 1px var(--accent);
}
/* The well: a full-width item (so it breaks the line and lands under its main's
   row), pressed into the surface like the handle slot — you are INSIDE the
   genre — with the sub bubbles raised out of it, smaller than the mains. */
.ob-gsubs {
  flex-basis: 100%; display: flex; flex-wrap: wrap; gap: 7px;
  padding: 11px; margin: 1px 0 3px; border-radius: 20px;
  box-shadow: inset 3px 3px 7px rgba(0,0,0,0.42), inset -3px -3px 7px rgba(255,255,255,0.05);
  animation: obFade .28s ease;
}
.sd-theme-light .ob-gsubs { box-shadow: inset 3px 3px 7px rgba(116,150,179,0.24), inset -3px -3px 7px #ffffff; }
.ob-gsubs .ob-gbub { padding: 7px 12px; font-size: 12px; }

/* 4/5 | Search + walls */
.ob-searchbar {
  display: flex; align-items: center; gap: 9px;
  background: var(--surface); border: 1.5px solid var(--border);
  border-radius: 12px; padding: 0 13px; margin-bottom: 12px;
}
.ob-searchbar svg { width: 16px; height: 16px; color: var(--text2); flex-shrink: 0; }
.ob-searchbar input {
  flex: 1; background: none; border: none; outline: none;
  font-family: inherit; font-size: 14px; color: var(--text); padding: 12px 0; min-width: 0;
}
.ob-searchbar input::placeholder { color: var(--text3); }

/* ⚠ `.ob-pinned` is gone — the walls' picks live in the dock (`.ob-picks-dock`). */
.ob-chip {
  display: inline-flex; align-items: center; gap: 7px;
  background: var(--accent); color: var(--bg);
  border: none; border-radius: 99px; padding: 4px 10px 4px 4px; cursor: pointer;
  font-family: inherit; font-size: 12.5px; font-weight: 700; max-width: 160px;
}
.ob-chip-img { width: 22px; height: 22px; border-radius: 6px; background-size: cover; background-position: center; flex-shrink: 0; }
.ob-chip-img--round { border-radius: 50%; }
/* A person's chip: initials on their gradient, where an artist has a photo. */
.ob-chip-av {
  width: 22px; height: 22px; border-radius: 50%; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 9px; font-weight: 800; color: #fff; letter-spacing: .02em;
}
.ob-chip-t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ob-chip-x { font-size: 15px; line-height: 1; opacity: .7; flex-shrink: 0; }

.ob-wall { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px 10px; }
.ob-card {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  background: none; border: none; padding: 0; cursor: pointer; font-family: inherit;
  color: var(--text); min-width: 0;
}
.ob-card-img {
  position: relative; width: 100%; aspect-ratio: 1; border-radius: 12px;
  background-size: cover; background-position: center; background-color: var(--surface2);
  box-shadow: 0 4px 12px rgba(0,0,0,.22); transition: transform .16s;
}
.ob-card-img--round { border-radius: 50%; }
.ob-card:active .ob-card-img { transform: scale(.94); }
.ob-card-check {
  position: absolute; inset: 0; border-radius: inherit;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--accent) 55%, transparent); opacity: 0; transition: opacity .16s;
}
.ob-card-check svg {
  width: 26px; height: 26px; color: #fff; padding: 5px;
  background: var(--accent); border-radius: 50%; box-sizing: content-box;
}
.ob-card--on .ob-card-check { opacity: 1; }
.ob-card--on .ob-card-img { outline: 2.5px solid var(--accent); outline-offset: 2px; }
.ob-card-t { font-size: 11.5px; font-weight: 700; text-align: center; line-height: 1.2; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; align-self: stretch; }
.ob-card-s { font-size: 10px; color: var(--text2); text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; align-self: stretch; margin-top: -3px; }
.ob-empty { grid-column: 1 / -1; text-align: center; color: var(--text2); font-size: 13px; padding: 30px 0; }

/* 6 | People you may know */
.ob-people { display: flex; flex-direction: column; }
.ob-person { display: flex; align-items: center; gap: 12px; padding: 11px 0; border-bottom: 1px solid var(--border); }
.ob-person:last-child { border-bottom: none; }
.ob-person-av {
  width: 42px; height: 42px; border-radius: 50%; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-size: 14px; font-weight: 700;
}
.ob-person-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ob-person-user { font-size: 14.5px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ob-person-mutual { font-size: 11.5px; color: var(--text2); }
.ob-follow {
  flex-shrink: 0; padding: 8px 18px; border-radius: 99px; cursor: pointer;
  font-family: inherit; font-size: 13px; font-weight: 700;
  background: var(--text); color: var(--bg); border: 1.5px solid var(--text); transition: all .15s;
}
.ob-follow--on { background: none; color: var(--text2); border-color: var(--border); }

/* 7 | Profile payoff */
.ob-profile { flex: 1; display: flex; flex-direction: column; padding: 20px 22px 24px; }
.ob-pf-hero { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 24px 0 26px; }
.ob-pf-av {
  width: 96px; height: 96px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 40px; font-weight: 800; color: var(--bg);
  background: linear-gradient(135deg, var(--accent), var(--accent2));
  box-shadow: 0 10px 30px color-mix(in srgb, var(--accent) 30%, transparent);
}
.ob-pf-name { font-size: 26px; font-weight: 800; letter-spacing: -0.5px; margin-top: 16px; font-family: var(--font-head); }
.ob-pf-tagline { font-size: 13px; color: var(--text2); margin-top: 3px; }
.ob-pf-badge {
  display: inline-flex; align-items: center; gap: 7px; margin-top: 14px;
  padding: 6px 13px; border-radius: 99px; font-size: 12px; font-weight: 600;
  background: var(--surface); border: 1px solid var(--border); color: var(--text2);
}
.ob-pf-badge-dot { width: 7px; height: 7px; border-radius: 50%; background: #3fbf6a; }
.ob-pf-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-top: 4px; }
.ob-stat { background: var(--surface); border: 1px solid var(--border); border-radius: 13px; padding: 14px 4px; text-align: center; }
.ob-stat-n { font-size: 22px; font-weight: 800; color: var(--text); }
.ob-stat-l { font-size: 10.5px; color: var(--text2); text-transform: uppercase; letter-spacing: .06em; margin-top: 2px; }
.ob-pf-note { font-size: 12px; color: var(--text2); text-align: center; margin-top: auto; padding-top: 22px; }

/* Footer */
.s-onboarding .ob-footer {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 22px calc(20px + env(safe-area-inset-bottom));
  flex-shrink: 0;
  /* ⚠ No rule above it. It had a hairline border-top, and with step 3's picks
     docked directly above, the line cut the picks off from the button they
     belong with. The dock and the footer are one strip. */
}
/* ⚠ Both buttons wear a RIM OF THE SCREEN'S BACKGROUND — a 5px ring of `--bg`
   drawn as a box-shadow — because on the scrolling steps the wall now runs
   under them and Skip, which was bare text, disappeared into faded album art.
   The ring is the surface showing through around each button, so they read as
   sitting ON the page rather than printed over whatever scrolled beneath. Skip
   became a real pill for the same reason: text alone has no edge to rim. */
.ob-skip {
  border: none; cursor: pointer; font-family: inherit;
  font-size: 14px; font-weight: 600; color: var(--text2); padding: 12px 16px; flex-shrink: 0;
  background: var(--surface); border-radius: 12px;
  box-shadow: 0 0 0 5px var(--bg);
}
.s-onboarding .ob-next { flex: 1; width: auto; margin: 0; box-shadow: 0 0 0 5px var(--bg); }
.s-onboarding .ob-next:disabled { opacity: .4; cursor: default; }


/* ============================================================
   PROFILE — "Funky" theme 01 · emboss base + textured skin PNG
   Base panel embossed OUT; pic + album wells embossed IN; the skin
   PNG is laid OVER the base (aligned to the holes) for the old-school
   look. Lives in the home shell (.s-home-v3 / --light) like other pages.
   ============================================================ */
.s-prof2 {
  --pf-base:   #3a3b45;                       /* raised panel colour */
  --pf-lt:     rgba(255,255,255,0.06);        /* emboss-out highlight */
  --pf-dk:     rgba(0,0,0,0.55);              /* emboss-out shadow */
  --pf-ink:    #ece7dd;                       /* text on the panel */
  --pf-ink2:   rgba(236,231,221,0.6);
  --pf-well-dk:rgba(0,0,0,0.62);              /* emboss-in shadow */
  --pf-well-lt:rgba(255,255,255,0.13);        /* emboss-in highlight */
  --pf-fg:     #e8e2d6;                       /* text on screen bg (userbar) */
  --pf-fg2:    rgba(232,226,214,0.55);
  --pf-surface:#1c1c23;                       /* picker sheet */
  --pf-border: rgba(232,226,214,0.12);
  --pf-edge:   rgba(0,0,0,0.4);               /* silhouette hairline */
  --pf-face:   #303139;                       /* recessed inner display face */
  --pf-slot:   #111116;                       /* username pill = screen bg */
  --pf-slot-ink:  #ffffff;                    /* username text (white on dark) */
  --pf-slot-ink2: rgba(255,255,255,0.55);
}
.s-prof2.s-home-v3--light {
  --pf-base:   #e0d8c6;
  --pf-lt:     rgba(255,255,255,0.95);
  --pf-dk:     rgba(120,102,72,0.4);
  --pf-ink:    #2c2417;
  --pf-ink2:   rgba(44,36,23,0.6);
  --pf-well-dk:rgba(120,100,68,0.45);
  --pf-well-lt:rgba(255,255,255,0.85);
  --pf-fg:     #1a1208;
  --pf-fg2:    rgba(26,18,8,0.55);
  --pf-surface:#ffffff;
  --pf-border: rgba(26,18,8,0.14);
  --pf-edge:   rgba(120,102,72,0.35);
  --pf-face:   #d6cdb8;
  --pf-slot:   #f0ece3;                       /* username pill = screen bg */
  --pf-slot-ink:  #111114;                    /* username text (black on light) */
  --pf-slot-ink2: rgba(0,0,0,0.5);
}

/* Scroll area inside v3-body — matches the homepage bento's ~10px side margins */
/* ⚠ The 12px side padding is cancelled by `.prof-fav-rail`'s `margin-inline`
   so the favourites carousel can run edge to edge. Change this and change that. */
/* ⚠ 14px at the top, not 6. The card is the first thing under the app header
   and at 6 it was tucked against it — this page opens on a large object and
   needs to look placed rather than jammed. */
.prof2-scroll { padding: 14px 12px 96px; }


/* ── Profile card — traced from ProfileTheme_Regular4 (1).svg ────────────
   A card topped by a name-banner tab holding a white username pill: left pane =
   profile picture, right pane = stats / bio / location / one action button.
   ⚠ EVERY PERCENTAGE IN THIS BLOCK RESOLVES AGAINST 690×460. Horizontals are
   `x / 690`, verticals are `y / 460`; a value copied from an older revision of
   this block lands in the wrong place WITHOUT looking obviously wrong, which is
   the trap every time the artwork changes. It has been 466, then 608, now 460.
   ⚠ Why 460 and not the file's 608: the artwork's bottom 150 units are the five
   favourite-album circles, and those are no longer in the card — they are a
   swipeable rail in their own section (`.prof-favs`). The canvas stops where
   the CARD stops, at y=449, plus a little air.
   What the revision itself changed: the card got TALLER (74.7→449 where it was
   65.7→328) and the picture pane much WIDER (0→374.5 where it stopped at
   262.3). The right pane pays for it — 45.6% of the width where it had 61.8% —
   which is why the stats are a 2×2 grid rather than one row of four. */
/* ⚠⚠ 690 / 556, and this number IS the SVG's viewBox height — see the warning
   in the Profile block of CLAUDE.md. preserveAspectRatio=meet scales the drawing
   to the SHORTER axis, so a 556-tall viewBox in a 460-tall box would render the
   whole card at 82.7% and centre it while every percentage-positioned element on
   top stayed where it was: nothing looks broken on its own and the card is
   completely wrong.
   ⚠ It was 690/460. The extra 96 units are the tag compartment, and EVERY
   vertical percentage below was rescaled by 460/556 = 0.82734 to keep the thing
   it points at in the same physical place. */
.prof-canvas { position: relative; width: 100%; margin: 4px auto 0; aspect-ratio: 690 / 556; }
.prof-base {
  position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1;
  pointer-events: none; overflow: visible;
  filter: drop-shadow(-5px -5px 9px var(--pf-lt)) drop-shadow(6px 7px 12px var(--pf-dk));
}
.prof-base-main { fill: var(--pf-base); stroke: var(--pf-edge); stroke-width: 0.5; }
/* Hairline splitting the picture pane from the info pane */
.prof-divide { fill: none; stroke: var(--pf-edge); stroke-width: 0.5; }
/* Name-banner tab — same raised panel colour, merges with the card silhouette.
   No stroke: its bottom edge runs into the card so the fill covers the card /
   divider top strokes, leaving no seam. The parent SVG's emboss shadow shapes it. */
.prof-name-tab { fill: var(--pf-base); stroke: none; }

/* White username pill nested in the banner. Left/top/height fixed; width is set
   by sizeProfName() so it grows with the banner. Inset shadow seats it in. */
.prof-name-pill {
  /* 16/690, 13.5/556, 47.5/556 — the pill is an explicit path in the new file
     (x 16→360, y 13.5→61) rather than something eyeballed into the banner.
     ⚠ The DENOMINATOR is 556 now, not 460: the y numbers are the same units in
     the same trace, they are just a smaller fraction of a taller box. */
  position: absolute; z-index: 2; left: 2.32%; top: 2.43%; height: 8.54%;
  border-radius: 999px; background: var(--pf-slot); pointer-events: none;
  box-shadow: inset 0 2px 4px var(--pf-well-dk), inset 0 -1px 2px var(--pf-well-lt);
}

/* Username seated in the pill — unbold; white on dark, black on light */
.prof-name-tab-lbl {
  position: absolute; z-index: 4; left: 6%; top: 2.4%; height: 8.52%;
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; pointer-events: none;
}
.prof-name-nick { font-size: 13px; font-weight: 500; color: var(--pf-slot-ink); letter-spacing: -0.2px; font-family: var(--font-head); }
.prof-name-at   { font-size: 10px; font-weight: 400; color: var(--pf-slot-ink2); font-family: var(--font-mono); }

/* Profile image — fills the SVG's left pane, below the banner (flush left, no gaps).
   Bottom-left corner rounded to the card; other corners square (banner / divide).
   ⚠ The pane is x 0.5→374.5, y 74.5→449.5 — unchanged in TRACE UNITS. The card
   grew underneath it and the photo did not, so against a 556-tall canvas the
   same pane is 13.65% → 67.18% instead of 16.5% → 81.2%.
   ⚠ Its bottom-left corner is SQUARE now (was `5%`): that arc belonged to the
   card's own corner, which is 96 units lower since the tag compartment. */
.prof-pic {
  position: absolute; z-index: 2; left: 0.4%; top: 13.65%; width: 53.88%; height: 67.18%;
  border-radius: 0 10px 0 0; background-size: cover; background-position: center;
  background-color: #0c0b0a;
  box-shadow: inset 0 2px 6px var(--pf-well-dk), inset 0 -1px 4px var(--pf-well-lt);
}

/* ── The right pane: x 374.5→688.9 = 54.28%→99.84%, y 74.7→449 = 16.24%→97.61%.
   ⚠ ONE flex column, not three separately positioned boxes. Stats / bio /
   location each used to carry a hand-computed `top`, and any restyle inside one
   of them silently pushed it into the next — giving the stats their inset wells
   grew that block by ~20px and dropped the bio on top of it. A column cannot
   overlap itself, and the numbers below are just the pane's own bounds.
   The bottom stops short of the pane: the action button owns that corner. */
.prof-right {
  position: absolute; z-index: 3;
  /* The pane runs 18% → 77.8% of the canvas — the card's top edge down to the
     tag compartment's seam. It holds the bio, and the location at its foot — a stack of two whose order cannot
     go wrong, because `.prof-meta`'s `margin-top: auto` puts all the slack
     between them rather than anywhere else.
     ⚠ `justify-content` is FLEX-START, not center: an auto margin eats the free
     space before justify-content ever sees it, so `center` here would be a rule
     that reads as if it does something and doesn't.
     ⚠ The padding is the bio's MARGIN, and `box-sizing` is set locally because
     this sheet has no global reset — without it the padding would grow the pane
     past the card's right edge instead of insetting the text.
     ⚠ The stats moved OUT of this pane, into `.prof-statbar` under the card, and
     the action button was never at the bottom of it, so nothing has to be left
     clear down there.
     ⚠ TOP MARGIN: 18% of 556 is 100 trace units, which sits ~25 below the
     card's top edge (74.7) where it used to sit ~8 below it. The bio had its
     head against the banner; it has room above it now, and still five clear
     lines under a taller pane.
     ⚠ BOTTOM is 22.2%, not 6%: the pane has to stop at the tag
     compartment's seam (y 449.5), not at the card's new bottom edge. */
  box-sizing: border-box;
  left: 56.4%; width: 41.4%; top: 18%; bottom: 22.2%;
  padding: 2px 9px 0 7px;
  display: flex; flex-direction: column; justify-content: flex-start;
  min-height: 0;
}
/* ⚠ `position: relative` because these are flow children now. The `.pfe-slot`
   badge and outline are absolutely positioned INSIDE their slot, and they used
   to get their containing block for free from the slot being absolute itself. */
.prof-info { position: relative; z-index: 3; min-width: 0; }
/* ⚠ 12.5px at weight 600, not 11px at 400. The bio has the pane to itself now
   that the stats are out of the card, and it is the only text on this page
   written to be READ rather than scanned — at a caption's size and weight it
   read as a footnote about the person instead of as the person. The colour goes
   with it: `--pf-ink` at 0.82 rather than `--pf-ink2`, so it holds the pane.
   ⚠ Still clamped, and to FIVE lines now, because bigger text plus the location
   at the foot leaves less room. The card is a fixed-aspect SVG trace: text that
   overflows it does not grow the card, it escapes it. */
.prof-desc {
  font-size: 12.5px; font-weight: 600; line-height: 1.5;
  color: color-mix(in srgb, var(--pf-ink) 82%, transparent);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 5; overflow: hidden;
}
/* Last in the column; `margin-top: auto` pins it to the bottom of the stack so
   a short bio leaves the gap above the location rather than below it. */
.prof-meta {
  position: relative; z-index: 3; margin-top: auto; min-width: 0;
  display: flex; flex-wrap: wrap; gap: 3px 12px;
}
/* ⚠ Never wraps. `.prof-meta` is the LAST thing in the column and clears the
   action button by about 3px — a location long enough to take a second line
   (measured: the row has ~116px and "United Kingdom" needs ~98) would land on
   top of it. It ellipses instead. */
.prof-meta-item {
  display: inline-flex; align-items: center; gap: 5px; min-width: 0;
  font-size: 10.5px; font-weight: 800; color: var(--pf-ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.prof-meta-item svg { width: 12px; height: 12px; opacity: 1; }

/* ── TAGS — the compartment at the FOOT OF THE CARD (`profTagsHtml`) ─────
   ⚠ These live INSIDE `.prof-canvas` now, which is why the silhouette grew 96
   trace units at the bottom — a strip floating under the card read as a caption
   about it rather than as part of who you are. Being in the canvas means being
   positioned in percentages of the trace: the compartment runs y 449.5 → 545,
   and these numbers are that, inset, over 556.
   ⚠ It SCROLLS sideways instead of wrapping. The compartment is one row tall by
   construction, and three long collectibles wrapping to a second line inside a
   fixed-aspect box would fall straight out of the card. */
.prof-canvas .prof-tags {
  position: absolute; z-index: 3;
  left: 3.2%; right: 3.2%; top: 82.7%; height: 13.5%;
  margin: 0;
  display: flex; flex-wrap: nowrap; align-items: center; gap: 6px;
  overflow-x: auto; overflow-y: hidden;
  scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.prof-canvas .prof-tags::-webkit-scrollbar { display: none; }
.prof-canvas .prof-tag { flex: 0 0 auto; }

/* ══ SD TEXTURES — the surface a tag is made of (`.sd-tex--*`) ═════════════
   A tag is a thing you COLLECT, and a flat coloured pill does not read as one.
   A metal head's should look like brushed steel; a festival's like the field it
   was handed out in. That surface is most of what makes a rare tag worth
   wearing at a glance, at a size where a label alone cannot do it.

   ⚠ GRADIENTS, NOT IMAGE FILES, on purpose:
   - a chip is ~26px tall, where a photographic texture is mush;
   - the good free tiling sets (Transparent Textures, Subtle Patterns) are
     CC-BY, which is a real attribution obligation on a shipped app;
   - zero requests, and every one of these re-tints itself from the tag's own
     `--tint` because the fill underneath shows through.

   ⚠ FILLER by intent. Swapping one for a real image is a single `--tex` value
   (`url(images/tex-metal.png)`) plus its `--tex-size`; nothing else in the
   system knows the difference.

   ⚠ Every texture must carry BOTH a white and a black layer. The chips sit on a
   dark card in one theme and a beige one in the other, and a white-only overlay
   is invisible on light while a black-only one is invisible on dark.

   ⚠ The consumer sets `background-image: var(--tex)` — these classes only
   declare the variables, so the SAME class works on `.prof-tag` (profile, form,
   shop) and on `.pp-tag` (the picker sheet). One texture, four hosts. */

/* Brushed steel — fine vertical strands under one diagonal sheen. */
.sd-tex--metal {
  --tex:
    linear-gradient(102deg, rgba(255,255,255,.30) 0%, rgba(255,255,255,0) 26%,
                            rgba(0,0,0,.20) 54%, rgba(255,255,255,.24) 76%, rgba(255,255,255,0) 100%),
    repeating-linear-gradient(90deg, rgba(255,255,255,.16) 0 1px, rgba(0,0,0,.13) 1px 2px,
                                     rgba(255,255,255,.06) 2px 3px, rgba(0,0,0,.05) 3px 4px);
  --tex-size: auto, auto;
}

/* Daisies — a yellow eye in a white flower, on two offset grids so it scatters
   rather than marches. */
.sd-tex--flower {
  --tex:
    radial-gradient(circle, rgba(255,214,102,.95) 0 1.1px, rgba(255,255,255,.88) 1.1px 3.1px, rgba(255,255,255,0) 3.4px),
    radial-gradient(circle, rgba(255,214,102,.75) 0 .8px, rgba(255,255,255,.62) .8px 2.3px, rgba(255,255,255,0) 2.6px),
    linear-gradient(0deg, rgba(0,0,0,.10), rgba(0,0,0,.10));
  --tex-size: 15px 15px, 10px 10px, auto;
  --tex-pos: 0 0, 7px 6px, 0 0;
}

/* Record grooves — concentric rings running off the bottom of the chip. */
.sd-tex--groove {
  --tex:
    repeating-radial-gradient(circle at 50% 150%, rgba(255,255,255,.15) 0 1px,
                              rgba(0,0,0,.14) 1px 3px);
  --tex-size: auto;
}

/* Night sky — two scattered star grids, one bright, one faint. */
.sd-tex--stars {
  --tex:
    radial-gradient(circle, rgba(255,255,255,.95) 0 .8px, rgba(255,255,255,0) 1.2px),
    radial-gradient(circle, rgba(255,255,255,.55) 0 .6px, rgba(255,255,255,0) 1px),
    linear-gradient(0deg, rgba(0,0,0,.16), rgba(0,0,0,.16));
  --tex-size: 12px 9px, 19px 14px, auto;
  --tex-pos: 2px 1px, 9px 6px, 0 0;
}

/* Shoegaze bloom — a soft smear of light across the pill, no edges anywhere. */
.sd-tex--haze {
  --tex:
    linear-gradient(118deg, rgba(255,255,255,.38) 0%, rgba(255,255,255,.04) 38%,
                            rgba(0,0,0,.16) 62%, rgba(255,255,255,.26) 84%, rgba(255,255,255,0) 100%),
    radial-gradient(120% 220% at 22% 0%, rgba(255,255,255,.28), rgba(255,255,255,0) 60%);
  --tex-size: auto, auto;
}

/* Clean diagonal ribbing — the tidy one, for the tag about not skipping. */
.sd-tex--stripe {
  --tex:
    repeating-linear-gradient(45deg, rgba(255,255,255,.17) 0 3px, rgba(0,0,0,.08) 3px 4px,
                                     rgba(255,255,255,0) 4px 8px);
  --tex-size: auto;
}

/* Rain on a car window — steep streaks, uneven spacing. */
.sd-tex--rain {
  --tex:
    repeating-linear-gradient(74deg, rgba(255,255,255,.26) 0 1px, rgba(255,255,255,0) 1px 5px,
                                     rgba(0,0,0,.10) 5px 6px, rgba(255,255,255,0) 6px 11px);
  --tex-size: auto;
}

/* Sleeve card — a fine cross-weave, the way a first pressing's board looks. */
.sd-tex--paper {
  --tex:
    repeating-linear-gradient(0deg,  rgba(0,0,0,.07) 0 1px, rgba(255,255,255,.08) 1px 2px),
    repeating-linear-gradient(90deg, rgba(0,0,0,.05) 0 1px, rgba(255,255,255,.06) 1px 3px);
  --tex-size: auto, auto;
}

/* Newsprint halftone — the Blue Note sleeve, basically. */
.sd-tex--halftone {
  --tex:
    radial-gradient(circle, rgba(0,0,0,.34) 0 1.5px, rgba(0,0,0,0) 1.8px),
    radial-gradient(circle, rgba(255,255,255,.20) 0 1.1px, rgba(255,255,255,0) 1.4px);
  --tex-size: 5px 5px, 5px 5px;
  --tex-pos: 0 0, 2.5px 2.5px;
}

/* Poured concrete — a mottled wedge pattern with no readable direction. */
.sd-tex--concrete {
  --tex:
    repeating-conic-gradient(from 14deg at 32% 44%, rgba(0,0,0,.13) 0 7deg, rgba(255,255,255,.10) 7deg 14deg),
    repeating-conic-gradient(from 51deg at 71% 63%, rgba(255,255,255,.08) 0 9deg, rgba(0,0,0,.07) 9deg 18deg);
  --tex-size: 11px 11px, 8px 8px;
}

/* Magnetic tape — tight vertical banding with a shine down the middle. */
.sd-tex--tape {
  --tex:
    linear-gradient(180deg, rgba(255,255,255,.22) 0%, rgba(255,255,255,0) 45%, rgba(0,0,0,.20) 100%),
    repeating-linear-gradient(90deg, rgba(0,0,0,.18) 0 2px, rgba(255,255,255,.13) 2px 4px);
  --tex-size: auto, auto;
}

/* Fur — short soft strokes lying one way, like a cat you just stroked. */
.sd-tex--fur {
  --tex:
    repeating-linear-gradient(64deg, rgba(255,255,255,.17) 0 1px, rgba(255,255,255,0) 1px 3px,
                                     rgba(0,0,0,.09) 3px 4px, rgba(255,255,255,0) 4px 6px);
  --tex-size: auto;
}

/* ── TAGS — the chips themselves ─────────────────────────────────────────
   ⚠ Each chip carries its own `--tint` as an "R,G,B" triple, the shop's
   convention, so the same markup is a profile chip here and a product shot on
   a storefront row. The FREE ones are outlined and the collectible ones are
   filled: the difference has to be visible at a glance, because the whole point
   of "DaisyChainsFestival2026" is that not everyone is wearing it. */
.prof-tags {
  display: flex; flex-wrap: wrap; gap: 6px;
  margin: 12px 0 2px;
}
/* ⚠ `background-image` over `background-color`, never shorthand `background`:
   the colour is the tag's own tint and the image is its texture, and the
   shorthand would wipe whichever one is set second. The `--tex*` variables come
   from an `.sd-tex--*` class and default to nothing, so an untextured tag is
   still a perfectly good flat chip. */
.prof-tag {
  display: inline-flex; align-items: center;
  padding: 5px 10px; border-radius: 999px;
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .03em; white-space: nowrap;
  color: rgb(var(--tint));
  background-color: rgba(var(--tint), .12);
  background-image: var(--tex, none);
  background-size: var(--tex-size, auto);
  background-position: var(--tex-pos, 0 0);
  border: 1px solid rgba(var(--tint), .34);
}
/* Collectible: filled, ringed, and the texture reads at full strength on it —
   which is the point. On the outlined free chips the same texture sits over a
   12%-alpha wash and stays a hint. */
.prof-tag--rare {
  color: #16130d;
  background-color: rgb(var(--tint));
  border-color: rgb(var(--tint));
  box-shadow: 0 0 0 2px rgba(var(--tint), .22);
  /* ⚠ The label rides ON the texture now, so it needs its own contrast. A
     1px light halo, not a dark shadow: the fill is a saturated tint and the
     ink is near-black. */
  text-shadow: 0 1px 0 rgba(255,255,255,.28);
}

/* ── The three figures — a ROW under the card (`profStatsHtml`) ──────────
   ⚠ These used to be stacked down the card's right pane, where the pane's
   width was the ceiling on how big a number could be set. Out here the row has
   the whole page, so the three read at once instead of being scanned down a
   column — which is how a follower count is actually read. The bio took the
   pane, where line length is worth something.
   The hairlines between the cells are borders on the cells, not separate
   elements, so the row can never end on a stray divider. */
.prof-statbar {
  display: flex; align-items: stretch;
  margin: 14px 0 4px; padding: 12px 0;
  border-top: 1px solid var(--pf-border);
  border-bottom: 1px solid var(--pf-border);
}
.prof-statb {
  flex: 1 1 0; min-width: 0;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  border-left: 1px solid var(--pf-border);
}
.prof-statb:first-child { border-left: none; }
.prof-statb-n {
  font-family: var(--font-head); font-size: 23px; font-weight: 800;
  letter-spacing: -.5px; line-height: 1; color: var(--pf-fg);
  white-space: nowrap;
}
.prof-statb-l {
  font-family: var(--font-mono); font-size: 8.5px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--pf-fg2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}

/* ── The card's ONE action button — UPPER RIGHT ──────────────────────────
   ⚠ Edit on your own profile, Follow on someone else's — the same button, not
   two. They were separate elements once, which meant both rendered on your own
   page; giving them one box then made the later one in the DOM hide the other,
   and the pencil vanished. A page offers one action here, so there is one
   element and `PROFILE_GUEST` decides what it says.
   ⚠ It sat in the card's LOWER right and has moved. Down there it was inside the
   card, level with the stats, and read as part of that block rather than as the
   page's one action; up here it is on the strip above the card, opposite the
   name banner — where the trace puts a pill (x 600→688, y 1→55) and where a
   profile's follow control is expected.
   ⚠ Wider than that traced pill on purpose: 12.75% is ~46px at the 393 frame and
   the dots plus "FOLLOW" want ~58. 17% gives ~61px and still starts at 81.5%,
   clear of the name banner, which ends at 67.8%. */
.prof-act {
  /* ⚠ top/height rescaled against the 556-tall canvas (was 0.2% / 11.7% of 460);
     the button has not moved a pixel on screen. */
  position: absolute; z-index: 4; left: auto; right: 1.5%; top: 0.17%;
  width: 17%; height: 9.68%;
  display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 6px;
  padding: 0 9px; border: none; cursor: pointer; background: var(--pf-base);
  border-radius: 999px; box-shadow: -2px -2px 5px var(--pf-lt), 3px 3px 7px var(--pf-dk);
  transition: box-shadow .18s; font-family: inherit;
}
.prof-act:active { box-shadow: inset -2px -2px 5px var(--pf-lt), inset 2px 2px 5px var(--pf-dk); }
.prof-act .v3-ring { width: 20px; height: 20px; color: var(--pf-ink); }
.prof-act .v3-ring-dot { width: 3.2px; height: 3.2px; }
.prof-act-ico { display: inline-flex; color: var(--pf-ink); }
.prof-act-ico svg { width: 13px; height: 13px; }
.prof-act-lbl {
  font-size: 7.5px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  color: var(--pf-ink2); font-family: var(--font-mono); line-height: 1;
}
.prof-act.is-following {
  box-shadow: inset -2px -2px 5px var(--pf-lt), inset 2px 2px 6px var(--pf-dk);
}
.prof-act.is-following .v3-ring   { color: var(--star); }
.prof-act.is-following .prof-act-lbl { color: var(--star); }

/* CD preview/platforms popup — reuses the .wall2-menu / .v3-cd-menu look. */
.prof-cd-menu { left: auto; right: auto; top: auto; bottom: auto; width: 172px; }
/* The favourites rail's own popup — the width and the reset stay, the position
   comes from `.prof-favs` below (one menu per slot, all centred over the rail). */

/* ── Favourite albums — a rail of three, one centred ─────────────────────
   ⚠ This replaces five small wells traced into the bottom of the card. At that
   size the COVER WAS ALL YOU GOT — no title, no artist, no year — and a cover is
   not enough to know an album by. Three big ones with the middle one framed
   leaves room under the rail to say what you are actually looking at.
   ⚠ CSS scroll-snap, NOT a hand-rolled gesture. This has to feel native under a
   thumb, and the browser's own momentum, rubber-band and snap beat anything
   written here; the swipe engines elsewhere exist because they animate a bento
   cell, which this does not. */
.prof-favs { position: relative; }
/* The rail is centred on one disc, the panel under it is centred text — a
   left-aligned heading over that is the only thing on the block off the axis. */
.prof-favs .prof-sec-hd { text-align: center; }
.prof-fav-rail {
  /* ⚠ `position: relative` is load-bearing, not decoration: it makes the rail
     each button's `offsetParent`, which is what puts `offsetLeft` in the same
     coordinate space as `scrollLeft` for `profFavPaint`. */
  position: relative;
  display: flex; gap: 12px;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
  /* ⚠ NO horizontal padding, ever. Percentage `flex-basis` resolves against the
     flex container's CONTENT box, so side padding of 19% made each disc 62% of
     the remaining 62% — 38.4% of the rail — and NOTHING could line up: the
     centring arithmetic is written in percentages of the rail, and padding
     silently redefines what a percentage means in here. */
  /* ⚠ The bottom padding is THE ROOM THE ARC FALLS INTO, not spacing. The rail
     is `overflow-y: hidden` (it has to be — `overflow-x: auto` forces the other
     axis to clip), so a disc swung down by `profFavArc` is cut off at the
     padding edge. A percentage of the rail's WIDTH, because that is what the
     drop is — R(1−cos θ) as a share of the disc spacing, and the spacing is 65%
     of the rail — so the two scale together and this never needs a second number
     when the frame width changes.
     ⚠⚠ IT IS PAIRED WITH `PROF_ARC_DEG` **AND** WITH THE DISC WIDTH. At 12°
     this was 8%; at 24° the drop nearly quadrupled (1−cos θ, not θ) and 8%
     clipped the bottom off both side discs. It then came DOWN to 13% when the
     discs went 62% → 46%: the drop is R(1−cos θ) and R is spacing/θ, so a
     narrower disc is a smaller hub and a shallower fall — at 46% the drop is
     ~40px against ~53px at 62%. Move any of the three, measure, move this.
     ⚠ Vertical padding only. Horizontal padding here is still forbidden for
     the reason above it. */
  padding: 4px 0 13%;
  /* ⚠ FULL BLEED — cancels `.prof2-scroll`'s 12px side padding so the rail runs
     edge to edge while its heading and info panel stay inset with everything
     else. A carousel that stops short of the screen reads as a boxed-in list;
     one that runs off both sides reads as something you can keep pulling.
     ⚠ These 12s are `.prof2-scroll`'s padding written a second time — change
     that and change this. The percentages inside are unaffected: they resolve
     against the rail's own (now wider) box, so the discs and the spacers scale
     together and the centring still lands. */
  margin-inline: -12px;
}
.prof-fav-rail::-webkit-scrollbar { display: none; }
/* ⚠ `.prof-fav-pad` is GONE, along with the ends it padded. The spacers existed
   so the FIRST and LAST disc could reach the centre — a looping rail has
   neither, and a spacer sitting at one of the seams would open a hole in the
   middle of the wheel. See `profFavLoop` in app.js. */
.prof-fav {
  position: relative;            /* the centre hole is absolute inside it */
  /* ⚠ 46%, not 62%. Three big circles in a row left no room to read the arc
     they sit on — the curve is the point, and at 62% you mostly saw one disc.
     ⚠ `profFavArc` needs NO change for this: it measures `spacing` off the DOM
     and derives the hub from it, so the drop scales with the disc on its own.
     The one number that does NOT follow automatically is the rail's
     padding-bottom — see the pairing note there. */
  flex: 0 0 46%; aspect-ratio: 1;
  scroll-snap-align: center;
  padding: 0; border: none; background: none; border-radius: 50%;
  cursor: pointer;
  /* The two neighbours are present but clearly not the subject — smaller and
     dimmed, so the rail reads as one album with context either side of it.
     ⚠ This pair is the NO-JS BASE only; `profFavArc` writes transform and
     opacity inline every scroll frame and `.is-arc` drops the transition. */
  transform: scale(.82); opacity: .5;
  transition: transform .28s cubic-bezier(.22,1,.36,1), opacity .28s;
}
.prof-fav.is-mid { transform: scale(1); opacity: 1; }
/* ⚠ Once `profFavArc` has run, the transform is written inline every scroll
   frame and the transition above has to go — .28s of easing on a value that is
   already being recomputed from `scrollLeft` 60 times a second is just smear.
   The two rules above stay as the no-JS base, which is why this is a class the
   arc adds rather than a `transition: none` on `.prof-fav` outright. */
.prof-fav-rail.is-arc .prof-fav { transition: none; }
.prof-fav-img {
  display: block; width: 100%; height: 100%; border-radius: 50%;
  background-size: cover; background-position: center; background-color: #0c0b0a;
  box-shadow: inset 3px 4px 10px var(--pf-well-dk), inset -2px -3px 9px var(--pf-well-lt),
              0 6px 18px rgba(0,0,0,.35);
}
/* Centre hole → reads as a CD; stays put while the art spins during preview */
/* ⚠ 13%, not the 21% the small wells carried. A spindle hole is a fixed size on
   a real record — it does NOT grow with the disc — so a percentage that read
   right at 60px reads like a doughnut at 200. */
.prof-fav-hole {
  position: absolute; top: 50%; left: 50%; width: 13%; height: 13%;
  transform: translate(-50%, -50%); border-radius: 50%;
  background: var(--pf-face); z-index: 2; pointer-events: none;
  box-shadow: inset 0 1px 3px rgba(0,0,0,.6), 0 0 0 2px rgba(0,0,0,.28);
}
.prof-fav-img.prof-fav--spin { animation: v3spin 3.2s linear infinite; }

.prof-fav--empty {
  display: flex; align-items: center; justify-content: center;
  background: #0c0b0a; box-shadow: inset 3px 4px 10px var(--pf-well-dk);
}
.prof-fav-add { font-size: 30px; font-weight: 300; color: rgba(255,255,255,.45); line-height: 1; }

/* The panel under the rail. Written by `profFavPaint` from whichever disc is
   centred — the markup carries no album at all, so it cannot go stale against
   the rail's scroll position. */
/* ⚠ The NEGATIVE margin is deliberate and it is the arc's fault. The rail now
   carries ~58px of bottom padding so the swung-down discs are not clipped, but
   that room is only USED at the two edges, where the side discs fall into it —
   under the middle it is a hole, and the album title was left floating a long
   way from the record it names. Pulling the panel back up reclaims it. The text
   is centred and the side discs are slivers at the extreme left and right, so
   the two live in the same band without ever meeting; the panel is after the
   rail in the DOM, so on a very long title it wins the overlap anyway.
   ⚠ `min-height` covers the panel WITH a two-line review in it, so the block
   does not grow and shrink as you scroll between a record they wrote about and
   one they didn't. The line hides, the box does not move. */
.prof-fav-info {
  /* ⚠ -20px, down from -30: this claws back the rail's bottom padding, and that
     padding shrank with the discs (16% → 13%). Reclaim more than is there and
     the title climbs into the wheel. */
  text-align: center; position: relative;
  margin-top: -20px; padding: 0 8px; min-height: 58px;   /* was 96 with stars + a quote under the artist */
}
/* Title and year on one line. The year is set quieter and lighter so it reads as
   part of naming the record rather than as a second fact competing with it. */
.prof-fav-alb {
  display: flex; align-items: baseline; justify-content: center; gap: 6px;
  min-width: 0;
}
.prof-fav-name {
  font-size: 15px; font-weight: 700; color: var(--pf-fg);
  font-family: var(--font-head); letter-spacing: -.2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.prof-fav-yr {
  flex-shrink: 0; font-size: 11px; font-weight: 400; color: var(--pf-fg2);
  font-family: var(--font-mono); letter-spacing: .02em;
}
.prof-fav-artist { font-size: 11.5px; font-weight: 700; color: var(--pf-fg2); margin-top: 1px; }
/* The stars / review-count row and the quote under it are GONE (2026-09-11):
   the panel is name · year · artist. What they wrote is the PINNED REVIEWS
   section under the rail, which reuses the review-history row. */
/* The pinned cards sit on the PROFILE, which has a theme (and a cream light
   mode), not on the album page's dark panel — so the card's hard-coded warm
   ink is re-pointed at the profile's own tokens here. */
/* The pins and the history are the HOME FEED'S CARD (Eric, 2026-09-18) —
   `.v3-rev-card--feed`, which inks itself off the `--sd-*` tokens. On the
   profile those are re-pointed at the profile's own `--pf-*` set here, so the
   one card reads right on the profile's surface (and its skin) without a
   parallel rule tree. The divider's full-width bleed (a ::after hung 10px past
   the card, sized for the feed's scroller) is pulled back to the card's own
   width — the profile's sections have their own gutters. */
.prof-pins, .prof-feed {
  --sd-ink: var(--pf-fg); --sd-ink2: var(--pf-fg2); --sd-ink3: var(--pf-fg2);
  --sd-line: var(--pf-line, rgba(128,128,128,0.16));
  --sd-card: var(--pf-well, rgba(128,128,128,0.12));
  --sd-pill: rgba(128,128,128,0.12);
}
.prof-pins .v3-rev-card--feed::after, .prof-feed .v3-rev-card--feed::after { left: 0; right: 0; }
.prof-pins-empty {
  padding: 10px 4px; font-family: var(--font-mono); font-size: 9.5px;
  letter-spacing: .04em; color: var(--pf-fg2);
}
.pfe-pin-q { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--font-main); }
/* ⚠ One menu per slot, all centred over the rail rather than hung off their own
   disc: only the CENTRED album is actionable, so that is the only place a popup
   ever needs to appear. They also sit outside the rail — it is
   `overflow-x: auto` and would clip them. */
.prof-favs .prof-cd-menu {
  position: absolute; z-index: 20;
  left: 50%; transform: translateX(-50%); bottom: 12px;
}

/* ── Content sections below the card (artists · songs · playlists · reviews) ── */
.prof-sec { margin-top: 20px; }
.prof-sec-hd, .prof-feed-hd {
  font-size: 11px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--pf-fg2); font-family: var(--font-mono); margin: 0 2px 11px;
}

/* Favourite artists — a row of 4 (avatars scale to fit) */
.prof-artists { display: flex; gap: 10px; }
.prof-art {
  flex: 1; min-width: 0; display: flex; flex-direction: column; align-items: center; gap: 7px;
  background: none; border: none; cursor: pointer; padding: 0; font-family: inherit;
}
.prof-art-av {
  width: 100%; max-width: 82px; aspect-ratio: 1; border-radius: 50%;
  background-size: cover; background-position: center;
  background-color: #0c0b0a; box-shadow: 0 4px 10px rgba(0,0,0,.28); transition: transform .14s;
}
.prof-art:hover .prof-art-av { transform: scale(1.04); }
.prof-art:active .prof-art-av { transform: scale(.96); }
.prof-art-nm { font-size: 10.5px; font-weight: 700; color: var(--pf-fg); text-align: center; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Favourite songs — a compact vertical list (art · title/album·artist · play) */
.prof-songs { display: flex; flex-direction: column; gap: 2px; }
.prof-song {
  display: flex; align-items: center; gap: 11px; width: 100%; padding: 7px 8px;
  background: none; border: none; cursor: pointer; text-align: left;
  border-radius: 10px; transition: background .14s; font-family: inherit;
}
.prof-song:hover { background: color-mix(in srgb, var(--pf-fg) 6%, transparent); }
.prof-song:active { transform: scale(.99); }
.prof-song-art {
  width: 40px; height: 40px; flex-shrink: 0; border-radius: 7px;
  background-size: cover; background-position: center; background-color: #0c0b0a;
  box-shadow: 0 3px 8px rgba(0,0,0,.22);
}
.prof-song-meta { display: flex; flex-direction: column; min-width: 0; flex: 1; gap: 1px; }
.prof-song-title { font-size: 12.5px; font-weight: 700; color: var(--pf-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prof-song-sub { font-size: 10.5px; color: var(--pf-fg2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prof-song-play { flex-shrink: 0; width: 24px; height: 24px; display: inline-flex; align-items: center; justify-content: center; color: var(--pf-fg2); }
.prof-song-play svg { width: 13px; height: 13px; }

/* ── The picks — Favourite songs · Listened · Listen later (csharpuser, 2026-09-25)
   A three-cell segmented picker in the section-heading slot, then a `.prof-songs`
   list per tab. Same pill language as `.set-seg`, inked off the profile's own
   `--pf-*` tokens so it sits on the skin; the ACTIVE cell is the profile's ink
   on a well, not the gold — a tab is navigation, not a rating. */
.prof-picks-seg {
  display: flex; gap: 2px; padding: 3px; margin: 0 0 8px;
  border-radius: 999px;
  background: var(--pf-well, rgba(128,128,128,0.12));
  box-shadow: inset 0 1px 2px rgba(0,0,0,0.18);
}
.prof-picks-tab {
  flex: 1; min-width: 0; border: none; background: none; cursor: pointer;
  padding: 7px 4px; border-radius: 999px;
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--pf-fg2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  transition: background .15s, color .15s;
}
.prof-picks-tab:hover { color: var(--pf-fg); }
.prof-picks-tab.active {
  background: var(--pf-fg); color: var(--pf-base, var(--pf-well, #16130d));
  box-shadow: 0 1px 3px rgba(0,0,0,0.22);
}
.prof-picks-pane[hidden] { display: none; }
/* Their number on a record they reviewed, in the glyph's slot. */
.prof-song-score {
  flex-shrink: 0; min-width: 24px; text-align: right;
  font-family: var(--font-mono); font-size: 11px; font-weight: 700;
  color: var(--pf-fg); letter-spacing: .02em;
}
/* The dot glyphs (clock / headphones) run slightly larger than the play
   triangle so the 5×5 grid reads at this size. */
.prof-song-play--dot svg { width: 15px; height: 15px; }

/* Top playlists — a row of 3 cards */
.prof-pls { display: flex; gap: 10px; }
.prof-pl {
  flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px;
  background: none; border: none; cursor: pointer; padding: 0; font-family: inherit; text-align: left;
}
.prof-pl-cover {
  width: 100%; aspect-ratio: 1; border-radius: 12px; background-size: cover; background-position: center;
  background-color: #0c0b0a; box-shadow: 0 4px 10px rgba(0,0,0,.26); transition: transform .14s;
}
.prof-pl:hover .prof-pl-cover { transform: translateY(-2px); }
.prof-pl:active .prof-pl-cover { transform: scale(.97); }
.prof-pl-nm { font-size: 11.5px; font-weight: 700; color: var(--pf-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prof-pl-meta { font-size: 9.5px; color: var(--pf-fg2); font-family: var(--font-mono); margin-top: -3px; }

/* ── Review history — the home feed's row, on a profile ──────────────────
   ⚠ There is no `.prof-rv-*` component any more, and there should not be one.
   The rows ARE `.ntf-row` — the same anatomy, the same sentence order and the
   same engagement pills the home feed uses — because a review is a review, and
   a profile that renders one its own way is a second component that will drift
   from the first. `--sd-*` reaches them because the profile screen is itself an
   `.s-home-v3`, which is where those tokens are scoped.
   The only thing this section adds is a bigger tap target: on home the pills sit
   in a scrolling feed of many verbs, here they are the point of the page. */
.prof-feed { margin-top: 20px; }
.v3-up--prof {
  font-size: 12.5px;
  padding: 7px 13px 7px 11px;
  gap: 7px;
}
.v3-up--prof svg { width: 17px; height: 17px; }

/* ── Favourite-album picker sheet ── */
.prof-picker {
  position: absolute; inset: 0; z-index: 60;
  display: flex; align-items: flex-end; justify-content: center;
  background: rgba(0,0,0,.42); opacity: 0; visibility: hidden; transition: opacity .2s ease, visibility .2s;
}
.prof-picker.open { opacity: 1; visibility: visible; }
.pp-sheet {
  width: 100%; max-height: 84%; display: flex; flex-direction: column;
  background: var(--pf-surface); border-radius: 22px 22px 0 0;
  border: 1px solid var(--pf-border); border-bottom: none;
  padding: 8px 14px calc(14px + env(safe-area-inset-bottom));
  transform: translateY(100%); transition: transform .26s cubic-bezier(.4,0,.2,1);
  box-shadow: 0 -12px 40px rgba(0,0,0,.35); color: var(--pf-fg);
}
.prof-picker.open .pp-sheet { transform: translateY(0); }
.pp-handle-bar { width: 38px; height: 4px; border-radius: 99px; background: var(--pf-border); margin: 4px auto 8px; flex-shrink: 0; }
.pp-top { display: flex; align-items: center; justify-content: space-between; padding: 2px 2px 10px; }
.pp-title { font-size: 16px; font-weight: 800; letter-spacing: -0.3px; color: var(--pf-fg); }
.pp-close { background: none; border: none; font-size: 24px; line-height: 1; color: var(--pf-fg2); cursor: pointer; padding: 0 4px; }
.pp-searchbar {
  display: flex; align-items: center; gap: 9px;
  background: color-mix(in srgb, var(--pf-fg) 6%, transparent); border: 1.5px solid var(--pf-border);
  border-radius: 12px; padding: 0 13px; margin-bottom: 12px; flex-shrink: 0;
}
.pp-searchbar svg { width: 16px; height: 16px; color: var(--pf-fg2); flex-shrink: 0; }
.pp-input { flex: 1; background: none; border: none; outline: none; font-family: inherit; font-size: 14px; color: var(--pf-fg); padding: 11px 0; min-width: 0; }
.pp-input::placeholder { color: var(--pf-fg2); }
.pp-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px 10px; overflow-y: auto; padding: 2px 2px 10px; }
.pp-item { display: flex; flex-direction: column; gap: 5px; background: none; border: none; padding: 0; cursor: pointer; font-family: inherit; color: var(--pf-fg); min-width: 0; }
.pp-img { width: 100%; aspect-ratio: 1; border-radius: 10px; background-size: cover; background-position: center; background-color: color-mix(in srgb, var(--pf-fg) 10%, transparent); box-shadow: 0 3px 10px rgba(0,0,0,.22); transition: transform .14s; }
.pp-item:active .pp-img { transform: scale(.94); }
.pp-item--on .pp-img { outline: 2.5px solid var(--star); outline-offset: 2px; }
.pp-t { font-size: 11px; font-weight: 700; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
.pp-s { font-size: 9.5px; color: var(--pf-fg2); text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; margin-top: -2px; }
.pp-empty { grid-column: 1 / -1; text-align: center; color: var(--pf-fg2); font-size: 13px; padding: 30px 0; }

/* ============================================================
   EDIT PROFILE (.s-pfedit) — a FORM.
   ⚠ This block used to style the profile CARD in edit mode: every
   region of the traced SVG carried .pfe-slot and a "+" badge tucked
   inside it, because the page WAS the card. It is a plain form now —
   photo, then one labelled row per field — so the per-region badge
   placements are gone with the markup that needed them. What is left
   of .pfe-slot styles the three things you still CHOOSE rather than
   type: the favourite discs, the playlist tiles and the song rows.
   ============================================================ */
.pfe-scroll { padding-bottom: 96px; }

/* top bar — back pill · title · Save.
   ⚠ 1fr auto 1fr, not space-between: the title has to sit on the page's centre
   line, and space-between centres it between two buttons of DIFFERENT widths,
   which is a different (and visibly off) place. The outer columns are equal, so
   whatever the buttons measure the middle one stays put.
   ⚠ The pill's own 10px margin-bottom moves to the bar, or the three items
   don't share a baseline. */
.pfe-top {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  gap: 10px; margin-bottom: 10px;
}
.pfe-top .plp-back-pill { margin-bottom: 0; justify-self: start; }
.pfe-editing {
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--pf-fg2);
  white-space: nowrap; text-align: center;
}
/* Sized off .plp-back-pill (72×34) so the two ends of the bar match. */
.pfe-save {
  justify-self: end;
  height: 34px; padding: 0 15px; border-radius: 999px; border: none;
  background: var(--star); color: #16130d;
  font-family: var(--font-main); font-size: 11.5px; font-weight: 800;
  white-space: nowrap; cursor: pointer; transition: filter .15s;
}
.pfe-save:hover { filter: brightness(1.07); }

/* ── the picture — the only field you change by looking ── */
.pfe-photo {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  margin: 6px 0 18px;
}
.pfe-avatar {
  position: relative; width: 132px; height: 132px; border-radius: 50%;
  border: none; padding: 0; cursor: pointer;
  background-color: var(--pf-face); background-size: cover; background-position: center;
  box-shadow: 0 6px 18px rgba(0,0,0,.35), inset 0 0 0 1px var(--pf-border);
  transition: transform .15s;
}
.pfe-avatar:hover { transform: scale(1.03); }
.pfe-avatar-badge {
  position: absolute; right: 3px; bottom: 3px;
  width: 36px; height: 36px; border-radius: 50%;
  background: var(--star); color: #16130d;
  font-family: var(--font-main); font-size: 27px; font-weight: 700;
  line-height: 34px; text-align: center;
  box-shadow: 0 2px 6px rgba(0,0,0,.5);
}
.pfe-photo-btn {
  background: none; border: none; cursor: pointer; padding: 2px 6px;
  color: var(--star);
  font-family: var(--font-mono); font-size: 10px; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase;
}
.pfe-photo-btn:hover { text-decoration: underline; }

/* ── the form — a label column and a field column, hairline between rows ──
   ⚠ The row's own bottom border IS the focus ring: :focus-within recolours it
   to --star. The inputs are transparent and border-less on purpose, so a row
   reads as a line of the profile rather than as a box to fill in. */
.pfe-form { margin-bottom: 22px; }
.pfe-row {
  display: grid; grid-template-columns: 82px 1fr; align-items: center;
  gap: 12px; padding: 11px 2px;
  border-bottom: 1px solid var(--pf-border);
  transition: border-color .15s;
}
.pfe-row:focus-within { border-bottom-color: var(--star); }
.pfe-row--tall { align-items: start; }
.pfe-lbl {
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--pf-fg2);
  cursor: pointer;
}
.pfe-row--tall .pfe-lbl { padding-top: 4px; }
.pfe-field { display: flex; align-items: center; gap: 5px; min-width: 0; }
.pfe-row--tall .pfe-field { flex-direction: column; align-items: stretch; gap: 3px; }
.pfe-pre { font-family: var(--font-mono); font-size: 13px; color: var(--pf-fg2); }
.pfe-in {
  width: 100%; min-width: 0; padding: 2px 0;
  background: none; border: none; outline: none;
  color: var(--pf-fg);
  font-family: var(--font-main); font-size: 13.5px; font-weight: 600;
}
.pfe-in::placeholder { color: var(--pf-fg2); font-weight: 400; }
.pfe-area { resize: none; min-height: 58px; line-height: 1.45; font-weight: 400; }
.pfe-count {
  align-self: flex-end;
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .04em;
  color: var(--pf-fg2);
}

/* ── favourite albums — five discs, straight in a row ──
   Not the profile's swipeable rail: the only question here is which record
   goes in which slot, and a carousel that has to be scrolled to reach slot 5
   makes that harder, not prettier. The disc face reuses .prof-fav-img /
   .prof-fav-hole so the two pages can't drift on what a CD looks like. */
.pfe-discs { display: flex; gap: 9px; }
.pfe-disc {
  position: relative; flex: 1 1 0; min-width: 0; aspect-ratio: 1;
  padding: 0; border: none; background: none; border-radius: 50%;
  cursor: pointer; transition: transform .15s;
}
.pfe-disc:hover { transform: scale(1.05); }
.pfe-disc--empty {
  display: flex; align-items: center; justify-content: center;
  border: 1px dashed var(--pf-border); color: var(--pf-fg2);
  transition: border-color .15s, color .15s, transform .15s;
}
.pfe-disc--empty:hover { border-color: var(--star); color: var(--star); }
.pfe-disc--empty .pfe-add-plus { font-size: 30px; }

/* ── a filled slot: the "+" badge and the hover outline ──
   NOTE: these are flow-level elements now (discs, tiles, rows), so unlike the
   card's absolutely-positioned regions they can all take position: relative. */
.pfe-slot { cursor: pointer; position: relative; }
.pfe-slot::after {
  content: ''; position: absolute; inset: -4px;
  border: 1px dashed transparent; border-radius: 8px;
  pointer-events: none; transition: border-color .15s, background .15s;
}
.pfe-slot:hover::after { border-color: var(--star); background: rgba(232,168,60,0.07); }
/* the round album discs get a round outline instead of a rounded rect */
.pfe-disc.pfe-slot::after { inset: -3px; border-radius: 50%; }

/* the badge sits on EVERY filled slot, so it's obvious the content is
   swappable without having to hover to find out */
.pfe-slot::before {
  content: '+';
  position: absolute; z-index: 6; top: -8px; right: -8px;
  width: 24px; height: 24px; border-radius: 50%;
  background: var(--star); color: #16130d;
  font-family: var(--font-main); font-size: 19px; font-weight: 700;
  line-height: 23px; text-align: center;
  pointer-events: none;
  box-shadow: 0 1px 4px rgba(0,0,0,0.5);
  transition: transform .15s;
}
.pfe-slot:hover::before { transform: scale(1.15); }
.pfe-disc.pfe-slot::before  { top: auto; bottom: -2px; right: -2px; width: 22px; height: 22px; font-size: 17px; line-height: 21px; }
.prof-pl.pfe-slot::before   { top: -7px; right: -7px; }
/* ⚠ NOT on the song rows. They end in their own full-size "+" (.pfe-song-plus,
   where the play triangle used to be), and a second little badge on the artwork
   was two plus signs arguing about which one you were meant to press. */
.prof-song.pfe-slot::before { content: none; }
.pfe-song-plus {
  flex-shrink: 0; width: 30px; height: 30px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--pf-fg2); font-family: var(--font-main); font-size: 26px;
  font-weight: 400; line-height: 1;
  transition: color .15s, background .15s;
}
.prof-song:hover .pfe-song-plus { color: var(--star); background: rgba(232,168,60,0.1); }

/* ── the two COLOUR rows (`skinRow` in screens.js) and their preview ──
   ⚠⚠ THE PREVIEW IS DRIVEN BY NOTHING. It paints itself from `var(--sd-bg)`
   and `var(--pf-base)` — the same two variables `profSkinCss` writes inline on
   this very screen — so it updates as you drag with no JS behind it at all, and
   it can never disagree with the card it is previewing. An earlier version had
   a per-row swatch that JS repainted by hand; it was more code and one more
   thing to leave stale. If you find yourself writing a `skinSwatch()` again,
   this is the note saying don't.
   ⚠ It shows the DERIVED tokens too — the inner pane is `--pf-face`, the dot is
   `--pf-ink` — so a colour that makes the card's own text unreadable shows you
   that here, before you save it onto your profile. */
.pfe-prev {
  flex: 0 0 auto; width: 74px; height: 50px; border-radius: 9px;
  display: flex; align-items: center; justify-content: center;
  background: var(--sd-bg);
  box-shadow: inset 0 0 0 1px var(--pf-border);
}
.pfe-prev-card {
  position: relative; width: 58px; height: 32px; border-radius: 5px;
  display: flex; align-items: stretch; overflow: hidden;
  background: var(--pf-base);
  box-shadow: -1px -1px 2px var(--pf-lt), 1px 2px 3px var(--pf-dk);
}
/* The left pane, the way the real card splits — and it is `--pf-face`, so the
   derived recess is on show next to the colour it was derived from. */
.pfe-prev-pane { flex: 0 0 44%; background: var(--pf-face); }
/* Two ticks of "text", in the ink the card would actually use. */
.pfe-prev-card::after {
  content: ''; position: absolute; right: 5px; top: 9px;
  width: 18px; height: 3px; border-radius: 2px;
  background: var(--pf-ink); opacity: .75;
}
.pfe-prev-card::before {
  content: ''; position: absolute; right: 5px; top: 16px;
  width: 11px; height: 3px; border-radius: 2px;
  background: var(--pf-ink); opacity: .4;
}

.pfe-skin-f { gap: 8px; }
/* ⚠ The native swatch chrome has to be stripped on BOTH pseudo-elements or
   Chrome leaves a 2px inset border and a white plate around the colour. */
.pfe-color {
  flex: 0 0 auto; width: 30px; height: 24px; padding: 0;
  border: 1px solid var(--pf-border); border-radius: 7px;
  background: none; cursor: pointer;
}
.pfe-color::-webkit-color-swatch-wrapper { padding: 2px; }
.pfe-color::-webkit-color-swatch { border: none; border-radius: 5px; }
.pfe-color::-moz-color-swatch { border: none; border-radius: 5px; }
/* The lightness track runs black → the card's own colour → white, so the slider
   says which way is which without a label at either end. */
.pfe-range {
  flex: 1 1 auto; min-width: 0; height: 4px; margin: 0;
  -webkit-appearance: none; appearance: none;
  border-radius: 999px; cursor: pointer;
  background: linear-gradient(90deg, #000 0%, var(--pf-base) 50%, #fff 100%);
}
.pfe-range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--star); border: 2px solid var(--pf-slot); cursor: pointer;
}
.pfe-range::-moz-range-thumb {
  width: 13px; height: 13px; border-radius: 50%;
  background: var(--star); border: 2px solid var(--pf-slot); cursor: pointer;
}
.pfe-skin-x {
  flex: 0 0 auto; width: 20px; height: 20px; border-radius: 50%;
  border: none; background: none; cursor: pointer;
  color: var(--pf-fg2); font-size: 15px; line-height: 1;
  transition: color .15s, background .15s;
}
.pfe-skin-x:hover { color: var(--star); background: rgba(232,168,60,0.1); }

/* ── the form's Tags row — a button, not a field ──
   The one row on the form you cannot type into: you wear what you own. It is
   laid out like a field so the column still reads as a list of your details,
   and the "+" on the right is the same one every other slot on this page uses. */
.pfe-tags {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  width: 100%; padding: 3px 0; background: none; border: none;
  cursor: pointer; text-align: left; font-family: inherit;
}
.pfe-tags .pfe-add-plus { margin-left: auto; color: var(--pf-fg2); transition: color .15s; }
.pfe-tags:hover .pfe-add-plus { color: var(--star); }
.pfe-tags-ph {
  font-family: var(--font-main); font-size: 13.5px; color: var(--pf-fg2);
}

/* ── the tag sheet (#prof-picker, kind `tag`) ──
   A wrapping list of chips, not the square grid the album/photo kinds use — a
   tag is a word of a length it chooses, and forcing them into equal cells makes
   the short ones look padded and truncates the long ones, which are exactly the
   collectible ones worth showing off. */
.pp-grid--tag { display: flex; flex-wrap: wrap; gap: 8px; }
.pp-tag-hint {
  width: 100%; margin-bottom: 2px;
  font-family: var(--font-mono); font-size: 9px; line-height: 1.5;
  color: var(--pf-fg2);
}
/* ⚠ Same split as `.prof-tag`: colour on `background-color`, texture on
   `background-image`. A tag that is brushed steel on your profile and flat grey
   in the sheet you picked it from is two different objects. */
.pp-tag {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 12px; border-radius: 999px; cursor: pointer;
  font-family: var(--font-mono); font-size: 10px; font-weight: 700;
  letter-spacing: .03em; white-space: nowrap;
  color: rgb(var(--tint));
  background-color: rgba(var(--tint), .1);
  background-image: var(--tex, none);
  background-size: var(--tex-size, auto);
  background-position: var(--tex-pos, 0 0);
  border: 1px solid rgba(var(--tint), .3);
  transition: background-color .15s, border-color .15s, opacity .15s, transform .12s;
}
.pp-tag:hover { border-color: rgb(var(--tint)); }
.pp-tag:active { transform: scale(.97); }
/* Worn: the same fill the profile gives a collectible, so "selected" here looks
   like what you are about to see there. */
.pp-tag--on {
  color: #16130d; background-color: rgb(var(--tint)); border-color: rgb(var(--tint));
  text-shadow: 0 1px 0 rgba(255,255,255,.28);
}
/* Three already worn and this is not one of them: it simply doesn't light up. */
.pp-tag--full { opacity: .4; }
/* Locked: dashed, with the price where the tick would be. Not disabled — it is
   a link to the shop, because a tag you can't wear yet IS the advertisement. */
/* Locked: dashed, and the texture is dialled right back — you can see what it
   is made of without it looking like something you already have. */
.pp-tag--locked { border-style: dashed; background-color: transparent; opacity: .78; }
.pp-tag-p {
  padding: 2px 7px; border-radius: 999px;
  background: rgba(var(--tint), .18); font-size: 9px;
}

/* ── the Pro upsell that stands in for the Favourite songs slots ──
   Gold-edged rather than dashed: a dashed box on this page means "empty, tap to
   fill", and this is the one block on the form that tapping does NOT fill. */
.pfe-pro {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 14px 13px; border-radius: 12px; cursor: pointer; text-align: left;
  color: var(--pf-fg);
  background: rgba(232,168,60,0.07);
  border: 1px solid color-mix(in srgb, var(--star) 42%, transparent);
  transition: background .15s, border-color .15s;
}
.pfe-pro:hover { background: rgba(232,168,60,0.13); border-color: var(--star); }
.pfe-pro-ico { flex-shrink: 0; width: 24px; height: 24px; color: var(--star); }
.pfe-pro-ico svg { width: 100%; height: 100%; }
.pfe-pro-txt { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.pfe-pro-t { font-family: var(--font-main); font-size: 12.5px; font-weight: 700; }
.pfe-pro-s {
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .04em;
  color: var(--pf-fg2);
}
.pfe-pro-go {
  flex-shrink: 0; padding: 7px 12px; border-radius: 999px;
  background: var(--star); color: #16130d;
  font-family: var(--font-main); font-size: 11px; font-weight: 800;
}

/* ── an empty slot: a dashed "+" tile (grid) or row (list) ── */
.pfe-add {
  display: flex; align-items: center; justify-content: center; gap: 7px;
  background: none; cursor: pointer; color: var(--pf-fg2);
  border: 1px dashed var(--pf-border); border-radius: 10px;
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .04em;
  transition: border-color .15s, color .15s, background .15s;
}
.pfe-add:hover { border-color: var(--star); color: var(--star); background: rgba(232,168,60,0.06); }
.pfe-add-plus { font-size: 28px; line-height: 1; font-weight: 300; }
.pfe-add--tile { flex-direction: column; aspect-ratio: 1; gap: 2px; padding: 8px; text-align: center; }
.pfe-add--row  { width: 100%; padding: 14px 13px; gap: 11px; justify-content: flex-start; }

/* ============================================================
   CONTENT EDITOR POPUP (#prof-picker) — one sheet, many kinds.
   The grid/searchbar chrome is shared with the original album
   picker; these add the photo grid and its upload tile.
   ⚠ The text-form kinds (name / bio / location / occupation) were
   removed with their rules — Edit Profile types those inline.
   ============================================================ */
/* photo kind — square grid + an upload tile above it */
.pp-upload {
  display: flex; align-items: center; justify-content: center;
  padding: 11px; margin-bottom: 12px; border-radius: 10px;
  border: 1px dashed var(--pf-border); color: var(--pf-fg2);
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .04em;
  cursor: pointer; transition: border-color .15s, color .15s;
}
.pp-upload:hover { border-color: var(--star); color: var(--star); }
.pp-upload input { display: none; }
.pp-grid--photo { grid-template-columns: repeat(4, 1fr); gap: 8px; }
.pp-grid--photo .pp-img { aspect-ratio: 1; border-radius: 8px; }


/* ============================================================
   NOTIFICATIONS (.s-ntf) + SETTINGS (.s-set)
   ============================================================
   Both ride the standard .s-home-v3 shell (header · v3-body · nowbar ·
   bottom nav) and share one set of scoped tokens, so the light theme is
   a single variable block per screen instead of a parallel rule tree.
   They also reuse existing furniture: .plp-back-pill for the back
   control and .wall2-cat pills for the notification filters. */
/* Scoped to `.s-home-v3` rather than just the two screens that started with it:
   the home activity feed reuses the `.ntf-*` row, so the whole home shell needs
   the tokens. `--sd-bg` is already exactly the home shell's own background in
   both themes (#111116 / #f0ece3), which is what lets the badge's punch-through
   ring read as a clean cut-out on all three screens.
   `.s-ntf` / `.s-set` are themselves `.s-home-v3`, so they are kept only to hold
   the more specific light block below. */
.s-home-v3, .s-ntf, .s-set {
  --sd-bg:      #111116;                      /* screen bg — badge rings punch through to it */
  --sd-ink:     #e8e2d6;
  --sd-ink2:    rgba(232,226,214,0.55);
  --sd-ink3:    rgba(232,226,214,0.32);
  --sd-card:    rgba(255,255,255,0.05);       /* raised row/card fill */
  --sd-card-hi: inset 0 1px 0 rgba(255,255,255,0.06);
  --sd-line:    rgba(232,226,214,0.09);
  --sd-well:    rgba(0,0,0,0.38);             /* recessed track (switch/segment) */
  --sd-hover:   rgba(255,255,255,0.04);
  /* Pill fill for controls that sit ON a row. ⚠️ Deliberately stronger than
     --sd-card: an unread row IS --sd-card, so a pill using it vanishes there. */
  --sd-pill:    rgba(255,255,255,0.09);
  --sd-pill-hi: rgba(255,255,255,0.16);
}
.s-home-v3--light, .s-ntf.s-home-v3--light, .s-set.s-home-v3--light {
  --sd-bg:      #f0ece3;
  --sd-ink:     #1a1208;
  --sd-ink2:    rgba(26,18,8,0.55);
  --sd-ink3:    rgba(26,18,8,0.34);
  --sd-card:    rgba(26,18,8,0.05);
  --sd-card-hi: inset 0 1px 0 rgba(255,255,255,0.6);
  --sd-line:    rgba(26,18,8,0.1);
  --sd-well:    rgba(120,102,72,0.2);
  --sd-hover:   rgba(26,18,8,0.05);
  --sd-pill:    rgba(26,18,8,0.07);
  --sd-pill-hi: rgba(26,18,8,0.14);
}

/* ── NOTIFICATIONS ────────────────────────────────────────── */
.ntf-scroll { padding: 4px 12px 75px; }   /* bottom clears the nowbar/nav overlay */
.ntf-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.ntf-top-r { display: flex; align-items: center; gap: 9px; }
.ntf-readall {
  border: none; background: none; cursor: pointer;
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .05em;
  color: var(--sd-ink3); padding: 6px 2px;
  transition: color .15s;
}
.ntf-readall:hover { color: var(--sd-ink); }
.ntf-readall[disabled] { opacity: .4; cursor: default; }

/* No page title and no filter pills — the rows start straight under the top
   row, so the unread chip rides beside "Mark all read". */
.ntf-count {
  font-family: var(--font-mono); font-size: 9.5px;
  padding: 3px 8px; border-radius: 999px;
  background: var(--star); color: #16130d;
}

/* Time bucket. The header sticks to the top of .v3-body (the scroller) and
   needs an opaque screen-bg fill so rows do not ghost through it. */
.ntf-group { margin-bottom: 6px; }
.ntf-group-hd {
  position: sticky; top: 0; z-index: 2;
  background: var(--sd-bg);
  padding: 6px 2px 8px;
  font-family: var(--font-mono); font-size: 9.5px;
  text-transform: uppercase; letter-spacing: .12em;
  color: var(--sd-ink3);
}
.ntf-group[hidden] { display: none; }

/* Row. Unread rows are the filled ones — read rows sit flat on the bg, so
   "what is new" is legible without a badge on every line. */
.ntf-row {
  position: relative;
  display: flex; align-items: flex-start; gap: 11px;
  padding: 10px 11px; margin-bottom: 3px;
  border-radius: 11px; cursor: pointer;
  transition: background .15s;
}
.ntf-row:hover { background: var(--sd-hover); }
.ntf-row--new { background: var(--sd-card); box-shadow: var(--sd-card-hi); }
.ntf-row--new:hover { filter: brightness(1.12); }
/* Unread rail — a bar on the left edge, NOT a corner dot: the right edge is
   occupied by the trailing album thumb / Follow button. Inset top and bottom
   so it clears the card's 11px rounded corners without needing overflow. */
.ntf-row--new::after {
  content: ''; position: absolute; left: 0; top: 11px; bottom: 11px;
  width: 3px; border-radius: 0 3px 3px 0;
  background: var(--star);
}
.ntf-row[hidden] { display: none; }

/* Timestamp + (on the feed's review rows) the like/comment pills, on one line.
   ⚠️ With no pills this is a flex row of ONE child, so it changes nothing for
   the inbox — which is the point: the engagement controls are an addition to
   the shared row, not a home-only restyling of it. The inbox's own like and
   comment rows can adopt `.ntf-acts` as-is when they want the same affordance.
   See the ⚠️ in renderFriendFeed for why the feed diverges here at all. */
/* ⚠️ The 5px lift moves from `.ntf-time` to the row: as a flex item the time's
   own margin-top survives, and `align-items: center` would then centre the
   pills against its margin-BOX — leaving them sitting a couple of px low. */
.ntf-foot { display: flex; align-items: center; gap: 10px; margin-top: 5px; }
.ntf-foot .ntf-time { margin-top: 0; }
.ntf-acts { display: flex; align-items: center; gap: 6px; margin-left: auto; }

/* ⚠️ SHAPE CARRIES MEANING on these rows, and the rule is app-wide: a circle is
   a PERSON, a rounded square is a RECORD. Every row is literally a sentence —
   round subject on the left, verb in the copy, square object on the right — so
   you can tell who did what to what before reading a word.
   The two exceptions both follow the rule rather than break it: a follow row's
   trailing art is the ARTIST's photo, so it goes round (`--round`), and a
   milestone row has no person at all, so the album cover stands in as the
   subject and stays square (`--art`). */
/* ⚠️ Avatar over timestamp, as one column. The time used to sit in `.ntf-body`
   under the copy; it moved here because the body is the row's only elastic
   part and the time was the one thing in it that never needed to be. Applied
   to BOTH the feed and the inbox — they are one component, and a time in a
   different place on each is exactly the divergence the ⚠️ in
   renderFriendFeed warns about. */
.ntf-who {
  display: flex; flex-direction: column; align-items: center;
  gap: 5px; flex-shrink: 0;
}
.ntf-who .ntf-time { margin-top: 0; text-align: center; }

.ntf-ava {
  position: relative; flex-shrink: 0;
  width: 44px; height: 44px; border-radius: 50%;
  background-size: cover; background-position: center;
  background-color: var(--sd-card);
}
.ntf-ava--art { border-radius: 9px; }   /* subject is a record, not a person */
.ntf-ava--art .ntf-badge { right: -5px; bottom: -5px; }
.ntf-badge {
  /* Pulled in from the -5px a square corner wanted: a circle's bottom-right
     sits ~6px further in, so the old offset left the badge visibly detached. */
  position: absolute; right: -2px; bottom: -2px;
  width: 20px; height: 20px; border-radius: 50%;
  display: grid; place-items: center;
  border: 2px solid var(--sd-bg);
  color: #16130d;               /* glyph ink on the coloured disc */
}
.ntf-badge svg { width: 11px; height: 11px; }
.ntf-badge--like      { background: #e05a6b; }
.ntf-badge--follow    { background: #6f9fe8; }
.ntf-badge--comment   { background: #7fcf9e; }
.ntf-badge--release   { background: #c9a7f0; }
.ntf-badge--milestone { background: var(--star); }
.ntf-badge--playlist  { background: #f0a77f; }
/* Home-feed-only kinds — the things a friend DID. The review badge takes the
   accent so the rows the app is actually about catch the eye in a scroll; a
   rating is the same event minus the words, so it gets the same gold, muted.
   The last three mirror the log sheet's own toggles.
   ⚠️ `.ntf-badge--release` is now inbox-only, and `--trending` is gone with the
   feed's systemic rows. */
.ntf-badge--review    { background: var(--star); }
.ntf-badge--rating    { background: #b99a5e; }
.ntf-badge--fav       { background: #ff5c8a; }
.ntf-badge--listened  { background: #7fcf9e; }
.ntf-badge--later     { background: #8fb8d8; }
/* An artist photo, not a cover — rounded like every other artist thumb. */
.ntf-art--round { border-radius: 50%; }

.ntf-body { flex: 1; min-width: 0; padding-right: 4px; }
/* album = regular weight, artist/person = bold — the app-wide convention */
.ntf-text { font-size: 12.5px; line-height: 1.42; color: var(--sd-ink2); }
.ntf-text b { font-weight: 700; color: var(--sd-ink); }
.ntf-text i { font-style: normal; font-weight: 400; color: var(--sd-ink); }
.ntf-quote {
  margin-top: 6px; padding: 7px 9px;
  border-left: 2px solid var(--sd-line); border-radius: 0 8px 8px 0;
  background: var(--sd-hover);
  font-size: 11.5px; font-style: italic; line-height: 1.35;
  color: var(--sd-ink2);
}
.ntf-time { margin-top: 5px; font-family: var(--font-mono); font-size: 9.5px; color: var(--sd-ink3); }

.ntf-art {
  flex-shrink: 0; width: 40px; height: 40px;
  border-radius: 5px; margin-top: 2px;
  background-size: cover; background-position: center;
  background-color: var(--sd-card);
}
/* The row's object: the cover, with the score a friend gave it UNDER it.
   Stacking costs the copy column no width — a score parked beside the thumb
   took ~35px off a row that already wraps. See the ⚠️ in renderFriendFeed for
   which rows get a number at all. */
.ntf-obj {
  flex-shrink: 0;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
}
/* The score, inside the sentence: "reviewed X by Y a 4.5".
   ⚠️ Takes the row's own ink, NOT `--star` — the gold read as an alert on a row
   whose whole job is to be scanned past, and it collided with the upvote pill's
   `is-on` gold in the same row. `--sd-ink` follows the SURFACE, so it stays
   legible on both themes. Mono so the number reads as a value, not a word. */
.ntf-line-score {
  font-family: var(--font-mono);
  font-weight: 700;
  color: var(--sd-ink);
  white-space: nowrap;
}
.ntf-follow {
  flex-shrink: 0; margin-top: 4px;
  padding: 6px 13px; border-radius: 999px;
  border: 1px solid var(--sd-line); background: none;
  font-family: var(--font-main); font-size: 11px; font-weight: 700;
  color: var(--sd-ink); cursor: pointer;
  transition: background .15s, color .15s, border-color .15s;
}
.ntf-follow:hover { background: var(--star); border-color: var(--star); color: #16130d; }
.ntf-follow.is-on {
  background: none; border-color: var(--sd-line);
  color: var(--sd-ink3); font-weight: 500;
}
.ntf-empty {
  padding: 34px 0; text-align: center;
  font-family: var(--font-mono); font-size: 10.5px; color: var(--sd-ink3);
}

/* ── SETTINGS ─────────────────────────────────────────────── */
.set-scroll { padding: 4px 12px 96px; }
.set-title {
  margin: 4px 0 13px; font-size: 26px; font-weight: 800;
  letter-spacing: -0.7px; color: var(--sd-ink);
}

/* Account card — the one non-list block; taps through to Edit Profile */
.set-acct {
  display: flex; align-items: center; gap: 11px;
  padding: 11px; margin-bottom: 9px;   /* Sign out sits right under it, as a pair */
  border-radius: 12px; cursor: pointer;
  background: var(--sd-card); box-shadow: var(--sd-card-hi);
  transition: filter .15s;
}
.set-acct:hover { filter: brightness(1.12); }
.set-acct-pic {
  width: 46px; height: 46px; flex-shrink: 0;
  border-radius: 10px; background-size: cover; background-position: center;
}
.set-acct-body { flex: 1; min-width: 0; }
.set-acct-name { font-size: 15.5px; font-weight: 800; letter-spacing: -0.3px; color: var(--sd-ink); }
.set-acct-handle { margin-top: 2px; font-family: var(--font-mono); font-size: 9.5px; color: var(--sd-ink2); }
.set-acct-edit {
  display: flex; align-items: center; gap: 1px; flex-shrink: 0;
  font-family: var(--font-mono); font-size: 9.5px; color: var(--sd-ink3);
}

.set-sec { margin-bottom: 17px; }
.set-sec-hd {
  padding: 0 4px 7px;
  font-family: var(--font-mono); font-size: 9.5px;
  text-transform: uppercase; letter-spacing: .12em;
  color: var(--sd-ink3);
}
.set-card {
  border-radius: 12px; overflow: hidden;
  background: var(--sd-card); box-shadow: var(--sd-card-hi);
}
.set-row {
  display: flex; align-items: center; gap: 11px;
  padding: 12px 13px;
  border-top: 1px solid var(--sd-line);
}
.set-row:first-child { border-top: none; }
.set-row--link { cursor: pointer; transition: background .15s; }
.set-row--link:hover { background: var(--sd-hover); }
.set-row-body { flex: 1; min-width: 0; }
.set-row-label { font-size: 12.5px; font-weight: 600; color: var(--sd-ink); }
.set-row-sub { margin-top: 2px; font-size: 10.5px; line-height: 1.3; color: var(--sd-ink3); }
.set-chev { width: 15px; height: 15px; flex-shrink: 0; color: var(--sd-ink3); }

/* Switch — recessed track, raised knob; ON fills with the star yellow */
.set-sw {
  flex-shrink: 0; position: relative;
  width: 40px; height: 23px; padding: 0;
  border: none; border-radius: 999px; cursor: pointer;
  background: var(--sd-well);
  box-shadow: inset 0 1px 2px rgba(0,0,0,0.4);
  transition: background .18s;
}
.set-sw-knob {
  position: absolute; top: 3px; left: 3px;
  width: 17px; height: 17px; border-radius: 50%;
  background: #e8e2d6;
  box-shadow: 0 1px 3px rgba(0,0,0,0.45);
  transition: transform .18s ease;
}
.set-sw.is-on { background: var(--star); }
.set-sw.is-on .set-sw-knob { transform: translateX(17px); background: #16130d; }

/* Segmented picker (Theme) */
.set-seg {
  flex-shrink: 0; display: flex; gap: 2px;
  padding: 2px; border-radius: 999px;
  background: var(--sd-well);
  box-shadow: inset 0 1px 2px rgba(0,0,0,0.35);
}
.set-seg button {
  border: none; background: none; cursor: pointer;
  padding: 5px 10px; border-radius: 999px;
  font-family: var(--font-main); font-size: 10.5px; font-weight: 600;
  color: var(--sd-ink2);
  transition: background .15s, color .15s;
}
.set-seg button:hover { color: var(--sd-ink); }
.set-seg button.active { background: var(--star); color: #16130d; font-weight: 700; }

/* Connected-service status pill */
.set-pill {
  flex-shrink: 0;
  padding: 4px 10px; border-radius: 999px;
  border: 1px solid var(--sd-line);
  font-family: var(--font-mono); font-size: 9.5px;
  color: var(--sd-ink3);
}
.set-pill--on { border-color: transparent; background: rgba(79,199,138,0.16); color: #4fc78a; }
.set-svc-dot { flex-shrink: 0; width: 9px; height: 9px; border-radius: 50%; }

.set-signout {
  width: 100%; margin-bottom: 20px; padding: 13px;
  border-radius: 12px; cursor: pointer;
  border: 1px solid rgba(224,90,107,0.35); background: none;
  font-family: var(--font-main); font-size: 12.5px; font-weight: 700;
  color: #e0616f;
  transition: background .15s;
}
.set-signout:hover { background: rgba(224,90,107,0.12); }

/* ── SHOP BUTTON — the nav scoop ──────────────────────────────
   Sits exactly where the pet did: same 63×30 box, same 49.1% centre (the notch
   in BOTTOM_NAV_FULL_INDENT.svg is a few units left of true centre), same
   z-index 7 to clear the emboss. The one difference that matters is
   `pointer-events` — the pet was decorative, this is pressable, so it takes the
   whole scoop as its hit area. Keep the geometry here in step with `.sd-scene`
   above; if the pet ever comes back they have to land on the same spot. */
.sd-shop-btn {
  position: absolute;
  left: 49.1%; transform: translateX(-50%);
  bottom: 2px;
  z-index: 7;
  width: 63px; height: 30px;
  display: flex; align-items: center; justify-content: center;
  padding: 0; border: 0; background: none;
  cursor: pointer;
  color: var(--sd-ink3);
  transition: color .18s, transform .18s;
}
/* ⚠️ The scoop's FLAT TOP is 63px, not its 123px opening — the notch narrows as
   it rises, so the glyph is sized against the narrow end, same as the face was.
   22px square leaves the bag breathing room on all four sides at 30px depth. */
.sd-shop-btn .sd-dot-ico { width: 22px; height: 22px; }
.sd-shop-btn:hover  { color: var(--star, #e8a83c); transform: translateX(-50%) translateY(-1px); }
.sd-shop-btn:active { transform: translateX(-50%) translateY(0) scale(.94); }
.sd-shop-btn.active { color: var(--star, #e8a83c); }
.sd-shop-btn:focus-visible { outline: 2px solid var(--star, #e8a83c); outline-offset: 3px; border-radius: 8px; }

/* ── SHOP SCREEN — bento sheets ───────────────────────────────
   The system in one line: a section is a SHEET, a sheet is TILES that butt
   together over a 3px seam of screen bg. Density inside a sheet, air between
   sheets. Four sheet types only — `--bento`, `--shelf`, `--row`, `--list`.

   ⚠️ `--list` was the fourth, and it took an argument to earn its place: the
   other three are trays of SWATCHES, things you judge at a glance, and an event
   is not one. It is four facts read in order — who, what, where-and-when, how
   much — which is a ROW, not a square. Anything that can be judged by looking
   at it belongs in one of the first three; only reach for a fifth type when
   something genuinely cannot be read in any of the four.

   ⚠️ Colour comes from the TILE, not the sheet. Frames and badges pass a
   `--tint` (an "r,g,b" triple) and the field fills with a soft wash of it; the
   home bento takes its colour from the cover, and this is the same idea applied
   to a product. Gold stays reserved for what you can act on. */
.shop-scroll { padding: 4px 12px 96px; }

/* ── The store's name, DRAWN and not set ──────────────────────
   ⚠️ It used to be DM Sans 800 at 34px — the Apple-Store half of the brief
   showing up in the type. It is now spelled in the dot language instead (see
   SHOP_WORD in screens.js), for a reason worth keeping: there is no wordmark
   FONT in this project. The SPINDECK lockup is a drawn PNG, so the only way for
   a heading to sound like the brand is to be built from the brand's own module
   — the same rounded square, the same 14% corner, the same currentColor tint as
   the bag in the nav scoop.
   ⚠️ Sized by HEIGHT, never width. The mark is a 19×5 grid in a viewBox, so
   one number sets the cap height and the width follows; `width: auto` is what
   stops a flex or grid parent from stretching the dots off their own pitch.
   ⚠️ The lede under it is gone on purpose. "Make Spindeck yours" told you
   nothing the shelves below don't say better, and the aisle bar needs the room
   more — a store's tagline is the one line a shopper never reads. */
/* ONE ROW with the back pill (Eric, 2026-09-17): the pill left, the word in
   the upper right at the pill's level, small enough to be a label rather than
   a heading — it was 34px on a line of its own and pushed the store down. */
.shop-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.shop-top .plp-back-pill { margin-bottom: 0; }
.shop-title { margin: 0 4px 0 0; }
.shop-title-mark {
  display: block;
  height: 18px; width: auto;
  color: var(--sd-ink);
}
/* The word itself, for screen readers and for find-in-page. SD_DOTS ships its
   svg `aria-hidden`, so without this the shop screen has no name at all. */
.shop-sr {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; white-space: nowrap; clip-path: inset(50%);
}

/* ── The aisle bar ────────────────────────────────────────────
   Four categories, and the only STICKY thing on the screen: the name of the
   store is a greeting you read once and let scroll away, but the aisle you are
   standing in is where you ARE, and you need it to leave.

   ⚠️ It needs an opaque background of its own. `.shop-scroll` paints nothing,
   so a transparent bar would have event art and theme swatches sliding through
   it — and it has to be `--sd-bg`, NOT `--bg`. `--bg` is a global that stays
   dark on both shells (`.s-home-v3` hard-codes `#111116` and the light shell
   never redefines it), so a bar painted with it dropped a black band across
   Float·Light and swallowed three of the four tabs. `--sd-bg` is the token that
   actually tracks the shell, which is the whole reason it exists.
   ⚠️ Bled to the frame edges and padded back, the `.shop-sheet--shelf` idiom —
   a sticky bar that stopped 12px short of each edge would leave two columns of
   screen for the tiles to show through as they pass under it. */
/* ⚠️ NOT STICKY any more (Eric, 2026-09-17): the bar scrolls away with the
   page, so it needs no opaque background either — the notes above describe
   the sticky bar it was. Still bled to the frame edges so the row can scroll
   sideways edge to edge. */
.shop-cats {
  display: flex; gap: 4px;
  margin: 0 -12px 2px; padding: 7px 12px 9px;
  overflow-x: auto; scrollbar-width: none;
}
.shop-cats::-webkit-scrollbar { display: none; }
.shop-cat {
  flex: 0 0 auto;
  padding: 6px 12px; border-radius: 999px;
  border: 1px solid rgba(232,226,214,.14); background: none;
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .09em; text-transform: uppercase;
  color: var(--sd-ink3); cursor: pointer; white-space: nowrap;
  transition: background .15s, color .15s, border-color .15s;
}
.s-home-v3--light .shop-cat { border-color: rgba(26,18,8,.14); }
.shop-cat:hover { color: var(--sd-ink); }
/* Gold is reserved for what you can act on, and the aisle you are in is the one
   thing on this bar that is not an offer — so the ON state takes the fill and
   the other three stay outlines. */
.shop-cat.is-on {
  background: var(--star, #e8a83c); border-color: var(--star, #e8a83c); color: #1a1208;
}
.shop-cat:focus-visible { outline: 2px solid var(--star, #e8a83c); outline-offset: 2px; }

/* ── The filter ───────────────────────────────────────────────
   One attribute on the screen, one on everything that belongs to an aisle, and
   nothing in JS but the write. A thing can be in TWO aisles
   (`data-cat="general events"`), which is how General shows a couple of events
   without a second copy of the tile to keep in step — and anything with NO
   `data-cat` (the back pill, the title, the bar itself, the footnote) is never
   filtered at all.

   ⚠️ The tabs carry `data-go`, not `data-cat`. They live inside `.shop-scroll`
   and a tab that could filter itself away would be a one-way door.
   ⚠️ Four rules rather than one: CSS cannot compare an ancestor's attribute
   value against a descendant's, so each aisle has to name itself. Add a fifth
   category and add a fifth line here. */
.s-shop[data-cat="general"] .shop-scroll [data-cat]:not([data-cat~="general"]),
.s-shop[data-cat="events"]  .shop-scroll [data-cat]:not([data-cat~="events"]),
.s-shop[data-cat="themes"]  .shop-scroll [data-cat]:not([data-cat~="themes"]),
.s-shop[data-cat="badges"]  .shop-scroll [data-cat]:not([data-cat~="badges"]) {
  display: none;
}

.shop-sec-hd {
  display: flex; align-items: baseline; gap: 7px;
  padding: 18px 4px 7px;
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 500;
  text-transform: uppercase; letter-spacing: .09em; color: var(--sd-ink3);
}
.shop-sec-hd span { text-transform: none; letter-spacing: 0; opacity: .62; }

/* The sheet. `overflow: hidden` is what rounds the tiles at the outer corners
   without any tile having to know it is on an edge — the same trick the bento's
   frame plays, and what keeps the inner junctions square. */
.shop-sheet {
  position: relative;
  border-radius: 18px;
  overflow: hidden;
}
/* ⚠️ The SHEET casts nothing — the tiles are what sit proud, and they do it
   the way every other card here does: an inset top highlight, no cast shadow.
   Each sheet type styles its own tiles (`.shop-tile--theme`, `.shop-tile--sm`);
   there is no shared tile base, because the two have nothing in common beyond
   that fill. */

/* ── Pro showcase — the real compact bento, on display ────────
   The bento inside here is the same component the home screen renders, so this
   block styles the CASE, never the bento. ⚠️ Anything that reaches inside and
   restyles a `.v3-*` will drift from home the first time home changes — if the
   showcase needs the bento to look different, that is a sign the bento should
   change, not that the shop should override it. */
/* ── The Pro showcase, as a SCALE MODEL ────────────────────────────
   The showcase is the real bento, and at full size it reads as the home screen
   with a shop bolted above it — you are looking at your phone, not at a
   product. Shrunk and centred, with air on every side, the same object reads as
   a display model: the thing on the shelf, not the thing you are using.

   ⚠ The scale is a TRANSFORM on a wrapper, never a width. Everything inside the
   bento is px (type, radii, the CD) against a %-positioned frame, so narrowing
   the box would shrink the frame and leave the type at full size — the layout
   would break rather than scale. `transform: scale()` is the only thing that
   takes the whole object down uniformly, which is what a scale model IS.
   ⚠ And it lives on `.shop-model`, not on `.v3-bento`: the showcase styles the
   CASE, never the product. */
.shop-showcase {
  --shop-model: 0.76;      /* the model's size against the real thing */
  position: relative;
  margin: 0 -2px 10px;
  padding-top: 6px;        /* the display needs air above it, not just beside */
}
.shop-model {
  transform: scale(var(--shop-model));
  transform-origin: top center;
  /* ⚠ A transform does not change the LAYOUT box, so the model's full-size
     height would leave a hole under it. Take the difference back. A percentage
     margin resolves against the parent's WIDTH, and the bento's height is its
     width × `--v3-bento-hw`, so this is exactly the height the scale removed. */
  margin-bottom: calc(-1 * (1 - var(--shop-model)) * var(--v3-bento-hw) * 100%);
}
/* The bento carries its own margin for the home screen's gutters; the shop has
   its own padding, so take that back rather than paying for it twice. */
.shop-showcase .v3-bento { margin: 0; }
/* `touch-action` for the cover is set with `.v3-album` / `.v3-album--wheel`,
   up by the cover's own rules — one place, ordered by specificity. It used to
   be two inline writes from JS racing each other, which is why a rule down here
   could never land. */

/* The one label. It sits ON the case, not in the bento, so nothing about the
   component has to know it is being sold. */
/* ⚠ The tag does NOT scale with the model. It is a label on the display, not a
   part of the product — shrinking it would make it a feature of the bento. It
   is therefore positioned off the MODEL's left edge, which the scale moved
   inward by half the width the model gave up. */
.shop-pro-tag {
  position: absolute; z-index: 8; top: -1px;
  left: calc((1 - var(--shop-model)) / 2 * 100% - 2px);
  padding: 4px 10px; border-radius: 999px;
  font-family: var(--font-mono); font-size: 9px; font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase;
  background: var(--star, #e8a83c); color: #1a1208;
  box-shadow: 0 2px 8px rgba(0,0,0,.35);
}

/* ── The shelf wheel — what Pro adds to the hold ──
   Built by `proWheelInit` and appended INTO `.v3-album`, so it inherits the
   art's rounded corners and covers exactly the thing you are holding. Appears
   in two places: always in the shop's showcase (it is the demo), and on the
   home bento only for a Pro account. */
/* ⚠ NO `backdrop-filter` here — this used to blur the backdrop and it could
   not be made to cover the cover. `backdrop-filter` samples its backdrop in the
   coordinate space of its BACKDROP ROOT, and `.v3-album` sits under both a
   `transform` (the sub-pixel optical nudge, `--album-dx`) and the viewer's
   `zoom` on `#phone-container`. Neither is accounted for, so the sampled rect
   lands offset and a strip of the cover stayed sharp. Growing the box only
   moved which edge the strip appeared on — left, then bottom — because the
   offset is in the sampling, not in the geometry.

   The blur is an ordinary painted layer instead (`.shop-pick-bg`): a copy of
   the cover, blurred with `filter`. Ancestors clip normal content reliably, so
   it cannot fall short, and it costs one background-image the browser has
   already decoded.

   ⚠ `inset: -4px` (symmetric) stays as belt and braces for the sub-pixel
   nudge, and the parent's `overflow: hidden` trims it. It MUST stay symmetric:
   `top: calc(50% - 13px)` below centres the wheel on this box, and a box grown
   equally on both sides has the same centre — an uneven bleed would slide the
   selection off the lens by half the difference. */
.shop-pick {
  /* ⚠️ ONE number, four jobs: the row height, the lens height, and the two
     `calc(50% - var(--pick-h)/2)` that centre the lens and the list on the art.
     It must equal `SHOP_PICK_H` in app.js, which is what `paint()` MOVES the
     list by — if they disagree, the row under the lens stops being the row you
     get. The finger is counted separately, in rendered px (`rowPx()`), because
     the shop scales the whole model and CSS px stop being screen px. */
  --pick-h: 34px;
  position: absolute; inset: -4px; z-index: 4;
  overflow: hidden; border-radius: inherit;
  opacity: 0; pointer-events: none;
  transition: opacity .18s;
}
/* The blurred cover. `scale` overscans so the blur's own soft, half-transparent
   edge is pushed outside the wheel's `overflow: hidden` instead of showing as a
   pale rim inside it. Filled by `syncBg()` on every arm. */
.shop-pick-bg {
  position: absolute; inset: 0;
  background-size: cover; background-position: center;
  filter: blur(9px); transform: scale(1.14);
}
/* The wash, over the blur — so it must be a LAYER, not a background on
   `.shop-pick`: a parent's own background paints beneath its children, which
   would put the tint under the very thing it is meant to darken. */
.shop-pick-tint { position: absolute; inset: 0; background: rgba(10,9,8,.55); }
/* Two hosts: the shop's case, and — once you own Pro — the home bento itself. */
.shop-showcase.is-armed .shop-pick,
.v3-bento.is-armed .shop-pick { opacity: 1; }
/* ⚠️ The same `calc(50% - var(--pick-h) / 2)` on BOTH the list and the lens
   centres selection 0 under the lens without either knowing the art's size.
   The art is square and sized by the bento's percentage grid, so there is no
   height to hard-code. Derived from `--pick-h` rather than written out, so the
   row height and the centring cannot drift apart. */
/* ⚠ Colour comes from the ALBUM, not from gold. `--v3-accent` is extracted
   from the cover by `applyAlbumColors` and set on the screen element, so it
   inherits down to here and the wheel re-tints with every album — the bento's
   own "album art drives colour" rule, applied to the thing sitting on top of
   the art. Gold is the fallback only, for a screen with no cover to extract
   from. `color-mix` because the var is one colour and the lens needs it at two
   alphas; a fixed `rgba()` triple cannot follow a variable. */
.shop-pick-lens {
  position: absolute; top: calc(50% - var(--pick-h) / 2);
  left: 10px; right: 10px; height: var(--pick-h);
  border-radius: 9px;
  background: color-mix(in srgb, var(--v3-accent, #e8a83c) 18%, transparent);
  border: 1px solid color-mix(in srgb, var(--v3-accent, #e8a83c) 50%, transparent);
}
.shop-pick-list {
  position: absolute; top: calc(50% - var(--pick-h) / 2); left: 0; right: 0;
  list-style: none; margin: 0; padding: 0;
  transition: transform .2s cubic-bezier(.22,1,.36,1);
}
/* ⚠️ A ROW IS A FIXED BOX. `height: var(--pick-h)` and `line-height: 1` keep it
   one — the type inside is free to change size, the box is not. Let the row
   grow with its font and every row below it shifts, so the drag arithmetic
   (whole units of `--pick-h`) walks out of step with what is under the lens.
   Flex + `align-items: center` is what lets 24px type sit in a 34px box without
   a line-height to re-tune every time the size changes. */
.shop-pick-i {
  display: flex; align-items: center; gap: 9px;
  height: var(--pick-h); line-height: 1; padding: 0 18px;
  font-family: var(--font-main); font-size: 13px; font-weight: 500;
  color: rgba(232,226,214,.38);
  transition: color .18s, font-size .18s, letter-spacing .18s;
}
.shop-pick-l { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The count — how many albums the shelf holds, so reading down the wheel shows
   where your listening actually sits. ⚠️ SERIF, against the labels' DM Sans: a
   different voice reads as an annotation on the list rather than as part of a
   label. Crimson Text is the app's serif — it sets the review composer.
   Fixed min-width so the labels stay on one left edge as the digits change. */
.shop-pick-n {
  flex-shrink: 0; min-width: 22px; text-align: right;
  font-family: 'Crimson Text', Georgia, serif; font-style: italic;
  font-size: 12px; font-weight: 400;
  color: inherit; opacity: .5;
}
/* The row under the lens is set BIGGER, not just brighter — that is what makes
   a list read as a wheel, the selection nearest the eye.
   ⚠ `height` and `line-height` stay pinned at 26px (= SHOP_PICK_H). Only the
   glyphs grow. Let the row itself grow and every row below it shifts, so the
   drag arithmetic — which counts in whole units of SHOP_PICK_H — would walk
   out of step with what is under the lens. */
/* The row under the lens is set MUCH bigger, not just brighter — the SIZE STEP
   is what makes a list read as a wheel, the selection nearest the eye. The
   number steps up with it but stays well under the label, so it keeps
   annotating rather than competing. */
.shop-pick-i.is-on {
  color: var(--v3-accent, #e8a83c);
  font-size: 24px; font-weight: 700; letter-spacing: -.7px;
}
.shop-pick-i.is-on .shop-pick-n { font-size: 14px; opacity: .75; }

/* The pitch row under the showcase — a plain sheet, because the bento above it
   is already doing all the talking. */
.shop-sheet--pro {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 11px 13px;
  background: var(--sd-card); box-shadow: var(--sd-card-hi);
}
.shop-buy-l { font-family: var(--font-mono); font-size: 8.5px; line-height: 1.5; color: var(--sd-ink2); }
.shop-buy-l b {
  display: block; margin-bottom: 1px;
  font-family: var(--font-main); font-size: 17px; font-weight: 800;
  letter-spacing: -.5px; color: var(--sd-ink);
}

@media (prefers-reduced-motion: reduce) {
  .shop-pick, .shop-pick-list, .shop-pick-i { transition: none; }
}

.shop-pro-btn {
  flex-shrink: 0; padding: 8px 15px; border: 0; border-radius: 999px;
  font-family: var(--font-main); font-size: 13.5px; font-weight: 800;
  background: var(--star, #e8a83c); color: #1a1208; cursor: pointer;
  transition: filter .15s, transform .15s;
}
.shop-pro-btn small { font-size: 9px; font-weight: 700; opacity: .72; }
.shop-pro-btn:hover  { filter: brightness(1.08); }
.shop-pro-btn:active { transform: scale(.96); }

/* ── sheet: --shelf ──
   The `.v3-aa-row` idiom, unchanged: bleed the rail to the frame edge, pad it
   back, and match `scroll-padding-left` to the pad so a snap does not scroll the
   padding away. The last tile peeking is the point — it says "keep going". */
.shop-sheet--shelf {
  display: flex; gap: 3px;
  overflow-x: auto; scroll-snap-type: x proximity;
  margin: 0 -12px; padding: 0 12px;
  scroll-padding-left: 12px;
  scrollbar-width: none;
  border-radius: 0;   /* the tiles round themselves — a scrolling rail cannot clip them */
}
.shop-sheet--shelf::-webkit-scrollbar { display: none; }
.shop-tile--theme {
  flex: 0 0 116px; scroll-snap-align: start;
  display: flex; flex-direction: column; gap: 1px;
  padding: 9px 9px 10px; border-radius: 15px;
  background: var(--sd-card); box-shadow: var(--sd-card-hi);
}
.shop-tile--theme .shop-field { aspect-ratio: 1; margin-bottom: 8px; }
.shop-tile--theme .shop-buy,
.shop-tile--theme .shop-owned { margin-top: 8px; align-self: flex-start; }

/* ── sheet: --row ──
   N equal tiles across. `1fr` columns rather than a fixed width so four frames
   and four badges line up on the same grid without either knowing about the
   other. */
.shop-sheet--row {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 3px;
}
.shop-tile--sm {
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 10px 5px 9px;
  background: var(--sd-card); box-shadow: var(--sd-card-hi);
}
.shop-tile--sm .shop-field { width: 42px; height: 42px; }

/* ── sheet: --list ──
   Full-width rows, same 3px seam. Nothing else in the shop stacks vertically,
   because nothing else in the shop is a sentence. */
.shop-sheet--list { display: flex; flex-direction: column; gap: 3px; }

/* An EVENT. Art, then the three lines, then the price — left to right in the
   order you ask the questions.
   ⚠️ The art is a `background-image` on a fixed box, not an <img>: it is a
   crop of a record sleeve at 46px, so cover-and-clip is the whole job and an
   <img> would only add a second thing to size. */
.shop-tile--event {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 11px 9px 9px;
  background: var(--sd-card); box-shadow: var(--sd-card-hi);
}
.shop-ev-art {
  position: relative; flex: 0 0 46px; width: 46px; height: 46px;
  border-radius: 12px; overflow: hidden;
  background: rgba(232,226,214,.06) center / cover no-repeat;
  transition: filter .2s, opacity .2s;
}
/* The Pro flag sits ON the art rather than beside the price, so the row reads
   as locked before you have got as far as the money. */
.shop-ev-pro {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 2px 0; text-align: center;
  font-family: var(--font-mono); font-size: 7px; font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase;
  background: var(--star, #e8a83c); color: #1a1208;
}
/* `min-width: 0` is what lets the three lines ellipsis instead of shoving the
   price off the row — a flex item's default min-width is its CONTENT. */
.shop-ev-line { display: flex; flex-direction: column; gap: 1px; flex: 1; min-width: 0; }
.shop-ev-who, .shop-ev-what, .shop-ev-where {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.shop-ev-who   { font-size: 13px; font-weight: 800; letter-spacing: -.2px; color: var(--sd-ink); }
.shop-ev-what  { font-size: 10.5px; color: var(--sd-ink2); }
.shop-ev-where { font-family: var(--font-mono); font-size: 8.5px; color: var(--sd-ink3); }

/* ── What Pro actually buys you here ──
   ⚠️ BOTH states ship in the markup and CSS picks one, gated on `body.sd-pro`
   like every other plan gate. The alternative — rendering only the state you
   are entitled to — would need a re-render on every plan change, and the plan
   change is exactly the moment this screen is being demonstrated: the toolbar's
   Free/Pro switch unlocks the whole list in front of you, with the art coming
   up out of its wash, and nothing rebuilds. That IS the pitch.
   ⚠️ `.shop-owned` is gated with the button. Buy a ticket as Pro, drop back to
   Free, and a bare "Going" pill would otherwise be left sitting on a row you
   are no longer allowed into. */
.shop-lock {
  flex-shrink: 0; white-space: nowrap;
  padding: 5px 11px; border-radius: 999px;
  border: 1px dashed rgba(232,226,214,.22);
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .04em; color: var(--sd-ink3);
}
.s-home-v3--light .shop-lock { border-color: rgba(26,18,8,.22); }
.shop-tile--event.is-pro .shop-buy,
.shop-tile--event.is-pro .shop-owned { display: none; }
.shop-tile--event.is-pro .shop-ev-art { filter: saturate(.25); opacity: .62; }
body.sd-pro .shop-tile--event.is-pro .shop-buy,
body.sd-pro .shop-tile--event.is-pro .shop-owned { display: inline-block; }
body.sd-pro .shop-tile--event.is-pro .shop-lock { display: none; }
body.sd-pro .shop-tile--event.is-pro .shop-ev-art { filter: none; opacity: 1; }

/* The field — where the product sits. Themes fill it with their own palette
   (inline), everything else washes it with the tile's `--tint`. */
.shop-field {
  position: relative; border-radius: 12px; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
}
.shop-field--tint {
  background: rgba(var(--tint), .13);
  color: rgb(var(--tint));
}
.shop-field--ico .sd-dot-ico { width: 15px; height: 15px; }
.shop-chip {
  position: absolute; left: 9px; bottom: 9px;
  width: 13px; height: 13px; border-radius: 14%;   /* SD_DOTS' cornerFrac */
}
.shop-ring { width: 24px; height: 24px; border-radius: 50%; }

.shop-tile-name {
  font-size: 12.5px; font-weight: 800; letter-spacing: -.2px; color: var(--sd-ink);
  text-align: center;
}

/* ── Tags — a LIST, not the 4-up grid the frames and badges use.
   A tag's label is a word of a length it chose (that is half of what you are
   buying: "DaisyChainsFestival2026" is long BECAUSE it is specific), and equal
   cells would truncate exactly the ones worth selling. Each row is the chip
   itself — there is nothing to picture beyond the thing you would wear. */
.shop-sheet--tags { display: flex; flex-direction: column; gap: 3px; }
.shop-tagrow {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 12px;
  background: var(--sd-card); box-shadow: var(--sd-card-hi);
}
.shop-tagrow-l { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; min-width: 0; flex: 1; }
.shop-tagrow-s {
  font-family: var(--font-mono); font-size: 8.5px; color: var(--sd-ink2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
}
.shop-tile--theme .shop-tile-name { font-size: 14px; text-align: left; }
.shop-tile-sub { font-family: var(--font-mono); font-size: 8.5px; color: var(--sd-ink2); }

/* Buy / Owned share a footprint so nothing reflows when sdBuy swaps one for
   the other. The price IS the button — there is no second word to read. */
.shop-buy, .shop-owned {
  flex-shrink: 0; text-align: center;
  padding: 5px 11px; border-radius: 999px;
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 700;
  letter-spacing: .04em;
}
.shop-buy {
  border: 1px solid rgba(232,226,214,.16); background: none;
  color: var(--sd-ink); cursor: pointer;
  transition: background .15s, color .15s, border-color .15s;
}
.s-home-v3--light .shop-buy { border-color: rgba(26,18,8,.16); }
.shop-buy:hover { background: var(--star, #e8a83c); border-color: var(--star, #e8a83c); color: #1a1208; }
.shop-buy:focus-visible { outline: 2px solid var(--star, #e8a83c); outline-offset: 2px; }
/* ⚠ Visible by DEFAULT. The fade belongs to the pill `sdBuy` swaps in, not to
   the pill the markup already ships — the tiles you start out owning (Funky 01,
   Hairline, Devotee) render straight from `shopHtml` with no JS to add `is-in`,
   so an `opacity: 0` base left all three blank. The fade is opt-in now. */
.shop-owned {
  border: 1px solid transparent; color: var(--sd-ink3);
  transition: opacity .22s;
}
.shop-owned--new { opacity: 0; }
.shop-owned--new.is-in { opacity: 1; }
.shop-owned--pro { color: var(--star, #e8a83c); }
/* A skin you own: the pill is a toggle. Wearing = filled in the star, the same
   look the buy button gives on hover, so on/off reads at a glance. */
.shop-wear.is-on { background: var(--star, #e8a83c); border-color: var(--star, #e8a83c); color: #1a1208; }
.shop-wear.is-on:hover { filter: brightness(1.07); }

.shop-note {
  margin-top: 18px; padding: 0 4px;
  font-family: var(--font-mono); font-size: 9px; line-height: 1.5; color: var(--sd-ink3);
}

@media (prefers-reduced-motion: reduce) {
  .shop-owned, .shop-buy, .shop-pro-btn, .shop-cat, .shop-ev-art { transition: none; }
}


/* ═══════════════════════════════════════════════════════════════════════
   csharpuser (2026-09-22) — THE REVIEW-ONLY FORK
   ═══════════════════════════════════════════════════════════════════════
   User testing said the app was too complicated, so this copy strips home
   down to the review feed. The bento is NOT deleted: it is the album page
   (`--review` + `--album`, see "Fullscreen is the album page" in CLAUDE.md),
   so it stays in the markup and is simply not shown until a record is
   opened. Everything the bento carried on home — the featured album, For
   You, the CD, the Live pill — goes with it; the feed gets the whole body.
   ⚠ `.s-home-v3--review` is never set without `--album`, so this one test
   covers the album page and the artist page both. */
.s-home-v3:not(.s-home-v3--review) .v3-bento { display: none; }
/* The feed used to start under the bento's 22px bottom margin; give it the
   same breathing room off the header now that it is first in the body. */
.s-home-v3:not(.s-home-v3--review) .v3-scroll-area { padding-top: 6px; }
/* The friends deck takes the bento's place on home, so the feed goes back
   under a 22px bottom margin there; the 6px above is only for the bare feed. */
/* 0.14b: this was `.v3-fb ~ .v3-scroll-area` — the air between the deck and
   the feed. The deck has left home for the Trending wall, so that sibling
   never matches here any more and the feed would have lost its top air. Same
   10px, no longer conditional on a deck being above it. */
.s-home-v3:not(.s-home-v3--review) .v3-scroll-area { padding-top: 10px; }


/* ═══════════════════════════════════════════════════════════════════════
   csharpuser (2026-09-23) — THE FRIENDS DECK (friendsBentoHtml · renderFriendsBento)
   ═══════════════════════════════════════════════════════════════════════
   Where the bento was: your friends' recent reviews as a deck of drop-shadow
   cards, NO frame — the iPod's cover flow, centred. ORDER (Eric,
   2026-09-23): the review box FIRST, the deck under it, the title and artist
   under the deck — the covers sat too high on the screen to reach. The front card is the
   bento's album slot 25% smaller, then up again (245px at the 393 frame) in the middle of
   the screen; the cards to come fan off to the right and the dealt ones to
   the left, all turned the same hard angle toward the centre like the iPod
   classic's cover flow, a step further out each so a sliver shows, the
   third faded out before the edge; each card carries its reflection
   (fbLayout in app.js — the transforms are inline, the deck is a ring,
   both sides always dealt). Under it, centred, the album
   page's info box — title large + year, artist below — on the screen bg now
   rather than the shell, so it inks off the --sd-* tokens; and under it the
   friend's review, centred (`.v3-fbr`, below). No album score line between
   them.
   ⚠️ Hidden wherever the real bento shows: the album/artist page (--review)
   and the review page (--rvp). The two are never up together. */
.v3-fb {
  position: relative;
  flex-shrink: 0;
  margin: 6px 10px 22px;
  display: flex; flex-direction: column; gap: 4px;   /* was 8, then 14, then 5 back, then 5 more (Eric) */
}
/* 0.14b: on the Trending wall the deck is the FIRST thing on the page rather
   than something tucked under the bento, so it takes a little more air above
   and a little less below, where the sort bar follows straight on. */
.s-wall2 .v3-fb { margin: 14px 10px 16px; }
.s-home-v3--review .v3-fb,
.s-home-v3--rvp .v3-fb { display: none; }

/* ── The deck ─────────────────────────────────────────────────────────── */
.v3-fb-flow {
  position: relative;
  height: 259px;                 /* one card + 14px of its reflection (22 → 14: the title 8px closer, Eric) */
  margin: 0 -10px;               /* bleeds past the inset to both screen edges; the fan runs off them */
  /* ⚠️ The perspective and its origin are what fbLayout solves against:
     P = 600 — SHORT, so a turned cover's far edge visibly shrinks and it
     reads as a trapezoid splaying away — and the origin at the CENTRE,
     where the front card is. */
  perspective: 600px;
  /* ⚠️ The origin is the covers' MIDDLE (half of FB_CARD, in px — not the
     flow's middle, the flow is taller by the reflection), so a turned cover
     slants top and bottom alike; fbPoseAt drops each side cover so its near
     bottom corner still meets the front cover's bottom line. */
  perspective-origin: 50% 122.5px;
  /* ⚠️ REAL depth sorting: the covers share one 3D space, so the browser
     orders them by their z and splits two that cross along the seam. With
     z-index instead, a swipe had one cover popping over another the instant
     the integers swapped — the flash. fbPaintDeck sets no z-index. */
  transform-style: preserve-3d;
  z-index: 1;
  /* ⚠️ `none`, not `pan-y` (Eric, 2026-09-26: "when u swipe on the carousel
     i dont want to be able to scroll vertically, it does both"): with pan-y
     the phone starts its own vertical scroll the moment a finger wanders,
     so a swipe on the deck dragged the page too. A touch that begins on the
     deck is the deck's, both axes; the page scrolls from anywhere else. */
  touch-action: none;
  /* ⚠️ On the phone the deck painted FLAT on load and only turned 3D after a
     touch (Eric, 2026-09-26) — the DOM was right from the first frame (the
     session replay shows the poses), so it is the compositor: WebKit only
     built the 3D scene once something moved. Promoting the flow to its own
     layer up front makes it build the scene straight away. */
  will-change: transform;
  transform: translateZ(0);
  cursor: grab;
  user-select: none; -webkit-user-select: none;
}
.v3-fb-flow:active { cursor: grabbing; }
/* Mid-drag the deck is painted every move — no tween, or it lags the finger. */
.v3-fb-flow.is-dragging .v3-fb-card, .v3-fb-flow.is-dragging .v3-fb-card::after { transition: none; }
.v3-fb-card {
  position: absolute;
  left: calc(50% - 122.5px); top: 0;   /* centred: half of FB_CARD */
  width: 245px; height: 245px;   /* ⚠️ = FB_CARD in app.js */
  border-radius: 6px;            /* was 10 — less (Eric) */
  transform-origin: 50% 50%;     /* ⚠️ fbLayout's turn is solved about the centre */
  /* No backface-visibility: past 90° a cover shows its (mirrored) back rather
     than vanishing — the Deck tab's turn runs the full ±180 (Eric). */
  background: #222 center / cover no-repeat;
  /* Two shadows: a tight one that separates a card from the one behind it,
     and a long soft one that lifts the deck off the page. */
  box-shadow: 0 3px 8px rgba(0,0,0,0.45), 0 16px 34px rgba(0,0,0,0.5);
  transform-style: preserve-3d;
  transition: transform .46s cubic-bezier(.22, 1, .36, 1), opacity .3s ease;
  will-change: transform;
  cursor: pointer;
  /* THE REFLECTION (the iPod's): the card mirrored under itself, fading out
     within ~20% of its height. The mask is painted in the card's own box
     and flipped WITH it, so "transparent at the top, ink at the bottom"
     comes out strongest against the card's foot. It turns with the card,
     which is what sells the floor. */
  -webkit-box-reflect: below 2px linear-gradient(transparent 79%, rgba(0,0,0,0.3) 100%);   /* was 60%: a shorter, faster fade (Eric) */
}
.v3-fb-card::after {          /* THE VEIL: the cover washed toward the screen bg, by --fb-veil (fbLayout) */
  content: ''; position: absolute; inset: 0; border-radius: inherit;
  background: var(--sd-bg, #111116);
  opacity: var(--fb-veil, 0); transition: opacity .3s ease;
  pointer-events: none;
}
.v3-fb-card.is-front { box-shadow: 0 4px 10px rgba(0,0,0,0.5), 0 22px 44px rgba(0,0,0,0.55); }
/* The FRONT cover's reflection is shorter and fainter than the sides' (Eric:
   a busy cover's reflection was sitting under the title and fighting it):
   gone within ~13% of its height, at 2/3 the ink. */
.v3-fb-card.is-front { -webkit-box-reflect: below 2px linear-gradient(transparent 87%, rgba(0,0,0,0.2) 100%); }
.s-home-v3--light .v3-fb-card { box-shadow: 0 3px 8px rgba(30,20,10,0.22), 0 16px 34px rgba(30,20,10,0.28); }
.s-home-v3--light .v3-fb-card.is-front { box-shadow: 0 4px 10px rgba(30,20,10,0.25), 0 22px 44px rgba(30,20,10,0.32); }

/* ── The strip: the bento's typography, off the shell ───────────────────── */
.v3-fb-strip {
  padding: 6px 4px 0;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  text-align: center;            /* under a centred cover */
  cursor: pointer;
  color: var(--sd-ink, #e8e2d6);
}
/* The title pair, as the album page sets it: the album at 22px Roboto Flex
   with the year inline beside it at 14px light, the artist 14.5px bold on
   its own line under — `.s-home-v3--review .v3-blue-*`'s numbers. */
.v3-fb-title {
  font-family: 'Roboto Flex', var(--font-main);
  font-size: 22px; font-weight: 400; line-height: 1.12;
  color: var(--sd-ink, #e8e2d6);
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
.v3-fb-album { display: inline; }
.v3-fb-year  { display: inline; margin-left: 6px; font-size: 14px; font-weight: 300; color: var(--sd-ink2, rgba(232,226,214,0.5)); }
.v3-fb-year:empty { display: none; }
/* The artist's name is a LINK to the artist page (Eric, 2026-09-25) —
   `fbArtistTap`, on both decks' strips. The strip around it still opens the
   album, so the name is the one part that goes elsewhere; the underline on
   hover is the only tell. */
.v3-fb-artist { cursor: pointer; }
.v3-fb-artist:hover { text-decoration: underline; text-underline-offset: 3px; }
.v3-fb-artist {
  margin-top: 5px;               /* 5 more under the title (Eric) */
  font-family: 'Roboto Flex', var(--font-main);
  font-size: 14.5px; font-weight: 700; line-height: 1.15;
  color: var(--sd-ink, #e8e2d6);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* ── The friend's review under the info (Eric, 2026-09-23): CENTRED ────────
   Its own shape, not the feed card's grid: the face 1.7× the feed's 36px in
   the middle, the like pill on its left and the comment pill on its right
   ("since we have space there"), the name over @handle · when under it, the
   score beside the discs under that, the review beneath. The pills are the
   feed's `.v3-up--sm` ones, unchanged. The numbers are the feed card's
   (name 12.5/700, sub 10.5/500, number 21/800, text 12.5/1.4). */
.v3-fb-review { padding: 0 4px; }
/* THE SHIPPED order (`s-home-v3--fb-up`, both variants): the deck first,
   the title under it, the review under that. Without the class the markup's
   own order shows — the review above the deck — kept as an idea (Eric). */
.s-home-v3--fb-up .v3-fb-flow   { order: 1; }
.s-home-v3--fb-up .v3-fb-strip  { order: 2; }
.s-home-v3--fb-up .v3-fb-review { order: 3; }
.s-home-v3--fb-up .v3-fbr       { margin: 9px 0 0; }   /* 14 → 9: the face closer to the album info (Eric) */
.v3-fbr {
  align-self: stretch;
  margin: 0 0 6px; padding: 2px 0 0;   /* first now, above the deck (was under the strip with 14px over it) */
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  text-align: center;
  cursor: pointer;
}
.v3-fbr-head { display: flex; align-items: center; justify-content: center; margin-bottom: 5px; }   /* 5 → 10, back to 5: the name closer to the face (Eric, 2026-09-24) */
.v3-fbr-av {
  position: relative; overflow: hidden;
  width: 64px; height: 64px; border-radius: 50%; flex-shrink: 0;   /* 76 → 64 (Eric, 2026-09-24: "a bit smaller") */
  background: #333;
  box-shadow: 0 0 0 1px var(--sd-line, rgba(255,255,255,0.08)), 0 6px 16px rgba(0,0,0,0.35);
}
/* The face is a STACK of layers (fbFace): the new one fades up over the old. */
.v3-fbr-face {
  position: absolute; inset: 0; border-radius: inherit;
  background: #333 center / cover no-repeat;
  opacity: 0; transition: opacity .4s ease;
}
.v3-fbr-face.is-in { opacity: 1; }
/* The counts' split-flap: each digit change is a quick flip about the middle. */
/* A real flip card (Eric, 2026-09-24): the number turns about its middle —
   down to edge-on, where fbTick swaps the text, then the new one comes
   back up. The duration is inline per flip (fbTick), longer as it settles. */
.v3-fbr .v3-up { perspective: 90px; }
.v3-fbr .v3-up-n { display: inline-block; transform-origin: 50% 50%; backface-visibility: hidden; }
.v3-fbr .v3-up-n.is-flip { animation: fbr-flip .18s linear; }   /* the duration is overridden inline */
@keyframes fbr-flip {
  0%   { transform: rotateX(0deg);   opacity: 1; }
  49%  { transform: rotateX(-90deg); opacity: .55; }
  51%  { transform: rotateX(90deg);  opacity: .55; }
  100% { transform: rotateX(0deg);   opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .v3-fbr-face { transition: none; }
  .v3-fbr .v3-up-n.is-flip { animation: none; }
}

/* ── THE LEFT-ALIGNED REVIEW (Eric, 2026-09-24) — was the DARK variant for a
   day, to compare against the centred stack; Eric picked the CENTRED one for
   both themes (2026-09-24), so this now only shows under an opt-in
   `s-home-v3--fb-left` on the shell — nobody adds it. Same markup, laid
   out as a grid: on the left the face (smaller here, its belt scaled with
   it) with the name UNDER it; to the right the score over its discs, the
   comment and the like and when; then the text across the width. The time
   shows in the score row here (`.v3-fbr-ago--row`), not beside the name. ── */
.v3-fbr-ago--row { display: none; }
.s-home-v3--fb-left .v3-fbr {
  --belt-scale: 0.5625;                    /* a 36px face over the 64 the belt is drawn for */
  display: grid;
  /* Three columns: the face on the left, the SCORE centred on the screen —
     in line with the covers above (Eric) — and the acts on the right. The
     outer columns are equal so the middle one is the true centre. */
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  grid-template-areas: "av score acts" "name score acts" "text text text" "more more more";
  align-items: center; justify-items: start;
  column-gap: 10px; row-gap: 4px;
  text-align: left;
  padding: 4px 2px 0;
}
.s-home-v3--fb-left .v3-fbr-head  { grid-area: av; margin: 0 0 0 15px; align-self: end; }   /* 15px in from the edge (Eric) */
.s-home-v3--fb-left .v3-fbr-av-wrap, .s-home-v3--fb-left .v3-fbr-av { width: 36px; height: 36px; }   /* 60% of the 60 (Eric) */
.s-home-v3--fb-left .v3-fbr-name  { grid-area: name; align-self: start; width: 90px; margin-left: -12px; text-align: center; font-size: 12px; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }   /* centred under the face (its centre is at 15 + 18) */
.s-home-v3--fb-left .v3-fbr-name .v3-fbr-ago { display: none; }
.s-home-v3--fb-left .v3-fbr-score { display: contents; }   /* its two parts take their own cells */
.s-home-v3--fb-left .v3-fbr-scorebox { grid-area: score; justify-self: center; flex-direction: column; align-items: center; gap: 4px; }
.s-home-v3--fb-left .v3-fbr-acts { grid-area: acts; display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding-left: 0; }   /* right-aligned (Eric) */
/* A longer count must not move the icons (Eric): every count is the same
   fixed width, in the mono it already wears, so "6" and "1.6k" take the
   same room and the row holds still. */
.s-home-v3--fb-left .v3-fbr .v3-up-n { display: inline-block; width: 3.4ch; text-align: left; }
.s-home-v3--fb-left .v3-fbr-act--like { order: 2; justify-content: flex-start; }
.s-home-v3--fb-left .v3-fbr-act--cmt  { order: 1; }
.s-home-v3--fb-left .v3-fbr-act--like .v3-up { flex-direction: row; }   /* icon then number, like the comment */
.s-home-v3--fb-left .v3-fbr-act { min-width: 0; }
.s-home-v3--fb-left .v3-fbr-ago--row { display: inline; order: 3; white-space: nowrap; }
/* Smaller, regular-weight counts and smaller glyphs in the compact layout (Eric). */
.s-home-v3--fb-left .v3-fbr .v3-up { font-size: 12px; font-weight: 400; gap: 5px; }
.s-home-v3--fb-left .v3-fbr .v3-up svg { width: 22px; height: 22px; }
.s-home-v3--fb-left .v3-fbr .v3-cmt-btn svg { width: 25px; height: 25px; }
.s-home-v3--fb-left .v3-fbr-text  { grid-area: text; width: auto; max-width: none; justify-self: stretch; margin-top: 4px; }
.s-home-v3--fb-left .v3-fbr-more  { grid-area: more; margin-top: 0; padding-left: 0; }
/* The like and the comment, either side of the face (Eric, 2026-09-23):
   NO pill — bare glyphs, 26px so they read beside a 61px face — with the
   count on the OUTSIDE of each: "12 ♥  [face]  ✎ 5". The left one runs
   row-reverse so its number sits away from the face; the right one reads
   as it comes. The pill's background, padding and hover fill are stripped
   here and only here; the feed's pills below are untouched. */
.v3-fbr .v3-up {
  font-size: 13px; gap: 12px; padding: 0;   /* gap 7 → 12: the counts a little further out (Eric) */
  background: none; border-radius: 0;
}
.v3-fbr .v3-up svg { width: 28px; height: 28px; overflow: visible; }   /* 26 → 36, then 28 beside the score (Eric) */
/* Thinner strokes at this size (Eric: "stroke smaller"): both glyphs carry a
   weight tuned for a 13–17px pill, which is ~3px here. A CSS stroke-width on
   the shapes beats the presentation attribute; both glyphs share the 100-unit
   box now, so one number — ~1.8px at 36px. */
.v3-fbr .v3-up svg * { stroke-width: 1.8; }   /* 5 → 1.8 (2026-09-25): the heart and bubble are plain 24-unit line icons now */
/* The bubble sits a touch small beside the heart at the same box size — its
   drawing is shorter than the box. 5% up (Eric). */
.v3-fbr .v3-cmt-btn svg { width: 31px; height: 31px; }   /* 3% off (Eric) */
/* THE COUNTS: 13px, REGULAR (Eric, 2026-09-25: "not bolded and smaller").
   They went to 17px on 09-24 ("larger numbers") and back; the pill's base
   rule is 700, so the weight is said here outright. ⚠ Don't grow these again
   without Eric naming the size. */
.v3-fbr .v3-up { font-size: 13px; font-weight: 400; }
.v3-fbr-act { display: flex; align-items: center; flex: 0 0 auto; min-width: 64px; }
.v3-fbr-act:first-child { justify-content: flex-end; }
.v3-fbr-act:first-child .v3-up { flex-direction: row-reverse; }
.v3-fbr-act:last-child  { justify-content: flex-start; }
.v3-fbr-name { font-family: var(--font-main); font-size: 13px; font-weight: 700; color: var(--sd-ink, #e8e2d6); }   /* 12.5 → 14.5 → 13 (Eric, 2026-09-24: "a bit smaller") */
/* csharpuser (2026-09-25): the ribbon that ran round a popular review's face
   is gone (Eric: no 3D ribbons), and the flat tag that briefly replaced it
   went the same day — a popular review looks like any other. */
.v3-fbr-av-wrap { position: relative; width: 64px; height: 64px; }
.v3-fbr-score .v3-fbr-discs { display: inline-flex; }
.v3-fbr-ago  { margin-left: 6px; font-family: var(--font-mono); font-size: 10px; font-weight: 700; color: var(--sd-ink3, rgba(232,226,214,0.4)); }
/* The score row (Eric, 2026-09-23): the like on the left, the number large
   over its discs in the middle, the comment on the right. */
/* The score is INDEPENDENT of the counts (Eric, 2026-09-24: "the large number
   changes spots depending on the icon numbers"): a three-column grid whose
   outer columns are equal, so the score sits at the true centre whatever
   "6" or "1.6k" the like and the comment carry; they hang off it, the like
   ending at its column's inner edge, the comment starting at its own. */
.v3-fbr-score {
  display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center; column-gap: 31px; margin-top: 6px;   /* 16 → 31: the icons 15px further out (Eric) */
}
.v3-fbr-score > .v3-fbr-act--like, .v3-fbr-acts > .v3-fbr-act--like { justify-self: end; }
.v3-fbr-score > .v3-fbr-act--cmt,  .v3-fbr-acts > .v3-fbr-act--cmt  { justify-self: start; }
.v3-fbr-scorebox { display: flex; flex-direction: column; align-items: center; gap: 5px; }
.v3-fbr-acts { display: contents; }            /* the centred layout: like · score · comment, one flex row */
.v3-fbr-act--like { order: -1; }
.v3-fbr-n { font-family: var(--font-main); font-size: 28px; font-weight: 800; line-height: 1; letter-spacing: -0.02em; color: var(--sd-ink, #e8e2d6); }   /* 21 → 28 (the 34 was a misread — Eric meant the counts) */
.v3-fbr-score .hstars { display: inline-flex; gap: 2px; }
.v3-fbr-text {
  font-family: var(--font-main);   /* pinned: never the serif */
  font-size: 12.5px; line-height: 1.55;   /* was 1.4, then 1.55, then 1.75 — and back to 1.55 = 2.5px less a line (Eric, 2026-09-25: "lower the line height for all reviews by 2.5px"). Every review in the app takes this or the feed card's copy of it. */
  color: var(--sd-ink2, rgba(232,226,214,0.68));
  width: 346px; max-width: 100%; margin-top: 8px;   /* 310 → 320 → 334 → 340 → 346: 3px less margin a side, twice (Eric, 2026-09-24) */
  text-align: left;              /* the block is centred by its margins; the lines inside read left (Eric) */
  white-space: pre-line;
  display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden;   /* five lines (Eric) */
}
/* A review that overflows FADES at the clamp instead of ending in a cut, and
   gets a View more; `.is-long` is measured (fbGo), so a short review keeps
   its last line at full ink. Open, the clamp and the fade come off. */
.v3-fbr.is-long .v3-fbr-text {   /* fades sooner (Eric), like the feed's cards */
  -webkit-mask-image: linear-gradient(to bottom, #000 40%, rgba(0,0,0,0.45) 70%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 40%, rgba(0,0,0,0.45) 70%, transparent 100%);
}
.v3-fbr.is-open .v3-fbr-text { -webkit-line-clamp: unset; display: block; -webkit-mask-image: none; mask-image: none; }
/* Two lines or fewer read better centred (Eric, 2026-09-24); `.is-short` is
   measured (fbMarkShort), so a real paragraph keeps its left edge. */
.v3-fbr.is-short .v3-fbr-text { text-align: center; }
.v3-fbr-more {
  display: none;
  margin-top: 4px; padding: 4px 10px;
  background: none; border: 0; cursor: pointer;
  font-family: var(--font-main); font-size: 11.5px; font-weight: 700; letter-spacing: 0.2px;
  color: var(--sd-ink2, rgba(232,226,214,0.55));
}
.v3-fbr.is-long .v3-fbr-more { display: inline-block; }
.v3-fbr-more:hover { color: var(--sd-ink, #e8e2d6); }
.s-home-v3--light .v3-fbr-text { color: rgba(26,18,8,0.9); }   /* the feed's light ink for a paragraph */
/* Ink only (the feed card's pill rules paint a background; these don't). */
.s-home-v3 .v3-fbr .v3-up { color: var(--sd-ink2); }
.s-home-v3 .v3-fbr .v3-up:hover { background: none; color: var(--sd-ink); }
.s-home-v3 .v3-fbr .v3-up.is-on { background: none; color: #ff5c8a; }
.s-home-v3 .v3-fbr .v3-up.is-on svg { fill: currentColor; }
.s-home-v3 .v3-fbr .v3-cmt-btn.is-open { background: none; color: var(--sd-ink2); }
.s-home-v3 .v3-fbr .v3-cmt-btn.is-mine { color: #3b82f6; }   /* the count goes blue with the bubble */
.s-home-v3--light .v3-fbr .v3-up.is-on { color: #e0396e; }

@media (prefers-reduced-motion: reduce) {
  .v3-fb-card, .v3-fb-card::after { transition: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE REVIEW SHEET (Eric, 2026-09-24) — `.v3-rsh-ov` / `.v3-rsh`, built by
   rshHtml (app.js). A bottom sheet inside the phone screen: the dimmed
   backdrop fades, the sheet slides up from below the screen's bottom edge and
   back down on close. Its top part is the deck's review block verbatim
   (`.v3-fbr`, so the face, belt, name, score row and pills are the same
   rules) — only the clamp comes off the text here. Under it the record line
   and the comments, in the review page's own `.rvp-cmts` / `.v3-cmt-*`
   clothes. It inks off the --sd-* tokens, so the light shell's sheet is
   light. ═══════════════════════════════════════════════════════════════════ */
.v3-rsh-ov {
  position: absolute; inset: 0; z-index: 220;
  background: rgba(0,0,0,0);
  transition: background .28s ease;
  font-family: var(--font-main);
}
.v3-rsh-ov.open { background: rgba(0,0,0,0.55); }
.s-home-v3--light .v3-rsh-ov.open { background: rgba(26,18,8,0.4); }
.v3-rsh {
  position: absolute; left: 0; right: 0; bottom: 0;
  max-height: 90%;
  display: flex; flex-direction: column;
  background: var(--sd-bg, #111116);
  color: var(--sd-ink, #e8e2d6);
  border-radius: 22px 22px 0 0;
  box-shadow: 0 -1px 0 var(--sd-line, rgba(232,226,214,0.09)), 0 -18px 48px rgba(0,0,0,0.45);
  transform: translateY(104%);
  transition: transform .34s cubic-bezier(.22,1,.36,1);
}
.v3-rsh-ov.open .v3-rsh { transform: translateY(0); }
.v3-rsh.is-drag { transition: none; }       /* while it follows the finger */
.v3-rsh-grab { flex: 0 0 auto; padding: 10px 0 4px; display: flex; justify-content: center; cursor: grab; touch-action: none; }
.v3-rsh-grab i { display: block; width: 38px; height: 4px; border-radius: 2px; background: var(--sd-ink3, rgba(232,226,214,0.32)); }
.v3-rsh-body {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  padding: 2px 18px 28px;
}
/* The deck's review block, unclamped: the whole review, no fade, no View more. */
.v3-rsh-body .v3-fbr--sheet { margin: 0; padding: 4px 0 0; cursor: default; touch-action: none; }
.v3-rsh-body .v3-fbr--sheet .v3-fbr-text {
  display: block; -webkit-line-clamp: unset; overflow: visible;
  width: auto; max-width: none; align-self: stretch;
  -webkit-mask-image: none; mask-image: none;
}
.v3-rsh-body .v3-fbr--sheet .v3-fbr-more { display: none; }
/* The record line: cover · album over artist · year — a tap opens the album. */
.v3-rsh-record {
  display: flex; align-items: center; gap: 10px;
  margin: 14px 0 0; padding: 9px 10px;
  border-radius: 12px; background: var(--sd-card, rgba(255,255,255,0.05));
  cursor: pointer;
}
.v3-rsh-record:hover { background: var(--sd-hover, rgba(255,255,255,0.04)); }
.v3-rsh-art { width: 40px; height: 40px; border-radius: 6px; flex: 0 0 auto; background: #222 center / cover no-repeat; box-shadow: 0 2px 6px rgba(0,0,0,0.35); }
.v3-rsh-rec-who { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* The CD, hard right (margin-left auto), always turning; tap = the NAV
   CONSOLE for this album (rshCdTap), as on the album page. Its hole is the
   bento CD's own (.v3-cd-hole). (It played a preview until 2026-09-25; the
   lit `.is-playing` ring and the `.is-none` blink went with that.) */
.v3-rsh-cd {
  position: relative; flex: 0 0 auto; margin-left: auto; margin-right: 2px;
  width: 40px; height: 40px; border-radius: 50%; overflow: hidden;
  background: #222 center / cover no-repeat;
  box-shadow: 0 3px 10px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.08);
  animation: v3spin 12s linear infinite;
  cursor: pointer;
  transition: box-shadow .2s ease, filter .2s ease;
}
.v3-rsh-cd .v3-cd-hole { width: 8px; height: 8px; background: var(--sd-bg, #111116); }
.v3-rsh-cd:hover { filter: brightness(1.08); }
/* ⚠️ The sheet's overlay is z 220 and the nav is z 5, so a console the sheet's
   CD raised would grow up BEHIND the sheet. While a sheet is up and the console
   is open, the nav rides over the overlay; the console's own state class is
   the gate, so the nav drops back under the sheet the moment it closes. */
.s-home-v3--console:has(.v3-rsh-ov) .v3-bottom-nav { z-index: 230; }
@media (prefers-reduced-motion: reduce) { .v3-rsh-cd { animation: none; } }
.v3-rsh-rec-album  { font-family: 'Roboto Flex', var(--font-main); font-size: 14px; font-weight: 500; color: var(--sd-ink, #e8e2d6); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.v3-rsh-rec-artist { font-size: 11.5px; color: var(--sd-ink2, rgba(232,226,214,0.55)); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.v3-rsh-body .rvp-cmts { margin-top: 6px; }
.v3-rsh-body .rvp-cmts-hd { color: var(--sd-ink3, rgba(232,226,214,0.45)); }
.v3-rsh-body .rvp-cmts-n { color: var(--sd-ink2, rgba(232,226,214,0.7)); }
@media (prefers-reduced-motion: reduce) {
  .v3-rsh, .v3-rsh-ov { transition: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE DISCOVERY DECK on the Trending wall (Eric, 2026-09-24) — `.v3-fb--dd`,
   discoveryDeckHtml (screens.js) / renderDiscoveryDeck (app.js). The home
   deck's rules do the covers and the title strip; this is only its place at
   the top of the wall and the ACTUAL REVIEW row under the strip: the label
   small in the mono, the aggregate score large beside its discs (the deck
   review's number and discs), the count under. ═══════════════════════════ */
.v3-fb--dd { margin: 2px -2px 12px; }   /* inside .wall2-scroll's 12px: the box 10px in from the edge, like home's */
.v3-dd-score {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  margin-top: 6px; padding: 0 4px;
  text-align: center; cursor: pointer;
}
.v3-dd-label {
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--sd-ink3, rgba(232,226,214,0.32));
}
/* The score is the home review's own box — `.v3-fbr-scorebox` / `.v3-fbr-n`
   (Eric, 2026-09-24: "the same as the homepage") — so only the count is here. */
.v3-dd-discs { display: inline-flex; }
.v3-dd-line { display: inline-flex; align-items: center; gap: 6px; margin-top: 6px; }   /* more air under the discs (Eric, 2026-09-24) */
.v3-dd-count, .v3-dd-tag { font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: 0.04em; color: var(--sd-ink2, rgba(232,226,214,0.55)); }
.v3-dd-tag::before { content: '·'; margin-right: 6px; }   /* "89k reviews · popular album", one weight */

/* ══════════════════════════════════════════════════════════════════════════
   YOUR REVIEW · COMMUNITY REVIEW on the album page (Eric, 2026-09-24).
   The rate ring and its four wings (`.v3-rate-group`, rules above, now
   dormant) are replaced by something plain: the headline score's shape,
   twice. Yours first — `.v3-rev-yours`, a band the full width of the screen
   (the panel's 10px padding pulled back with negative margins), the label
   small in the mono, your number large over your discs (the same
   `.v3-rev-score` rules as the community's below, so the two match), a tap
   → the log sheet. Then "Community review" over the album's own score. The
   numbers line up: the band's left padding is the panel's 10 + the
   community score's 15.5 nudge. ════════════════════════════════════════════ */
.v3-rev-top { margin-bottom: 0; }
.v3-rev-mine { padding: 0; }
/* RAISED, not inverted (Eric, 2026-09-25: "just give that section a drop
   shadow"): the band keeps the page's colours — a faint lift of the surface
   with a soft shadow under it — so it reads as a card sitting on the page,
   the same ink as the community score below. */
.v3-rev-yours {
  margin: 0 -10px;
  padding: 12px 10px 14px 25.5px;
  background: rgba(255,255,255,0.04);
  box-shadow: 0 1px 0 rgba(255,255,255,0.05) inset, 0 8px 22px rgba(0,0,0,0.38), 0 2px 6px rgba(0,0,0,0.28);
  cursor: pointer;
  position: relative; z-index: 1;   /* the shadow falls over what follows */
}
.v3-rev-yours:hover { background: rgba(255,255,255,0.06); }
.v3-rev-lbl {
  display: block;
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
  color: rgba(232,226,214,0.45);
}
/* THE TWO NUMBERS SHARE ONE LEFT EDGE (Eric, 2026-09-25: "that's the whole
   point"): every nudge the score rule carries — and the dev box's live copy
   of it, which wins ties — is zeroed on BOTH rows (`!important`, because the
   injected style is later and would otherwise put its 15.5px back), and the
   offset lives on the wrappers instead: the band pads 25.5 from the screen
   edge, the community box 15.5 inside the panel's 10. Same number. */
.v3-rev-community { padding-left: 15.5px; margin-top: 16px; }
.v3-rev-lbl--community { margin: 0; }
.s-home-v3--review .v3-rev-score--mine,
.s-home-v3--review .v3-rev-community .v3-rev-score {
  left: 0 !important; top: 0 !important; padding: 6px 0 0 !important; max-width: 100%;
}
/* Unrated: the 0.0 dim. Rated: your number in the album gold. */
.s-home-v3--review .v3-rev-score--mine .v3-rev-score-n,
.s-home-v3--light.s-home-v3--review .v3-rev-score--mine .v3-rev-score-n { color: rgba(232,226,214,0.4); }
.s-home-v3--review .v3-rev-yours.is-rated .v3-rev-score-n,
.s-home-v3--light.s-home-v3--review .v3-rev-yours.is-rated .v3-rev-score-n { color: var(--star, #e8a83c); }
.v3-rev-yours .v3-rev-score-count:empty { display: none; }

/* ══════════════════════════════════════════════════════════════════════════
   THE WALL'S CHIPS, SPREAD AND BARE (Eric, 2026-09-25): Popular ·
   Controversial · Genres · Week share the row evenly instead of stacking
   left, and — a version to try — no pill behind them: bare words, the
   active one in the album gold with a hairline under it, the open dropdown
   the same. The neumorphic pills above stay as the fallback: drop this block
   to get them back. ═══════════════════════════════════════════════════════ */
.s-home-v3 .wall2-bar { justify-content: space-between; gap: 0; padding: 2px 4px 12px; }
/* 0.14b (2026-10-09) — HOME'S THREE TABS SIT LEFT, NOT SPREAD. Eric: "can we
   have them stacked left side instead of them spread out tis so spread out
   haha biut only for the homepage the trending page has enough tags to keep
   them spread". Friends / Popular / You across a phone's width on
   space-between leaves a canyon between each; four chips on the wall fill it.
   ⚠ Scoped to `.v3-feed-tabs`, which ONLY home's bar carries — the rule above
   is shared by every `.s-home-v3` screen, the Trending wall included, so
   editing it in place would have un-spread the wall too. Three classes to its
   two, and it follows it, so it wins either way.
   ⚠ The gap has to be restored explicitly: the rule above zeroes it for
   space-between, and these chips are flat text with 2px of side padding, so
   with no gap the three words would run together. */
/* 0.14b (2026-10-12) — SPREAD AGAIN, LIKE THE WALL. Eric: "can we make the
   categories at the top the same spacing and aes as the four categories in
   the trending page … lets just put an empty live button u cant click at the
   end on the right side". A fourth chip (Live, disabled) makes four, so the
   wall's space-between reads right here too; the left-stacking below is
   switched off rather than deleted.
   ⚠ The sides are 16px, not the wall's 4px: the wall's bar sits inside
   `.wall2-scroll` (12px side padding), home's sits straight in `.v3-body`,
   so 12 + 4 puts the words on the same lines as the wall's. */
.s-home-v3 .wall2-bar.v3-feed-tabs { padding: 2px 16px 12px; }
.s-home-v3 .wall2-cat.v3-feed-tab--soon,
.s-home-v3 .wall2-cat.v3-feed-tab--soon:hover { color: var(--sd-ink2, rgba(232,226,214,0.55)); opacity: 0.35; cursor: default; }
/* (was, 2026-10-09: .s-home-v3 .wall2-bar.v3-feed-tabs { justify-content: flex-start; gap: 18px; padding-left: 10px; }) */
/* … and OFF THE EDGE by the feed's own gutter (Eric: "a litte off the left
   side please tis like right on the edge offset by like 10px"). 10px is not a
   guess: `.s-home-v3 .v3-scroll-area` pads the feed by exactly 10px, so the
   first tab lands on the same line as the cards under it rather than near it.
   ⚠ The chip's own 2px of left padding is taken off the FIRST one, or the
   text would sit at 12px and miss that line by two. Zeroed on the chip rather
   than subtracted from the bar, so the bar keeps the feed's gutter as its
   value and this stays true if the chip padding ever changes. */
/* (was: .s-home-v3 .wall2-bar.v3-feed-tabs .wall2-cat:first-child { padding-left: 0; } — off with the spread) */
.s-home-v3 .wall2-cat, .s-home-v3--light .wall2-cat {
  background: none; box-shadow: none; padding: 6px 2px 5px;
  border-bottom: 1.5px solid transparent; border-radius: 0;
  font-size: 11.5px; letter-spacing: 0; font-weight: 600;
  color: var(--sd-ink2, rgba(232,226,214,0.55));
}
.s-home-v3 .wall2-cat:hover, .s-home-v3--light .wall2-cat:hover { background: none; color: var(--sd-ink, #e8e2d6); }
.s-home-v3 .wall2-cat.active, .s-home-v3--light .wall2-cat.active,
.s-home-v3 .wall2-drop-btn.open, .s-home-v3--light .wall2-drop-btn.open {
  background: none; box-shadow: none; color: var(--star); border-bottom-color: var(--star);
}

/* THE REVIEW PAGE wears the sheet's body (Eric, 2026-09-25) — `.v3-rsh-body--page`
   inside `.rvp-scroll`, under the back pill. No sheet: it is in flow and the
   page's `.v3-body` scrolls it, so the body's own scroller and padding come
   off (the scroll wrapper already pads the sides). */
.v3-rsh-body--page { overflow: visible; padding: 0 6px 8px; flex: none; }   /* 12 + 6 = the sheet's 18px sides */

/* TAP TO RATE (Eric, 2026-09-25): SIDEWAYS down the band's right edge — the
   glyphs' tops point right (vertical-rl) — and marqueeing: the line is in
   there twice and slides up by one copy, so it loops without a seam. Shown
   only until the record is rated or written about; the empty discs breathe
   in turn meanwhile. The band pads right for it. */
.v3-rev-yours { padding-right: 30px; }
.v3-rev-hint {
  display: none;
  position: absolute; top: 0; bottom: 0; right: 6px; width: 18px;
  overflow: hidden; pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
          mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
}
.v3-rev-yours:not(.is-rated):not(.is-written) .v3-rev-hint { display: block; }
.v3-rev-hint-run {
  display: block;
  writing-mode: vertical-rl;            /* top to bottom, tops of the letters to the right */
  white-space: nowrap;
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase;
  color: rgba(232,226,214,0.5);
  animation: rev-hint-marquee 24s linear infinite;   /* 9 → 24: slower (Eric) */
}
@keyframes rev-hint-marquee { from { transform: translateY(0); } to { transform: translateY(-100%); } }
.v3-rev-yours:not(.is-rated):not(.is-written) .hstar { animation: rev-hint-disc 2.4s ease-in-out infinite; }
.v3-rev-yours:not(.is-rated):not(.is-written) .hstar:nth-child(2) { animation-delay: .12s; }
.v3-rev-yours:not(.is-rated):not(.is-written) .hstar:nth-child(3) { animation-delay: .24s; }
.v3-rev-yours:not(.is-rated):not(.is-written) .hstar:nth-child(4) { animation-delay: .36s; }
.v3-rev-yours:not(.is-rated):not(.is-written) .hstar:nth-child(5) { animation-delay: .48s; }
@keyframes rev-hint-disc { 0%, 70%, 100% { transform: scale(1); } 20% { transform: scale(1.16); } }
@media (prefers-reduced-motion: reduce) {
  .v3-rev-hint-run, .v3-rev-yours .hstar { animation: none; }
}

/* ══════════════════════════════════════════════════════════════════════════
   COMMENTS IN THE LIGHT THEME (2026-09-25). The thread's ink was hard-coded
   light for the dark surfaces it lived on (the flooded album page, the dark
   review page). The review sheet and, since today, the review page sit on
   the light theme's cream — where every row vanished (seen in the viewer).
   Dark ink for all of it here: the header, the rows, the composer. ════════ */
.s-home-v3--light .rvp-cmts-hd { color: rgba(26,18,8,0.45); }
.s-home-v3--light .rvp-cmts-n  { color: rgba(26,18,8,0.7); }
.s-home-v3--light .v3-cmt { border-top-color: rgba(26,18,8,0.09); }
.s-home-v3--light .v3-cmt-user { color: rgba(26,18,8,0.92); }
.s-home-v3--light .v3-cmt-text { color: rgba(26,18,8,0.8); }
.s-home-v3--light .v3-cmt-ago  { color: rgba(26,18,8,0.42); }
.s-home-v3--light .v3-cmt-like, .s-home-v3--light .v3-cmt-reply { color: rgba(26,18,8,0.42); }
.s-home-v3--light .v3-cmt-like:hover, .s-home-v3--light .v3-cmt-reply:hover { color: rgba(26,18,8,0.9); }
.s-home-v3--light .v3-cmt-like.is-on { color: #e0396e; }
.s-home-v3--light .v3-cmt-av { background-color: rgba(26,18,8,0.08); box-shadow: 0 0 0 1px rgba(26,18,8,0.1); }
.s-home-v3--light .v3-cmt-none { color: rgba(26,18,8,0.4); }
.s-home-v3--light .v3-cmt-input {
  background: rgba(26,18,8,0.05); border-color: rgba(26,18,8,0.12); color: #1a1208;
}
.s-home-v3--light .v3-cmt-input::placeholder { color: rgba(26,18,8,0.38); }
.s-home-v3--light .v3-cmt-post { background: rgba(176,106,42,0.16); color: #b06a2a; }
.s-home-v3--light .v3-cmt-post:hover { background: #b06a2a; color: #fff; }
.s-home-v3--light .v3-cmt-to { color: rgba(26,18,8,0.6); }

/* The album page's IN-PLACE review is the sheet's look end to end (2026-09-25):
   the whole shell drops the album flood — header included — for the screen's
   own background, in both themes. (0,2,0) beats the flood's (0,1,0). */
.s-home-v3.s-home-v3--rvp { background: var(--sd-bg, #111116); }
.s-home-v3--light.s-home-v3--rvp { background: var(--sd-bg, #f0ece3); }

/* ══════════════════════════════════════════════════════════════════════════
   THE REVIEW POPUP AND THE REVIEW PAGE: EDGE TO EDGE, NO SIDE GAPS (Eric,
   2026-09-25). The popup spans the screen (no 10px float like the log
   sheet — tried, "there's gaps in the sides"), its inside 12px; the page's
   content sits at the same 12px from the screen edge, on the same surface,
   no card around it. ═══════════════════════════════════════════════════════ */
.v3-rsh { left: 0; right: 0; bottom: 0; border-radius: 20px 20px 0 0; }
.v3-rsh-body { padding: 2px 20px 24px; }                    /* the home review's column: 346 in 385 → ~20 a side (Eric, 2026-09-25: "same margins as the homepage main review") */
.rvp-scroll { padding-left: 20px; padding-right: 20px; }    /* the page keeps step */
.v3-rsh-body--page { margin: 0; padding: 0 0 8px; background: none; border-radius: 0; box-shadow: none; }
.s-home-v3--light .v3-rsh-body--page { background: none; box-shadow: none; }

/* ══════════════════════════════════════════════════════════════════════════
   THE WRITE-A-REVIEW SHEET, EDGE TO EDGE (Eric, 2026-09-25: "the popup for
   the review has gaps in the sides, the popup for the comment doesn't — I
   like that aesthetic, make it that margin"). The log sheet floated 10px in
   from both sides and off the bottom; the comment sheet (`.v3-rsh`) spans
   the screen with its top corners rounded and 12px inside. Same here now:
   full width, flush with the bottom, top corners only, the comment sheet's
   surface colour, 12px inside. ══════════════════════════════════════════ */
.sd-log-sheet {
  width: 100%; margin-bottom: 0;
  border-radius: 20px 20px 0 0;
  background: var(--sd-bg, #111116);
  padding: 12px 12px 14px;
  box-shadow: 0 -1px 0 var(--sd-line, rgba(232,226,214,0.09)), 0 -18px 48px rgba(0,0,0,0.45);
}
.s-home-v3--light .sd-log-sheet { background: var(--sd-bg, #f0ece3); box-shadow: 0 -1px 0 var(--sd-line, rgba(26,18,8,0.1)), 0 -18px 48px rgba(0,0,0,0.18); }

/* ══════════════════════════════════════════════════════════════════════════
   THE WRITE-A-REVIEW SHEET, 2026-09-25 (Eric):
   · POST rides the cover — bottom centre of the picture, a drop shadow under
     it, nothing pushed down — and only appears once something changed.
   · The SONG rows' discs are larger, with the song's score beside them at
     the discs' height, "0.0" dim until rated, like the big one.
   · Listened / Listen later / Favourite EMBOSS upward when on: the button
     lifts on a raised neumorphic shadow and takes its colour. ═══════════════ */
.sd-log-cover { position: relative; }
.sd-log-cover .sd-log-actions {
  position: absolute; left: 0; right: 0; bottom: 10px;
  display: none; justify-content: center;
}
.sd-log-cover .sd-log-actions:has(.sd-log-post[data-mode="post"]) { display: flex; }
.sd-log-cover .sd-log-post {
  box-shadow: 0 6px 18px rgba(0,0,0,0.55), 0 2px 5px rgba(0,0,0,0.4);
}
/* The song rows. */
.sd-log-song-rate-track { --rec: 20px; --gap: 4px; }   /* 14 → 20 */
.sd-log-song-val {
  min-width: 2.2ch; text-align: right;
  font-family: var(--font-main); font-size: 20px; line-height: 1; font-weight: 800; letter-spacing: -0.03em;
  color: var(--sd-ink, #e8e2d6); font-variant-numeric: tabular-nums;
}
.sd-log-song-val.is-blank { color: var(--sd-ink3, rgba(232,226,214,0.32)); }
/* The three toggles (csharpuser, 2026-09-25): the POPULAR belts round the
   words are gone with the ribbon. Air above and below the row; ON is the
   icon and the word in the toggle's colour, nothing else. */
.sd-log-opts { margin: 12px 0 18px; justify-content: center; gap: 16px; }
.sd-log-opt { flex: 0 0 auto; padding: 8px 10px; }
.sd-log-opt, .sd-log-opt.on { background: none; box-shadow: none; transform: none; }
.sd-log-opt-lbl { position: relative; }
.sd-log-opt.on .sd-log-opt-lbl { color: var(--star, #e8a83c); }
.sd-log-opt[data-k="fav"].on .sd-log-opt-lbl { color: #ff5c8a; }
.sd-log-opt.on .sd-log-opt-ico { color: var(--star, #e8a83c); }
.sd-log-opt[data-k="fav"].on .sd-log-opt-ico { color: #ff5c8a; }
/* The song numbers a touch in from the edge; Post near the TOP of the cover. */
.sd-log-song-num { margin-left: 4px; }
.sd-log-cover .sd-log-actions { top: 10px; bottom: auto; }

/* THE CD POPUP on the album page, more air (Eric, 2026-09-25: "a bit small,
   the icons touch the streaming icons"): a taller preview row with room
   under it, the streaming rows padded and spaced, the menu padded. */
.v3-cd-menu { padding: 8px; gap: 3px; min-width: 168px; }
.v3-cd-menu .v3-stream-preview { padding: 10px 12px; margin: 2px 0 6px; }
.v3-cd-menu-sep { margin: 4px 6px 6px; }
.v3-cd-menu .wall2-menu-item { padding: 10px 12px; }
.v3-cd-menu .plp-plat-item { gap: 10px; }

/* The bottom sheets' TOP corners, tighter (Eric, 2026-09-25: "lower the
   corner radius at the top, not the bottom"): 20 → 12 on the review sheet
   and the write-a-review sheet; the bottom stays flush. */
.v3-rsh, .sd-log-sheet { border-radius: 12px 12px 0 0; }

/* ============================================================
   ONBOARDING, SIMPLER — csharpuser (2026-09-25). Eric: "make the onboarding
   simpler as well: no more emboss, and left align". Every neu-emboss pair
   (the raised buttons, the pressed-in wells) becomes a flat surface with a
   1px border, ON is a filled accent border, and everything that was
   centred under the field, on the profile step and in the wall cards sits
   on the left edge with the headings. The original rules stay above;
   this block overrides them by order. (Step 0's "This is how friends find
   you on Spindeck." line came out of screens.js the same day.)
   ============================================================ */
/* Flat surfaces */
.ob-user-well, .sd-theme-light .ob-user-well,
.ob-svc, .sd-theme-light .ob-svc,
.ob-mix-back, .sd-theme-light .ob-mix-back,
.ob-view, .sd-theme-light .ob-view,
.ob-view-btn.is-on, .sd-theme-light .ob-view-btn.is-on,
.ob-gbub, .sd-theme-light .ob-gbub,
.ob-gsubs, .sd-theme-light .ob-gsubs,
.ob-card-img, .ob-pf-av { box-shadow: none; }
.ob-user-well, .ob-svc, .ob-gbub, .ob-gsubs, .ob-view, .ob-mix-back {
  background: var(--surface); border: 1px solid var(--border);
}
.ob-svc:active, .ob-gbub:active { transform: none; }
/* ON states: a border in the accent, the fill a shade up */
.ob-plate--ok .ob-user-well, .sd-theme-light .ob-plate--ok .ob-user-well { box-shadow: none; border-color: var(--ob-fill); }
.ob-svc--on, .sd-theme-light .ob-svc--on { box-shadow: none; border-color: var(--accent); background: var(--surface2); }
.ob-gbub.is-lit, .sd-theme-light .ob-gbub.is-lit { box-shadow: none; border-color: var(--accent); }
.ob-gbub.is-on, .sd-theme-light .ob-gbub.is-on { box-shadow: none; border-color: var(--accent); background: var(--surface2); }
.ob-view-btn.is-on { background: var(--surface2); }
/* Left aligned */
.ob-user-read { text-align: left; }
.ob-user-meter { margin: 0; }
.ob-user-note, .ob-note { margin-left: 0; margin-right: 0; max-width: none; text-align: left; }
.ob-empty { text-align: left; padding: 20px 0; }
.ob-card { align-items: flex-start; }
.ob-card-t, .ob-card-s { text-align: left; }
.ob-pf-hero { align-items: flex-start; text-align: left; padding-top: 8px; }
.ob-stat { text-align: left; padding: 12px 12px; }
.ob-pf-note { text-align: left; }

/* THE RAIL + THE CONTINUE BAND (Eric, 2026-09-26: "put the skip button at
   the top near the 01/07 numbers, make it full width and styled like our
   large-number review on the album page"). Skip is a quiet text pill on the
   rail's right, beside the count. The footer is ONE band, edge to edge and
   flush with the bottom: the "Your review" band's recipe — a faint lift of
   the surface, a soft shadow above it, a mono 9.5px label over a 26px/800
   line — as a button. */
.ob-rail { gap: 10px; }
.ob-rail .ob-stepcount { margin-left: auto; }
.ob-rail .ob-skip {
  padding: 6px 10px; margin: -6px -10px -6px 0;
  font-size: 12px; font-weight: 600; color: var(--text2);
  background: none; border-radius: 8px; box-shadow: none;
}
.ob-rail .ob-skip:hover { color: var(--text); background: var(--surface); }
/* ⚠ `.s-onboarding .ob-footer` (0,2,0) pads 10/22/20 + the safe area; that
   padding is what kept the band off the edges and off the bottom. */
.s-onboarding .ob-footer { display: block; padding: 0; }
.s-onboarding .ob-next {
  display: block; width: 100%; margin: 0; flex: none;
  padding: 12px 24px 26px; border: 0; border-radius: 0; text-align: left;
  background: rgba(255,255,255,0.04); color: var(--text);
  box-shadow: 0 1px 0 rgba(255,255,255,0.05) inset, 0 -8px 22px rgba(0,0,0,0.38), 0 -2px 6px rgba(0,0,0,0.28);
  font-family: var(--font-main); font-size: 26px; font-weight: 800; letter-spacing: -0.03em; line-height: 1.1;
  position: relative; z-index: 1;
}
.s-onboarding .ob-next::before {
  content: attr(data-lbl); display: block; margin-bottom: 4px;
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text2); opacity: .7;
}
.s-onboarding .ob-next:hover { background: rgba(255,255,255,0.06); }
.s-onboarding .ob-next:disabled { opacity: .4; }
.s-onboarding.sd-theme-light .ob-next {
  background: rgba(26,18,8,0.05);
  box-shadow: 0 1px 0 rgba(255,255,255,0.4) inset, 0 -8px 22px rgba(30,20,10,0.16), 0 -2px 6px rgba(30,20,10,0.1);
}
.s-onboarding.sd-theme-light .ob-next:hover { background: rgba(26,18,8,0.08); }
/* The dock sits on the band; its own bottom air is the band's top padding now. */
.ob-has-dock .ob-picks-dock { padding-bottom: 6px; }

/* A name or a face opens that person's profile (sdPerson, app.js, 2026-09-26). */
.v3-rev-av, .v3-rev-name, .v3-fbr-av, .v3-fbr-who, .v3-cmt-av, .v3-cmt-user { cursor: pointer; }

/* MORPH (morph.js, 2026-09-26): while the tapped thing glides into its place
   on the next screen, the shell's own cover/bento slides stay still so the
   destination box is not a moving target. */
.sd-morphing .v3-album, .sd-morphing .v3-bento, .sd-morphing .v3-blue, .sd-morphing .v3-for-single, .sd-morphing .v3-review-panel { transition: none !important; animation: none !important; }

/* ============================================================
   0.11 (2026-09-28) — three things on home. Eric: "have the review under
   [the carousel] be the same style as the review below but make it clear
   that it's for the album carousel … add a people u may know like 3 reviews
   down … a + icon in the center of the ui bar at the bottom".
   ============================================================ */

/* ── 1. THE DECK'S REVIEW IS THE FEED'S CARD, IN A PANEL ─────────────────
   fbGo renders `.v3-rev-card--feed.v3-rev-card--deck` into `.v3-fb-review`
   (the centred `.v3-fbr` block is gone from home; the review sheet keeps
   its own). What says "this belongs to the carousel": the panel (the feed's
   cards below sit flat on the page, this one is on a raised surface), the
   notch in its top edge pointing up at the cover and its title, and the
   caption naming the record. Everything inside is the feed card's own CSS.
   The panel's fill and hairline are the --sd-* tokens, so light follows. */
.s-home-v3 .v3-fb-review:not(:empty) {
  align-self: stretch; position: relative;
  margin: 16px 2px 0; padding: 0;
  border-radius: 16px;
  background: var(--sd-card);
  box-shadow: 0 0 0 1px var(--sd-line);
  cursor: pointer;
}
.s-home-v3 .v3-fb-review:not(:empty)::before {   /* the notch */
  content: ''; position: absolute; left: 50%; top: -6px;
  width: 12px; height: 12px; margin-left: -6px;
  background: var(--sd-bg);
  background-image: linear-gradient(var(--sd-card), var(--sd-card));
  box-shadow: -1px -1px 0 0 var(--sd-line);
  border-top-left-radius: 3px;
  rotate: 45deg;
  pointer-events: none;
}
.v3-fb-revcap {
  position: relative;
  padding: 13px 14px 0;
  font-family: var(--font-mono); font-size: 9.5px; font-weight: 500;
  letter-spacing: 0.08em; text-transform: uppercase; text-align: left;   /* left, over the face (Eric, 2026-09-28) — 14px in, the card's own inset */
  color: var(--sd-ink3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.v3-fb-revcap b { font-weight: 700; color: var(--sd-ink2); }
/* A PILL, not a line of text (Eric, 2026-09-28: "a bubble in the upper left
   or pill with friend in it"): "Friend", or "Popular" on a community review,
   in the panel's upper-left corner over the face. Friend is the quiet one
   (the page's pill fill); Popular takes the album's gold so the two read
   apart at a glance. */
.v3-fb-revpill {
  display: inline-flex; align-items: center; height: 20px; padding: 0 9px;
  border-radius: 999px;
  font-family: var(--font-main); font-size: 10.5px; font-weight: 700;
  letter-spacing: 0.02em; text-transform: none;
  background: var(--sd-pill); color: var(--sd-ink2);
}
.v3-fb-revpill--pop { background: color-mix(in srgb, var(--star) 20%, transparent); color: var(--star); }
.s-home-v3 .v3-rev-card.v3-rev-card--feed.v3-rev-card--deck { padding: 12px 14px 15px; }
.s-home-v3 .v3-rev-card.v3-rev-card--feed.v3-rev-card--deck::after { display: none; }   /* no divider: the panel is the edge */
/* No record row here, so the text takes the gap the record row gives it on
   the feed; and it is HELD at four lines so the feed under the deck stays
   put while you flick between a short review and a long one. */
.s-home-v3 .v3-rev-card--deck .v3-rev-text { margin-top: 21.5px; }
/* THE BOX FITS ITS REVIEW (Eric, 2026-09-28: "when the review is short …
   make the box smaller as well"): the four-line hold above is gone, so a
   one-line review makes a small panel. The feed under it moves when the
   height changes, which is why the change is EASED (fbGo pins the old
   height and transitions to the new one; `.is-sizing` clips the card while
   the box is between the two). */
.s-home-v3 .v3-fb-review { transition: height .28s cubic-bezier(.22, 1, .36, 1); }
.s-home-v3 .v3-fb-review.is-sizing { overflow: hidden; }
/* THE SWAP: the title strip and the card come in from the side the deck
   moved from, 14px and a fade. `backwards`, never `both` — a fill-forwards
   animation on text keeps its layer and reads soft (see the review page's
   note). The panel itself stays put; only what changed moves. */
@keyframes fb-swap {
  from { opacity: 0; transform: translateX(calc(var(--fb-dir, 1) * 14px)); }
  to   { opacity: 1; transform: none; }
}
.v3-fb-strip.is-swap > *,
.v3-fb-review.is-swap > .v3-fb-revcap,
.v3-fb-review.is-swap > .v3-rev-card > * { animation: fb-swap .26s cubic-bezier(.22, 1, .36, 1) backwards; }
.v3-fb-review.is-swap > .v3-rev-card > .v3-rev-text { animation-delay: .04s; }
@media (prefers-reduced-motion: reduce) {
  .v3-fb-strip.is-swap > *, .v3-fb-review.is-swap > .v3-rev-card > *, .v3-fb-review.is-swap > .v3-fb-revcap { animation: none; }
  .s-home-v3 .v3-fb-review { transition: none; }
}

/* ── 1b. THE LIKE AND THE COMMENT SIT 10px FROM THE TIME (Eric, 2026-09-28:
   "closer to the time ago … like 10px away from it") ─────────────────────
   Every feed card, the deck's and the activity cards included. The gap was
   6px of margin PLUS the slack in two fixed boxes (the like count's 16px,
   the time's 20px), so it read as 15-25px. Both boxes are content-sized now
   and the margin is the whole gap.
   SAME DAY, second pass (Eric: "make the number smaller and make sure it
   doesn't move, maybe we can put the numbers on the left side"): the counts
   are 11px (were 13.5) and sit to the LEFT of their icons (row-reverse), so
   a longer count grows away from the time, not into it. Nothing moves:
   - the heart is hard against the 10px gap, with nothing between it and
     the time;
   - the like's count is a fixed 22px box, right-aligned ("1.2k" fits), so
     the comment bubble left of it holds its place too;
   - the time is a fixed 20px box with its text LEFT-aligned, so "2h" and
     "11h" both start exactly 10px after the heart. ⚠️ The price is on the
     far right: a two-character time ends ~7px short of the card's edge.
     `text-align: right` squares the edge again and lets the gap vary. */
/* Third pass (Eric, same day): the heart 3px off the time ("as close as
   possible"), and the comment closer to the likes — 4px between the two
   pills (was 9) and the like's count box 16px (was 22: three digits fit; a
   rare "1.2k" nudges the bubble ~5px). */
.s-home-v3 .v3-rev-card--feed .v3-rev-top-acts { margin-right: 3px; gap: 4px; }
.s-home-v3 .v3-rev-card--feed .v3-rev-top-acts + .v3-rev-time { min-width: 20px; text-align: left; }
.s-home-v3 .v3-rev-card--feed .v3-rev-top-acts .v3-up { flex-direction: row-reverse; font-size: 11px; }
.s-home-v3 .v3-rev-card--feed .v3-rev-top-acts .v3-up--like .v3-up-n { min-width: 16px; text-align: right; }

/* ── 4. THE PROFILE'S HEAD (profHeadHtml) — the traced card is off the page ──
   Plain flow, centred: round picture, name, @handle, bio, location, tags,
   the one action. Inked off --pf-fg / --pf-fg2, the page's own text tokens
   (the card's --pf-ink was for the card's surface), so skins and light
   follow. `.prof-pic`, `.prof-tags` and `.prof-act` are the card's classes,
   re-seated here: every rule is `.prof-head …` to out-rank the card's
   absolute, percentage-of-the-trace placement. */
.prof-head {
  display: flex; flex-direction: column; align-items: center;
  padding: 10px 14px 6px; text-align: center;
}
.prof-head .prof-pic {
  position: relative; left: auto; top: auto;
  width: 112px; height: 112px; border-radius: 50%;
  background-size: cover; background-position: center;
  box-shadow: 0 0 0 1px var(--sd-line), 0 10px 26px rgba(0,0,0,0.28);
}
.prof-head-name {
  margin-top: 14px; max-width: 100%;
  font-family: var(--font-main); font-size: 21px; font-weight: 800; line-height: 1.15; letter-spacing: -0.01em;
  color: var(--pf-fg, var(--sd-ink));
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.prof-head-at {
  margin-top: 3px;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.03em;
  color: var(--pf-fg2, var(--sd-ink3));
}
.prof-head-bio {
  margin-top: 12px; max-width: 300px;
  font-family: var(--font-main); font-size: 13px; line-height: 1.5;
  color: var(--pf-fg, var(--sd-ink)); opacity: 0.86;
  text-wrap: pretty;
}
.prof-head-loc {
  margin-top: 9px; display: inline-flex; align-items: center; gap: 5px;
  font-family: var(--font-main); font-size: 11.5px;
  color: var(--pf-fg2, var(--sd-ink3));
}
.prof-head-loc svg { width: 12px; height: 12px; }
.prof-head .prof-tags { justify-content: center; margin: 14px 0 0; }
.prof-head .prof-act {
  position: static; width: auto; height: 34px; margin-top: 16px; padding: 0 20px;
  background: transparent; box-shadow: inset 0 0 0 1px var(--sd-line);
  color: var(--pf-fg, var(--sd-ink));
}
.prof-head .prof-act:active { box-shadow: inset 0 0 0 1px var(--sd-line); opacity: 0.7; }
.prof-head .prof-act-ico { color: inherit; }
.prof-head .prof-act-lbl { font-family: var(--font-main); font-size: 12px; font-weight: 700; letter-spacing: 0; text-transform: none; color: inherit; }
.prof-head .prof-act--follow { background: var(--pf-fg, var(--sd-ink)); box-shadow: none; color: var(--sd-bg); min-width: 120px; }
.prof-head .prof-act--follow.is-following { background: transparent; box-shadow: inset 0 0 0 1px var(--sd-line); color: var(--pf-fg2, var(--sd-ink2)); }
.prof-head .prof-act--follow.is-following .prof-act-lbl { color: inherit; }

/* ── 5. TAGS ARE FLAT COLOUR (Eric, 2026-09-28: "get rid of the bg pattern
   and just use colors") ─────────────────────────────────────────────────
   The textures (`.sd-tex--*`, brushed steel, daisies, grooves…) are switched
   off on every chip — the profile, the Edit Profile picker and the shop wear
   the same tag, so all three go flat together. A chip is its own tint: a
   free one a 12% wash with a tinted outline, a collectible the full colour
   with its ring. The light halo under a collectible's label was there to
   read over a texture and goes with it. The texture classes are untouched
   above; delete this rule to bring them back. */
.prof-tag, .pp-tag { background-image: none; text-shadow: none; }

/* ── 2. PEOPLE YOU MAY KNOW (pymkHtml, three cards down the feed) ───────── */
.v3-pymk { position: relative; padding: 18px 0 20px; }
.v3-pymk::after {   /* the feed card's full-width divider */
  content: ''; position: absolute; left: -10px; right: -10px; bottom: 0; height: 1px;
  background: var(--sd-line); pointer-events: none;
}
.v3-pymk-hd {
  padding: 0 9px 12px;
  font-family: var(--font-main); font-size: 13px; font-weight: 700;
  color: var(--sd-ink);
}
/* Bleeds to the screen's edges (the scroll area's 10px) so the last card
   peeks off the frame; scroll-padding keeps a snap from eating the inset. */
.v3-pymk-rail {
  display: flex; gap: 8px;
  margin: 0 -10px; padding: 0 19px 2px;
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x proximity; scroll-padding-left: 19px;
  scrollbar-width: none;
}
.v3-pymk-rail::-webkit-scrollbar { display: none; }
.s-home-v3 .v3-pymk .v3-pymk-card {
  flex: 0 0 112px; min-width: 0; margin: 0;
  display: flex; flex-direction: column; align-items: center; gap: 0;
  padding: 14px 10px 12px;
  border-radius: 14px;
  background: var(--sd-card);
  box-shadow: 0 0 0 1px var(--sd-line);
  scroll-snap-align: start;
  text-align: center;
}
.s-home-v3 .v3-pymk .v3-pymk-av { width: 56px; height: 56px; box-shadow: 0 0 0 1px var(--sd-line); }
.v3-pymk-name {
  max-width: 100%; margin-top: 9px;
  font-family: var(--font-main); font-size: 12.5px; font-weight: 700;
  color: var(--sd-ink); cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.v3-pymk-sub {
  margin-top: 2px;
  font-family: var(--font-main); font-size: 10.5px;
  color: var(--sd-ink3); white-space: nowrap;
}
.v3-pymk-btn {
  margin-top: 11px; width: 100%; height: 28px; padding: 0;
  border: none; border-radius: 999px; cursor: pointer;
  font-family: var(--font-main); font-size: 11.5px; font-weight: 700;
  background: var(--sd-ink); color: var(--sd-bg);
}
.v3-pymk-btn.is-on { background: transparent; color: var(--sd-ink2); box-shadow: inset 0 0 0 1px var(--sd-line); }

/* ── 3. THE NAV'S + (bottomNav) AND ITS SEARCH (openSearch 'review') ─────
   Five items now; the middle one is a filled disc so it reads as the one
   ACTION among four places. It turns to an × while its search is up. */
.v3-nav-add {
  width: 34px; height: 34px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: #e8e2d6; color: #111116;
}
.v3-nav-item--add .v3-nav-add svg { width: 17px; height: 17px; }
.v3-nav-item--add.is-on .v3-nav-add svg { rotate: 45deg; }
.s-home-v3--light .v3-nav-add { background: #1a1208; color: #f0ece3; }
/* The overlay in review mode: a heading, no tabs, and a "Review" tag on
   every row so it is plain what a tap will do. */
.sds-mode { display: none; }
/* It stands IN FOR THE TABS: same row, same height (a tab is 9 + 11 of
   padding round a 12px line), same hairline under it — so switching between
   Search and Write a review moves nothing above or below it. */
.sds-overlay--review .sds-mode {
  display: block; padding: 8px 16px 10px;
  font-family: var(--font-main); font-size: 14px; font-weight: 800; line-height: 18px;
  color: #e8e2d6;
  border-bottom: 1px solid rgba(255,255,255,0.07);
}
.sds-overlay--review .sds-tabs { display: none; }
.sds-row-go {
  flex-shrink: 0; padding: 5px 10px; border-radius: 999px;
  font-family: var(--font-main); font-size: 11px; font-weight: 700;
  color: #e8e2d6; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.16);
}
/* The zero state: six popular albums in the wall's grid, then the ad slot.
   The cells are <button>s here (the overlay's delegated tap reads their data
   attributes), so the button chrome comes off and the text goes back left. */
.sds-pickgrid { padding: 8px 16px 0; }
.sds-pickgrid .wall2-cell { position: relative; padding: 0; border: none; background: none; text-align: left; font-family: var(--font-main); }
.sds-ad {
  width: 300px; max-width: calc(100% - 32px); aspect-ratio: 300 / 250;
  margin: 22px auto 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  border-radius: 10px;
  border: 1px dashed rgba(232,226,214,0.22);
  background: rgba(255,255,255,0.03);
  font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: 0.8px; text-transform: uppercase;
  color: rgba(232,226,214,0.4);
}
.sds-ad i { font-style: normal; font-weight: 400; color: rgba(232,226,214,0.28); }
.sds-overlay--light .wall2-album  { color: #1a1208; }
.sds-overlay--light .wall2-artist { color: rgba(26,18,8,0.5); }
.sds-overlay--light .wall2-score  { color: rgba(26,18,8,0.55); }
.sds-overlay--light .wall2-art    { box-shadow: 0 6px 14px rgba(30,20,10,0.18), 0 2px 5px rgba(30,20,10,0.15); }
.sds-overlay--light .sds-ad { border-color: rgba(26,18,8,0.22); background: rgba(26,18,8,0.03); color: rgba(26,18,8,0.45); }
.sds-overlay--light .sds-ad i { color: rgba(26,18,8,0.32); }
.sds-overlay--light .sds-mode { color: #1a1208; border-bottom-color: rgba(26,18,8,0.1); }
.sds-overlay--light .sds-row-go { color: #1a1208; box-shadow: inset 0 0 0 1px rgba(26,18,8,0.2); }

/* ============================================================
   MINIMAL MOTION — csharpuser (2026-09-25). Eric: "reduce the interactions
   like the animations to a minimum". At-rest loops are off for everyone
   (the same resting states the prefers-reduced-motion rules above pick),
   entry animations are gone (`animation: none` shows the resting state —
   every one of them fills backwards from it), and the long transitions
   are cut to a fast, functional length, so sheets still slide and states
   still settle but nothing lingers. Gestures and their end states are
   untouched. To bring something back, delete its line HERE, not the
   original rule. ⚠ Keep this block LAST in the file: it wins by order.
   ============================================================ */
/* At-rest loops */
.v3-cmt-btn .cmt-dot { animation: none; opacity: 1; }
.v3-blue-quote--scroll .v3-blue-quote-text { animation: none; }
.v3-cd, .plp-cd, .v3-rsh-cd, .prof-fav-img.prof-fav--spin { animation: none; }   /* no spinning records */
.v3-live-dot, .v3-ring-dot, .v3-rev-caret, .ob-track-wave i { animation: none; }
.v3-up--heart:not(.is-on) .rvp-hole { animation: none; opacity: 0; }
.sd-ico-live { animation: none; }
.v3-rev-hint-run, .v3-rev-yours .hstar { animation: none; }
.v3-fbr .v3-up-n.is-flip { animation: none; }
/* Entries */
.s-home-v3--review .v3-review-panel,
.s-home-v3--rvp .v3-rvp-panel,
:is(.s-rvp, .s-home-v3--rvp) .v3-body,
body.sd-return .s-home-v3--review .v3-body,
.ob-panel, .ob-genres.is-on, .ob-hub-t,
.mix-inline.is-opening .ob-dial, .mix-inline.is-opening .ob-hub-svg { animation: none; }
/* Transitions: everything that ran .3s or longer now runs .2s */
.sd-face-dot, .v3-for-single, .v3-blue-artist, .v3-blue-album, .s-home-v3, .v3-ring-dot,
.s-home-v3--review .v3-bento, .s-home-v3--review .v3-album, .s-home-v3--review .v3-blue,
.sd-log-sheet, .plc-sheet, .pldel-sheet, .sds-panel, .pl2-card-art,
.v3-fb-card, .v3-fb-card::after, .v3-fbr-face { transition-duration: .2s; }
.v3-rsh.is-drag { transition: none; }   /* restated after the cap above: the sheet still follows the finger */

/* ══════════════════════════════════════════════════════════════════════════
   THE SPLIT FEED CARD (0.12, 2026-10-02) — the layout Eric's friend drew.
   revCardInner `split`. Top row: photo · name · time on the left, year ·
   album over artist right-aligned on the right (both in the "top" area, the
   titles hung right). Then the review on the left and, beside it, the cover
   big with the score and discs under it. Comment + like counts at the foot,
   left. Scoped (0,4,0) so it out-ranks every `.v3-rev-card--feed` rule above.
   ══════════════════════════════════════════════════════════════════════════ */
.s-home-v3 .v3-rev-card.v3-rev-card--feed.v3-rev-card--split,
.s-home-v3 .v3-rev-card.v3-rev-card--page.v3-rev-card--split {
  grid-template-columns: minmax(0, 1fr) 80px;   /* 96 → 80 (Eric, 2026-10-02: "album a bit smaller") */
  grid-template-areas:
    "top  top"
    "text side"
    "acts side"
    "foot foot";
  grid-template-rows: auto 1fr auto auto;
  column-gap: 14px; row-gap: 0;
}
.s-home-v3 .v3-rev-card--split .v3-rev-card-top { grid-area: top; min-width: 0; max-width: 52%; align-self: start; }
.s-home-v3 .v3-rev-card--split .v3-rev-name-row { display: flex; align-items: baseline; gap: 9px; min-width: 0; }
.s-home-v3 .v3-rev-card--split .v3-rev-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.s-home-v3 .v3-rev-card--split .v3-rsp-time {
  flex-shrink: 0; font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em; color: var(--sd-ink3);
}
/* The record's titles: the right half of the top row, right-aligned. */
.s-home-v3 .v3-rev-card--split .v3-rsp-rec {
  grid-area: top; justify-self: end; align-self: center; max-width: 50%; min-width: 0;
  display: flex; flex-direction: column; align-items: flex-end; text-align: right; gap: 2px; cursor: pointer;
}
.s-home-v3 .v3-rev-card--split .v3-rsp-rec .v3-rev-record-line { display: flex; align-items: baseline; justify-content: flex-end; gap: 5px; max-width: 100%; min-width: 0; position: static; }
.s-home-v3 .v3-rev-card--split .v3-rsp-rec .v3-rev-record-album { font-size: 12.5px; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.s-home-v3 .v3-rev-card--split .v3-rsp-rec .v3-rev-record-year { font-size: 10px; flex-shrink: 0; }
.s-home-v3 .v3-rev-card--split .v3-rsp-rec .v3-rev-record-artist { font-size: 11px; max-width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; position: static; }
/* The review, left. Keeps the feed's clamp + fade (.v3-rev-text / .is-long). */
.s-home-v3 .v3-rev-card--split .v3-rsp-text { grid-area: text; margin-top: 16px; align-self: start; min-width: 0; }
/* The cover and the score, right. */
.s-home-v3 .v3-rev-card--split .v3-rsp-side {
  grid-area: side; margin-top: 12px; display: flex; flex-direction: column; align-items: center; gap: 9px;
}
.s-home-v3 .v3-rev-card--split .v3-rsp-art {
  width: 80px; aspect-ratio: 1; border-radius: 8px; background: var(--sd-card) center / cover no-repeat;
  box-shadow: 0 0 0 1px var(--sd-line), 0 6px 16px rgba(0,0,0,.35); cursor: pointer;
}
.s-home-v3 .v3-rev-card--split .v3-rsp-score { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.s-home-v3 .v3-rev-card--split .v3-rsp-score .v3-rev-big-n { font-size: 22px; line-height: 1; }
.s-home-v3 .v3-rev-card--split .v3-rsp-score .hstars { top: 0; }
/* Comment + like at the foot, left: bare glyphs and counts, small. */
.s-home-v3 .v3-rev-card--split .v3-rsp-acts { grid-area: acts; align-self: end; margin-top: 14px; display: flex; align-items: center; gap: 12px; }
.s-home-v3 .v3-rev-card--split .v3-rsp-acts .v3-up {
  padding: 3px 0; gap: 4px; background: transparent; color: var(--sd-ink3); font-size: 12px; border-radius: 0;
}
.s-home-v3 .v3-rev-card--split .v3-rsp-acts .v3-up svg { width: 16px; height: 16px; }
/* 0.13 (2026-10-08): the PENCIL, LAST in the acts row so it reads to the right
   of the likes. It inherits the row's .v3-up sizing and ink; only the hover
   colour is its own, so it never looks like a like. */
.s-home-v3 .v3-rev-card--split .v3-rsp-acts .v3-rev-editbtn:hover { color: var(--star, #e8a83c); }
.s-home-v3 .v3-rev-card--split .v3-rsp-acts .v3-up:hover { background: transparent; color: var(--sd-ink); }
.s-home-v3 .v3-rev-card--split .v3-rsp-acts .v3-up--heart.is-on { color: #ff5c8a; }
.s-home-v3 .v3-rev-card--split .v3-rsp-acts .v3-up--heart.is-on svg { fill: currentColor; }

/* The split card on the ALBUM page and the ARTIST page (0.12, 2026-10-02 —
   Eric: the album page's reviews and the profile's wear it too; the profile's
   are --feed and need nothing more). These are `--page` cards: no --sd-* re-ink,
   they sit on the album page's dark surface, so the counts and the time take
   the page's light ink rather than the feed's theme ink. The artist page's
   cream light theme re-inks them dark, as the rules near `--light.--artist`
   do for the rest of its panel. `.v3-rsp-foot` is your own card's share. */
.s-home-v3 .v3-rev-card--page.v3-rev-card--split .v3-rsp-time { color: rgba(232,226,214,0.42); }
.s-home-v3 .v3-rev-card--page.v3-rev-card--split .v3-rsp-acts .v3-up { color: rgba(232,226,214,0.55); }
.s-home-v3 .v3-rev-card--page.v3-rev-card--split .v3-rsp-acts .v3-up:hover { color: rgba(232,226,214,0.9); }
.s-home-v3 .v3-rev-card--page.v3-rev-card--split .v3-rsp-acts .v3-up--heart.is-on { color: #ff5c8a; }
.s-home-v3 .v3-rev-card--page.v3-rev-card--split .v3-rsp-acts .v3-up--cmtcol[data-n="0"] { display: none; }
/* … EXCEPT ON YOUR OWN (Eric, 2026-10-08: "comment icon on the album review
   too for yours"). A review you wrote carries the comment icon whether or not
   anyone has answered it yet, so the row reads cmt · like · pencil here the
   same as it does on the home feed. One class more than the rule above, so it
   wins on specificity wherever it sits. */
.s-home-v3 .v3-rev-card--page.v3-rev-card--split.v3-rev-card--mine .v3-rsp-acts .v3-up--cmtcol[data-n="0"] { display: inline-flex; }
.s-home-v3 .v3-rev-card--split .v3-rsp-foot { grid-area: foot; margin-top: 12px; }
/* 0.12 (2026-10-02): no "X listened to this" pill on the album page or the
   artist page (Eric). JS un-hides `.v3-rev-rec` when a friend logged the
   record, so this wins with !important rather than racing it. */
.s-home-v3--album .v3-rev-rec { display: none !important; }
/* 0.12 (2026-10-02): review cards sit 3.5px closer to the screen edges
   (Eric — "a bit large"). Home + profile: the feed card's 9px sides → 5.5.
   Album + artist pages: the list's 12px gutter → 8.5, and the pinned card's
   bleed follows it (it must equal the gutter so its tint still meets the edge
   while its text lines up with the rest). The discovery wall is untouched. */
.s-home-v3 .v3-rev-card.v3-rev-card--feed.v3-rev-card--split { padding-left: 5.5px; padding-right: 5.5px; }
.s-home-v3 .v3-rev-list { margin-left: 8.5px; margin-right: 8.5px; }
.s-home-v3 .v3-rev-card.v3-rev-card--pinned { --rev-bleed: 8.5px; }
.s-home-v3--album .v3-rev-card.v3-rev-card--pinned { --rev-bleed: 18.5px; }
/* …and the TOP review on home (the deck's `.v3-fbr`) the same way (Eric): its
   text column 346 → 354px and the box's 4px side padding off, so at the 385
   frame the text starts 15.5px in — level with the feed cards under it. */
.s-home-v3 .v3-fb-review { padding-left: 0; padding-right: 0; }
.s-home-v3:not(.s-home-v3--fb-left) .v3-fbr-text { width: 354px; }   /* not the Alts compact phone, which sizes it by grid */
/* ══ ONBOARDING (0.12, 2026-10-02) ══════════════════════════════════════════
   Genres are the flat bubble LIST only — the Wheel | List switch and the
   dial's back are gone with the wheel (OB.genreView is pinned to 'list').
   Step 8, QUICK REVIEW: rows in the app's shape (the search overlay's album
   row) — cover · album over artist · a "Review" pill, or your score and a
   check once reviewed. */
.ob-panel--genres .ob-view-row,
.ob-panel--genres .ob-genres[data-genres="wheel"] { display: none !important; }
.ob-rlist { display: flex; flex-direction: column; }   /* the panel already pads for the overlaid foot (.ob-stage-fade) */
.ob-rrow {
  display: flex; align-items: center; gap: 12px; width: 100%;
  padding: 9px 0; border: 0; border-bottom: 1px solid var(--border); background: none;
  color: var(--text); font-family: var(--font-main); text-align: left; cursor: pointer;
}
.ob-rrow-art { width: 48px; height: 48px; flex-shrink: 0; border-radius: 6px; background: var(--surface) center / cover no-repeat; }
.ob-rrow-who { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ob-rrow-album { font-size: 14px; font-weight: 400; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ob-rrow-artist { font-size: 12px; font-weight: 700; color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ob-rrow-go {
  flex-shrink: 0; padding: 7px 13px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--border);
  font-size: 12px; font-weight: 700; color: var(--text);
}
.ob-rrow.is-done .ob-rrow-go { background: var(--star, #e8a83c); border-color: transparent; color: #16130d; }

/* On the ALBUM page itself the record is the page (Eric, 2026-10-02): no cover
   and no album / artist / year on the cards — just the score on the right.
   The artist page keeps them (its banner is the artist, not the album), and
   so do home and the profile. */
.s-home-v3--album:not(.s-home-v3--artist) .v3-rev-card--page.v3-rev-card--split { grid-template-columns: minmax(0, 1fr) auto; }
.s-home-v3--album:not(.s-home-v3--artist) .v3-rev-card--page.v3-rev-card--split .v3-rsp-art,
.s-home-v3--album:not(.s-home-v3--artist) .v3-rev-card--page.v3-rev-card--split .v3-rsp-rec { display: none; }
.s-home-v3--album:not(.s-home-v3--artist) .v3-rev-card--page.v3-rev-card--split .v3-rev-card-top { max-width: 100%; }
.s-home-v3--album:not(.s-home-v3--artist) .v3-rev-card--page.v3-rev-card--split .v3-rsp-side { margin-top: 16px; }
.s-home-v3--light.s-home-v3--album .v3-rev-card--page.v3-rev-card--split .v3-rsp-time,
.s-home-v3--light.s-home-v3--album .v3-rev-card--page.v3-rev-card--split .v3-rsp-acts .v3-up,
.s-home-v3--light.s-home-v3--album .v3-rev-card--page.v3-rev-card--split .v3-rev-record-year { color: rgba(26,18,8,0.42); }
.s-home-v3--light.s-home-v3--album .v3-rev-card--page.v3-rev-card--split .v3-rev-record-album,
.s-home-v3--light.s-home-v3--album .v3-rev-card--page.v3-rev-card--split .v3-rev-big-n { color: #1a1208; }
.s-home-v3--light.s-home-v3--album .v3-rev-card--page.v3-rev-card--split .v3-rev-record-artist { color: rgba(26,18,8,0.6); }
.s-home-v3--light.s-home-v3--album .v3-rev-card--page.v3-rev-card--split .v3-rsp-acts .v3-up--heart.is-on { color: #e0396e; }

/* 0.12 (2026-10-02): THE DECK'S REVIEW IS 0.1's AGAIN — centred, no box.
   fbGo puts 0.1's `.v3-fbr` block back into `.v3-fb-review`, so 0.11's panel
   (fill, hairline, notch, rounded box — "1. THE DECK'S REVIEW IS THE FEED'S
   CARD, IN A PANEL" above) is switched off here and the box takes 0.1's plain
   `padding: 0 4px`. Delete this block and restore 0.11's fbGo block to bring
   the panel back. */
.s-home-v3 .v3-fb-review:not(:empty) {
  margin: 0; padding: 0 4px; border-radius: 0;
  background: none; box-shadow: none; cursor: default;
}
.s-home-v3 .v3-fb-review:not(:empty)::before,
.s-home-v3 .v3-fb-review:not(:empty)::after { display: none; }

/* 0.12 (2026-10-02): the album wheel is back on the profile (profileHtml →
   profFavsHtml). Its heading stays on one line. */
.prof-favs .prof-sec-hd { white-space: nowrap; }

/* 0.12 (2026-10-02): THE ALBUM PAGE'S RATE BUTTON, JUST THE CIRCLE — back in
   place of the "Your review" bar (Eric: "make it a round circle button
   again"). -0.11's rules: the wings, the four lobes (Listened · Later ·
   Favorite · Share — all in the log sheet), the arc label and the "_._"
   placeholder are hidden; the unrated circle says "Rate / album" (syncRevCta).
   The group box keeps the drawing's 1633×1321 shape so the circle keeps its
   place; the negative margin gives back the room the lower wings took. */
.v3-rate-group .v3-rg-wing,
.v3-rate-group .v3-rg-btn,
.v3-rate-group .v3-rg-arc,
.v3-rate-group .v3-rg-blank { display: none; }
.v3-rate-group { margin-top: 7px; margin-bottom: calc(10px - 17.74%); }   /* margin-top: 14 (a touch lower), 10 back up, then 3 down (Eric, 2026-10-02) */
.v3-rg-word--rate {
  font-size: 24px; font-weight: 800; line-height: 1.05; letter-spacing: -0.01em;
  text-align: center; color: rgba(232,226,214,0.92);
}

/* 0.12 (2026-10-02): THE CDs SPIN on the album page again (Eric). MINIMAL
   MOTION above stops every record; this turns the album page's own CD and the
   review record line's CD back on, at their original 12s a turn. Everywhere
   else stays still. */
.s-home-v3--album .v3-cd,
.s-home-v3--album .v3-rsh-cd { animation: v3spin 12s linear infinite; }

/* 0.12 (2026-10-02): SAVE CHANGES, pinned to the bottom of the rate & review
   sheet while anything is unsaved (paintLogSave toggles `hidden`). Sticky in
   the sheet — the sheet is the scroller — so it stays put however far you
   scroll. The Post that rode the cover is off; the bar does the saving. */
.sd-log-cover .sd-log-actions,
.sd-log-cover .sd-log-actions:has(.sd-log-post[data-mode="post"]) { display: none; }
.sd-log-savebar {
  position: sticky; bottom: 0; z-index: 3;
  margin: 14px -12px -14px; padding: 12px 12px 16px;
  background: linear-gradient(to bottom, transparent, var(--sd-bg, #111116) 34%);
  display: flex; justify-content: center;
}
.sd-log-savebar[hidden] { display: none; }

/* ── SONG COMMENTS (0.14, 2026-10-09) ──────────────────────────────────────
   The thread under a song's discs, in the review threads' own clothes (the
   markup IS cmtWrapHtml's, so .v3-cmt* styles it). Eric: "not a review per
   say its more casual".
   ⚠️ ONE SCROLLER, AND IT IS THE SHEET. The thread grows to its full height
   and the POPUP scrolls (Eric, same day: "the comment section in the songs
   comments doesnt just scroll like the whole song popup scrolls"). It was the
   other way round for an hour — `.sd-log-cmts-body` capped at 176px with its
   own `overflow-y: auto` — and that reads as a window inside a window: a box
   to scroll separately, inside a sheet already capped at 60%, with the discs
   stranded above it. So NO max-height and NO overflow here. `.sd-log-sheet` is
   already `overflow-y: auto`, `--song` caps it at 60%, and `.sd-log-savebar`
   is `position: sticky` — so Post review stays on the bottom edge however far
   down the thread you have scrolled. */
.sd-log-cmts {
  margin: 14px -2px 0; padding-top: 10px;
  border-top: 1px solid rgba(232,226,214,0.10);
}
.sd-log-cmts[hidden] { display: none; }
.sd-log-cmts-hd {
  font-family: var(--font-mono); font-size: 8.5px;
  text-transform: uppercase; letter-spacing: 0.12em;
  color: rgba(232,226,214,0.3);
  margin-bottom: 2px;
}
.sd-log-cmts-n { color: rgba(232,226,214,0.5); }

/* ── THE INFINITE-SCROLL TAIL (0.14, 2026-10-09) ───────────────────────
   What replaced "View n more" (Eric: "just have all the comments and have a
   inf scroll type load"). Three dots at the end of the list while anything is
   still held back; cmtAutoMore pages the rest in as you reach them, and this
   row disappears with the last of them. NOT a control — no cursor, no hit
   target, aria-hidden in the markup: tapping it must do nothing, because
   scrolling is the gesture now. */
.v3-cmt-loading {
  display: flex; align-items: center; justify-content: center; gap: 5px;
  padding: 16px 0 10px;
}
.v3-cmt-loading i {
  width: 4px; height: 4px; border-radius: 50%;
  background: rgba(232,226,214,0.32);
  animation: cmtDots 1.1s ease-in-out infinite;
}
.v3-cmt-loading i:nth-child(2) { animation-delay: .16s; }
.v3-cmt-loading i:nth-child(3) { animation-delay: .32s; }
@keyframes cmtDots {
  0%, 60%, 100% { opacity: .28; transform: translateY(0); }
  30%           { opacity: .85; transform: translateY(-2px); }
}
.s-home-v3--light .v3-cmt-loading i { background: rgba(26,18,8,0.3); }
/* Reduced motion: still three dots, just still ones. */
@media (prefers-reduced-motion: reduce) {
  .v3-cmt-loading i { animation: none; opacity: .4; }
}
.s-home-v3--light .sd-log-cmts { border-top-color: rgba(232,226,214,0.14); }

/* The tracklist's comment cell — the glyph and the count where the length
   was. Keeps .v3-song-dur (and so the column width), restyles the inside. */
.v3-song-cmt {
  display: inline-flex; align-items: center; justify-content: flex-end; gap: 4px;
}
.v3-song-cmt svg { width: 11px; height: 11px; flex-shrink: 0; }
.v3-song-cmt-n { font-family: var(--font-mono); font-size: 10px; }
/* A song nobody has talked about yet: the glyph alone, dimmed, no 0. */
.v3-song-cmt-n:empty { display: none; }
/* 0.20 (2026-10-13): THE GLYPHS LINE UP (Eric: "they are offset by number of
   comments rn"). The count gets a fixed three-digit slot, left-aligned, so the
   bubble sits in the same place on every row — empty included (the slot stays,
   only the 0 is missing). `ch` is exact in the mono face. */
.v3-song-cmt-n, .v3-song-cmt-n:empty { display: inline-block; min-width: 3ch; text-align: left; }
/* 0.13 (2026-10-08): the bar is a ROW capped at the old button's width. SAVE
   CHANGES and EDIT REVIEW each FILL it (they are never up together — Edit is
   the locked state), and the delete circle sits to their right.
   ⚠️ The max-width moved off the button onto the row — left on the button, it
   and the circle would be 320px + 46px and overflow the sheet. */
.sd-log-saverow {
  width: 100%; max-width: 320px;
  display: flex; align-items: center; justify-content: flex-end; gap: 10px;
}
/* ⚠️ EXPLICIT GOLD, NOT `var(--star)` (2026-10-09). Inside a home shell
   `--star` is `var(--v3-star, var(--persona-accent, #e8a83c))`, i.e. the
   FEATURED ALBUM's extracted colour — and it is forced to #e8a83c only on the
   album page (`.s-home-v3--review.s-home-v3--album`). So the sheet looked
   right when opened from an album, and when the PENCIL opened it over the
   bento it took whatever tint that cover happened to yield: Eric's Post review
   button "doesn't light up". The sheet is a modal — it is not about the album
   behind it — so it carries its own gold in both themes. Same trap as the
   just-posted ring. */
.sd-log-savebtn, .sd-log-editbtn {
  flex: 1 1 auto; min-width: 0; height: 46px; border: 0; border-radius: 23px;
  background: #e8a83c; color: #1a1208;
  font: 800 15px var(--font-main); letter-spacing: .01em; cursor: pointer;
  box-shadow: 0 8px 22px rgba(0,0,0,0.45);
}
.s-home-v3--light .sd-log-savebtn, .s-home-v3--light .sd-log-editbtn { background: #c08818; color: #fffaf0; }
.sd-log-savebtn:active, .sd-log-editbtn:active { transform: scale(.98); }
/* Dimmed, not gone, in the unlocked-but-unchanged state: the bar must not
   jump the moment you press Edit review. */
.sd-log-savebtn:disabled { opacity: .42; box-shadow: none; cursor: default; }
.sd-log-savebtn:disabled:active { transform: none; }
/* — the delete circle — right of Edit review. Red in the TYPE and the hairline
   only, like the playlist trash (.plnew-delete): a destructive option, never a
   second call to action. It only opens the confirm (logAskDelete). */
.sd-log-delbtn {
  flex: 0 0 auto; width: 46px; height: 46px; padding: 0;
  border-radius: 50%; border: 1px solid rgba(232,86,86,0.34);
  background: transparent; color: #e85656;
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; transition: background .15s, border-color .15s;
}
.sd-log-delbtn svg { width: 19px; height: 19px; }
.sd-log-delbtn:hover { background: rgba(232,86,86,0.09); border-color: rgba(232,86,86,0.55); }
.sd-log-delbtn:active { transform: scale(.94); }
/* Explicit, because the flex rules above would otherwise beat the UA's
   [hidden] and leave two buttons on screen at once. */
.sd-log-savebtn[hidden], .sd-log-editbtn[hidden], .sd-log-delbtn[hidden] { display: none; }

/* — "edit review" — the nudge a locked control answers with (nudgeEditReview).
   Anchored to the bar, which is `position: sticky` and therefore already a
   containing block, so it rides above Edit review wherever the bar has got to.
   `pointer-events: none`: it must never eat the tap that follows it. */
.sd-log-nudge {
  /* CLEAR OF THE BUTTON. `calc(100% - 10px)` hung the pill 10px down INTO the
     bar and its 5px arrow then reached 15px in, past the bar's 12px of top
     padding, so the arrow sat on Edit review (Eric, 2026-10-08). Measured from
     the bar's padding-box top OUTWARDS now, so the arrow tip lands just above
     it and the button is never touched. */
  position: absolute; left: 50%; bottom: calc(100% + 6px);
  transform: translate(-50%, 6px);
  padding: 6px 13px; border-radius: 999px;
  background: rgba(16,16,20,0.95); color: #f2ece0;
  font: 700 11.5px var(--font-main); letter-spacing: .03em; white-space: nowrap;
  box-shadow: 0 6px 18px rgba(0,0,0,0.5);
  opacity: 0; pointer-events: none;
  transition: opacity .16s ease, transform .16s ease;
}
.sd-log-nudge::after {
  content: ''; position: absolute; top: 100%; left: 50%;
  margin-left: -5px; border: 5px solid transparent;
  border-top-color: rgba(16,16,20,0.95);
}
.sd-log-savebar.is-nudging .sd-log-nudge { opacity: 1; transform: translate(-50%, 0); }
/* The locked sheet reads as locked: the controls you cannot touch go quiet.
   NOT pointer-events:none — the tap has to land so it can be answered with
   the nudge above. */
.sd-log-sheet.is-locked .sd-log-stars-track,
.sd-log-sheet.is-locked .sd-log-song > :not(.sd-log-song-cmt) { opacity: .72; }   /* rows, not the song threads (0.20) */   /* not .sd-log-opts — the toggles stay live (0.20) */
.sd-log-sheet.is-locked .sd-log-write { opacity: .82; cursor: default; }
.s-home-v3--light .sd-log-savebar { background: linear-gradient(to bottom, transparent, var(--sd-bg, #f0ece3) 34%); }
.s-home-v3--light .sd-log-delbtn { color: #c93b3b; border-color: rgba(201,59,59,0.32); }
.s-home-v3--light .sd-log-delbtn:hover { background: rgba(201,59,59,0.08); border-color: rgba(201,59,59,0.55); }

/* 0.12 (2026-10-02): what you did, inside the round rate button (syncRevCta):
   the toggles you set and "Written review", under the number + discs. Ink
   follows the disc — dark on the gold (rated), light on the dark disc. */
.v3-rg-did, .v3-rg-wrote {
  max-width: 82%; text-align: center; line-height: 1.25;
  font-family: var(--font-main); font-size: 10.5px; font-weight: 700; letter-spacing: .01em;
}
.v3-rg-did { margin-top: 4px; }
.v3-rg-wrote { font-weight: 600; opacity: .85; }
.v3-rg-rate:not(.is-rated) .v3-rg-did,
.v3-rg-rate:not(.is-rated) .v3-rg-wrote { color: rgba(232,226,214,0.82); }

/* ══════════════════════════════════════════════════════════════════════════
   THE BACK CARET (0.12, 2026-10-02). One back control: the quick review
   page's bare "‹" (.sds-back's look), in the header's top-left where the
   notification bell sits. It shows — and the bell hides — on any screen with
   a way back: the album / artist page (`--review`), a review open in place
   (`--rvp`), and every screen that carries a back pill (notifications,
   settings, edit profile, the review page). The floating back pills and the
   album page's corner "Back" pill are hidden; sdHeaderBack does their job.
   ══════════════════════════════════════════════════════════════════════════ */
.v3-hdr-back {
  display: none; flex-shrink: 0; width: 32px; height: 32px; margin-left: -4px;
  border: none; background: none; padding: 0; cursor: pointer;
  color: rgba(232,226,214,0.85); align-items: center; justify-content: center;
}
.v3-hdr-back svg { width: 22px; height: 22px; }
.s-home-v3--light .v3-hdr-back { color: rgba(26,18,8,0.75); }
.s-home-v3--review .v3-hdr-back,
.s-home-v3--rvp .v3-hdr-back,
.app-screen:has(.plp-back-pill) .v3-hdr-back { display: inline-flex; }
.s-home-v3--review .v3-bubble--notif,
.s-home-v3--rvp .v3-bubble--notif,
.app-screen:has(.plp-back-pill) .v3-bubble--notif { display: none; }
/* The old backs, off: the pills, and the album page's corner Back. */
.app-screen .plp-back-pill { display: none !important; }
.s-home-v3--review .v3-live-pill,
.s-home-v3--rvp .v3-live-pill { display: none !important; }

/* ══ 0.13 (Eric, 2026-10-03): THE TOP REVIEW'S BYLINE, LIKE THE FEED'S ══════
   "make the profile name on the right side of the profile image like the
   other ones … the same size image as well". Home's deck review (.v3-fbr,
   not the review sheet's --sheet) puts its name INSIDE .v3-fbr-head (see
   fbGo), so the head is the feed card's top row: the 36px face (.v3-rev-av)
   with the name and the time beside it, 9px apart — the pair CENTRED over
   the score (Eric, same day: "have it centered … just the one at the top";
   the feed's cards stay left). */
.s-home-v3 .v3-fbr:not(.v3-fbr--sheet) .v3-fbr-head {
  align-self: center; max-width: 100%;
  justify-content: center; gap: 9px; margin-bottom: 2px;
}
.s-home-v3 .v3-fbr:not(.v3-fbr--sheet) .v3-fbr-av-wrap,
.s-home-v3 .v3-fbr:not(.v3-fbr--sheet) .v3-fbr-av { width: 36px; height: 36px; }
.s-home-v3 .v3-fbr:not(.v3-fbr--sheet) .v3-fbr-av { box-shadow: 0 0 0 1px var(--sd-line, rgba(255,255,255,0.08)); }   /* the feed face's hairline, no drop shadow */
.s-home-v3 .v3-fbr:not(.v3-fbr--sheet) .v3-fbr-name {
  display: flex; align-items: baseline; gap: 9px; min-width: 0;
  font-size: 12.5px; white-space: nowrap;
}
.s-home-v3 .v3-fbr:not(.v3-fbr--sheet) .v3-fbr-who { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.s-home-v3 .v3-fbr:not(.v3-fbr--sheet) .v3-fbr-name .v3-fbr-ago {   /* the feed's time: mono 11, light */
  margin-left: 0; flex-shrink: 0; font-size: 11px; font-weight: 400; letter-spacing: .04em;
}
/* 0.13 (Eric, 2026-10-03): on the FEED's split cards (cover on the right) —
   home's feed and the profile's pinned + history, all `--feed` — the big
   score number is gone, just the discs under the cover. The top review
   (.v3-fbr) and the album / artist pages (`--page`) keep their number. */
.s-home-v3 .v3-rev-card--feed.v3-rev-card--split .v3-rsp-score .v3-rev-big-n { display: none; }

/* ══ 0.13 (Eric, 2026-10-03): THE GEAR ON YOUR PROFILE, FEEDBACK EVERYWHERE ELSE
   Both headers carry both bubbles (screens.js); this picks one. A friend's
   profile (`s-prof2--guest`) is "everywhere else" — the settings are yours. */
.v3-bubble--settings { display: none; }
.s-prof2:not(.s-prof2--guest) .v3-bubble--settings { display: flex; }
.s-prof2:not(.s-prof2--guest) .v3-bubble--feedback { display: none; }
.v3-bubble--feedback svg { width: 18px; height: 18px; }

/* The popup: a card inside the phone over a dimmed screen — the name prompt's
   look (style.css .rec-*), DM Sans throughout. */
.sd-fb {
  position: absolute; inset: 0; z-index: 9000;
  display: flex; align-items: center; justify-content: center; padding: 18px;
  background: rgba(0,0,0,0.6); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  opacity: 0; transition: opacity .18s ease;
}
.sd-fb.open { opacity: 1; }
.sd-fb-card {
  width: 100%; max-width: 360px; box-sizing: border-box;
  padding: 18px; border-radius: 14px;
  background: #1c1c1c; border: 1px solid #303030; box-shadow: 0 20px 60px rgba(0,0,0,0.6);
  color: #e8e8e8; font-family: 'DM Sans', var(--font-main), system-ui, sans-serif;
  transform: translateY(10px) scale(.98); transition: transform .2s cubic-bezier(.22, 1, .36, 1);
}
.sd-fb.open .sd-fb-card { transform: none; }
.sd-fb-hd { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
.sd-fb-title { font-size: 18px; font-weight: 700; }
.sd-fb-x {
  width: 30px; height: 30px; margin: -4px -6px -4px 0; padding: 0; border: 0; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: transparent; color: #9a9a9a; cursor: pointer;
}
.sd-fb-x:hover { background: rgba(255,255,255,0.08); color: #e8e8e8; }
.sd-fb-x svg { width: 16px; height: 16px; }
.sd-fb-lbl { display: block; margin: 0 0 6px; font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: #8a8a8a; }
.sd-fb-selwrap { position: relative; margin-bottom: 14px; }
.sd-fb-sel {
  width: 100%; box-sizing: border-box; appearance: none; -webkit-appearance: none;
  padding: 10px 34px 10px 12px; border-radius: 8px; border: 1px solid #3a3a3a;
  background: #121212; color: #fff; font: inherit; font-size: 14px; cursor: pointer;
}
.sd-fb-selwrap svg { position: absolute; right: 11px; top: 50%; width: 16px; height: 16px; margin-top: -8px; color: #8a8a8a; pointer-events: none; }
.sd-fb-text {
  width: 100%; box-sizing: border-box; resize: none;
  padding: 10px 12px; border-radius: 8px; border: 1px solid #3a3a3a;
  background: #121212; color: #fff; font: inherit; font-size: 14px; line-height: 1.45;
}
.sd-fb-sel:focus, .sd-fb-text:focus { outline: none; border-color: #6a6a6a; }
.sd-fb-text::placeholder { color: #6a6a6a; }
.sd-fb-status { min-height: 16px; margin: 6px 0 8px; font-size: 12px; color: #f0c674; }
.sd-fb-send {
  width: 100%; padding: 11px; border: 0; border-radius: 8px;
  background: #e8e8e8; color: #111; font: inherit; font-size: 14px; font-weight: 700; cursor: pointer;
}
.sd-fb-send:hover { background: #fff; }
.sd-fb-card.is-sent .sd-fb-send { background: #7ed69a; }

/* 0.13 (Eric, 2026-10-03): the album page's round rate button, once RATED, is
   no longer filled gold — the disc keeps its resting fill and wears a gold
   OUTLINE; the number, the discs and what-you-did go back to light ink. */
.v3-rate-group:has(.v3-rg-rate.is-rated) .v3-rg-disc {
  fill: rgba(232,226,214,0.06); stroke: var(--star, #e8a83c); stroke-width: 2.5px;
}
.v3-rg-rate.is-rated { color: rgba(232,226,214,0.92); --vinyl-empty: rgba(232,226,214,0.28); --star: inherit; }
.v3-rg-rate.is-rated .v3-rg-did,
.v3-rg-rate.is-rated .v3-rg-wrote { color: rgba(232,226,214,0.82); }
/* 0.13 (Eric, same day): the ARTIST page's review cards lose the big score
   number too, like home and the profile — discs only under the cover. The
   album page's cards (no cover there) keep theirs. */
.s-home-v3--artist .v3-rev-card--split .v3-rsp-score .v3-rev-big-n { display: none; }

/* 0.13: the + flow's handoff (sdsPickForReview) — the rate sheet rises OVER the
   still-open quick review list, which then closes underneath with no fade. */
.sd-log-overlay.sd-log-overlay--over { z-index: 220; }
.sds-overlay.sds-overlay--cut { transition: none !important; }

/* ══ WIREFRAME MODE (0.13, Eric 2026-10-03) ═════════════════════════════════
   "for all artist, album, and profile images can we replace them with a
   placeholder text like Artist Image, Album Image, and for profile we have the
   default silhouette, just so we can see the flow easier … toggle this in the
   notifications and we start at wireframes mode and u can switch to image mode".
   `html.sd-wire` (set before first paint in index.html; sdSetViewMode flips
   it, remembered as `sd-view-mode`). Pure CSS, nothing re-rendered: every
   picture in the app is an inline background-image, so it is matched by WHAT
   it shows — the path of the file — and painted over with a placeholder.
   FACES go by the element's class (avatars, faces, profile pics), because a
   profile picture can be any file (the default persona's is a playlist photo).
   The album's colours (applyAlbumColors) still come from the real cover. */
html.sd-wire {
  --wf-album: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%232c2c33'/%3E%3Cpath d='M0 0L100 100M100 0L0 100' stroke='%2338383f' stroke-width='1'/%3E%3Crect x='22' y='38' width='56' height='26' rx='4' fill='%232c2c33'/%3E%3Ctext x='50' y='49' text-anchor='middle' font-family='DM Sans,Arial,sans-serif' font-size='10' font-weight='700' fill='%238b8b96'%3EAlbum%3C/text%3E%3Ctext x='50' y='60' text-anchor='middle' font-family='DM Sans,Arial,sans-serif' font-size='10' font-weight='700' fill='%238b8b96'%3EImage%3C/text%3E%3C/svg%3E");
  --wf-artist: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%23303038'/%3E%3Cpath d='M0 0L100 100M100 0L0 100' stroke='%233c3c44' stroke-width='1'/%3E%3Crect x='22' y='38' width='56' height='26' rx='4' fill='%23303038'/%3E%3Ctext x='50' y='49' text-anchor='middle' font-family='DM Sans,Arial,sans-serif' font-size='10' font-weight='700' fill='%238b8b96'%3EArtist%3C/text%3E%3Ctext x='50' y='60' text-anchor='middle' font-family='DM Sans,Arial,sans-serif' font-size='10' font-weight='700' fill='%238b8b96'%3EImage%3C/text%3E%3C/svg%3E");
  --wf-face: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' fill='%233a3a43'/%3E%3Ccircle cx='50' cy='39' r='17' fill='%23767682'/%3E%3Cpath d='M16 100c0-21 15-34 34-34s34 13 34 34z' fill='%23767682'/%3E%3C/svg%3E");
}
html.sd-wire [style*="images/album-"],
html.sd-wire [style*="images/playlist-"],
html.sd-wire [style*="/images/cover/"] {
  background-image: var(--wf-album) !important;
  background-color: #2c2c33 !important;
  background-size: contain !important; background-position: center !important; background-repeat: no-repeat !important;
}
html.sd-wire [style*="images/artist-"],
html.sd-wire [style*="/images/artist/"] {
  background-image: var(--wf-artist) !important;
  background-color: #303038 !important;
  background-size: contain !important; background-position: center !important; background-repeat: no-repeat !important;
}
/* The ARTIST page's banner is one of their covers (the bento's .v3-album), but
   the slot is the artist's — it reads as Artist Image. */
html.sd-wire .s-home-v3--artist .v3-album[style*="url("] {
  background-image: var(--wf-artist) !important; background-color: #303038 !important;
  background-size: contain !important; background-position: center !important; background-repeat: no-repeat !important;
}
/* Faces: the silhouette. Listed after the album rules and more specific, so a
   profile picture that happens to be an album or playlist file still reads as
   a person. A notification's system row wears the album as its avatar — that
   one stays an album. */
html.sd-wire [style*="images/rp-"],
html.sd-wire [class*="-av"]:not(.ntf-ava--art)[style*="url("],
html.sd-wire [class*="-face"][style*="url("],
html.sd-wire [class*="-pic"][style*="url("] {
  background-image: var(--wf-face) !important;
  background-color: #3a3a43 !important;
  background-size: cover !important; background-position: center !important; background-repeat: no-repeat !important;
}

/* THE SWITCH, at the top of Notifications: Wireframe | Images. */
.sd-viewmode {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin: 4px 0 14px; padding: 10px 12px; border-radius: 12px;
  background: var(--sd-card, rgba(255,255,255,0.05)); box-shadow: 0 0 0 1px var(--sd-line, rgba(255,255,255,0.08));
  font-family: var(--font-main);
}
.sd-viewmode-lbl { font-size: 13px; font-weight: 700; color: var(--sd-ink, #e8e2d6); }
.sd-viewmode-sub { display: block; font-size: 11px; font-weight: 400; color: var(--sd-ink3, rgba(232,226,214,0.45)); margin-top: 1px; }
.sd-viewmode-seg { display: flex; padding: 3px; border-radius: 999px; background: var(--sd-pill, rgba(255,255,255,0.07)); flex-shrink: 0; }
.sd-viewmode-seg button {
  border: 0; background: none; padding: 6px 12px; border-radius: 999px; cursor: pointer;
  font: 700 12px var(--font-main); color: var(--sd-ink3, rgba(232,226,214,0.5));
}
.sd-viewmode-seg button.is-on { background: var(--sd-ink, #e8e2d6); color: var(--sd-bg, #111116); }

/* 0.13 (Eric, 2026-10-03): the wall's ARTISTS tab — round photos, not squares.
   The cell's meta centres under the circle. In wireframe mode the photo reads
   Artist Image, even where a cover stands in for a missing artist photo. */
.wall2-art.wall2-art--artist { border-radius: 50%; }
.wall2-cell--artist .wall2-meta { align-items: center; text-align: center; }
.wall2-cell--artist .wall2-rating { justify-content: center; }
.wall2-art--artist .wall2-rank { border-radius: 999px; }
html.sd-wire .wall2-art--artist[style*="url("] {
  background-image: var(--wf-artist) !important; background-color: #303038 !important;
  background-size: cover !important; background-position: center !important; background-repeat: no-repeat !important;
}

/* 0.13: the "Skip onboarding?" card (sdAskSkipOnboarding) — the feedback
   popup's card, with a quiet second choice under the main button. */
.sd-obskip-note { margin: -4px 0 16px; font-size: 13px; line-height: 1.5; color: #9a9a9a; }
.sd-obskip-stay {
  width: 100%; margin-top: 8px; padding: 11px; border: 1px solid #3a3a3a; border-radius: 8px;
  background: transparent; color: #e8e8e8; font: inherit; font-size: 14px; font-weight: 700; cursor: pointer;
}
.sd-obskip-stay:hover { border-color: #6a6a6a; }

/* 0.13 (Eric, 2026-10-03): the rate & review sheet's score is STACKED — the
   big number centred over the five discs, which are a bit larger (46 → 56).
   The song rows' discs are larger too and tighter (20/4 → 24/1). --rec /
   --gap are what recFillWidth measures the fill in, so the fill follows. */
.sd-log-rate { flex-direction: column; gap: 10px; --rec: 56px; --gap: 4px; }
.sd-log-num { font-size: 44px; min-width: 0; text-align: center; }
.sd-log-song-rate-track { --rec: 24px; --gap: 1px; }

/* 0.13: the song rows' discs take the album control's gesture (see the
   pointer handlers by fillLogSongs): a taller hit area, no scroll while
   sliding, and the same little press on the discs while a finger is down. */
.sd-log-song-rate-track { touch-action: none; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; outline: none; }
.sd-log-song-rate-track::before { content: ''; position: absolute; inset: -10px -2px; }
.sd-log-song-rate-track:focus-visible { box-shadow: 0 0 0 2px rgba(232,168,60,0.5); border-radius: 6px; }
.sd-log-song .sd-rec { transition: transform .12s ease-out; }
.sd-log-song-rate-track.is-rating .sd-rec { transform: scale(1.06); }
.sd-log-song-fill { transition: width .12s ease-out; }
.sd-log-song-rate-track.is-rating .sd-log-song-fill { transition: width .06s linear; }
@media (prefers-reduced-motion: reduce) {
  .sd-log-song .sd-rec, .sd-log-song-fill { transition: none; }
  .sd-log-song-rate-track.is-rating .sd-rec { transform: none; }
}

/* 0.14b (2026-10-09): LISTENED · LISTEN LATER · FAVORITE back on the album
   page, a straight row under the rate circle (`.v3-rev-opts`, screens.js).
   It IS the log sheet's row — same `.sd-log-opts` / `.sd-log-opt` classes, so
   size, spacing and the ON colours come from the sheet's rules above and the
   two can't drift. Only the row's own air is set here. Hidden on the artist
   page, which keeps its corner Favorite pill. */
.v3-rev-opts { margin: 4px 0 14px; }
.s-home-v3--artist .v3-rev-opts { display: none; }
/* 0.20 (2026-10-13): the row was DEAD — Eric: "the listened listen later and
   fav are not working on the album page". `.v3-rate-group`'s negative
   margin-bottom (calc(10px - 17.74%), for the hidden wings) lets its
   positioned box hang over this row and swallow every tap. Stack the row
   above it. */
.v3-rev-opts { position: relative; z-index: 1; }

/* 0.14b (2026-10-12) — THE TRENDING CAROUSEL IS ALBUMS, NOT REVIEWS. Eric: "in
   the discover/trending page, can we take out the review and username from
   the carousel, just leaving the album info and score and like and comment".
   The deck (friendsBentoHtml / fbGo) still builds the whole `.v3-fbr`; on the
   wall the face + name head, the review text, its View more and the when are
   hidden, so the score row with its like and comment is all that is left
   under the album. Scoped to `.s-wall2`, so the card markup and every other
   place it shows stay as they were. */
.s-wall2 .v3-fbr-head,
.s-wall2 .v3-fbr-text,
.s-wall2 .v3-fbr-more,
.s-wall2 .v3-fbr-ago { display: none !important; }

/* 0.14b (2026-10-12) — THE HOME INTRO (sdHomeIntro, end of app.js): the
   belt-drive logo drawing itself over the phone screen between onboarding and
   home. The cover sits on BODY, fixed over the phone's rect (navigate rebuilds
   the phone), so it carries the screen's colour inline and the logo's ink in
   --sd-intro-ink. */
.sd-intro {
  position: fixed; z-index: 9999; overflow: hidden; opacity: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 22px;
  transition: opacity .34s ease;
}
.sd-intro.is-out { opacity: 0 !important; pointer-events: none; }
.sd-intro-logo { width: 34%; max-width: 150px; overflow: visible; }
.sd-intro-draw { fill: none; stroke: var(--sd-intro-ink); stroke-width: 26; stroke-linecap: round; stroke-linejoin: round; }
.sd-intro-holes { fill: var(--sd-intro-ink); opacity: 0; transform-box: view-box; transform-origin: 969.17px 833.22px; }
.sd-intro-word {
  width: 46%; max-width: 190px; aspect-ratio: 1749 / 249; opacity: 0;
  background-color: var(--sd-intro-ink);
  -webkit-mask: url('images/spindeck-wordmark.png') center / contain no-repeat;
  mask: url('images/spindeck-wordmark.png') center / contain no-repeat;
}
/* 0.20 (2026-10-13): the wordmark flying from the intro into the header
   (sdIntroFly). Above the cover; the header's own mark is held hidden until
   the flight lands on it. */
.sd-intro-fly {
  position: fixed; z-index: 10000; pointer-events: none; transform-origin: 0 0;
  -webkit-mask: url('images/spindeck-wordmark.png') center / contain no-repeat;
  mask: url('images/spindeck-wordmark.png') center / contain no-repeat;
}
.v3-header-logo.sd-intro-hold { visibility: hidden; }


/* ═══ SETTINGS, MADE REAL (2026-10-12) ═══════════════════════════════════
   Eric: "make most of the settings menu functional". */

/* REDUCE MOTION — Settings › Appearance (html.sd-reduce-motion, app.js
   sdApplyMotion). Every CSS animation and transition finishes at once, on its
   end state; JS-driven motion asks sdReducedMotion(), which reads the class. */
html.sd-reduce-motion *, html.sd-reduce-motion *::before, html.sd-reduce-motion *::after {
  animation-duration: .001ms !important;
  animation-delay: 0s !important;
  animation-iteration-count: 1 !important;
  transition-duration: .001ms !important;
  transition-delay: 0s !important;
  scroll-behavior: auto !important;
}

/* A service the prototype cannot connect yet: dimmed, still tappable (the
   sheet says why). */
.set-row--na .set-row-label, .set-row--na .set-svc-dot { opacity: .5; }

/* THE CONNECT SHEET (#sdsvc) — the playlist delete confirm's material
   (.pldel-*), dark in both themes, with a field and a progress line. */
.svc-hd { display: flex; align-items: center; gap: 9px; margin-top: 6px; }
.svc-hd .pldel-title { margin-top: 0; }
.svc-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.svc-sheet .svc-sub { font-family: var(--font-main); font-size: 11.5px; line-height: 1.45; }
.svc-input {
  display: block; width: 100%; box-sizing: border-box;
  margin: 0 0 8px; padding: 12px 13px;
  border: 1px solid rgba(232,226,214,0.14); border-radius: 12px;
  background: rgba(232,226,214,0.06); color: #e8e2d6;
  font-family: var(--font-main); font-size: 14px; outline: none;
}
.svc-input:focus { border-color: rgba(232,226,214,0.4); }
.svc-input[hidden] { display: none; }
.svc-msg {
  min-height: 14px; margin: 0 0 10px;
  font-family: var(--font-mono); font-size: 9.5px; line-height: 1.5;
  color: rgba(232,226,214,0.6);
}
.svc-msg.is-err { color: #ff8a80; }
.svc-go { background: #e8e2d6; color: #111116; }
.svc-go:hover { filter: brightness(1.06); }
.svc-go:disabled { opacity: .45; cursor: default; filter: none; }
.svc-go--off { background: #e85656; color: #fff; }
.svc-go[hidden] { display: none; }
/* No action (an unavailable service): Cancel takes the row, reading as Close. */
.svc-sheet .pldel-btns:has(.svc-go[hidden]) { grid-template-columns: 1fr; }

/* Four tabs now (Fav songs · Fav albums · Listened · Listen later, 2026-10-13):
   a little less tracking so each label fits its quarter. */
.prof-picks-tab { letter-spacing: .03em; padding: 7px 2px; }

/* 0.20 (2026-10-13): COMMENTS ON SONGS IN THE REVIEW PAGE. The bubble took the
   per-song number's place left of the discs (`.sd-log-song-cmt`, fillLogSongs);
   the thread opens in `.sd-log-song-cmts` under the row and wears the review
   cards' comment styles as-is. Count reads like the tracklist's. */
.sd-log-song-cmt {
  /* FIXED WIDTH, icon pinned left (Eric 2026-10-13: "the icons are always
     aligned despite how many comment numbers") — room for a three-digit count,
     so 0, 8 and 120 all leave the bubble and the discs in the same place. */
  flex: 0 0 36px; width: 36px; height: 26px; padding: 0; margin-right: -2px;
  display: inline-flex; align-items: center; justify-content: flex-start; gap: 3px;
  background: none; border: none; cursor: pointer;
  color: rgba(232,226,214,0.4); font: 600 10.5px var(--font-main);
  font-variant-numeric: tabular-nums; transition: color .15s;
}
.sd-log-song-cmt svg { width: 14px; height: 14px; }
.sd-log-song-cmt:hover { color: rgba(232,226,214,0.75); }
.sd-log-song-cmt.is-open { color: var(--star, #e8a83c); }
.sd-log-song-cmt-n:empty { display: none; }
.sd-log-song-cmts { padding: 4px 0 12px; border-top: 1px solid rgba(232,226,214,0.06); }
.sd-log-song-cmts[hidden] { display: none; }
.s-home-v3--light .sd-log-song-cmt { color: rgba(26,18,8,0.45); }
.s-home-v3--light .sd-log-song-cmt.is-open { color: var(--star, #e8a83c); }
/* "See more comments" ends a review-page song thread in place of the dots. */
.v3-cmt-seemore {
  display: block; width: 100%; margin-top: 6px; padding: 9px 0;
  background: rgba(232,226,214,0.06); border: 1px solid rgba(232,226,214,0.1); border-radius: 10px;
  color: rgba(232,226,214,0.75); font: 600 12px var(--font-main); cursor: pointer;
  transition: background .15s, color .15s;
}
.v3-cmt-seemore:hover { background: rgba(232,226,214,0.1); color: #e8e2d6; }
.s-home-v3--light .v3-cmt-seemore { background: rgba(26,18,8,0.05); border-color: rgba(26,18,8,0.1); color: rgba(26,18,8,0.7); }
/* 0.20 (2026-10-13): no "COMMENTS" heading over the comment box (Eric: "get
   rid of that text 'comments' in all caps above the comment box for all pages
   review page song review page"). Hidden, not removed — cmtRender still
   repaints the counts inside, harmlessly. */
.rvp-cmts-hd, .sd-log-cmts-hd { display: none; }

/* 0.20 (2026-10-13): THE KEYBOARD COMPOSER (sdKbComposer, end of app.js) —
   the bar that rides the keyboard while you write a review or a comment, and
   the scrim above it that closes it on a tap. Body-level and fixed; `top` is
   set from visualViewport. 16px type so iOS never zooms. */
.sd-kb-scrim {
  position: fixed; left: 0; right: 0; top: 0; height: 100%; z-index: 10001;
  background: rgba(0,0,0,.38); opacity: 0; pointer-events: none;
  transition: opacity .16s ease; touch-action: none;
}
.sd-kb-scrim.is-open { opacity: 1; pointer-events: auto; }
.sd-kb-bar {
  position: fixed; left: 0; right: 0; top: 100%; z-index: 10002;
  display: none; align-items: flex-end; gap: 8px; box-sizing: border-box;
  padding: 8px 10px; background: #18181e;
  border-top: 1px solid rgba(232,226,214,0.1);
}
.sd-kb-bar.is-open { display: flex; }
.sd-kb-input {
  flex: 1; min-width: 0; box-sizing: border-box; resize: none; margin: 0;
  max-height: 132px; padding: 9px 12px; border-radius: 18px;
  border: 1px solid rgba(232,226,214,0.14); background: rgba(232,226,214,0.06);
  color: #e8e2d6; font: 400 16px/1.35 var(--font-main, 'DM Sans', sans-serif);
  outline: none;
}
.sd-kb-input::placeholder { color: rgba(232,226,214,0.4); }
.sd-kb-act {
  flex-shrink: 0; height: 38px; padding: 0 14px; border-radius: 19px; border: none;
  background: rgba(232,226,214,0.12); color: #e8e2d6;
  font: 700 14px var(--font-main, 'DM Sans', sans-serif); cursor: pointer;
}
.sd-kb-act.is-post { background: var(--star, #e8a83c); color: #1a1208; }
.sd-kb-act:disabled { opacity: .4; }
.sd-kb-bar.is-light { background: #f7f4ee; border-top-color: rgba(26,18,8,0.1); }
.sd-kb-bar.is-light .sd-kb-input { background: rgba(26,18,8,0.05); border-color: rgba(26,18,8,0.14); color: #1a1208; }
.sd-kb-bar.is-light .sd-kb-input::placeholder { color: rgba(26,18,8,0.4); }
.sd-kb-bar.is-light .sd-kb-act:not(.is-post) { background: rgba(26,18,8,0.08); color: #1a1208; }

/* 0.20 (2026-10-13): LISTENED goes BLUE when on (Eric: "get the listened icon
   on the review and album page to turn blue") — the bell's #3b82f6. Covers the
   review sheet's row and the album page's copy of it (same classes), icon and
   label, both themes. Listen later stays gold, Favorite pink. */
.sd-log-opt[data-k="listened"].on .sd-log-opt-ico,
.sd-log-opt[data-k="listened"].on .sd-log-opt-lbl,
.s-home-v3--light .sd-log-opt[data-k="listened"].on .sd-log-opt-ico,
.s-home-v3--light .sd-log-opt[data-k="listened"].on .sd-log-opt-lbl { color: #3b82f6; }

/* 0.20 (2026-10-13): the review composer is the review box itself, lifted
   (dressAsReview copies its look inline). No strip behind it; the Done pill
   sits on the box's top-right corner, 10px of air above the keyboard. */
.sd-kb-bar.is-review, .sd-kb-bar.is-review.is-light {
  background: none; border-top: none; padding: 0 0 10px;
  flex-direction: column; align-items: flex-end; gap: 8px;
}
.sd-kb-bar.is-review .sd-kb-act { order: -1; }
.sd-kb-bar.is-review .sd-kb-input { flex: none; width: 100%; outline: none; }
.sd-kb-bar.is-review .sd-kb-input::placeholder { color: var(--kb-ph, rgba(232,226,214,0.4)); }
.sd-kb-bar.is-review .sd-kb-act {
  background-color: var(--kb-under, #16161c);
  background-image: linear-gradient(rgba(232,226,214,0.12), rgba(232,226,214,0.12));
}
.sd-kb-bar.is-review.is-light .sd-kb-act { background-image: linear-gradient(rgba(26,18,8,0.08), rgba(26,18,8,0.08)); }

/* 0.20 (2026-10-13): USE AS AN APP — the top row of the feedback sheet and
   the steps card it opens (sdInstallOpen, app.js). Same sheet material. */
.sd-fb-app {
  display: flex; align-items: center; gap: 11px; width: 100%; margin: 0 0 16px;
  padding: 11px 12px; border: 1px solid #3a3a3a; border-radius: 10px;
  background: rgba(255,255,255,0.04); color: #e8e8e8; font: inherit; text-align: left; cursor: pointer;
}
.sd-fb-app:hover { border-color: #6a6a6a; }
.sd-fb-app > svg:first-child { width: 22px; height: 22px; flex-shrink: 0; }
.sd-fb-app span { flex: 1; display: flex; flex-direction: column; gap: 2px; }
.sd-fb-app b { font-size: 14px; font-weight: 700; }
.sd-fb-app i { font-style: normal; font-size: 12px; color: #9a9a9a; }
.sd-fb-app-go { width: 16px; height: 16px; flex-shrink: 0; color: #9a9a9a; }
.sd-inst-steps { margin: 0 0 14px; padding: 0 0 0 20px; color: #e8e8e8; font-size: 14px; line-height: 1.5; }
.sd-inst-steps li { margin-bottom: 10px; padding-left: 4px; }
.sd-inst-steps b { font-weight: 700; }
.sd-inst-ico { display: inline-block; width: 18px; height: 18px; vertical-align: -4px; margin-left: 2px; color: #3b82f6; }

/* 0.20 (2026-10-13): THE NAV BUBBLE — Eric: "the page that its on has a white
   bubble that travels across the nav bar to get to the page u are looking at,
   its what instagram has". One capsule behind the icons (`.v3-nav-bubble`,
   first child of .v3-nav-items); sdNavBubble (app.js) parks it under the
   active tab — or Search while the search is up — and carries it over from
   the tab you left when a new screen's nav mounts. */
.v3-nav-bubble {
  position: absolute; left: 0; top: 50%; z-index: 0;
  width: 52px; height: 38px; margin-top: -19px; border-radius: 999px;
  background: rgba(255,255,255,0.2);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.12);
  pointer-events: none; opacity: 0;
  transition: transform .42s cubic-bezier(.3, 1.25, .45, 1), opacity .2s ease;
}
.v3-nav-bubble.is-on { opacity: 1; }
.v3-nav-bubble.no-anim { transition: none; }
.v3-nav-items > .v3-nav-item { position: relative; z-index: 1; }
.s-home-v3--light .v3-nav-bubble { background: rgba(26,18,8,0.09); box-shadow: inset 0 0 0 1px rgba(26,18,8,0.06); }
@media (prefers-reduced-motion: reduce) { .v3-nav-bubble { transition: opacity .2s ease; } }

/* 0.20 (2026-10-13): THE REVIEW SHEET IS ONE HEIGHT — Eric: "that popup to
   always be the same height like 70% of the screen height from the bottom".
   Was content-sized up to 90%; now 70% whatever the review, the body scrolls. */
.v3-rsh { height: 70%; max-height: 70%; }

/* 0.20 (2026-10-13): THE "READY" POP — the moment a pull arms, the record
   bumps up and throws a ring. Stands in for a haptic: iOS gives a page no
   tick mid-drag (tested on Eric's phone, haptic-test.html rows 8-9). */
.sd-ptr.is-armed .sd-ptr-disc { animation: sdPtrPop .34s cubic-bezier(.3, 1.6, .5, 1); }
.sd-ptr-disc::after {
  content: ''; position: absolute; inset: -2px; border-radius: 50%;
  border: 2px solid currentColor; opacity: 0; pointer-events: none;
}
.sd-ptr.is-armed .sd-ptr-disc::after { animation: sdPtrRing .5s ease-out; }
@keyframes sdPtrPop { 0% { transform: scale(1); } 45% { transform: scale(1.3); } 100% { transform: scale(1); } }
@keyframes sdPtrRing { 0% { opacity: .7; transform: scale(1); } 100% { opacity: 0; transform: scale(1.9); } }
@media (prefers-reduced-motion: reduce) {
  .sd-ptr.is-armed .sd-ptr-disc, .sd-ptr.is-armed .sd-ptr-disc::after { animation: none; }
}

/* 0.20 (2026-10-13): NO SONG-ROW HOVER ON A PHONE — Eric: "on the album page
   the hover for the songs is ... glitching out when im not even touching it".
   iOS fires :hover on whatever a scrolling finger crosses and leaves it stuck,
   so the tracklist flickered. Touch screens get no hover; a mouse still does. */
@media (hover: none) {
  .v3-song-row:hover, .s-home-v3--light .v3-song-row:hover,
  .v3-song-rate--tap:hover, .v3-songs-more:hover { background: none; }
}

/* ── ALBUM PAGE, LIGHT (Eric, 2026-10-14) ───────────────────────────────
   The album page goes cream in the light theme now (see the `--light.--album`
   bg rule by the artist page). The artist page's re-inks are scoped `--album`
   so they cover the review cards and comments; these are the album-only bits
   that were written as light ink for the old dark flood. */
.s-home-v3--light.s-home-v3--album .v3-blue-album { color: #1a1208; }
.s-home-v3--light.s-home-v3--album .v3-blue-artist { color: rgba(26,18,8,0.85); }
.s-home-v3--light.s-home-v3--album .v3-blue-date { color: rgba(26,18,8,0.5); }
.s-home-v3--light.s-home-v3--album .v3-cred-lbl { color: rgba(26,18,8,0.4); }
.s-home-v3--light.s-home-v3--album .v3-cred-names { color: rgba(26,18,8,0.65); }
/* The round rate button and its lobes */
.s-home-v3--light.s-home-v3--album .v3-rate-group { color: rgba(26,18,8,0.75); }
.s-home-v3--light.s-home-v3--album .v3-rg-wing,
.s-home-v3--light.s-home-v3--album .v3-rg-disc { fill: rgba(26,18,8,0.04); stroke: rgba(26,18,8,0.16); }
.s-home-v3--light.s-home-v3--album .v3-rate-group:has(.v3-rg-rate.is-rated) .v3-rg-disc { fill: rgba(26,18,8,0.04); stroke: var(--star, #e8a83c); }
.s-home-v3--light.s-home-v3--album .v3-rg-word,
.s-home-v3--light.s-home-v3--album .v3-rg-word--sm { color: #1a1208; }
.s-home-v3--light.s-home-v3--album .v3-rg-blank { fill: rgba(26,18,8,0.22); }
.s-home-v3--light.s-home-v3--album .v3-rg-rate.is-rated { color: #1a1208; --vinyl-empty: rgba(26,18,8,0.25); }
.s-home-v3--light.s-home-v3--album .v3-rg-rate .v3-rg-did,
.s-home-v3--light.s-home-v3--album .v3-rg-rate .v3-rg-wrote { color: rgba(26,18,8,0.7); }
/* Community review + histogram */
.s-home-v3--light.s-home-v3--album .v3-rev-lbl,
.s-home-v3--light.s-home-v3--album .v3-rev-score-count,
.s-home-v3--light.s-home-v3--album .v3-rev-hist-axis { color: rgba(26,18,8,0.45); }
.s-home-v3--light.s-home-v3--album .v3-rev-score-n { color: #1a1208; }
.s-home-v3--light.s-home-v3--album { --vinyl-empty: rgba(26,18,8,0.25); }
/* Tracklist */
.s-home-v3--light.s-home-v3--album .v3-song-row { color: #1a1208; border-top-color: rgba(26,18,8,0.08); }
.s-home-v3--light.s-home-v3--album .v3-song-row:hover { background: rgba(26,18,8,0.04); }
.s-home-v3--light.s-home-v3--album .v3-song-num,
.s-home-v3--light.s-home-v3--album .v3-song-dur,
.s-home-v3--light.s-home-v3--album .v3-song-cmt-n { color: rgba(26,18,8,0.45); }
.s-home-v3--light.s-home-v3--album .v3-song-head,
.s-home-v3--light.s-home-v3--album .v3-song-head * { color: rgba(26,18,8,0.4); }
.s-home-v3--light.s-home-v3--album .v3-songs-more { border-color: rgba(26,18,8,0.08); color: #b06a2a; }
.s-home-v3--light.s-home-v3--album .v3-rev-count { color: rgba(26,18,8,0.45); }
