/* Vibe Check — Listen Later studio (producer). Apple-grade dark: base #0C0C0E, surface cards
   #1C1C1E/#2C2C2E, one blue #3B74F0 for actions, red #FF453A for expiry/destructive only.
   Token-driven so the whole surface follows :root. */
:root{
  --page:#0C0C0E; --card:#1C1C1E; --card-2:#2C2C2E; --line:#38383A; --line-2:rgba(255,255,255,.08);
  --ink:#ffffff; --ink-2:rgba(255,255,255,.60); --ink-3:rgba(255,255,255,.42); --ink-4:rgba(255,255,255,.55);
  /* --blue is NOT redeclared here. It was, at the old #3B74F0, which silently shadowed tokens.css
     and is why the product-wide blue change never reached the studio. The derivatives below follow
     the shared value. */
  --blue-press:#CD5F32; --blue-soft:rgba(228,111,64,.18); --blue-edge:rgba(228,111,64,.40);
  --coral:#FF453A; --coral-line:rgba(255,69,58,.38); --coral-soft:rgba(255,69,58,.16);
  --sh-card:0 1px 3px rgba(0,0,0,.45);
  --sh-pop:0 24px 60px rgba(0,0,0,.55);
  --sh-blue:0 8px 20px rgba(228,111,64,.40);
  --r-card:22px; --r-panel:14px; --r-ctl:13px; --r-pill:999px;
  --f:'Inter',system-ui,-apple-system,BlinkMacSystemFont,sans-serif;
  /* --mono IS GONE FROM HERE (Sid, 2026-08-14). (ez) retired Roboto Mono from every rendered web
     role and let the family stay declared in TOKENS as history — one place, clearly historical.
     This was a second declaration shadowing that one, which is exactly what the (dk)/(dl) token law
     forbids and exactly how mono came back twice: a sweep reads tokens.css, sees the family is only
     history, and never learns the studio kept its own copy for anything to reach for.
     Nothing references it now. The size step --t-mono (12.5) is untouched and still renders Inter,
     which is what (dk) ruled it to be. */
}
*{ box-sizing:border-box; -webkit-tap-highlight-color:transparent; }
[hidden]{ display:none !important; }
/* lock the app shell so overscroll can't pull-to-refresh / reload — the tracks list is the only scroller */
html,body{ margin:0; height:100%; overflow:hidden; overscroll-behavior:none; }
/* full-bleed: the .sb-stage (from sendbox.css) places the box top-left over the wallpaper.
   No body centering/padding — that's what was pushing the card to the middle. */
body{ background:var(--page); color:var(--ink); font-family:var(--f); -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility; margin:0; }
.wrap{ width:100%; max-width:600px; display:flex; flex-direction:column; gap:16px; }

/* lockup — wordmark is tracked text (no STREAKY; maker credit lives in copy) */
.lockup{ text-align:center; margin-top:2px; }
.wordmark{ height:22px; width:auto; display:inline-block; }
.logo-img{ display:block; margin:0 auto; width:200px; max-width:56%; height:auto; }
.modewrap{ display:none; }  /* no LISTEN LATER pill — mode is carried by content */

/* sheet (card) */
.sheet{ background:var(--card); border:1px solid var(--line); border-radius:var(--r-card); box-shadow:var(--sh-card); padding:30px 30px 24px; }
.muted{ color:var(--ink-2); font-size:13.5px; line-height:1.55; }

/* signed-out */
.signedout{ text-align:center; }
.signedout h2{ margin:14px 0 8px; font-size:19px; font-weight:700; color:var(--ink); }
.signedout .muted{ margin:0 auto 20px; max-width:400px; }
.bigGlyph{ color:var(--blue); display:flex; justify-content:center; }
.bigGlyph svg{ width:38px; height:38px; }

/* header row: section label left · usage opposite (right) */
.lhead{ display:flex; align-items:flex-start; justify-content:space-between; gap:20px; margin-bottom:14px; }
.sect{ font-family:var(--ui); font-size:11px; font-weight:500; letter-spacing:1px; text-transform:uppercase; color:var(--ink-3); padding-top:3px; }
.sect2{ font-family:var(--ui); font-size:11px; font-weight:500; letter-spacing:1px; text-transform:uppercase; color:var(--ink-3); margin:26px 0 2px; }

/* (the old header usage block died with the studio restructure — the meter now lives at the card's bottom: .sv-usage) */
.pbar{ height:6px; border-radius:4px; background:rgba(255,255,255,.12); overflow:hidden; }
.pbar > i{ display:block; height:100%; width:0%; background:var(--blue); border-radius:4px; transition:width .3s ease; }
.pbar > i.warn{ background:var(--coral); }

/* drop zone */
.drop{ display:block; border:1.5px dashed rgba(255,255,255,.20); border-radius:var(--r-panel); background:rgba(255,255,255,.04);
  cursor:pointer; transition:border-color .15s, background .15s; }
.drop:hover, .drop.drag{ border-color:var(--blue); background:rgba(228,111,64,.12); }
.drop.has{ border-style:solid; border-color:var(--blue); background:rgba(228,111,64,.12); }
.dropInner{ padding:26px 20px; text-align:center; }
.dropGlyph{ width:38px; height:38px; margin:0 auto 10px; display:flex; align-items:center; justify-content:center; color:var(--blue); }
.dropGlyph svg{ width:30px; height:30px; }
.dropText{ font-size:15px; font-weight:700; color:var(--ink); }
.dropText b{ color:var(--ink); }
.dropHint{ margin-top:4px; font-size:12.5px; color:var(--ink-2); }

/* fields */
.fields{ display:flex; flex-direction:column; gap:14px; margin-top:16px; }
.fieldRow{ display:flex; gap:14px; }
.fieldRow .field{ flex:1 1 0; min-width:0; }
.field{ display:flex; flex-direction:column; gap:7px; }
.field span{ font-size:11px; font-weight:600; color:var(--ink-3); text-transform:uppercase; letter-spacing:.04em; }
.field .cnt{ float:right; text-transform:none; letter-spacing:0; color:var(--ink-3); font-weight:500; }
.field input, .field select, .field textarea{ padding:0 13px; border-radius:var(--r-ctl); background:var(--card-2);
  border:1px solid var(--line); color:var(--ink); font:500 14px/1 var(--f); outline:none; width:100%; }
