/* 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(--ui); 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(--ui); 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 now lives in sendbox.css — the shared file both this page and the
   homepage load — because the homepage's "checking files" pulse needed the same motion and a
   second copy is how two animations drift. Used below exactly as before. */
@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 DELETED (Sid, 2026-08-15): a SECOND frosted-well construction, with its own
   blur and its own scale, on a class no markup carries — zero uses in index.html. It rendered
   nothing, and it was one grep away from being "fixed" by a future pass that assumed it was live. */
.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:1.5px; background:var(--wave-playhead); border-radius:2px; box-shadow:0 0 8px rgba(0,0,0,.6); }
.vp-tL, .vp-tR{ position:absolute; bottom:9px; font-family:var(--ui); 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% */
/* THE REAL GAP, AND THE ONE THAT WAS ACTUALLY WINNING (Sid, 2026-08-16). Yesterday I took
   .vp-boxFrom's margin-BOTTOM to --sp-tight and reported the 8px closed. It was not: .vp-box is
   display:block, adjacent siblings' margins collapse, and this 8 is the larger of the two — so the
   gap never moved and my check passed because it measured the property I had changed rather than
   the gap it was meant to produce. This is the declaration that renders. */
.vp-boxFrom:not([hidden]) + .vp-boxMsg{ margin-top:var(--sp-tight); }

/* 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(--ui); 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(--ui); 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;
  position:relative; }
/* 44pt TAP AREA WITHOUT MOVING THE 30px GLYPH (Sid, 2026-08-16). The avatar is drawn 30 and was
   TAPPED at 30 — under the ruled 44 minimum, a pre-existing accessibility fault rather than a
   consequence of anything recent. This is the same construction the transport already uses, which
   tokens.css states as "tap targets 44 minimum everywhere (transport 44/52 slots)": the glyph keeps
   its drawn size and grows an invisible ::after that the finger lands on. Nothing drawn moves — the
   circle is still 30 at the same coordinates — and because the pad is a pseudo-element rather than
   padding, the flex row's own layout is untouched.
   SQUARE, not round: a 44 circle centred on a 30 glyph leaves its corners to whatever is behind,
   which on this header is the wordmark's right edge. */
.vp-vava::after{ content:''; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  width:44px; height:44px; }

/* 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; }
/* CENTRED AT EVERY WIDTH, NOT JUST THE ONE IT WAS BUILT AT (Sid, 2026-08-15). This was
   `margin:-18px 0 0 14px` against `width:min(384px, calc(100% - 28px))`, and the 14 was only ever
   half the slack while the PERCENTAGE branch won: 100%-28 leaves exactly 14 each side. The moment
   the 384px cap takes over — any card wider than 412 — the slack grows and the margin does not, so
   the drawer sits hard left with the remainder pooled on its right. Measured: symmetric at 393, 402
   and 430; 14 left against 132 right at 834 and 1280.
   `margin:auto` IS NOT THE FIX and was tried and reverted on 2026-08-15: the drawer's parent is
   .sb-stage, not the card, so auto centres it in the STAGE. At phone that coincides (the card is
   alignSelf:stretch and fills the stage's content box, which is why 100%-28 worked at all), but at
   desktop the card is a fixed 530 inside an 1136 stage, so auto put the drawer 376px right of the
   card's left edge — worse than the 14 it replaced.
   THE REAL CAUSE, and it is structural: the drawer takes BOTH its percentage basis and its
   alignment context from the stage, and the card only agrees with the stage at phone widths.
   RULED AND FIXED 2026-08-15: --card-w is the card's width, declared ONCE, and the drawer is that
   width less 28. The two are still siblings the stage left-aligns independently — nothing about
   that changed and nothing here fights it — but they no longer have to agree by coincidence,
   because they are now computing from the same number instead of from two numbers that happened to
   match below 412px of card. The percentage basis is the same for both (the stage's content box is
   the containing block of each), so `min(--card-w, 100%)` is literally the card's own width
   expression, evaluated a second time. Symmetric BY CONSTRUCTION: 14 left is declared, 14 right is
   what is left over, at every width, because the width is derived from the thing it sits under.
   The 384px cap is GONE, not raised — it was the third number in a three-number system and it is
   the one that broke the other two. --card-w bounds the drawer on a wide window now, which is what
   384 was standing in for. */
.vp-drawer{ position:relative; z-index:0; width:calc(min(var(--card-w, 100%), 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 */
/* ---- THE SHARE CONTROL AND ITS MENU (Sid, 2026-08-16) -----------------------------------------
   One button where three used to be. The wrapper is the positioning context so the menu can hang
   under the button without leaving the drawer, and it is what the outside-click handler tests, so a
   tap inside the menu never closes it before the item fires.
   Modelled on the studio's row menu (.sv-rmenu): same rounded panel, same item metrics, same
   quiet hover. It is REBUILT rather than shared because .sv-rmenu lives in studio.css, which the
   recipient player does not load — sendbox.css is the only stylesheet both surfaces have, and a
   two-surface component belongs there rather than being imported sideways. Stated so the next
   person knows this is a deliberate copy and where the original is. */
/* "close" ON A PHONE, "close preview" WHERE THERE IS ROOM (Sid, 2026-08-16). Measured: the full
   words are 102.3px and the left slot is 79 at 375, so the control ran 23.3px under the wordmark —
   `‹ studio` fitted only because it was 56.3. The second word is the part that does not fit, so the
   second word is what goes. "close" alone measures like the link it replaced.
   A width-dependent STRING rather than a width-dependent size, and the difference matters: the slot
   genuinely is a different size at the two widths, so this is the copy fitting the space rather than
   one value drifting into two. */
@media (max-width:480px){ .vp-backLong{ display:none; } }
/* THE CLOSE CONTROL IS "‹ studio"'s TREATMENT WITH A NEW GLYPH AND NEW WORDS (Sid, 2026-08-16).
   It reuses .vp-back entirely — same absolute left slot, same quiet ink, same 13/500 lowercase — so
   nothing new is declared for it. What it adds is the 44pt tap area the old link never had: an
   invisible ::after, the same construction the avatar and the transport use, sized to the control's
   own width so a wide text target is not artificially narrowed. */
.vp-back{ position:relative; }
.vp-back::after{ content:''; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
  width:100%; height:44px; min-width:44px; }
.vp-shareWrap{ position:relative; flex:1; display:flex; }
/* THE MENU HAS TO ESCAPE THE DRAWER'S STACKING CONTEXT (2026-08-16). The card is z-index:1 and the
   drawer z-index:0 — deliberately, so the card's rounded corners sit above the drawer's tucked top
   edge. A child of the drawer therefore cannot paint above the card no matter what z-index it takes,
   because it is inside the drawer's own stacking context. Measured on real Mobile Safari: the menu
   opened, and its three items rendered GHOSTED THROUGH the card, unreadable.
   So the drawer itself is lifted for as long as the menu is open, and put back when it closes. One
   class, set by the same open/close code that owns aria-expanded, so the two cannot disagree. */
.vp-drawer.menuOpen{ z-index:2; }
.vp-shareMenu{ position:fixed; z-index:60; min-width:176px;
  background:#2C2C2E; border:1px solid rgba(255,255,255,.10); border-radius:12px; padding:6px;
  box-shadow:0 18px 40px rgba(0,0,0,.5); display:flex; flex-direction:column; }
.vp-shareMenu button{ appearance:none; background:none; border:0; text-align:left; cursor:pointer;
  color:#fff; font:500 14px var(--ui); padding:11px 12px; border-radius:8px; min-height:44px; }
.vp-shareMenu button:hover{ background:rgba(255,255,255,.07); }
.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) */
/* ONE HAIRLINE, ONE TOKEN (Sid, 2026-08-15). These two did the same job at different weights: this
   one 0.5px at 10% alpha, .vp-box's border-top 1px at 8%. Thinner but denser against thicker but
   fainter, which is why the lower read crisper and the upper lighter. Both are --hairline at 1px
   now — the token already existed in tokens.css and neither was using it. */
.vp-div{ height:1px; background:var(--hairline); 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(--ui); 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 var(--ui); 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 var(--ui); 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(--ui); 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. */
/* THE SCROLL BOUNDARY IS BELOW THE WELL (Sid, 2026-08-15). This rule used to end in overflow-y:auto
   and everything above was right about WHY it had to exist — the card really cannot hold the ruled
   content on a short owner window — and wrong about WHERE. #player starts above the well, so the one
   time the last resort fired it took the waveform, the title and the meta line with it, up under the
   header. Measured on the owner skin at 390x664: 42px over, well from y=84 to y=42, while the page
   (664/664), the stage (664/664) and the card frame (544/544) all correctly refused to move.
   #player is the FIXED HEAD now — it clips and never scrolls. .vp-tail below carries the scroll. */
body.has-tracks #player{ display:flex; flex-direction:column; flex:1 1 auto; min-height:0; overflow:hidden; }
/* the tail is the one region that gives, and the only one that moves */
/* THE TAIL NO LONGER SCROLLS (Sid, 2026-08-16). Yesterday it carried tracks, sender, hairline and
   expiry together and scrolled the lot; Sid split that into a fixed head, a fixed foot and one
   moving list. The tail is now just the flexible middle that holds the tracklist — it gives its
   height, and .vp-tkList inside it does the scrolling, which it has always been built to do. */
body.has-tracks #player > #tail{ flex:0 0 auto; min-height:0; overflow:hidden; }
/* 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 > *, body.has-tracks #tail > *{ flex:0 0 auto; }
body.has-tracks #tail > .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; }
/* THE ROWS MAY NOT GIVE (2026-08-15). .vp-tkList is a flex COLUMN and its rows are flex items, so
   they carried the default flex-shrink:1 — and a capped flex column does not scroll, it SQUASHES.
   Measured before this: `flex: 0 1 auto`, shrink 1, no guard anywhere, while #player > * and
   .vp-card > * have both had one for generations. The list is the element allowed to give; the rows
   inside it never are, or a long collection silently compresses to fit and every flag still reads
   pass. Same guard, same reason, as .st-tkRow in the studio: a declared `height` does NOT stop a
   flex item shrinking — only flex-shrink:0 does.
   NOTE: this is a structural guarantee, not a repair of a reproducing fault. At 2, 5 and 15 tracks
   on this build the rows already render at their true 44/46 because the window quantiser's inline
   max-height caps the list before the flex algorithm has to compress anything. What this removes is
   the possibility — the crush was one layout change away at all times. */
body.has-tracks .vp-tkRow{ flex:0 0 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); }
/* (the title's own margin is RETIRED on a collection, 2026-08-16: the title is a flex item inside
   .vp-metaRow now and the ROW carries that same --sp-block, so this was a second copy of the gap
   living on the child. It also outranked the rule meant to clear it — `body.has-tracks .vp-card
   .vp-name` is 0,3,1 against `.vp-card .vp-metaRow .vp-name` at 0,3,0 — so the title kept an 18px
   top margin, its margin box grew, and align-items:center centred the MARGIN box: the title sat
   exactly 9px, half of 18, below auto-play's centre at every width. Measured 590 vs 581, 452 vs 443,
   383 vs 374. The identical trap the meta line hit yesterday, so deleted rather than out-specified:
   one gap, on the row that owns it.) */
/* (the meta's own margin-top RETIRED 2026-08-15: the meta is a flex item inside .vp-metaRow now and
   the ROW carries that same --sp-tight, so this was a second copy of the gap living on the child.
   It also outranked the rule meant to clear it — `body.has-tracks .vp-card .vp-tkMeta` is 0,3,1 and
   `.vp-card .vp-metaRow .vp-tkMeta` is 0,3,0, so the child kept a 6px top margin, its margin box
   grew the row to 19px and align-items:center then centred the MARGIN box: the caption sat 3px
   below the toggle's centre while every declaration said they were centred together. Deleted rather
   than out-specified — one gap, on the row that owns it.) */
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. */
/* ...and the +2px went the same way as the 37 (Sid, 2026-08-15). It was here for "the list's own
   padding", and the list's padding measures 0 top and 0 bottom — so it was 2px of dead band under
   the last row, the same defect as --tkrow and only smaller. --tk-pad is that padding MEASURED, by
   the same function, so the floor still lands on whole-rows-plus-a-half if padding ever comes back
   and costs nothing while there is none. */
/* THE FLOOR MOVES TO WHERE THE SPACE IS ALLOTTED (Sid, 2026-08-16). It was a min-height on
   .vp-tracks — a box INSIDE #tail, which is overflow:hidden. A min-height cannot win against a
   parent that clips: it only makes the box taller than the visible area. Measured at 375x667 with a
   two-line title, a long message and 15 tracks: the list's box was held at its ruled 154 (3.5 rows)
   while the tail was given 90.6, so it was clipped by 81.4px and ONE row was visible. The bottom
   fade went with it, 81px below the cut, which is why it read as broken rather than scrolled.
   On #tail the floor is a demand the flex chain has to satisfy before it hands the space anywhere
   else, so the rows are visible rather than merely allotted. */
/* + the tracks' own top margin: the list sits --sp-block below the tail's top edge, so a tail sized
   to exactly the floor pushes the last row 18px past its own bottom — measured, the peek came out at
   4px of 44 instead of 22. The demand has to cover what is inside it, not just the rows. */
/* THE GROWN HEIGHT QUANTISES, NOT ONLY THE FLOOR (Sid, 2026-08-16: "the tail lands on exactly
   (k + 0.5) rows and the WELL absorbs the remainder").
   The tail used to be flex:1 1 auto — the column's one greedy child — so its height was
   player minus everything else, a subtraction that is never a multiple of a row. Quantising the
   FLOOR could not help, because the tail was never at its floor: 221.9 against 172 at 375x667,
   291.8 against 216 at 440x734. The peek that resulted was 27.9, 26.2, 9.8 and 20.8 against a 44px
   row, where the scroll model says half a row, every time.
   (The list's own cap was quantised correctly all along — 242, 242, 330, 418 measured — and never
   bound, because it is computed with the cap cleared and the flex chain then hands the list less
   than that: 204, 202, 274, 373. A cap that is never reached is not a rule, it is a coincidence.)
   ONE EXPRESSION, --tk-box, serves both ends: --tk-rows is the measured answer and it FALLS BACK to
   --tk-floor, so the floor is simply the grown height before anything has been measured. There is no
   second number for the two to disagree about, which is what broke this the first time. */
body.has-tracks #player > #tail{
  --tk-box:calc(var(--tkrow) * var(--tk-rows, var(--tk-floor, 4.5)) + var(--tk-pad, 0px) + var(--sp-block));
  height:var(--tk-box); }
/* ...and the box inside stops asking for it separately: one floor, on the element that can enforce it */
body.has-tracks #tail > .vp-tracks{ min-height:0; }
/* AND THE WELL IS WHAT YIELDS (Sid's ruling): on a short phone the waveform's ceremony gives way,
   because the tracks are the point of a collection. It is the only child of #player allowed to
   shrink — the head's title row and the whole foot stay rigid — and it stops at the ladder's own
   declared short-phone tier rather than a number invented here, so the wave stays legible. */
/* 52 is the ladder's SHORTEST declared wave tier (sendbox.css, max-height:620), and it is the right
   floor here rather than the 96 of the 760 tier: measured at 390x664, a 96px floor left the card
   unable to hold head + well + floor + foot, and since the foot is rigid it was the EXPIRY that went
   off the card — the one thing Sid ruled must never go. The well yields further so the foot never
   has to. It only reaches this on the shortest phones; everywhere else the well keeps its own tier. */
/* ...and the well is now the column's elastic child, not merely its shrinkable one: with the tail
   fixed to whole rows, the 0-43px remainder has to land somewhere, and Sid ruled it lands here.
   Its floor is unchanged, and it is the bound that makes k step DOWN rather than the well squash. */
body.has-tracks #player > #well{ flex:1 1 auto; min-height:calc(52px + var(--ll-strip)); }
/* 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. A mask belongs to the
   box, so it stays on the edge.
   (It rode on #player until 2026-08-15 and moved with the scroll boundary — the fade must be on
   whatever actually scrolls, and #player does not any more. Note the old reason "#player has no
   wrapper of its own to hang one on" is now false: .vp-tail IS that wrapper. The mask stays a mask
   anyway, because nothing about the overlay/mask trade changed with it.)
   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. */
/* (RETIRED 2026-08-16 with the tail's scrolling. The fade belongs to whatever actually moves, and
   that is the tracklist now — see .vp-tracks::before/::after, which is the overlay construction this
   mask was replaced by on the list itself. Kept as a comment rather than deleted so the next reader
   finds the reason the tail has no fade rather than assuming one was forgotten.) */
/* AND THE SAME AT THE TOP, because the boundary moved and took the problem with it (2026-08-15).
   Found on real Mobile Safari, not in the engine harness: with the tail scrolled, a track row rides
   up under the meta line and is cut dead against the tail's top edge — the identical clean cut this
   mask exists to forbid, at the end nobody had ever been able to reach before, because until today
   the scroller started above the well and its top edge was the card's own top. It is the SAME
   gradient mirrored and the same 26px: nothing new is designed here, the existing one just gained
   the end it was always missing. .up is "scrolled away from the top", written by the same function
   and on the same scroll event as .more, so the two ends cannot disagree. */

/* 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 #tail > .vp-tracks{ position:relative; }
/* ONE FADE, A WHOLE ROW, AT BOTH ENDS (Sid, 2026-08-16: "if both ends should be a full row, make
   them one declaration so they cannot drift apart again").
   These were two rules with two heights: the bottom was half a row because the quantiser was
   believed to GUARANTEE the last row is cut at exactly half. It does not, any more. The floor moved
   onto #tail, and #tail is flex:1 1 auto — the min-height snaps to (k+0.5) rows but the tail then
   GROWS past that floor to fill the leftover column, and nothing quantises the grown height. Cut
   depths measured on the live site against a 44px row: 27.9 at 375x667, 26.2 at 390x664, 9.8 at
   440x734, 20.8 at 390x844. Where the cut is deeper than the fade is tall, the top of the cut row
   sits OUTSIDE the fade entirely and its glyph tops land at ~37% of the ramp — Sid's fully legible
   half-row, seen in pixels, after a computed opacity of 1 had said the fade was fine.
   A row cannot be cut by more than its own height, so one row of fade covers any cut either end can
   produce, whatever the tail grows to. Height, ground and stops are declared ONCE here; the two ends
   differ only in which edge they pin to. The top end is the SAME BOX FLIPPED (scaleY(-1)) rather
   than a second gradient written backwards, so there is no second set of stops to drift.
   An overlay, not a mask, for the reason it always was: a mask DISSOLVES the ink where an overlay
   COVERS it, and a mask's source is what iOS screenshots drop. rgb(21,22,23) is the list's 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 and .up are written by syncTkFade on the same
   scroll event, so the two ends cannot disagree about when they fire either. */
body.has-tracks #tail > .vp-tracks::before,
body.has-tracks #tail > .vp-tracks::after{ content:''; position:absolute; left:1px; right:1px;
  height:var(--tkrow); pointer-events:none; opacity:0; transition:opacity .12s linear;
  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%); }
/* 0, NOT 1px, at the fading edge. The old rules inset every side by 1px, which left ONE pixel line
   of the cut row sitting BELOW the fade at full strength — measured on the live site at both ends:
   the brightest surviving ink was 233 at 27px into a 27.9px cut, 25 into 26.2, 20 into 20.8, and at
   0px at the top end, always the outermost line, while the row's body washed to 70. A hard line of
   glyph exactly at the cut is the clean cut this whole construction exists to forbid. The sides keep
   their 1px because there is no ramp across them; only the fading edge goes to 0. */
body.has-tracks #tail > .vp-tracks::after{ bottom:0; border-radius:0 0 10px 10px; }
/* flipped, so the radius is written as it RENDERS: these are the card's top corners after scaleY */
body.has-tracks #tail > .vp-tracks::before{ top:0; transform:scaleY(-1); border-radius:0 0 10px 10px; }
body.has-tracks #tail > .vp-tracks.more::after,
body.has-tracks #tail > .vp-tracks.up::before{ 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 */
/* ONE ORDER VALUE FOR ONE JOB (Sid, 2026-08-16). This carried order:-1 while the single card's
   status carried the default 0 — two values doing the same thing, on an element that is supposed to
   be identical on both cards. It is redundant either way: .vp-card .vp-tkAll{ order:1 } already puts
   the pill after the status, so the default puts the expiry left on both. Deleted, not equalised. */
body.has-tracks #relRow > #status{ 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 */
/* (the .solo centring is DELETED, 2026-08-16. It centred the expiry whenever the download button
   was absent, so the foot rearranged itself depending on a per-link setting — an older ruling
   against Sid's newer one, and his wins: the expiry sits hard left in both states. Nothing replaces
   it; .vp-rel is space-between and space-between with one child already leaves it at the start,
   which is exactly the behaviour the single card has always had. app.js still writes the .solo class
   and that is fine — nothing paints from it now, and the class still says which state the row is
   in.) */
.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(--ui);
  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-block); }   /* bottom matches the row's own top gap — see the note below */
