/* Vibe Check — RECIPIENT PLAYERS (Listen Later + Listen Live). Loaded on top of tokens.css +
   sendbox.css so the player card is IDENTICAL to the studio's: same .sb shell (max-width, radius,
   padding, near-opaque surface), same .sb-wall/.sb-scrim backdrop, same type tokens. Everything
   below is player-specific chrome. Prefix: .vp- (vibe player). Both players load this same file
   (the live player pulls it from /l/_assets/ cross-service, Caddy-routed) so they never drift. */

/* (bf) parity with the studio: lock the app shell so overscroll can't pull-to-refresh / reload the
   player mid-listen (this file loads ONLY on the two player pages, so the lock is player-scoped).
   The stage becomes the ONLY scroller, contained — a card taller than a small viewport stays
   reachable (.sb-stage is a plain block with min-height, so nothing gets clipped unreachable). */
html, body{ height:100%; overflow:hidden; overscroll-behavior:none; }
/* the players' wallpaper, decided in CSS before first paint (overrides sendbox.css's studio
   default): desktop/mobile listening-room crops for the frosted wells + the mobile object slot */
:root{ --wall-img:url("/l/_assets/listen-bg-desktop.jpg"); }
@media (max-width:720px){
  :root{ --wall-img:url("/l/_assets/listen-bg-mobile.jpg"); }
  .sb-wall img{ object-position:50% 12%; }
}

.vp-stage{ height:100%; overflow-y:auto; overscroll-behavior:contain; }

/* the card sits TOP-LEFT over the backdrop, exactly like the homepage/studio (.sb-stage places it) */
.vp-card{ text-align:left; text-transform:lowercase; }   /* OUR copy lowercased; user content exempted below */
/* user content stays as-typed: track title, message, and the sender's name (just the bold name — the
   surrounding "sent you …" copy still lowercases). Covers both the single-track and live players. */
.vp-name, .vp-msg, .vp-fromTxt b, .vp-ava, #sourceName.typed{ text-transform:none; }   /* user content stays as typed (incl. a TYPED live track title); the generic "x's studio" label lowercases with the rest */

/* ============ LISTEN LIVE skin deltas — on the SAME shared .st-card/.st-win/.st-wordmark as Listen Later ============
   The live player adds: a tagline under the wordmark, a frosted .st-win "live well" with the red Live badge, a small
   centred-glyph mute hero at the LEFT of a row beside the source name + sub, then the muted status line. */