.field input, .field select{ height:44px; }
.field textarea{ min-height:62px; padding:12px 13px; line-height:1.45; resize:vertical; font-weight:500; }
.field input::placeholder, .field textarea::placeholder{ color:rgba(255,255,255,.30); }
.field select{ appearance:none; -webkit-appearance:none; cursor:pointer;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23ffffff' stroke-opacity='.5' stroke-width='1.6' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat; background-position:right 13px center; padding-right:34px; }
.field input:focus, .field select:focus, .field textarea:focus{ border-color:var(--blue); box-shadow:0 0 0 3px var(--blue-soft); }

/* a collapsed "Add a message" affordance that expands to the textarea */
.addmsg{ display:inline-flex; align-items:center; gap:7px; background:none; border:none; cursor:pointer; padding:2px 0;
  color:var(--blue); font:600 13px var(--f); }
.addmsg svg{ width:15px; height:15px; }

/* buttons */
.btn{ height:48px; display:inline-flex; align-items:center; justify-content:center; gap:9px; padding:0 16px;
  border-radius:14px; font:700 15px/1 var(--f); cursor:pointer; border:1px solid transparent; text-decoration:none;
  transition:background .18s ease, border-color .18s ease, color .18s ease, box-shadow .18s ease; white-space:nowrap; }
.btn svg{ width:17px; height:17px; flex:0 0 auto; }
.btn--primary{ background:var(--blue); color:#fff; box-shadow:var(--sh-blue); }
.btn--primary:hover{ background:var(--blue-press); }
.btn--primary:disabled{ background:rgba(255,255,255,.16); color:rgba(255,255,255,.45); box-shadow:none; cursor:default; }
.btn--secondary{ background:var(--card-2); color:var(--ink); border-color:var(--line); }
.btn--secondary:hover{ background:#3a3a3c; border-color:rgba(255,255,255,.22); }
.btn--secondary svg{ color:var(--ink-2); }
.btn--block{ width:100%; margin-top:18px; }
.btn.danger{ color:var(--coral); border-color:var(--coral-line); }
.btn.danger:hover{ background:var(--coral-soft); }

/* progress + messages */
.prog{ margin-top:14px; height:30px; border-radius:9px; background:rgba(255,255,255,.10); position:relative; overflow:hidden; }
.prog i{ display:block; height:100%; width:0%; background:var(--blue); transition:width .15s ease; }
.prog span{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:600; color:#fff; }
.formMsg{ margin-top:14px; font-size:13px; padding:10px 12px; border-radius:10px; }
.formMsg.err{ background:var(--coral-soft); color:#ff8a80; border:1px solid var(--coral-line); }
.formMsg.info{ background:var(--blue-soft); color:#ecb4a0; border:1px solid rgba(228,111,64,.30); }

/* link list */
.links{ margin-top:8px; }
.lrow{ display:flex; align-items:center; gap:12px; padding:14px 0; border-top:1px solid var(--line); }
.lrow .info{ flex:1; min-width:0; }
.lrow .lt{ font-size:16px; font-weight:700; color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.lrow .lm{ font-size:13px; color:var(--ink-3); margin-top:2px; }
.lrow .lm .dot{ opacity:.55; margin:0 6px; width:auto; height:auto; border-radius:0; background:none; display:inline; }
.lrow .lm .exp{ color:var(--coral); font-weight:600; }
.lrow .lm .pin{ color:var(--ink-2); font-weight:600; }
.iconbtn{ width:30px; height:30px; flex:0 0 auto; border:1px solid var(--line); border-radius:8px; background:var(--card-2);
  display:flex; align-items:center; justify-content:center; color:var(--ink-3); cursor:pointer; text-decoration:none; transition:.15s; }
.iconbtn:hover{ background:#3a3a3c; color:var(--ink-2); }
.iconbtn.copied{ color:var(--blue); border-color:var(--blue-edge); }
.iconbtn svg{ width:14px; height:14px; }

/* Play button = circle with a blue ▶; tapping it (or the file name) opens the full player. */
.lrow .lt{ cursor:pointer; }
.lrow .playbtn{ width:40px; height:40px; border-radius:50%; border:1px solid var(--blue-edge); background:var(--card-2);
  box-shadow:0 4px 11px rgba(228,111,64,.18); color:var(--blue); }
.lrow .playbtn svg{ width:15px; height:15px; }
.lrow .playbtn .i-pause{ display:none; }

/* transient undo bar */
.undobar{ display:flex; align-items:center; justify-content:space-between; background:var(--card-2); border:1px solid var(--line);
  border-radius:10px; padding:12px 15px; margin-top:12px; font-size:13px; color:var(--ink-2); }
.undobar button{ background:none; border:none; color:var(--blue); font:600 13px var(--f); cursor:pointer; }
.empty{ color:var(--ink-3); font-size:13px; text-align:center; padding:18px; }

/* footer */
.foot{ text-align:center; color:var(--ink-3); font-size:12px; margin-top:2px; }
.foot a{ color:var(--blue); text-decoration:none; font-weight:600; }

/* ---------- Modal (link ready / edit) ---------- */
.overlay{ position:fixed; inset:0; background:rgba(0,0,0,.62); display:flex; align-items:center; justify-content:center; padding:24px; z-index:50; }
.modal{ width:520px; max-width:100%; background:var(--card); border:1px solid var(--line); border-radius:var(--r-card); box-shadow:var(--sh-pop); padding:26px 26px 24px; }
.modal__t{ display:flex; align-items:center; justify-content:center; gap:8px; text-align:center; font-size:13px; font-weight:700;
  letter-spacing:.06em; text-transform:uppercase; color:var(--blue); margin-bottom:20px; }
.modal__t svg{ width:16px; height:16px; }
.modal__l{ font-size:11px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:var(--ink-3); margin:0 0 8px; }
.copyrow{ display:flex; align-items:center; gap:8px; background:var(--card-2); border:1px solid var(--line); border-radius:var(--r-ctl); padding:0 14px; height:48px; }
.copyrow .url{ flex:1; font-size:13.5px; color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.modal__note{ font-size:12.5px; color:var(--ink-2); line-height:1.55; margin-top:14px; text-align:center; }
.modal__rows{ display:flex; flex-direction:column; gap:8px; margin-top:16px; }
.modal__rows .r{ display:flex; align-items:center; gap:10px; font-size:13px; color:var(--ink-2); }
.modal__rows.specrows .r{ justify-content:space-between; }
.modal__rows.specrows .r .k{ color:var(--ink-3); }
.minisel{ height:32px; border:1px solid var(--line); border-radius:8px; background:var(--card-2); padding:0 28px 0 11px; font:600 13px var(--f); color:var(--ink); cursor:pointer;
  appearance:none; -webkit-appearance:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23ffffff' stroke-opacity='.5' stroke-width='1.6' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat; background-position:right 9px center; }
.minisel:focus{ border-color:var(--blue); box-shadow:0 0 0 3px var(--blue-soft); outline:none; }
.miniinput{ height:32px; width:160px; border:1px solid var(--line); border-radius:8px; background:var(--card-2); padding:0 11px; font:600 13px var(--f); color:var(--ink); outline:none; }
.miniinput::placeholder{ color:rgba(255,255,255,.30); font-weight:500; }
.miniinput:focus{ border-color:var(--blue); box-shadow:0 0 0 3px var(--blue-soft); }
/* full-width name/message inputs in the link editor (claimed/just-made link) */
.modalinput{ width:100%; height:44px; border:1px solid var(--line); border-radius:var(--r-ctl); background:var(--card-2); padding:0 13px; color:var(--ink); font:500 14px var(--f); outline:none; margin-top:10px; }
.modalinput::placeholder{ color:rgba(255,255,255,.30); }
.modalinput:focus{ border-color:var(--blue); box-shadow:0 0 0 3px var(--blue-soft); }
.modal__actions{ display:flex; gap:12px; justify-content:center; margin-top:22px; }

/* usage meter, restored: bottom of the studio card, quiet tertiary, only shown near a cap */
/* CENTRED IN ITS BAND (Sid, 2026-08-15). Measured: 10px above the text and 27 below (its own 2px,
   then the card's 24px bottom padding), so it sat hard against the last row and its peek. The card's
   padding is not the footer's to spend, so the footer's own padding-top takes --sp-air to match it:
   24 above, 25 below. A declared step, no new number. Costs 14px of card height. */
.sv-usage{ flex:0 0 auto; text-align:center; font-size:11.5px; color:rgba(255,255,255,.38); padding:var(--sp-air) 0 0; }

/* ---------- Account modal steps (profile image · log out everywhere · name) ---------- */
.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; }
.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; }
/* same badge, asking rather than warning — "upload as" is a choice, not a danger */
.acmodal .blueicon{ width:46px; height:46px; border-radius:50%; background:rgba(228,111,64,.14); color:var(--blue);
  display:flex; align-items:center; justify-content:center; margin-bottom:14px; }
.acmodal .blueicon svg{ width:22px; height:22px; }
/* circular crop + zoom */
.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 .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); }
.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; }

@media (max-width:480px){ .sheet{ padding:22px 18px 20px; } .fieldRow{ flex-direction:column; } .lhead{ flex-direction:column; gap:12px; } .usage{ text-align:left; min-width:0; width:100%; } }

/* ==============================================================================================
   CONFIRMS STACK FOR THUMBS (fd), frames 3j / 3k / 3l
   ==============================================================================================
   A row of confirm buttons at 390 gives each one a third of 278px and puts the destructive one
   under the thumb by accident. Stacked, full width, and the ORDER IS RULED — safe default first,
   destructive last, cancel/keep-editing at the bottom nearest the thumb, one order across all
   three phone confirms. (3j had drawn save first and 3k the destructive first; the ruling settled
   them into one.) The DOM order stays the desktop's, which reads cancel-first left to right, so
   the order here is `order:` on the three known buttons rather than a second markup arrangement
   that would have to be kept in step.
   A track delete has no safe default, so its destructive action stands alone above cancel — that
   falls out of the same numbering with the middle slot simply empty.
   THE TREATMENT IS THE DRAWN ONE: an outlined destructive (transparent, 1px --red at 38%, red
   label), a filled --red only for the loudest action a dialog carries, a filled --field cancel,
   radius 12, and 8px between. The desktop confirm keeps what it has: 2j/2k draw the same outlined
   treatment there too, but that is unbuilt (fc) work and a phone pass is not where it lands. */
@media (max-width:480px){
  .acmodal.confirm .acmodal__row{ flex-direction:column; align-items:stretch; gap:8px; }
  .acmodal.confirm .acbtn{ flex:0 0 auto; width:100%; min-height:46px; border-radius:12px; }
  /* ORDER: safe first, destructive last, the way out at the bottom.
     THREE IDS, and not for fun: both dialogs already carry a DESKTOP arrangement further down this
     file — `#delRow.three #delKeep{ order:3; flex:1 0 100% }` and the `#pinModal.staged` trio — at
     (2,1,0), later in the file, so a matching weight here would lose on order alone. This is the
     same trap the sheet's coarse-pointer rule had to be moved to the end of the file for; three ids
     wins outright instead, and cannot be undone by anything moving. The desktop `flex:1 0 100%`
     also has to go: in a COLUMN that basis is a height, so "keep" would stretch to fill the row. */
  #delModal #delRow #delKeep{ order:1; flex:0 0 auto; }
  #delModal #delRow #delConfirm{ order:2; }
  #delModal #delRow #delCancel{ order:3; }
  /* .staged is the three-answer unsaved dialog and ONLY it. askPin is shared with ordinary
     questions ("already in this collection", the PIN prompts), whose confirm is a plain action —
     dressing every pinConfirm as destructive would tell a sender that adding a track is dangerous. */
  #pinModal.staged #pinRow #pinAlt{ order:1; }
  #pinModal.staged #pinRow #pinConfirm{ order:2; }
  #pinModal.staged #pinRow #pinCancel{ order:3; margin-right:0; }
  /* the drawn button faces */
  .acmodal.confirm .acbtn--sec{ background:var(--field, #2C2C2E); border:1px solid var(--field-border, #38383A); color:#fff; }
  #delModal #delRow #delKeep,
  #pinModal.staged #pinRow #pinConfirm{ background:transparent; border:1px solid rgba(255,69,58,.38); color:#FF453A; margin-right:0; }
  #delModal #delRow #delConfirm{ background:#FF453A; color:#fff; border:0; }
  #pinModal.staged #pinRow #pinAlt{ background:var(--blue); color:#fff; border:0; box-shadow:none; }
  /* a TRACK delete has only the destructive and the way out, and 3k draws it outlined: it is the
     only action in the dialog, so nothing has to shout over anything else. `.three` is the class
     the collection case already sets, so this is the single case by construction. */
  #delModal #delRow:not(.three) #delConfirm{ background:transparent; border:1px solid rgba(255,69,58,.38); color:#FF453A; }
  /* 3k/3l/3j are inset 35 each side (tokens.css "phone inset 35 each side") */
  .acmodal.confirm{ width:calc(100vw - 70px); max-width:calc(100vw - 70px); }
}

/* ============================================================================
   UNIFIED OWNER PANEL — ONE component that morphs in place (Studio Manage design).
   Values verbatim from the bundle. Solid #1C1C1E card; the drop zone + waveform
   well are the only TRANSPARENT elements (a window to the wallpaper + a scrim).
   ============================================================================ */
/* --wall-img + the card chrome + .st-win frosted well + .st-wordmark are SHARED in sendbox.css
   (single source of truth — the homepage reuses these exact components). studio.css only adds the
   studio-specific shell (fixed height, internal scroll) on top. */
@keyframes vcsh{ 0%{transform:translateX(-100%)} 100%{transform:translateX(230%)} }
@keyframes vcpulse{ 0%,100%{transform:scale(1);opacity:.55} 50%{transform:scale(1.6);opacity:0} }

/* card SHELL only — the chrome (bg/border/radius/shadow/padding/width) is shared in sendbox.css (.st-card).
   The studio adds a fixed height + internal scroll so only the list scrolls; page/wallpaper never move. */
/* max-height uses svh (the STABLE small-viewport unit), NOT dvh: dvh changes when iOS Safari collapses the
   URL bar on keyboard focus and doesn't restore it, which grew the card and left it grown. svh is static
   across URL-bar + keyboard, so the card never resizes from typing — and svh always fits the visible viewport. */
/* the card WRAPS its content (ends just after the last row, the card padding is the comfortable
   bottom band); when the list outgrows the viewport it caps at max-height and the LIST scrolls
   within (.sv-list/.st-list are the flex-scrolling children) — the page itself never scrolls. */
/* overflow:CLIP, not hidden, and the difference is the whole point: hidden makes a scroll container
   that draws no bars but is still scrollable programmatically — which is exactly the handle WebKit
   grabs to chase a focused field, and it is what verify-ios-focus exists to catch. clip makes no
   scroll container at all. The action bar bleeds to this box's padding edge, so this is the element
   that has to be unable to scroll. (Same reasoning, same fix as the shared .st-win.) */
/* THE DRAWN CARD (design/redesign-2026-08/tokens.css line 72: "desktop card 600 wide, top 36,
   min-height 420, hugs content; radius 22; padding gutters 18"), studio only (Sid, 2026-08-13).
   NOT the live /l/_assets/tokens.css — its line 72 is --blue-text, and citing it bare sent one
   reader to the wrong file.
   The build sat at sendbox's shared 412 and it had started to cost: the collection strip's fixed
   slots (prev 44, play 52, next 44, time, ✕ 44) left the title a 68px box, so a ten-character
   track name truncated.
   SCOPED BY THE FILE IT LIVES IN, which is this codebase's own convention and the reason the rule
   above it exists: studio.css is loaded by /studio/ and nothing else (splash.html mentions it only
   in a comment), so a bare .st-card here cannot reach the homepage's sender box or — the one that
   matters — the LIVE listener player, which is the same shared .st-card in sendbox.css wearing
   .vp-card. Widening sendbox.css:220 instead would have moved both.
   THE GUTTER STAYS 22 (Sid's ruling: the width, the top and the radius, not the insets). It is load
   bearing: .st-card > .st-bar's margin:0 -22px, .sv-sheet's margin:0 -22px and --sv-gut:22px are
   three hand-kept copies of it, and .st-card is overflow:clip — at the drawn 18 they would bleed
   4px past the content edge and be sliced by the card's own border and bottom radius.
   96 -> 72 because 96 was 48 top + 48 bottom and the drawn top is 36; without it the sheet's
   ceiling loses 24px of the room the board gives it. The short-viewport step below keeps its own. */
/* ⚠ THE PLACEMENT OF THIS RULE IS LOAD BEARING. It must stay ABOVE the @media (max-width:480px)
   block below, which re-declares .st-card{ width:100%; max-width:440px }. Both are (0,1,0) and a
   media query adds no specificity, so ORDER is the only thing that lets the phone rule win. Move
   this line (or add another .st-card width) after it and width:600px replaces width:100% while
   max-width:440px survives — the phone card becomes a rigid 440 in a 375 viewport, and because
   sendbox.css and this file both set html,body{overflow:hidden} there is no scrollbar: the right
   65px, carrying save, the avatar and the sort control, is silently amputated.
   (The same trap is already live one rule down: .st-card{ padding:24px 22px } at the card-padding
   comment below sits AFTER the phone block, so that block's padding:18px 20px is dead code today.) */
.st-card{ display:flex; flex-direction:column; overflow:clip; max-height:calc(100svh - 72px); text-transform:lowercase;
  width:600px; border-radius:22px; }
/* card ground: sendbox.css owns it now — var(--surface-card) at the base, one surface family
   across homepage box, studio card and player (Sid's ruling, 2026-07-30) */

/* site-wide lowercase: OUR copy is lowercased by the card (and modals) above; everything the USER typed
   or that must keep its case is exempted here — track names, messages, account name/email, the link URL,
   the name input, and avatar initials. Keeps "people's studio files stay exactly as named". */
.acmodal{ text-transform:lowercase; }
.sv-rname, #svName, #title, #message, #acctName, #acctEmail, #urlText, #pmUrlMuted, #nameInput,
.st-avatar, .sb-acctpop .ava,
/* EVERY PLACE A TRACK'S TITLE IS PAINTED, not just the top-level row (Sid, 2026-08-14). The list's
   own rows (.sv-rname) and the transport strip (#svName) were exempted; the two lists that show
   the SAME titles one level down were not, so a collection painted "Roie Shpigler - Fires" in the
   strip and "roie shpigler - fires" in the rows beneath it. Same title, two ways, one card, which
   is the exact fault the ruling was written for.
   .st-tkName is the track row in all three of its homes (the create panel, the separate-links
   walk, and the collection sheet); .sv-subName is the studio list's expanded sub-rows. Swept by
   rendering each surface and reading the computed transform, not by reading selectors. */
.st-tkName, .sv-subName,
/* the wizard's receipt rows, for BOTH reasons this list exists: .st-sepName is what the sender
   typed, and .st-sepUrl carries a link token that is CASE-SENSITIVE — lowercasing it renders a
   dead link that still looks right (acetate.audio/a3tEbANo shown as .../a3tebano). */
.st-sepName, .st-sepUrl{ text-transform:none; }

/* THE DRAWN TOP IS 36 (design/redesign-2026-08/tokens.css line 72). --card-top is NOT touched: sendbox declares it 48 and
   steps it to 7 on a short viewport and 14 at phone width, and a flat override here would win over
   both of those regardless of their media queries and undo the compaction. So this moves only the
   case where the token is 48, by inverting sendbox's own two conditions rather than restating its
   ladder — one more hand-kept copy of a value is exactly what the gutter comment above warns about. */
@media (min-height:801px) and (min-width:481px){
  .sb-stage{ padding-top:max(36px, env(safe-area-inset-top)); }
}

/* short windows: reclaim the studio's vertical margin so the list keeps room (player/fields stay pinned; page never scrolls) */
@media (max-height:800px){
  .sb-stage{ padding-bottom:7px; }   /* the TOP is --card-top now, shared with every other surface */
  .st-card{ max-height:calc(100svh - 14px); }
}
/* THE TRACKLIST WINS THE SHORT-VIEWPORT PIXELS (Sid, Windows arc).
   Below 820 the 4.5-row floor cannot hold: measured at 1280x620 the card's fixed regions came to
   447.6px of a 526.7px stage, leaving the list 79px — 1.2 rows, which reads as "there is no track
   list" rather than "there is more below". It is NOT a Windows bug; the same viewport height does
   the same thing on Mac Chrome (557px measured 0.64 rows), it had simply never been looked at on a
   short window. So the floor drops to 3.5 — the PHONE floor, reused rather than a fifth number —
   and everything else gives way to it.
   820, not a new threshold: sendbox.css already breaks its shared well tiers there, and the
   computed break point is 818. Two numbers two pixels apart is how tiers drift.
   Every value below steps DOWN the declared spacing ladder (tight 6 / close 12 / block 18 / air 24)
   rather than inventing a compact number — the type/spacing harness fails anything off the steps,
   and a gap that exists only in a media query is exactly the drift it was written to catch.
   The well is NOT here: it compacts through the shared --well-h tiers in sendbox.css, which is what
   .sv-well rejoined this pass. */
@media (min-width:481px) and (max-height:820px){
  .sv-upload{ margin-top:var(--sp-close); }      /* 16 -> 12 */
  .sv-well{ margin-top:var(--sp-close); }        /* block 18 -> close 12 */
  .sv-loaded{ margin-top:var(--sp-close); }      /* block 18 -> close 12 */
  .sv-transport{ margin-top:var(--sp-close); }   /* air 24 -> close 12 */
  .st-card{ --filter-band:var(--sp-close); }     /* the whole band tightens on a short window, both sides together */
  /* THE PLAY BUTTON STEPS DOWN ONE TIER (Sid, Windows arc). 84 is a ruled component dimension and
     it was held back until Sid spent it deliberately; it is what carries the list from 2.78 rows
     toward the 3.5 floor, which cannot be met at 620 without it.
     72, NOT 64: the phone tier below already declares exactly this step — .sv-play 72, transport
     min-height 72, gap 12, .sv-tbtn 44. Sid's ruling was "step down one tier", and this IS that
     tier; 64 would have been a fifth number four pixels from an existing one, which is the drift
     the brief names. The container is sized by the BUTTON, so shrinking min-height alone did
     nothing — measured, it stayed 84. Short screens only; >=820 keeps 84 and 874 is untouched. */
  .sv-transport{ min-height:72px; gap:12px; }
  .sv-tbtn{ width:44px; height:44px; }
}
/* one header for both views: ← · wordmark · avatar. wordmark stays left, avatar is anchored right with
   margin-left:auto, so the only change on the panel (Create/Edit) is the back button appearing on the left. */
.st-head{ flex:0 0 auto; display:flex; align-items:center; }
/* .st-wordmark — shared in sendbox.css */
.st-avatar{ flex:0 0 auto; margin-left:auto; 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; border:none; cursor:pointer; }
.st-sectlabel{ flex:0 0 auto; font-family:var(--ui); font-size:11px; letter-spacing:0.2px; color:rgba(255,255,255,.5); }

/* .st-win frosted "window" (blurred wallpaper + scrim) — shared in sendbox.css */

/* drop zone (at rest) */
.st-drop{ flex:0 0 auto; border:2px dashed rgba(255,255,255,.22); border-radius:15px; padding:18px; margin-top:10px;
  display:flex; align-items:center; justify-content:center; gap:12px; cursor:pointer; transition:border-color .15s; }
.st-drop.drag{ border-color:var(--blue); }
.st-dropIcon{ flex:none; width:38px; height:38px; border-radius:50%; background:rgba(255,255,255,.07); display:flex; align-items:center; justify-content:center; color:#dc9d86; }
.st-dropT{ font-weight:600; font-size:14.5px; color:#fff; }
.st-dropH{ font-size:12px; color:rgba(255,255,255,.5); margin-top:2px; }

/* "Drop another track" slim strip (panel open) */
.st-dropStrip{ flex:0 0 auto; display:flex; align-items:center; gap:9px; border:1.5px dashed rgba(255,255,255,.2); border-radius:11px;
  padding:9px 13px; margin-top:14px; cursor:pointer; transition:border-color .15s; }
.st-dropStrip.drag{ border-color:var(--blue); }
.st-dropStrip svg{ flex:none; color:#dc9d86; }
.st-dropStrip span{ font-size:13px; font-weight:600; color:rgba(255,255,255,.78); }

/* player region — FIXED height so fields below never move as it morphs */
.st-player{ flex:0 0 auto; margin-top:12px; display:flex; flex-direction:column; justify-content:center; }
.st-pmFmt{ flex:0 0 auto; text-align:center; font-size:12.5px; color:rgba(255,255,255,.5); margin-top:8px; }   /* lossless · 44.1kHz / 16-bit */
/* overflow:CLIP, not hidden — the waveform canvas runs ~40px past the well, and a hidden box is
   still programmatically scrollable, so WebKit could scroll it to chase a caret exactly as it did
   the panel. Both axes are hidden here, so clip does not degrade to hidden the way it does beside
   an auto axis. Painting is unchanged; the clip-path already did that job. */
/* height comes from the shared component in sendbox.css. The create card was 110 while the panel
   was 120 — the studio's own two wells disagreed, so both are on the ruled height now. */
.st-well{ position:relative; border-radius:13px; clip-path:inset(0 round 13px); overflow:clip; cursor:pointer; }
.st-well.drag{ box-shadow:inset 0 0 0 2px var(--blue); }   /* drag-over feedback for the upload drop zone */
.st-well canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }
/* THE UPLOAD'S NUMBER IS THE WELL'S CONTENT (Sid, 2026-08-17). It was a 12px pill on a dark chip,
   narrating "uploading <name>… 6%"; the name lives under the well now and the number is what the
   well is for while there is nothing else in it.
   NOTHING RENDERS ON TOP OF A WAVEFORM. That rule stands and this does not break it: the number
   exists ONLY while there is no waveform and is gone by the time there is one — paintWellPct(null)
   clears it on landing, which is the same moment the real waveform arrives, so the two can never
   share the well. Do not read this as permission to label a real waveform.
   SIZED FROM THE WELL, not typed: a fraction of --well-h, so it follows every declared tier on its
   own. No new literal, same construction as --topbar-h. Tabular figures so it does not jitter. */
.st-wellPct{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  font:700 calc(var(--well-h) * 0.42)/1 var(--ui); letter-spacing:-.02em; color:#fff;
  font-variant-numeric:tabular-nums; pointer-events:none; z-index:3;
  text-shadow:0 2px 18px rgba(0,0,0,.55); }
/* HALF, when it is a SENTENCE rather than a number (Sid, 2026-08-17: "preparing your track after
   it's uploaded is massive"). 0.42 of the well is right for two or three digits and enormous for a
   line of prose. Half of the same fraction, not a new size — it stays tied to --well-h and follows
   every tier with the number it replaces. Centred and allowed to wrap, since it is a sentence. */
.st-wellPct.prep{ font-size:calc(var(--well-h) * 0.21); line-height:1.25; font-weight:600;
  letter-spacing:normal; padding:0 var(--sp-close); text-align:center; }
.st-wellPct[hidden]{ display:none; }
.st-wellHead{ position:absolute; top:0; bottom:0; width:2px; background:rgba(197,107,75,.75); }   /* upload fill edge */
.st-playhead{ position:absolute; top:7px; bottom:7px; width:1.5px; background:var(--wave-playhead); border-radius:2px; box-shadow:0 0 8px rgba(0,0,0,.6); }

/* anchored fields — .st-fields/.st-input/.st-detailRow/.st-flabel/.st-pill/.st-expWrap/.st-menu/.st-pin/.st-go
   now live in the SHARED sendbox.css, so the studio uploader and the signed-out homepage box render from ONE
   source and the field type/spacing can't drift. (Moved 2026-06-26; values byte-identical, studio unchanged.) */

/* manage controls (morph in place of Get-your-link) */
.st-manage{ flex:0 0 auto; display:flex; align-items:flex-start; gap:9px; margin-top:12px; padding-bottom:10px; border-bottom:1px solid rgba(255,255,255,.1); }
.st-manageL{ flex:1; min-width:0; display:flex; flex-direction:column; gap:8px; }
.st-manageR{ flex:none; display:flex; flex-direction:column; gap:8px; }
.st-url{ display:flex; align-items:center; background:rgba(255,255,255,.07); border:1px solid rgba(255,255,255,.18); border-radius:11px; padding:9px 13px; }
.st-url .u{ flex:1; min-width:0; font-family:var(--ui); font-size:13px; color:rgba(255,255,255,.78); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.st-url .ok{ display:none; align-items:center; gap:4px; font-size:11px; font-weight:600; color:#30D158; white-space:nowrap; margin-left:8px; }
.st-url.copied .ok{ display:inline-flex; }
.st-del{ align-self:flex-start; display:inline-flex; align-items:center; gap:6px; background:none; border:none; cursor:pointer; padding:4px 2px; font-family:inherit; }
.st-del span{ font-size:11px; font-weight:600; color:#FF453A; }
.st-mbtn{ display:flex; align-items:center; justify-content:center; gap:7px; padding:9px 18px; border-radius:11px; cursor:pointer; font:600 14.5px var(--f); border:none; }
.st-mbtn.copy{ background:var(--blue); color:#fff; box-shadow:0 8px 18px rgba(197,107,75,.30); }
.st-mbtn.share{ background:transparent; color:#fff; border:1px solid rgba(255,255,255,.18); }
/* `.demoted` is DELETED, not left lying about: with the bar there is one primary slot and copy+share
   step out of it entirely while a save is pending, so a demoted copy button no longer exists. */

/* the list — flexes to fill, scrolls inside the card (page never scrolls) */
.st-list{ flex:1 1 auto; min-height:0; overflow-y:auto; margin-top:2px; padding:2px 0; }
.st-list.strip{ padding:6px 0;
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 18px,#000 calc(100% - 18px),transparent 100%);
  mask-image:linear-gradient(180deg,transparent 0,#000 18px,#000 calc(100% - 18px),transparent 100%); }
/* NO custom scrollbar on the studio list — the reference styles it NOWHERE (zero scrollbar rules,
   measured headless), which is what buys its behaviour: the OS overlay bar, hidden at rest,
   appearing only while scrolling, in the system's dark subtle style. Any ::-webkit-scrollbar rule
   is what DEFEATS that in Chrome and Safari alike — it switches the element to always-visible
   custom bars, which is exactly the standing light-grey bar Sid flagged. Matching the reference
   here means deleting, not restyling. (The panel tracklist's hidden-scrollbar rule is separate
   and deliberate: its fade is the affordance.) */
.st-lrow{ display:flex; align-items:center; gap:11px; padding:9px 0; border-bottom:1px solid rgba(255,255,255,.1); }
.st-lrow:last-child{ border-bottom:none; }
.st-linfo{ flex:1; min-width:0; }
.st-lname{ font-weight:600; font-size:13.5px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color:#fff; cursor:pointer; }
.st-lmeta{ font-size:11.5px; color:rgba(255,255,255,.55); margin-top:1px; }
.st-lmeta .x{ font-weight:600; }   /* time-left, coloured by the expiry scale */
.st-lkebab{ flex:none; border:none; background:none; cursor:pointer; padding:5px; display:flex; }
.st-empty{ font-size:13px; color:rgba(255,255,255,.45); text-align:center; padding:18px 0; }
.st-foot{ flex:0 0 auto; text-align:center; margin-top:16px; padding-top:12px; border-top:1px solid rgba(255,255,255,.1); }
.st-foot div{ font-size:11px; color:rgba(255,255,255,.45); font-weight:500; }

@media (max-width:480px){
  .st-card{ width:100%; max-width:440px; max-height:calc(100svh - 16px - env(safe-area-inset-top,0px)); padding:18px 20px; }
  .st-player{ margin-top:10px; }
  /* .st-detailRow/.st-flabel/.st-pill/.st-pin mobile sizing moved to the shared sendbox.css (one source) */
}

/* ====================== STUDIO RESTRUCTURE — views ====================== */
/* the visible view fills the card; its list is the only internal scroller */
#viewStudio, #viewPanel{ display:flex; flex-direction:column; flex:1 1 auto; min-height:0; }
#viewPanel{ overflow-y:auto; }   /* create/edit/ready scroll within the capped card instead of overflowing the window */
.st-back{ display:inline-flex; align-items:center; gap:5px; margin-right:13px; border:none; background:none; cursor:pointer; padding:0;
  font:600 13px var(--f); color:rgba(255,255,255,.7); }

/* upload-another strip (→ Create) */
.sv-upload{ flex:0 0 auto; display:flex; align-items:center; gap:9px; margin-top:16px; padding:12px 13px; cursor:pointer;   /* ref strip is 40 tall; 12px vertical (--sp-close's value) lands 41 */
  border:none; border-radius:11px; clip-path:inset(0 round 11px); background:transparent; font:inherit; color:inherit; text-align:left;
  outline:1.5px dashed rgba(255,255,255,.2); outline-offset:-1.5px; }   /* dashed via OUTLINE not border: a real border + blurred .st-win ::before defeats the rounded-corner clip on iOS */
.sv-upload svg{ flex:none; }
.sv-upload span{ font-size:13px; font-weight:600; color:rgba(255,255,255,.78); }
.sv-upload .chev{ margin-left:auto; }
.sv-upload:hover{ border-color:rgba(255,255,255,.34); }

/* loaded-track waveform well */
 /* PREVIEW REGION to the reference (2026-07-30): the overlay showed live's whole preview +
   transport block ~49px shorter, so every list row ghosted high. Measured: well 120 (was 102, a
   component dimension like the play button's 84), strip->well and well->title 18 (--sp-block, was
   12), spec->play and play->filter on --sp-air (was 14px literals). The phone keeps its own block
   (well 84 and the smaller step values) — the reference is desktop-only. */
/* height comes from the SHARED well component in sendbox.css (var(--well-h) + its tiers). The
   hardcoded 120px that used to live here was the copy that kept this well out of every compaction
   tier — deleted rather than overridden, per the sendbox law. */
/* overflow:CLIP, not hidden, for the reason .st-win states at length in sendbox.css: hidden makes a
   box that draws no scrollbars but is still programmatically SCROLLABLE, and WebKit scrolls the
   nearest such ancestor to chase a focused caret. The canvas runs ~40px past the well, so this box
   really did have somewhere to go — measured scrollWidth 592 against clientWidth 552. .st-win sets
   clip and this rule, loading later, was quietly putting it back to hidden. */
.sv-well{ position:relative; flex:0 0 auto; border-radius:13px; clip-path:inset(0 round 13px); overflow:clip; cursor:pointer; margin-top:var(--sp-block); }
.sv-well canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }
.sv-playhead{ position:absolute; top:7px; bottom:7px; width:1.5px; background:var(--wave-playhead); border-radius:2px; box-shadow:0 0 8px rgba(0,0,0,.6); }
.sv-tL, .sv-tR{ position:absolute; bottom:8px; 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; }
.sv-tL{ left:12px; } .sv-tR{ right:12px; }
.sv-wellMsg{ position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); font-family:var(--ui); font-size:11.5px;
  color:rgba(255,255,255,.7); background:rgba(0,0,0,.5); padding:4px 11px; border-radius:7px; white-space:nowrap; }

/* loaded-track label */
.sv-loaded{ flex:0 0 auto; text-align:center; margin-top:var(--sp-block); }
.sv-loadedName{ font-weight:600; font-size:14px; color:#fff; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; cursor:pointer; }   /* tap to reveal in full (.expanded) */
.sv-loadedFmt{ font-size:11.5px; color:rgba(255,255,255,.5); margin-top:1px; }

/* transport row */
.sv-tbtn{ flex:none; width:50px; height:50px; border-radius:50%; border:none; background:#2C2C2E; cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; }
.sv-tbtn:disabled{ opacity:.4; cursor:not-allowed; }
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */
/* the downloads row reuses the sv-toggle switch; quiet tertiary note beside it. NO margin
   override: the .st-detailRow family rule (sendbox.css) supplies the same 9px gap as every
   other field row, and its flex centring aligns the toggle + note on the label. */
.st-dlRow .sv-toggle{ flex:none; width:26px; height:16px; cursor:pointer; }
.st-dlRow .sv-toggle .knob{ width:12px; height:12px; }
.st-dlNote{ font-size:11.5px; color:rgba(255,255,255,.38); }
.st-dlNote.on{ color:var(--blue-text); }
.sv-toggle{ display:inline-flex; align-items:center; width:20px; height:12px; border-radius:999px; background:rgba(255,255,255,.2);
  padding:1.5px; cursor:pointer; border:none; justify-content:flex-start; transition:background .15s,justify-content 0s; }
.sv-toggle .knob{ width:9px; height:9px; border-radius:50%; background:#fff; }
.sv-toggle[aria-checked="true"]{ background:var(--blue); justify-content:flex-end; }   /* blue, never green (fc) */
.sv-toggle[aria-checked="true"] .knob{ box-shadow:0 1px 2px rgba(0,0,0,.4); }
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */

/* YOUR TRACKS header + sort */
/* THE BAND IS ONE NUMBER, DECLARED ONCE (Sid, 2026-08-15, third time of asking). The row was never
   misaligned - align-items:center is set once, wins uncontested, and the filter buttons and the
   auto-play toggle share a centre line to within a pixel. It sat low because its BAND was lopsided:
   --sp-air (24) above from this rule, and the list's own margin below, which was 4 and which I then
   half-corrected to 12. 24 against 12 is still low, which is why it kept looking unchanged.
   Both sides now read the SAME custom property, so they cannot drift apart again and there is no
   second copy to keep in step - which is what let this survive two fixes. --sp-block sits between
   the 24 and the 12 it replaces, so neither side lurches. */
.st-card{ --filter-band:var(--sp-block); }
.sv-tracksHead{ position:relative; flex:0 0 auto; display:flex; align-items:center; justify-content:space-between; margin-top:var(--filter-band); min-height:18px; }
.sv-headR{ display:inline-flex; align-items:center; gap:12px; }
.sv-sortWrap{ position:relative; }
.sv-sortBtn{ display:inline-flex; align-items:center; gap:6px; border:1px solid rgba(255,255,255,.18); background:rgba(255,255,255,.07);
  border-radius:8px; padding:5px 8px; cursor:pointer; font:inherit; }
.sv-sortBtn.open{ border-color:var(--blue); background:rgba(228,111,64,.14); }
.sv-sortTxt{ font-size:11.5px; font-weight:600; color:rgba(255,255,255,.78); }
.sv-sortChev{ transition:transform .15s; }
.sv-sortBtn.open .sv-sortChev{ transform:rotate(180deg); }
.sv-sortMenu{ position:absolute; top:calc(100% + 8px); right:0; width:188px; background:#1C1C1E; border:.5px solid rgba(255,255,255,.2);
  border-radius:12px; box-shadow:0 18px 44px rgba(0,0,0,.6); padding:5px; z-index:40; }
.sv-sortMenu .mi{ display:flex; align-items:center; justify-content:space-between; padding:9px 11px; border-radius:8px; font-size:13px; color:#fff; cursor:pointer; }
.sv-sortMenu .mi:hover{ background:rgba(255,255,255,.06); }
.sv-sortMenu .mi.on{ background:rgba(228,111,64,.18); font-weight:600; }
.sv-sortMenu .mi svg{ opacity:0; }
.sv-sortMenu .mi.on svg{ opacity:1; }

/* the queue — the only scroller (page/wallpaper never move) */
/* THE FILTER ROW WAS NOT MISALIGNED, IT WAS JAMMED (Sid, 2026-08-15). Measured on WebKit at phone
   width: the row computes align-items:center, and the filter buttons and the auto-play toggle share
   a centre line to within a pixel — the row's own box hugs its content exactly, so "centre it in its
   own height" was already true. What was not balanced is the row within its BAND: 24px above it from
   --sp-air, and 4px below before this list. That asymmetry is what reads as sitting low against the
   divider beneath. The previous pass removed an align-items:baseline override that was real but was
   not the cause, which is why the deployed file changed and the phone did not.
   4 was also off the spacing scale (6/12/18/24/30). It becomes --sp-close: still tighter to its own
   list than to the block above, which is right by proximity, but no longer touching it. */
.sv-list{ flex:1 1 auto; min-height:0; overflow-y:auto; overflow-x:hidden; margin-top:var(--filter-band); padding:2px 0;
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 14px,#000 calc(100% - 14px),transparent 100%);
  mask-image:linear-gradient(180deg,transparent 0,#000 14px,#000 calc(100% - 14px),transparent 100%); }
/* AIRY RULING EXTENDED TO THE TOP LEVEL (Sid, 2026-07-30). The reference's rows carry 12px above
   and below the content — the gap either side of each row's bottom line — where live had 9.
   12px FLAT AT BOTH WIDTHS, a component dimension like the sub-rows' own 10px slack: the ruling
   says "same slack behaviour as the sub-rows", and the sub-row family holds its vertical slack
   flat on phones (only its horizontal follows the phone scale). The first cut put this on
   var(--sp-close), which is 9 at 390 — top rows compressed while their own sub-rows did not, and
   the harness passed because it asserted that choice rather than the ruling. Padding-based, so a
   one-line and a two-line collection title carry identical slack and read as one family. Type
   sizes stay on their snapped steps; only the room changes. */
/* The LEFT INSET is what makes the divider read long (Sid, 2026-07-31). Measured, the dividers
   themselves were already identical — ref and live both span 1 -> 367 of the card's content box,
   366 wide. What differed is the content: the reference insets a row's content 12px, so the line
   runs visibly past the drag dots at both ends; live's content started level with the line, so the
   line looked to begin under the dots and therefore short. Same pixels of line, different reading.
   --sp-close, so the phone keeps its own step. It also lands the dl stripe where the reference
   draws it — see .sv-row.dl::before. */
.sv-row{ position:relative; display:flex; align-items:center; gap:11px;
  padding:12px var(--sp-tight) 12px var(--sp-close); border-bottom:1px solid var(--row-line); }
/* downloads-on indicator: a thin primary-blue stripe on the row's LEFT EDGE, drawn in the card's
   padding gutter (absolute, negative left) — zero horizontal content cost, clear of the drag
   handle in custom-order mode, holds at 320px (the card keeps >=14px side padding everywhere). */
/* FULL height, divider to divider (Sid's ruling, 2026-07-30): the row's box runs exactly line to
   line, so top/bottom 0 is the reference's "runs tight". The 6px insets were making it float with
   gaps either side. NB adopted from the worded ruling — neither reference HTML renders a
   downloads row to measure, and that is stated rather than papered over. Blue stays the RULED
   shared --blue. */
/* DIVIDER TO DIVIDER, meant literally. Absolute offsets resolve against the PADDING box, so
   top:0/bottom:0 stopped 1px short of the row's border box — exactly the row's own divider, left
   uncovered — and the 2px radius on a 3px bar rounded the last pixels away from both lines on top
   of that. bottom:-1px reaches the border box; square ends meet the lines instead of curving off
   them. Measured against live's own painted divider positions: neither reference export renders a
   downloads row, so the row's real bounds are the anchor. */
/* WIDTH FROM THE REFERENCE, which does render this: not as a bar in the gutter but as a 2px
   border-left on the row itself (rgb(47,107,250)) — invisible to a pseudo-element scan, which is
   why the first two attempts had nothing to measure and worked from the wording instead.
   Adopted here: 2px, and vertical bounds that span the row's BORDER box, which is what a
   border-left spans and is therefore divider to divider by construction.
   PLACEMENT, resolved by the row inset above rather than flagged: the reference's stripe sits ON
   the row's left edge, where the divider lines begin, so line/stripe/line read as one connected
   bracket. Live's floated 9px out in the gutter and the three never touched. The blocker was that
   moving it in would put it under the drag dots — but the 12px content inset moves the dots clear,
   so left:-(--sp-close) now lands the stripe exactly on the row's border-box edge, which is where
   a border-left paints. Same result as the reference's mechanism, without the layout shift a real
   border-left would impose on dl rows only. */
/* left:0 — the row's own left edge, where the divider lines begin. An absolute offset resolves
   against the containing block's PADDING BOX EDGE; it is not additionally shifted by padding-left.
   left:-(--sp-close) therefore did NOT cancel the row's new inset, it pushed the stripe 11px out
   into the gutter, which is where it had been all along. Caught by the overlay, not by the
   harness: my check computed the painted x with the same wrong arithmetic and passed. */
/* the list is a scroll container, so it CLIPS at its own box — widen it leftward with a
   compensating negative margin (content doesn't move a pixel) so a shade that bleeds into the
   gutter isn't cut off */
.sv-list{ padding-left:12px; margin-left:-12px; }
/* ROW STATE SHADES (fc): playing is the stronger tint plus the bars glyph, hover is a full step
   weaker, and TINTS NEVER STACK — :not(.playing) on the hover rule is what enforces that, so a
   playing row under the pointer keeps its own shade instead of compounding to a third value.
   The shade bleeds 8px into the gutter on each side so it reads as a band, not a boxed row. */
/* THE SHADE IS PAINT, NOT LAYOUT (Sid, 2026-08-14 — measured, not eyeballed).
   These two rules used to carry the 8px bleed as GEOMETRY: `margin:0 -8px; padding-left:8px;
   padding-right:8px`. Because the resting row is padded 12 left / 6 right, hovering did not merely
   shade it — it moved it. Measured at 1440 on a real row: the box went x 79 -> 71 and width
   554 -> 570, and the cells inside shifted with it, .sv-drag/.sv-rplay/.sv-rinfo each by -12px and
   .sv-rkwrap by +6. That is the jump: a 16px-wider box and up to 12px of travel per cell, on every
   pointer move across the list. The same rule governs .playing, so a row also jumped the moment it
   started playing.
   The bleed is real and ruled ("the shade bleeds 8px into the gutter on each side so it reads as a
   band, not a boxed row") — it just has to be PAINTED rather than laid out. This band is an
   absolutely-positioned ::before at `inset:0 -8px`, so it covers exactly the same area the margin
   trick did and the row's box, padding and children never move. z-index:-1 puts it behind the row's
   own content; the row is already position:relative, so it is positioned against the row. */
.sv-row::before{ content:""; position:absolute; inset:0 -8px; border-radius:10px;
  background:transparent; pointer-events:none; z-index:-1; }
.sv-row.playing::before{ background:var(--row-playing); }
.sv-row:not(.playing):hover::before{ background:var(--row-hover); }
.sv-row.playing{ border-bottom-color:transparent; }
/* tap a truncated title to reveal it in full. overflow-wrap:anywhere lets an UNBROKEN token (a long
   filename like Artist_Track_2448_Mix_1) break mid-word and wrap DOWN — without it white-space:normal
   has no break points, the text pours out sideways, and the list grows a horizontal scrollbar. */
.sv-rname.expanded, .sv-loadedName.expanded{ white-space:normal; overflow:visible; overflow-wrap:anywhere; word-break:break-word; }
.sv-row:last-child{ border-bottom:none; }
.sv-row.dragging{ opacity:.4; }
/* (.sv-row.liftrow deleted with the (fd) pass — a lift treatment nothing ever applied, carrying
   invented values. The lift the phone board actually draws is .sv-row.lifting, further down.) */
/* (base .sv-drag retired — the shared handle rule further down owns both levels) */
.sv-rplay{ flex:none; width:30px; height:30px; border-radius:50%; border:none; background:var(--blue); cursor:pointer; display:flex; align-items:center; justify-content:center; padding:0; }
.sv-rinfo{ flex:1; min-width:0; }
/* rows: truncate with ellipsis; TAP the title to reveal it in full (the established list
   behaviour). The expanded state uses anywhere-breaking because plugin-created takes are single
   UNBROKEN underscore strings ('MattJones_CouldBeSomething_2448_Mix_1_…' — zero spaces). */
.sv-rname{ font-weight:600; font-size:13.5px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color:#fff; cursor:pointer; }
/* the title -> meta breath. The reference's 5.38px is measured between GLYPH boxes; live's title
   and meta are blocks, so their boxes are LINE boxes and already carry the half-leading of the
   declared line-heights above. Applying 5.38 as a margin therefore counted that leading twice and
   pushed the one-line row to 66 against the reference's 63. 4px is the line-box equivalent: it
   reproduces the reference's visible 5.38 between the glyphs and its 63px row.
   Same box-vs-ink error as the sub-row gaps, in the vertical axis — a measurement is only
   transferable between two documents if it is taken from the same kind of edge in both. */
.sv-rmeta{ font-size:11.5px; color:rgba(255,255,255,.55); margin-top:4px; }
.sv-rmeta .x{ font-weight:600; }
.sv-rkwrap{ position:relative; flex:none; }
.sv-rkebab{ border:none; background:none; cursor:pointer; padding:5px; display:flex; }
.sv-rmenu{ position:absolute; top:calc(100% + 4px); right:0; width:128px; background:#1C1C1E; border:.5px solid rgba(255,255,255,.2);
  border-radius:11px; box-shadow:0 18px 40px rgba(0,0,0,.6); padding:5px; z-index:30; }
.sv-rmenu button{ display:block; width:100%; text-align:left; border:none; background:none; cursor:pointer; font:500 13px var(--f);
  color:#fff; padding:8px 10px; border-radius:7px; }
.sv-rmenu button:hover{ background:rgba(255,255,255,.06); }

@media (max-width:480px){
  /* .sv-well's height came out of here too — a SECOND escaped hardcode, same class as the 120px one
     in the base rule. The shared ladder already answers this width (96 at <=760, 52 at <=620), and
     a flat 84 here overrode both. Deleted, not restyled. */
  .sv-transport{ min-height:72px; gap:12px; }
  .sv-tbtn{ width:44px; height:44px; }
}

/* ====================== CREATE / EDIT / LINK-READY ====================== */
.st-card.auto{ height:auto; }   /* Create/Edit/Ready size to content; the Studio view keeps the fixed 800 */
.st-mutedUrl{ flex:0 0 auto; font-family:var(--ui); font-size:11.5px; color:rgba(255,255,255,.4); margin-top:14px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.st-shareRow{ display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:14px; }
.st-shareTxt{ min-width:0; padding-right:12px; }
.st-shareT{ font-size:13.5px; font-weight:500; color:#fff; }
.st-shareH{ font-size:11.5px; color:rgba(255,255,255,.5); margin-top:1px; }
.st-sw{ flex:none; display:inline-flex; align-items:center; width:42px; height:25px; border-radius:999px; background:rgba(255,255,255,.18); padding:2.5px; cursor:pointer; border:none; justify-content:flex-start; transition:background .15s; }
.st-sw .knob{ width:20px; height:20px; border-radius:50%; background:#fff; }
.st-sw[aria-checked="true"]{ background:#30D158; justify-content:flex-end; }
.st-sw[aria-checked="true"] .knob{ box-shadow:0 1px 3px rgba(0,0,0,.4); }
/* ONE DESTRUCTIVE RED, AND NO LITERAL (Sid, 2026-08-15). This carried a bare #FF453A — the bright
   system red that (fi) deliberately kept out of expiry — which is why delete was two different reds
   depending on which rule won. --red-del #E0403A everywhere now. Contrast on the card ground is
   4.07:1, accepted knowingly: delete should not be easy to find, and the label says "delete". */
.st-delLink{ display:inline-flex; align-items:center; gap:6px; border:none; background:none; cursor:pointer; padding:0; font:600 12.5px var(--f); color:var(--red-del); }
/* link-ready beat */
#pmReady{ flex:0 0 auto; display:flex; flex-direction:column; }
.st-readyCheck{ width:54px; height:54px; border-radius:50%; background:rgba(48,209,88,.14); display:flex; align-items:center; justify-content:center; margin:22px auto 0; }
.st-readyTitle{ text-align:center; font-weight:700; font-size:19px; letter-spacing:-.3px; color:#fff; margin-top:14px; }
.st-readyUrl{ margin-top:9px; padding:8px 13px; }   /* slim to match the name/message field height */
/* post-save confirmation: green tick + "saved" on a transparent button (green = success, never a green fill) */
#saveBtn.saved{ background:transparent; box-shadow:none; border:1px solid rgba(48,209,88,.45); color:#30D158; }
.st-readyCue{ text-align:center; font-size:12px; color:rgba(255,255,255,.45); margin-top:8px; }

/* mobile gesture scoping: the well is the scrubber (horizontal drag, no browser gesture);
   the lists keep normal vertical scroll but never bubble overscroll to the page (no pull-to-refresh) */
.sv-well, .st-well{ touch-action:none; }
.sv-list, .st-list{ overscroll-behavior:contain; }

/* MOBILE stage: the card spans nearly the full viewport (14px side margins — the desktop 56px
   gutters made it feel shrunken); bottom keeps the need-help pill clearance */
@media (max-width:480px){
  .sb-stage{ padding:var(--card-top) 14px max(64px, calc(env(safe-area-inset-bottom) + 50px)); }
}

/* ============ MASTER LINK — panel tracklist (dh §1) ============================================
   The stack's running order, sitting where a single link shows just its one track. Rows are quiet
   furniture on solid surface-1 (GLOBAL card rule: list rows never go glassy); the waveform well
   above stays the hero. Drag handle + × mirror the studio list's affordances so nothing new has to
   be learned. Numbers/durations are Roboto Mono per the type rule. */
.st-tracks{ margin:2px 0 14px; }
.st-tkHead{ display:flex; align-items:baseline; justify-content:space-between; margin-bottom:6px; }
.st-tkCount{ font:500 11.5px/1 var(--ui); letter-spacing:.04em; color:rgba(255,255,255,.38); }
.st-tkAdd{ background:none; border:0; padding:0; cursor:pointer; font:600 12px Inter,sans-serif; color:var(--blue-text); }
.st-tkAdd:hover{ text-decoration:underline; }
.st-tkAdd[disabled]{ color:rgba(255,255,255,.28); cursor:default; text-decoration:none; }
.st-tkList{ display:flex; flex-direction:column; }
.st-tkRow{ display:flex; align-items:center; gap:9px; padding:7px 0; border-bottom:1px solid rgba(255,255,255,.1); }
.st-tkRow:last-child{ border-bottom:none; }
.st-tkRow.dragging{ opacity:.4; }
.st-tkDrag{ flex:none; cursor:grab; display:flex; padding:1px; touch-action:none; }
.st-tkDrag:active{ cursor:grabbing; }
.st-tkNum{ flex:none; width:15px; font:500 11px/1 var(--ui); color:rgba(255,255,255,.3); }
.st-tkName{ flex:1; min-width:0; font:600 13px Inter,sans-serif; color:#fff; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.st-tkDur{ flex:none; font:400 11px/1 var(--ui); color:rgba(255,255,255,.4); }
.st-tkX{ flex:none; background:none; border:0; padding:2px; cursor:pointer; display:flex; border-radius:6px; }
.st-tkX:hover{ background:rgba(255,255,255,.07); }
/* uploading rows: the per-track progress the sequential queue writes into */
.st-tkRow.pending .st-tkName{ color:rgba(255,255,255,.5); }
.st-tkRow.pending .st-tkDur{ color:var(--blue-text); }
/* ONE CAPTION VOICE UNDER THE QUEUE HEADER (Sid, 2026-08-02). The batch line ("received 2 files ·
   separate links") was rendering Roboto Mono while the caption directly beneath it ("uploading one
   at a time…") renders Inter — two families for two lines of the same furniture, stacked, on the
   same screen. They SHARE this declaration now rather than holding matching copies of it: the two
   have drifted apart once already, and a shared rule is the only version that cannot drift again.
   Family, weight and tracking are all inherited here, by both, for the same reason. */
.st-tkNote, .st-tkDiag{ font-size:11.5px; line-height:1.45; color:rgba(255,255,255,.38); }
.st-tkNote{ margin-top:8px; }
.st-tkNote.warn{ color:#FF9F0A; }
/* add-source sheet */
.st-tkPick{ margin-top:9px; padding:10px 11px; background:#2C2C2E; border-radius:11px; }
.st-tkPickFile{ width:100%; background:none; border:1px dashed rgba(255,255,255,.22); border-radius:9px;
  padding:9px; cursor:pointer; font:600 12.5px Inter,sans-serif; color:#fff; }
.st-tkPickFile:hover{ border-color:var(--blue); }
.st-tkPickHead{ margin:11px 0 5px; font:500 11px/1 var(--ui); color:rgba(255,255,255,.32); }
/* A SCROLL LANE, not a styled scrollbar — the delete-scrollbar-css ruling stands and nothing here
   touches ::-webkit-scrollbar. These rows are CLICK TARGETS, which is what makes this list
   different from the read-only ones: measured live, the list scrolls (269px of rows in 190px), the
   bar is an OVERLAY that paints over content, and the rows cleared a 0px lane — so on a machine set
   to "always show scroll bars", where the bar is opaque and hit-testable, the bar sits on top of
   every row's right edge and takes the click. That is Sid's "clicks land on or under the bar".
   15px is the widest classic macOS bar; an overlay bar is narrower, so clearing 15 clears both.
   Trade-off, stated: where the bar IS classic the browser already insets the content box, so the
   rows end up 30px in rather than 15. Safe in both directions, which is the point — the alternative
   (scrollbar-gutter:stable) is a no-op under overlay bars and would leave the reported bug alone. */
.st-tkPickList{ max-height:190px; overflow-y:auto; padding-right:15px; }
.st-tkPickRow{ display:flex; align-items:center; gap:9px; padding:7px 2px; cursor:pointer;
  border-bottom:1px solid rgba(255,255,255,.07); }
.st-tkPickRow:last-child{ border-bottom:none; }
.st-tkPickRow .nm{ flex:1; min-width:0; font:500 12.5px Inter,sans-serif; color:#fff; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.st-tkPickRow .du{ flex:none; font:400 10.5px/1 var(--ui); color:rgba(255,255,255,.4); }
/* how many copies of this track the collection holds. Sits WITH the name, not with the tick: it is
   a fact about the running order, and the tick is a fact about the row. */
.st-tkPickRow .x2{ flex:none; font:600 10.5px/1 var(--ui); color:var(--blue-text); }
.st-tkPickRow .tick{ flex:none; width:15px; height:15px; display:flex; align-items:center; justify-content:center; opacity:0; }
.st-tkPickRow.added .tick{ opacity:1; }
.st-tkPickRow.added .nm{ color:rgba(255,255,255,.45); }
/* the optimistic tick is already painted, so this must NOT dim the row — it only stops a second tap
   landing on a round trip that has not answered yet (which is how you get two copies from one
   intention). No opacity, no spinner: the row is supposed to look done, because it almost is. */
.st-tkPickRow.pending{ pointer-events:none; }
.st-tkPickEmpty{ padding:6px 2px; font-size:11.5px; color:rgba(255,255,255,.32); }
/* master links can't burn — the expiry menu drops "after 1 play" and this says why */
.st-burnNote{ margin-top:7px; font-size:11.5px; color:rgba(255,255,255,.38); }

/* ============ MASTER LINK — the studio stack row (dh §2) =======================================
   One row in Your Tracks: title + "N tracks · X days left". The chevron alone expands (tapping the
   row keeps doing what every other row does); the versions appear as indented sub-rows carrying a
   name, a duration and a reorder handle — and NOTHING else: the envelope owns the rules, so
   sub-rows have no play, no menu, no download of their own. */
.sv-chev{ flex:none; background:none; border:0; padding:3px; cursor:pointer; display:flex; border-radius:6px; transition:transform .18s ease; }
.sv-chev:hover{ background:rgba(255,255,255,.07); }
/* (open-state rotation is a QUARTER turn now, right -> down, and lives with the bare-glyph rules at
   the end of this file: closed points right, open points down. A 180deg flip here would have made
   an open row point UP, which is the pair (fc) ruled out.) */
.sv-sub{ padding:2px 0 8px 34px; border-bottom:1px solid rgba(255,255,255,.1); }
.sv-sub[hidden]{ display:none; }
.sv-subRow{ display:flex; align-items:center; gap:9px; padding:5px 0; }
.sv-subRow.dragging{ opacity:.4; }
/* THE HANDLE, one rule for both levels: 2px dot pairs (2 cols x 3 rows, 5px pitch) at half
   strength, the reference's treatment measured at top level AND inside the panel, where it is
   identical.
   The 6px padding rebuilds the 19x24 HIT AREA around the 7x12 dot field, so the grab target never
   shrank to the dots' own size; the negative margin cancels that padding FOR LAYOUT ONLY. Measured
   ink-edge to ink-edge, the padding was pushing the dots 6px in from the row's own padding edge and
   opening the dots->circle gap to 18 against the reference's 12. The hit area is unchanged — it
   simply overhangs into the row padding, where there is nothing to collide with. Box edges said 12
   and looked wrong; ink edges said 18 and were right. */
/* -webkit-user-select + touch-callout: on iOS a long press near text starts a SELECTION, so
   reaching for the dots highlighted the track title instead of grabbing the handle. touch-action
   already stopped the scroll gesture; it does nothing about selection. The dragging class extends
   the same suppression to the row for the length of the drag, because the finger drifts off the
   7x12 dot field almost immediately. */
.sv-drag, .sv-subDrag, .st-tkDrag{ flex:none; cursor:grab; display:flex; padding:6px; margin:-6px; opacity:.5;
  touch-action:none; -webkit-user-select:none; user-select:none; -webkit-touch-callout:none; }
.sv-row.dragging, .sv-subRow.dragging, .st-tkRow.dragging,
.sv-row.dragging *, .sv-subRow.dragging *, .st-tkRow.dragging *{
  -webkit-user-select:none; user-select:none; -webkit-touch-callout:none; }
.sv-drag:active, .sv-subDrag:active, .st-tkDrag:active{ cursor:grabbing; }
/* HOVER-REVEAL, SPACE RESERVED (fc) — top-level rows only. Hidden at rest so a resting list reads as
   titles alone; revealed when the pointer is on the row. Opacity, NOT display: the slot keeps its
   width, so the row does not shift under the pointer as the dots appear. Sub-rows are deliberately
   excluded: a collection's running order is manual by nature and its dots stay visible, and there is
   no hover on the phone, which is why reorder there is a long-press instead (fd). renderQueue hides
   the element outright in newest, where there is no order to author. */
/* HOVER-REVEAL RETIRED 2026-08-15 (Sid: "the drag handles have disappeared from every studio row").
   They had not disappeared — measured, the element was on every row with hidden=false and drag
   worked and persisted — but a handle at opacity 0 until the pointer is on the row is a handle
   nobody can find, and the order it authors is the order a recipient hears. The affordance is worth
   more than a resting list that reads as titles alone. .5 is the shared handle opacity from the rule
   above, so all three levels (row, sub-row, sheet) are one treatment again.
   The slot was ALREADY reserved for this at full width, which is why nothing shifts on desktop. */
/* ---- ONE RULE, SPLIT ON POINTER TYPE (Sid, 2026-08-15) -----------------------------------------
   Hide-until-hover on desktop; visible at rest wherever there is no hover to reveal it with. Both
   halves are CSS and nothing else: the sort gate is gone, the template's `hidden` is gone, and no
   JavaScript decides whether a studio row's grip exists — that attribute is what caused both of
   today's breakages (a ReferenceError that rendered ZERO rows, then a template `hidden` that
   outlived the line which used to clear it).
   The condition is `hover:hover`, not `pointer:fine`, and deliberately: the reason to hide the grip
   is that hovering will bring it back, so the capability that decides is the one that brings it
   back. A touch laptop or an iPad with a trackpad hovers, and gets the desktop behaviour, which is
   right — it has a pointer to reveal it with. Anything that cannot hover keeps the base .5 and never
   has to discover an invisible control.
   Base opacity stays on the shared rule above, so at rest a phone is showing the SAME .5 the
   sub-rows and the sheet's handles have always shown. This block is the only place that departs
   from it, and only where the departure can be undone by moving the pointer. */
/* THE SHEET'S ROWS JOIN THIS RULE (Sid, 2026-08-16), rather than getting one of their own: same
   grips, same law — hidden until hover where hovering exists, visible at rest where it does not.
   Adding the selector IS the reuse; a second block that agreed would be the drift this ruling has
   spent two days removing.
   The two inline `drag.style.visibility='hidden'` lines on uploading and failed rows still win over
   all of this, because an inline style beats any stylesheet — so a row that cannot be reordered
   still shows no grip, at either pointer type. */
@media (hover:hover){
  .sv-row .sv-drag, #svSheet .st-tkRow .st-tkDrag{ opacity:0; transition:opacity .12s; }
  .sv-row:hover .sv-drag, .sv-row .sv-drag:focus-visible, .sv-row.dragging .sv-drag,
  #svSheet .st-tkRow:hover .st-tkDrag, #svSheet .st-tkRow .st-tkDrag:focus-visible,
  #svSheet .st-tkRow.dragging .st-tkDrag{ opacity:.65; }
}

/* THE DOTS NEVER RENDER ON A PHONE (fd) — and display:none, not the opacity above, because the
   opacity version left a 19x24 handle that elementFromPoint still answered for: an invisible drag
   target on every row, on the one pointer type that has no hover to ever reveal it. Measured on
   this build before the change: opacity 0, hit test lands on .sv-drag. Taking the slot back is also
   what puts the glyph column on the drawn 24 at the row's own edge (3a: chevron 51, triangle 48,
   title 75 against a row at 43) — the desktop keeps its reserved column, which is what "space
   reserved" was written for. The sub-rows' handles go with them: their frames draw no dots either,
   and the collection sheet's own helper says "hold to reorder". */
/* AND THE PHONE GETS THEM BACK, 2026-08-15, same ruling. (fd)'s reason was sound for what it was
   fixing: at opacity 0 with no hover to ever reveal it, the handle was an INVISIBLE drag target that
   elementFromPoint still answered for, so display:none was the honest fix for an invisible control.
   It is not invisible any more — the rule above paints it at .5 on every pointer type — so the
   objection is spent, and what is left is a phone with no visible way to reorder at all. Long-press
   to lift (armLift) still works exactly as it did; this gives it something to point at.
   MEASURED CONSEQUENCE, stated because (fd) spent it deliberately: taking the slot back moves the
   phone row's glyph column, which is the 24 at the row's own edge that (fd) bought. That is the
   price of the affordance and it is Sid's to spend. */
@media (pointer:coarse){
  /* (the `display:flex; opacity:.5` that sat here is RETIRED, not moved: it existed to undo the
     display:none this block used to carry, and there is nothing left to undo. A phone now inherits
     the shared .5 from the base rule, and the hover block above simply does not match it — which is
     the whole point of splitting on `hover:hover` rather than restating the value twice.) */
  /* AND THE LONG PRESS HAS TO REACH THE APP, which on this build it never did. Measured on real
     Mobile Safari: holding a row — on its title OR on the handle — raises iOS's text selection with
     the Copy / Find Selection / Look Up callout, spanning three rows. armLift's timer is 500ms and
     iOS starts selecting at about the same moment, so the two race and selection wins; the lift
     never arms and nothing reorders. That is why "hold to reorder" has not worked on a phone.
     The rule above this block already suppresses selection on .sv-drag and on a row ONCE .dragging
     is set — and that is exactly one moment too late, because .dragging is set by the lift that the
     selection prevented. The suppression has to be in place BEFORE the press, so it goes on the row
     at rest, on coarse pointers only.
     THE COST, stated: a track title cannot be text-selected by finger any more. Copy of the thing
     anyone actually wants — the link — is in the row's own menu and is untouched. Inputs are
     exempted so the rename field keeps its caret, its selection and its callout. */
  .sv-row, .sv-subRow, .st-tkRow{ -webkit-user-select:none; user-select:none; -webkit-touch-callout:none; }
  .sv-row input, .sv-subRow input, .st-tkRow input,
  .sv-row textarea, .sv-subRow textarea, .st-tkRow textarea{
    -webkit-user-select:text; user-select:text; -webkit-touch-callout:default; }
}
/* EVERY TAP TARGET AT LEAST 44 (fd), VIA PADDED HIT AREAS — the glyph does not move.
   The rule is "44pt via padded hit areas around small glyphs (glyphs stay optically centred in the
   24px column)", so nothing here changes a drawn size or position: each control keeps its box and
   grows an invisible ::after that the finger lands on. Measured before, at 390: plus 36, avatar 30,
   row play 24, chevron 24, kebab 27x23.5. The row is 62 tall, so a 44 area sits inside it whole.
   SQUARE, NOT ROUND: a 44 circle centred on a glyph leaves its corners to whatever is behind, and
   a hit test across the box measured 71% reachable — which is a 44pt target only on paper.
   THE FILTER ROW REACHES 44 NOW, and it did not before this pass. Its band is well-bottom to
   list-top: measured 43.6px while the phone still stepped the spacing scale down, and 49.6px once
   "one scale everywhere" (Sid, 2026-08-14) put --sp-air back on 24. So the words and the sort
   control take a full 44+ of height inside their own band, without reaching into the first row.
   HORIZONTALLY they cannot, and that is the drawn layout rather than a build choice: "all" is
   14.6px of ink and its neighbour starts 8px later, so a 44-wide area per word would overlap the
   word beside it. Each takes half the gap on each side. Recorded, not forced. */
.sv-rplay, .sv-chev, .sv-rkebab, .st-plus, .st-avatar, .sv-fbtn, .sv-sortBtn, .sv-toggle{ position:relative; }
@media (pointer:coarse){
  .sv-rplay::after, .sv-chev::after, .st-plus::after, .st-avatar::after{
    content:''; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
    width:44px; height:44px;
  }
  /* the kebab's area grows LEFT from its own right edge instead of centring: the card is
     overflow:clip and a centred 44 ran 3px past the padding edge and came back 40 wide. 3a draws
     the same shape — its 44 slot starts 13px left of the glyph — so this is the drawn slot, not a
     dodge. */
  .sv-rkebab::after{ content:''; position:absolute; top:50%; right:0; transform:translateY(-50%);
    width:44px; height:44px; }
  /* THE FILTER BAND IS ASYMMETRIC, so these areas are anchored UPWARD rather than centred.
     Measured at 390: the well ends 24.8px above the words and the list starts 4.8px below them.
     A centred area therefore spends half its height on 4.8px of room and gets overpainted by the
     list — hit-tested at 36 tall while the CSS asked for 44. Reaching up into the well's own gap
     costs nothing (the well is an earlier sibling, so the area paints over it) and reaching down
     past 4.8 would take the first row's taps. 21 + 20 + 4 = 45 for a word, 19 + 21 + 4 = 44 for
     the sort control, and the toggle needs a little more above because it is only 16 tall. */
  .sv-fbtn::after, .sv-sortBtn::after{ content:''; position:absolute; inset:-21px -4px -4px; }
  .sv-toggle::after{ content:''; position:absolute; inset:-26px -7.5px -4px; }
}

/* THE LIFT (3e). Measured off the drawn frame: surface --menu #232427, radius 12, 1px --blue at
   40%, shadow 0 24px 60px rgba(0,0,0,.7), and the row scaled 1.02 with the ~0.6deg tilt the board
   draws (matrix 1.01994, 0.0106812). It is position:fixed from JS while lifted, so this only has
   to dress it — and z-index has to clear the sheet scrim and the well, not just its neighbours.
   ID-SCOPED ON PURPOSE: `.sv-sub + .sv-row` (line ~1203) re-declares border-top at the same (0,2,0)
   weight and sits LATER in the file, so a plain .sv-row.lifting lost its top edge alone and lifted
   a row with three blue sides and a grey one — caught by measuring the computed border, not by eye,
   because the lifted row is under a finger. #svList wins outright and cannot be undone by order. */
#svList .sv-row.lifting, .sv-subRow.lifting, .st-tkRow.lifting{
  z-index:40; background:var(--menu, #232427); border-radius:12px;
  border:1px solid rgba(228,111,64,.4); box-shadow:0 24px 60px rgba(0,0,0,.7);
  transform:scale(1.02) rotate(.6deg); pointer-events:none;
}
/* the hole it will drop into: a real slot in the flow, so the rows below actually move (3e draws
   the gap 63 tall with a 48 ring inside it — the row height and the ruled ring) */
.st-dropGap{ flex:0 0 auto; height:62px; display:flex; align-items:center; }
.st-dropGap::after{ content:""; display:block; width:100%; height:48px; border-radius:10px;
  border:1px solid rgba(228,111,64,.35); background:var(--drop-highlight, rgba(228,111,64,.12)); }
/* RESERVED, RIGHT-ALIGNED, and no wider than it needs to be (Sid, 2026-07-30). The column stays
   reserved so titles keep one left edge at ten-plus tracks — that ruling stands — but the numeral
   now sits at its RIGHT edge, snug against the gap to the title, which is what the reference's
   hugging column does visibly. 2ch with tabular figures IS the two-digit width exactly, so the
   old fixed 14px (3.4px wider than "15" needs) stops donating slack to the gap, and the digits
   column-align down the list as a bonus. See the width note in the type block below. */
.sv-subNum{ flex:none; font:500 10.5px/1 var(--ui); color:rgba(255,255,255,.28);
  /* AFTER the shorthand, always: `font:` resets font-variant, so declaring tabular figures above
     it silently does nothing. `2ch` resolves against the element's FINAL computed font (Inter at
     --t-caption, set further down), not this line's. */
  /* Reserved from the collection's OWN largest number (--num-ch, set per panel in studio.js),
     not from the 15-track cap. Reservation exists so titles share one left edge WITHIN a list, and
     the width that achieves that is the widest number IN that list — a flat 2ch made every
     single-digit collection donate a digit of slack to the circle->number gap for a two-digit case
     it does not have. 3 tracks: 1ch, and both gaps measure 12 like the reference. 12 tracks: 2ch,
     titles align, and the gaps stay 12 because the ink now fills the column. */
  width:calc(var(--num-ch, 2) * 1ch); text-align:right; font-variant-numeric:tabular-nums; }
.sv-subName{ flex:1; min-width:0; font:500 12.5px Inter,sans-serif; color:rgba(255,255,255,.82);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.sv-subDur{ flex:none; font:400 10.5px/1 var(--ui); color:rgba(255,255,255,.35); }
/* (the blue release stripe that ran the envelope row and its expansion is RETIRED — downloads-on is
   words in the meta line now, so there is no stripe to carry down into the sub-rows.) */

/* whole-surface drop target (bug: a file dropped off the small well made the BROWSER open the file
   and the studio vanished). The card lights its edge so it's obvious the drop will land. */
/* THE OUTLINE MOVED TO THE CARD with the target itself (Sid, 2026-08-14). It used to trace the two
   view containers, which is precisely why it drew a line UNDER the header and left the + outside
   the lit area — the picture of the bug: the one control that says "add audio" sat outside the
   only region that accepted one. Insets are POSITIVE here, unlike the -6 the inner containers
   could afford: .st-card is overflow:clip, so a negative inset would be clipped away and the
   outline would never paint. 3in / 19r keeps it concentric with the card's own 22. */
#studioBox.dropping{ position:relative; }
#studioBox.dropping::after{
  content:''; position:absolute; inset:3px; border:1.5px dashed var(--blue); border-radius:19px;
  background:rgba(228,111,64,.06); pointer-events:none; z-index:5; }

/* ---- the link-cap door ----------------------------------------------------------------------
   The capped strip takes "upload another track"'s slot and window finish, so the card's shape
   holds; only the meaning changes. The message is quiet caption ink — it is information, not an
   alarm — and the waitlist action is smaller still: the primary move on a full account is
   deleting a link, and the visual weight says so. */
.sv-capNote{ flex:0 0 auto; margin-top:16px; padding:10px 13px; border-radius:11px; clip-path:inset(0 round 11px); }
.sv-capMsg{ font:500 var(--t-caption)/1.45 Inter,sans-serif; color:rgba(255,255,255,.6); }
.sv-capAction{ margin-top:6px; }
.st-capNotify{ background:none; border:0; padding:0; cursor:pointer; font:600 var(--t-mono)/1.4 Inter,sans-serif;
  color:rgba(255,255,255,.45); text-decoration:underline; text-underline-offset:2px; }
.st-capNotify:hover{ color:rgba(255,255,255,.65); }
/* tapped: a quiet receipt, not a control — no underline, no hover invitation */
.st-capNotify[disabled]{ cursor:default; text-decoration:none; color:rgba(255,255,255,.4); }
/* the panel's cap note hosts the same action inline after the message */
.st-tkNote .st-capNotify{ margin-left:4px; }

/* ---- separate-links wizard: the step marker + the done receipt ------------------------------
   The marker takes the SAME quiet blue caption treatment as .st-namePrompt — both say "there is
   one thing to do on this screen", so they read as one voice rather than two. The receipt's rows
   reuse .st-url's finish (the same well the copy-link box sits in) so a list of links looks like
   the link box the sender already knows. */
/* THE STEP MARKER IS THE SCREEN'S HEADING (Sid, 2026-08-14), and it has left the pinned bar. The
   2026-08-02 reading was that it is meta ABOUT the action, so it took the panel's label size (13,
   medium, muted) and sat hard left in the bar. On a walk of twelve it is the only thing saying
   where you are, and at 13px muted in the bottom corner it was a footnote. It heads the step, so
   it goes above the field it heads and takes a heading's size.
   THE STEP IS --t-title (22/700), and it is an existing step on the shared scale, not a size picked
   by eye. Alternatives considered, all declared: --t-panellabel (13, where it was — no lift at
   all); --t-caption (13, the same number wearing a shared name); --t-body (14.5, a lift, but still
   the register of a field label rather than a heading). --t-title wins because the wizard already
   HAS a heading step on its other screen — .st-sepDoneTitle, the receipt's "N separate links", is
   --t-title/700 — so the walk and its receipt now carry one heading treatment instead of two.
   Inter, per (ez): the retired mono never applied here, and the ruled tracking comes with the step.
   It wraps rather than truncating now. In the bar it had to yield to the button on one row; above
   a full-width field there is no row to protect, and a heading that ellipses its own count would
   defeat the reason it was moved. */
.st-sepStep{ font:var(--t-w-bold) var(--t-title)/1.15 var(--ui); letter-spacing:var(--t-title-ls);
  color:#fff; margin:0 0 var(--sp-block); }
#pmSepDone{ flex:0 0 auto; display:flex; flex-direction:column; }
.st-sepDoneTitle{ font:700 var(--t-title)/1.15 Inter,sans-serif; letter-spacing:var(--t-title-ls);
  color:#fff; margin:4px 0 var(--sp-block); }
/* NO second scroller: #viewPanel already scrolls inside the capped card, which is the panel's own
   law for create/edit. A nested one would trap the wheel over the rows and leave the card's edge
   saying nothing. */
.st-sepList{ display:flex; flex-direction:column; gap:8px; }
/* the apps invitation under the wizard's finish button. Typography comes from notice.js (.vc-apps),
   one source for every surface that shows it; only the spacing is this surface's business. */
.st-appsLine{ margin-top:18px; }
.st-sepRow{ flex:0 0 auto; display:flex; align-items:center; gap:10px;
  background:rgba(255,255,255,.07); border:1px solid rgba(255,255,255,.18); border-radius:11px; padding:9px 13px; }
.st-sepInfo{ flex:1; min-width:0; }
.st-sepName{ font:600 var(--t-caption)/1.25 Inter,sans-serif; color:#fff;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.st-sepUrl{ font-family:var(--ui); font-size:var(--t-mono); letter-spacing:var(--t-mono-ls);
  color:rgba(255,255,255,.55); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:3px; }
.st-sepCopy{ flex:none; border:1px solid rgba(255,255,255,.18); background:transparent; color:#fff;
  border-radius:9px; padding:6px 13px; cursor:pointer; font:600 var(--t-caption) var(--f); }
.st-sepCopy:hover{ background:rgba(255,255,255,.07); }
/* green = the receipt, never a green fill (same rule as "saved" on the edit row) */
.st-sepCopy.done{ border-color:rgba(48,209,88,.45); color:#30D158; }

/* ---- panel: name prompt, preview skips, picker tabs, active track row (feedback round 1) ---- */
.st-namePrompt{ font:600 11.5px/1 Inter,sans-serif; color:var(--blue-text); margin:0 0 6px 2px; }
.st-tkRow.on{ background:#2C2C2E; border-radius:8px; border-bottom-color:transparent; }
.st-tkRow.on .st-tkName{ color:#fff; }
.st-tkRow.on .st-tkNum{ color:var(--blue-text); }
.st-tkRow:not(.pending){ cursor:pointer; }
.st-tkPickTabs{ display:flex; gap:6px; margin-bottom:10px; }
.st-tkTab{ flex:1; background:none; border:1px solid rgba(255,255,255,.14); border-radius:9px;
  padding:7px 6px; cursor:pointer; font:600 12px Inter,sans-serif; color:rgba(255,255,255,.6); }
.st-tkTab.on{ border-color:var(--blue); color:#fff; background:rgba(228,111,64,.12); }
.st-tkPickHint{ margin-top:7px; text-align:center; font-size:11px; color:rgba(255,255,255,.32); }
/* disabled expiry option: visible, so the reason lands on the option itself */
.st-menu .mi.off{ opacity:.4; pointer-events:none; }
/* delete reads destructive in the row menu too — the third of the three places */
.sv-rmenu .mi-del{ color:var(--red-del); }
/* a collection's row: a second ring behind the play button says "there's more than one in here" */
.sv-row.stack .sv-rplay{ position:relative; }
.sv-row.stack .sv-rplay::before{ content:''; position:absolute; inset:-4px -4px -4px 4px; border-radius:50%;
  border:1.5px solid rgba(255,255,255,.22); z-index:-1; }
.sv-row.stack .sv-rplay::after{ content:''; position:absolute; inset:-7px -7px -7px 7px; border-radius:50%;
  border:1px solid rgba(255,255,255,.12); z-index:-1; }
/* drag a single track's row onto a collection row to add it */
.sv-row.dropTarget{ background:rgba(228,111,64,.14); border-radius:8px; box-shadow:inset 0 0 0 1.5px var(--blue); }
/* THE DROP-ZONE SPLIT (Sid, 2026-08-02). Two targets on one list, so they have to be unmistakable
   at a glance: the ROW highlights when a drop would merge (above), and this line shows where a drop
   would land when it would reorder. One or the other is on, never both — a drag that showed a
   highlight AND a line would be asking the user to guess.
   Absolutely positioned inside the scroller and offset by its scrollTop, so it stays on the
   boundary it names while the list scrolls under it. It is a receipt, not a control: no pointer
   events, so it can never sit between the finger and a row. */
.sv-list, #viewPanel .st-tkList{ position:relative; }
.st-dropLine{ position:absolute; left:0; right:0; height:2px; margin-top:-1px; z-index:4;
  background:var(--blue); border-radius:2px; pointer-events:none; }
.st-dropLine::before{ content:''; position:absolute; left:0; top:-3px; width:8px; height:8px;
  border-radius:50%; background:var(--blue); }
/* the same receipt on the panel's queue row: the picker's tick says "it is in", and this is where
   it lands. Scrolled into view by revealTrack, because a 3.8-row window appends out of sight. */
.sv-row.justAdded, #viewPanel .st-tkList .st-tkRow.justAdded{ animation:svAdded 1.1s ease; }

/* ---- THE MERGE IS WORKING, and it says so ---------------------------------------------------
   Founding a collection copies audio on the storage side: measured against the live box, a
   single-onto-single drop waits 2219ms (promote + tracks/from, one blob copy each) and a
   single-onto-collection drop 1275ms cold. That is several seconds of a list that looks exactly
   as it did before the drop, which reads as "nothing happened" — and a second drag is the
   reasonable response to it.
   (A reorder is 29ms and copies nothing. It gets none of this: an overlay on a fast path is
   noise that teaches you to ignore overlays.)

   NO LAYOUT SHIFT is the constraint: .sv-row is already position:relative, so the pill floats
   INSIDE the row it is about and nothing below it moves. */
.sv-row.committing{ background:rgba(228,111,64,.14); border-radius:8px; box-shadow:inset 0 0 0 1.5px var(--blue); }
/* the row being given away recedes; it is still there, it is just no longer the subject */
.sv-row.giving{ opacity:.4; transition:opacity .18s ease; }

/* A POSITIONING LAYER, not a curtain. The first pass scrimmed the whole row, and the row's own
   title read straight through it — two texts competing inside one 44pt row. The row's content
   recedes instead (below) and a compact pill carries the message on its own ground. */
/* The scrim lives HERE, on the overlay, not as opacity on the row's children. Dimming the
   children looked right in the selector and lost in the cascade: .sv-chev took the .28 and
   .sv-rplay / .sv-rinfo / .sv-rkwrap kept their own later rules at 1, so half a row receded and
   half did not (measured, not guessed). One layer with one background cannot half-apply, and it
   costs no !important fight with rules that are correct for every other state. */
.sv-merge{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  border-radius:8px; z-index:3; pointer-events:none; background:rgba(16,16,19,.78); }
.sv-merge > .sv-mergePill{ display:flex; align-items:center; gap:9px; max-width:calc(100% - 24px);
  padding:6px 14px; border-radius:999px; background:#101013; border:1px solid rgba(255,255,255,.12);
  box-shadow:0 6px 18px rgba(0,0,0,.5);
  font:500 12.5px var(--ui); color:rgba(255,255,255,.78); letter-spacing:var(--t-tracking);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* the failure state takes clicks, because it carries the only way to dismiss it */
.sv-merge.err > .sv-mergePill{ color:var(--coral,#FF453A); border-color:rgba(255,69,58,.35);
  pointer-events:auto; white-space:normal; }
.sv-merge .sv-mergeX{ pointer-events:auto; background:none; border:1px solid rgba(255,255,255,.22);
  color:rgba(255,255,255,.72); border-radius:999px; font:600 11px var(--ui); padding:3px 10px; cursor:pointer; }
.sv-merge .sv-mergeX:hover{ border-color:rgba(255,255,255,.5); color:#fff; }

/* the disc: the product's own mark, turning. A record is what this app is named after, so the
   waiting glyph is that and not a generic spinner. */
.sv-disc{ width:15px; height:15px; border-radius:50%; flex:0 0 auto;
  border:1.5px solid rgba(255,255,255,.30); border-top-color:rgba(255,255,255,.85);
  position:relative; animation:svSpin .9s linear infinite; }
.sv-disc::after{ content:''; position:absolute; inset:5px; border-radius:50%;
  background:rgba(255,255,255,.55); }
@keyframes svSpin{ to{ transform:rotate(360deg); } }
@media (prefers-reduced-motion:reduce){ .sv-disc{ animation-duration:2.4s; } }
@keyframes svAdded{ 0%{ background:rgba(228,111,64,.30); } 100%{ background:transparent; } }

/* ---- round 2: stack row, filter, sub-row play, rename, panel auto-play ---- */
/* a collection has no play button — the chevron occupies that slot and IS the marker */
.sv-row.stack .sv-rplay{ display:none; }
/* NO order:-1. It pulled the chevron to the visual FRONT of the row, ahead of the drag handle, so a
   collection read chevron-first / dots-inward while a solo read dots-first. Moving the chevron in
   the markup could never fix that on its own — flex `order` beats DOM order, and a DOM-order
   assertion passes while the screen shows the opposite. Source order now governs: drag handle
   outermost on both row kinds, chevron in the slot the solo's play button occupies. The round grey
   button treatment stays; only the reordering goes. */
.sv-row.stack .sv-chev{ width:28px; height:28px; border-radius:50%; background:#2C2C2E;
  align-items:center; justify-content:center; margin-right:2px; }
.sv-row.stack .sv-chev:hover{ background:#3A3A3C; }
.sv-row.stack .sv-rplay::before, .sv-row.stack .sv-rplay::after{ content:none; }   /* stacked rings retired */
/* sub-rows gain their own small play button */
.sv-subPlay{ flex:none; width:22px; height:22px; border-radius:50%; border:0; cursor:pointer;
  background:var(--blue); display:flex; align-items:center; justify-content:center; padding:0; }
.sv-subPlay:hover{ filter:brightness(1.1); }
.sv-subRow.playing .sv-subName{ color:#fff; font-weight:600; }
/* all · collections filter */
.sv-filter{ display:flex; align-items:center; gap:5px; margin-right:9px; }
.sv-fbtn{ background:none; border:0; padding:2px 1px; cursor:pointer; font:500 11.5px Inter,sans-serif;
  color:rgba(255,255,255,.38); }
.sv-fbtn.on{ color:#fff; }
.sv-fdot{ color:rgba(255,255,255,.22); font-size:11px; }
/* inline rename */
.st-tkEdit{ flex:none; background:none; border:0; padding:2px; cursor:pointer; display:flex; border-radius:6px; opacity:.6; }
.st-tkEdit:hover{ opacity:1; background:rgba(255,255,255,.07); }
.st-tkRenameIn{ flex:1; min-width:0; background:#2C2C2E; border:1px solid var(--blue); border-radius:7px;
  padding:3px 7px; font:600 13px Inter,sans-serif; color:#fff; outline:none; }
/* the auditioning row shows its spec where the duration was */
.st-tkRow.spec .st-tkDur{ color:var(--blue-text); }
/* panel auto-play, positioned like the studio player's (play stays centred) */
/* (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) */
/* TOGGLE COLOUR RULE: green = playback preference (auto-play), blue = link setting (downloads) */
/* the two auto-play switches used to pin the STATUS green here, at a specificity that outranked
   the shared component (.sv-toggle.green is 3 selectors; #svAuto is an id). Both are auto-play, so
   both take --green-on from the component in sendbox.css and these overrides are gone. #dlToggle is
   NOT auto-play — it is the downloads switch — and keeps the status green from the .sv-toggle
   base above, deliberately: Sid's ruling is about the auto-play control. */

/* ---- round 3 ---- */
/* titles show in FULL and wrap to two lines; they are the play target (tap-to-expand retired) */
.sv-rname, .sv-subName, .st-tkName{ white-space:normal; overflow:visible; text-overflow:clip;
  overflow-wrap:anywhere; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden; }
.sv-rname, .sv-subName, .st-tkName{ cursor:pointer; }
/* failed / cancelled upload rows: keep their place, say why, offer a retry */
/* flex-wrap is LOAD-BEARING here: .st-tkWhy asks for flex-basis:100% to sit on its own line under
   the name, and on a row that cannot wrap that request is served by shrinking every sibling instead
   — the track name collapsed to zero width and the row said why it failed without ever saying WHICH
   file failed (Sid, 2026-08-03). The reason belongs under the name, not in place of it. */
.st-tkRow.failed{ align-items:flex-start; flex-wrap:wrap; }
.st-tkRow.failed .st-tkName{ color:rgba(255,255,255,.5); }
.st-tkWhy{ flex-basis:100%; order:9; font-size:11px; line-height:1.4; color:#FF9F0A; padding:2px 0 0 24px; }
.st-tkRetry{ order:8; flex:none; background:none; border:1px solid rgba(255,255,255,.2); border-radius:999px;
  padding:2px 9px; cursor:pointer; font:600 11px Inter,sans-serif; color:#fff; }
.st-tkRetry:hover{ border-color:var(--blue); }
.st-tkAbandon{ background:none; border:0; padding:0; cursor:pointer; font:600 11.5px Inter,sans-serif; color:#FF453A; }
.st-tkAbandon:hover{ text-decoration:underline; }
/* the "switched to custom order" notice */
/* .sv-toast now lives in sendbox.css — the stylesheet BOTH this surface and the player load —
   because the owner preview needed to say "couldn't copy" and there was no toast on that page. One
   definition, two surfaces, rather than a second pill that has to look the same by hand. */
/* empty collections filter state */
/* THE FILTER ROW CENTRES IN ITS OWN HEIGHT (Sid, 2026-08-15). This used to say align-items:baseline,
   which existed to sit .st-sectlabel on the same baseline as the filter buttons. That label is
   display:none now (see "belt: any cached markup still carrying it stays quiet" further down), so
   baseline was aligning something that no longer renders — and with the toggle being the tallest
   item in the row, baseline drops everything toward the divider beneath it. The rule outlived its
   reason. .sv-tracksHead already declares align-items:center where it is defined; this override is
   simply removed rather than restated, so there is one answer and not two. */
.st-sectlabel, .sv-fbtn{ font-size:11.5px; letter-spacing:.06em; }
/* short spec form inside rows: "44.1 / 24" */
.sv-loadedFmt .short{ font-family:var(--ui); }

/* ---- what a batch becomes (one collection vs separate links) ----
   The inline pills that used to live here are GONE: they were missable, and a first-time user
   sailed straight past them into an outcome they hadn't chosen. The question is now the
   #qmodeModal dialog, styled by the shared .acmodal rules above. */
/* the row whose file is going up right now — the well names it, this makes it findable in the list */
.st-tkRow.uploading{ background:rgba(228,111,64,.10); box-shadow:inset 2px 0 0 var(--blue); }
.st-tkRow.uploading .st-tkName{ color:#fff; }
.st-tkRow.uploading .st-tkDur{ color:var(--blue-text); font-weight:600; }

/* temporary drop diagnostic: how many files the browser actually handed over */
/* furniture in the queue header, not a headline of its own. Its LAYOUT only — the type comes from
   the shared caption rule above, which is the whole point of the 2026-08-02 fix. */
.st-tkDiag{ flex:0 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  text-align:center; padding:0 8px; }

/* an unanswered batch's collection: quiet, one line, one tap out of it. Deliberately NOT a warning
   colour — nothing is broken, it is just an outcome the producer never actually picked. */
.sv-rflag{ font:500 11px Inter,sans-serif; color:var(--ink-2); margin-top:3px; line-height:1.35; }
.sv-rsplit{ background:none; border:0; padding:0; cursor:pointer; font:600 11px Inter,sans-serif;
  color:var(--blue-text); text-decoration:underline; text-underline-offset:2px; }
.sv-rsplit:hover{ color:#fff; }
.sv-rsplit[disabled]{ color:var(--ink-2); text-decoration:none; cursor:default; }

/* ============================================================================
   PANEL — collection visual pass (items 1, 2, 5, 8). Appended at the END so these
   rules win outright; this section carries its own phone block at the bottom.
   ============================================================================ */

/* item 2 — the auditioning row's tech line, matched to the recipient player. Quiet mono, NOT
   blue: blue is the interactive tint here too, the row number already carries it to mark
   "playing", and two blues on one row is noise. (Was blue since round 2; unified now that the
   same treatment has to read identically on both surfaces.) */
.st-tkRow.spec .st-tkDur{ color:rgba(255,255,255,.6); letter-spacing:-.2px; }

/* item 8 — the standing drop meaning on a finished single's well. Centred, quiet, and clearly a
   label rather than a control: it sits on the well's scrim, not on the waveform's business. */
/* a CAPTION under the well, same register as the panel's other captions (.st-pmFmt / .st-tkNote):
   centred, quiet, no pill and no background. It was a chip ON the waveform, competing with the two
   time readouts for the tightest space on the card. */
.st-wellHint{ text-align:center; margin-top:9px; font:400 11.5px/1.3 Inter,sans-serif;
  color:rgba(255,255,255,.4); }

/* item 5 — the panel's tracklist is the same WINDOW as the recipient's: recessed edge, its own
   inset ground, bottom fade, a half-cut row at overflow. item 1's "no stray scrollbar" is the
   same rule: the fade is the affordance, so the scrollbar is removed rather than styled. */
.st-tkList{ border-radius:10px; padding:1px 10px;
  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); }
/* THE WINDOW IS MEASURED, NOT DECLARED (Sid, 2026-08-02 — the same rule the player's tracklist
   already follows). `calc(37px * 3.8)` was a magic number that only ever matched one kind of row:
   these rows are min-height 44 and render at ~47, so 140.6px came to exactly 3.0 of them — a whole
   row cut cleanly at the edge, which makes a 5-track queue read as a 3-track one. studio.js sizes
   it from the row it actually rendered, to a half-row peek, and sets .scroll from real overflow
   rather than from a track count (see fitPanelWindow). */
.st-tkList.scroll{ overflow-y:auto; scrollbar-width:none; }
/* and the fade is a claim about what is below, so it goes when there is nothing below — .more is
   set from the scroll position, exactly as on the player's list */
.st-tkList.scroll.more{
  -webkit-mask-image:linear-gradient(180deg,#000 0,#000 calc(100% - 8px),transparent 100%);
  mask-image:linear-gradient(180deg,#000 0,#000 calc(100% - 8px),transparent 100%); }
.st-tkList.scroll::-webkit-scrollbar{ width:0; height:0; }
/* A CAPPED LIST MUST SCROLL, NOT SQUASH. .st-tkList is a flex column, so its rows are flex items
   and shrink before the container will ever overflow — the moment the window's max-height arrived
   in the sheet, seven 40px rows quietly became seven 25.7px rows and "all seven fit" was true for
   the wrong reason. The panel never showed this because its rows carry min-height:44, which floors
   the shrink; the sheet's rows declare a flat height:40 and had nothing holding them up.
   flex:0 0 auto on the row, not another min-height, so each home keeps its own drawn height. */
.st-tkList .st-tkRow{ flex:0 0 auto; border-bottom:1px solid rgba(255,255,255,.055); }
.st-tkList .st-tkRow:last-child{ border-bottom:none; }

/* ---- item 7: the same three-step scale on the panel ------------------------------------------- */
.st-player + .st-tkDiag{ margin-top:14px; }
#viewPanel .st-tracks{ margin:16px 0 18px; }        /* transport/spec → the window → the fields */
#viewPanel .st-tkHead{ margin-bottom:8px; }
#viewPanel .st-pmFmt{ margin-top:10px; }
#viewPanel .st-tkNote{ margin-top:10px; }
@media (max-width:480px){
  #viewPanel .st-tracks{ margin:12px 0 14px; }
}

/* ---- one type treatment across the tracks-header row ----------------------------------------
   "your tracks" and "all · collections" were ALREADY the same size with the same letter-spacing
   (11.5px / 0.69px, measured). The row read uneven because the label is Roboto Mono and the filter
   is Inter, and mono's advance width is simply wider — so tightening letter-spacing would have
   chased the symptom and never matched. Same family and weight as the filter; the label keeps its
   quieter colour, because it labels the row rather than acting on it. */
.sv-tracksHead .st-sectlabel{ font-family:Inter,-apple-system,sans-serif; font-weight:500; }

/* ---- auto-play joins the transport CLUSTER (all three surfaces identical) ---------------------
   It was position:absolute right:0, which parked it hard against the card edge, wide of the
   buttons and optically high — a stray control rather than part of the transport. Now it is a
   normal flex member sitting one row-gap from the next arrow, align-self:center so the toggle and
   its label share the arrows' centreline.
   PLAY STAYS DEAD-CENTRE: taking it out of absolute positioning would otherwise push the hero left
   by its own width, so the row carries a mirrored 38px spacer when auto is present. 38px is the
   control's measured natural width (36.2px label, the widest child) rounded up — the spacer and the
   control must be equal or the centring is a near-miss. — the "keep these numbers in step with the
   .vp-auto block in player.css" instruction that used to end this note is gone with the copies:
   the control is ONE component in sendbox.css, which the player DOES 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) */
/* the spacer exists ONLY while auto does, or a hidden control would shift the cluster off-centre */
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */


/* ---- ONE WINDOW COMPONENT: the studio's expanded sub-list gets the recipient tracklist's
   treatment — recessed inset edge, its own darker ground, a bottom fade over the cut row. An
   expansion used to be flat rows on the card, so a long collection just ran off with nothing
   saying so; the two surfaces now read as the same object.
   SCROLLBAR CLEARANCE: it grazed the track timings. Thin overlay style with the gutter reserved in
   padding, so the mono durations never sit under it. ---- */
.sv-sub{ position:relative; margin:2px 0 8px 34px; padding:2px 0 4px; border-bottom:none;
  border-radius:10px; 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); }
/* (the old padding-left:10/padding-right:16 "scrollbar lane" rule lived here — dead since the
   panel pass set the shorthand 10px 12px on the same selector later in the file, and the lane
   itself is gone now the scrollbar is the OS overlay, which draws over content by design) */
.sv-sub.scroll{ overflow-y:auto; max-height:calc(31px * 3.8);
  -webkit-mask-image:linear-gradient(180deg,#000 0,#000 calc(100% - 8px),transparent 100%);
  mask-image:linear-gradient(180deg,#000 0,#000 calc(100% - 8px),transparent 100%); }
/* same law as the studio list: the reference has NO scrollbar styling, so neither does the
   expansion window — native overlay, hidden at rest, visible only while scrolling */
/* (release-stripe offset retired with the stripe itself) */

/* ---- an OPEN collection is ONE block ----------------------------------------------------------
   The row divider normally sits under every top-level row. On an expanded collection that line fell
   between the header and its OWN sub-rows, slicing the block it belongs to in half and making the
   tracks look like they belonged to whatever came next. When open, the header's line goes and the
   separator moves BELOW the window — marking where the next TOP-LEVEL row begins, which is what a
   separator is for. Collapsed rows are untouched.
   It is drawn on the sub's ::after (::before is the blue release stripe) and pulled back out of
   the indent so it spans the full row width like every other divider. The pull-back MIRRORS the
   panel's own margins via --sub-indent/--sub-outdent: it was a bare -34px, the indent of the
   generation that wrote it, so when the panel moved to 60px the line started 26px short of the
   left edge and nobody's harness looked (Sid's report, 2026-07-30 — the reference runs it to the
   row edge on both sides, measured). */
.sv-row.open{ border-bottom-color:transparent; }
/* THE SEPARATOR IS DRAWN BY THE NEXT ROW, not by the panel, and that is the fix for the horizontal
   scrollbar Sid photographed across this panel's bottom edge. It used to be an ::after on .sv-sub
   pulled out of the indent to span the full row width — `right:-7px` at desktop, `-6px` at 390,
   which is 7px (6px) of SCROLLABLE OVERFLOW inside a box that becomes overflow-y:auto the moment a
   collection has more than four tracks. overflow-x then computes to auto beside it, and a machine
   set to "always show scroll bars" draws a full-width classic bar for those 7px. Measured before
   the fix: client 298, scrollWidth 305, and 12px of vertical overflow from `bottom:-12px` on top.
   A pseudo-element of the SCROLLER can only ever be inside the scroller, so no offset fixes this —
   the line has to belong to something else. The next top-level row is the honest owner anyway: the
   separator marks where that row begins, it spans the full width for free, and where there is no
   next row there is correctly no line (which is the same rule .sv-row:last-child already follows).
   The panel's 12px margin-bottom is what puts the line exactly where the ::after drew it. */
.sv-sub + .sv-row{ border-top:1px solid var(--row-line); }

/* item 4: the tracks header is now filter (left) + sort (right) — the label is gone, so the row no
   longer needs the label's baseline alignment to hold it together. */
.sv-tracksHead .sv-filter{ margin-right:auto; }
.sv-tracksHead .st-sectlabel{ display:none; }   /* belt: any cached markup still carrying it stays quiet */

/* ---- TYPE SCALE on the panel, so the family stays matched with the player cards --------------
   Same four voices, same tokens (declared here too: the studio never loads player.css).
   The SPACING tokens are declared here for the same reason — they were defined only in player.css,
   so every `var(--sp-*)` written in this file silently resolved to nothing and the rule did not
   apply at all. That is how the panel and studio-preview title->spec air measured 0px while the
   player card measured 20px: not a tuning difference, a dead variable. */
:root{
  /* THE STUDIO'S OWN VOICES. The four shared type voices, their tracking, the weights and the five
     spacing steps moved to tokens.css on 2026-08-01 so the player inherits ONE scale instead of
     agreeing with a copy of it — see the note there. What is left here is what only the studio has.

     THE ROW BLOCK, RE-MEASURED OFF THE SIGNED-OFF BOARD (Sid, 2026-08-14 — "one scale everywhere").
     The values below used to come from Acetate_Collection.html, the July reference: title 15.5/600,
     the "N days left" run 12/600, meta base 13. That reference is RETIRED — verify-collection was
     repointed off it on 2026-08-13 for the same reason, and every number it carried is a surface
     the redesign has since redrawn. design/redesign-2026-08/tokens.css rules these instead:
       "row title  15px 600"   and   "meta  12.5px 400 #8E9196; emphasised values 600 #C3C5C9"
     so the title loses its half-pixel and the meta and its urgency run join on 12.5. Both were
     wrong by the same amount at BOTH widths, which is why removing the phone step-down did not fix
     them: they were never the step-down, they were the old reference.
     NOT CHANGED HERE, and recorded rather than tuned: --t-subrow is 14.5 against the board's 15,
     but it lives in tokens.css and player.css reads it for both players' tracklists — shared
     component work, which this pass stays out of. Same for .sv-rname's ink: the board rules
     #E8E9EA resting and the build renders --txt-bright #F3F4F5. Sizes are this ruling; inks want
     their own. */
  --t-rowtitle:15px;           /* collection + solo row titles (redesign tokens: "row title 15px 600") */
  --t-rowdays:12.5px;          /* the "N days left" run inside row meta — the meta voice, emphasised */
  --t-rowmeta:12.5px;          /* row meta base (redesign tokens: "meta 12.5px 400 #8E9196") */
  /* (--t-subrow moved to tokens.css 2026-08-01: the players' tracklists take the same voice now) */

  /* ---- COLLECTION CREATE/EDIT PANEL (Acetate_Collection.html, 2026-07-31) ----
     Every value below was read headless off that reference, not taken from the brief's table —
     the table is orientation, the render is the authority (it is what caught "glyph 25" being a
     font-size rather than a painted size, twice now). The panel's SURFACES and INKS moved to
     tokens.css with the rest of the ramp; these are its type voices, which only it has. */
  --t-panelfield:15.5px;       /* name field VALUE */
  --t-panelmsg:14.5px;         /* message field / placeholder */
  --t-panellabel:13px;         /* "disappears in", "N tracks", row number + duration */
  --t-panelurl:14px;           /* the link row's url */
  --t-panelsave:15.5px;        /* save changes */
  --t-paneldel:14px;           /* delete link */
}
#viewPanel .st-pmFmt,
#viewPanel .st-tL, #viewPanel .st-tR,
#viewPanel .st-tkNum, #viewPanel .st-tkDur,
#viewPanel .st-tkCount{ font-size:var(--t-mono); letter-spacing:var(--t-mono-ls); }
#viewPanel .st-tkName{ font-size:var(--t-caption); }
/* the batch caption joins the caption voice here too, and leaves the mono line above it: it is a
   sentence, not a readout, and it sits directly above another sentence in the same voice. */
#viewPanel .st-tkNote, #viewPanel .st-tkWhy, #viewPanel .st-tkDiag{ font-size:var(--t-caption); }
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */

/* the studio LIST's own mono readouts join the same treatment — one mono product-wide */
/* .sv-subNum/.sv-subDur left this list 2026-07-30: they stopped being mono at the family fix and
   the reference tracks them normal — the later sub-row rule owns them now. */
.sv-loadedFmt{ letter-spacing:var(--t-mono-ls); }
/* (the ≤480 step-down for the shared scale moved to tokens.css with the scale itself) */

/* the rest of the panel joins the scale — nothing on this surface sits outside it */
#viewPanel .st-flabel, #viewPanel #expLabelTxt,
#viewPanel #copyBtn, #viewPanel #shareBtn, #viewPanel #saveBtn{ font-size:var(--t-body); }
#viewPanel .st-tkAdd, #viewPanel .st-dlNote, #viewPanel #delBtn{ font-size:var(--t-caption); }
#viewPanel #urlText{ font-size:var(--t-mono); letter-spacing:var(--t-mono-ls); }

/* item 1: the SAME title -> spec relationship on the panel and the studio preview */
#viewPanel .st-pmFmt{ margin-top:var(--sp-tight); }
.sv-loaded .sv-loadedFmt{ margin-top:var(--sp-tight); }

/* item 2: the well's time chips are numeric readouts like every other — same family, same size
   step, same tracking. .sv-tL/.sv-tR were the drift: right family and size, but no --t-mono-ls, so
   they tracked wider than every other number in the product. All wells, all surfaces. */
.st-tL, .st-tR, .sv-tL, .sv-tR{
  font-family:var(--ui);
  font-size:var(--t-mono); letter-spacing:var(--t-mono-ls); }
/* the sort control's label joins the scale too (it was a stray 11.5px) */
.sv-sortTxt{ font-size:var(--t-caption); }


/* the panel and the studio rows take the SAME moved scale — the design session only drew the
   player card, but the tokens are shared, so these follow rather than drift. */
#viewPanel .st-pmName{ font-weight:var(--t-w-bold); letter-spacing:var(--t-title-ls); }
#viewPanel .st-pmFmt{ font-size:var(--t-caption); letter-spacing:var(--t-tracking); }
/* well timecodes are Inter on every player well now (the design retires mono from the card face);
   the studio LIST's numbers and durations keep the mono family. */
#viewPanel .st-tL, #viewPanel .st-tR, .sv-tL, .sv-tR{
  font-family:Inter,-apple-system,sans-serif; font-weight:var(--t-w-med);
  letter-spacing:var(--t-tracking); color:var(--txt-lead); }


/* ============================================================================================
   STUDIO — approved reference, mapped onto the shared tokens. Read headless from the mock; no
   markup, class or rule imported. Families are ours (Inter): the export's Helvetica/Arial are
   fallbacks it had no webfont for, not values.

   SNAPPED to the nearest declared step rather than minting new ones (no widening, no fudging):
     auto-play    12   -> --t-mono   (12.5)
     days left    12   -> --t-mono   (12.5)
     row title    15.5 -> --t-body   (14.5)   same value Sid already ruled on for the player
     ...          15   -> --t-body   (14.5)
     avatar init  17   -> --t-body   (14.5)
   ============================================================================================ */

/* --- header --- */
.st-wordmark{ font-size:var(--t-title); font-weight:var(--t-w-med);
  letter-spacing:var(--t-logo-ls); color:var(--txt-bright); }
.st-avatar{ width:30px; height:30px; background-color:#fff; color:var(--ink-invert);
  font-size:var(--t-body); font-weight:var(--t-w-bold); font-style:normal; }
#viewStudio .sv-upload span, .sv-uploadMore span{ font-size:var(--t-body);
  font-weight:var(--t-w-semi); letter-spacing:var(--t-tracking); color:var(--txt-bright); }

/* --- preview player: same voices as the recipient card --- */
.sv-loadedName{ font-size:var(--t-title); font-weight:var(--t-w-bold);
  letter-spacing:var(--t-title-ls); color:var(--txt-bright); }
.sv-loadedFmt{ font-family:Inter,-apple-system,sans-serif; font-size:var(--t-caption);
  font-weight:var(--t-w-reg); letter-spacing:var(--t-tracking); color:var(--txt-muted); }

/* --- transport --- */
.sv-tbtn{ width:44px; height:44px; background:#2A2B2E; }
/* (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) */

/* --- filter row: smaller and quieter than it was --- */
.sv-fbtn{ font:var(--t-w-reg) var(--t-caption) Inter,sans-serif; letter-spacing:0;
  color:var(--txt-muted); }
.sv-fbtn.on, .sv-fbtn[aria-pressed="true"]{ color:var(--txt-bright); }
.sv-fdot{ font-size:var(--t-caption); color:#55585C; }

/* --- sort control: the labelled box retires, chevron only --- */
.sv-sortBtn{ width:26px; height:19px; padding:0; gap:0; justify-content:center; }
.sv-sortTxt{ display:none; }

/* --- auto-play, now at the right end of the filter row (fc) ---
   The SHARED .sv-auto geometry (sendbox.css) is a vertical stack with an absolutely-positioned label,
   built so only the pill participates in a TRANSPORT row's centring. The live player still needs
   exactly that, so it is untouched: this override is scoped to the studio's filter row, where the
   control reads as one inline line — label, then pill — on the list's own baseline. */
.sv-tracksHead .sv-auto{ flex:0 0 auto; width:auto; flex-direction:row; align-items:center; gap:7px; }
.sv-tracksHead .sv-auto > span{ position:static; top:auto; left:auto; right:auto; transform:none;
  font-size:var(--t-helper); font-weight:var(--t-w-reg); color:var(--txt-muted); }

/* --- rows --- */
/* line-height DECLARED, not left to `normal`. The reference declares 19.375/15.5 = 1.25 on the
   title and 15.6/13 = 1.2 on the meta; live inherited `normal`, which is a FAMILY-DEPENDENT value
   — the same trap as the tracking, one level up. With the title at its new 15.5 the row had grown
   to 66 against the reference's 63, because Inter's normal line box is taller than Helvetica's AND
   the 6px meta margin was stacking on top of that slack. Declaring both puts the row back on the
   reference's geometry at both widths. */
.sv-rname{ font-size:var(--t-rowtitle); font-weight:var(--t-w-semi); line-height:1.25;
  letter-spacing:var(--t-tracking); color:var(--txt-bright); }
.sv-rmeta{ font-size:var(--t-rowmeta); font-weight:var(--t-w-reg); line-height:1.2;
  letter-spacing:var(--t-tracking); color:var(--txt-muted); }
/* row circles: 34 either way — surface-2 chevron on a collection, blue play on a track */
.sv-chev, .sv-row.stack .sv-chev{ width:34px; height:34px; border-radius:50%; background:#2A2B2E;
  align-items:center; justify-content:center; padding:0; }
.sv-rplay{ width:34px; height:34px; background:var(--blue); }

/* the kebab reads as the mock's glyph. The SVG stays in the DOM (its aria-label is the accessible
   name); only its painting changes, so no markup or behaviour moved. */
.sv-rkebab{ align-items:center; justify-content:center; }
.sv-rkebab svg{ display:none; }
.sv-rkebab::before{ content:'\00B7\00B7\00B7'; font-size:var(--t-body); font-weight:var(--t-w-reg);
  letter-spacing:1.2px; color:var(--txt-muted); line-height:1; }

/* days left keeps the URGENCY SCALE — the reference paints every row red, including "7 days left",
   which would throw away the signal. Only the RED VALUE is adopted from it. */
.sv-rmeta .x{ font-size:var(--t-rowdays); font-weight:var(--t-w-semi); letter-spacing:var(--t-tracking); }
/* no .soon/.urgent rules here on purpose: studio.js sets the urgency colour INLINE (timeLeft()),
   so class-based rules would be dead code masquerading as coverage. The scale lives there. */


/* ============================================================================================
   Two fixes against the studio reference.

   1. AUTO-PLAY ON THE TRANSPORT CENTRELINE. The wrapper is a flex column holding the pill and its
      label, and the ROW centres that whole column — so the pill rode (label + gap)/2 above the
      centreline, measured 9px high against the reference's exact 0. Only the pill participates in
      the row's centring now; the label hangs beneath it at --sp-tight, which is the 6px the
      reference measures between pill bottom and label top.
   2. THE SORT BOX RETIRES. The reference is a bare glyph: no border, no background, no radius.
      The 26x19 hit area is untouched — only the painting goes, so the target stays thumb-sized.
   ============================================================================================ */
/* (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) */

.sv-sortBtn{ border:0; background:none; border-radius:0; padding:0; }
/* open state can no longer be a box, so the glyph itself brightens */
.sv-sortBtn.open{ border-color:transparent; background:none; }
.sv-sortBtn svg{ opacity:.72; transition:opacity .15s ease; }
.sv-sortBtn:hover svg, .sv-sortBtn.open svg{ opacity:1; }


/* sort glyph 25% up (13 -> 16.25, chevron 9 -> 11.25). Sized in CSS rather than the SVG attributes
   so the two stay locked to one ratio. The 26x19 hit area is deliberately unchanged: with the box
   retired there is no border or fill to overflow, so the glyph can exceed it harmlessly while the
   tap target stays the size it was. */
.sv-sortBtn > svg{ flex:none; width:16.25px; height:16.25px; }
.sv-sortBtn .sv-sortChev{ flex:none; width:11.25px; height:11.25px; }
/* the two glyphs together are wider than the 26px hit area, so without flex:none the row SQUASHED
   them — 15.36 wide against 16.25 tall, i.e. square icons rendering non-square. */


/* ============================================================================================
   EXPANDED-COLLECTION REFERENCE PASS. Values read headless from the mock's expanded state
   (chevron clicked), not from prose. Nothing imported.

   Component dimensions, NOT layout gaps — same class as the hero's 88px and the pill's padding,
   so they are deliberately off the 6/12/18/24/30 spacing scale and out of the spacing harness:
   the 9px alignment inset (Sid's call: component inset, do not snap), the panel's 61/7 indents,
   the sub-row's 47px height and 10/12 padding, and the 10px sub-row play glyph.
   The type all lands ON steps: number + duration --t-caption, title --t-body, glyphs --t-caption.
   ============================================================================================ */

/* card padding matches the reference AND the solo player card, which was already 24/22 —
   the studio was the transposed one (22 vertical / 24 horizontal). */
.st-card{ padding:24px 22px; }

/* the 9px inward alignment: filter row from the left, sort + avatar from the right, so the sort
   control's right edge and the avatar's right edge sit on one line. */
/* each of these sits in a container with its own 1px (and, for the avatar, 8px) offset from the
   card's content edge, so the margin is the DELTA that lands the measured 9px, not 9 itself.
   Verified by measuring the served page, not by assuming the containers align. */
.sv-filter{ margin-left:8px; }
.sv-headR{ margin-right:8px; }
/* the avatar is position:absolute and an #id rule zeroes its margins, so margin-right was never
   the lever — computed 0px however large it was set. `right` is. Its parent sits 1px inside the
   card content edge, so right:8px lands the avatar's right edge on the sort control's (9px in). */
#avatar.st-avatar{ right:8px; }

/* the inset sub-row panel. The indents live in custom props because the OPEN-collection divider
   below must mirror them exactly: its old left:-34px was the previous generation's indent kept as
   a constant, so when the panel moved to 60px the line silently started 26px short of the row
   edge (Sid's report). A constant rots; the prop cannot.
   The EDGE is the reference's, measured headless: a faint 1px ring in #2C2E31 — the same ink as
   the sub-row separators — and NO inset shadow. box-shadow:none is load-bearing: the older
   generation's rule still declares the recessed inset pair, and at 0.055 alpha it read as no
   border at all rather than as the reference's edge. */
/* VERTICAL AIR, measured off the reference 2026-07-30: it sits EVEN, 12 above and 12 below.
   Above is not a margin there — the panel butts straight onto the collection row's box and the
   row's own 12px bottom padding IS the air, so margin-top goes to 0 (live's extra 2px made it 14
   above against 5 below: tight AND lopsided). Below, the next row begins 12px down and the divider
   is drawn exactly there. 12 is --sp-close's value, held FLAT at both widths like the row slack it
   pairs with — the panel's HORIZONTAL indent still follows the phone scale via the props. */
.sv-sub{ --sub-indent:60px; --sub-outdent:6px;
  background:#202124; border-radius:12px; padding:0;
  margin-top:0; margin-bottom:12px;
  margin-left:var(--sub-indent); margin-right:var(--sub-outdent);
  border:1px solid #2C2E31; box-shadow:none; }
/* .sv-sub .sv-subRow, because that pair already sets padding-left/right and outranks a bare class.
   Re-measured against the reference 2026-07-30 (airy ruling): single-line 47 with 10px slack each
   side, a two-line row keeps the SAME slack and grows — min-height + padding is exactly that pair,
   so both cases read as one family rather than content-hugged. The internal gap is the reference's
   12px, and it lands ON a declared step: --sp-close is 12 at desktop and 9 at 390, which keeps the
   phone's ruled tighter indent scale without a second rule. */
.sv-sub .sv-subRow{ min-height:47px; height:auto; box-sizing:border-box; padding:10px 12px; background:none; gap:var(--sp-close); }
/* separators BETWEEN rows only — the first row has none, exactly as the reference measures */
.sv-subRow + .sv-subRow{ border-top:1px solid #2C2E31; }

/* sub-row parts */
.sv-subPlay{ width:26px; height:26px; background:var(--blue); }
.sv-subPlay svg{ width:17.4px; height:17.4px; }
/* FAMILY, not just size. These inherited the studio's mono treatment and I changed only their
   size/weight/tracking/colour, so they stayed Roboto Mono while the table I checked said "13px,
   #8E9196 — matches". The reference has them sans. Never assert a subset of what you changed. */
/* tracking: NORMAL, not the shared -0.005em — the reference measures its sub-row numerals
   (number + duration) untracked while every text role around them carries --t-tracking. Verified
   headless 2026-07-30 on the expanded state; adopting it is what the "tighter and slicker" diff
   actually came down to for numerals. */
.sv-subNum, .sv-subDur{ font-family:Inter,-apple-system,sans-serif;
  font-size:var(--t-caption); font-weight:var(--t-w-reg);
  letter-spacing:normal; color:var(--txt-muted); }
/* SID'S RULING, overrides the reference: a track's title is shown IN FULL, never ellipsised. It
   wraps to a second line and the row grows to fit. Truncating a title hides which take you are
   about to play, which matters more than a uniform row height. */
.sv-subName{ font-family:Inter,-apple-system,sans-serif;
  font-size:var(--t-body); font-weight:var(--t-w-reg);
  letter-spacing:var(--t-tracking); color:var(--txt-bright);
  white-space:normal; min-width:0; overflow-wrap:anywhere;
  /* FINAL: two lines maximum, ellipsis only beyond two — the clamp mechanism, used deliberately
     this time rather than fought. overflow MUST stay hidden: with the clamp active and overflow
     visible, WebKit paints the clamped text and the overflow together, which is the double-render
     that showed a clipped and a full title at once. */
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;
  text-overflow:ellipsis; }

/* one declared step down (--t-body -> --t-caption), Sid's overrule of the reference so long names
   fit in two lines more often. */
.sv-sub .sv-subName{ font-size:var(--t-subrow); }

/* PLAYING changes the circle and the COLOUR only. It must not change size or weight: reflowing the
   type on play made a two-line title jump to three, or back to one. */
.sv-subRow.playing .sv-subName{ font-weight:var(--t-w-reg); font-size:var(--t-subrow); color:#fff; }

/* the hero title above the transport wraps to two lines on its own — no tap. The .expanded escape
   hatch set overflow:visible, which with the clamp is the same double-render; it is neutralised. */
/* break-word, NOT anywhere (Windows arc). `anywhere` licenses a break at ANY character, so a normal
   multi-word title cut mid-word — "…how long this title / ca" — which reads as a broken string
   rather than a clamped one, and the clamp's ellipsis lands after a fragment instead of a word.
   `break-word` keeps ordinary titles breaking at spaces and still rescues the case `anywhere` was
   chosen for: a plugin-created take that is one unbreakable token still breaks rather than
   overflowing. The clamp supplies the ellipsis either way. */
.sv-loadedName, .sv-loadedName.expanded{
  white-space:normal; overflow-wrap:break-word;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden; text-overflow:ellipsis; }

/* row glyphs are 13 in this reference; the expanded chevron circle keeps surface-2 */
/* GLYPH : CIRCLE (Sid, 2026-07-30). The reference's PAINTED triangle fills 0.279 of its circle at
   both row levels; live's filled 0.16, which is what read small.
   0.279 and not the 0.338 first shipped: the reference's glyph is the TEXT "▶", and a Range rect
   returns its ADVANCE box, which is wider than the ink. Sizing from the advance overshot by ~20%
   and Sid caught it as too big. Canvas actualBoundingBoxLeft/Right gives the painted extent:
   7.28px in a 26 circle, 9.46 in a 34. This is the same box-vs-ink error corrected elsewhere in
   this pass, made in my own measurement — hence the value here is the INK ratio, stated as such.
   Live's glyph is an svg path inset in its viewBox (ink is 10/24 of the box), so the box is
   0.279/0.4167 = 0.670 of the circle: 26 -> 17.4, 34 -> 22.8.
   NB the two triangles are not the same shape — Arial's ▶ is 0.87 wide-to-tall, this path is 0.77 —
   so matching the WIDTH (what reads as size on a right-pointing glyph) leaves live's marginally
   taller. Matching both is not possible without redrawing the path, which is not on the table.
   The chevron leaves this rule: it is not a play glyph and only ever shared the line by
   coincidence of size. */
.sv-chev svg{ width:13px; height:13px; }
.sv-rplay svg{ width:22.8px; height:22.8px; }
.sv-row.open .sv-chev{ background:#2A2B2E; }

/* upload row gets a filled surface rather than a bare outline */
.sv-upload, .sv-uploadMore{ background:#1E1F22; border-radius:12px; }

/* DRAG HANDLES STAY — reorder behaviour untouched, only the painting changes: the reference's
   2px round dots, two columns by three rows on a 5px pitch. The 6px padding keeps a 19x24 hit
   area around a 7x12 dot field, so dragging is no harder than before.
   ONE TREATMENT, ONE RULE (Sid, 2026-07-30). The reference renders the identical handle at both
   levels; live had TWO implementations of it — this level drew the dots as a repeating
   radial-gradient with the svg hidden, the sub-rows drew an svg — and they had drifted: the
   gradient rendered at full strength while the sub-rows rendered at 0.5, which is the difference
   that showed. Both now use the same svg and the same rule below; the gradient is gone. */


/* PHONE: the desktop reference's 61px indent does not translate — at 390 it eats the title column,
   so a name that fits two lines on desktop needs three. Indent and inner padding come down to the
   phone scale's own steps (wide 23 / tight 5, and close 9 horizontally) which hands the title
   column ~45px back. Order is untouched: dots, circle, number, title, duration. The two-line cap
   and the 13px title hold at both widths. Proportions are my judgement — the reference is
   desktop-only. */
@media (max-width:480px){
  /* retune the PROPS, not the margins: the margins and the open-collection divider both read the
     props, so the phone indent and the divider span can never disagree again */
  .sv-sub{ --sub-indent:var(--sp-wide); --sub-outdent:var(--sp-tight); }
  .sv-sub .sv-subRow{ padding:10px var(--sp-close); }
}


/* the batch caption ("received 2 files · separate links") shares a flex line with "+ add tracks"
   and was being crushed into an ellipsis and wrapping awkwardly beside it. It is furniture, not a
   headline, so it takes its own full-width line beneath the action rather than competing for the
   same one. Gap from the spacing scale. */
.st-tkHead{ flex-wrap:wrap; row-gap:var(--sp-close); }
.st-tkHead .st-tkDiag{ flex:1 0 100%; order:2; white-space:normal; overflow:visible;
  text-overflow:clip; text-align:left; }


/* ==============================================================================================
   COLLECTION CREATE/EDIT PANEL — to Acetate_Collection.html (Sid, 2026-07-31)
   Scoped to #viewPanel wherever a rule would otherwise reach the studio list, which has its own
   reference and its own settled values. Tokens only; the ruled tracking compensation applies on
   top of every size here, and the numerals stay exempt exactly as they are in the list.
   ============================================================================================== */

/* the WELL is a transparent window with its own edge, not a filled box */
/* height from the shared component (sendbox.css), tiers included. This rule is id-scoped, so its
   hardcoded 120 outranked the shared .st-well and the studio never compacted on a short window
   while the player did — which is exactly the "player well reads smaller" difference. */
/* NO height here any more. It restated var(--well-h) — harmless while that was the only answer, and
   an id-scoped block on the shared component the moment there were two: .st-card.has-list's compact
   state is a class rule and this would have outranked it, so the panel would have kept the full
   well while the player compacted. The shared component owns the height; this owns the chrome. */
#viewPanel .st-well{ border-radius:14px; clip-path:inset(0 round 14px);
  border:1px solid var(--line-well); }
#viewPanel .st-playhead{ width:1.5px; }   /* one ruled playhead everywhere (fc), not a thicker panel variant */
/* THE PANEL'S TRANSPORT IS WELDED INTO THE WELL (Sid, 2026-08-14), mirroring #svStrip field for
   field. What it replaces: an 84px blue disc under the waveform with 44px discs either side of it.
   The disc and its glow are gone by the same ruling that took them off the listener — and on this
   surface the blue was owed to SAVE anyway, under the one-primary-per-card law.
   The .st-tL/.st-tR time chips went with it: the time reads in the strip now, exactly as the studio
   and the listener read it, so the classes have left the markup and their rules with them.
   53 is the studio's strip height, taken rather than re-picked. */
.st-wave{ position:relative; height:var(--well-h); }
.st-wave canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }
/* THE STRIP OWNS ITS HEIGHT, ONCE (Sid, 2026-08-17). 53 was written in four places: the strip
   itself, the sheet's strip, and the two rules where the WELL adds the strip's band to its own
   height. The wave subtracts from that number, so a change in one place and not the others silently
   mis-sizes the waveform — the same one-owner problem as --topbar-h, and exactly the shape that
   drifts. Consumers read --strip-h and never the literal. */
:root{ --strip-h:53px; }
.st-strip{ position:absolute; left:0; right:0; bottom:0; height:var(--strip-h);
  display:flex; align-items:center; gap:0; padding:0 0 0 7px;
  background:var(--strip-bg, rgba(10,11,14,.55)); }
/* NOTHING TO PLAY MEANS NO TRANSPORT, not a dead greyed one — the standing ruling. The well is the
   wave alone while a file is uploading, so the 50% screen carries no play at all. */
.st-well:not(.has-strip) .st-strip{ display:none; }
#viewPanel .st-well.has-strip{ height:calc(var(--well-h) + var(--strip-h)); }
.st-card.has-list #viewPanel .st-well.has-strip{ height:calc(min(var(--well-h), var(--well-h-tight)) + var(--strip-h)); }
.st-stripPlay{ flex:0 0 52px; width:52px; height:44px; padding:0; border:0; background:none;
  display:flex; align-items:center; justify-content:center; cursor:pointer; }
.st-stripPlay svg{ width:33px; height:33px; }
.st-stripPlay:disabled{ opacity:.4; cursor:default; }
.st-stripSkip{ flex:0 0 44px; width:44px; height:44px; padding:0; border:0; background:none;
  display:flex; align-items:center; justify-content:center; cursor:pointer; }
.st-stripSkip svg{ width:17px; height:17px; }
.st-stripSkip:hover svg path, .st-stripSkip:hover svg rect{ fill:rgba(255,255,255,.75); }
.st-stripTime{ flex:0 0 auto; margin-left:auto; padding:0 12px 0 12px;
  font-family:var(--ui); font-size:12.5px; font-weight:500; letter-spacing:var(--t-tracking);
  color:rgba(255,255,255,.75); white-space:nowrap; }
/* auto-play is a setting, not a transport control, so it keeps its own row under the well */
.st-pmAutoRow{ display:flex; align-items:center; justify-content:center; }
.st-pmAutoRow:empty, .st-pmAutoRow:not(.hasAuto){ margin-top:0; }
.st-pmAutoRow.hasAuto{ margin-top:var(--sp-close); }

/* queue header */
#viewPanel .st-tkAdd{ font-size:var(--t-body); font-weight:var(--t-w-semi); letter-spacing:var(--t-tracking); }
#viewPanel .st-tkCount{ font-family:Inter,-apple-system,sans-serif; font-size:var(--t-panellabel);
  font-weight:var(--t-w-reg); letter-spacing:var(--t-tracking); color:var(--txt-muted); }

/* the track panel: a filled inset with its own edge, like the studio's expansion */
#viewPanel .st-tkList{ background:var(--surface-inset); border:1px solid var(--line-inset);
  border-radius:13px; padding:0; box-shadow:none; }
/* A REAL 15px LANE, on the LIST — which is the correction to my own last pass. That one put the 15
   on the row's PADDING to keep the reference's full-width separators, and it moved the buttons out
   from under the bar but left the row BOX running to the edge: the whole row is a click target
   (tapping it auditions the track), so a click in the lane still scrolled instead of playing.
   The row box has to move, and the separator has to stay full width. Both, and neither is a
   compromise: the list carries the lane, and the separator is a ::before that spans back across it
   — right:-15px lands exactly on the list's padding edge, so it adds no scrollable overflow.
   Cost, stated: the auditioning row's highlight now stops at the lane rather than at the window
   edge. That is what a lane IS. */
#viewPanel .st-tkList{ padding:0 15px 0 0; }
#viewPanel .st-tkList .st-tkRow{ position:relative; min-height:44px; height:auto; box-sizing:border-box;
  padding:10px 12px; gap:var(--sp-close); border-bottom:none; }
/* separator BETWEEN rows only — the first has none, exactly as the reference measures */
#viewPanel .st-tkList .st-tkRow + .st-tkRow{ border-top:none; }
#viewPanel .st-tkList .st-tkRow + .st-tkRow::before{ content:''; position:absolute; pointer-events:none;
  left:0; right:-15px; top:0; border-top:1px solid var(--line-inset); }
/* the handle is the ONE treatment the whole product uses (see .sv-drag/.sv-subDrag) */
/* (ds) the panel's handle is on the shared rule now — see .sv-drag/.sv-subDrag/.st-tkDrag */
#viewPanel .st-tkNum{ font-family:Inter,-apple-system,sans-serif; font-size:var(--t-panellabel);
  font-weight:var(--t-w-reg); letter-spacing:normal; color:var(--txt-muted);
  width:calc(var(--num-ch, 2) * 1ch); text-align:right; font-variant-numeric:tabular-nums; }
#viewPanel .st-tkName{ font-family:Inter,-apple-system,sans-serif; font-size:var(--t-rowtitle);
  font-weight:var(--t-w-semi); letter-spacing:var(--t-tracking); color:var(--ink-value); }
#viewPanel .st-tkDur{ font-family:Inter,-apple-system,sans-serif; font-size:var(--t-panellabel);
  font-weight:var(--t-w-reg); letter-spacing:normal; color:var(--ink-quiet); }
#viewPanel .st-tkEdit svg{ width:12.5px; height:12.5px; }
#viewPanel .st-tkX svg{ width:14px; height:14px; }

/* fields: filled inset grounds, not translucent white */
#viewPanel .st-namePrompt{ font-size:var(--t-panellabel); font-weight:var(--t-w-semi);
  letter-spacing:var(--t-tracking); }
#viewPanel .st-input{ background:var(--surface-inset); border:1px solid var(--line-inset);
  border-radius:12px; padding:14px 15px; letter-spacing:var(--t-tracking); }
#viewPanel #title{ min-height:47px; font-size:var(--t-panelfield); font-weight:var(--t-w-med); color:var(--ink-value); }
#viewPanel #message{ min-height:45px; font-size:var(--t-panelmsg); font-weight:var(--t-w-reg); color:var(--ink-value); }
#viewPanel .st-input::placeholder{ color:var(--ink-quiet); }
/* focus is the RULED blue, the same one every fill uses */
#viewPanel .st-input:focus{ border-color:var(--blue); box-shadow:none; }
#viewPanel .st-flabel{ font-size:var(--t-panellabel); font-weight:var(--t-w-reg);
  letter-spacing:var(--t-tracking); color:var(--txt-muted); }
#viewPanel .st-pill{ flex:none; width:82px; height:34px; background:var(--surface-inset);
  border:1px solid var(--line-inset); border-radius:10px; font-size:var(--t-panelurl);
  font-weight:var(--t-w-semi); letter-spacing:var(--t-tracking); color:var(--ink-value); }
/* THE PILL BREATHES AT 390 (Sid's ruling 1A, 2026-08-03). The row read as a gap problem and was
   not one: the wrapper's basis is 0%, so it only ever claimed the LEFTOVER after a pin that could
   not shrink. At 390 the leftover was 33.9px, the 82px pill overflowed its own wrapper, and the
   two controls overlapped by ~10px. Whoever is fixed wins that argument, and it was the pin.
   min-content is the pill's own declared width, read rather than restated — the wrapper may still
   grow to absorb desktop slack, it just cannot shrink below the control it exists to hold. The pin
   keeps 138px as its BASIS, so nothing moves where there is room; it gives up only the refusal to
   shrink, down to the 64px floor sendbox.css already declares for the "no pir" clip. */
#viewPanel .st-expWrap{ min-width:min-content; }
#viewPanel .st-pin{ flex:0 1 138px; height:38px; background:var(--surface-inset);
  border:1px solid var(--line-inset); border-radius:10px; font-size:var(--t-panelurl);
  font-weight:var(--t-w-reg); color:var(--ink-value); }
#viewPanel #dlNote{ font-size:var(--t-panellabel); font-weight:var(--t-w-reg);
  letter-spacing:var(--t-tracking); }
/* auto-play label: the reference reads 12/400 #8E9196, live carried the old 10.5/500 */
/* (moved to sendbox.css — ONE shared control, see the SHARED CONTROLS block there) */

/* the link row. Its BUTTONS moved to the pinned bar; the url readout stays in the body, because it
   is something you READ, not something you do. */
#viewPanel .st-url{ height:40px; background:var(--surface-inset); border:1px solid var(--line-inset);
  border-radius:12px; padding:0 14px; }
/* the "copied" receipt steps aside for a lifted "?" chip, and only then (--vcs-reserve is set by
   syncChipReserve and cleared the moment they stop meeting) */
#viewPanel .st-readyUrl .ok{ margin-right:var(--vcs-reserve, 0px); }
#viewPanel #urlText{ font-size:var(--t-panelurl); font-weight:var(--t-w-reg);
  letter-spacing:normal; color:var(--ink-quiet); }

/* ---- THE PINNED ACTION BAR ------------------------------------------------------------------
   THE BAR IS THE CARD'S CHILD, NOT THE PANEL'S. The first build made it sticky inside #viewPanel,
   which forced the SCROLLER to carry a child 44px wider than its own content box, and to get that
   the panel had to bleed too (margin:0 -22px / padding:0 22px). Two faults came back from real use
   and both trace to that one decision:
     · a horizontal scrollbar on the panel at desktop. The construction is flush by ARITHMETIC — with
       a classic 15px bar the scrollport is 395, the content box 351, and the bar's margin box
       351+44 = 395 exactly. Flush is not the same as safe: it is one rounding away from overflow,
       and it cannot be reproduced here, because headless Chrome will not draw a space-taking
       scrollbar at all (::-webkit-scrollbar reserves 0px — measured; --disable-features=
       OverlayScrollbar changes nothing). A construction that survives every measurement I can make
       and still fails on a real machine is one to REMOVE, not to tune.
     · the panel's own vertical scrollbar moved 22px right with its box, out to the card's edge,
       where it reads as a page scrollbar instead of a panel one. That is the "grey scrollbar".
   Out here the bleed lives in .st-card, which is overflow:clip — not a scroll container at all, so
   it can neither draw a bar nor be scrolled programmatically. #viewPanel goes back to exactly the
   box it had before this arc, and so does its scrollbar. The bar still sits at the bottom of the
   card, because it is the last flex child of a column whose #viewPanel is the flexible one — and it
   is pinned by CONSTRUCTION now rather than by sticky, so it cannot drift.
   The bottom -22 is real here (it was inert under sticky): the bar eats 22 of the card's 24px
   bottom padding and lands 2px above the inner edge, which is what the approved crop shows. */
/* BREATHING ROOM ABOVE THE BAR (Sid, 2026-08-02). The url row ended flush against the hairline:
   the panel's last element stopped exactly where the bar began, so a readout and a control edge
   shared one line with nothing between them. On the panel's own padding rather than on any one
   element, because "the last thing before the bar" is a different element in every state — the url
   row on a clean link, the downloads row on create, the receipt in the wizard. A scroll container's
   end padding counts as scrollable, so the last row can still clear the bar completely.
   --sp-block: 18 at desktop, 14 at 390. The fade is 26 and sits over this. */
#viewPanel{ padding-bottom:var(--sp-block); }
.st-card > .st-bar{ flex:0 0 auto; position:relative; z-index:6;
  margin:0 -22px -22px; padding:14px 22px calc(14px + env(safe-area-inset-bottom,0px));
  display:flex; align-items:center; gap:var(--sp-close);
  /* NO TOP HAIRLINE (Sid, 2026-08-02): the FADE is the scroll cue, and a line that is always there
     says "there is more below" even at the end of the panel — the same untruth the fade itself was
     fixed for. The bar is already separated by its own ground and the fade above it. Easy revert:
     border-top:1px solid var(--line-inset). */
  background:var(--surface-card); }
/* THE FADE MEANS "THERE IS MORE BELOW", so it is wired to the scroll position and not painted
   permanently — at the end of the panel it says something untrue. studio.js sets .more while the
   panel still has somewhere to go (see syncPanelFade). 26px of the card ground, over the panel's
   last rows, ignoring the pointer. */
.st-card > .st-bar::before{ content:''; position:absolute; left:0; right:0; bottom:100%; height:26px;
  opacity:0; transition:opacity .12s linear;
  pointer-events:none; background:linear-gradient(180deg, rgba(26,27,29,0) 0%, var(--surface-card) 100%); }
.st-card > .st-bar.more::before{ opacity:1; }
.st-barGap{ flex:1 1 auto; }
/* IOS: THE BAR STANDS DOWN WHILE THE KEYBOARD IS UP, on coarse pointers only. The card is capped in
   svh, which the keyboard does not change, so a bar at the card's bottom sits BEHIND the keyboard —
   occupying room the sender cannot see or reach while covering nothing they can. Hiding it gives
   that room back to the field being typed into and brings it straight back when the keyboard goes.
   studio.js decides from visualViewport, NOT from focus: the panel focuses #title itself in the
   separate-links wizard and the name prompt, neither of which opens a keyboard, and a focus-driven
   rule took the wizard's only action off screen.
   (The first build unpinned it instead, so it could be scrolled to; that only worked while it was a
   sticky child of the scroller. It is not, for the reasons above, so this is the honest equivalent
   and it is a deviation from the ruled mechanic, not from its purpose.) */
.st-card > .st-bar.kbd{ display:none; }

.st-card > .st-bar .st-mbtn{ flex:0 0 auto; height:43px; padding:0 18px; border-radius:12px;
  background:var(--surface-inset); border:1px solid var(--line-inset); color:var(--ink-value);
  font-size:var(--t-body); font-weight:var(--t-w-semi); letter-spacing:var(--t-tracking); box-shadow:none; }
/* copy link is the primary on a clean panel: the link exists, handing it over is the next thing */
.st-card > .st-bar .st-mbtn.copy{ min-width:150px; background:var(--blue); border-color:var(--blue); color:#fff; }
/* #delBtn, not .st-delLink: an earlier rule targets the ID and would outrank a class here.
   flex:0 1 with min-width:0 is what lets it WRAP to two lines at 390 instead of shouldering the
   primary off the card — the one element in the bar that is allowed to give. */
#delBtn{ flex:0 1 auto; min-width:0; font-size:var(--t-paneldel); font-weight:var(--t-w-semi);
  letter-spacing:var(--t-tracking); color:var(--red-del); text-align:left; }
/* the two primaries share one slot and one shape — only one is ever on screen */
.st-card > .st-bar #saveBtn, .st-card > .st-bar #goBtn{ flex:0 0 auto; width:auto; min-width:150px;
  height:46px; padding:0 18px; margin-top:0; border-radius:12px;
  font-size:var(--t-panelsave); font-weight:var(--t-w-bold); letter-spacing:var(--t-tracking); }

/* the collection delete dialog: three actions do not fit the two-button row's default sizing, so
   the row wraps and the keep action takes the full width under cancel/delete. Same component,
   same buttons — only the shape of the row changes when the third one is present. */
#delRow.three{ flex-wrap:wrap; row-gap:var(--sp-tight); }
#delRow.three #delKeep{ order:3; flex:1 0 100%; }


/* ---- collection panel, round 2: the six Sid marked (2026-07-31) --------------------------------
   BLOCK RHYTHM. The first pass sized every box in the table and never measured the space BETWEEN
   them, so a 31/31 run could be green while the whole panel read tight. The reference's gaps, on
   declared steps: well -> transport 24, transport -> queue 24, queue header -> panel 12, panel ->
   name label 24, label -> field 12, field -> field 12, message -> the detail rows 30. */
/* margin-BOTTOM too: an earlier rule sets `margin:16px 0 18px`, so setting only the top left 18px
   of its bottom stacking under the fields' own 24 and the panel->name gap measured 42. */
#viewPanel .st-tracks{ margin-top:var(--sp-air); margin-bottom:0; }
#viewPanel .st-tkHead{ margin-bottom:var(--sp-close); }
#viewPanel .st-fields{ margin-top:var(--sp-air); }
/* THE SOLO PANEL'S SPEC LINE SITS CLOSER TO THE FIELDS (Sid, 2026-08-02). --sp-air is the gap from
   the QUEUE to the fields, which is a block-to-block step; on a single there is no queue and the
   thing above is the spec line, which belongs to the well's own block — so it read as a hole. One
   step down, --sp-block, which is the panel's block rhythm.
   Keyed on #pmTracks being hidden, because "there is no tracklist" IS "this is a single" here, and
   that is the condition rather than a class someone has to remember to set. */
#viewPanel #pmTracks[hidden] + .st-fields{ margin-top:var(--sp-block); }
#viewPanel .st-namePrompt{ margin:0 0 var(--sp-close) 0; }
#viewPanel .st-input + .st-input{ margin-top:var(--sp-close); }
/* THE LOWER REGION runs on --sp-block, all of it. Last pass set 30 here because the measurement
   was taken to the LABEL and the fix applied to the ROW — the label sits inside the row's own
   padding, so anchoring the two to different elements baked in a 12px error, and the harness then
   asserted my number rather than the reference's. Measured row-to-row: 18 throughout. */
#viewPanel #message + .st-detailRow{ margin-top:var(--sp-block); }
#viewPanel .st-detailRow + .st-detailRow{ margin-top:var(--sp-block); }
/* the link row is not a sibling of the detail rows (it lives in #pmReady, outside #pmFields), so
   the gap is its OWN margin — .st-readyUrl's 9, not anything the rows could set */
#viewPanel .st-readyUrl{ margin-top:var(--sp-block); }
/* the expiry pill's VALUE: an earlier #id rule pulls it to --t-body, so the pill's own 14 never
   reached the text inside it */
#viewPanel #expLabelTxt{ font-size:var(--t-panelurl); }
/* the pin is SANS in the reference, not mono — the third time a family has survived because I
   asserted a field's box, radius and alignment and never what it is set in */
#viewPanel .st-pin{ font-family:Inter,-apple-system,sans-serif; }

/* EDGE INSET. Row content sits 9px in from the card's content edge — the same COMPONENT inset the
   studio list already runs on (e09d9ca, ruled deliberately off the 6/12/18/24/30 scale). Full-width
   blocks (the well, the track panel, the fields, the link row) stay flush at the edge; only the
   text rows come in, which is what stops "+ add tracks" and the labels reading as if they had
   fallen off the side. */
/* 8, not 9: these rows sit inside the card's 1px border, so 8 is the DELTA that lands the measured
   9 from the card's content edge. Same lesson as the studio's own 9px inset, which needed
   margin-left:8 for the same reason — the number in the ruling is where the ink ends up, not what
   you type. */
/* The wizard's step marker joins this rule (Sid, 2026-08-02): it sits in the pinned bar, which is
   the CARD's child and not the panel's, so it was landing on the card's content edge while every
   row above it was inset — reading as if it had fallen off the side, which is the exact failure
   this rule's own comment describes. It takes the inset from THIS declaration rather than a number
   copied out of it, so the marker cannot drift off the "Downloads" label's edge later. */
#viewPanel .st-tkHead, #viewPanel .st-detailRow, .st-card > .st-bar > .st-sepStep{ padding-left:8px; padding-right:8px; }

/* the url is SANS in the reference, not mono — the first pass asserted its size and colour from
   the table and never its family, which is the same subset-of-what-I-changed failure the sub-row
   mono incident already cost a round. Case preservation is untouched (it is in the exempt list),
   and letter-spacing stays normal: the reference's +0.01em is in Helvetica, and Inter's
   compensation is -0.01em, so the two cancel. */
#viewPanel #urlText{ font-family:Inter,-apple-system,sans-serif; }

/* the pin reads left, like every other field on the card; centring it was a leftover from when it
   was a lone 4-digit box */
#viewPanel .st-pin{ text-align:left; }


/* ---- the TEXT blue, applied by role (Sid, 2026-07-31) -----------------------------------------
   Small blue TEXT takes --blue-text; fills, buttons and focus borders keep --blue. Swept rather
   than matched to the demo: the collection reference only happens to render two of these, but the
   rule is about what a thing IS, not which screenshot showed it. */
#viewPanel .st-tkAdd, #viewPanel .st-namePrompt, #viewPanel #dlNote.on{ color:var(--blue-text); }
.sv-rsplit{ color:var(--blue-text); }


/* ==============================================================================================
   iOS SAFARI: the focused-field shift (Sid's device, 2026-07-31)
   ============================================================================================== */

/* CAUSE 1 — AUTO-ZOOM. WebKit zooms the page whenever a focused text input computes under 16px,
   and leaves it zoomed and pannable afterwards, which is the card sitting off the left edge and
   the page scrolling sideways. The panel's fields are 15.5 / 14.5 / 14, so every one of them
   trips it. The fix is the field's own size on touch pointers only — not a layout change, and
   NOT maximum-scale in the viewport meta, which would disable pinch zoom for everyone.
   text-size-adjust does not help here: it governs text INFLATION on reflow, not the focus zoom.
   Desktop keeps the declared steps; a coarse pointer is the only place this applies. */
@media (pointer:coarse){
  /* by ID, because the panel pass sets these with #viewPanel #title / #message (two IDs) and a
     media query adds NO specificity of its own — the class version of this rule lost silently and
     only the pin, which has no #id rule, actually moved. The harness caught it; the eye would not
     have, because 15.5 and 16 look identical and the zoom is the only tell. */
  /* the panel's own fields; the SHEET's are handled at the very end of this file, because the sheet
     block below re-declares .st-input at equal specificity and would otherwise win on order alone. */
  #viewPanel .st-input, #viewPanel .st-pin, .acmodal .acinput, .acmodal .modalinput{
    font-size:16px;
  }
}

/* CAUSE 3, and the one that actually survives typing. #viewPanel has overflow-y:auto, and the
   spec forces overflow-x to auto alongside it — so the panel is a horizontally SCROLLABLE
   container, and WebKit scrolls the nearest scrollable ancestor to keep the caret in view on focus
   AND on every keystroke. That is why the card slid off to the left and STAYED there after typing,
   and why the 16px font fix alone did not stop it.
   Measured, not guessed. The transport's natural width is 38 spacer + 44 prev + 84 play + 44 next
   + 38 auto + four 18px gaps = 320, against 301 of panel content at 375 and 316 at 390. A centred
   flex row that overflows spills EQUALLY both ways, so half of that 19px went past the right edge
   and became scrollable width. The label adds to it: this surface renders it at 12px (the
   collection pass took it up from 10.5), so it measures 52.3px inside a box that the shared rule
   sized 38px for a 36.2px label at 8px — and being absolutely positioned and centred, it hangs
   ~7px past the box on each side.
   Two earlier attempts here were inert and are recorded so they are not tried again: `width:max-content`
   on the cluster, which loses to `flex:0 0 38px` because flex-basis beats width for a flex item;
   and `overflow-x:clip` on the panel, which the spec degrades to `hidden` when overflow-y is auto,
   and a hidden box is still programmatically scrollable — exactly the trick WebKit is using. The
   only fix that holds is removing the overflow itself.
   So, phone and panel only — the desktop cluster, the collection view and the player are untouched:
   the transport gap drops one declared step, which brings the row to 284 at 390 and fits 375 with
   room; and the label is anchored to its box's right edge rather than centred on it, so the
   rightmost element in the row cannot hang past the card. The label sits ~7px left of the toggle's
   centreline as a result; that is deliberate, it aligns to the row's right edge instead. */
@media (max-width:480px){
  }

/* CAUSE 2 — a genuine overflow I introduced. The link-row actions took the reference's fixed
   252 + 102 from a 412 card; at 390 the card is 362 wide with ~318 of content, so the pair
   overran and .st-card's overflow:hidden CLIPPED them rather than scrolling. The fixed widths are
   gone with the old two-button row; what remains is the same lesson applied to the bar — at 390 it
   carries THREE things on one line and the phone is where it has to give.
   WHO GIVES, in order: the secondary loses its side padding first, then "delete link" wraps to two
   lines (which is what the approved 390 crop shows), and the primary never shrinks below the width
   its label needs. The primary is the last thing on the card that should be squeezed. */
@media (max-width:480px){
  .st-card > .st-bar{ gap:var(--sp-tight); }
  .st-card > .st-bar .st-mbtn.share{ padding-left:14px; padding-right:14px; }
  .st-card > .st-bar .st-mbtn.copy{ min-width:0; }
  .st-card > .st-bar #saveBtn, .st-card > .st-bar #goBtn{ min-width:0; }
}

/* SCROLLBARS ON TOUCH. Desktop deliberately carries NO scrollbar styling on the studio list and
   the expansion window (see the rules by .st-list and .sv-sub): the reference styles none, and
   that absence is precisely what buys the OS overlay bar — dark, and hidden at rest. Any
   ::-webkit-scrollbar rule switches those elements to always-visible custom bars, so the desktop
   treatment must stay untouched. On a phone the same unstyled path renders a standing grey bar
   instead, which is what Sid flagged. A finger is its own affordance there, so on coarse pointers
   the bars are removed outright — the same treatment .st-tkList already carries everywhere.
   Scoped to (pointer:coarse) so nothing about the desktop overlay changes. */
/* .st-list has LEFT this block — it is hidden on every pointer type now, from the shared (ea)
   exception in sendbox.css, because Windows desktop draws the same standing bar this block was
   written to remove on phones. The rest stay coarse-only: their desktop treatment is still the OS
   overlay bar and nothing here should change it. */
@media (pointer:coarse){
  .sv-sub, .st-tkList, #viewPanel, .st-card, .st-sepList{ scrollbar-width:none; }
  .sv-sub::-webkit-scrollbar, .st-tkList::-webkit-scrollbar,
  #viewPanel::-webkit-scrollbar, .st-card::-webkit-scrollbar, .st-sepList::-webkit-scrollbar{
    width:0; height:0; }
}

/* ==== THE RULED EXCEPTION TO "DO NOT STYLE SCROLLBARS" (Sid, 2026-08-02, applied 08-03) ========
   The standing ruling is that this product styles NO scrollbar: the absence is what buys the macOS
   overlay bar, dark and hidden at rest, and any ::-webkit-scrollbar rule opts an element out of it
   into an always-visible custom bar. That ruling stands everywhere it is not named below.
   THE EXCEPTION, AND ITS TEST: a scroller may hide its bar when it already carries a FADE OR A PEEK
   as its affordance — because then the bar is not the thing telling you it scrolls, it is a second,
   uglier thing saying what the fade already said. That is the whole class. A scroller with no fade
   and no peek keeps its bar, because hiding it there would remove the only cue it has.
   #viewPanel qualifies: the pinned bar's own gradient sits over its last rows and is wired to its
   scroll position (.st-bar.more::before + syncPanelFade). On a machine set to "always show scroll
   bars" it was drawing a full-height classic grey bar down the panel — Sid's screenshot — which the
   coarse-pointer block already removed on phones and nowhere else.
   .st-tkList already hides its bar at every pointer and has since the window pass; it carries its
   own mask (.st-tkList.scroll.more). It is listed here so the class has ONE home rather than a rule
   here and a precedent over there.
   DELIBERATELY NOT SWEPT: the studio list (.st-list / .sv-list) and the expansion window (.sv-sub).
   They are the surfaces the no-styling ruling was written FOR, they have no fade of their own, and
   removing their bars on desktop would take away their only scroll cue. That stays Sid's call. */
#viewPanel, .st-tkList{ scrollbar-width:none; }
#viewPanel::-webkit-scrollbar, .st-tkList::-webkit-scrollbar{ width:0; height:0; }

/* ============================================================================================
   REDESIGN 2026-08 — ROW GLYPHS ARE BARE (fc)
   "Row glyph order outermost-first: drag dots, then play triangle beside the name; collections
   carry a stroked chevron in the triangle's slot." A triangle and a stroked chevron — not two
   filled discs. The blue play disc and the chevron's grey disc were the pre-redesign treatment:
   they made every row carry two heavy circles, which is why the drawn list reads so much quieter
   than the built one. The ruled sizes are the token file's: play triangle 14 on rows (22 in the
   transport), chevron 13, both optically centred in a 24px column.
   Appended LAST on purpose: the earlier .sv-rplay / .sv-chev size-and-disc rules (lines ~555, ~915,
   ~1285) are equal-specificity, so only a later rule retires them without editing five places.
   ============================================================================================ */
.sv-rplay{ width:24px; height:24px; background:none; border-radius:0; }
.sv-rplay svg{ width:14px; height:14px; }
.sv-rplay svg path{ fill:var(--txt-bright); }
.sv-chev, .sv-row.stack .sv-chev{ width:24px; height:24px; background:none; border-radius:0; }
.sv-row.open .sv-chev, .sv-chev:hover, .sv-row.stack .sv-chev:hover{ background:none; }
.sv-chev svg{ width:13px; height:13px; }
/* the chevron is optically centred: a right-pointing glyph's ink sits right of its box centre, so
   the box shifts 2px left to put the INK on the triangles' centreline (fc, caught at zoom). */
.sv-row.stack .sv-chev{ transform:translateX(-2px); }
.sv-row.stack.open .sv-chev{ transform:translateX(-2px) rotate(90deg); }

/* THE BARE TRIANGLE IS RULED FOR EVERY SURFACE (Sid, 2026-08-13), sub-rows included. They kept a
   blue disc after the top-level rows went bare, which left an open collection reading louder than
   the list around it — the opposite of what expanding one should do. Same treatment, one step
   smaller, matching the sub-row's quieter voice. */
.sv-subPlay{ width:20px; height:20px; background:none; border-radius:0; }
.sv-subPlay:hover{ filter:none; opacity:.8; }
.sv-subPlay svg{ width:12px; height:12px; }
.sv-subPlay svg path{ fill:var(--txt-bright); }

/* ============================================================================================
   REDESIGN 2026-08 — THE WELL IS ONE COMPONENT WITH TWO FACES (fc)
   Measured off frame 2a/2b at 1440x900: the well is 564 wide, radius 14, 1px --line-well, and it
   is 120 high at rest. The player face adds the strip beneath the wave (the strip itself is the
   next piece of work). The drop face carries an inset outline — 9px in, radius 9, a 1px
   rgba(255,255,255,.18) edge — with the lead at 13.5/600 and the hint at 11.5/400 --txt-muted.
   Switching faces changes NOTHING about the list: no row re-renders, no row moves relative to its
   neighbours. The card grows by the strip's height when a track loads, exactly as the boards draw
   it (card 746 -> 799), so the list translates as a block and never re-lays-out.
   ============================================================================================ */
.sv-well{ border-radius:14px; clip-path:inset(0 round 14px); border:1px solid var(--line-well); }
.sv-well{ height:120px; }

/* --- the drop face (at rest) --- */
.sv-wellDrop{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  padding:9px; cursor:pointer; }
.sv-wellDropInner{ flex:1; align-self:stretch; display:flex; flex-direction:column; align-items:center;
  justify-content:center; gap:5px; border:1px solid rgba(255,255,255,.18); border-radius:9px;
  transition:border-color .12s, background .12s; }
.sv-dropLead{ font-size:13.5px; font-weight:600; color:rgba(255,255,255,.8); }
.sv-dropHint{ font-size:11.5px; font-weight:400; color:var(--txt-muted); }
.sv-dropTap{ display:none; font-size:13.5px; font-weight:600; color:rgba(255,255,255,.8); }
/* THE PHONE'S ADD FACE (fd / 3a): one line, no drop language, no inset outline. Gated on the
   POINTER rather than the width because that is the ruling's own reason — files are not dragged on
   a phone — and because a coarse pointer is exactly the device where the outline was drawing a
   dashed target for a gesture that cannot happen. 13.5/600 rgba(255,255,255,.8) is the lead's own
   voice, unchanged; only the words and the box around them go. */
@media (pointer:coarse){
  .sv-dropLead, .sv-dropHint{ display:none; }
  .sv-dropTap{ display:block; }
  .sv-wellDropInner{ border:0; }
}
.sv-wellDrop:hover .sv-wellDropInner{ border-color:rgba(255,255,255,.3); }
/* the drag state is the blue the rest of the system uses for "this drop will land here" */
.sv-wellDrop.drag .sv-wellDropInner{ border-color:var(--blue); background:var(--drop-highlight, rgba(228,111,64,.12)); }

/* --- which face is showing --- */
.sv-well:not(.has-track) canvas,
.sv-well:not(.has-track) .sv-playhead,
.sv-well:not(.has-track) .sv-tL,
.sv-well:not(.has-track) .sv-tR{ display:none; }
.sv-well.has-track .sv-wellDrop{ display:none; }
/* AND WHILE A SHEET IS OPEN, AN EMPTY WELL IS NOT DRAWN AT ALL (Sid, 2026-08-15). Not width-scoped:
   the ruling is about the sheet, not about phones. Scoped to .sheeting so the studio LIST view keeps
   its drop face, which is the whole point of the well there. */
#viewStudio.sheeting .sv-well:not(.has-track){ display:none; }
/* at rest the well is a target, not a scrubber */
.sv-well:not(.has-track){ cursor:default; }

/* ON A PHONE THERE IS NO EMPTY DROP WINDOW (Sid, 2026-08-15). You cannot drag a file on a
   touchscreen, so at rest that box is not a drop target — it is a "tap +" prompt pointing at the +
   button directly above it. It duplicated the + and cost a third of the screen.

   THE SELECTOR IS THE WHOLE POINT, so read it before changing it: this hides the well ONLY in its
   EMPTY state. The well is two faces in one box — the drop zone at rest, the player when a track is
   playing — and .has-track is what decides which. `:not(.has-track)` removes the empty face and
   leaves the player face completely untouched, so a track playing on a phone still gets its
   waveform, its strip and its transport exactly as before. "Remove the well on mobile" would be the
   wrong rule and would take the player with it.

   Desktop keeps the box, because dragging is real there. Width rather than pointer:coarse because
   480 is the phone tier this file already declares everywhere; a coarse-pointer laptop still drags. */
@media (max-width:480px){
  .sv-well:not(.has-track){ display:none; }
}

/* the separate "upload another track" strip is retired — the well at rest IS that door */
.sv-upload, .sv-uploadMore{ display:none; }

/* --- the + browse route, top-left of the card header (2a: 36px, --blue, radius 50%) --- */
.st-plus{ position:absolute; left:0; top:50%; transform:translateY(-50%);
  width:36px; height:36px; border-radius:50%; border:0; background:var(--blue); cursor:pointer;
  display:flex; align-items:center; justify-content:center; padding:0; }
.st-plus:hover{ background:var(--blue-hover); }
.st-head{ position:relative; }

/* ============================================================================================
   REDESIGN 2026-08 — THE STRIP (fc), measured off frames 2b / 2c / 2f
   564 wide inside the well, 53 high, --strip-bg over the blurred wallpaper. One row:
     play  52x44 hit area, 22px triangle, 7px in
     title 15/600 #FFFFFF, starting x59, taking the slack
     time  12.5/500 rgba(255,255,255,.75), right of the title
     ✕     44x44 hit area, 11px glyph, at the right edge
   No prev/next: no board draws them in the strip. No second meta line either — 2b, 2c and 2f all
   draw the title alone, vertically centred, so the spec that used to sit under the title has no
   drawn home on this face and is not invented here.
   ============================================================================================ */
.sv-well.has-track{ height:173px; }        /* 120 wave + 53 strip */
/* THE LABEL FACE'S WELL GROWS WITH ITS STRIP. 2c draws the single sheet's well as 120 wave + 65
   strip = 185; the strip is position:absolute inside the well, so growing it to 65 without growing
   the well simply ate 12px of the waveform instead of adding a line beneath it. Caught by
   verify-shared-controls asserting the studio well's own height rather than the shared tier. */
.sv-well.has-track.labelled{ height:185px; }   /* 120 wave + 65 label strip (2c) */
.sv-wave{ position:relative; height:120px; }
.sv-well canvas{ position:absolute; inset:0; width:100%; height:100%; display:block; }
.sv-well .sv-playhead{ top:7px; bottom:auto; height:106px; }

.sv-strip{ position:absolute; left:0; right:0; bottom:0; height:var(--strip-h);
  display:flex; align-items:center; gap:0; padding:0 0 0 7px;
  background:var(--strip-bg, rgba(10,11,14,.55)); }
.sv-well:not(.has-track) .sv-strip{ display:none; }

/* PREV/NEXT, COLLECTION FACE ONLY (2f: 44x44 slots either side of the 52 play, 17px glyphs at
   rgba(255,255,255,.45) against the play triangle's solid white). The FACE CLASS decides, not
   JavaScript: .has-track already governs drop-vs-player the same way, and a control whose presence
   is a CSS consequence of the face cannot be left behind by a code path that forgot to repaint.
   A single's strip is play · title · time · ✕ and draws no skips (2b).

   EVERYWHERE A COLLECTION IS, ON BOTH PLATFORMS (Sid, 2026-08-13). Well face and sheet, desktop and
   phone. DO NOT ADD A WIDTH GATE HERE: the phone collection-sheet frames 3g and 3h draw play alone
   while their desktop twins 2o and 2q draw prev · play · next, and that disagreement is ruled an
   inconsistency IN THE BOARDS, not a rule to build to. This selector is deliberately width-blind. */
.sv-stripSkip{ flex:0 0 44px; width:44px; height:44px; padding:0; border:0; background:none;
  cursor:pointer; display:flex; align-items:center; justify-content:center; }
.sv-stripSkip svg{ width:17px; height:17px; }
.sv-stripSkip:hover svg path, .sv-stripSkip:hover svg rect{ fill:rgba(255,255,255,.75); }
.sv-well:not(.in-collection) .sv-stripSkip{ display:none; }
.sv-stripPlay{ flex:0 0 52px; width:52px; height:44px; padding:0; border:0; background:none;
  cursor:pointer; display:flex; align-items:center; justify-content:center; }
.sv-stripPlay svg{ width:33px; height:33px; }
.sv-stripPlay:disabled{ opacity:.4; cursor:default; }

.sv-stripLabel{ flex:1; min-width:0; display:flex; flex-direction:column; justify-content:center; gap:2px; }
/* CLIP, not hidden, for the same reason the well takes clip: an ellipsised box whose text overflows
   is programmatically SCROLLABLE under overflow:hidden — measured 12px of travel on #svName at 390 —
   and WebKit scrolls the nearest such ancestor to chase a focused caret. text-overflow:ellipsis
   applies to clip exactly as it does to hidden, so the truncation is unchanged. */
.sv-stripTitle{ min-width:0; font-size:15px; font-weight:600; color:#fff;
  white-space:nowrap; overflow:clip; text-overflow:ellipsis; }
/* the meta beneath it, present only on a single's sheet — 12.5/400, the one secondary grey */
.sv-stripMeta{ min-width:0; font-size:12.5px; font-weight:400; color:var(--txt-muted);
  white-space:nowrap; overflow:clip; text-overflow:ellipsis; letter-spacing:var(--t-tracking); }
/* two lines need the room the board draws for them: 53 -> 65, and the well grows with it. */
.sv-well.labelled .sv-strip{ height:65px; }
/* NO DISMISS WHILE A SHEET IS OPEN (2c) — for BOTH sheets, not just the single's label face: the
   ✕ returns the drop face, which would empty the well out from under the settings still on screen.
   The way out is the sheet's own handle. */
/* THE DISMISS COMES BACK ON THIS SURFACE (Sid, 2026-08-15). It was suppressed while sheeting, from
   the generation where the sheet's well was always loaded and there was nothing to dismiss it to.
   Now that the well is only drawn once something plays, ✕ is the way to put it away again — and it
   is the studio's own control, wired to unloadTrack(), which clears .has-track, which is the exact
   signal the well-hiding rule already reads. Nothing new is drawn and nothing new is wired. */

.sv-stripTime{ flex:0 0 auto; padding:0 10px 0 12px;
  font-family:var(--ui); font-size:12.5px; font-weight:500; color:rgba(255,255,255,.75);
  white-space:nowrap; }

.sv-stripX{ flex:0 0 44px; width:44px; height:44px; padding:0; border:0; background:none;
  cursor:pointer; display:flex; align-items:center; justify-content:center; opacity:.8; }
.sv-stripX:hover{ opacity:1; }
.sv-stripX svg{ width:11px; height:11px; }

/* ==============================================================================================
   THE TWO-LINE STRIP IS THE PHONE STANDARD, ON EVERY FACE (fd) — measured off 3b/3c/3f/3g
   ==============================================================================================
   One line cannot hold a title at this width and the boards do not ask it to. The slots are fixed
   — prev 44 + play 52 + next 44 + the time chip ~104 + ✕ 44 — so the title got whatever was left:
   measured on this build, 3.5px at 375, 18.5 at 390. A collection sub-track played with no visible
   name. Restoring prev/next on the collection face (Sid, 2026-08-13) is what spent the last 88px,
   and "no width gate" is the ruling, so the answer is the one the boards already draw rather than
   hiding a control that was just ruled back on.

   THE NUMBERS ARE DRAWN, NOT CHOSEN. tokens.css: "phone strip two lines: title line 20 (9 top pad)
   + control line 48". Over the 1px hairline BOTH boards draw at the strip's top edge: 1 + 29 + 48
   = 78, and the well grows with it to 200 (frame 3b measures the well 304x200 — 120 wave + 78
   strip + its own 2px of border).

   NO DOM CHANGE, DELIBERATELY. The strip keeps the same six children in the same order on both
   platforms, so no repaint path can leave one arrangement behind — the same reasoning that makes
   prev/next a consequence of the FACE CLASS rather than of JavaScript. The title takes order:-1
   and a full-width basis, so it lands alone on the first wrapped line and the controls fall onto
   the second in their DOM order.

   THE TITLE KEEPS THE 14px RAIL (3b: box edge 44, first glyph 58) — the same rail the sheet's ink
   sits on, and the reason its padding is 12 against the strip's own 2. The control line takes the
   drawn 2px inset instead: a 44px hit slot is furniture, and its glyph is centred inside it. */
@media (max-width:480px){
  .sv-strip{ height:78px; flex-wrap:wrap; align-content:flex-start;
    padding:0 2px; border-top:1px solid rgba(255,255,255,.1); }
  /* line one — the title alone, full width, on the rail */
  .sv-stripLabel{ order:-1; flex:0 0 100%; width:100%; height:29px; display:block; padding:9px 12px 0; }
  .sv-stripTitle{ display:block; line-height:20px; }
  /* the meta has no drawn home on this face: 3b, 3c, 3f and 3g all draw the title ALONE, and a
     single's spec lives in the sheet's own fields two blocks below it. */
  .sv-stripMeta{ display:none; }
  /* line two — controls left, time right, dismiss far right (3b/3c) */
  .sv-stripSkip, .sv-stripPlay, .sv-stripX{ height:48px; }
  .sv-stripTime{ margin-left:auto; padding:0 2px 0 12px; }
  /* the well grows by the second line. BOTH states: on a phone the labelled face is the same two
     lines (3f/3g draw no meta), so it does not step to the desktop's 65 here. */
  .sv-well.has-track, .sv-well.has-track.labelled{ height:200px; }
  .sv-well.labelled .sv-strip{ height:78px; }
}


/* ==============================================================================================
   THE SHEET (frames 2c · 2o · 2q, measured off the drawn boards at 1440x900)
   ==============================================================================================
   PER-TRACK AND PER-ACETATE SETTINGS ONLY. Account settings stay the full right slide-in.

   THE HEIGHT LAW, and why no JavaScript measures it.
   2q puts the sheet's top edge on the strip's bottom edge (measured: well.bottom 280, sheet.top
   280) and 2o's four-track sheet stops 4px short of the same line because its CONTENT stops there
   (sheet 579 tall against the ceiling's 583). One rule produces both: the sheet grows downward
   from a fixed top and the card gives it the room it asks for, until the card runs out.
   Expressed structurally — the card is a flex column already capped in svh, the well is
   flex:0 0 auto, and the sheet is the ONE child allowed to shrink. So:
     content short  -> the card hugs, the well and strip stay where they are, nothing scrolls
     content long   -> the card hits its cap, the sheet gives, and its BODY scrolls as one region
   A measured JS height would have to re-run on every field, row, font and window change, and each
   miss is a sheet that overlaps the strip or floats off the card. This cannot drift.

   THE BODY IS ONE SCROLL REGION (2q): fields and the track list scroll TOGETHER, one gesture —
   the title field rides up under the top fade exactly as a track row does. The bar is a sibling
   of the body, not a child, so "pinned outside the scroll" is structural rather than sticky.

   ONE TEXT RAIL (fc). Every line in the body — head, labels, helpers, the fact line, the bar's
   staging line and "delete this track" — sits on the field boxes' own internal text inset:
   box edge + 14 (13px padding + 1px border). --sv-rail is that 14, declared once. Glyph columns
   (drag dots) are furniture and do NOT take the rail; the right side stays at the box edge.        */

.sv-sheet{
  /* bleeds to the card's edges: the sheet is a plane of its own, not a block inside the padding.
     -22 matches the card's computed horizontal padding, -24 its bottom. */
  flex:0 1 auto; min-height:0; display:flex; flex-direction:column; position:relative;
  /* ZERO GAP (2c: "tight under the well, one continuous stack, no dead band"). Measured on 2q the
     sheet's top edge IS the well's bottom edge; the margin here is only the bleed to the card. */
  margin:0 -22px -24px; padding:0;
  background:var(--surface-1);                    /* #1C1C1E — the drawn sheet ground, a step off the card */
  /* NO HAIRLINE HERE (Sid, 2026-08-15). The sheet already meets the well at a clear edge — its own
     ground is a step off the card — so the rule was drawing a box around something already boxed.
     DELIBERATELY NOT HARMONISED with the recipient listener card, where two hairlines bracket the
     sender and message and were ruled to stay. Different surfaces, different answers, on purpose. */
  --sv-rail:14px;                                 /* the field box's internal text inset */
  --sv-gut:22px;                                  /* the sheet's own gutter */
  --sv-tkInset:8px;                               /* the track ROW's internal inset — see .sv-tkLabRow */
}
.sv-sheetHandle{ flex:none; align-self:center; margin-top:var(--sp-close); width:36px; height:4px; padding:0; border:0;
  border-radius:2px; background:rgba(255,255,255,.18); cursor:pointer; }
.sv-sheetHandle:hover{ background:rgba(255,255,255,.34); }
/* head + labels + helpers: ONE secondary ink with the filter voice, lowercase, no letterspacing */
/* the head is NOT inside the body's gutter, so it carries the gutter AND the rail itself — the
   first cut shared one padding-left with .sv-lab, which overrode the shorthand and landed
   "collection settings" 22px left of every other line in the sheet. */
/* the 6 goes through the token like the three 12s did (Sid, 2026-08-14) — same fault, and leaving
   one literal behind is how the next sweep concludes the file is clean. A true no-op today: (fh)
   retired the phone step-down, so --sp-tight is 6px on every width. */
/* LOWER, SO IT INTRODUCES THE BLOCK BENEATH IT RATHER THAN FLOATING (Sid, 2026-08-16).
   It was 6 above and 12 below — nearer the sheet's top edge than the thing it names, which is why
   it read as floating between the card header and the list. Swapped onto the same scale: --sp-block
   above, and the body's own top inset drops to --sp-tight below, so the heading now sits three
   times further from the edge than from its block. Both sheets share this rule and both move. */
.sv-sheetHead{ flex:none; padding:var(--sp-block) var(--sv-gut) 0 calc(var(--sv-gut) + var(--sv-rail)); }
/* THE SHEET ANNOUNCES ITSELF LIKE THE NAMING SCREEN DOES (Sid, 2026-08-15). Same step as
   .st-sepStep's "edit track details 1 of 12" — --t-title, 22/700 — so both sheets say where you are
   in one voice. The head no longer shares .sv-lab's 13/400 muted treatment; the field labels keep it. */
.sv-lab{ font-size:13px; font-weight:400; color:var(--txt-muted); }
/* ...AND THEN CAME DOWN TO --t-body (Sid, 2026-08-15): 22 was half again too big for a settings
   sheet. 11 is not a step, so the choice was between --t-body (14.5) and --t-caption (13), and this
   is 14.5. The reason is the line under it: every field label is 13/400 muted, so a 13px heading
   would be the SAME SIZE as everything it introduces, leaving weight and colour alone to carry the
   hierarchy. 14.5/700 white keeps one size step above them, which is what makes it read as a heading
   rather than a bolder label. --t-title still belongs to the naming screen; these two sheets no
   longer share that voice, which is the one thing this trades away.
   ONE RULE, BOTH SHEETS — the collection's and the track's heading are the same element. */
.sv-sheetHead{ font:var(--t-w-bold) var(--t-body)/1.25 var(--ui); letter-spacing:normal;
  color:#fff; }
.sv-lab{ padding-left:var(--sv-rail); }
.sv-help, .sv-fact{ font-size:var(--t-helper); font-weight:400; color:var(--txt-muted); padding-left:var(--sv-rail); }

.sv-sheetScroll{ position:relative; flex:1 1 auto; min-height:0; display:flex; flex-direction:column; }
/* the bottom padding is the "?" chip's reserve — 0 unless the chip is actually overlapping this
   region (studio.js, syncSheetChipReserve), so nothing is given away where the two never meet. */
.sv-sheetBody{ flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain;
  padding:var(--sp-tight) var(--sv-gut) var(--sv-chip-reserve, 0px); display:flex; flex-direction:column; gap:var(--sp-close); }
/* 12 between blocks (Sid, 2026-08-16: "tighten the whole thing and give the height to the track
   list"), 6 from a label to its OWN content — the two ruled gaps, and the only two. This was 18,
   itself one step down from --sp-air on 2026-08-15; it is now one step down again, --sp-close.
   Four block gaps on this sheet, so the step is worth 24px. */
.sv-blk{ display:flex; flex-direction:column; gap:var(--sp-tight); min-width:0; }
.sv-pair{ flex-direction:row; gap:var(--sp-close); }
.sv-half{ flex:1 1 0; min-width:0; }
/* downloads is a control row: label left, toggle right, ON THE SAME LINE, status beneath it */
.sv-ctl{ flex-direction:row; align-items:center; gap:var(--sp-close); padding:2px 0; }
.sv-ctlText{ flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:var(--sp-tight); }
/* ONE LINE, MIDDOT-SEPARATED (Sid, 2026-08-15) — the create panel's construction, and literally its
   element: SHEET_MOVES relocates #dlNote itself into #svSlotDlNote, so both surfaces have always
   shared one node and one string writer. Only the container's axis differed. Row instead of column,
   and the separator is the same middot the note's own copy already uses. */
#svSheet .sv-ctlText{ flex-direction:row; align-items:baseline; gap:var(--sp-tight); flex-wrap:nowrap; }
#svSheet .sv-ctlText .sv-slotNote{ min-width:0; }
#svSheet .sv-ctlText .sv-slotNote::before{ content:'·'; margin-right:var(--sp-tight); color:var(--txt-muted); }
.sv-slotSw{ flex:none; }
/* THE LABEL ROW SITS ON ITS OWN CONTENT'S EDGES, NOT THE SHEET'S (Sid, 2026-08-16).
   Every other label on this sheet is inset by --sv-rail because it introduces a FIELD BOX whose text
   is inset by 14. This one introduces TRACK ROWS, which are inset by 8, so the same rule — a label
   lines up with the text it names — puts it 6px further left than the field rail, not on it.
   Measured before: "tracks" text at 51 against the row's first child at 45; "+ add tracks" right at
   338 against the row's × at 330. One value now governs both the rows and this row above them, so
   the two cannot drift apart. */
.sv-tkLabRow{ display:flex; align-items:baseline; gap:var(--sp-close); padding-right:var(--sv-tkInset); }
.sv-tkLabRow .sv-lab{ flex:1 1 auto; padding-left:var(--sv-tkInset); }
/* the track list is the "tracks" label's own content, so it takes the 6px gap, not the 24 */
.sv-tkBlk{ gap:var(--sp-tight); }
/* A SLOT WITH NOTHING SHOWING TAKES NO GAP (Sid, 2026-08-16: "the biggest empty space on the
   sheet"). svSlotTkNote and svSlotPick each hold one [hidden] child at rest, so both measured
   ZERO HIGH and yet each still claimed its 6px share of the block's gap — 12px of space below the
   track list belonging to two things that are not there. Not :empty, which a whitespace text node
   defeats: the test is whether anything inside is actually showing. Self-maintaining — the day a
   note or picker unhides, its gap comes back on its own. */
.sv-tkBlk > .sv-slot:not(:has(> :not([hidden]))){ display:none; }

/* ---- the fields, restated at the sheet's sizes ------------------------------------------------
   44 high, radius 13, --surface-2 on --separator, value 14/500 white. The SAME elements the
   create panel uses (#title, #message, #expWrap, #pin, #dlToggle) — moved in here by openSheet
   and moved back by closeSheet, so there is one of each and no second set to keep in step. */
#svSheet .st-input, #svSheet .st-pin, #svSheet .st-pill{
  width:100%; height:44px; box-sizing:border-box; padding:0 13px; margin:0;
  border-radius:13px; background:var(--surface-2); border:1px solid var(--separator);
  font-family:var(--ui); font-size:14px; font-weight:500; color:#fff; text-align:left;
  letter-spacing:var(--t-tracking); }
#svSheet .st-input::placeholder, #svSheet .st-pin::placeholder{ color:rgba(255,255,255,.3); }
#svSheet .st-input:focus, #svSheet .st-pin:focus{ border-color:var(--blue); box-shadow:0 0 0 3px rgba(228,111,64,.18); }
#svSheet .st-expWrap{ display:block; width:100%; }
#svSheet .st-pill{ display:flex; align-items:center; justify-content:space-between; gap:8px; cursor:pointer; }
#svSheet .st-dlNote{ font-size:var(--t-helper); font-weight:400; color:var(--txt-muted); padding-left:var(--sv-rail); }
#svSheet .st-dlNote.on{ color:var(--txt-muted); }   /* the toggle carries the state; the line is a fact */
/* the ruled switch: 46 x 28, knob 24, blue when on. Green does not exist. */
#svSheet .sv-toggle{ width:46px; height:28px; padding:2px; }
#svSheet .sv-toggle .knob{ width:24px; height:24px; }
#svSheet .st-tkAdd{ font:400 var(--t-helper) var(--ui); color:var(--blue-text); }

/* ---- the track rows (2o) ----------------------------------------------------------------------
   40 high, hairline between, drag dots hover-reveal with their space reserved (custom order IS
   the running order here, so the affordance is always meaningful), title 15/400 against the
   parent's 600, duration right. The hover shade is the ruled --row-hover, bleeding 8px into the
   gutter exactly as the studio list's own rows do. */
/* the panel's list insets its rows 10px and its drag handle carries a -6 margin — both tuned for
   the create panel's narrower block. In the sheet they push the title off the rail, so the list
   sits flush and the handle sits in its column; the row's own -8 bleed then puts the dots exactly
   where 2o draws them, 14 left of the rail, with the title 6 right of it. */
/* THE WINDOW REACHES THE SHEET (Sid, 2026-08-14). `overflow:visible; max-height:none` was 2q's "no
   second scroller", and it is what let a seven-track list run 75px past the body's bottom edge.
   Both were also ID-SCOPED, so they outranked the shared .st-tkList.scroll rules at (1,1,0) against
   (0,2,0) — the inline cap studio.js sets would have won on max-height, but overflow would still
   have been visible and the "scroller" would have had no scrollbar and no clip. Removing them is
   what actually hands this list back to the shared component; the JS change alone was not enough. */
#svSheet .st-tkList{ display:flex; flex-direction:column; padding:0; }
/* FIVE COLUMNS FOR FIVE THINGS: dots · title · pencil · duration · ×. The first cut declared four
   and the × dropped onto an implicit second grid row, landing on top of the drag dots — invisible
   until measured, because it is transparent at rest.
   The title column is minmax(0,max-content) so the pencil sits AGAINST the title as drawn (2o)
   rather than at the far end of a 1fr, while the 0 minimum keeps the ellipsis for a long one; the
   duration takes the flexible column and is pushed to its end, so it stays on the right edge.
   Pencil and × reserve their space at rest (opacity, not display) — the same law as the drag dots:
   a row must not move under the pointer that is about to click it. */
/* FLEX, NOT A GRID (Sid, 2026-08-15) — and the grid broke for a reason worth recording: its column
   count was written for the items that happened to be laid out, and restoring the drag handle on
   coarse pointers earlier today added one. Six children in a five-column template (four on a phone)
   put .st-tkX on an implicit second row at column 1 — measured at x=37, the drag handle's column,
   26px below its own title. That is the reported "x below and to the left".
   This is the studio list row's construction, taken rather than reinvented: a flex row, the name the
   only item allowed to grow (flex:1 with min-width:0 so a long title ellipses instead of pushing),
   and everything else flex:none — so the last child sits at the right edge because it is last, not
   because a column was counted. Adding a seventh child cannot break it. */
/* THE 8px BLEED IS THE STUDIO'S, NOT THIS SURFACE'S (Sid, 2026-08-15). The reach was a NEGATIVE
   MARGIN here (margin:0 -8px), so the row's box was 16px wider than the rounded block it sits in and
   :hover painted that full box — tint and shadow spilling past the container's corners. The studio's
   own bleed is a different mechanism on a different class in a different container (.sv-row::before,
   inset:0 -8px, full-width rows with nothing to spill out of), so it is untouched by this: no
   selector here matches .sv-row. The padding stays, so the text keeps its inset and only the paint
   moves. */
#svSheet .st-tkRow{ display:flex; align-items:center; gap:10px; height:40px; box-sizing:border-box;
  border-bottom:0; border-top:1px solid var(--row-hairline); margin:0; padding:0 var(--sv-tkInset);
  border-radius:8px; }
#svSheet .st-tkRow > *{ flex:0 0 auto; }
#svSheet .st-tkName{ flex:1 1 auto; min-width:0; }
#svSheet .st-tkDur{ text-align:right; }
/* the dots keep their drawn 10px column instead of overflowing it and shunting the title right */
#svSheet .st-tkDrag{ width:10px; margin:0; padding:0; justify-content:center; }
#svSheet .st-tkList .st-tkRow:first-child{ border-top:0; }
#svSheet .st-tkRow:hover{ background:var(--row-hover); }
#svSheet .st-tkRow.on{ background:var(--row-playing); }
#svSheet .st-tkNum{ display:none; }              /* no numeral column is drawn: order IS the number */
/* (the sheet's OWN hide/reveal pair is DELETED, 2026-08-16. It set opacity:0 unconditionally and sat
   later in the file than the shared rule, so it won at every pointer type — which is why the sheet's
   grips were invisible on touch even after the shared rule was extended to reach them. Deleting it
   is the reuse Sid asked for: the sheet now falls under the one rule that governs the studio's rows,
   hidden until hover where hovering exists and visible at rest where it does not. Two rules for one
   behaviour is exactly what this ruling exists to remove.) */
#svSheet .st-tkName{ font:400 15px var(--ui); color:var(--ink-title); letter-spacing:var(--t-tracking); }
#svSheet .st-tkRow.on .st-tkName{ color:#fff; }
#svSheet .st-tkDur{ font:500 12.5px var(--ui); color:rgba(255,255,255,.6); letter-spacing:var(--t-tracking); }
/* pencil and × are hover-revealed: at rest the row reads as drawn (title, duration, nothing else) */
#svSheet .st-tkEdit, #svSheet .st-tkX{ opacity:0; transition:opacity .12s; flex:none; }
#svSheet .st-tkRow:hover .st-tkEdit, #svSheet .st-tkRow:hover .st-tkX,
#svSheet .st-tkEdit:focus-visible, #svSheet .st-tkX:focus-visible{ opacity:1; }
/* AT REST ON TOUCH (fd): "the rename pencil renders at rest in the collection sheet, because hover
   cannot carry discoverability on touch". The × goes with it for the same reason and one more: a
   control that is invisible but still tappable is worse than one that is simply absent — the
   opacity-0 drag handle on every list row was exactly that trap, measured this pass.
   3g draws the phone tracklist row as title + pencil + duration and no ×, while 2o draws the ×
   on the desktop twin. That is the same class of board disagreement (fg) already ruled on for
   prev/next — an inconsistency in the boards, not a rule — so the × stays reachable and the
   disagreement is recorded rather than settled here. */
.sv-helpTouch{ display:none; }
@media (pointer:coarse){
  /* the two-face helper (3g), and the desktop sentence goes with the handle it describes */
  /* (#svTkHelp is gone with its sentence — the grips it stood in for are visible now.) */
  .sv-helpTouch{ display:block; }
}
@media (pointer:coarse){
  #svSheet .st-tkEdit, #svSheet .st-tkX{ opacity:1; }
  /* AND THE TEMPLATE LOSES THE HANDLE'S COLUMN WITH IT. This row is a GRID, not a flex line, so
     display:none on the handle does not just free its space — it shifts every remaining child one
     column left: measured at 390, the title landed in the 10px drag column (w=10) and the × in the
     1fr (w=222.6, stretched across the row and swallowing every tap meant for the title). Four
     items, four columns. The flex lists are immune, which is exactly why this only bit here. */
  /* (the 4-column phone template is retired with the grid it belonged to — see the flex rule.) */
}
/* the rename, in place: the row's title becomes the input, blue ring, nothing else moves */
#svSheet .st-tkRenameIn{ width:100%; height:30px; box-sizing:border-box; margin:0; padding:0 8px;
  border:0; border-radius:8px; background:var(--surface-2); box-shadow:0 0 0 1px var(--blue);
  font:400 15px var(--ui); color:#fff; letter-spacing:var(--t-tracking); }
#svSheet .st-tkRenameIn:focus{ outline:none; }

/* ---- the fades ---------------------------------------------------------------------------------
   BOTH EDGES, WHEREVER CONTENT CONTINUES, and never where it does not — a gradient that is always
   there is decoration claiming to be information (the same rule the panel's own fade follows).
   Ruled: bottom 44 high (0 -> .96), top 40 high (.9 -> 0 at 92%), in the sheet's ground. */
.sv-sheetFadeT, .sv-sheetFadeB{ position:absolute; left:0; right:0; pointer-events:none;
  opacity:0; transition:opacity .12s linear; z-index:3; }
.sv-sheetFadeT{ top:0; height:40px; background:linear-gradient(180deg, rgba(28,28,30,.9), rgba(28,28,30,0) 92%); }
.sv-sheetFadeB{ bottom:0; height:44px; background:linear-gradient(180deg, rgba(28,28,30,0), rgba(28,28,30,.96)); }
.sv-sheet.moreUp .sv-sheetFadeT{ opacity:1; }
.sv-sheet.moreDown .sv-sheetFadeB{ opacity:1; }

/* ---- the bar, pinned outside the scroll --------------------------------------------------------
   delete left on the rail, the staging line, save right at the box edge. The line is the staging
   law said out loud, and it is why the bar is never empty. */
/* THE BAR HAS BOTH DRAWN FORMS, and takes whichever the room allows.
   2c and 2o draw delete, caption and save on ONE row, against a 600-wide card. 3f draws them as
   two — "the staging caption alone on the text rail, then delete left and save right" — for the
   phone. They are not in conflict: they are the same bar at two widths, and now that the card is
   the drawn 600 the desktop one is reachable.
   TWO ROWS IS THE BASE because it is the form that cannot fail; one row is the enhancement, and it
   turns on only where the card is genuinely 600 wide. 712 = the card's 600 plus .sb-stage's 56px
   gutters either side (sendbox.css:58) — below that .st-card's max-width:100% is what is actually
   governing and the card is narrower than it is declared, which is precisely when the one-row form
   starts wrapping "delete this track" and "save" onto two lines each. */
/* iOS: THE SHEET'S BAR STANDS DOWN WHILE THE KEYBOARD IS UP, on coarse pointers only, and for the
   same reason .st-card > .st-bar.kbd does — see studio.js's syncBarKbd. Hiding it hands the room
   back to the field being typed into and brings it straight back when the keyboard goes. */
.sv-sheetBar.kbd{ display:none; }
/* ONE ROW (Sid, 2026-08-15), now the staging line has gone: delete hard left, then cancel and the
   primary hard right. Delete keeps its own column and the 1fr between absorbs every width, so the
   red never has to share a line with the primary or wrap under it at phone. */
.sv-sheetBar{ flex:none; display:flex; align-items:center; gap:12px;
  padding:12px var(--sv-gut) 14px; }   /* no rule above it either: the
  bar already sits on its own darker band, which is the separation (Sid, 2026-08-15) */
.sv-sheetBar .sv-slotDel{ flex:0 1 auto; min-width:0; margin-right:auto; }
.sv-sheetCancel{ flex:0 0 auto; border:0; background:none; padding:0; cursor:pointer;
  font:600 13px var(--ui); letter-spacing:var(--t-tracking); color:var(--txt-muted); }
.sv-sheetCancel:hover{ color:#fff; }
/* one row, as 2c and 2o draw it: delete on the rail, the staging line taking the slack and reading
   right, save at the box edge. */
@media (min-width:712px){
  /* (the wider tier's grid retired with the staging line — the bar is one flex row at every width now) */
}
.sv-sheetBar .sv-slotDel{ min-width:0; align-self:center; }
.sv-sheetBar .sv-slotSave{ justify-self:end; }
.sv-sheetBar .sv-slotDel:empty{ display:none; }
/* "delete this track" is NOT red here: red lives on the confirm only (fc). */
/* THE ONE DESTRUCTIVE RED (Sid, 2026-08-15): --red-del #E0403A, the softer red, NOT the bright
   #FF453A that (fi) kept out of expiry. Already the colour #delBtn uses on the panel's own bar;
   the sheet was overriding it back to title ink, which is why delete read white in here. */
#svSheet #delBtn{ font:600 13px var(--ui); color:var(--red-del); letter-spacing:var(--t-tracking);
  padding-left:var(--sv-rail); white-space:nowrap; text-align:left; }
#svSheet #delBtn:hover{ color:#FF6A63; }   /* lighter red on hover, never white — it stays destructive */
#svSheet #saveBtn{ flex:none; width:auto; min-width:0; height:44px; margin:0; padding:0 30px; white-space:nowrap;
  border-radius:14px; background:var(--blue); box-shadow:0 8px 20px rgba(228,111,64,.4);
  font:700 15px var(--ui); color:#fff; letter-spacing:var(--t-tracking); }
#svSheet #saveBtn:hover{ background:var(--blue-hover); }

/* the sheet OWNS the card while it is open: no filter row, no list, no ghost rows behind it
   (3f — "the stack is well, strip, sheet, nothing between"). */
#viewStudio.sheeting .sv-tracksHead, #viewStudio.sheeting .sv-list,
#viewStudio.sheeting .sv-usage, #viewStudio.sheeting .st-empty{ display:none; }

/* THE UNSAVED-CHANGES CONFIRM (2e). Three actions, in the drawn order and the drawn inks:
   discard leads on the left in destructive OUTLINE (red text, not a red fill — the fill belongs to
   an action that actually destroys audio), keep-editing and save sit together on the right, and
   save is the blue one because saving is the answer that loses nothing. */
#pinModal.staged .acbtn{ flex:0 1 auto; }
#pinModal.staged #pinConfirm{ order:1; margin-right:auto; background:transparent; color:var(--red);
  border:1px solid rgba(255,69,58,.38); }
#pinModal.staged #pinConfirm:hover{ background:rgba(255,69,58,.1); }
#pinModal.staged #pinCancel{ order:2; background:var(--surface-2); border-color:var(--separator); color:#fff; }
#pinModal.staged #pinAlt{ order:3; box-shadow:0 8px 20px rgba(228,111,64,.4); }


/* ==============================================================================================
   iOS AUTO-ZOOM, THE LAST WORD — deliberately the last rule in this file.
   ==============================================================================================
   WebKit zooms the page whenever a focused text input computes under 16px, and leaves it zoomed and
   pannable: the card off the left edge, the page scrolling sideways. The override for that has
   existed for a long while, scoped `#viewPanel #title, …` — and that scoping became wrong the moment
   openEdit started MOVING those fields into the sheet. On the edit surface the rule stopped matching
   and iOS was free to zoom again.
   Un-scoping it was not enough either: `#svSheet .st-input` in the sheet block above is (1,1,0), the
   same weight as any selector written here, so ORDER decides — which is why this sits at the end of
   the file rather than beside its sibling 300 lines up. Moving it back up silently reinstates the
   bug, so it says so here.
   Desktop keeps the declared steps; a coarse pointer is the only place this applies. */
@media (pointer:coarse){
  #svSheet .st-input, #svSheet .st-pin, #svSheet #title, #svSheet #message, #svSheet #pin{
    font-size:16px;
  }
}

/* ---- item 4: the create panel gets the sender OUT (Sid, 2026-08-16) --------------------------
   "get the sender in and out to the studio as fast as possible. They can add tracks from the
   studio." Three things went, all of them instructions or facts that the studio repeats a moment
   later: the well's "drop a file to add another track" caption, the "lossless" spec line under it,
   and "+ add tracks".
   SCOPED TO THE PANEL, not deleted, because #pmTkAdd is the SAME NODE the edit sheet borrows into
   #svSlotAdd — the slot machinery moves it rather than cloning it, so deleting it here would take
   the sheet's add control with it, and the sheet is exactly where Sid wants adding to happen.
   Once it is in the sheet this selector no longer matches and it comes back on its own. */
#viewPanel #wellHint,
#viewPanel #pmFmt,
#viewPanel #pmTkAdd{ display:none !important; }

/* ---- the row's copy control: DESKTOP ONLY (Sid, 2026-08-16) ----------------------------------
   Hidden by default and shown only where there is BOTH a mouse and the width to afford it. Two
   conditions on purpose: hover:hover alone would show it in a desktop window dragged to 375, which
   has exactly the phone's problem — measured, a 44pt control there cuts the row title from 191 to
   ~147. 721px is the same desktop tier sendbox.css already uses, so this is not a new breakpoint.
   Quiet at rest like the kebab beside it; it brightens on hover, the way every other row control
   does, rather than announcing itself on a list of twenty rows. */
.sv-rcopy{ display:none; }
@media (hover:hover) and (min-width:721px){
  .sv-rcopy{ display:inline-flex; align-items:center; justify-content:center;
    width:28px; height:28px; padding:0; border:0; background:none; cursor:pointer;
    border-radius:8px; opacity:.55; transition:opacity .12s linear, background-color .12s linear; }
  .sv-rcopy:hover{ opacity:1; background:rgba(255,255,255,.06); }
  .sv-rcopy:focus-visible{ opacity:1; outline:2px solid var(--focus, #E46F40); outline-offset:2px; }
  .sv-rkwrap{ display:flex; align-items:center; gap:var(--sp-tight); }
}

/* ================================================================================================
   ACCOUNT & MAIN SETTINGS — the full slide-in. 2d (desktop, 1440) and 3i (phone, 390).
   ================================================================================================
   Every number below is READ OFF THE BOARD, not chosen here. Where the board's value already has a
   token it uses the token, so this screen cannot drift from the rest of the studio:
     #8E9196 -> --txt-muted     #E8E9EA -> --ink-title     #3B7BFA -> --blue-text
     #2F6BFA -> --blue          #101112 -> --ink-invert
   Two values the board declares have NO token and are declared ONCE here rather than repeated at
   each use, which is the --filter-band lesson from (fj): a value written twice is a value that will
   fall out of step.
     --acs-ground  the slide-in surface. The redesign bundle names it --slide-in: #121214, and it is
                   deliberately NOT --surface-card: this is a screen, not a card on a wallpaper.
     --acs-line    the row divider. The board draws .08 here against the shared --hairline's .10, so
                   it is written down with its reason and a later consistency sweep leaves it alone.

   WHAT THIS DOES NOT TAKE FROM THE BUNDLE: design/redesign-2026-08/tokens.css also carries
   --well-scrim: rgba(10,11,14,.66) "over the blurred wallpaper inside the well (blur 14px, inset
   -24px)". That is the construction Sid killed on 2026-08-15, and taking the bundle's CSS verbatim
   would restore it. The bundle is the drawn source for geometry and type; its well values are
   pre-(fj) and are not used anywhere in this file.

   Z-INDEX, placed rather than guessed (the (fj) fault where a menu painted behind the card and
   thirteen assertions never asked whether it was visible): 45 sits ABOVE the card and the studio's
   own .st-menu (40), and BELOW .overlay (50) so "change photo" opens its existing #imgModal OVER
   this screen instead of under it. .vc-rotate keeps 9000 and still covers everything.
   ================================================================================================ */
.acs{
  --acs-ground:#121214;
  --acs-line:rgba(255,255,255,.08);
  --acs-tidy:1;   /* the layout generation (the 2026-09-14 tidy). A verification run reads it to prove which build it measured; nothing in the product reads it. */
  position:fixed; inset:0; z-index:45; background:var(--acs-ground);
  display:flex; flex-direction:column; overflow:hidden;
}
.acs[hidden]{ display:none; }

/* the head: back left, title optically centred on the SCREEN (absolute, so a long back label cannot
   shove it off centre), close right in a 44 target. Board: padding 20/32, 13.5/600 blue back,
   13/400 muted title. */
.acs-head{ position:relative; flex:0 0 auto; display:flex; align-items:center; padding:var(--sp-block) var(--sp-wide); }
.acs-back{ background:none; border:0; padding:0; cursor:pointer; font:600 13.5px var(--f, Inter,sans-serif); color:var(--blue-text); }
/* THE SCREEN'S TITLE. DELIBERATE DEVIATION FROM THE BOARD (Sid, 2026-08-23): both 2d and 3i draw
   this at 13/400 in --txt-muted, which is the caption voice, and the build reproduced it faithfully.
   Sid's call is that a screen title reading as a caption is wrong whatever the board says, so it
   takes the declared title step and the title ink instead. --t-title is 22, and 20 under 480 from
   the scale's own step-down, so the phone is not a separate number. Written down as a deviation so a
   later pass comparing build to board does not "correct" it back. */
.acs-title{ position:absolute; left:0; right:0; text-align:center;
  font:var(--t-w-semi) var(--t-title)/1.2 var(--f, Inter,sans-serif); color:var(--ink-title);
  pointer-events:none; }
.acs-x{ margin-left:auto; width:44px; height:44px; display:flex; align-items:center; justify-content:center;
  background:none; border:0; padding:0; cursor:pointer; color:rgba(255,255,255,.55); }
.acs-x:hover{ color:rgba(255,255,255,.85); }
.acs-back:focus-visible, .acs-x:focus-visible{ outline:2px solid var(--blue); outline-offset:2px; border-radius:8px; }

/* THE SLIDE-IN ITSELF SCROLLS (fd). One scroll region for the whole body, so neither column can
   become a second scroller inside the first. */
.acs-scroll{ flex:1 1 auto; overflow-y:auto; -webkit-overflow-scrolling:touch;
  padding-bottom:max(64px, calc(env(safe-area-inset-bottom, 0px) + 50px)); }
/* ^ THE "need help?" PILL'S ZONE, not a rhythm step. support.js pins that pill 14-18px from the bottom
   right corner, 33px tall (a 42px circle on the phone), above everything. The tidy made the pictures
   the last group, so the panel's final control is a right-aligned save button sitting exactly in
   that corner, and 30px of padding left it under the pill at 390, 820 and 1024. This is the same
   reservation sendbox.css makes for the same pill on the phone homepage, so the two stay one value. */

/* THE TIDY (Sid, 2026-09-14). Four groups, each labelled once, every gap on the declared scale
   (6/12/18/24/30 and nothing between). The markup explains the grouping; this is only how it sits.
   ONE COLUMN BY DEFAULT, in the markup's order: your account, new links, apps, then the pictures.
   The old layout was a 640 rail plus whatever was left, which is why the side column measured 240
   wide at 1024 and 36 wide at 820. A single column cannot be squeezed that way. */
.acs-cols{ box-sizing:border-box; width:100%; max-width:700px; margin:var(--sp-air) auto 0; padding:0 var(--sp-wide);
  display:flex; flex-direction:column; gap:var(--sp-wide); }
.acs-col{ display:flex; flex-direction:column; gap:var(--sp-wide); min-width:0; }
/* TWO COLUMNS ONCE THEY FIT. 360 for the short groups and up to 640 for the pictures, 30 apart.
   The short column STAYS PUT while the pictures scroll beside it, so it never reads as a column of
   empty space. This is the only place sticky is used, and only at a width where the columns sit side
   by side: stacked, a sticky block would ride over the pictures scrolling under it. */
@media (min-width:900px){
  .acs-cols{ max-width:1090px; display:grid; grid-template-columns:360px minmax(0, 640px);
    align-items:start; justify-content:center; }   /* the 30 between them is the base rule's own gap */
  .acs-col--side{ position:sticky; top:var(--sp-air); }
}

/* a group: its label, then its items, 12 apart.
   THE LABEL IS A VOICE NO ITEM USES. Items are bright (sign out, expiry's value, wallpaper); the four
   group labels are the declared caption step, semibold, in the muted ink. The first pass set them
   bright at 14.5, which was 0.5px and nothing else away from "sign out" at 600 14, so a row under
   "your account" read as a second heading. Caught by review, not by the harness, which only compared
   the labels with each other. */
.acs-grp{ display:flex; flex-direction:column; gap:var(--sp-close); margin:0; }
.acs-grpLab{ margin:0; font:var(--t-w-semi) var(--t-caption)/1.2 var(--f, Inter,sans-serif);
  letter-spacing:var(--t-tracking); color:var(--txt-muted); }
.acs-rows{ display:flex; flex-direction:column; }
/* a section inside a group (wallpaper, link preview picture): opened by the same hairline the rows
   use, so every item under a group label starts on a line. Two sections sit 24 apart. */
.acs-sec{ display:flex; flex-direction:column; gap:var(--sp-close); border-top:1px solid var(--acs-line); padding-top:var(--sp-close); }
.acs-sec + .acs-sec{ margin-top:var(--sp-close); }
/* a name sits WITH its description (6), and each way to choose the wallpaper stands 18 clear of what
   is above it, with the "or" between them on the same 18. Evenly spaced at 12 the top of the section
   read as four lines of one list, and "pick one of ours" looked like more of the wallpaper intro. */
.acs-intro{ display:flex; flex-direction:column; gap:var(--sp-tight); }
.acs-sec > .acs-blk, .acs-sec > .acs-or{ margin-top:var(--sp-tight); }

/* THE APPS LINK IS A ROW NOW, drawn like sign out and expiry beside it. It is a LINK: no background,
   no box, only the panel's text-link colour. It used to be a 22px line above the columns at the
   scroll region's own left edge, which put it outside the layout and against the screen.
   The class is NOT .acs-dl: that name was already taken by the downloads toggle's knob wrapper. */
.acs-getapps{ height:48px; display:flex; align-items:center; text-decoration:none; cursor:pointer;
  border-top:1px solid var(--acs-line); font:var(--t-w-semi) 14px var(--f, Inter,sans-serif); color:var(--blue-text); }
.acs-getapps:hover{ color:#fff; }
.acs-getapps:focus-visible{ outline:2px solid var(--blue); outline-offset:-2px; border-radius:8px; }

.acs-blk{ display:flex; flex-direction:column; gap:var(--sp-close); }
/* a section's own name (wallpaper, link preview picture) is an ITEM, so it takes the bright item voice;
   the two ways to fill the wallpaper sit one step under it */
.acs-lab{ font:var(--t-w-semi) 14px var(--f, Inter,sans-serif); color:var(--ink-title); }
.acs-way{ font:var(--t-w-med) 13.5px var(--f, Inter,sans-serif); color:var(--ink-quiet); }
/* "or" between the curated five and your own: a hairline either side, so the two ways read as
   alternatives for ONE wallpaper rather than two features */
.acs-or{ display:flex; align-items:center; gap:var(--sp-close);
  font:var(--t-w-reg) 12.5px var(--f, Inter,sans-serif); color:var(--txt-muted); }
.acs-or::before, .acs-or::after{ content:''; flex:1 1 auto; border-top:1px solid var(--acs-line); }

/* account row: 52 avatar, name 16/700, email 12.5 muted, "change photo" blue text at the far end.
   It opens the group on a hairline like every other first item. */
.acs-acct{ display:flex; align-items:center; gap:var(--sp-close); border-top:1px solid var(--acs-line); padding-top:var(--sp-close); }
.acs-ava{ flex:0 0 auto; width:52px; height:52px; border-radius:50%; background:#fff; color:var(--ink-invert);
  display:flex; align-items:center; justify-content:center; font:600 19px var(--f, Inter,sans-serif);
  background-size:cover; background-position:50% 50%; overflow:hidden; }
.acs-ava.has-img{ color:transparent; }
.acs-who{ display:flex; flex-direction:column; gap:2px; min-width:0; }
.acs-nm{ font:700 16px var(--f, Inter,sans-serif); color:#fff; }
.acs-em{ font:400 12.5px var(--f, Inter,sans-serif); color:var(--txt-muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.acs-link{ margin-left:auto; flex:0 0 auto; background:none; border:0; padding:0; cursor:pointer;
  font:600 13px var(--f, Inter,sans-serif); color:var(--blue-text); }
.acs-link:focus-visible{ outline:2px solid var(--blue); outline-offset:3px; border-radius:6px; }

/* the value rows. 48 tall with a top hairline, label muted, value 13.5/600 at the right end.
   A row that is a CONTROL is a <button> and says so on hover and focus; a row that is a STATUS LINE
   is a <div> and never lights up, because the board gives it no affordance. */
.acs-row{ height:48px; display:flex; align-items:center; gap:12px; border-top:1px solid var(--acs-line);
  width:100%; padding:0; background:none; text-align:left; }
.acs-rlab{ font:400 13.5px var(--f, Inter,sans-serif); color:var(--txt-muted); }
.acs-rval{ margin-left:auto; display:flex; align-items:center; gap:var(--sp-tight);
  font:600 13.5px var(--f, Inter,sans-serif); color:var(--ink-title); }
.acs-ract{ font:600 14px var(--f, Inter,sans-serif); color:var(--ink-title); }
/* every row that is a <button> must have the UA's default border stripped on the three sides the
   hairline does not own. Missing .acs-row--switch off this list drew a box round the downloads
   row: invisible to every measurement I had, obvious the moment I looked at it. */
.acs-row--pick, .acs-row--act, .acs-row--switch{ border-left:0; border-right:0; border-bottom:0; cursor:pointer; }
.acs-row--pick .acs-rval svg{ color:rgba(255,255,255,.4); }
.acs-row--pick:hover .acs-rval, .acs-row--act:hover .acs-ract:not(.acs-ract--danger){ color:#fff; }
/* the board declares #FF453A here, which IS --red. tokens.css reserves --red for destructive
   actions (delete buttons, confirms) and this ends every session on every device, so the token and
   the board agree. NOT --red-del: that one is the softer delete-a-link ink, deliberately different.
   The hover rule above excludes this deliberately — a destructive row brightening to white would
   read as if it had stopped being destructive. */
/* the account default's switch, borrowing the send form's .sv-toggle so there is one switch look in
   the product. Only the placement is new. */
.acs-dl{ flex:0 0 auto; pointer-events:none; }   /* drawn state only; the row takes the tap */
.acs-row--switch:hover .acs-rnote{ color:#fff; }
.acs-row--switch:focus-visible{ outline:2px solid var(--blue); outline-offset:-2px; border-radius:8px; }
/* the drawn track follows the row's own state, since the row is the control now */
.acs-row--switch[aria-checked="true"] .sv-toggle{ background:var(--blue); justify-content:flex-end; }
.acs-row--switch[aria-checked="true"] .sv-toggle .knob{ box-shadow:0 1px 2px rgba(0,0,0,.4); }
.acs-row .acs-rnote{ font:400 13.5px var(--f, Inter,sans-serif); color:var(--ink-title); }
.acs-ract--danger{ color:var(--red); }
.acs-row--act:hover .acs-ract--danger{ color:#FF6257; }
.acs-rnote{ font:400 11.5px var(--f, Inter,sans-serif); color:var(--txt-muted); }
/* THE BUILD LINE at the foot of the pane (Sid, 2026-09-16): the deployed site version, one quiet line in
   the row notes' own voice, no label, on the columns' own measure. Filled by version.js; hidden until then. */
.acs-ver{ box-sizing:border-box; width:100%; max-width:700px; margin:var(--sp-air) auto 0; padding:0 var(--sp-wide);
  font:400 11.5px/1.3 var(--f, Inter,sans-serif); color:var(--txt-muted); }
.acs-row--pick:focus-visible, .acs-row--act:focus-visible{ outline:2px solid var(--blue); outline-offset:-2px; border-radius:8px; }

/* the account default's menu. .st-menu carries the look; this only anchors it. */
.acs-menu{ position:fixed; }

/* ---- the curated five (2d / 3i) ---------------------------------------------------------------
   Board values: five equal columns, 10px apart, tiles 74 tall on desktop and 52 on phone, radius 10.
   The selected tile takes a 2px --blue ring and an 18px --blue check at top right; every tile carries
   its picture's ruled accent as a 9px dot at bottom left, ringed in black at 1.5 so it reads on a
   light photograph as well as a dark one.
   THE DOT IS THE ONLY PLACE THE STUDIO USES AN ACCENT. It is the swatch's identity, not a control
   colour: the studio keeps --blue and --blue-text for everything that does something. */
.acs-sub{ font:400 12.5px/1.55 var(--f, Inter,sans-serif); color:var(--txt-muted); }
.acs-note{ font:400 11.5px/1.55 var(--f, Inter,sans-serif); color:rgba(255,255,255,.35); }
.acs-walls{ display:grid; grid-template-columns:repeat(5, 1fr); gap:var(--sp-close); }
.acs-wall{ position:relative; height:74px; border-radius:10px; overflow:hidden; border:0; padding:0;
  cursor:pointer; background-color:#232427; background-size:cover; background-position:center;
  box-shadow:0 0 0 0 var(--blue); transition:box-shadow .12s linear; }
.acs-wall:hover{ box-shadow:0 0 0 2px rgba(255,255,255,.28); }
.acs-wall[aria-checked="true"], .acs-wall[aria-checked="true"]:hover{ box-shadow:0 0 0 2px var(--blue); }
.acs-wall:focus-visible{ outline:2px solid var(--blue); outline-offset:3px; }
/* the ruled accent, read from tokens.css so there is exactly one declaration of each of the five */
.acs-wallDot{ position:absolute; left:6px; bottom:6px; width:9px; height:9px; border-radius:50%;
  box-shadow:0 0 0 1.5px rgba(0,0,0,.45); }
.acs-wall[data-wall="turntable"] .acs-wallDot{ background:var(--accent-turntable); }
.acs-wall[data-wall="moog"]      .acs-wallDot{ background:var(--accent-moog); }
.acs-wall[data-wall="ssl"]       .acs-wallDot{ background:var(--accent-ssl); }
.acs-wall[data-wall="blacksand"] .acs-wallDot{ background:var(--accent-blacksand); }
.acs-wall[data-wall="whitesand"] .acs-wallDot{ background:var(--accent-whitesand); }
/* the tick: present on every tile so the row never reflows on a change, shown on the chosen one */
.acs-wallTick{ position:absolute; right:6px; top:6px; width:18px; height:18px; border-radius:50%;
  background:var(--blue); display:none; align-items:center; justify-content:center; }
.acs-wall[aria-checked="true"] .acs-wallTick{ display:flex; }
.acs-wallName{ position:absolute; inset:0; display:none; }

/* ---- custom wallpaper, two slots (2d / 3i) ----------------------------------------------------
   Board values: phone slot 110x190, desktop slot fills the rest at the same height, 14 apart,
   radius 10. Each carries the board's drawn card stand-in, and the desktop slot carries the crop
   rectangle with its grip when the phone picture is standing in for it. */
.acs-labRow{ display:flex; align-items:baseline; gap:var(--sp-tight); }
.acs-labTag{ font:400 11px var(--f, Inter,sans-serif); color:rgba(255,255,255,.3); }
.acs-slots{ display:flex; gap:var(--sp-close); align-items:stretch; }
.acs-slotWrap{ display:flex; flex-direction:column; gap:6px; }
.acs-slotWrap--desk{ flex:1; min-width:0; }
.acs-slot{ position:relative; display:block; width:110px; height:190px; border-radius:10px; overflow:hidden;
  border:1px dashed rgba(255,255,255,.18); padding:0; cursor:pointer; background:#202124;
  background-size:cover; background-position:50% 50%; }
.acs-slotWrap--desk .acs-slot{ width:100%; }
.acs-slot.has-img{ border-style:solid; border-color:transparent; }
.acs-slot:focus-visible{ outline:2px solid var(--blue); outline-offset:3px; }
.acs-slotScrim{ position:absolute; inset:0; display:none;
  background:linear-gradient(180deg,rgba(8,6,6,.3),rgba(8,6,6,.45)); }
.acs-slot.has-img .acs-slotScrim{ display:block; }
.acs-slotEmpty{ position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  font:400 11.5px var(--f, Inter,sans-serif); color:rgba(255,255,255,.42); }
.acs-slot.has-img .acs-slotEmpty{ display:none; }
/* THE BOARD'S DRAWN CARD STAND-IN, not the real .st-card. See the markup for why: the readable
   version is the in-situ preview, which is with Design. */
.acs-slotCard{ position:absolute; left:8px; right:8px; bottom:8px; height:62px; display:none;
  background:rgba(26,27,29,.93); border:1px solid #26282B; border-radius:8px; }
.acs-slotCard--desk{ left:10px; right:auto; bottom:10px; width:96px; height:58px; }
.acs-slot.has-img .acs-slotCard{ display:block; }
.acs-slotCard .b1{ display:block; margin:9px auto 0; width:34px; height:5px; border-radius:3px; background:rgba(255,255,255,.55); }
.acs-slotCard .b2{ display:block; margin:8px auto 0; width:62px; height:4px; border-radius:2px; background:rgba(255,255,255,.25); }
.acs-slotCard .b3{ display:block; margin:5px auto 0; width:44px; height:4px; border-radius:2px; background:rgba(255,255,255,.18); }
.acs-slotCard--desk .b1{ margin:8px auto 0; width:30px; }
.acs-slotCard--desk .b2{ width:52px; }
/* the focus point: a window on the phone picture saying which slice the desktop takes */
.acs-crop{ position:absolute; width:56%; height:68%; border:2px solid var(--blue); border-radius:6px;
  box-shadow:0 0 0 2000px rgba(8,6,6,.35); cursor:grab; touch-action:none; }
.acs-crop:active{ cursor:grabbing; }
.acs-cropGrip{ position:absolute; left:50%; top:-9px; transform:translateX(-50%);
  width:26px; height:5px; border-radius:3px; background:var(--blue); }
.acs-cropHint{ position:absolute; right:8px; top:8px; font:600 10.5px var(--f, Inter,sans-serif); color:rgba(255,255,255,.8); }
.acs-slotLab{ font:400 11.5px var(--f, Inter,sans-serif); color:var(--txt-muted); }
/* a reject is a plain sentence, never a badge or a colour beyond the destructive ink */
.acs-err{ font:400 12px/1.5 var(--f, Inter,sans-serif); color:var(--red-del); }
/* both save rows the same: actions at the right end, 12 apart, the save always last */
.acs-saveRow{ display:flex; justify-content:flex-end; align-items:center; gap:var(--sp-close); }
.acs-save{ height:36px; padding:0 20px; border:0; border-radius:11px; cursor:pointer;
  background:var(--blue); box-shadow:0 8px 20px rgba(228,111,64,.4);
  font:700 13px var(--f, Inter,sans-serif); color:#fff; }
.acs-save:disabled{ opacity:.4; cursor:default; box-shadow:none; }
.acs-save:focus-visible{ outline:2px solid var(--blue); outline-offset:3px; }

/* ---------------------------------------------------------------------------------------------
   PHONE, 3i. 390pt first class, not a squeezed desktop: one column in the board's order, the head
   tightened to the phone's own padding, and every target at 44.
   The studio's phone tier is max-width:480px, which is the one this file already uses. No new
   breakpoint is introduced.
   --------------------------------------------------------------------------------------------- */
@media (max-width:480px){
  .acs-head{ padding:var(--sp-block); padding-top:max(var(--sp-block), env(safe-area-inset-top, 0px)); }
  .acs-back{ font-size:13.5px; }
  /* EVERY TARGET 44pt VIA PADDED HIT AREAS (fd). These two are TEXT, not glyphs, so they were 16px
     tall and drawn correctly while being untappable — caught by measuring the rendered rects rather
     than by looking. The padding grows the hit box and the negative margin gives the height back to
     the layout, so the type lands exactly where the board puts it and nothing around it moves.
     Bound to the width tier rather than pointer:coarse deliberately: a desktop window dragged to 390
     has the same problem, and a larger target there costs nothing. */
  .acs-back, .acs-link{ min-height:44px; display:inline-flex; align-items:center; margin-top:-14px; margin-bottom:-14px; }
  .acs-back{ padding-right:12px; }
  .acs-link{ padding-left:12px; }
  /* the phone keeps the one column the wider screens now start from, on its own gutter. The column
     STRETCHES (the flex default): a shrink-wrapped column once stopped the rows and their hairlines
     less than half way across, visible only in a screenshot, so do not set align-items here. */
  .acs-cols{ max-width:none; margin:var(--sp-tight) 0 0; gap:var(--sp-air); padding:0 var(--sp-block); }
  .acs-col{ gap:var(--sp-air); }
  .acs-getapps{ height:44px; }
  /* the account row loses the desktop's roomier avatar, matching 3i's 48 */
  .acs-ava{ width:48px; height:48px; font-size:18px; }
  .acs-nm{ font-size:16px; }
  .acs-em{ font-size:12.5px; }
  .acs-row{ height:44px; }
  /* 44pt ON TOUCH (fd). The board draws this button at 36 on the desktop frame and does not draw it
     at all on 3i, so the height here is a TOUCH adaptation rather than a taste one, which is exactly
     the kind (fd) authorises. Caught by Block 1's own target-size assertion running over a Block 3
     element, which is the point of leaving that check in place. */
  .acs-save{ min-height:44px; }
  /* 3i: the two slots keep their shapes but step down to the phone's own sizes */
  .acs-slot{ width:88px; height:150px; }
  .acs-slotCard{ left:6px; right:6px; bottom:6px; height:46px; }
  .acs-slotCard--desk{ left:8px; right:auto; width:76px; height:44px; }
  .acs-slotCard .b1{ margin:7px auto 0; width:26px; height:4px; }
  .acs-slotCard .b2{ margin:6px auto 0; width:46px; height:3px; }
  .acs-slotCard .b3{ display:none; }
  .acs-cropHint{ font-size:10px; right:6px; top:6px; }
  /* 3i: tiles 52 tall, 8 apart, with the dot and tick stepped down to match */
  .acs-walls{ gap:var(--sp-tight); }
  .acs-wall{ height:52px; }
  .acs-wallDot{ left:5px; bottom:5px; width:8px; height:8px; }
  .acs-wallTick{ right:5px; top:5px; width:16px; height:16px; }
  /* 3i draws "log out everywhere" alone; the caption is a desktop affordance and at 390 it would
     either wrap the row to two lines or push the label off its rail. */
  .acs-rnote{ display:none; }
}


/* ---- THE LINK PREVIEW PICTURE SLOT --------------------------------------------------------------
   A wide slot, because the thing it holds is wide: the sender is choosing a picture that lands as a
   roughly 2:1 strip in a messages thread, and a tall or square well would misrepresent the crop they
   are actually choosing. It reuses .acs-slot's ground, border and empty-state so it cannot drift
   from the wallpaper slots beside it; only the shape and the label row are its own. */
/* SMALLER THAN THE DESKTOP WALLPAPER SLOT, ON PURPOSE. At full width this was the largest thing
   in the panel, so reading down it looked like the main event when it is the least important of the
   three pictures, and a big well implies a big file when the limit here is 600KB against the
   wallpaper's 10MB. Measured live: phone 110, desktop 516. 344 is about two thirds of the desktop
   slot, so the three read as a set stepping down. A max-width rather than a percentage so it is not
   coupled to the wallpaper row's own widths, with width:100% under it so a narrower panel still
   shrinks it rather than overflowing. */
.acs-slotWrap--card{ width:100%; max-width:344px; }
/* height:auto is LOAD BEARING. .acs-slot sets a fixed 190px height for the two portrait wallpaper
   slots, and a fixed height beats aspect-ratio, so without this the card slot rendered 640x190 -
   a 3.37:1 letterbox. That misrepresents the very thing the sender is choosing: they would be
   judging a crop far wider than the roughly 2:1 strip the messaging app actually shows. Measured
   in both engines before and after. */
.acs-slot--card{ width:100%; height:auto; aspect-ratio:1200/630;
  background-size:cover; background-position:center; }
.acs-slot--card.has-img .acs-slotScrim{ opacity:0; }
.acs-clear{ background:none; border:0; color:var(--blue-text); font-size:var(--t-caption);
  font-family:inherit; letter-spacing:var(--t-tracking); cursor:pointer; padding:0; }
.acs-clear:hover{ text-decoration:underline; text-underline-offset:3px; }

/* ---- THE SENDER'S WALLPAPER BEHIND THE STUDIO (Sid, 2026-09-16) ------------------------------------
   Reverses (fj) "the studio stays on Acetate's own look" and (ew) "the studio is not a scrapbook", for
   the wallpaper only. The picture itself is stamped by the server (studio.js repaintWall).
   Held back until the account's own picture is known, so a sender on their own picture never sees the
   default first: the .sb-wall gradient shows meanwhile. */
.wall-pending .sb-wall picture{ visibility:hidden; }
/* THE PLAYERS' DIMMING, AS DECLARED COPIES. The studio cannot load player.css (it locks html and body
   overflow and redefines .overlay, .acmodal and .acbtn), so these two rules are copied VERBATIM from it:
   player.css `.wall-whitesand .sb-scrim` and its `@media (min-width:481px) .sb-scrim` tier. At 480 and
   below both pages use the shared sendbox.css lift, and a custom picture's dimming arrives in the
   substitution's own <style>. web/deploy/verify-studio-wallpaper.cjs renders the studio beside the same
   account's player at 390, 600 and 1440 in WebKit and fails if their dimming differs, or if their pixels
   differ beyond the one half-pixel allowance Sid accepted, which it names. Change these in both files or in
   neither. (Note for the next change the players are allowed: player.css has no pointer back to here.) */
.wall-whitesand .sb-scrim{ background:linear-gradient(180deg,rgba(8,6,6,.72),rgba(8,6,6,.66) 45%,rgba(8,6,6,.8)); }
@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)); }
}

/* ---- THE USERNAME ROW AND EDITOR (Stage E) ---------------------------------------------------------
   THE ROW is an ordinary pick row. Its value ellipses: "@" and 30 wide letters are wider than the 354px phone
   column and the 360px side column, so the label keeps its width, the value shrinks to what is left and cuts
   with an ellipsis. The full name stays in the button's text, so its accessible name is never cut. */
#acsHandleRow .acs-rlab{ flex:0 0 auto; }
#acsHandleRow .acs-rval{ flex:0 1 auto; min-width:0; overflow:hidden; }
#acsHandleRow .acs-rval svg{ flex:0 0 auto; }
.acs-hval{ min-width:0; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
/* THE EDITOR keeps the shared .acmodal look. Its words are already lower case in table U, and a refused
   character has to show exactly as typed (an "E" with its accent, not a lower-cased one), so the dialog's
   lowercasing is off here; the field keeps what was typed and the preview line shows the lower case. */
#handleModal .acmodal, #handleConfirm .acmodal{ text-transform:none; }
.acs-hm .sub{ margin-top:8px; overflow-wrap:anywhere; }
.acs-hm .acs-hmTaken{ color:var(--ink); }
.acs-hmLab{ display:block; margin-top:14px; font:600 12.5px var(--f); color:var(--ink-2); }
.acs-hmField{ position:relative; margin-top:6px; }
.acmodal .acs-hmIn{ margin-top:0; padding-left:33px; }   /* the drawn "@" is about 16px wide at 16px type, from 14px in */
.acs-hmAt{ position:absolute; left:14px; top:50%; transform:translateY(-50%); font:500 14px var(--f); color:var(--ink-2); pointer-events:none; }
.acs-hmPrev{ margin-top:6px; font:500 12.5px var(--f); color:var(--ink-2); overflow-wrap:anywhere; }
/* the error line keeps its place in the page while empty (a live region that is display:none is not read out
   when it fills); only its spacing goes */
.acs-hmErr{ margin-top:6px; font:400 12.5px/1.45 var(--f); color:var(--red-del); overflow-wrap:anywhere; }
.acs-hmErr:empty{ margin-top:0; }
.acs-hmHint, .acs-hmNote{ margin-top:6px; font:400 12px/1.45 var(--f); color:var(--ink-2); }
.acs-hmState{ margin-top:8px; font:500 12.5px/1.45 var(--f); color:var(--ink); }
/* the state line is a live region too, so it stays drawn while empty for the same reason; only its spacing goes */
.acs-hmState:empty{ margin-top:0; }
.acs-hmHeld{ list-style:none; margin:6px 0 0; padding:0; font:400 12px/1.45 var(--f); color:var(--ink-2); overflow-wrap:anywhere; }
.acs-hmHeld:empty{ display:none; }
.acs-hmRemove{ margin-top:14px; }
/* a control that cannot act right now says why (aria-describedby names the line) rather than going dead */
.acs-hm .acbtn[aria-disabled="true"], .acs-hm .st-delLink[aria-disabled="true"]{ opacity:.45; cursor:default; }
.acs-hcText{ color:var(--ink); }
.acmodal.confirm .acs-hcText{ margin-top:0; }
/* 16px under touch, so iPhone does not zoom the page when the field takes focus (the shared rule already covers
   .acmodal .acinput; this keeps the drawn "@" the same size as the letters beside it) */
@media (pointer:coarse){
  #handleModal .acs-hmIn, #handleModal .acs-hmAt{ font-size:16px; }
  /* A 44PX TAP AREA for the editor's two small controls under touch (repair round 2; measured 109x15 and 28x28 at 390
     with touch). Remove is a text link: it takes the height itself, its text centred, and gives back the gap above
     it, so its words sit where they did. Close keeps its 28px circle and takes a 44px square around it, the way the
     row buttons do (.sv-rplay::after and the rest). */
  #handleModal .acs-hmRemove{ min-height:44px; min-width:44px; margin-top:0; }
  #hmClose::after{ content:''; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:44px; height:44px; }
}
/* ON A PHONE THE EDITOR SITS AT THE TOP, under the safe area: centred, the keyboard would cover its error line
   and its save. The overlay scrolls if a long list of held names makes it taller than what is left. */
@media (max-width:480px){
  #handleModal{ align-items:flex-start; padding-top:max(16px, env(safe-area-inset-top, 0px)); overflow-y:auto; }
}