/* 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. */
/* EQUALISED 2026-08-16, and this is the fix for a fault that looked like two. The gap ABOVE the
   expiry row is one shared rule — .vp-card .vp-rel{ margin-top:var(--sp-block) } — and it measured
   18 on both cards. The space BELOW was not shared: 18/22 on a single against this card's 12, so one
   rule produced 18-vs-19 on a single (centred) and 18-vs-13 here (5px high). The asymmetry was
   underneath the rule, not in it, so the answer is to equalise the space rather than add a second
   margin — one rule keeps serving both, which is what Sid asked for rather than fixing it twice.
   --sp-block, so the band below the row is the same declared step as the band above it. */
body.has-tracks .st-card.vp-card{ padding-bottom:var(--sp-block); }

/* 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; } }

/* THE OWNER DROPS THE FOOTER, NOT THE HAIRLINE (Sid, 2026-08-15). This rule read
   `.vp-div, .vc-pfoot` because when it was written the divider WAS the footer's — mountOwnerControls
   still says so: "both lines and the divider above them". The divider moved above the expiry row on
   2026-08-15, where it caps the human part and has nothing to do with the footer, so pairing the two
   suppressed a hairline that now does a different job. The owner card lost it and the recipient's
   kept it, which is exactly the mismatch. The footer alone is dropped now.
   Not a guess about making room for the edit bar: the bar is in .vp-drawer, outside the card. */
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. */
/* --t-mono, and FLAT: measured 12.5 on a phone against 13 on desktop, a width-dependent size in a
   scale that is meant to be one step at every width. --t-mono is the declared role for this line —
   "numbers and specs, rate and depth" — and it is the same token the tracklist's own durations use,
   so the card and its rows say a sample rate in one voice. */