.vp-tagline{ text-align:center; font-size:13.5px; color:rgba(255,255,255,.6); margin-top:7px; }   /* "Live, listen-only lossless audio" */
/* height: shared component */
.live-well{ border-radius:14px; clip-path:inset(0 round 14px); margin-top:18px; }    /* the well IS .st-win; this only adds size + clip */
.live-well canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }
.vp-liverow{ display:flex; align-items:center; gap:14px; margin-top:20px; text-align:left; }        /* left edge shared by mute + source name + sub */
.vp-hero.vp-hero--live{ width:52px; height:52px; box-shadow:0 12px 26px rgba(63,111,214,.5); }
.vp-hero.vp-hero--live svg{ width:22px; height:22px; }
.vp-livemeta{ min-width:0; }
.vp-livename{ display:flex; align-items:center; gap:8px; font-weight:700; font-size:17px; letter-spacing:-.2px; color:#fff; min-width:0; }
.vp-livename #sourceName{ min-width:0; overflow-wrap:anywhere; word-break:break-word; }   /* long typed take names wrap down, never sideways */
.vp-livename .vp-ava{ width:22px; height:22px; font-size:11px; }
.vp-livesub{ font-size:13px; color:rgba(255,255,255,.6); margin-top:3px; }

/* ---- Listen zone: title + quality + note ------------------------------------------------- */
.vp-title{ font-weight:700; font-size:21px; letter-spacing:-.3px; line-height:1.25; color:#fff; margin-top:20px; }
.vp-specrow{ display:flex; align-items:center; gap:10px; margin-top:5px; }
.vp-spec{ font-size:13.5px; color:rgba(255,255,255,.62); }
.vp-noteWrap{ position:relative; margin-left:auto; display:inline-flex; }
.vp-noteBtn{ position:relative; width:30px; height:30px; border-radius:50%; border:none; background:rgba(255,255,255,.1);
  cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; }
.vp-noteBtn svg{ display:block; }
.vp-noteBtn.pulse{ animation:vp-notepulse 1.5s ease-out 3; }
.vp-noteDot{ position:absolute; top:0; right:0; width:8px; height:8px; border-radius:50%; background:var(--blue); border:1.5px solid #0b0a0c; }
.vp-pop{ position:absolute; bottom:calc(100% + 12px); right:-4px; width:258px; max-width:78vw; background:#1C1C1E;
  border:.5px solid rgba(255,255,255,.2); border-radius:13px; box-shadow:0 16px 40px rgba(0,0,0,.55); padding:13px 15px; z-index:30; }
.vp-pop .vp-popText{ font-size:13px; line-height:1.5; color:rgba(255,255,255,.6); padding-right:16px; }
.vp-pop .vp-popText b{ font-weight:700; color:#fff; }
.vp-pop .vp-popClose{ position:absolute; top:9px; right:9px; width:18px; height:18px; border:none; background:none;
  cursor:pointer; padding:0; display:flex; align-items:center; justify-content:center; }
.vp-pop .vp-popArrow{ position:absolute; bottom:-7px; right:13px; width:12px; height:12px; background:#1C1C1E;
  border-right:.5px solid rgba(255,255,255,.2); border-bottom:.5px solid rgba(255,255,255,.2); transform:rotate(45deg); }

/* ---- live "Live from …" badge, bottom-left inside the .st-win live well ------------------- */
.vp-liveBadge{ position:absolute; left:13px; bottom:12px; display:flex; align-items:center; gap:7px;
  background:rgba(0,0,0,.55); padding:6px 13px; border-radius:999px; }
.vp-liveBadge .dot{ width:8px; height:8px; border-radius:50%; background:var(--red); animation:vp-pdot 1.4s ease-in-out infinite; }
.vp-liveBadge .txt{ font-weight:600; font-size:12.5px; color:#fff; }

/* ---- hero play / mute button ------------------------------------------------------------- */
.vp-playRow{ position:relative; display:flex; justify-content:center; margin-top:22px; }
.vp-hero{ flex:none; width:88px; height:88px; border-radius:50%; border:none; background:var(--blue);
  box-shadow:0 18px 38px rgba(63,111,214,.6); cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; }
.vp-hero svg{ width:34px; height:34px; display:block; }
.vp-hero:active{ transform:scale(.97); }
/* download: play's little sibling — same blue circle family, right of the centred hero (absolute
   so the hero never shifts; the same placement trick as the live note button) */
.vp-dl{ position:absolute; top:50%; right:6px; transform:translateY(-50%); width:44px; height:44px;
  border-radius:50%; border:none; background:var(--blue); box-shadow:0 8px 18px rgba(63,111,214,.45);
  cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; }
.vp-dl:active{ transform:translateY(-50%) scale(.96); }
.vp-dl svg{ display:block; }
/* the note button can sit to the right of the hero (live playing) */
.vp-playRow .vp-noteWrap{ position:absolute; top:50%; right:0; transform:translateY(-50%); margin:0; }

/* ---- scrubber (Listen Later only) -------------------------------------------------------- */
.vp-scrub{ margin-top:20px; }
.vp-track{ height:6px; border-radius:3px; background:rgba(255,255,255,.2); position:relative; cursor:pointer; }
.vp-buf{ position:absolute; left:0; top:0; bottom:0; width:0; background:rgba(255,255,255,.16); border-radius:3px; }
.vp-fill{ position:absolute; left:0; top:0; bottom:0; width:0; background:var(--blue); border-radius:3px; }
.vp-knob{ position:absolute; left:0; top:50%; transform:translate(-8px,-50%); width:16px; height:16px; border-radius:50%;
  background:#fff; box-shadow:0 1px 5px rgba(0,0,0,.4); pointer-events:none; }
.vp-times{ display:flex; justify-content:space-between; margin-top:11px; }
.vp-times span{ font-family:var(--mono); font-size:12.5px; color:rgba(255,255,255,.65); }

/* ---- expiry + cue ------------------------------------------------------------------------ */
.vp-expiry{ display:flex; align-items:center; justify-content:center; gap:7px; margin-top:18px; }
.vp-expiry span{ font-size:11px; font-weight:600; color:var(--red); }
.vp-cue{ text-align:center; font-size:12.5px; color:rgba(255,255,255,.55); margin-top:9px; }
.vp-status{ text-align:center; font-size:12.5px; color:rgba(255,255,255,.55); margin-top:18px; }

/* ---- footer: avatar (shared, used in the live source row + Later from-line) + by ---------- */
.vp-foot{ margin-top:20px; padding-top:18px; border-top:1px solid rgba(255,255,255,.12); }
.vp-ava{ flex:none; width:24px; height:24px; border-radius:50%; background:var(--blue) center/cover no-repeat; color:#fff;
  display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:600; }
.vp-by{ text-align:center; font-size:11px; margin-top:13px; color:rgba(255,255,255,.4); }

/* ---- Listen Live: waiting ---------------------------------------------------------------- */
.vp-wait{ text-align:center; }
.vp-ring{ position:relative; background:rgba(255,255,255,.07); border-radius:14px; height:120px; display:flex;
  align-items:center; justify-content:center; overflow:hidden; margin-top:18px; }
.vp-ring .line{ position:absolute; left:0; right:0; top:50%; height:2px; background:rgba(255,69,58,.3); }
.vp-ring .pulse{ position:relative; width:54px; height:54px; display:flex; align-items:center; justify-content:center; }
.vp-ring .pulse span.r{ position:absolute; inset:0; border-radius:50%; background:rgba(255,69,58,.22); animation:vp-ring 1.8s ease-out infinite; }
.vp-ring .pulse span.r2{ animation-delay:.9s; }
.vp-ring .pulse span.core{ width:14px; height:14px; border-radius:50%; background:var(--red); position:relative; }
.vp-waitTitle{ font-weight:800; font-size:26px; letter-spacing:-.5px; color:#fff; margin-top:20px; }
.vp-waitSub{ font-size:14px; color:rgba(255,255,255,.7); margin-top:8px; line-height:1.45; text-wrap:balance; }
.vp-conn{ display:flex; align-items:center; justify-content:center; gap:7px; margin-top:18px; }
.vp-conn .dot{ width:8px; height:8px; border-radius:50%; background:var(--red); animation:vp-pdot 1.4s ease-in-out infinite; }
.vp-conn .txt{ font-size:12px; font-weight:600; color:rgba(255,255,255,.85); }

/* ---- generic states: glyph + title + text + "What is Vibe Check?" ------------------------- */
.vp-state{ text-align:center; }
.vp-glyph{ width:60px; height:60px; border-radius:50%; display:flex; align-items:center; justify-content:center; margin:30px auto 0; }
.vp-glyph.red{ background:rgba(255,69,58,.16); }
.vp-glyph.neutral{ background:rgba(255,255,255,.1); }
.vp-stTitle{ font-weight:800; font-size:23px; letter-spacing:-.5px; color:#fff; margin-top:22px; }
.vp-stText{ font-size:14px; color:rgba(255,255,255,.68); margin-top:10px; line-height:1.5; }
.vp-what{ margin-top:26px; padding-top:16px; border-top:1px solid rgba(255,255,255,.1); }
.vp-whatLink{ font-size:13px; color:#7fa0e8; font-weight:600; background:none; border:none; cursor:pointer; padding:0; font-family:inherit; }

/* ---- PIN gate ---------------------------------------------------------------------------- */
.vp-pin{ text-align:center; }
.vp-pinIcon{ width:56px; height:56px; border-radius:50%; background:rgba(255,255,255,.1); display:flex;
  align-items:center; justify-content:center; margin:26px auto 0; }
.vp-pinTitle{ font-weight:800; font-size:21px; letter-spacing:-.4px; color:#fff; margin-top:18px; }
.vp-pinSub{ font-size:13.5px; color:rgba(255,255,255,.65); margin-top:8px; line-height:1.45; text-wrap:balance; }
.vp-pinrow{ display:flex; gap:10px; justify-content:center; margin-top:24px; }
.vp-pinbox{ width:50px; height:60px; border-radius:13px; border:1.5px solid rgba(255,255,255,.28); background:rgba(255,255,255,.07);
  color:#fff; outline:none; text-align:center; font-family:var(--mono); font-size:26px; font-weight:500; padding:0; }
.vp-pinbox:focus{ border:2px solid var(--blue); background:rgba(63,111,214,.18); }
.vp-pinbox.bad{ border-color:var(--red); background:rgba(255,69,58,.10); }
.vp-btn{ width:100%; padding:15px; border-radius:13px; border:none; background:var(--blue); color:#fff; cursor:pointer;
  font-family:inherit; font-weight:700; font-size:15px; margin-top:18px; box-shadow:0 8px 20px rgba(63,111,214,.4); }
.vp-btn:active{ transform:scale(.99); }

/* ---- paste-a-live-link landing ----------------------------------------------------------- */
.vp-paste{ text-align:center; }
.vp-pasteTitle{ font-weight:800; font-size:22px; letter-spacing:-.4px; color:#fff; margin-top:30px; line-height:1.2; }
.vp-pasteSub{ font-size:13.5px; color:rgba(255,255,255,.65); margin-top:8px; line-height:1.45; }
.vp-pasteInput{ width:100%; font-family:inherit; font-size:14px; text-align:center; padding:14px 15px;
  border:1.5px solid rgba(255,255,255,.22); border-radius:13px; outline:none; color:#fff; background:rgba(255,255,255,.07); margin-top:22px; }
.vp-pasteInput::placeholder{ color:rgba(255,255,255,.4); }
.vp-pasteInput:focus{ border-color:var(--blue); box-shadow:0 0 0 3px rgba(59,116,240,.18); }
.vp-pasteFoot{ font-size:12px; color:rgba(255,255,255,.5); margin-top:18px; padding-top:16px; border-top:1px solid rgba(255,255,255,.1); }

/* ---- explainer ("What is Vibe Check?") --------------------------------------------------- */
.vp-ex{ }
.vp-exTitle{ text-align:center; font-weight:800; font-size:23px; letter-spacing:-.5px; color:#fff; margin-top:20px; line-height:1.15; text-wrap:balance; }
.vp-exSub{ text-align:center; font-size:13.5px; color:rgba(255,255,255,.65); margin-top:8px; line-height:1.45; text-wrap:balance; }
.vp-checks{ margin-top:20px; display:flex; flex-direction:column; gap:11px; }
.vp-check{ display:flex; align-items:center; gap:11px; }
.vp-check .ic{ flex:none; width:22px; height:22px; border-radius:50%; background:rgba(59,116,240,.16); display:flex; align-items:center; justify-content:center; }
.vp-check .lbl{ font-size:13.5px; color:#fff; font-weight:500; }
.vp-exShare{ margin-top:20px; padding-top:16px; border-top:1px solid rgba(255,255,255,.1); }
.vp-exShareTitle{ font-weight:700; font-size:15px; color:#fff; text-align:center; letter-spacing:-.2px; }
.vp-exShareSub{ text-align:center; font-size:12.5px; color:rgba(255,255,255,.6); margin-top:6px; line-height:1.45; text-wrap:balance; }
.vp-points{ margin-top:14px; display:flex; flex-direction:column; gap:10px; }
.vp-point{ display:flex; align-items:center; gap:11px; }
.vp-point .ic{ flex:none; width:30px; height:30px; border-radius:9px; background:rgba(255,255,255,.08); display:flex; align-items:center; justify-content:center; }
.vp-point .lbl{ font-size:13px; color:rgba(255,255,255,.85); }

/* ---- animations -------------------------------------------------------------------------- */
@keyframes vp-ring{ 0%{transform:scale(.75);opacity:.7} 80%{transform:scale(1.7);opacity:0} 100%{opacity:0} }
@keyframes vp-pdot{ 0%,100%{opacity:1} 50%{opacity:.3} }
@keyframes vp-breathe{ 0%,100%{opacity:1} 50%{opacity:.74} }
@keyframes vp-notepulse{ 0%{box-shadow:0 0 0 0 rgba(59,116,240,.5)} 70%{box-shadow:0 0 0 9px rgba(59,116,240,0)} 100%{box-shadow:0 0 0 0 rgba(59,116,240,0)} }

/* the live waveform "breathes" while playing — gated on motion preference */
.vp-well.live .vp-waveBreathe{ animation:vp-breathe 2.6s ease-in-out infinite; }

@media (prefers-reduced-motion: reduce){
  .vp-noteBtn.pulse,
  .vp-liveBadge .dot,
  .vp-conn .dot,
  .vp-ring .pulse span.r,
  .vp-well.live .vp-waveBreathe{ animation:none !important; }
}

/* ---- mobile compaction lives in ONE block at the END of this file: an earlier version of it sat
   here and silently LOST the cascade to the base rules and duplicate @media blocks written below it
   (same specificity, later order wins) — the well rendered 128 instead of 104, the hero 68 instead
   of 64, and the card+drawer overflowed short phone viewports. Keep all ≤480px rules down there. */

/* ============ SINGLE-TRACK PLAYER — Surfaces layout (owner + recipient skins) ============ */
.vp-head{ display:flex; align-items:center; justify-content:space-between; }
.vp-head.recip{ justify-content:center; }
.vp-wm{ height:21px; width:auto; display:block; }
.vp-ownerBtns{ display:flex; gap:8px; }
.vp-obtn{ display:inline-flex; align-items:center; gap:5px; border:1px solid rgba(255,255,255,.18); background:rgba(255,255,255,.07);
  border-radius:9px; padding:6px 11px; cursor:pointer; color:#fff; font-family:inherit; font-weight:600; font-size:12.5px; }
.vp-obtn:hover{ background:rgba(255,255,255,.12); }

/* grey-box from-line text: Inter 400 14px; the sender name Inter 600 (avatar reuses the base .vp-ava, 24px) */
.vp-fromTxt{ font-size:14px; color:rgba(255,255,255,.78); }
.vp-fromTxt b{ color:#fff; font-weight:600; }

/* the well is a frosted window onto the wallpaper + the scrubber */
.vp-well{ position:relative; height:150px; border-radius:14px; margin-top:16px; padding:0 13px; overflow:hidden; isolation:isolate;
  background:transparent; cursor:pointer; }
.vp-well::before{ content:""; position:absolute; inset:0; z-index:-2; background-image:var(--wall-img); background-size:cover; background-position:center; filter:blur(9px); transform:scale(1.2); }
.vp-well::after{ content:""; position:absolute; inset:0; z-index:-1; background:rgba(0,0,0,.70); }
.vp-playhead{ position:absolute; top:9px; bottom:9px; width:2px; background:#fff; border-radius:2px; box-shadow:0 0 8px rgba(0,0,0,.6); }
.vp-tL, .vp-tR{ position:absolute; bottom:9px; font-family:var(--mono),'Roboto Mono',monospace; font-size:11px; color:rgba(255,255,255,.78); background:rgba(0,0,0,.62); padding:2px 7px; border-radius:6px; }
.vp-tL{ left:13px; } .vp-tR{ right:13px; }

.vp-playRow{ position:relative; display:flex; align-items:center; justify-content:center; gap:14px; margin-top:20px; }   /* well→play 20 */
/* 88px, matching the studio player's hero — the size the spec calls for and the one that makes play
   DOMINATE its row. This rule has said 76px since the recipient players were first rebuilt, so the
   hero never shrank; it was 12px under the studio's all along, and only started reading small when
   auto-play moved inline and gave the cluster around it more to compete with.
   min-width/min-height guard the size against any future flex context: `flex:none` fixes the basis
   but a squeezed row could still compress the box. */
/* BOTH SCALES ARE TOKENS (Sid, 2026-08-02). The literals moved to tokens.css so the collection's
   compact transport is a declared VALUE beside the full one rather than a second copy of these
   rules — the auto-play switch and the well both taught what a hand-kept copy costs, and
   verify-shared-controls asserts both scales so neither can drift. */
.vp-hero{ flex:0 0 auto; width:var(--hero); height:var(--hero); min-width:var(--hero); min-height:var(--hero);
  border-radius:50%; border:none; background:var(--blue); box-shadow:0 16px 32px rgba(63,111,214,.55);
  cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; }
.vp-hero svg{ width:var(--heroGlyph); height:var(--heroGlyph); display:block; }   /* glyph scales with the hero */
.vp-share{ flex:none; display:inline-flex; align-items:center; gap:6px; border:1px solid rgba(255,255,255,.18); background:rgba(255,255,255,.07);
  border-radius:11px; padding:11px 14px; cursor:pointer; color:#fff; font-family:inherit; font-weight:600; font-size:13px; }
.vp-share:hover{ background:rgba(255,255,255,.12); }

/* TYPE SCALE (both skins): title Inter 600 ~20px #fff; spec Roboto Mono 400 ~12px white@60%, tightened tracking.
   overflow-wrap:anywhere — an UNBROKEN token (a long filename title) breaks mid-word and wraps DOWN instead of
   spilling past the card edge. */
.vp-name{ text-align:center; font-weight:600; font-size:20px; letter-spacing:-.3px; color:#fff; margin-top:16px; overflow-wrap:anywhere; word-break:break-word; }   /* play→title 16; unbroken plugin take names wrap too */
.vp-fmt{ text-align:center; font-size:12px; color:rgba(255,255,255,.6); margin-top:6px; }                            /* title→spec 6 */

/* GREY BOX (surface-2): recipient = from-line (+ optional message); owner = message only. */
.vp-box{ background:#2C2C2E; border-radius:12px; padding:12px 14px; margin-top:16px; text-align:left; }              /* expiry→box 16 */
.vp-boxFrom{ display:flex; align-items:center; gap:9px; }
.vp-boxMsg{ font-size:14px; color:rgba(255,255,255,.7); line-height:1.45; }                                          /* message white@70% */
.vp-boxFrom:not([hidden]) + .vp-boxMsg{ margin-top:8px; }

/* expiry line: Inter 400 ~14px white@60%, the value ("5 days") red via the inline .x span (owner appends "· N plays") */
.vp-status{ display:flex; align-items:center; justify-content:center; gap:7px; margin-top:14px; font-size:14px; color:rgba(255,255,255,.6); text-align:center; }   /* spec→expiry 14 */
.vp-status .x{ font-weight:600; }

.vp-foot{ text-align:center; margin-top:16px; padding-top:13px; border-top:1px solid rgba(255,255,255,.1); }
/* footer tagline: Inter 400 ~13px white@~50%, an <a> → homepage (new tab); quiet, no underline (brightens on hover) */
.vp-by{ display:inline-block; font-size:13px; color:rgba(255,255,255,.5); font-weight:400; margin-top:0; text-decoration:none; }
.vp-by:hover{ color:rgba(255,255,255,.72); }

/* (mobile sizes for .vp-well/.vp-hero/.vp-name live in the consolidated ≤480px block at file end) */

/* ============ LISTEN LATER recipient skin — on the studio's REAL components ============
   card = .st-card, well = .st-win, wordmark = .st-wordmark (all from sendbox.css). Below is ONLY the
   recipient layout the studio doesn't already provide (everything Later-scoped so the live player is untouched). */
.vp-head.recip{ justify-content:center; }

/* spec line = Roboto Mono 400, TIGHTENED tracking (the default is too loose); "lossless" a touch brighter */
.vp-fmt{ font-family:var(--mono),'Roboto Mono',monospace; font-size:12px; font-weight:400; letter-spacing:-.2px; }
.vp-lossless{ color:rgba(255,255,255,.82); }

/* the well IS .st-win (transparent window onto the wallpaper + the in-well scrim); .ll-well only adds the
   size + rounded clip + the scrub cursor. blue=played / translucent-white=unplayed bars draw on the canvas.
   touch-action:none (bf): a horizontal drag inside the well is ALWAYS a scrub — the browser never turns it
   into a page scroll; the page scrolls normally everywhere outside the well. */
/* height: shared component */
.ll-well{ border-radius:14px; clip-path:inset(0 round 14px); margin-top:16px; cursor:pointer; touch-action:none; }
.ll-well canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }
/* the honest loading state: centred mono caption in the well, same chip treatment as the time
   badges so it reads as part of the well's instrument family. pointer-events none — the well
   stays the seek surface; visibility is app.js's call (shown only while playback is held). */
.vp-loading{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center; pointer-events:none; }
.vp-loading[hidden]{ display:none; }   /* display:flex would otherwise beat the hidden attribute (the (cv) play-button bug) */
.vp-loading span{ font-family:var(--mono),'Roboto Mono',monospace; font-size:12px; letter-spacing:.02em;
  color:rgba(255,255,255,.85); background:rgba(0,0,0,.62); padding:5px 12px; border-radius:8px; }

/* footer tagline lives in .vp-foot; .st-card-scoped so the live player's .vp-foot keeps its border. The blue
   "get it free" link is retained ONLY for the end states (expired/notfound/played CTA), not the player footer. */
.vp-getfree{ color:#7fa0e8; font-weight:600; text-decoration:none; }
.vp-getfree:hover{ text-decoration:underline; }
.st-card .vp-foot{ border-top:none; padding-top:0; margin-top:13px; }   /* divider→tagline 13 */

/* TOP BAR (both skins, same component): the wordmark is ALWAYS centred. Owner adds ONLY "‹ studio" pinned top-left
   (absolute → never shifts the wordmark); nothing else at the top. All .st-card-scoped so the live player is untouched. */
.st-card .vp-head{ position:relative; justify-content:center; }
.vp-wm{ display:inline-flex; align-items:center; text-decoration:none; }   /* wordmark is a contextual home link → keeps it centred as the one in-flow child */
.st-card .vp-back{ position:absolute; left:0; top:50%; transform:translateY(-50%); display:inline-flex; align-items:center; gap:4px; }
.vp-back{ color:rgba(255,255,255,.6); font-size:13px; font-weight:500; text-decoration:none; white-space:nowrap; }
.vp-back:hover{ color:#fff; }
.vp-back svg{ display:block; flex:0 0 auto; width:15px; height:15px; }

/* RECIPIENT viewer slot, top-RIGHT (absolute → mirrors ‹ studio so the wordmark stays centred). Owner: empty.
   anon → quiet "sign in" text link; signed-in non-owner → their avatar chip (→ studio). */
.st-card .vp-viewer{ position:absolute; right:0; top:50%; transform:translateY(-50%); display:flex; align-items:center; }
.vp-signin{ color:rgba(255,255,255,.7); font-size:13px; font-weight:500; text-decoration:none; }
.vp-signin:hover{ color:#fff; }
.vp-vava{ width:30px; height:30px; border-radius:50%; background:var(--blue) center/cover no-repeat; color:#fff;
  display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:600; text-decoration:none; text-transform:none; }

/* owner edit/share footer ROW (bottom slot, NOT a panel): two equal transparent outlined buttons, blue tint. */
/* ---- the SENDER DRAWER: extends from under the card's bottom edge (WeTransfer, inverted).
   The card keeps its own full rounded corners and z-sits ABOVE; the drawer tucks 18px behind the
   seam, a step lighter than the card, rounded bottom corners, never wider than the card. ---- */
.vp-card{ position:relative; z-index:1; }
.vp-drawer{ position:relative; z-index:0; width:min(384px, calc(100% - 28px)); margin:-18px 0 0 14px;
  background:#34343c; border:1px solid rgba(0,0,0,.55); border-top:none; border-radius:0 0 18px 18px;
  padding:calc(18px + var(--sp-tight)) 18px var(--sp-close);   /* 18 tucked under the card + a declared step of air */
  box-shadow:0 22px 48px rgba(0,0,0,.5); }   /* a CLEAR step lighter than the card — two obvious surfaces at the seam */
/* the hairline belongs to the CAPTION, which is why removing the caption removes the line — the
   owner drawer is three buttons on the drawer's own ground now (Sid, 2026-08-02). The signed-OUT
   sender's bar keeps its caption, and this is why it is a separate template: that caption carries
   the sign-in link that claims the link, so it is a control, not a label. */
.vp-drawer .vp-prevcap{ margin-top:0; padding-top:12px; border-top:1px solid rgba(255,255,255,.18); color:rgba(255,255,255,.55); }
/* air BELOW the caption when there is one; the drawer's own padding is the air when there is not */
.vp-drawer .vp-actions{ margin-top:0; }
.vp-drawer .vp-prevcap + .vp-actions{ margin-top:12px; }
/* the apps invitation, under the owner's copy/share row. Sits inside the drawer so it is measured
   with it — the owner card sizes itself around the drawer's real height (--card-inset), and a line
   added outside that measurement would push the drawer off the bottom on a short window. */
.vp-drawer [data-apps]{ margin-top:14px; }

.vp-actions{ display:flex; gap:10px; margin-top:13px; }   /* footer slot (divider→row 13), NO panel behind it */
.vp-actions .vp-obtn{ text-decoration:none; white-space:nowrap; }   /* anchors in the row match the buttons */
.vp-actions.wrap{ flex-wrap:wrap; }                        /* owner/sender bars: extra actions wrap when tight */
.vp-actions .vp-obtn.primary{ background:var(--blue); border-color:transparent; color:#fff; }
.vp-actions .vp-obtn.primary:hover{ background:var(--blue-hover); border-color:transparent; }
.vp-prevcap a{ color:#7fa0e8; font-weight:600; text-decoration:underline; text-underline-offset:2px; cursor:pointer; }
.vp-prevcap a:hover{ color:#a9c2ff; }
.vp-actions .vp-obtn{ flex:1; display:inline-flex; align-items:center; justify-content:center; gap:7px;
  border:.5px solid rgba(255,255,255,.15); border-radius:10px; background:none; padding:11px;
  color:var(--blue); font-family:inherit; font-weight:600; font-size:14px; cursor:pointer; }   /* a BUTTON keeps the surface blue even when its label is the blue part; was the pre-token #3B74F0 */
.vp-actions .vp-obtn:hover{ background:rgba(59,116,240,.10); border-color:rgba(59,116,240,.45); }
.vp-actions .vp-obtn svg{ display:block; }
/* NO drawer-local button variants: the drawer recolour changed the BACKGROUND only — buttons keep
   the shared .vp-obtn secondary treatment above and the standard .primary blue pill. */
/* owner-only quiet caption ABOVE the edit/share bar — a faint footnote, NOT a control (no blue, not tappable) */
.vp-prevcap{ text-align:center; font-size:11.5px; color:rgba(255,255,255,.3); margin-top:16px; }

/* 0.5px divider before the footer tagline (recipient only) */
.vp-div{ height:0.5px; background:rgba(255,255,255,.1); margin:16px 0 0; }   /* box→divider 16 */

/* ============ END STATES (cm): dead well + headline/sub + blue pill CTA → /about ============
   Sizes/tokens are the LIVE player's (stTitle scale, pinSub sub scale, --blue/--red, the real
   .st-win well) — the mockup was layout/copy reference only. */
.vp-deadwell{ display:flex; align-items:center; justify-content:center; }
.vp-flatline{ width:82%; height:2px; border-radius:1px; background:rgba(255,255,255,.30); }
.vp-stSub{ font-size:13.5px; color:rgba(255,255,255,.65); margin-top:9px; line-height:1.5;
  text-wrap:balance; max-width:290px; margin-left:auto; margin-right:auto; }
.vp-stCtaRow{ margin-top:22px; }
.vp-cta{ display:inline-block; background:var(--blue); color:#fff; text-decoration:none;
  font-size:14px; font-weight:500; letter-spacing:.01em; padding:11px 20px; border-radius:999px; }
.vp-cta:hover{ filter:brightness(1.08); }
.vp-cta:active{ transform:scale(.99); }
/* lockout: mono red countdown fed by the server's remaining lock seconds */
.vp-lockCount{ font-family:var(--mono),'Roboto Mono',monospace; font-size:13px; color:var(--red); margin-top:12px; }
.vp-state .vp-div{ margin-top:24px; }

/* PIN: surface-2 boxes + error line + low-tries warning + shake on wrong / lockout */
.vp-pinbox{ background:#2C2C2E; border-color:rgba(255,255,255,.14); }
.vp-pinErr{ font-size:12.5px; color:var(--red); font-weight:500; margin-top:13px; }
.vp-pinTries{ font-size:12px; color:rgba(255,255,255,.3); margin-top:8px; }
.vp-pinrow.shake{ animation:vp-shake .4s ease; }
@keyframes vp-shake{ 0%,100%{transform:translateX(0)} 20%{transform:translateX(-7px)} 40%{transform:translateX(6px)} 60%{transform:translateX(-4px)} 80%{transform:translateX(3px)} }

/* (the .ll-well mobile height lives in the consolidated ≤480px block at file end) */

/* ============ ACCOUNT MENU MODALS (used by acctmenu.js) ============
   The dropdown panel (.sb-acctpop + .achead/.acrow/...) is already shared in sendbox.css. These are the modal
   styles, COPIED verbatim from studio.css (.overlay/.acmodal/.acbtn/crop/zoom), plus the handful of CSS vars
   the player's tokens.css doesn't define. (Temporary copy — reconcile with the studio's when it adopts acctmenu.js.) */
.vp-card, body{ --ink:#fff; --ink-2:rgba(255,255,255,.60); --ink-3:rgba(255,255,255,.42);
  --coral:#FF453A; --coral-soft:rgba(255,69,58,.16); --blue-soft:rgba(59,116,240,.18); --blue-press:#2E63D8;
  --f:'Inter',system-ui,-apple-system,BlinkMacSystemFont,sans-serif; }
.overlay{ position:fixed; inset:0; background:rgba(0,0,0,.62); display:flex; align-items:center; justify-content:center; padding:24px; z-index:50; }
.acmodal{ width:360px; max-width:100%; background:#1C1C1E; border:1px solid rgba(255,255,255,.12); border-radius:16px;
  box-shadow:0 18px 50px rgba(20,24,40,.45); padding:22px 22px 20px; position:relative; text-transform:lowercase; }
.acmodal.confirm{ padding:24px 22px 20px; }
.acmodal__x{ position:absolute; top:14px; right:14px; width:28px; height:28px; border-radius:50%; border:none;
  background:rgba(255,255,255,.08); color:rgba(255,255,255,.6); display:flex; align-items:center; justify-content:center; cursor:pointer; }
.acmodal__x:hover{ background:rgba(255,255,255,.14); color:#fff; }
/* on the product step, not a literal (Sid, 2026-08-01). Was -.2px, which is -0.0125em at 16px and
   -0.0111em at the .confirm variant's 18px — the same declaration reading as two different
   trackings depending on which modal it was in, which is exactly what an em token fixes.
   Kept identical in studio.css and player.css: the modal markup is injected by acctmenu.js and the
   player copies these rules verbatim, so the two must move together. */
.acmodal h3{ margin:0; font-size:16px; font-weight:700; color:var(--ink); letter-spacing:var(--t-tracking); }
.acmodal.confirm h3{ font-size:18px; }
.acmodal .sub{ font-size:12.5px; color:var(--ink-2); margin-top:5px; line-height:1.5; }
.acmodal.confirm .sub{ font-size:13.5px; }
.acmodal .redicon{ width:46px; height:46px; border-radius:50%; background:var(--coral-soft); color:var(--coral);
  display:flex; align-items:center; justify-content:center; margin-bottom:14px; }
.acmodal .redicon svg{ width:22px; height:22px; }
.acmodal .crop{ width:170px; height:170px; border-radius:50%; border:2px solid var(--blue); overflow:hidden;
  margin:18px auto 0; position:relative; background:#0c0c0e; touch-action:none; }
.acmodal .crop canvas{ display:block; width:100%; height:100%; cursor:grab; }
.acmodal .crop canvas:active{ cursor:grabbing; }
.acmodal .crop.empty{ display:flex; align-items:center; justify-content:center; cursor:pointer; }
.acmodal .crop .ph{ color:rgba(255,255,255,.45); font-size:12.5px; font-weight:500; }
.acmodal .zoom{ display:flex; align-items:center; gap:11px; margin:16px 2px 2px; color:rgba(255,255,255,.4); }
.acmodal .zoom svg{ width:15px; height:15px; flex:0 0 auto; }
.acmodal .zoom input[type=range]{ flex:1; -webkit-appearance:none; appearance:none; height:4px; border-radius:2px; background:rgba(255,255,255,.16); outline:none; }
.acmodal .zoom input[type=range]::-webkit-slider-thumb{ -webkit-appearance:none; width:16px; height:16px; border-radius:50%; background:#fff; cursor:pointer; box-shadow:0 1px 4px rgba(0,0,0,.5); }
.acmodal .zoom input[type=range]::-moz-range-thumb{ width:16px; height:16px; border:none; border-radius:50%; background:#fff; cursor:pointer; }
.acmodal .zoom input[type=range]:disabled{ opacity:.4; }
.acmodal__row{ display:flex; gap:10px; margin-top:18px; }
.acbtn{ flex:1; display:flex; align-items:center; justify-content:center; text-align:center; gap:8px; padding:13px 12px;
  border-radius:10px; font:700 14px var(--f); cursor:pointer; border:none; transition:background .15s, filter .15s; }
.acbtn--sec{ background:transparent; color:var(--ink); border:1px solid rgba(255,255,255,.18); font-weight:600; }
.acbtn--sec:hover{ background:rgba(255,255,255,.06); }
.acbtn--blue{ background:var(--blue); color:#fff; }
.acbtn--blue:hover{ background:var(--blue-press); }
.acbtn--red{ background:var(--coral); color:#fff; }
.acbtn--red:hover{ filter:brightness(1.08); }
.acbtn:disabled{ opacity:.45; cursor:default; }

[hidden]{ display:none !important; }   /* hidden attr must beat explicit display:flex on .vp-from etc. */

/* trust line + remote message block, under the footer on both players (notice.js fills + styles the text) */
.vp-notice{ display:flex; flex-direction:column; align-items:center; gap:6px; margin-top:11px; text-align:center; }
.vp-notice:empty, .vp-notice > div:empty{ display:none; }

/* ============ MOBILE (≤480px) — the ONE authoritative compaction section ============
   Must stay LAST in this file: earlier attempts lost the cascade to base rules written after them
   (same specificity → later order wins) and the card+drawer overflowed short phones.
   Three tiers, each additive over the last:
   · baseline  — every phone: the sizes the original mobile fix intended (well 104, hero 64…)
   · ≤760px tall — iPhone Safari with its bars showing (~660-740 of visual height): tightened
     spacing so card + FULL drawer end above the need-help pill's reserved zone, no stage scroll
   · ≤620px tall — 320×568-class small phones: deep compaction, same discipline as the homepage
     post-drop fix (short well, small hero, tight print) — everything still fits, pill stays clear
   The .vp-card-prefixed footer-stack rules out-rank notice.js's runtime single-class styles. */
@media (max-width:480px){
  /* reserved bottom zone: the need-help pill never overlaps the drawer controls (rule as homepage) */
  /* THE RESERVED ZONE IS THE PILL'S ACTUAL SIZE (Sid, 2026-08-02 — "dead space at the card bottom").
     On a phone support.js collapses the pill to a 42px circle at bottom:14, so it occupies 14..56
     from the bottom edge; 70 reserved 14px for nothing. 60 keeps a 4px gap above it and hands the
     rest to the card, which is the surface that needs it. */
  .vp-stage{ padding:var(--card-top) 14px max(60px, calc(env(safe-area-inset-bottom) + 46px)); }
  /* and the card's own cap follows the zone it was derived from: 88 was 12 top + 70 reserved + air,
     so with the reservation at 60 the card gets those 14px back rather than leaving them dead.
     On :root, not on the card: the base cap is declared LATER in this file, so a card-scoped
     override here would lose on source order at equal specificity. */
  :root{ --card-inset:74px; }
  .st-card.vp-card{ padding:18px 18px 20px; }
  /* (well height is the shared component's, tiers included — see sendbox.css) */
  .vp-well{ height:130px; }
  .vp-playRow{ margin-top:14px; }
  .vp-hero{ width:64px; height:64px; }
  .vp-hero svg{ width:24px; height:24px; }
  .vp-name{ font-size:18px; margin-top:10px; }
  .vp-status{ margin-top:10px; }
  .vp-drawer{ padding-bottom:14px; }
  .vp-title{ font-size:18px; }
  .vp-pinbox{ width:46px; height:56px; font-size:24px; }
}
@media (max-width:480px) and (max-height:760px){
  /* (the card's top offset is --card-top, shared — this tier no longer keeps its own) */
  .st-card.vp-card{ padding:16px 16px 14px; }
  .ll-well{ margin-top:12px; }
  .vp-playRow{ margin-top:12px; }
  .vp-hero{ width:60px; height:60px; }
  .vp-fmt{ margin-top:5px; }
  .vp-status{ margin-top:9px; }
  /* the stack keeps the DESKTOP air above it (div 16 + pfoot 15) — only its internals tighten */
  .vp-card .vc-pfoot{ line-height:1.5; }
  .vp-card .vc-pf2, .vp-card .vc-pf3{ margin-top:8px; }
  .vp-card .vc-pf4{ margin-top:2px; }
  .vp-drawer{ padding:24px 16px 12px; }   /* 18 tucked + 6 air */
  .vp-drawer .vp-prevcap{ padding-top:10px; }
  .vp-drawer .vp-actions{ margin-top:10px; }
  .vp-actions{ gap:8px; }
  .vp-actions .vp-obtn{ padding:9px; font-size:13.5px; }
}
@media (max-width:480px) and (max-height:620px){
  /* (the card's top offset is --card-top, shared — this tier no longer keeps its own) */
  .st-card.vp-card{ padding:10px 14px 10px; }
  .ll-well{ margin-top:8px; }
  .vp-playRow{ margin-top:6px; }
  .vp-hero{ width:48px; height:48px; }
  .vp-hero svg{ width:20px; height:20px; }
  .vp-name{ font-size:16px; margin-top:4px; }
  .vp-fmt{ margin-top:2px; font-size:11px; }
  .vp-status{ margin-top:4px; font-size:12px; }
  /* footer-stack air above stays on the desktop rhythm here too; internals tighten */
  .vp-card .vc-pfoot{ line-height:1.4; }
  .vp-card .vc-pf2, .vp-card .vc-pf3{ margin-top:5px; }
  .vp-drawer{ padding:22px 14px 8px; }
  .vp-drawer .vp-prevcap{ padding-top:8px; }
  .vp-prevcap{ font-size:10.5px; }
  .vp-drawer .vp-actions{ margin-top:8px; }
  .vp-actions{ gap:6px; }
  .vp-actions .vp-obtn{ padding:8px; font-size:13px; }
}

/* the display-name prompt's input (acctmenu.js nameModal) — copied verbatim from studio.css */
.acmodal .acinput{ width:100%; margin-top:16px; padding:12px 13px; border-radius:11px; border:1px solid rgba(255,255,255,.18);
  background:rgba(255,255,255,.07); color:var(--ink); font:500 14px var(--f); outline:none; }
.acmodal .acinput::placeholder{ color:rgba(255,255,255,.34); }
.acmodal .acinput:focus{ border-color:var(--blue); box-shadow:0 0 0 3px var(--blue-soft); }

/* ============ DESKTOP short windows (>480px wide) — same tiered discipline as mobile ============
   The desktop card + drawer are fixed-height (~759px incl. stage top padding): taller than a
   shallow browser window, so the stage scrolled internally and the card moved. Two height tiers
   keep card + FULL drawer above the need-help pill with NO scroll (padding-bottom 57 = the pill's
   fixed zone: 18 offset + 33 pill + 6 air). The stage keeps overflow-y:auto ONLY as a last-resort
   fallback for pathological window heights; at these tiers nothing scrolls. */
/* the inset shares --card-top's threshold, NOT the well tier's: between 801 and 820 the top offset
   is still 48, and a 64 inset there would push the card down into the pill's reserved zone. */
@media (min-width:481px) and (max-height:800px){ :root{ --card-inset:64px; } }
@media (min-width:481px) and (max-height:820px){
  .vp-stage{ padding:var(--card-top) 56px 57px; }
  /* THE CARD TAKES THE ROOM THE LADDER LEAVES (Sid, 2026-08-02, "find the few pixels honestly").
     --card-inset is what the card must leave clear: the top offset plus the stage's bottom
     reservation. At this tier that is 7 + 57 = 64, and the cap was still carrying 88 from the
     generation where the top offset was 20 — 24px of page room reserved twice and used once, while
     the tagline's last line clipped. Derived from the two numbers rather than re-guessed, and
     min-width-scoped so a phone (which matches this height query too) keeps its own 74. */
  .ll-well{ margin-top:14px; }
  .vp-well{ height:120px; }
  .vp-playRow{ margin-top:14px; }
  .vp-hero{ width:64px; height:64px; }
  .vp-hero svg{ width:26px; height:26px; }
  .vp-name{ margin-top:12px; }
  .vp-status{ margin-top:10px; }
  .vp-div{ margin-top:14px; }
  .vp-card .vc-pfoot{ line-height:1.5; }
  .vp-card .vc-pf2, .vp-card .vc-pf3{ margin-top:9px; }
  .vp-drawer{ padding:24px 18px 14px; }
  .vp-drawer .vp-prevcap{ padding-top:10px; }
  .vp-drawer .vp-actions{ margin-top:10px; }
  .vp-actions .vp-obtn{ padding:10px; }
}
@media (min-width:481px) and (max-height:680px){
  .vp-stage{ padding:var(--card-top) 56px 57px; }
  .ll-well{ margin-top:12px; }
  .vp-well{ height:96px; }
  .vp-playRow{ margin-top:12px; }
  .vp-hero{ width:56px; height:56px; }
  .vp-hero svg{ width:22px; height:22px; }
  .vp-name{ margin-top:10px; font-size:19px; }
  .vp-fmt{ margin-top:5px; }
  .vp-status{ margin-top:8px; }
  .vp-div{ margin-top:12px; }
  .vp-card .vc-pfoot{ margin-top:12px; line-height:1.45; }
  .vp-card .vc-pf2, .vp-card .vc-pf3{ margin-top:8px; }
  .vp-drawer{ padding:22px 18px 12px; }
  .vp-drawer .vp-prevcap{ padding-top:9px; }
  .vp-drawer .vp-actions{ margin-top:9px; }
  .vp-actions .vp-obtn{ padding:9px; }
}

/* ============ MASTER LINK — recipient tracklist (dh §3) ========================================
   Quiet mono rows under the title block, in the tech-line register: the card (well + hero play +
   title) stays the hero and this reads as furniture. Deliberately NOT studio-like — no play
   buttons, no menus, no handles. The only affordances are "load this one" and, once released,
   "download this one" / "download all". Rows sit on the solid card surface (GLOBAL card rule). */
.vp-tracks{ margin:2px 0 12px; }
.vp-tkList{ display:flex; flex-direction:column; }
/* MOBILE VIEWPORT RULE: up to 4 rows sit inline; beyond that the TRACKLIST alone scrolls so the
   card itself never grows past the viewport. ~34px a row. */
.vp-tkList.scroll{ max-height:136px; overflow-y:auto; -webkit-overflow-scrolling:touch;
  border-top:1px solid rgba(255,255,255,.08); border-bottom:1px solid rgba(255,255,255,.08); }
.vp-tkRow{ display:flex; align-items:center; gap:9px; padding:8px 2px; cursor:pointer;
  border-bottom:1px solid rgba(255,255,255,.07); }
.vp-tkList.scroll .vp-tkRow:last-child{ border-bottom:none; }
.vp-tkRow:last-child{ border-bottom:none; }
.vp-tkRow:hover .vp-tkName{ color:#fff; }
.vp-tkNum{ flex:none; width:15px; font:400 11px/1 'Roboto Mono',monospace; color:rgba(255,255,255,.3); }
.vp-tkName{ flex:1; min-width:0; font:400 13px/1.3 Inter,sans-serif; color:rgba(255,255,255,.62);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.vp-tkDur{ flex:none; font:400 11px/1 'Roboto Mono',monospace; color:rgba(255,255,255,.3); }
/* the one that's loaded — blue is the only tint, so it marks "this is playing" */
.vp-tkRow.on .vp-tkName{ color:#fff; font-weight:600; }
.vp-tkRow.on .vp-tkNum{ color:var(--blue-text); }
.vp-tkDl{ flex:none; background:none; border:0; padding:3px; cursor:pointer; display:flex; border-radius:6px; opacity:.55; }
.vp-tkDl:hover{ opacity:1; background:rgba(255,255,255,.08); }
.vp-tkAll{ width:100%; margin-top:9px; background:none; border:1px solid rgba(255,255,255,.16);
  border-radius:10px; padding:8px; cursor:pointer; font:600 12.5px Inter,sans-serif; color:#fff; }
.vp-tkAll:hover{ border-color:var(--blue); }

/* ---- feedback round 1: recipient player ---- */
/* play is the ONLY large circle; auto-play sits beside it (studio placement) */
.vp-playRow{ display:flex; align-items:center; justify-content:center; gap:16px; position:relative; }
/* (the player's ORIGINAL auto-play — 26x16, blue on-state, absolutely-positioned knob — lived
   here. Three generations of this control existed in this one file; all are gone, and the control
   is the shared component in sendbox.css.) */
/* a single released track uses the SAME row + arrow as a collection's rows */
.vp-tkRow.single{ cursor:default; }
.vp-tkRow.single:hover .vp-tkName{ color:#fff; }
/* "download all tracks" is a small blue button, not a big outline block */
.vp-tkAll{ width:auto; display:block; margin:10px auto 0; padding:7px 14px; border:0; border-radius:999px;
  background:var(--blue); color:#fff; font:600 12px Inter,sans-serif; cursor:pointer; }
.vp-tkAll:hover{ filter:brightness(1.08); }
.vp-tkAll[disabled]{ opacity:.6; cursor:default; }
/* recipient footer: lead + one line + one way in */
.vc-pfoot .vc-pfcta{ margin-top:9px; }
.vc-pfoot .vc-pfcta a{ color:var(--blue-text); font:600 12.5px Inter,sans-serif; text-decoration:none; }
.vc-pfoot .vc-pfcta a:hover{ text-decoration:underline; }

/* ---- round 2: recipient player ---- */
/* prev/next flank play; play stays the centred hero */
.vp-skip{ flex:none; width:var(--skip); height:var(--skip); border-radius:50%; border:0; cursor:pointer;
  background:#2C2C2E; display:flex; align-items:center; justify-content:center; }
.vp-skip svg{ width:var(--skipGlyph); height:var(--skipGlyph); }
.vp-skip:hover{ background:#3A3A3C; }
/* sender chip: the MESSAGE is the prominent line, "sent by {name}" smaller and quieter above it */
.vp-boxFrom{ font-size:11.5px; color:rgba(255,255,255,.45); }
.vp-boxFrom .vp-fromTxt b{ color:rgba(255,255,255,.7); font-weight:600; }
.vp-boxMsg{ font-size:15px; line-height:1.45; color:#fff; margin-top:5px; }
/* auto-play is a PLAYBACK preference → green when on (link settings stay blue) */
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */


/* ---- round 3 ---- */
/* titles show in full, wrapping to two lines, and ARE the play target */
.vp-tkName{ white-space:normal; overflow-wrap:anywhere; display:-webkit-box; -webkit-line-clamp:2;
  -webkit-box-orient:vertical; overflow:hidden; line-height:1.3; cursor:pointer; }
/* transport cluster: the studio player's exact spacing, here too */
.vp-playRow{ display:flex; align-items:center; justify-content:center; gap:18px; position:relative; }
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */

/* sender chip: small italic "sent by", then avatar, then name — one unit above the message */
.vp-boxFrom{ display:flex; align-items:center; gap:7px; }
.vp-fromLabel{ font:italic 400 11px Inter,sans-serif; color:rgba(255,255,255,.4); }
.vp-boxFrom .vp-ava{ order:2; }
.vp-boxFrom .vp-fromTxt{ order:3; font:600 12.5px Inter,sans-serif; color:rgba(255,255,255,.72); }

/* ============================================================================
   COLLECTION PLAYER — visual pass (items 2, 4, 5). Appended at the END on purpose:
   this file has twice lost rules to a later duplicate @media of the same specificity,
   so everything below wins over everything above, and this section's OWN phone block
   sits last inside it. Only the collection skin is touched — body.has-tracks is set by
   app.js when a tracklist renders, so a single-track link keeps the layout it had.
   ============================================================================ */

/* item 2 — the tech line lives on the row it describes. Same slot as the duration, so the
   column never shifts: the playing row just says something more useful about itself.
   Quiet mono, NOT blue: blue is the interactive tint and a readout is not tappable (the
   row's number is already blue to mark "playing", and two blues on one row is noise). */
.vp-tkDur.spec{ font-family:var(--mono),'Roboto Mono',monospace; color:rgba(255,255,255,.6); letter-spacing:-.2px; }

/* item 4 — the CARD is fixed; only the tracklist moves. Waveform, transport and title stay exactly
   where the eye left them while the list scrolls under them. The card is capped to the viewport and
   clips, and the tracklist is the one child allowed to give: everything else keeps its natural
   height, so a long collection shortens the LIST rather than pushing the card off screen or handing
   the scroll to the page. (The drawer is a SIBLING of the card, so clipping here never touches it.) */
/* --card-inset is the room the card must leave: the stage's top padding plus the need-help pill's
   reserved zone. It is a token so the phone breakpoint can shrink it with the reservation it
   mirrors, from :root, which is the only place that wins against this rule's source position. */
body.has-tracks .st-card.vp-card{ display:flex; flex-direction:column; overflow:hidden;
  max-height:calc(100svh - var(--card-inset, 88px)); }
body.has-tracks .vp-card > *{ flex:0 0 auto; }
/* THE CHAIN HAS TO BE CONTINUOUS, and this is the link that was missing. The two rules below say
   "the tracklist is the child that gives" — and they were INERT, because #player sits between them
   and the card: the `> *` rule above makes it rigid, and a plain block hands no flex context to its
   children, so `flex:0 1 auto` on .vp-tracks was addressed to nobody. The card capped, nothing
   inside could shrink, and overflow:hidden did the only thing left: it CLIPPED. Measured on an
   8-track collection before the fix — 104px gone at 1440x800, 227 at 1440x650, 54 at 390x740,
   135 at 390x600; 0 after, at every one. (The well's compaction tiers were a partial mitigation for
   this and stay: they compact a short viewport, which is a different axis.)
   NOT a shared component with the studio panel, and worth saying so: sendbox.css owns .st-card and
   the well, not either tracklist. The studio's chain (#viewPanel, flex:1 1 auto + min-height:0 +
   overflow-y:auto) was already continuous and clips nothing. Same rule, applied twice, in the two
   stylesheets that own the two chains.
   Margin collapsing was the risk in making #player a flex container, so it was A/B'd live rather
   than assumed: every child's margin-bottom is 0 today, so nothing collapses and no gap moved at
   1440 or 390. */
/* overflow-y:auto is the LAST RESORT, and the order matters: flex shrinking happens before
   scrolling, so the tracklist still gives first and item 4's rule ("the card is fixed, only the
   list moves") holds wherever it can. It cannot hold everywhere — measured at 1440x650, the card's
   NON-tracklist content alone overruns the cap by 62px, so with the list collapsed to nothing there
   was still 62px being cut. Cutting the footer off with no way to reach it is worse than the card
   scrolling at a window height where it simply does not fit. */
body.has-tracks #player{ display:flex; flex-direction:column; flex:1 1 auto; min-height:0; overflow-y:auto; }
/* AND THE SAME LAW ONE LEVEL DOWN, which the first version of this fix missed and the shared-controls
   harness caught: making #player a flex container gave every one of its children flex:0 1 auto, so
   the WELL started giving too — 110 became 64.5 at 1440x800 and 84 became 0 at 1440x650, against a
   studio well still on the ruled tier. Only the tracklist may give, exactly as the card's own `> *`
   rule says one level up. The id is in the tracks selector on purpose: `#player > *` outranks a
   bare .vp-tracks, so without it the rule below would be the inert one. */
body.has-tracks #player > *{ flex:0 0 auto; }
body.has-tracks #player > .vp-tracks{ display:flex; flex-direction:column; min-height:0; flex:0 1 auto; }
/* RESIDUAL, stated rather than hidden: the bottom fade is on .scroll, which app.js sets by COUNT
   (>4 tracks). A short collection on a very short window now scrolls this list without the fade.
   That is still strictly better than the card eating the rows, and the fade's own rule is the
   cut-row design for a long collection, which is a different question. */
/* flex:1 1 — it GROWS into whatever the card can spare, and shrinks only when the card would
   otherwise clip. 0 1 was the regression: it could give but never take, so the list stayed at its
   cap however much room sat around it. */
body.has-tracks .vp-tkList{ min-height:0; flex:1 1 auto; overflow-y:auto; }

/* item 5 — the tracklist is a WINDOW, not a list that happens to clip: recessed edge, its own
   inset ground, a bottom fade, and a deliberately HALF-CUT row at overflow so the next track's
   existence is obvious without a scrollbar announcing it. */
/* 37px is MEASURED, not assumed: 8px padding top and bottom around a 1.3 line-height 13px name.
   The first guess (34) plus a 30px fade put the cut row's TEXT 4.3px into view and then faded all
   4.3 of it — the list looked like a tidy three and tracks 4-6 were invisible. */
:root{ --tkrow:37px; }
/* 15, not 10, and the 15 is the scroll lane (Sid, 2026-08-01 — the same one the picker and the
   panel tracklist carry). This list scrolls, every row is a click target, and 15px is the widest
   classic macOS scrollbar; an overlay bar is narrower, so clearing 15 clears both. SYMMETRIC on
   purpose: the rows sit inside a recessed window whose divider spans the row, so a lane on the
   right alone would leave every divider 5px short at one end and the window visibly off-centre.
   Cost: 5px of row width at each side. */
body.has-tracks .vp-tkList{ border-radius:10px; padding:1px 15px;
  background:rgba(0,0,0,.20);
  box-shadow:inset 0 1px 2px rgba(0,0,0,.55), inset 0 0 0 1px rgba(255,255,255,.055); }
/* 3.8 rows with an 8px fade, both derived from the measurement above: three whole rows, then ~29px
   of the fourth, whose text clears the fade by ~12px. That is the point — a cut row you can READ a
   bit of says "there is more" in a way an empty sliver never does. */
/* ---- THE BLEND (Sid, 2026-08-02) ------------------------------------------------------------
   "On a collection the TRACKLIST is the most important element on the page and the layout must
   protect it first." Two halves, both mocked from live before either was written:
     · the ceremony COMPACTS — the well through the shared component's .has-list state, the
       transport onto its declared compact scale. Collection skin only; the solo is untouched
       because .has-list is never set there, and that is asserted.
     · the tracklist's FLOOR rises to 4.5 rows, so the list stops being the only thing that gives.
   Measured at 1440x800 on the mock: 1.57 rows today, 4.55 with the floor alone (card scrolling
   104px on 8 tracks), 3.57 with the compaction alone, 4.55 with both and 20px of card scroll. The
   blend is the only one that fixes the phone AND the desktop — at 390 the floor never binds at all,
   so route A on its own changed nothing there.
   THE COMPACT SCALE IS TOKENS, NOT A SECOND SET OF RULES: only the values change here. */
body.has-tracks .vp-card{ --hero:var(--hero-tight); --skip:var(--skip-tight);
  --heroGlyph:var(--heroGlyph-tight); --skipGlyph:var(--skipGlyph-tight); }
/* the hero is smaller, so its shadow is too — a 48px button under an 88px button's shadow reads as
   a mistake rather than as a smaller button */
body.has-tracks .vp-card .vp-hero{ box-shadow:0 8px 18px rgba(63,111,214,.45); }
/* the ceremony's own gaps come down one declared step each, which is the rest of the freed height */
body.has-tracks .vp-card .vp-playRow{ margin-top:var(--sp-block); }
body.has-tracks .vp-card .vp-name{ margin-top:var(--sp-block); }
body.has-tracks .vp-card .vp-tkMeta{ margin-top:var(--sp-tight); }
body.has-tracks .vp-card .vp-tracks{ margin-top:var(--sp-block); }

/* THE FLOOR BELONGS TO THE WRAPPER, not to the list, and that is Sid's "download all sits tight
   against the tracklist box". .vp-tracks is the shrinkable one (flex:0 1 auto, min-height:0) and
   the list had the floor, so once the card squeezed the wrapper past that floor the LIST kept its
   height and simply overflowed it — and "download all tracks" is spaced from the WRAPPER, so the
   list ran straight into the button. Measured before the fix at 1440x800: the list's bottom sat 7px
   BELOW the button's top on an 8-track collection, 44px below at 1440x700. With the floor here the
   wrapper can never be shorter than the list it contains, and the gap goes back to being the ruled
   step it always declared (30 at desktop, 23 at 390 — --sp-wide, verified at every height).
   THE FLOOR IS 4.5 ROWS ON A COLLECTION (Sid, 2026-08-02, the blend's number). It was 1.5, which
   was enough to stop the list vanishing and nowhere near enough to make it the protected element:
   at 1440x800 a six-track collection rendered a one-and-a-half row window while the well, hero,
   message and footer all kept full size. The .5 is not decoration — a whole number of rows is
   exactly the clean cut the peek rule forbids, and +2px is the list's own padding so the floor
   lands ON 4.5 rows after the quantiser rather than one row under it (it rounds DOWN to
   whole-rows-plus-a-half). Below this the card scrolls, which is the honest last resort.
   AND IT IS THE SHORTER OF 4.5 ROWS AND THE WHOLE LIST. app.js writes --tk-floor from the track
   count, because a 3-track collection given a flat 4.5-row floor got a window with 1.5 rows of
   empty inset ground under its last row — a floor protecting rows that do not exist. */
body.has-tracks #player > .vp-tracks{ min-height:calc(var(--tkrow) * var(--tk-floor, 4.5) + 2px); }
/* THE MIDDLE REGION ANNOUNCES ITSELF (Sid, 2026-08-02). At 1200x700 #player scrolled 76px with
   nothing straddling its edge — a clean cut, which the scroll law forbids, and the one thing
   verify-shared-controls has been refusing to pass since the card-fit pass.
   A MASK rather than an overlay, and the reason is structural: an absolutely-positioned gradient
   inside a scroller scrolls away with the content it is meant to be fading, and #player has no
   wrapper of its own to hang one on. A mask belongs to the box, so it stays on the edge.
   Only while there is more below — app.js toggles .more from the scroll position, exactly as the
   tracklist's fade does. A gradient that never leaves is the fault that was fixed there. */
body.has-tracks #player.more{
  -webkit-mask-image:linear-gradient(180deg, #000 calc(100% - 26px), transparent 100%);
          mask-image:linear-gradient(180deg, #000 calc(100% - 26px), transparent 100%); }
/* NO HARD 3.8-ROW CAP ANY MORE (Sid, 2026-08-01). The cap was what made the list ignore room the
   card plainly had: at 1440x1200 the card sat at 828 inside a 1112 allowance — 284px spare — and
   still showed 3.81 of 8 tracks behind a scroll, and at 1440x800 it was down to the two-row floor.
   The list TAKES the available height now (flex:1 1 auto above) and the card's own max-height is
   what bounds it, so a tall window shows the whole running order and a short one still gives the
   list back to the card rather than clipping. The cut row and its fade are not lost — they appear
   exactly when the list really is too short for its rows, which is what they were always FOR.
   app.js no longer writes an inline max-height either; the two together were the cap. */
/* max-height:NONE, explicitly, because the cap is declared in TWO older places that both still win
   on specificity or order if this only omits it: .vp-tkList.scroll's own 136px literal (the
   pre-has-tracks generation) and the phone block's calc. Removing this rule's cap alone left the
   list measuring 136 on desktop and 140.6 on a phone — the cap looking exactly like no cap. */
body.has-tracks .vp-tkList.scroll{ overflow-y:auto; max-height:none;
  border-top:none; border-bottom:none;
  /* the fade IS the affordance; a scrollbar would just be furniture on top of it */
  scrollbar-width:none; }
/* THE FADE IS AN OVERLAY, NOT A MASK (Sid, 2026-08-02). Sid reports the tracklist rendering as an
   empty black box in iPhone SCREENSHOTS while the live page is fine — the signature of a masked
   layer that the screenshot compositor rasterises without its mask source. I could not reproduce it
   (headless Chrome screenshots the mask correctly, and there is no way to drive iOS's compositor
   from here), so this is not a verified fix of that bug — it REMOVES THE MECHANISM. There is no
   mask on this element any more, so there is nothing for a compositor to drop.
   It is the same gradient by construction: the mask faded content to transparent over the list's
   ground, and this paints that ground over the content instead. rgb(21,22,23) is that ground
   computed — rgba(0,0,0,.20) over --surface-card #1A1B1D. On the WRAPPER because an absolute child
   of the scroller would scroll with the rows; .more is mirrored onto it by app.js.
   Every screenshot of a player is marketing, which is why this was worth the paint change. */
body.has-tracks #player > .vp-tracks{ position:relative; }
/* HALF A ROW, not 8px, and three stops rather than two. The mask it replaced was 8px too, but a
   mask DISSOLVES the ink and an overlay COVERS it: ramping an opaque ground from 0 to 100% in 8px
   against a 37px row put a visible seam across the middle of the peeking row's glyphs — Sid's hard
   dark band, and it is real, it is just the ramp and not the colour (the ground matches: the list
   is rgba(0,0,0,.20) over #1A1B1D = rgb(21,22,23), measured). It fades over the half-row that
   peeks, which is what it is describing. */
body.has-tracks #player > .vp-tracks::after{ content:''; position:absolute; inset:auto 1px 1px 1px;
  height:calc(var(--tkrow) * 0.5); pointer-events:none; opacity:0; transition:opacity .12s linear;
  border-radius:0 0 10px 10px;
  background:linear-gradient(180deg, rgba(21,22,23,0) 0%, rgba(21,22,23,.45) 45%,
                             rgba(21,22,23,.85) 78%, rgba(21,22,23,1) 100%); }
body.has-tracks #player > .vp-tracks.more::after{ opacity:1; }
body.has-tracks .vp-tkList.scroll::-webkit-scrollbar{ width:0; height:0; }
/* inside the window the divider is a hairline between rows, and the last row never draws one */
body.has-tracks .vp-tkRow{ border-bottom:1px solid rgba(255,255,255,.055); }
body.has-tracks .vp-tkRow:last-child{ border-bottom:none; }

/* ---- item 7: ONE spacing scale for the collection card ----------------------------------------
   The card read bunched because every gap was its own number (16/6/14/16/13…) with no relationship
   between them. Three steps only, and each gap is one of them:
     --sp-air   air around the HERO cluster (well → transport → title). The eye's first two stops.
     --sp-block between blocks that are separate thoughts (title → tracks → download-all → expiry)
     --sp-close inside the quiet close (chip → divider → footer), which reads as ONE group
   The intended read is waveform → play → title → tracks, with a pause at each. Anything that is not
   one of those four gets less air, not more. */
/* the scale is the SAME 6-rhythm, now five steps: 6 / 12 / 18 / 24 / 30. --sp-wide (30) is the
   rhythm's next step, not a value minted between steps — it exists because equalising the two
   gaps that flank the message box moved 6px above the download pill. The design's earlier
   off-rhythm 28 still snaps to 24; nothing sits between steps. Phones: 5 / 9 / 14 / 18 / 23,
   the same ~0.75 step-down rounded half-up, exactly as the other four are. */
/* (the five spacing steps are declared in tokens.css — this file inherits them) */
.vp-card .vp-playRow{ margin-top:var(--sp-air); }                 /* well → transport */
.vp-card .vp-name{ margin-top:var(--sp-air); }                    /* transport → title (equal air, so the hero cluster is symmetric) */
.vp-card .vp-fmt{ margin-top:7px; }                               /* single only: its spec stays tucked under the title */
body.has-tracks .vp-tracks{ margin:var(--sp-block) 0 0; }         /* title → the window */
body.has-tracks .vp-tkAll{ margin:var(--sp-block) auto 0; }       /* window → download-all: generous, it is a decision */
.vp-card .vp-status{ margin-top:var(--sp-block); }                /* → expiry: generous, it is the other decision */
/* THE RELEASE ROW — collection skins only (Sid, 2026-08-02). Expiry and download-all were a stack
   of two centred decisions costing two gaps and two rows of height on a card whose tracklist was
   the thing going short. One row: expiry LEFT, pill RIGHT. The row owns the gaps, so the children's
   own margins go — in a flex row they would not collapse and the stack's steps would double up.
   `order:-1` rather than reordering the markup: the SOLO player's stack was ruled separately and
   keeps DOM order untouched, so this is a change to one skin, not to both.
   The freed height goes nowhere in particular by design — .vp-tkList is flex:1 1 and simply takes
   it, and the half-row peek re-quantises against the new space on the next layout pass.
   IDS IN THE SELECTORS ON PURPOSE: .vp-tkAll's and .vp-status's stack margins are re-declared
   further down this file at equal specificity, so a class-only rule here would be overruled by
   source order and the row would carry the stack's gaps inside it. */
body.has-tracks #relRow{ display:flex; align-items:center; justify-content:space-between;
  gap:var(--sp-close); margin-top:var(--sp-block); }   /* the one rhythm — see .vp-tracks below */
body.has-tracks #relRow > #status{ order:-1; margin-top:0; text-align:left; }
body.has-tracks #relRow > #tkAll{ margin:0; flex:0 0 auto; }
/* downloads off: there is one thing on the row, and it sits where it always sat */
body.has-tracks #relRow.solo{ justify-content:center; }
body.has-tracks #relRow.solo > #status{ text-align:center; }
.vp-card .vp-box{ margin-top:var(--sp-block); }                   /* the quiet close starts here */
.vp-card .vp-div{ margin-top:var(--sp-close); }
.vp-card .vc-pfoot{ margin-top:var(--sp-close); }                 /* chip + footer read as one close */

/* phones: the same scale, one step down. Kept in THIS section (last in the file) so it cannot lose
   the cascade to the older ≤480 block above — the trap this file has fallen into twice. */
@media (max-width:480px){
  /* the ≤480 step-down lives in tokens.css. The three values that used to sit here (18/15/10) were
     already dead — a later block in this file overrode them with the shared 18/14/9 — and would
     have come back to life the moment that later block was removed. Deleted rather than left. */
  /* the compaction tiers below set width/height; min-* must follow or the guard would pin the hero
     at desktop size on a phone and blow the one-viewport rule. */
  .vp-hero{ min-width:0; min-height:0; }
  /* THE PHONE'S COPY OF THE 3.8-ROW CAP IS GONE TOO, and it had to be found the same way as the
     desktop one: the list measured 140.6px on a phone and 136 on desktop after the cap was
     "removed", which is what a second and third declaration of the same law look like from the
     outside. The list takes the card's available height on both. */
  body.has-tracks .vp-tkList.scroll{ max-height:none; }
  /* (a .vp-card .vp-name{font-size:19px} sat here and never applied: the type-scale section below
     sets the same selector at equal specificity and wins on order. Removed, not revived — the
     phone size is --t-title's 20px.) */
}

/* ---- auto-play: the STUDIO player's control, replicated exactly -------------------------------
   The player does not load studio.css, so the shared .sv-toggle base never reaches this surface
   and the recipient had grown its own bigger, inline version (26x16 beside a 10.5px label). These
   are the studio's own values: stacked, 20x12 switch, 9px knob, 8px label, 3px gap, pinned right
   of the transport the same way. — SUPERSEDED: there is nothing to keep in step any more, the
   control is ONE component in sendbox.css, which all three surfaces load. */
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */

/* ---- auto-play joins the transport cluster ---------------------------------------------------
   (this used to say "the player never loads studio.css, so these values are repeated deliberately;
   keep them in step". They did not stay in step — every field had drifted. sendbox.css, which the
   player DOES load, is the shared home now.) */
.vp-playRow{ align-items:center; }
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */


/* the collection's own line: what this link holds, stated once, true wherever the list is scrolled */
.vp-tkMeta{ text-align:center; margin-top:7px; font:400 11px/1 var(--mono),'Roboto Mono',monospace;
  letter-spacing:-.2px; color:rgba(255,255,255,.42); }

/* item 8: the spec line under the title — same tracking as every other surface (it carried the
   tracklist row's -.2px, which is right inside a mono row and wrong as a standalone caption), and
   the whitespace scale's close step between title and spec instead of a one-off 7px. */
.vp-card .vp-fmt{ margin-top:var(--sp-close); letter-spacing:normal; }

/* ============================================================================================
   TYPE SCALE — the twin of the whitespace scale. The card was speaking in ELEVEN size/family
   voices (seven Inter sizes: 11, 12, 12.5, 13, 13.5, 14, 15). Four voices, and every text element
   on the card maps to one. Emphasis comes from WEIGHT and COLOUR, never from a new size.
     --t-title    the one large voice: the track/collection name
     --t-body     statements the reader acts on: expiry, who sent it
     --t-caption  supporting lines: labels, the message, the footer
     --t-mono     numbers and specs — times, track numbers, durations, rate/depth
   --t-micro is a FIFTH step and exists for exactly one thing: the auto-play control's label, which
   lives inside a 38px control slot matched across three transports. Caption there would break that
   geometry, so it is declared rather than smuggled.
   The wordmark is a LOGOTYPE, not a voice — letter-spaced brand, deliberately outside the scale.
   ============================================================================================ */
/* (this whole block — the four type voices, the tracking, the weights — is declared in tokens.css
   as of 2026-08-01. The player inherits it; it no longer keeps its own copy in step by hand.) */

/* title */
.vp-card .vp-name{ font-size:var(--t-title); }
/* body — the two things the reader acts on */
.vp-card .vp-status, .vp-card #statusText, .vp-card .vp-status .x{ font-size:var(--t-body); }
.vp-card .vp-fromTxt{ font-size:var(--t-body); }
/* caption — everything supporting */
.vp-card .vp-fromLabel,
.vp-card .vp-boxMsg,
.vp-card .vp-tkName,
.vp-card .vp-tkAll,
.vp-card .vp-ava,
.vp-card .vc-pf1,
.vp-card .vc-pf2,
.vp-card .vc-pfcta a{ font-size:var(--t-caption); }
.vp-card .vc-pf1 a{ font-size:var(--t-caption); }   /* the footer wordmark keeps its tracking, at caption size */
/* mono — one family, ONE tracking. The spec was the odd one out at ls 0 (wider than its siblings);
   every mono caption now shares --t-mono-ls, on every surface the spec renders. */
.vp-card .vp-fmt,
.vp-card .vp-tkMeta,
.vp-card .vp-tkNum,
.vp-card .vp-tkDur,
.vp-card .vp-tL, .vp-card .vp-tR{ font-size:var(--t-mono); letter-spacing:var(--t-mono-ls); }
/* micro — the transport's control label only */
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */


/* phones step the WHOLE scale down in one move, exactly as the spacing scale does */
/* (the ≤480 step-down moved to tokens.css with the scale) */

/* item 1: title -> spec takes the spacing scale's block step (the close step up one), so the pair
   breathes the same on every surface where a title and its spec stack. */
.vp-card .vp-fmt{ margin-top:var(--sp-tight); }

/* item 4: the sign-off is TWO lines now, so the close reads as one block rather than a stack with a
   hole where the third line was. Every gap here is a scale step — chip, divider and footer all take
   the close step, and the footer's own two lines sit on it too. */
.vp-card .vp-box{ margin-top:var(--sp-block); }
.vp-card .vp-div{ margin-top:var(--sp-close); }
.vp-card .vc-pfoot{ margin-top:var(--sp-close); }
.vp-card .vc-pf2{ margin-top:var(--sp-close); font-size:var(--t-caption); }


/* ============================================================================================
   DESIGN SESSION — approved values mapped onto the tokens. No element carries a literal size,
   weight or gap: the design moved the SCALE, so the scale moved and the components follow.

   Four of the design's eight sizes are not voices. Sid ruled on each; none became a fifth step:
     15.5 sender name      -> --t-body + weight 600   (Sid's ruling: body, not a fifth step)
     13.5 footer tagline   -> --t-caption             (Sid's ruling: caption)
     14   avatar letter    -> --t-body
     25   play glyph       -> a DECLARED non-text exception, like the wordmark: it is the hero
                             button's glyph, a component dimension alongside the button's own 88px.
   ============================================================================================ */

/* --- the card's own bounds: 24 to the wordmark, 24 under the tagline --- */
/* .st-card.vp-card, because the breakpoint rules that set the old literal padding carry that
   specificity — a plain .vp-card here would lose to them at 390 and the token would go dead. */
.st-card.vp-card{ padding-top:var(--sp-air); padding-bottom:var(--sp-air); }
/* the collection skin's bottom band comes down with the footer it holds — the solo keeps --sp-air.
   --sp-close, not --sp-block: the footer block above it is already on --sp-close, so the band that
   closes the card matches it and the whole close reads as one tightened block rather than a tight
   footer inside a loose card. It is also the last 3px of Sid's "misses fitting by a few pixels" —
   the other 24 were page room the ladder reserved twice (see --card-inset). Both are declared
   steps; neither is a nudge. */
body.has-tracks .st-card.vp-card{ padding-bottom:var(--sp-close); }

/* wordmark -> waveform is ONE gap owned by ONE rule. The head and the well sit in different
   parents, so the head's margin-bottom collapses through on one skin and not the other (18 vs 34).
   Zero the head and let the well own it: both skins then read the same step. */
.vp-card .vp-head{ margin-bottom:0; }
.vp-card .ll-well{ margin-top:var(--sp-block); }

/* --- vertical rhythm, every gap a step --- */
/* wordmark -> waveform: owned by the .ll-well rule below (margin collapse) */
.vp-card .vp-playRow{ margin-top:var(--sp-block); }    /* waveform -> play      18 */
.vp-card .vp-name{ margin-top:var(--sp-block); }       /* play -> title         18 */
.vp-card .vp-fmt{ margin-top:var(--sp-tight); }        /* title -> spec          6 */
.vp-card .vp-actions{ margin-top:var(--sp-air); }      /* spec -> download 28 -> 24 */
.vp-card .vp-status{ margin-top:var(--sp-close); }     /* download -> expiry    12 */
.vp-card .vp-box{ margin-top:var(--sp-air); }          /* expiry -> message box 24 */
/* OWNER SKIN: no footer at all — see mountOwnerControls. The divider goes with it, because the
   divider exists to separate the card from the footer and there is nothing left to separate. */
/* THE EXPLAINER RETURNS, without the line it used to carry (Sid, 2026-08-02). With the footer gone
   the drawer had no words at all, and the sender's own preview is the one face that benefits from
   being told what it is looking at. `.plain` is the no-divider variant: the hairline belonged to
   the old caption and there is nothing above this one to separate it from. Caption voice and a
   declared step of air below it, nothing else. */
.vp-drawer .vp-prevcap.plain{ border-top:none; padding-top:0; }
.vp-drawer .vp-prevcap.plain + .vp-actions{ margin-top:var(--sp-close); }
/* BUTTONS WIN OVER CAPTION, and only on the height where they have to: at 390x664 the drawer cannot
   carry both and still fit fixed, so the caption yields there and nowhere else. The harness asserts
   the yield rather than letting it be a surprise. */
@media (max-width:480px) and (max-height:700px){ .vp-drawer .vp-prevcap.plain{ display:none; } }

body.owner-skin .vp-card .vp-div,
body.owner-skin .vp-card .vc-pfoot{ display:none; }

/* THE FOOTER BLOCK TIGHTENS (Sid, 2026-08-02). Both lines stay — it is the marketing line — but the
   air around and inside it comes down one declared step each: --sp-block -> --sp-close above the
   divider and above the stack, and the gap BETWEEN the two lines from an 11px literal to --sp-tight.
   Tight, not cramped: the lines keep their own line-height and the block keeps its centring. The
   height it frees goes to the phone fit. */
.vp-card .vp-div{ margin-top:var(--sp-close); }        /* box -> divider */
.vp-card .vc-pfoot{ margin-top:var(--sp-close); }      /* divider -> sent via */
/* notice.js injects an 11px literal between the stack's lines at runtime; a .vp-card-prefixed rule
   is what out-ranks it (the same mechanism the rest of this block relies on) */
.vp-card .vc-pf2, .vp-card .vc-pf3{ margin-top:var(--sp-tight); }
.vp-card .vc-pf2{ margin-top:var(--sp-close); }        /* sent via -> tagline   12 */
.vp-card .vp-boxMsg{ margin-top:var(--sp-close); }     /* sender row -> message 12 */

/* --- voices: size from the scale, emphasis from weight and tracking --- */
.vp-card .st-wordmark{ font-size:var(--t-title); font-weight:var(--t-w-med);
                       letter-spacing:var(--t-logo-ls); }
.vp-card .vp-name{ font-size:var(--t-title); font-weight:var(--t-w-bold);
                   letter-spacing:var(--t-title-ls); }
.vp-card .vp-fromTxt{ font-size:var(--t-body); font-weight:var(--t-w-semi);
                      letter-spacing:var(--t-tracking); }
.vp-card .vp-boxMsg{ font-size:var(--t-body); font-weight:var(--t-w-reg);
                     letter-spacing:var(--t-tracking); }
.vp-card .vp-btn{ font-size:var(--t-body); font-weight:var(--t-w-semi);
                  letter-spacing:var(--t-tracking); }
.vp-card .vp-ava{ font-size:var(--t-body); font-weight:var(--t-w-bold); letter-spacing:0; }
.vp-card .vc-pf2, .vp-card .vc-pf2 a{ font-size:var(--t-caption); font-weight:var(--t-w-reg);
                                      letter-spacing:var(--t-tracking); }
/* the spec line keeps the mono FAMILY (the design table assigns no families) and takes the
   caption STEP the design gives it — a voice is a size, the family is separate. */
.vp-card .vp-fmt{ font-size:var(--t-caption); font-weight:var(--t-w-reg);
                  letter-spacing:var(--t-tracking); }
.vp-card .vp-status, .vp-card #statusText{ font-size:var(--t-caption); font-weight:var(--t-w-reg);
                                           letter-spacing:var(--t-tracking); }
.vp-card .vp-status .x{ font-size:var(--t-caption); font-weight:var(--t-w-bold); }  /* "7 days" */
.vp-card .vp-fromLabel{ font-size:var(--t-caption); font-weight:var(--t-w-reg);
                        letter-spacing:var(--t-tracking); }
.vp-card .vc-pf1{ font-size:var(--t-caption); font-weight:var(--t-w-reg);
                  /* was letter-spacing:0 — the one caption in this run that did not take the token,
                     while .vp-fmt, .vp-status and .vp-fromLabel around it all did. Its CHILD <a> is
                     the footer logotype and does carry its own tracking, which is the likely reason
                     the parent got zeroed with it. Plain Inter caption text: it takes the step. */
                  letter-spacing:var(--t-tracking); }
.vp-card .vc-pf1 a{ font-size:var(--t-caption); font-weight:var(--t-w-med);
                    letter-spacing:var(--t-logo-foot-ls); }
.vp-card .vp-tL, .vp-card .vp-tR, .vp-card .st-tL, .vp-card .st-tR{
  font-size:var(--t-mono); font-weight:var(--t-w-med); letter-spacing:var(--t-mono-ls); }
/* SUPERSEDED 2026-08-01 by the inheritance ruling at the end of this file — the tracklist is no
   longer mono at all, so "exempt because Roboto Mono has different metrics" no longer describes it.
   Kept only so the cascade below reads in order; the final block owns these voices. */
.vp-card .vp-tkName{ letter-spacing:var(--t-tracking); }
.vp-card .vp-tkNum, .vp-card .vp-tkDur{ letter-spacing:normal; }


/* ============================================================================================
   DESIGN SESSION — three structural changes. Values come from the token table; nothing here
   sets a size, weight or gap by hand.
   ============================================================================================ */

/* 1. DOWNLOAD is a labelled blue pill, centred, in its own slot between spec and expiry. It
      replaces the small button that used to sit inside the tracklist. Same pill on both skins. */
.vp-card .vp-tkAll{
  width:auto; display:block; margin:var(--sp-air) auto 0;
  padding:11px 22px; border:0; border-radius:999px;
  background:var(--blue); color:#fff; cursor:pointer;
  font-family:var(--ui);
  font-size:var(--t-body); font-weight:var(--t-w-semi); letter-spacing:var(--t-tracking);
}
.vp-card .vp-tkAll:hover{ filter:brightness(1.08); }
.vp-card .vp-tkAll[disabled]{ opacity:.6; cursor:default; }
/* a collection's pill follows the tracklist rather than the spec line, but keeps the SAME slot
   and the same step above it, so the two skins line up. */
body.has-tracks .vp-card .vp-tkAll{ margin:var(--sp-air) auto 0; }

/* 2. the sender box centres: the chip row as one centred unit, the message centred beneath. */
.vp-card .vp-box{ text-align:center; }
.vp-card .vp-boxFrom{ justify-content:center; }
.vp-card .vp-boxMsg{ text-align:center; }

/* 3. the avatar chip inverts — white chip, dark initial. background-COLOR only, so a sender who
      has a real avatar image keeps it (app.js sets background-image on this element). */
.vp-card .vp-ava{ background-color:#fff; color:#1C1C1E; }


/* ============================================================================================
   REFERENCE MOCK — exact values read from the approved player redesign and written here against
   tokens. Read, not imported: no class, rule or markup came from its stylesheet.

   Where the mock and the token table disagreed, the TABLE won (Sid's standing rule):
     · footer tagline   mock 13.5  vs table caption 13     -> 13
     · avatar initial   mock 14    vs table body 14.5      -> 14.5
     · spec -> download mock 28    vs the agreed snap 24   -> 24
   The mock's font-family reads as Helvetica Neue only because the bundle ships no webfont; it is
   a fallback, not a value, so Inter stands.

   Component dimensions (card/box/pill padding and radii) are NOT layout gaps and so are not on
   the 6/12/18/24 spacing scale — the same class as the hero's 88px and the avatar's 24px. The
   card's VERTICAL padding is the one that is a layout gap, and it is --sp-air.
   ============================================================================================ */
.st-card.vp-card{
  padding-left:22px; padding-right:22px;      /* mock: 24px 22px — the 24 is --sp-air above */
  border-radius:24px;                          /* mock: 24 (was 20) */
  background:var(--surface-card);
}
.vp-card .vp-tkAll{
  background:var(--blue);
  padding:7px 24px;                            /* mock exactly */
}
.vp-card .vp-box{
  background:var(--surface-box);
  border-radius:13px;                          /* mock: 13 (was 12) */
  padding:12px 16px 14px;                      /* mock exactly */
}
.vp-card .vp-ava{ background-color:#fff; color:var(--ink-invert); }
.vp-card .vp-fromLabel, .vp-card .vp-status, .vp-card #statusText,
.vp-card .vc-pf2, .vp-card .vc-pf2 a{ color:var(--txt-muted); }
.vp-card .vc-pf1{ color:var(--txt-lead); }


/* ============================================================================================
   SIDE-BY-SIDE PASS — every delta a full computed-style diff found between live and the approved
   mock. The type scale already said "on a token"; it could not say "on the RIGHT token", which is
   what these were.

   MONO RETIRES FROM THE RECIPIENT CARD'S FACE. The spec line and the well's timecodes move to the
   design table's own steps in Inter. The mono FAMILY stays where the mock keeps it: studio and
   panel readouts. --t-mono is therefore a SIZE step (12.5), not a family — the family is set
   separately, and the two surfaces now differ on purpose rather than by drift.
   ============================================================================================ */

/* spec line: Inter at the caption step, the mock's muted grey (was Roboto Mono at white@82%) */
.vp-card .vp-fmt{
  font-family:var(--ui);
  font-size:var(--t-caption); font-weight:var(--t-w-reg);
  letter-spacing:var(--t-tracking); color:var(--txt-muted);
}
.vp-card .vp-fmt .vp-lossless{ color:inherit; }

/* the well's timecodes: Inter at the smallest numeric step, the mock's lead grey */
.vp-card .vp-tL, .vp-card .vp-tR, .vp-card .st-tL, .vp-card .st-tR,
#viewPanel .st-tL, #viewPanel .st-tR, .sv-tL, .sv-tR{
  font-family:var(--ui);
  font-size:var(--t-mono); font-weight:var(--t-w-med);
  letter-spacing:var(--t-tracking); color:var(--txt-lead);
}

/* the pill measured 31px against the mock's 29 — line-height, not size or padding. The mock sets
   it to exactly 1, so the label's own box is its type size and the 7px padding is the whole
   difference. */
.vp-card .vp-tkAll{ line-height:1; }

/* inks the diff caught: the wordmark is not pure white, the quote is not pure white, and the
   expiry value is a deeper red as TEXT than the badge red. */
.vp-card .st-wordmark, .vp-card .vc-pf1 a, .vp-card .vp-name{ color:var(--txt-bright); }
.vp-card .vp-boxMsg{ color:var(--txt-lead); }
.vp-card .vp-status .x, .vp-card #statusText .x{ color:var(--red-ink); font-weight:var(--t-w-bold); }


/* ============================================================================================
   The message box now sits in EQUAL air: the gap above it (from the expiry line) and the gap
   below it (to the divider) are both --sp-block. Measured live they were 24 above and 18 below.
   The pill and the expiry line move DOWN as a group to close that, so the 6px lands above the
   pill (--sp-air -> --sp-wide) and the card's total height is unchanged.
   ============================================================================================ */
.vp-card .vp-tkAll,
body.has-tracks .vp-card .vp-tkAll{ margin-top:var(--sp-wide); }   /* spec / tracklist -> pill */
.vp-card .vp-box{ margin-top:var(--sp-block); }                    /* expiry -> box, now == below */

/* NO-DOWNLOAD variant: the pill is in the DOM but hidden, so without this the expiry line would
   inherit the pill's own 12px and collapse tight against the spec line. It takes the pill's slot. */
.vp-card .vp-tkAll[hidden] + .vp-status{ margin-top:var(--sp-wide); }


/* ============================================================================================
   PLAYER LAYOUT INHERITANCE (Sid's ruling, 2026-08-01). Three changes, both players, both skins.
   This block is LAST in the file on purpose: every earlier section here is a previous design
   session, and this file's recurring trap is a later block quietly overriding an earlier one.

   1. ORDER — title and spec move above the transport (the DOM moved, in index.html; nothing here
      depends on the old order).

   2. RHYTHM, re-measured rather than taken from the brief. Sid's three numbers were well->title 18,
      spec->play 24, play->next 27. Measured on the live studio panel — the surface that took the
      full reference treatment, so it is the one that rules the steps — the preview region runs on a
      SINGLE step: --sp-air between every block (24 desktop / 18 at 390), and --sp-tight between a
      title and its spec. So two of Sid's three numbers are the same declared step read at different
      widths: 18 is --sp-air on a phone, 24 is --sp-air on desktop.
      27 is not a step and appears nowhere in the studio; the neighbours are --sp-air 24 and
      --sp-wide 30. Taken as --sp-wide, because Sid's number is ABOVE 24 and the direction is the
      point: less air above play than below it, so play sits with what it plays and separates from
      what follows. FLAGGED — if 27 was meant as "about 24", this one line moves to --sp-air.
      The void Sid saw came from the old order giving play --sp-air on BOTH sides while carrying an
      88px hero: symmetric air around the biggest object on the card reads as a hole. It is now
      --sp-air above, --sp-wide below, and the title block sits between the well and the play button
      rather than after it.

   3. ONE PRIMARY PER CARD — the download pill demotes to the collection panel's quiet bordered
      treatment (--surface-inset ground, --line-inset edge, --ink-value label), so play is the only
      blue fill on the card. Padding drops 7->6 / 24->23 to absorb the 1px border the fill never
      had, keeping the pill's measured 28.5px box and its width.
   ============================================================================================ */
.vp-card .ll-well{ margin-top:var(--sp-block); }        /* card -> well */
.vp-card .vp-name{ margin-top:var(--sp-block); }        /* well -> title        18 / 14 */
.vp-card .vp-fmt{ margin-top:var(--sp-tight); }         /* title -> spec         6 / 5  */
.vp-card .vp-tkMeta{ margin-top:var(--sp-tight); }      /* title -> meta line, the spec's slot */
.vp-card .vp-playRow{ margin-top:var(--sp-air); }       /* spec -> play         24 / 18 */

/* play -> whatever comes next, on every combination of skin and state: the tracklist on a
   collection, the pill on a single with downloads released, the expiry line when they are not. */
/* ONE RHYTHM DOWN THE COLLECTION CARD (Sid, 2026-08-02). The gaps Sid arrowed were 18 above the
   transport and 30 below it, then 30 to the release row and 18 to everything after — uneven, and
   the unevenness was the slack that made the card scroll. Every one of them is --sp-block now, so
   the transport sits in equal air and the blocks below it read as one rhythm. Measured at 1440x800
   on a six-track collection: 24px recovered and a 19px scroll became none. */
body.has-tracks .vp-card .vp-tracks{ margin:var(--sp-block) 0 0; }
.vp-card .vp-tkAll,
body.has-tracks .vp-card .vp-tkAll{ margin-top:var(--sp-wide); }
.vp-card .vp-tkAll[hidden] + .vp-status{ margin-top:var(--sp-wide); }
/* a single leaves #tracks in the DOM, unhidden and empty. Its margin used to be a stray 2px between
   the spec and the pill; in the new order it would land between play and the pill and push that gap
   off the step. Zero it — an empty window is not a block. */
body:not(.has-tracks) .vp-card .vp-tracks{ margin:0; }

/* the demotion */
.vp-card .vp-tkAll{
  background:var(--surface-inset);
  border:1px solid var(--line-inset);
  color:var(--ink-value);
  box-shadow:none;
  padding:6px 23px;
}
.vp-card .vp-tkAll:hover{ filter:none; border-color:rgba(255,255,255,.28); }
.vp-card .vp-tkAll[disabled]{ opacity:.6; cursor:default; }


/* ============================================================================================
   TRACKING CONSISTENCY WITH THE STUDIO (Sid, 2026-08-01)
   Audited every text-bearing element on both players (both skins), the pin gate, the dead card and
   the live player against the studio list and panel, comparing in EM rather than px — -0.015em is
   0.33px at 22px and 0.195px at 13px, so a px comparison invents drift where there is none.
   The main card was already on the step; the drift was in the states the earlier tracking pass
   never opened. They carry hand-typed pixel values from before the scale existed: the pin title
   (-0.4px), the end-state title (-0.5px), the paste and expiry titles (-0.4/-0.5px), the live
   waiting title (-0.5px), the live source name (-0.2px), the auto-play label (-0.1px, where the
   studio's identical label already takes the token), and a CTA at POSITIVE .01em. Several more —
   the pin sub, the end-state sub, the footer's "sent via" — declared nothing at all and so sat at
   0, outside the scale rather than merely on an old value.
   The sweep is deliberately weak: `.vp-card *` is specificity (0,1,0), so it overrides exactly the
   bare single-class literals that ARE the drift, while every compound rule — the logotypes at
   --t-logo-ls, the mono numerals held at normal — outranks it and survives untouched. That is why
   this is a sweep and not thirty edits: the rules worth keeping defend themselves.
   EXEMPT, and matching the studio element for element:
     · the tracklist numerals and durations (.vp-tkNum/.vp-tkDur, compound, already normal) — the
       compensation exists for Inter's metrics, which is not what Roboto Mono has;
     · the wordmark and footer logotype, which are brand, not a voice;
     · the avatar's single glyph and the PIN digit boxes, where trailing tracking decentres a
       character sitting alone in a box. The studio's own avatar is normal for the same reason.
   NOT changed, and reported rather than silently reconciled: the studio deliberately renders its
   filter row (.sv-fbtn) at 0 and the panel's url at normal, both read off the collection reference;
   and .acmodal h3 carries an identical -.2px in studio.css AND player.css, so the two already
   agree with each other even though they sit off the product step.
   ============================================================================================ */
.vp-card *{ letter-spacing:var(--t-tracking); }
.vp-card .vp-ava, .vp-card .vp-vava, .vp-card .vp-pinbox{ letter-spacing:normal; }
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */



/* ============================================================================================
   PLAYER TYPE INHERITANCE (Sid's ruling, 2026-08-01). This SUPERSEDES the quiet-mono-tracklist
   ruling and the numeral exemptions the tracking pass recorded for these elements.
   The collection player's meta line, tracklist titles, numbers, durations and spec values were
   still Roboto Mono. That is why the spacing read wrong against the studio: it was never a
   tracking difference, it was a FAMILY difference wearing tracking's clothes. Mono sets every
   glyph on the same advance, so a mono readout beside an Inter title cannot be brought into line
   by any letter-spacing value — the fix is the family, and then the tracking follows.
   The model is the studio LIST's expanded sub-rows, not the edit panel's queue: those are the same
   object as this list — a collection's tracks, seen by the person receiving them.
     title   --t-subrow 14.5/400 --txt-bright   (= .sv-subName, held flat at 390)
     num/dur --t-caption 13/400  --txt-muted, letter-spacing NORMAL  (= .sv-subNum/.sv-subDur)
     meta    --t-caption 13/400  --txt-muted
   The numerals keep letter-spacing:normal, but that is now the STUDIO'S NUMERAL RULE applying to
   an Inter numeral column, not an exemption for a different family. They are on-scale, and the
   audit says so.
   ACTIVE ROW IS COLOUR, NOT WEIGHT — it used to add font-weight:600, which reflows a two-line title
   to three the moment you press play. Size and weight are pinned so only the ink changes; this is
   the same rule .sv-subRow.playing already follows in the studio.
   The single player's spec line is already Inter and is untouched, as ruled.
   ============================================================================================ */
.vp-card .vp-tkMeta{ font-family:var(--ui); font-size:var(--t-caption); font-weight:var(--t-w-reg);
  letter-spacing:var(--t-tracking); color:var(--txt-muted); }
.vp-card .vp-tkName{ font-family:var(--ui); font-size:var(--t-subrow); font-weight:var(--t-w-reg);
  letter-spacing:var(--t-tracking); color:var(--txt-bright); }
.vp-card .vp-tkNum, .vp-card .vp-tkDur, .vp-card .vp-tkDur.spec{
  font-family:var(--ui); font-size:var(--t-caption); font-weight:var(--t-w-reg);
  letter-spacing:normal; color:var(--txt-muted); font-variant-numeric:tabular-nums; }
/* the one row carrying real spec data ("48 / 16") keeps the step up in ink it has today — a ruled
   ink, not the .6 white literal it used to be. Still not blue: blue is the interactive tint and a
   readout is not tappable. */
.vp-card .vp-tkDur.spec{ color:var(--ink-quiet); }
/* colour only — size and weight are restated so nothing below can reflow the row on play */
.vp-card .vp-tkRow.on .vp-tkName{ color:#fff;
  font-weight:var(--t-w-reg); font-size:var(--t-subrow); }
.vp-card .vp-tkRow.on .vp-tkNum{ color:var(--blue-text); }
.vp-card .vp-tkRow:hover .vp-tkName{ color:#fff; }
/* RESERVED NUMERAL COLUMN, the studio's rule (Sid, 2026-07-31: keep the reserved width for
   alignment at ten-plus tracks, right-align the numeral). Not cosmetic here — it is a regression
   the family change would otherwise ship. Mono 11px put a digit on a 6.6px advance, so "10" fitted
   the old 15px box; tabular Inter at 13px is an 8.4px advance, so two digits need 16.8px and would
   have clipped the moment a collection reached ten tracks. 1ch IS the digit advance under
   tabular-nums, so the column is exactly as wide as the digits it must hold. */
.vp-card .vp-tkNum{ width:calc(var(--num-ch, 2) * 1ch); text-align:right; }