.vp-card .vp-fmt{ font-size:var(--t-mono); 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); }
/* the COLOUR is the shared tier's job now (.is-calm/.is-soon/.is-urgent in sendbox.css) — this rule
   kept the expiry ink at every distance, which made a week-away link shout like an hour-away one.
   Weight stays here; only the colour moved. */
.vp-card .vp-status .x, .vp-card #statusText .x{ 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 */

/* THE NO-DOWNLOAD VARIANT'S 30px IS RETIRED (Sid, 2026-08-15). It gave the hidden pill's slot back
   to the expiry when the two were stacked in a COLUMN; that 30, on top of the row's own 16 padding
   and 16 margin, was the empty band in the middle of the card. The two share a ROW now, so a hidden
   pill leaves no slot to reclaim and the expiry simply sits alone on the left. */


/* ============================================================================================
   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-metaRow{ margin-top:var(--sp-tight); }     /* title -> meta line, the spec's slot — on the ROW now */
.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); }
/* 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; }

/* ==================================================================================================
   THE LISTENER PAGE REDESIGN (ex, signed off 2026-08-10 · built 2026-08-14)
   ==================================================================================================
   ⚠ THE SIGNED-OFF r2 BOARD IS NOT IN THIS REPO. design/redesign-2026-08/ carries the studio frames
   only (2a-2q, 3a-3l); the one listener board on this machine is the SUPERSEDED r1·2 — it draws the
   play control floating in a dead middle and the time chips inside the waveform box, which are two of
   the exact faults (ex) overturned. So every number below is cited to (ex)'s own prose or to
   design/redesign-2026-08/tokens.css, and NOTHING is taken from that board except details (ex) left
   standing (--well-h-hero:150, the accent painting played bars on listener surfaces only).

   WHAT WENT, and each was ruled out by name (Sid, 2026-08-14):
     the filled blue play disc with its glow · the red clock on the expiry line ·
     the time chips floating inside the waveform box · the sales line in the footer.
   ================================================================================================== */

/* ---- the card floats ON the picture, it is not pasted onto it (ex) ------------------------------
   93% opacity, so the wallpaper is under the card rather than stopped by it. The studio's card stays
   solid: it is a workspace and its list has to be read against a stable ground. This is scoped to
   .vp-card, which is the listener's own skin of the shared .st-card. */
.vp-card{ background:rgba(28,28,30,.93); }

/* PHONE MARGIN 24, and the picture owns the top half (ex: "Margin: study 2c, floating 24px, every
   phone listener screen. Picture reads at ~52% of the phone screen. 0px (46%) and 12px (49%) were
   drawn and rejected: at those insets the photograph is a border, not a photograph").
   The card is pushed to the BOTTOM of the stage, which is what makes the top half picture — (ez)
   states the phone top-space is effectively the sender's page. .vp-stage is the listener's own
   stage class, so the homepage and the studio keep the shared 14. */
@media (max-width:480px){
  .vp-stage{ padding-left:24px; padding-right:24px; justify-content:flex-end; padding-bottom:24px; }
  .vp-card{ margin-bottom:0; }
}

/* ---- THE WELL IS THE HERO, AND THE TRANSPORT IS WELDED TO IT (ex) ------------------------------
   One object. "A free-floating play control was rejected three rounds running."
   HEIGHTS: --well-h-hero 150 is the listener's own tier (named in deliverable 2 and confirmed by
   (ex): "Waveform well keeps the 150 hero tier"); the phone keeps the shared 120. The strip is the
   52 tokens.css declares ("desktop wave 120 high + 52 strip"), so the well is wave + 52.
   NO ARTWORK SLOT ANYWHERE. There is no sleeve, no placeholder, no square — the waveform is the
   only picture the card carries, and that is the ruling. */
:root{ --well-h-hero:150px; --ll-strip:52px; }
.vp-card .ll-well{ height:calc(var(--well-h-hero) + var(--ll-strip)); position:relative; }
/* THE PHONE TAKES A FIXED 120, NOT var(--well-h) — measured, the token picks up the shared
   short-viewport ladder and the well compacted to 148 at 740 and 104 at 600 while the desktop held
   150 at every height. One ruling cannot mean two things by platform: "the waveform is the hero"
   holds at every tier on both, exactly as the studio's redesigned well does. Whether a hero SHOULD
   give way on a short window is the open question the studio's carries too, and it is not answered
   by accident here. */
@media (max-width:480px){ .vp-card .ll-well{ height:calc(120px + var(--ll-strip)); } }
.vp-card .ll-well canvas{ height:calc(100% - var(--ll-strip)); top:0; bottom:auto; }
.vp-card .vp-playhead{ top:0; height:calc(100% - var(--ll-strip)); }

/* THE SEEK SURFACE EXCLUDES THE STRIP (ex: "The seek target excludes the transport strip, so a tap
   there never scrubs"). The well itself is no longer the target: this pad is, and it stops where the
   strip starts. */
.ll-seek{ position:absolute; left:0; right:0; top:0; height:calc(100% - var(--ll-strip)); cursor:pointer; touch-action:none; z-index:2; }
.vp-card .ll-well{ cursor:default; }

.ll-strip{ position:absolute; left:0; right:0; bottom:0; height:var(--ll-strip); z-index:3;
  display:flex; align-items:center; gap:0; padding:0 2px;
  background:var(--strip-bg, rgba(10,11,14,.55)); }   /* the seam is shared in sendbox.css now — one declaration for all three strips */

/* PLAY IS A BARE WHITE TRIANGLE (Sid, 2026-08-14). No filled circle, no glow — the 88px blue disc
   with its 26px shadow was the loudest thing on a page whose hero is supposed to be the waveform.
   The GLYPH is 22; the TAP TARGET stays large at the 52x44 slot tokens.css declares for a transport
   ("tap targets 44 minimum everywhere (transport 44/52 slots)"). */
.vp-card .ll-strip .vp-hero{
  flex:none; width:52px; height:44px; padding:0; border:0; border-radius:0;
  background:none; box-shadow:none; display:flex; align-items:center; justify-content:center; cursor:pointer; }
.vp-card .ll-strip .vp-hero:hover{ background:none; box-shadow:none; }
.vp-card .ll-strip .vp-hero svg{ width:33px; height:33px; }   /* 33, Sid 2026-08-15: one play size on
   every transport. The slot stays 52x44 — a 33 box clears it by 11px — so nothing grows and no
   waveform height is lost. */
.vp-card .ll-strip .vp-hero svg path, .vp-card .ll-strip .vp-hero svg rect{ fill:#fff; }
.vp-card .ll-strip .vp-skip{ flex:none; width:44px; height:44px; padding:0; border:0; background:none;
  display:flex; align-items:center; justify-content:center; cursor:pointer; }
.ll-mute{ flex:none; width:44px; height:44px; padding:0; border:0; background:none; cursor:pointer;
  display:flex; align-items:center; justify-content:center; }

/* the time lives HERE now, not in a chip floating on the waveform. 12.5/500 at 75% is tokens.css's
   timer voice, and it is the same one the studio strip uses — one transport language. */
/* THE STRIP'S TRACK NAME — the studio's .sv-stripLabel / .sv-stripTitle, taken field for field.
   flex:1 with min-width:0 so it takes the leftover and ellipses rather than pushing the time off;
   overflow:CLIP not hidden, for the reason the studio records: an ellipsised box whose text overflows
   is programmatically scrollable under overflow:hidden, and WebKit scrolls the nearest such ancestor
   to chase a caret. text-overflow:ellipsis applies to clip exactly as it does to hidden. */
.ll-stripLabel{ flex:1; min-width:0; display:flex; align-items:center; }
.ll-stripTitle{ min-width:0; font-size:13px; font-weight:600; color:#fff;
  white-space:nowrap; overflow:clip; text-overflow:ellipsis; }
.ll-time{ margin-left:auto; padding:0 10px 0 12px; white-space:nowrap;
  font-size:12.5px; font-weight:500; color:rgba(255,255,255,.75); letter-spacing:var(--t-tracking); }
.ll-time .vp-tL, .ll-time .vp-tR{ position:static; background:none; padding:0; border-radius:0;
  font-size:12.5px; font-weight:500; color:rgba(255,255,255,.75); }
.ll-sep{ opacity:.55; }

/* the live rail's own controls: a badge, and mute which is NOT play (the live pass cannot be paused
   into silence and resumed in the past — muting is the only honest control) */
.ll-live{ display:inline-flex; align-items:center; gap:6px; margin-left:6px;
  font-size:12.5px; font-weight:500; color:rgba(255,255,255,.75); }
.ll-live .dot{ width:7px; height:7px; border-radius:50%; background:#FF453A; }

/* auto-play is a PREFERENCE, not transport (fc rules it out of every transport), so it sits under
   the well rather than riding in the welded strip. Positioned HERE, not by a wrapper element: the
   wrapper that used to do this job also carried the hidden attribute and broke the control. */
/* ONE LINE: meta left, auto-play right (Sid, 2026-08-15).
   The toggle used to take a whole row of its own beneath the meta. margin-top moves to the ROW so
   the two cannot drift apart vertically, and the toggle's own top margin goes.
   CENTRES, NOT BASELINES (Sid, 2026-08-15). align-items:baseline was the same mistake the studio's
   filter row took three attempts to find: a 16px pill has no text baseline of its own, so the
   browser synthesises one from its bottom margin edge and the row hangs the toggle against the
   caption's baseline instead of its middle. Measured before: meta centre 341.5, toggle centre 338.5
   — 3px out, and out by whatever the caption's descender happens to be, not by a number anyone
   chose. Centres are the only alignment that survives a tall control meeting small text. */
/* THE META ROW IS THE TITLE ROW NOW (Sid, 2026-08-16): the collection's name left, auto-play right,
   centres aligned. The row takes the title's margin so the two sit on one line with nothing pushing
   them apart, and --sp-block is the declared step the title already carried on desktop (the phone's
   16 comes up 2px onto the scale rather than staying a one-off). min-width:0 lets a long collection
   name ellipse instead of shoving auto-play off the edge. */
.vp-card .vp-metaRow{ display:flex; align-items:center; justify-content:space-between; gap:12px;
  margin-top:var(--sp-close); }   /* well -> title row: one step down, the height goes to the list */
/* min-width:0 so a long title ellipses inside the row instead of shoving auto-play off the edge.
   No text-overflow here: the clamp above owns the truncation and does its own ellipsis — declaring
   both would be two rules for one job, and text-overflow is the single-line answer. */
.vp-card .vp-metaRow .vp-name{ margin-top:0; min-width:0; }
.vp-card .vp-metaRow .vp-tkMeta{ margin-top:0; text-align:left; }
.vp-card .vp-auto{ display:flex; align-items:center; justify-content:flex-end; gap:8px; margin-top:0; flex:0 0 auto; }

/* ---- LABEL BESIDE THE TOGGLE, ON THE META LINE (Sid, 2026-08-15) -------------------------------
   The shared control in sendbox.css is a 38px COLUMN with the label absolutely positioned beneath
   the pill, and it is right to be: on the studio's and the live player's TRANSPORTS the label has to
   sit out of flow so that only the pill takes part in the row's vertical centring. On the collection
   card auto-play is not in a transport — it shares the meta line — and that geometry gave it a
   second line of its own, hanging into the 18px gap above the tracklist (measured: label centre
   359.5 against a toggle centre of 338.5, its box ending 0.5px INSIDE the tracklist's top edge).
   So this overrides PLACEMENT only, scoped to the meta row, and touches no field of the component
   itself — the pill, the knob, the tint, the label's type all still come from sendbox.css, once.
   `order:-1` rather than row-reverse: the markup is toggle-then-label because the transports read
   that way top-to-bottom, and the DOM stays the transports'. */
.vp-card .vp-metaRow .vp-auto{ position:static; flex-direction:row; align-items:center;
  width:auto; flex:0 0 auto; gap:var(--sp-tight); }
.vp-card .vp-metaRow .vp-auto > span{ position:static; order:-1;
  top:auto; left:auto; right:auto; transform:none; }

/* ---- the title is the loudest thing on the page (ex: "--t-title: 22 to 30 (phone 20 to 24)") ----
   THE ONE WIDTH-DEPENDENT SIZE LEFT IN THE PRODUCT, and deliberately: it is a per-element decision
   drawn at two sizes, not the ramp step-down that "one scale everywhere" (Sid, 2026-08-14) deleted.
   A 30px title in a 342-wide phone card wraps a real track name to three lines. */
/* ONE STEP, BOTH CARDS (Sid, 2026-08-16). This was 30 on desktop and a one-off 24 on phones, and
   NEITHER was on the type scale — the scale runs 22 / 14.5 / 13 / 12.5 / 11.5 / 8 and has no 18, 24
   or 30 in it. Sid was reaching for 18 because the step he wanted did not exist; --t-title is the
   only real step above the body voice and it is what every other heading in the product already
   uses, so taking it retires two invented numbers rather than adding a third.
   NOT scoped to the collection, deliberately: a title is a title, and two sizes for one element is
   how a third appears later. The single card never had the collection's problem — no list underneath
   — but 30 was shouting with nothing to shout over.
   TWO LINES THEN ELLIPSIS: the -webkit-line-clamp construction already used in four places in this
   product (.vp-tkName here, three more in the studio), taken verbatim. The full title stays in the
   DOM, so nothing is lost to a screen reader. overflow-wrap:anywhere so an unbroken filename-style
   title breaks rather than running off the edge. */
.vp-card .vp-name{ font-size:var(--t-title); font-weight:700; letter-spacing:var(--t-title-ls);
  text-align:left; margin-top:18px;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
  overflow-wrap:anywhere; }
.vp-card .vp-fmt{ text-align:left; font-size:13px; color:var(--ink-secondary,#8E9196); margin-top:6px; }
.vp-card .vp-tkMeta{ text-align:left; }
/* (the phone's 24 is retired with the desktop's 30 — --t-title is one flat step at every width, the
   same ruling --t-helper took on 2026-08-13. The 16px margin goes with it: the title is a flex item
   in .vp-metaRow now and the ROW carries that gap.) */

/* ONE SPINE (ex): "wordmark centred, everything else left, transport aligned to its well." */
.vp-card .vp-status, .vp-card .vp-box, .vp-card .vp-rel{ text-align:left; }

/* ---- expiry: quiet body ink, no icon, no red (ex + Sid, 2026-08-14) ----------------------------
   The voice is "gone in 3 days", final day "gone tonight" — app.js owns the words. The ink is the
   one secondary grey with the value emphasised, which is tokens.css's meta role exactly
   ("meta 12.5px 400 #8E9196; emphasised values 600 #C3C5C9"). The red clock is gone from the markup;
   this makes sure nothing paints red here again. */
.vp-card .vp-status{ display:block; gap:0; font-size:12.5px; font-weight:400;
  color:var(--ink-secondary,#8E9196); margin-top:16px; }
.vp-card .vp-status svg{ display:none; }
.vp-card .vp-status .x, .vp-card .vp-status b, .vp-card .vp-status strong{
  font-weight:600; color:var(--ink-emphasis,#C3C5C9); }

/* ---- the sender's message is a designed element, not a grey box (ex) ---------------------------
   "Restored: from-line with the inverted avatar chip, then the message on its own lines, no
   container, no accent bar." The #2C2C2E panel is gone; what carries it now is the type and a single
   hairline above, which is what a designed element looks like when it is not a box. */
.vp-card .vp-box{ background:none; border:0; border-radius:0; padding:14px 0 0;
  margin-top:20px; border-top:1px solid var(--row-hairline,rgba(255,255,255,.06)); text-align:left; }
/* margin-bottom:0 — the gap below the sender row has exactly one owner now, the rule above. With a
   message that owner is the collapse winner; with NO message this margin was still in force and made
   the band lopsided (13 above the sender line, 18 below it, on both cards). Zeroing it makes the
   no-message band 13/12, which is the same shape the with-message band already had. */
.vp-card .vp-boxFrom{ display:flex; align-items:center; gap:9px; margin-bottom:0; }
.vp-card .vp-fromLabel{ font-size:12.5px; color:var(--ink-secondary,#8E9196); }
.vp-card .vp-ava{ width:24px; height:24px; border-radius:50%; background:#fff; color:#101112;
  display:inline-flex; align-items:center; justify-content:center; font-size:12px; font-weight:600; }
.vp-card .vp-fromTxt{ font-size:12.5px; color:var(--ink-emphasis,#C3C5C9); font-weight:600; }
.vp-card .vp-boxMsg{ font-size:14.5px; line-height:1.5; color:var(--ink-title,#E8E9EA); }

/* ---- lossless, stated plainly as a fact (Sid, 2026-08-14) --------------------------------------
   It is the spec line under the title and it says what it is: "lossless · 48 / 24". No badge, no
   pill, no icon — a fact in the quiet voice, which is where a fact belongs. */
.vp-card .vp-fmt{ font-family:var(--ui),Inter,sans-serif; letter-spacing:var(--t-tracking); }

/* ---- the card hugs its content (ex, the fix that turned r1 around) -----------------------------
   "r1 drew a fixed full-height card, so every state with fewer than about ten tracks had a dead
   middle. That is the exact void we rejected on the competitor's page." Height follows content with
   a declared minimum; the middle scroller only engages once content exceeds the viewport. */
.vp-card{ height:auto; min-height:auto; }
/* min-height:auto, not 0, and the difference is the whole bug (Sid, 2026-09-01: the footer had
   fallen out of the card on a short window). The stage is a COLUMN FLEX box the height of the
   viewport, so the card is a flex ITEM. A flex item's automatic minimum is what stops it being
   squeezed below its own content; min-height:0 removes that guard, and once the window is shorter
   than the card wants to be, the box shrinks while the content does not. With overflow:visible the
   expiry line and the copy link then sit BELOW the rounded background, on the wallpaper. Measured
   on Sid's Windows Chrome at 894x577: box 478.3 against 499 of content, the footer 16.3px outside.
   Reproduced on Mac Chrome and WebKit at the same viewport heights — it was never a Windows fault,
   only a short-window one, which is why every proof at 900px tall passed.
   The card still HUGS its content (height:auto, the r1 fix above): the guard only stops it going
   SMALLER than its content, never bigger.
   Collections keep the old behaviour, because there the shrink is the point: the tracklist is the
   scroller and the card is meant to be capped. That is the rule directly below, and it is the only
   place min-height:0 belongs. */
body.has-tracks .vp-card{ min-height:0; }

/* ==================================================================================================
   THE FIVE, AND THEIR TWO CONSTANTS (Sid, 2026-08-14 — confirmed after measurement)
   ==================================================================================================
   (fb) ships the curated set as FIVE, one of them the DEFAULT so an acetate is never blank. Each
   picture carries TWO constants, not the three (fb) named: the third, a phone focus point, was
   retired — the five use the default centre crop.

     1 ACCENT, pulled from the picture, LISTENER SURFACES ONLY. It paints the played bar and
       nothing else. Every value below is the picture's own measured hue and saturation, lifted in
       LIGHTNESS ONLY until it reaches 3.73:1 on the card — which is exactly the contrast of the
       #2F6BFA it replaces. Parity, not a WCAG floor: the accent does one job and should carry the
       weight the blue carried, and a 4.5 floor made all five louder than what they replace.
     2 SCRIM TIER, light picture or dark picture, and it governs the WELL as well as the page
       (Sid: "the waveform is the hero, so a washed-out well is a failure"). White sand measures 186
       in the phone crop; under the dark tier's .66 the well ground lands at 70 and the waveform
       washes out. The light tier's .82 brings it to 42, level with where ssl sits on the dark tier.

   DARK TIER = what the listener already ran (.sb-scrim .38/.22/.52, well .66).
   LIGHT TIER = the studio's own heavier ramp, which already exists as a ruled value, plus well .82.
   ================================================================================================== */
/* THESE FIVE STAND, and the boards do not overrule them (Sid, 2026-08-14). tokens-listener.css
   carries --accent-curated #C09055 and --accent-uploaded #8FA6C8; both PREDATE these and were drawn
   against the old listen-bg-mobile/desktop.jpg, which is the picture every r2 frame actually loads.
   These five were measured off the five pictures that ship. Likewise the board README's "each of the
   ten wallpapers stores accent + luminance key + phone focus point": there are FIVE, and there are
   NO focus points — all five take the default centre crop, ruled the same day. */
.wall-turntable{ --accent:var(--accent-turntable); }   /* the neon behind the deck · hue 351, 14.7% of frame · DEFAULT */
.wall-moog     { --accent:var(--accent-moog); }   /* the patch cables · hue 4 */
.wall-ssl      { --accent:var(--accent-ssl); }   /* the desk and its backlights · hue 23, 38.4% of frame */
.wall-blacksand{ --accent:var(--accent-blacksand); }   /* a MONOCHROME accent for a monochrome picture: the file is
                                          literally greyscale, R=G=B in every pixel, so there is no
                                          hue to pull. Ruled honest rather than borrowing the blue,
                                          which would have broken the accent-from-the-picture rule
                                          for one picture (Sid, 2026-08-14). */
.wall-whitesand{ --accent:var(--accent-whitesand); }   /* the warm cast of the frame · hue 32. Thin at 0.01%, and it
                                          stands: the accent only paints the played bar. */

/* the light tier, which one picture needs. THE WELL'S SHARE OF IT IS GONE (Sid, 2026-08-15): the
   tier only ever existed because the wallpaper showed through the well, and the well is painted
   opaque now, so there is nothing for a per-picture scrim to correct. The PAGE scrim stays - that
   one is about the photograph behind the card, which is still a photograph. This also means a
   custom wallpaper upload needs no well machinery at all, rather than having to derive a correct
   tier automatically from a stranger's photo. */
.wall-whitesand .sb-scrim{ background:linear-gradient(180deg,rgba(8,6,6,.72),rgba(8,6,6,.66) 45%,rgba(8,6,6,.8)); }

/* the wallpaper itself, per picture, both shapes. The DEFAULT is turntable: the page the recipient
   opens must never be blank, and turntable continues the register the listener already had — a dark
   room, a warm practical light, a LISTENING picture rather than a making one. */
:root{ --wall-img:url("/l/_assets/listen-bg-turntable-desktop.jpg"); }
.wall-turntable{ --wall-img:url("/l/_assets/listen-bg-turntable-desktop.jpg"); }
.wall-moog     { --wall-img:url("/l/_assets/listen-bg-moog-desktop.jpg"); }
.wall-ssl      { --wall-img:url("/l/_assets/listen-bg-ssl-desktop.jpg"); }
.wall-blacksand{ --wall-img:url("/l/_assets/listen-bg-blacksand-desktop.jpg"); }
.wall-whitesand{ --wall-img:url("/l/_assets/listen-bg-whitesand-desktop.jpg"); }
@media (max-width:720px){
  :root{ --wall-img:url("/l/_assets/listen-bg-turntable-mobile.jpg"); }
  .wall-turntable{ --wall-img:url("/l/_assets/listen-bg-turntable-mobile.jpg"); }
  .wall-moog     { --wall-img:url("/l/_assets/listen-bg-moog-mobile.jpg"); }
  .wall-ssl      { --wall-img:url("/l/_assets/listen-bg-ssl-mobile.jpg"); }
  .wall-blacksand{ --wall-img:url("/l/_assets/listen-bg-blacksand-mobile.jpg"); }
  .wall-whitesand{ --wall-img:url("/l/_assets/listen-bg-whitesand-mobile.jpg"); }
}

/* THE PLAYED BAR READS THE ACCENT, AND ONLY THE LISTENER'S DOES (ex/deliverable 2: "played bars read
   --accent on listener surfaces only"). wave.js already takes its played colour from --wave-played
   on the canvas, defaulting to the brand blue — so this is the whole wiring, and the studio's own
   waveform is untouched because nothing sets --accent there. */
.vp-card #wave{ --wave-played:var(--accent,#2F6BFA); }

/* the live rail's one plain line (Sid, 2026-08-14) — body voice, the one secondary grey, no icon */
.ll-liveNote{ margin-top:12px; text-align:center; font-size:12.5px; line-height:1.5;
  color:var(--ink-secondary,#8E9196); letter-spacing:var(--t-tracking); }

/* ---- three corrections, each caught by measuring the built page rather than reading the rules ----
   1. THE 93% LOST TO A LATER RULE. .vp-card{background:rgba(...)} measured back as solid
      rgb(26,27,29): sendbox's shared .st-card ground is declared after it and at equal weight, so
      order decided. Two classes, and the card floats as ruled.
   2. THE HERO KEPT ITS GLOW ON A COLLECTION, and its 88px disc on desktop. Both come from rules
      further down this file that never knew a transport strip would exist; measured 88x88 at 1440
      and a rgba(63,111,214,.45) shadow at every width on the collection skin. A bare white triangle
      is the ruling, so the strip's own play wins outright wherever it lives.
   3. THE WAVEFORM STOPPED BEING THE HERO ON A COLLECTION. .st-card.has-list compacts the shared
      well to --well-h-tight 84, which is right for the studio (there, the list is the point) and
      wrong here: measured, it left 32px of waveform under a 52px strip. On the LISTENER the well
      keeps its hero tier on both skins, because "the waveform is the hero" is the ruling and a
      tracklist does not outrank it. */
.vp-card.vp-card{ background:rgba(28,28,30,.93); }
.vp-card .ll-strip .vp-hero.vp-hero{
  width:52px; height:44px; border-radius:0; background:none; box-shadow:none; }
@media (min-width:481px){ .vp-card .ll-strip .vp-hero.vp-hero{ width:52px; height:44px; } }
.st-card.has-list .vp-card .ll-well, .vp-card.has-list .ll-well{
  height:calc(var(--well-h-hero) + var(--ll-strip)); }
@media (max-width:480px){
  .st-card.has-list .vp-card .ll-well, .vp-card.has-list .ll-well{
    height:calc(120px + var(--ll-strip)); }
}

/* AND ITS min-width/min-height, which is what actually held the 88. Line ~228 sets the shared
   transport hero with --hero AND floors it with min-width/min-height, so overriding width alone
   left the floor in place and the desktop play measured back at 88x88 inside a 52-wide slot. */
.vp-card .ll-strip .vp-hero.vp-hero{ min-width:52px; min-height:44px; }

/* ==================================================================================================
   THE r2 BOARDS LANDED (commit 7e3acf2 · measured 2026-08-14)
   ==================================================================================================
   Everything above in the listener block was built from (ex)'s prose with a warning that the boards
   were absent. They are here now: design/redesign-2026-08/listener-phone.html (2c-2f),
   listener-desktop.html (2g-2h), tokens-listener.css. Every number below was MEASURED off those
   frames in a browser, not read from a comment — the token file's own header says the drawn pixels
   are the source of truth, and twice in this pass they disagreed with its prose.

   The frames, measured (box relative to frame, Inter confirmed loading):
     2c one track   390x844  card l24 t301.8 342x514.2 b28   well 304x235 = 180 wave + 53 + 2
     2d twelve      390x844  card l24 t24    342x796   b24   well 304x195 = 140 wave + 53 + 2
     2e emptied     390x844  card l24 t439.2 342x376.8 b28   well 304x66  = 64 flatline, NO strip
     2f live        390x844  card l24 t283   342x533   b28   well 304x219 = 160 wave + 57 + 2
     2g desk one   1440x900  card l72 t234.6 530x601.4 b64   well 480x275 = 220 wave + 53 + 2
     2h desk twelve 1440x900 card l72 t35    530x830   b35   well 480x205 = 150 wave + 53 + 2

   WHY THE STRIP STAYS 52 AND IS NOT RE-POINTED AT THE DRAWN 53: the boards carry no box-sizing
   reset, so their 52-high strip DRAWS 53 with the 1px weld hairline added outside it. This build
   loads sendbox.css's `*{box-sizing:border-box}`, so 52 already contains the hairline. 52 is the
   ruled number (tokens-listener.css "transport row 52 high (live 56)") and measures back as 52 here.
   The same reasoning makes each well = wave + 52, not wave + 53.
   ================================================================================================== */

/* ---- THE WELL'S FOUR TIERS. r1's 150/120 is overturned by name: tokens-listener.css says
   "--well-h-hero: 220 (phone 180), replacing r1's 150". Hero and collection are two different ruled
   heights, which the build had aliased to one token — that is why a twelve-track page and a one-track
   page drew the same well. Desktop collection 150 is the single value that survives unchanged. */
:root{ --well-h-hero:220px; --well-h-coll:150px; --ll-strip:52px; }
.vp-card .ll-well{ height:calc(var(--well-h-hero) + var(--ll-strip)); }
.st-card.has-list .vp-card .ll-well, .vp-card.has-list .ll-well{
  height:calc(var(--well-h-coll) + var(--ll-strip)); }
@media (max-width:480px){
  :root{ --well-h-hero:180px; --well-h-coll:140px; }
  .vp-card .ll-well{ height:calc(var(--well-h-hero) + var(--ll-strip)); }
  .st-card.has-list .vp-card .ll-well, .vp-card.has-list .ll-well{
    height:calc(var(--well-h-coll) + var(--ll-strip)); }
}

/* ---- THE CARD IS ANCHORED LOW, AND THAT IS WHAT MAKES THE PICTURE A PICTURE -------------------
   The held item. 2c/2e/2f all draw the card hugging the BOTTOM (b28) with the picture owning what
   the hug leaves above it; 2g draws it low-LEFT (l72, b64). The previous pass wrote
   `justify-content:flex-end` onto .vp-stage, which is a plain block — the declaration was inert and
   the card stayed at the top. It needs to be a flex column.
   margin-top:auto, NOT justify-content:flex-end: an auto margin collapses to 0 once the content
   overflows, so the top of a tall card stays scroll-reachable. justify-content:flex-end makes
   overflow unreachable, which is the exact trap the note at the head of this file warns about. */
.vp-stage{ display:flex; flex-direction:column; align-items:flex-start; }
.vp-stage > .vp-card{ margin-top:auto; }

/* desktop: 2g/2h — left 72, card 530 wide (the well is the 480 tokens-listener.css calls the card),
   bottom 64 when hugging. 2h's full card leaves 35 top and bottom. */
@media (min-width:481px){
  .vp-stage{ padding-left:72px; padding-right:72px; padding-bottom:64px; padding-top:35px; }
  /* --card-w IS THE CARD'S WIDTH, AND THE ONLY COPY OF IT. The drawer under the card reads the same
     token (see .vp-drawer), so the bar cannot drift away from the card it belongs to again — which
     is exactly what happened while the drawer carried its own 384px. On :root rather than on the
     card, because the drawer is the card's SIBLING and would never inherit it from there. */
  :root{ --card-w:530px; }
  .vp-card.vp-card{ width:var(--card-w); max-width:100%; padding:26px 24px 22px; }
}
/* phone: 2c ruling, floating 24 each side on EVERY phone screen; 28 at the foot when the card hugs,
   24 when it is full (2d). The card stretches to the stage, which is what draws it 342 at 390. */
@media (max-width:480px){
  .vp-stage{ padding:24px 24px 28px; }
  .vp-stage > .vp-card{ align-self:stretch; }
  .st-card.vp-card{ width:auto; padding:20px 18px 18px; }
}

/* ---- THE CARD'S OWN GROUND. tokens-listener.css --listener-card is rgba(26,27,29,.93); the build
   had rgba(28,28,30,.93), which is the STUDIO's #1C1C1E carried across. The listener card is its own
   surface and the token file says so in as many words. */
.vp-card.vp-card{ background:rgba(26,27,29,.93); }

/* ---- TYPE, at the sizes the frames draw ------------------------------------------------------
   Each of these was built from prose at one size and is drawn at two. */
.vp-card .vp-name{ color:#FFFFFF; letter-spacing:-.015em; }          /* the one pure-white element */
.vp-card .vp-status{ font-size:14.5px; color:rgba(255,255,255,.6); } /* expiry: its own quiet body ink */
.vp-card .vp-status .x, .vp-card .vp-status b, .vp-card .vp-status strong,
.vp-card #statusText .x{ font-weight:600; color:#C3C5C9; }
/* TWO LINES, FULL TEXT IN THE DOM (Sid, 2026-08-16). The tracks are the point of a collection and
   the message is a note attached to it, so the message is what yields — not the list. Deterministic
   rather than a scroller: it keeps the foot a fixed height, which is what makes the list's floor
   arithmetic predictable, and it adds no third scroller to a card ruled to have one.
   Same -webkit-line-clamp construction as the card title, the tracklist name and three in the
   studio — six places now, one idea. Nothing is lost: the whole message stays in the DOM for a
   screen reader and for anyone selecting it. */
.vp-card .vp-boxMsg{ font-size:14.5px; line-height:1.6; color:#F3F4F5; text-align:left;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
  overflow-wrap:anywhere; }
.vp-card .vp-boxFrom{ justify-content:flex-start; }                  /* the boards draw it LEFT */
.vp-card .vp-fromLabel, .vp-card .vp-fromTxt{ font-size:13px; }
.vp-card .vp-ava{ font-size:11px; font-weight:600; }
.vp-card .vp-fmt{ font-size:13px; }
.vp-card .st-wordmark{ font-size:22.5px; font-weight:200; }
@media (max-width:480px){
  .vp-card .vp-status{ font-size:13.5px; }
  .vp-card .vp-boxMsg{ font-size:13.5px; }
  .vp-card .vp-fromLabel, .vp-card .vp-fromTxt{ font-size:12px; }
  .vp-card .vp-fmt{ font-size:12.5px; }
  .vp-card .st-wordmark{ font-size:19px; }
}

/* ---- SCRIMS, at the declared tiers. The desktop tier is lighter above the fold (where the subject
   of the photograph lives) and heavier under the card — it is a different shape from the phone's,
   not a darker version of it. */
@media (min-width:481px){
  .sb-scrim{ background:linear-gradient(180deg, rgba(8,6,6,.18), rgba(8,6,6,.12) 45%, rgba(8,6,6,.55)); }
  .ll-strip{ background:rgba(10,11,14,.5); }
}
@media (max-width:480px){
  /* THE STUDIO'S PHONE ANSWER, TAKEN WHOLE (2026-08-16). Measured on this surface: the controls take
     227.3px of a 304px strip at 390, leaving 72.7px against a 141.3px track name. The studio hit the
     same wall and solved it by giving the label a full-width line of its OWN above the controls
     rather than shortening anything — .sv-stripLabel{ order:-1; flex:0 0 100% }, and its strip grows.
     This is that rule on the listener's strip. The well's height is fixed by tier, so what gives is
     the waveform's own height, not the card's: the cost is contained to the well. */
  /* THE TOKEN IS THE STRIP'S HEIGHT, AND THREE THINGS SUBTRACT IT (fixed 2026-08-16). The wave, the
     playhead and the seek pad are all calc(100% - var(--ll-strip)) — one token, three readers. When
     this rule gave the strip a wrapped `height:auto` it grew to 68 while the token still said 52, so
     the wave kept its old 52 subtraction and the strip's extra 16px rode UP OVER the waveform. The
     label then painted on the wave through a 55%-opaque background, which is Sid's "nothing renders
     on top of a waveform" broken by the very rule that gave the label its own line.
     So the token moves instead: the strip IS 68 here, the wave subtracts 68, and the well grows by
     the same 16 so the waveform keeps its ruled 120. Nothing can overlap because nothing is guessing
     at the other's height any more. */
  /* THE TALLER STRIP IS EARNED, NOT ASSUMED (Sid, 2026-08-16). This raised the token to 68 for every
     phone surface, so a strip with no label carried 23px of dead band under its controls — on the
     SINGLE card, which never shows a track name at all, and on a collection AT REST before anything
     has loaded. The extra line exists to hold the label, so it arrives with the label and leaves
     with it.
     The token lives on the CARD rather than :root because everything that must agree with it — the
     wave, the playhead, the seek pad and the strip — reads it from there, so one class moves all
     four together. app.js sets .stripTall in the same breath as stripLabel.hidden, so the height and
     the thing it makes room for cannot disagree. */
  :root{ --ll-strip:52px; }
  .vp-card.stripTall{ --ll-strip:68px; }
  .ll-strip{ flex-wrap:wrap; align-content:flex-start; }
  .ll-stripLabel{ order:-1; flex:0 0 100%; width:100%; padding:7px 12px 0; }
  .ll-strip{ background:rgba(10,11,14,.55); }
}

/* ---- the live rail's two small drawn facts: the dot is 6, and mute is a bordered circle, which is
   what stops it reading as another bare glyph. (The rest of the live face is NOT built — see the
   report; 2f rules a welded strip with pause + mute + badge + clock and this build still draws a
   free-standing row.) */
.ll-live .dot{ width:6px; height:6px; }
.ll-mute{ border:1px solid rgba(255,255,255,.18); border-radius:50%; }

/* ---- THE ORDER OF THE CLOSE, AND WHICH ROW OWNS THE HAIRLINE ---------------------------------
   Measured off 2c, where the card's foot reads: title 613 · meta 646 · from-line 675 · message 707
   · expiry 766. The build had the expiry ABOVE the from-line, and had put the hairline on the
   message. Both are the wrong way round: tokens-listener.css gives the rule to the expiry
   ("expiry row border-top rgba(255,255,255,.08), padding-top 10-16") and describes the message as
   "from-line, then the message on its own lines, NO container" — a container is what a rule of its
   own makes it. So the message closes the human part of the card, and the expiry is the fact ruled
   off underneath it.
   Done with `order` rather than by moving the markup: .vp-rel and .vp-box are shared with the
   sender/owner skin, which these boards do not rule. Items with no order default to 0 and keep
   their source position ahead of these. */
.vp-card{ display:flex; flex-direction:column; }
.vp-card > *{ flex:0 0 auto; }
/* THE ORDER NEVER APPLIED ON A SINGLE (Sid, 2026-08-15). These rules select .vp-card's DESCENDANTS,
   but #box and #relRow are children of #player, and #player was only display:flex under
   body.has-tracks — a collection. On a single it stayed a block box, `order` was inert, and the DOM
   order won: expiry above the sender, which is the reverse of what the boards ruled. The band in
   the middle was the same bug's other half (see the retired --sp-wide rule below).
   #player is a flex column on both skins now, so the ruled order actually renders. Nothing else
   about it changes: has-tracks still adds its own flex:1 1 auto + scrolling for the tracklist.
   Margins here are all margin-top with no margin-bottom, so nothing was relying on collapsing. */
#player{ display:flex; flex-direction:column; }
#player > *{ flex:0 0 auto; }
/* THE TAIL IS A FLEX COLUMN ON BOTH SKINS, and it has to be: the four `order` values below are what
   render the ruled bottom stack, and they are only meaningful among siblings of a flex container.
   Those four sit in .vp-tail now rather than in #player, so without this the single skin would fall
   back to DOM order and put the expiry above the sender — the exact bug the block above records
   being fixed once already. Scrolling is NOT declared here: on a collection body.has-tracks adds it,
   and a single has nothing long enough to need it. */
.vp-tail{ display:flex; flex-direction:column; }
.vp-tail > *{ flex:0 0 auto; }
.vp-card .vp-box{ order:1; }
.vp-card .vp-rel{ order:2; }
/* THE HAIRLINE MOVES UP ONE ROW (Sid, 2026-08-15). It sat below the expiry, closing off the bottom
   of the card and separating nothing. Above the expiry it caps the human part - sender and message -
   and marks where the practical part starts. This moves the SAME element rather than deleting one
   rule and adding a border somewhere else, so there is still exactly one hairline object and one
   place that styles it. box 1, hairline 2, expiry row 3, footer 4. */
.vp-card .vp-div{ order:2; }
.vp-card .vc-pfoot{ order:4; }
/* THE HAIRLINE MOVED WITH THE BLOCK IT INTRODUCES. The ruled stack is well, title, format,
   hairline, sender, message, then the expiry row — so the rule belongs to the top of the human
   part (.vp-box), not to the expiry row it used to sit on. */
/* ONE STEP TIGHTER, AND ON THE SCALE (Sid, 2026-08-15): the band around this hairline was 16 above
   and 16 below — a number that is on no step of --sp-tight/close/block/air/wide, written twice. It
   is --sp-close both sides now, which is the band .vp-div already carries above it, so the comment
   below is finally true of the spacing as well as the colour. 32px of band becomes 24. */
.vp-card .vp-box{ border-top:1px solid var(--hairline); padding-top:var(--sp-tight); margin-top:var(--sp-close); }
/* list -> "sent by" was 25 (12 margin + 1 hairline + 12 padding). The padding comes down a step to
   --sp-tight, so the band under the hairline matches the one the sender row already carries below
   it, and the 6 freed goes to the list. */   /* same token, same weight as .vp-div */
/* THE BOTTOM ROW IS ONE LINE: expiry LEFT, download RIGHT. When the pill is hidden the row is the
   expiry alone, hard left, with nothing holding the right-hand slot open — space-between on a
   single child puts it at the start, so no placeholder and no gap. */
.vp-card .vp-rel{ order:3; border-top:0; padding-top:0; margin-top:var(--sp-block);
  display:flex; flex-direction:row; align-items:center; justify-content:space-between; gap:12px; }
.vp-card .vp-status{ margin-top:0; text-align:left; }
/* EXPIRY LEFT, DOWNLOAD RIGHT (Sid, 2026-08-15). The markup has the pill first, because the DOM
   order is the solo skin's and the pill was ruled to come first when the two were stacked. In the
   row that puts the download on the left and the expiry on the right, which is the wrong way round.
   Flipped with `order` on the pill rather than by moving the markup, for the reason stated above
   this block: .vp-rel is shared with the sender/owner skin, which these boards do not rule.
   order:1 against the status's default 0 — nothing else in the row needs an opinion. */
.vp-card .vp-tkAll{ margin:0; order:1; }   /* margin:0, not margin-top:0 — the pill's base rule is
   `margin:10px auto 0`, and those AUTO side margins eat the row's leftover space and hold it 45px
   off the right edge. Clearing only the top left the centring in place. */

/* ==================================================================================================
   FOUR RULINGS, 2026-08-14 (Sid, after the r2 audit)
   ================================================================================================== */

/* ---- 1. AUTO-PLAY LEAVES THE LISTENER ---------------------------------------------------------
   2d and 2h draw no auto-play control anywhere on the card, and the ruling is that the listener
   carries no controls beyond play, pause and scrub. It is not moved, it is gone: this is a
   recipient's page, and a preference belongs to the person who owns the collection, not the person
   who was sent it. The studio keeps its own (fc: right end of the filter row) — untouched, because
   nothing here is scoped outside .vp-card. */
/* REVERSED (Sid, 2026-08-14, same day). Removing the control was the wrong reading of "the listener
   carries no controls beyond play, pause and scrub": auto-play is not a transport control, it is the
   preference that governs whether a collection plays through — and it stays ON by default, because a
   collection should. Taking the switch away left the BEHAVIOUR running with no way for the recipient
   to stop it, which is the thing actually worth avoiding. The boards not drawing it is a GAP IN THE
   BOARDS, recorded as such in BUILD-NOTES; 2d and 2h simply do not show this control, and absence in
   a drawing is not a ruling. So it sits where it always sat, under the well and out of the strip. */

/* ---- 2. TRACK TITLES KEEP THEIR CAPITALS ------------------------------------------------------
   The lowercase is OUR voice, not the sender's. .vp-name was already exempt, so a single track has
   always been shown as typed while the same title inside a collection was mangled — "Send It" read
   "send it" in the list and "Send It" above it. Mangling someone's title is the same fault as
   showing them a file extension. 2d and 2h draw every row as typed. */
.vp-card .vp-tkName{ text-transform:none; }

/* ---- 3. THE PHONE CROP IS PLAIN CENTRE --------------------------------------------------------
   Every phone frame draws the wallpaper `center/cover`. The build carried object-position:50% 12%,
   which is a focus point by another name — and the ruling is that the five have none. */
@media (max-width:720px){ .sb-wall img{ object-position:50% 50%; } }

/* ---- 4. THE EMPTIED COLLECTION IS A CARD FACE, NOT A DEAD CARD (2e) ---------------------------
   Measured: card 342x376.8 at t439.2 (b28, the same hug as every other phone face), well 304x66 —
   a 64 interior and NO transport strip — with a 1px rule at rgba(255,255,255,.22) inset 8 each
   side, across the middle. The card keeps its title, from-line, message and expiry; only the well
   and the two lines under the title change. */
body.empty-coll .vp-card .ll-well{ height:64px; }
body.empty-coll .ll-strip{ display:none; }
body.empty-coll .ll-seek{ display:none; }          /* nothing to scrub */
body.empty-coll .vp-card .ll-well canvas{ display:none; }
body.empty-coll .vp-card .ll-well::after{
  content:""; position:absolute; left:8px; right:8px; top:50%; height:1px;
  background:rgba(255,255,255,.22); z-index:4; }
/* the note under the meta line: the helper voice tokens-listener.css names
   ("helper/status 12.5-13.5px 400 rgba(255,255,255,.55)"), left like everything but the wordmark */
.vp-emptyNote{ margin-top:10px; text-align:left; font-size:13.5px; line-height:1.6;
  color:rgba(255,255,255,.55); letter-spacing:var(--t-tracking); }

/* ==================================================================================================
   THE TRACKLIST, AGAINST 2d AND 2h (measured 2026-08-14)
   ==================================================================================================
   Drawn, phone 2d / desktop 2h:
     LIST  transparent · radius 0 · padding 0 · no shadow · full card width (304 / 480)
     ROW   grid 20px 1fr auto (22 / 1fr / auto) · gap 12 (14) · height 44 (46)
           border-top 1px rgba(255,255,255,.06), and the FIRST row carries none
     cells numeral 20 (22) wide, 12.5/400 #8E9196, right-aligned
           name    14 (14.5) /400 #E8E9EA
           duration 12.5/500 rgba(255,255,255,.6) — on EVERY row, the playing one included
     PLAYING  name 600 #FFFFFF; the numeral is REPLACED by a 13x13 three-bar glyph in the accent,
              right-aligned in its column exactly where the numeral was.
   The build had none of this: a flex row with one 9px gap, inside a recessed window with its own
   ground, an inset shadow and a 15px scroll lane. The board draws the rows ON the card. */

/* the window goes — the rows sit on the card, which is why the hairlines are all that separate them */
body.has-tracks .vp-card .vp-tkList{
  border-radius:0; padding:0; background:none; box-shadow:none; }

/* the row is a grid, not a flex line. The fourth column appears only when this link's per-row
   download arrows are on — the boards draw three columns because their download affordance is the
   pill at the card foot, and that pill is the deferred downloads pass, not this one. So the arrow
   keeps its current behaviour and takes a column of its own rather than wrapping the row. */
.vp-card .vp-tkRow{
  display:grid; grid-template-columns:20px 1fr auto; gap:12px; align-items:center;
  height:44px; padding:0; border-bottom:0;
  border-top:1px solid rgba(255,255,255,.06); }
.vp-card .vp-tkRow:has(.vp-tkDl){ grid-template-columns:20px 1fr auto auto; }
.vp-card .vp-tkList > .vp-tkRow:first-child{ border-top:0; }
body.has-tracks .vp-card .vp-tkRow{ border-bottom:0; }   /* the old rule drew it underneath */

.vp-card .vp-tkNum{ width:auto; font-size:12.5px; font-weight:400; color:#8E9196; text-align:right; }
.vp-card .vp-tkName{ font-size:14px; font-weight:400; color:#E8E9EA; }
.vp-card .vp-tkDur, .vp-card .vp-tkDur.spec{
  font-size:12.5px; font-weight:500; color:rgba(255,255,255,.6); }

@media (min-width:481px){
  .vp-card .vp-tkRow{ grid-template-columns:22px 1fr auto; gap:14px; height:46px; }
  .vp-card .vp-tkRow:has(.vp-tkDl){ grid-template-columns:22px 1fr auto auto; }
  .vp-card .vp-tkName{ font-size:14.5px; }
}

/* THE PLAYING ROW. The blue numeral goes: blue is the interactive colour and this is a state, not a
   control — and the accent is the one colour on the card that belongs to this acetate's picture.
   The glyph is drawn as three bars sized exactly as 2d draws them (2.5 wide; heights 8, 11, 7 from
   baselines 5, 2, 6 in a 13-box), masked so a single element can carry the accent as its own paint. */
.vp-card .vp-tkRow.on .vp-tkName{ color:#FFFFFF; font-weight:600; }
.vp-card .vp-tkRow.on .vp-tkNum{
  color:transparent; position:relative; }
.vp-card .vp-tkRow.on .vp-tkNum::after{
  content:""; position:absolute; right:0; top:50%; width:13px; height:13px; margin-top:-6.5px;
  background:var(--accent,#2F6BFA);
  -webkit-mask:var(--tk-bars) center/13px 13px no-repeat; mask:var(--tk-bars) center/13px 13px no-repeat; }
:root{ --tk-bars:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13"><rect x="1" y="5" width="2.5" height="8" fill="black"/><rect x="5.25" y="2" width="2.5" height="11" fill="black"/><rect x="9.5" y="6" width="2.5" height="7" fill="black"/></svg>'); }

/* the fade the scroller wears: 44 high, and the card's own ground fading up out of nothing */
body.has-tracks .vp-card .vp-tkList.scroll::after,
body.has-tracks .vp-card .vp-tkWrap::after{
  height:44px; background:linear-gradient(180deg, rgba(26,27,29,0), rgba(26,27,29,.96)); }

/* the playing row's SIZE, which the block above missed: an older rule sets font-size on
   `.vp-card .vp-tkRow.on .vp-tkName` (0,4,0), so a plain `.vp-card .vp-tkName` (0,2,0) could not
   reach it and the playing row stayed 14.5 at phone width while every resting row was 14. The
   boards draw one size per width for every row — the playing one differs by WEIGHT and INK, which
   is the whole point of a state that is not a control. */
.vp-card .vp-tkRow.on .vp-tkName{ font-size:14px; }
@media (min-width:481px){ .vp-card .vp-tkRow.on .vp-tkName{ font-size:14.5px; } }

/* THE 15px LANE MEETS THE BOARDS, AND scrollbar-gutter IS THE HONEST ANSWER --------------------
   Sid's ruling of 2026-08-01: rows clear a 15px lane so no scrollbar of either kind can cover row
   content. The r2 boards draw the tracklist rows FLUSH — full card width, hairlines only — because
   their frames do not scroll and no bar is ever depicted. So the board does not overturn the lane
   ruling; it simply never shows the case the ruling exists for.
   A symmetric 15px padding would contradict the drawn rows on every machine, to protect the subset
   with classic scrollbars. `scrollbar-gutter:stable` reserves the space ONLY where a classic bar
   actually takes layout width: rows are flush as drawn on overlay systems, and on a classic-bar
   system the gutter appears and the duration and download arrow stay clear of it. Both rulings, in
   the condition each was written for. FLAGGED for Sid rather than settled here. */
body.has-tracks .vp-card .vp-tkList{ scrollbar-gutter:stable; }

/* ==================================================================================================
   LISTEN LIVE, AGAINST 2f (measured 2026-08-14)
   ==================================================================================================
   Drawn: card 342x533 at b28 · well 304x219 = 160 wave + 57 strip box + 2 border
     strip  302x57 → 56 high under the weld hairline, ground rgba(10,11,14,.55)
     pause  52x44 slot, 22 glyph — the SAME slot the listener's play uses, one transport language
     mute   44 circle, 1px rgba(255,255,255,.18), 16 glyph at 80% — a circle so it can never be
            mistaken for the transport. The build had it wearing the blue hero disc, which made
            mute the play control on a page listeners see. That is the fault 2f names (q2).
     right  the live badge (6px #FF453A dot + "live" 12.5/600 at 90%) then the clock 12.5/500 at 75%
   The live page carries NO expiry — nothing is kept to expire — and says so instead. */
.live-well{ height:calc(160px + 56px); }
.ll-strip--live{ height:56px; }
.ll-strip--live .vp-hero{
  flex:none; width:52px; height:44px; min-width:52px; min-height:44px; padding:0; border:0;
  border-radius:0; background:none; box-shadow:none;
  display:flex; align-items:center; justify-content:center; cursor:pointer; }
.ll-strip--live .vp-hero:hover{ background:none; box-shadow:none; }
.ll-strip--live .vp-hero svg{ width:22px; height:22px; }
.ll-strip--live .ll-mute{
  flex:none; width:44px; height:44px; margin-left:4px; padding:0; background:none; cursor:pointer;
  border:1px solid rgba(255,255,255,.18); border-radius:50%;
  display:flex; align-items:center; justify-content:center; }
.ll-strip--live .ll-mute svg{ width:16px; height:16px; }
.ll-liveRight{ margin-left:auto; display:flex; align-items:center; gap:10px; padding-right:12px; }
.ll-strip--live .ll-live{
  display:inline-flex; align-items:center; gap:6px; margin-left:0;
  font-size:12.5px; font-weight:600; color:rgba(255,255,255,.9); letter-spacing:var(--t-tracking); }
.ll-strip--live .ll-live .dot{ width:6px; height:6px; border-radius:50%; background:#FF453A; }
.ll-clock{ font-size:12.5px; font-weight:500; color:rgba(255,255,255,.75); letter-spacing:var(--t-tracking); }
/* the badge is IN the strip now, not floating inside the well */
.vp-liveBadge{ display:none; }
/* the delay note takes the helper voice and the card's one spine — left, like everything but the wordmark */
.ll-liveNote{ margin-top:10px; text-align:left; font-size:13.5px; line-height:1.6;
  color:rgba(255,255,255,.55); }

/* the live well needs to out-rank the listener's hero tier, which is `.vp-card .ll-well` (0,2,0) and
   was holding it at 180+52=232 where 2f draws 160+56=216. And the strip's own left padding: the base
   .ll-strip pads 2, 2f draws the pause slot starting 8 in from the well's edge. */
.vp-card .ll-well.live-well{ height:calc(160px + 56px); }
.ll-strip--live{ padding-left:8px; }

/* ---- the OWNER's close control stands out (Sid, 2026-08-17) ---------------------------------
   It shared .vp-back's quiet 13/500 ink with the recipient's "‹ studio" link, which is right for a
   passive way back and wrong for the one action on a preview: the sender is here to look and leave,
   and the leaving was the quietest thing on the card. So the OWNER's copy gets a ring around the
   glyph and its words in white bold. Scoped to .vp-back#ownerBack so the recipient's link is
   untouched — same element, same slot, same 44pt area, different weight of voice.
   The ring is drawn on the SVG rather than added as an element: nothing new in the DOM, and the
   existing ::after tap target still governs the hit area, which stays 44. */
#ownerBack{ color:#fff; font-weight:700; gap:var(--sp-tight); }
#ownerBack svg{ box-sizing:content-box; width:13px; height:13px; padding:5px;
  border:1.5px solid rgba(255,255,255,.55); border-radius:50%; }
#ownerBack:hover svg{ border-color:#fff; }
