/* =====================================================================
   The Leftovers — Practice Suite
   Design system: fresh, dark, high-contrast, mobile-first, big touch.
   ===================================================================== */
:root{
  --bg:#17140f;            /* warm near-black */
  --surface:#211c16;
  --surface-2:#2b241b;
  --border:#3c3327;
  --text:#f7f2ea;          /* ~15:1 on --bg */
  --muted:#c3b6a3;         /* ~7:1 on --bg  */
  --accent:#e8903f;        /* warm amber    */
  --accent-press:#cf7726;
  --accent-ink:#1a1206;    /* dark text on accent */
  --good:#7ac36a;
  --danger:#e5675f;
  --chip:#332b20;
  --focus:#ffd591;
  /* v172 — the six singer swatches (see UI.voxHtml). Chosen against THIS
     background: every one ≥6.8:1 on the row surface, and none of them is a
     colour that already means something here — amber is Next/armed, teal is
     the player, green is good, red is danger. The reading themes below
     re-point all six (rule 8). */
  --vox-1:#f59ab8;   /* rose   */
  --vox-2:#c3b0f7;   /* violet */
  --vox-3:#86c9f2;   /* sky    */
  --vox-4:#cfd27a;   /* olive  */
  --vox-5:#8fe0c9;   /* mint   */
  --vox-6:#eaa0ea;   /* orchid */
  --radius:14px;
  --tap:52px;              /* min touch target */
  --maxw:900px;
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%; touch-action:manipulation}
body{
  margin:0;
  font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  font-size:18px; line-height:1.5;
  background:var(--bg); color:var(--text);
  -webkit-font-smoothing:antialiased;
}
a{color:var(--accent)}
h1,h2,h3{font-weight:700; letter-spacing:.2px; margin:0}

/* Focus visibility for keyboard users */
:focus-visible{outline:3px solid var(--focus); outline-offset:2px; border-radius:6px}

/* ---------- layout ---------- */
.wrap{max-width:var(--maxw); margin:0 auto; padding:16px}
.topbar{
  position:sticky; top:0; z-index:20;
  background:linear-gradient(var(--bg),var(--bg) 78%,rgba(23,20,15,0));
  /* env(safe-area-inset-top) lifts the nav clear of the status bar when the app
     runs full-screen from the Home Screen (viewport-fit=cover) — otherwise the
     links sit jammed against the very top edge and are hard to tap. Falls back to
     just the 14px in a normal browser tab. */
  padding:calc(env(safe-area-inset-top,0px) + 14px) 16px 10px;
}
.topbar-inner{max-width:var(--maxw); margin:0 auto; display:flex; align-items:center; gap:12px}
.brand{display:flex; align-items:center; gap:10px; min-width:0}
.brand img{height:42px; width:42px; display:block; border-radius:50%}
.brand .name{font-size:1.15rem; font-weight:800; white-space:nowrap}
.brand .name .accent{color:var(--accent)}
.spacer{flex:1}
/* Top-bar controls never wrap their own label (keeps "Sign out" on one line when
   the row is tight) — the brand name is the only thing allowed to give way. */
.topbar-inner .btn{white-space:nowrap}
.whoami{color:var(--muted); font-size:.95rem; display:flex; align-items:center; gap:8px}
.whoami .dot{width:9px;height:9px;border-radius:50%;background:var(--good)}

/* ---------- buttons ---------- */
.btn{
  appearance:none; border:1px solid var(--border); background:var(--surface-2);
  color:var(--text); font-size:1rem; font-weight:700;
  min-height:var(--tap); padding:0 18px; border-radius:var(--radius);
  cursor:pointer; display:inline-flex; align-items:center; justify-content:center; gap:8px;
  transition:transform .06s ease, background .15s ease, border-color .15s ease;
}
.btn:active{transform:scale(.97)}
/* Phase 40: a .btn's display:inline-flex overrides the UA [hidden] rule, so a
   `hidden` .btn would still show as an empty box. Restore the hide (used by the
   "↩ Resume" shortcut, which is only present when there's a spot to return to). */
.btn[hidden]{display:none !important}
.btn-primary{background:var(--accent); color:var(--accent-ink); border-color:var(--accent)}
.btn-primary:hover{background:var(--accent-press)}
.btn-ghost{background:transparent}
.btn-danger{background:transparent; color:var(--danger); border-color:var(--danger)}
.btn-block{width:100%}
.btn-sm{min-height:42px; padding:0 14px; font-size:.92rem}

/* ---------- inputs ---------- */
label{display:block; font-weight:700; margin:0 0 6px; font-size:.95rem}
input[type=text],input[type=password],input[type=url],input[type=number],input[type=date],select,textarea{
  width:100%; min-height:var(--tap); font-size:1.05rem;
  background:var(--surface); color:var(--text);
  border:1px solid var(--border); border-radius:var(--radius);
  padding:10px 14px;
}
textarea{min-height:90px; line-height:1.4}
input[type=date]{color-scheme:dark}   /* v163: the picker chrome matches the theme */
input::placeholder{color:#8f8574}

/* ---------- search + filters ---------- */
.controls{display:flex; flex-direction:column; gap:12px; margin:6px 0 14px}
.search{position:relative}
.search input{padding-left:44px}
.search svg{position:absolute; left:14px; top:50%; transform:translateY(-50%); opacity:.6}
.filters{display:flex; flex-wrap:wrap; gap:8px}
.chip{
  appearance:none; border:1px solid var(--border); background:var(--chip); color:var(--text);
  min-height:44px; padding:0 16px; border-radius:999px; font-size:.98rem; font-weight:700; cursor:pointer;
}
.chip[aria-pressed=true]{background:var(--accent); color:var(--accent-ink); border-color:var(--accent)}
.count{color:var(--muted); font-size:.9rem; padding:2px 2px 0}

/* ---------- song cards ---------- */
.list{display:flex; flex-direction:column; gap:10px; padding-bottom:90px}
.song{
  background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
  padding:14px 14px 12px; border-left:5px solid var(--accent);
}
.song.no-vox{border-left-color:var(--border)}
.song .row1{display:flex; align-items:baseline; gap:10px}
.song .title{font-size:1.12rem; font-weight:800; line-height:1.25}
.song .artist{color:var(--muted); font-size:.98rem; margin-top:2px}
.badges{display:flex; flex-wrap:wrap; gap:6px; margin-top:10px}
.badge{
  font-size:.82rem; font-weight:700; color:var(--text);
  background:var(--surface-2); border:1px solid var(--border);
  border-radius:8px; padding:3px 9px; display:inline-flex; align-items:center; gap:5px;
}
.badge.key{color:var(--accent)}
.badge.vibe{background:#2a2536}
.badge.keys::before{content:"🎹"}
.badge.solo::before{content:"🎸"}
.actions{display:flex; flex-wrap:wrap; gap:8px; margin-top:12px}
.actions a.linkbtn,.actions button.linkbtn{
  min-height:44px; padding:0 14px; border-radius:10px; font-size:.9rem; font-weight:700;
  display:inline-flex; align-items:center; gap:6px; text-decoration:none;
  background:var(--surface-2); border:1px solid var(--border); color:var(--text); cursor:pointer;
}
.actions .practice{background:var(--accent); color:var(--accent-ink); border-color:var(--accent)}
/* The right-hand cluster: Revisit then Edit, pushed to the far right with Edit
   last. The auto-margin rides on Revisit (the first of the pair) so both go right. */
.actions .revisit{margin-left:auto}

/* Phase 27: "bring it back" revisit toggle + its tally pill. Teal when you've
   flagged it; the count shows how many bandmates want it back. */
.actions .revisit.on{ background:var(--play); color:var(--play-ink); border-color:var(--play); }
.revcount{
  background:var(--accent); color:var(--accent-ink); font-size:.7rem; font-weight:800;
  border-radius:999px; min-width:17px; height:17px; padding:0 5px;
  display:inline-flex; align-items:center; justify-content:center;
}

/* ---------- floating add ---------- */
.fab{
  position:fixed; right:18px; bottom:18px; z-index:30;
  min-height:60px; padding:0 22px; border-radius:999px;
  background:var(--accent); color:var(--accent-ink); border:none; font-weight:800; font-size:1.05rem;
  box-shadow:0 8px 22px rgba(0,0,0,.5); cursor:pointer; display:inline-flex; align-items:center; gap:8px;
}

/* ---------- modal ---------- */
.modal-back{
  position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:40;
  display:none; align-items:flex-end; justify-content:center; padding:0;
}
.modal-back.open{display:flex}
.modal{
  background:var(--surface); width:100%; max-width:560px; max-height:92vh; overflow:auto;
  border-radius:20px 20px 0 0; border:1px solid var(--border); padding:18px 16px 24px;
}
.modal h2{font-size:1.2rem; margin-bottom:14px}
.field{margin-bottom:14px}
.field-row{display:flex; gap:12px}
.field-row .field{flex:1}
.voxpick{display:flex; flex-wrap:wrap; gap:8px}
.voxpick .chip{min-height:46px}
.checkline{display:flex; align-items:center; gap:10px; margin:6px 0}
.checkline input{width:24px; height:24px; min-height:0}
.modal-actions{display:flex; gap:10px; margin-top:8px}
.modal-actions .btn{flex:1}
.hint{color:var(--muted); font-size:.85rem; margin-top:4px}

/* ---------- login ---------- */
.login-wrap{max-width:460px; margin:0 auto; padding:28px 18px}
.login-logo{display:flex; flex-direction:column; align-items:center; gap:12px; margin:18px 0 26px; text-align:center}
.login-logo img{height:70px}
.login-logo h1{font-size:1.5rem}
.login-logo .sub{color:var(--muted); font-size:1rem}
.namegrid{display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:8px 0 4px}
.namebtn{
  min-height:58px; border:1px solid var(--border); background:var(--surface); color:var(--text);
  border-radius:var(--radius); font-size:1.1rem; font-weight:700; cursor:pointer;
}
.namebtn[aria-pressed=true]{background:var(--accent); color:var(--accent-ink); border-color:var(--accent)}
.error{background:rgba(229,103,95,.15); border:1px solid var(--danger); color:#ffd7d4;
  padding:12px 14px; border-radius:var(--radius); margin:10px 0; font-weight:600}
.toast{
  position:fixed; left:50%; bottom:96px; transform:translateX(-50%);
  background:var(--surface-2); color:var(--text); border:1px solid var(--border);
  padding:12px 18px; border-radius:999px; z-index:50; font-weight:700; opacity:0;
  transition:opacity .2s ease; pointer-events:none; box-shadow:0 6px 18px rgba(0,0,0,.4);
}
.toast.show{opacity:1}
.empty{color:var(--muted); text-align:center; padding:40px 10px}

@media (min-width:640px){
  .modal-back{align-items:center}
  .modal{border-radius:20px}
}

/* =====================================================================
   Phase 2 — practice player + song notes
   ===================================================================== */

/* ---------- notes button on repertoire cards ---------- */
.linkbtn.notes{position:relative}
.notecount{
  background:var(--accent); color:var(--accent-ink); font-size:.72rem; font-weight:800;
  border-radius:999px; min-width:18px; height:18px; padding:0 5px;
  display:inline-flex; align-items:center; justify-content:center;
}
.privdot{width:8px; height:8px; border-radius:50%; background:var(--good); display:inline-block}

/* ---------- player page ---------- */
.player-wrap{padding-bottom:60px}
.player-head{margin:4px 0 16px}
.player-head h1{font-size:1.5rem; line-height:1.2}
.player-artist{color:var(--muted); font-size:1rem; margin-top:4px}

.mixer{margin-bottom:26px}
.master-controls{
  background:var(--surface-2); border:1px solid var(--border); border-radius:var(--radius);
  padding:16px; margin-bottom:16px;
}
.transport{display:flex; gap:10px; margin-bottom:6px}
.transport .btn{flex:1}
.player-loading{color:var(--muted); font-size:.9rem; margin:8px 0}
.player-loading.ready{color:var(--good); font-weight:700}
.player-loading.err{color:var(--danger); font-weight:700}
.master-seek{width:100%; height:26px; accent-color:var(--accent); cursor:pointer; margin-top:6px}
.timeline{display:flex; justify-content:space-between; color:var(--muted); font-size:.85rem; margin-top:2px}
.mixer-hint{color:var(--muted); font-size:.88rem; margin:12px 2px 0}

.tracks{display:flex; flex-direction:column; gap:10px}
.track{
  display:flex; align-items:center; gap:12px;
  background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
  border-left:5px solid var(--accent); padding:12px 14px;
}
.track.silenced{border-left-color:var(--border); opacity:.55}
.track.soloed{border-left-color:var(--focus); opacity:1}
.track-name{flex:0 0 92px; font-weight:800; font-size:1rem}
.track-ctrls{display:flex; align-items:center; gap:10px; flex:1; min-width:0}
.tbtn{
  appearance:none; border:1px solid var(--border); background:var(--surface-2); color:var(--text);
  min-height:44px; padding:0 14px; border-radius:10px; font-size:.9rem; font-weight:700; cursor:pointer;
}
.tbtn.mute[aria-pressed=true]{background:var(--danger); color:#1a0605; border-color:var(--danger)}
.tbtn.solo[aria-pressed=true]{background:var(--focus); color:var(--accent-ink); border-color:var(--focus)}
.tvol{flex:1; min-width:60px; height:26px; accent-color:var(--accent); cursor:pointer}

/* ---------- notes widget (player page + modal) ---------- */
.notes-block{border-top:1px solid var(--border); padding-top:18px}
.notes-h{font-size:1.2rem; margin-bottom:12px}
.notes-song{color:var(--muted); font-weight:700; margin:-6px 0 14px}
.modal-head{display:flex; align-items:center; justify-content:space-between; margin-bottom:14px}
.modal-head h2{margin:0}

.notes-widget{display:flex; flex-direction:column; gap:22px}
.notes-sub{font-size:1rem; margin-bottom:8px}
.note-list-wrap{margin-bottom:12px}
.note-list{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:8px}
.note-item{background:var(--surface-2); border:1px solid var(--border); border-radius:12px; padding:10px 12px}
.note-meta{display:flex; align-items:baseline; gap:8px; margin-bottom:3px}
.note-author{font-weight:800; font-size:.9rem; color:var(--accent)}
.note-date{color:var(--muted); font-size:.78rem}
.note-body{font-size:.98rem; line-height:1.4; white-space:pre-wrap; word-break:break-word}
.note-empty{color:var(--muted); font-size:.92rem; padding:2px 0}
.note-label{display:block; font-weight:700; font-size:.9rem; margin:0 0 6px}
.note-input{width:100%; min-height:76px; line-height:1.4}
.note-save-row{display:flex; align-items:center; gap:12px; margin-top:8px}
.note-status{color:var(--good); font-size:.85rem; font-weight:700}

@media (min-width:640px){
  .notes-widget{flex-direction:row}
  .notes-widget .notes-col{flex:1; min-width:0}
}
/* Phase 32: a single-lane popover (Practice Log on its own, or personal notes on
   their own) stays one full-width column even on desktop. */
.notes-widget.solo{flex-direction:column}

/* =====================================================================
   Phase 3 — practice planner
   ===================================================================== */

/* ---------- top nav (Repertoire / Practice) ---------- */
/* On phones the nav drops to its own full-width row so it never crowds the
   brand / name / sign-out; on wider screens it sits inline in the bar. */
.topbar-inner{flex-wrap:wrap; row-gap:10px}
.brand{flex-shrink:0}
.topnav{display:flex; gap:6px; order:5; flex-basis:100%}
.topnav a{
  color:var(--muted); text-decoration:none; font-weight:700; font-size:.95rem;
  padding:8px 14px; border-radius:10px; line-height:1; min-height:40px;
  display:inline-flex; align-items:center; flex:1; justify-content:center;
}
.topnav a:hover{color:var(--text); background:var(--surface-2)}
.topnav a.active{color:var(--accent-ink); background:var(--accent)}
@media (min-width:640px){
  /* Once the nav is inline (tablet/desktop), keep the whole bar on ONE row — no
     wrapping "Sign out" onto a second line. If a narrow tablet ever runs tight,
     the brand name gives way first (ellipsis) rather than the row breaking. */
  .topbar-inner{flex-wrap:nowrap}
  .brand{flex-shrink:1; min-width:0}
  .brand .name{overflow:hidden; text-overflow:ellipsis}
  .topnav{order:0; flex-basis:auto; margin-left:4px}
  .topnav a{flex:none; justify-content:flex-start}
}

/* ---------- utilities ---------- */
.sr-only{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0;
}
.muted-inline{color:var(--muted); font-weight:400}

/* ---------- planner top: picker + practice head ---------- */
.planner-top{margin-bottom:18px}
.planner-picker{display:flex; gap:10px; align-items:center}
.planner-picker select{
  flex:1; min-width:0; min-height:var(--tap);
  background:var(--surface-2); color:var(--text);
  border:1px solid var(--border); border-radius:var(--radius);
  padding:0 14px; font-size:1rem; font-weight:700; font-family:inherit;
}
.planner-picker .btn-sm{white-space:nowrap}

.practice-head{
  display:flex; align-items:center; flex-wrap:wrap; gap:10px 14px; margin-top:16px;
}
.practice-when{display:flex; align-items:baseline; gap:10px; min-width:0}
.practice-date{font-size:1.5rem; font-weight:800; letter-spacing:.2px}
.practice-rel{color:var(--accent); font-weight:700; font-size:.95rem}
.practice-label{color:var(--muted); font-size:1rem}
.practice-head #editPracticeBtn{margin-left:auto}

/* ---------- sections ---------- */
.planner-section{
  background:var(--surface); border:1px solid var(--border);
  border-radius:var(--radius); padding:16px; margin-bottom:16px;
}
.section-head{display:flex; align-items:baseline; gap:12px; margin-bottom:14px}
.section-head h2{font-size:1.15rem}
.section-sub{color:var(--muted); font-size:.9rem; font-weight:700; margin-left:auto}

/* Two-row variant (practice header): title + My-notes toggle share the top row,
   the running-time / progress readout drops to its own line so nothing gets
   squeezed on a phone. */
.section-head-2row{ flex-direction:column; align-items:stretch; gap:6px; }
.section-head-2row .section-head-top{
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  flex-wrap:wrap;   /* Batch 135: three buttons left + Add right; let it break */
}
.section-head-2row .section-sub{ margin-left:0; }
/* Batch 135 — ＋ Add a song takes the right edge of the practice header row, in
   contrast to the Rehearsal / Print cluster on the left. margin-left:auto rather
   than relying on justify-content, so it still hugs the right when the row wraps
   and the two clusters end up on separate lines. */
.gig-actions-add{ margin-left:auto; }

/* ---------- attendance: compact colour-coded chips ---------- */
.att-strip{display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin-top:14px}
.att-chip{
  appearance:none; display:inline-flex; align-items:center; gap:6px; cursor:pointer;
  min-height:38px; padding:6px 13px; border-radius:999px; font-weight:700; font-size:.92rem;
  border:1px solid var(--border); background:var(--surface-2); color:var(--muted);
  text-decoration:underline dotted; text-underline-offset:3px; text-decoration-thickness:1px;
}
.att-chip .att-mark{font-size:.82rem; opacity:.85}
.att-chip.att-none{color:var(--text); text-decoration-color:var(--muted)}
.att-chip.att-yes  {background:var(--good);   color:#10240c; border-color:var(--good);   text-decoration-color:rgba(0,0,0,.4)}
.att-chip.att-maybe{background:var(--accent); color:var(--accent-ink); border-color:var(--accent); text-decoration-color:rgba(0,0,0,.4)}
.att-chip.att-no   {background:var(--danger); color:#2a0b09; border-color:var(--danger); text-decoration-color:rgba(0,0,0,.4)}
.att-summary{color:var(--muted); font-size:.85rem; margin-left:4px}

/* tap a chip -> pick In / Maybe / Out */
.att-pop{
  position:fixed; z-index:60; min-width:150px; display:none; flex-direction:column; gap:2px;
  background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:6px;
  box-shadow:0 12px 30px rgba(0,0,0,.55);
}
.att-pop.open{display:flex}
.att-pop-name{font-size:.78rem; color:var(--muted); font-weight:700; padding:4px 8px 2px}
.att-pop button{
  appearance:none; display:flex; align-items:center; gap:9px; text-align:left; width:100%;
  min-height:44px; padding:0 10px; border-radius:8px; border:1px solid transparent;
  background:transparent; color:var(--text); font-weight:700; font-size:.95rem; cursor:pointer;
}
.att-pop button:hover{background:var(--surface-2)}
.att-pop .dotc{width:12px; height:12px; border-radius:50%; flex:none; border:1px solid rgba(0,0,0,.2)}
.att-pop .dotc.yes{background:var(--good)}
.att-pop .dotc.maybe{background:var(--accent)}
.att-pop .dotc.no{background:var(--danger)}

/* ---------- song queue ---------- */
.queue{display:flex; flex-direction:column; gap:10px; margin-bottom:14px}
.qitem{
  display:flex; align-items:flex-start; gap:10px;
  background:var(--surface-2); border:1px solid var(--border);
  border-radius:12px; padding:10px 12px;
}
.qitem.done{opacity:.55}
.qitem.done .qtitle{text-decoration:line-through}

/* ---------------------------------------------------------------------
   v157 — practice style: per-singer blocks.

   A block is a VIEW of the one queue (see practiceOrder in planner.js),
   so there is deliberately no card, no border box, no set-like chrome
   around it: a heavy container would read as structure and this isn't
   structure. A rule line and a name, the way a section heading works.
   --------------------------------------------------------------------- */
.queue-grouped{gap:0}
.qblock{display:flex; flex-direction:column; gap:10px}
.qblock + .qblock{margin-top:20px}
.qblock-head{
  display:flex; align-items:center; gap:10px;
  padding:0 2px 8px;
  border-bottom:2px solid var(--border);
}
.qblock-main{display:flex; align-items:baseline; flex-wrap:wrap; gap:4px 10px; min-width:0; flex:1}
.qblock-name{
  font-weight:800; font-size:1.05rem; letter-spacing:.02em; color:var(--accent);
}
.qblock-sub{color:var(--muted); font-size:.82rem}
/* An out singer's whole block is the warning, so the name carries it and the
   rows underneath keep their own per-song ⚠ for the duet case. */
.qblock-out .qblock-name{color:var(--danger)}
.qblock-out .qblock-name .vox{color:var(--danger)}   /* v172: the warning still wins over the singer's colour */
/* The no-singer block is a loose end, not somebody's turn — in the accent it
   read as a third band member. Muted, and it sits last. */
.qblock-none .qblock-name{color:var(--muted); font-weight:700}
.qblock-move{display:flex; gap:4px; flex:none}
.qblock-move .qbtn{width:34px; min-height:34px; font-size:.8rem}

@media (max-width:520px){
  .qblock + .qblock{margin-top:16px}
  .qblock-name{font-size:1rem}
}
/* `.qmove` / `.smove` were the ▲▼ reorder stacks. The buttons were deleted
   batches ago and the rules sat here wearing nothing — swept in v174 along
   with the @media rules that still deferred to them. → ipad_reorder_deadend */
.qbtn{
  appearance:none; width:38px; min-height:34px; border-radius:8px;
  border:1px solid var(--border); background:var(--surface); color:var(--text);
  font-size:.9rem; cursor:pointer; display:inline-flex; align-items:center; justify-content:center;
}
.qbtn:disabled{opacity:.3; cursor:default}
.qbtn.remove{min-height:44px; width:44px; color:var(--danger); font-size:1.1rem}

.qcheck{position:relative; display:inline-flex; align-items:center; cursor:pointer; padding-top:2px}
.qcheck input{position:absolute; opacity:0; width:44px; height:44px; margin:0; cursor:pointer}
.qcheckbox{
  width:26px; height:26px; border-radius:8px;
  border:2px solid var(--border); background:var(--surface); display:inline-block;
}
.qcheck input:checked + .qcheckbox{
  background:var(--good); border-color:var(--good);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%2310240c' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
  background-repeat:no-repeat; background-position:center;
}
.qcheck input:focus-visible + .qcheckbox{outline:3px solid var(--focus); outline-offset:2px}

/* `.qright` / `.sright` held the row's ↩ and ✕. v174 moved both into the row
   menu (three controls per row is what blocked the compact view), so these and
   the two rules that hid them are swept — rule 19's broom, and rule 22's
   second half: sweep whatever pointed at the thing you removed. */
.qmain{flex:1; min-width:0}
/* v171 — TWO TARGETS. `.qhead` is a flex row: the launch button (title + meta,
   the big target) and the caret button (the expand, its own 44px square).
   Tap the title once → the row arms in the accent; again → Rehearsal Mode on
   that song. The pattern and the colours are the Stage "?" button's, the
   account swap's and the note line's — nothing new to learn. */
.qhead{display:flex; align-items:flex-start; gap:4px; min-width:0}
.qlaunch{
  appearance:none; background:none; border:0; padding:4px 8px 4px 6px; margin:0 0 0 -6px;
  font:inherit; color:inherit; text-align:left; cursor:pointer; flex:0 1 auto; min-width:0;
  border-radius:10px; min-height:44px; display:block;
  transition:background-color .12s ease, color .12s ease;
}
/* flex:0 1 auto, not 1 1: the button hugs its words so the caret sits right
   after the title, where batch 135 put it — not marooned at the head's edge. */
.qlaunch:focus-visible{outline:3px solid var(--focus); outline-offset:2px}
.qcaret-btn{
  appearance:none; background:none; border:0; padding:0; margin:0;
  flex:0 0 auto; width:44px; min-height:44px; cursor:pointer;
  display:inline-flex; align-items:center; justify-content:center;
  border-radius:10px; color:var(--muted);
}
.qcaret-btn:hover{background:var(--surface)}
.qcaret-btn:focus-visible{outline:3px solid var(--focus); outline-offset:2px}
/* Armed: the accent fill says "you're one tap away" and the title grows the
   words, in the same voice the account swap uses ("— tap again to switch"). */
.qitem.armed .qlaunch{background:var(--accent); color:var(--accent-ink)}
/* While armed (a few seconds) the meta line steps aside for the instruction —
   one clean line, no orange-on-orange chips, and the row doesn't grow. */
.qitem.armed .qsub{display:none}
.qitem.armed .qtitle::after{content:"tap again to rehearse ›"; flex:1 0 100%; font-weight:700; font-size:.8em; opacity:.85; letter-spacing:.2px}
.qtitle{font-weight:800; font-size:1.02rem; word-break:break-word; display:flex; align-items:center; gap:6px; flex-wrap:wrap}
/* Batch 135: the caret is ▸ now, not › — the universal "more under here" triangle,
   matching the repertoire card's toggle. It's the ONLY cue that a row expands, so
   it's a shade brighter than the old chevron and it doesn't shrink on a phone.
   flex:none stops it collapsing when a long title eats the row. */
.qcaret{color:var(--muted); font-size:.95em; flex:none; transition:transform .12s ease; display:inline-block}
.qitem.open .qcaret{transform:rotate(90deg)}
.qsub{display:block; color:var(--muted); font-size:.88rem; margin-top:3px; word-break:break-word; line-height:1.7}
.qsub .singer-out{color:var(--danger); font-weight:800}
/* v137: the key chip now LEADS this line instead of owning a `.qflags` row of its
   own. `.badge` is inline-flex, so it needs vertical-align to sit on the text
   baseline rather than hanging low — this fixes the ⚠ singer-out badge that already
   lived here too. The bumped line-height stops the chip's padding cramping the row
   when the line wraps; a tighter value clips the borders on the second line. */
.qsub .badge{vertical-align:-1px}
.qsub .qkey{margin-right:1px}
.qdetails{margin-top:10px; display:none}
.qitem.open .qdetails{display:block}
.qdetails .badges{margin-top:0; margin-bottom:10px}
/* Batch 135 — the expand now leads with your personal note and the action cluster,
   so kill the top margin on whatever lands first or the panel opens with a gap. */
.qdetails > *:first-child{margin-top:0}
/* The leader's readiness select, moved off the collapsed row into the expand. */
.qdstatus{margin:10px 0 4px}

.qactions{display:flex; align-items:center; gap:8px}
.qactions .linkbtn{
  min-height:44px; padding:0 12px; border-radius:10px; font-size:1rem;
  display:inline-flex; align-items:center; text-decoration:none;
  background:var(--surface); border:1px solid var(--border); color:var(--text);
}
.qactions .practice{background:var(--accent); color:var(--accent-ink); border-color:var(--accent)}

/* ---------- add-song picker ---------- */
/* Wider than the default modal so each filter row (Vibe has 7 chips, Era 8)
   fits on a single line instead of wrapping. Only kicks in where there's room:
   width:100% still shrinks it to the viewport on phones (bottom-sheet). */
#pickBack .modal{max-width:720px}
.field-hint{margin-top:6px; color:var(--muted); font-size:.8rem; line-height:1.45}
.pick-search{margin-bottom:12px}
.pick-list{display:flex; flex-direction:column; gap:8px; max-height:56vh; overflow:auto}
.pickitem{
  appearance:none; text-align:left; cursor:pointer; width:100%;
  display:flex; align-items:center; gap:12px; min-height:56px;
  background:var(--surface-2); border:1px solid var(--border);
  border-radius:12px; padding:8px 12px; color:var(--text);
}
.pickitem:hover{border-color:var(--accent)}
.pickitem.added{opacity:.55; cursor:default}
.pickmain{flex:1; min-width:0; display:flex; flex-direction:column; gap:2px}
.picktitle{font-weight:800; font-size:1rem; word-break:break-word}
.picksub{color:var(--muted); font-size:.85rem}
.pickadd{
  flex:none; font-weight:800; font-size:1.1rem; color:var(--accent);
  min-width:52px; text-align:center;
}
.pickitem.added .pickadd{color:var(--good); font-size:.9rem}

/* ---------------------------------------------------------------------
   Batch 156 — the setlist source in the practice picker.

   The source row swaps the list from "the whole repertoire, alphabetical"
   to "one setlist, in running order, grouped by set". Songs already in the
   practice keep the existing .pickitem.added treatment (opacity .55 + a
   green "Added") rather than being hidden — seeing what's handled is half
   the point of the feature.
   --------------------------------------------------------------------- */
.pf-srcrow{gap:8px}
.pf-src{
  flex:1 1 220px; min-width:0; max-width:100%;
  min-height:40px; padding:8px 10px;
  background:var(--chip); color:var(--text);
  border:1px solid var(--border); border-radius:10px;
  font-size:.9rem; font-family:inherit;
}
.pf-src:focus{border-color:var(--accent)}
/* Says once, in words, where a bulk add lands. Cheaper than making every
   button carry the caveat, and it's the answer to "could I dump a whole
   setlist onto the practice by accident?" — no, they land in the pool. */
.pf-srcnote{
  color:var(--muted); font-size:.8rem; line-height:1.45;
  margin:-2px 0 0 0; padding:6px 10px;
  background:var(--surface-2); border-radius:8px;
}
.pf-srcnote strong{color:var(--text)}

/* Each set is its own group, and THAT is what makes the sticky header work.
   Sticky siblings sharing one scroller pile up: rendered flat, Set 1's header
   stayed pinned while Set 2's stacked underneath it, so two headers claimed the
   same rows and Set 1's "＋ Add 3" sat over Set 2's songs. Scoped to a group,
   each header sticks only until its own set scrolls past, then gets pushed off
   by the next — which is the behaviour that makes the ＋ reachable down a
   fifteen-song set without ever hovering over the wrong one. */
.pick-setgrp{display:flex; flex-direction:column; gap:8px}
.pick-setgrp + .pick-setgrp{margin-top:6px}
.pick-seth{
  position:sticky; top:0; z-index:2;
  display:flex; align-items:center; gap:10px;
  padding:8px 2px 8px 4px;
  background:var(--surface);
  border-bottom:1px solid var(--border);
}
.pick-seth-main{flex:1; min-width:0; display:flex; flex-direction:column; gap:1px}
.pick-seth-n{
  font-weight:800; font-size:.78rem; letter-spacing:.06em;
  text-transform:uppercase; color:var(--accent);
}
.pick-seth-sub{color:var(--muted); font-size:.8rem}
.pick-setadd{
  appearance:none; cursor:pointer; flex:none; white-space:nowrap;
  min-height:36px; padding:6px 12px;
  background:var(--chip); color:var(--text);
  border:1px solid var(--accent); border-radius:999px;
  font-size:.85rem; font-weight:800; font-family:inherit;
}
.pick-setadd:hover{background:var(--accent); color:var(--accent-ink)}
.pick-allrow{display:flex; justify-content:center; padding:10px 0 2px}
.pick-addall{min-height:44px; padding:8px 18px}

/* =====================================================================
   Phase 4 — gig setlists
   ===================================================================== */

/* ---------- setlist head ---------- */
.setlist-head{display:flex; align-items:flex-start; flex-wrap:wrap; gap:8px 14px; margin-top:16px}
.setlist-when{min-width:0; flex:1 1 auto}
/* Phase 35: name + running total share the top line, baseline-aligned; the total
   wraps under the name on a tight screen. */
.setlist-title-row{display:flex; align-items:baseline; flex-wrap:wrap; gap:4px 12px}
.setlist-name{font-size:1.5rem; font-weight:800; letter-spacing:.2px; word-break:break-word}
.setlist-total{color:var(--muted); font-size:1rem; font-weight:600}
.setlist-total strong{color:var(--text); font-weight:800; font-size:1.1rem}
.setlist-total .untimed{font-weight:600}
.setlist-meta{color:var(--muted); font-size:.95rem; margin-top:2px}
.setlist-meta .setlist-rel{color:var(--accent); font-weight:700}
/* Right cluster: offline save/readiness + leader Duplicate/Edit, pushed right. */
.setlist-head-actions{display:flex; align-items:center; flex-wrap:wrap; gap:8px 10px; margin-left:auto}
.setlist-head-actions .gig-offline{width:auto; margin:0}
.setlist-notes{
  color:var(--muted); font-size:.95rem; margin-top:10px; white-space:pre-wrap; word-break:break-word;
  background:var(--surface); border:1px solid var(--border); border-left:4px solid var(--accent);
  border-radius:10px; padding:10px 12px;
}

.gig-toolbar{
  display:flex; align-items:center; flex-wrap:wrap; gap:10px; margin-top:14px;
  padding-top:14px; border-top:1px solid var(--border);
}
.grand-total{font-size:1rem; color:var(--muted)}
.grand-total strong{color:var(--text); font-size:1.15rem}
.gig-actions{display:flex; gap:8px; flex-wrap:wrap}
/* Phase 35: split toolbar — primary tools left, the paper utilities pushed right. */
.gig-actions-primary{align-items:center}
.gig-actions-secondary{margin-left:auto}
/* De-emphasise the "make paper" utilities so they read as secondary to Stage mode. */
.gig-util{color:var(--muted); font-weight:700}
.gig-util:hover:not(:disabled){color:var(--text)}

/* Phase 37: the 🖨 Print control + its pop-up menu (Setlist / Setlist + My Notes /
   One song per page). Trigger shows icon-only on phones, "🖨 Print ▾" from tablet up. */
.print-menu-wrap{ position:relative; display:inline-flex; }
.print-trigger{ display:inline-flex; align-items:center; gap:6px; }
.print-trigger .print-caret{ font-size:.7em; opacity:.8; }
.print-trigger[aria-expanded="true"] .print-caret{ transform:rotate(180deg); }
@media (max-width:639.98px){
  /* Space is tight on phones — collapse to just the printer icon. */
  .print-trigger .print-lbl, .print-trigger .print-caret{ display:none; }
  .print-trigger{ gap:0; }
}
.print-menu{
  position:absolute; right:0; top:calc(100% + 6px); z-index:40;
  display:flex; flex-direction:column; gap:2px;
  min-width:230px; max-width:min(300px, calc(100vw - 24px)); padding:6px;
  background:var(--surface); border:1px solid var(--border); border-radius:12px;
  box-shadow:0 12px 34px rgba(0,0,0,.38);
}
.print-menu[hidden]{ display:none; }
.print-menu-item{
  display:flex; flex-direction:column; align-items:flex-start; gap:1px;
  width:100%; text-align:left; padding:9px 11px; border:0; border-radius:8px;
  background:transparent; color:var(--text); cursor:pointer; font:inherit;
}
.print-menu-item:hover, .print-menu-item:focus-visible{ background:var(--surface-2); outline:none; }
.pmi-title{ font-weight:700; font-size:.95rem; }
.pmi-sub{ font-size:.78rem; color:var(--muted); line-height:1.25; }

/* ---------- sets ---------- */
.sset{
  background:var(--surface); border:1px solid var(--border);
  border-radius:var(--radius); padding:14px; margin-bottom:14px;
}
.sset-head{display:flex; align-items:baseline; gap:10px 12px; flex-wrap:wrap; margin-bottom:12px}
.sset-head h2{font-size:1.15rem}
.sset-sub{color:var(--muted); font-size:.9rem; font-weight:700}
/* v142 — the leader's per-set readiness rollup ("2 no lyrics · 1 no timing").
   Same muted-dashed voice as the .badge.nolyrics/.notiming chips it summarizes:
   a to-do, not an alarm. Rendered only for a leader and only when a gap exists
   (see renderSets) — a clean set shows nothing, and nothing means ready. */
.sset-gaps{
  color:var(--muted); font-size:.82rem; font-weight:700; white-space:nowrap;
  border:1px dashed var(--border); border-radius:999px; padding:2px 10px;
}
.sset-remove{margin-left:auto; color:var(--danger); border-color:transparent}
/* Phase 39: per-set "▶ Start here" — launch Stage Mode into this set. Right-aligned,
   quiet orange to relate to Stage mode; only ever shown on a non-empty set. */
.sset-stage{
  margin-left:auto; appearance:none; cursor:pointer; white-space:nowrap;
  min-height:32px; padding:0 12px; border-radius:999px; line-height:1;
  background:transparent; color:var(--accent); border:1px solid rgba(232,144,63,.55);
  font-weight:800; font-size:.82rem;
}
.sset-stage:hover{ background:rgba(232,144,63,.16); }
/* Phase 40: the "Set N" heading is a quiet collapse toggle. A caret shows the state
   (▾ open, ▸ collapsed); when collapsed, the load chips, songs, and add button fold
   away, leaving just the header + summary so a later set's "Start here" is one tap. */
.sset-toggle{
  appearance:none; background:transparent; border:0; padding:0; margin:0; cursor:pointer;
  display:inline-flex; align-items:center; gap:8px;
  font:inherit; font-size:1.15rem; font-weight:800; color:var(--text);
}
.sset-caret::before{ content:'▾'; display:inline-block; color:var(--muted); font-size:.85em; transition:transform .15s ease; }
.sset-toggle[aria-expanded="false"] .sset-caret::before{ transform:rotate(-90deg); }
.sset.collapsed .sset-load,
.sset.collapsed .sset-songs,
.sset.collapsed .sset-add{ display:none; }
/* Batch 135: the same ＋ button now sits above the songs as well as below. It
   wears .sset-add too, so the collapsed-set rule above already hides it — check
   that stays true if you ever rename either. The margin closes the gap to the
   singer-load chips so the header block still reads as one unit. */
.sset-add-top{ margin-bottom:12px; }
.sset-songs{display:flex; flex-direction:column; gap:10px; margin-bottom:12px; min-height:8px}
.sset-empty{color:var(--muted); font-size:.92rem; padding:10px 4px; border:1px dashed var(--border); border-radius:10px; text-align:center}
.setlist-addset{width:100%; margin-bottom:26px; border-style:dashed}

/* ---------- v172: the singer's name, in their colour ----------
   `.vox` is the name span UI.voxHtml emits; `.vox-N` picks the swatch. A
   singer with no swatch renders as plain bold text, same weight, so the
   line reads evenly whether or not everyone has chosen one. The colour
   wins over whatever the parent set (muted meta lines, the accent block
   heading) — that is the point. */
.vox{ font-weight:700; }
.vox-1{ color:var(--vox-1); } .vox-2{ color:var(--vox-2); } .vox-3{ color:var(--vox-3); }
.vox-4{ color:var(--vox-4); } .vox-5{ color:var(--vox-5); } .vox-6{ color:var(--vox-6); }
.vox-sep{ color:var(--muted); font-weight:400; }
/* On the set map a tile wears the singer as a small tag under the title. */
.setmap-tag{ font-size:.72rem; font-weight:700; letter-spacing:.2px; }
/* The swatch picker in the "What do you play?" dialog. */
.vox-pick{ display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin:4px 0 2px; }
.vox-pick-lbl{ font-size:.85rem; color:var(--muted); font-weight:700; margin-right:2px; }
.vox-sw{
  appearance:none; cursor:pointer; width:38px; height:38px; border-radius:50%;
  border:2px solid var(--border); background:var(--surface-2); padding:0;
  display:inline-flex; align-items:center; justify-content:center;
  color:var(--sw); box-shadow:inset 0 0 0 4px var(--surface-2), inset 0 0 0 20px var(--sw);
}
.vox-sw.on{ border-color:var(--text); box-shadow:inset 0 0 0 3px var(--surface-2), inset 0 0 0 20px var(--sw); }
.vox-sw:focus-visible{ outline:3px solid var(--focus); outline-offset:2px; }
.vox-sw-none{ box-shadow:none; color:var(--muted); font-size:.75rem; font-weight:700; }
.vox-sw-none.on{ border-color:var(--text); }

/* ---------- setlist rows ---------- */
.sitem{
  display:flex; align-items:center; gap:10px;
  background:var(--surface-2); border:1px solid var(--border);
  border-radius:12px; padding:10px 12px;
}
.sitem.dragging{opacity:.4}
.sdrag{
  cursor:grab; color:var(--muted); font-size:1.2rem; line-height:1; flex:none;
  padding:0 2px; touch-action:none; user-select:none;
}
.sdrag:active{cursor:grabbing}
.smain{flex:1; min-width:0}
/* v171 — the title block is a launch button: arm on one tap (accent), Stage
   Mode on the second. Same look and manners as the practice row's .qlaunch. */
.shead{display:flex; align-items:center; gap:6px 10px; flex-wrap:wrap; min-width:0}
.stools{display:inline-flex; align-items:center; gap:6px; flex:0 0 auto}
.slaunch{
  appearance:none; background:none; border:0; padding:4px 8px 4px 6px; margin:0 0 0 -6px;
  font:inherit; color:inherit; text-align:left; cursor:pointer; display:block; flex:1 1 55%; min-width:0;
  border-radius:10px; min-height:44px; transition:background-color .12s ease, color .12s ease;
}
.slaunch:focus-visible{outline:3px solid var(--focus); outline-offset:2px}
.sitem.armed .slaunch{background:var(--accent); color:var(--accent-ink)}
.sitem.armed .ssub{display:none}
.sitem.armed .snum{color:var(--accent-ink); opacity:.85}
.sitem.armed .stitle::after{content:"tap again for Stage Mode ›"; flex:1 0 100%; font-weight:700; font-size:.8em; opacity:.85; letter-spacing:.2px}
.sitem.armed .slaunch .badge{color:var(--accent-ink); border-color:currentColor; background:transparent}
.stitle{font-weight:800; font-size:1.02rem; word-break:break-word; display:flex; align-items:center; gap:8px; flex-wrap:wrap}
.ssub{color:var(--muted); font-size:.88rem; margin-top:2px; word-break:break-word; display:block}
.slplay{text-decoration:none; font-size:1rem; margin-left:2px}
.badge.reprise{
  background:rgba(232,144,63,.16); color:var(--accent); border-color:var(--accent);
  font-size:.72rem; text-transform:uppercase; letter-spacing:.4px;
}

/* Phase 12: attribute badges under a setlist row (vibe, not-gig-ready flag),
   the "+N untimed" caveat on running times, and the picker filter chips. */
.sbadges{display:flex; flex-wrap:wrap; gap:6px; margin-top:6px}
.sbadges .badge{margin:0; font-size:.72rem; padding:2px 8px}
.untimed{color:var(--muted); font-weight:600}
.pick-filters{display:flex; flex-direction:column; gap:8px; margin:2px 0 12px}
.pf-row{display:flex; flex-wrap:wrap; align-items:center; gap:6px}
.pf-lbl{font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); min-width:52px}
.pf-row .chip{min-height:36px; padding:6px 12px; font-size:.85rem}
.pf-clear{margin-left:auto; color:var(--muted)}
/* Singer-load tally: a quiet per-set readout of who carries how much. Neutral
   by design — a readout, not a verdict. */
.sset-load{display:flex; flex-wrap:wrap; gap:6px; margin:2px 0 10px}
.load-chip{
  font-size:.74rem; font-weight:600; color:var(--muted);
  background:var(--surface-2); border:1px solid var(--border);
  border-radius:999px; padding:2px 9px;
}
.load-chip b{color:var(--text); font-weight:800}
.load-unknown{opacity:.75; font-style:italic}
/* A singer who's out for the show — dimmed + labelled in the per-set tally. */
.load-chip.load-out{opacity:.55; text-decoration:line-through; text-decoration-color:var(--danger)}
.load-chip.load-out .load-out-tag{
  text-decoration:none; margin-left:4px; font-weight:800; letter-spacing:.3px;
  font-size:.66rem; text-transform:uppercase; color:var(--danger);
}

/* Phase 13: gig attendance strip — who's in the room for this show. Sits above
   Set 1. Tap a name to toggle them out. Flags songs, never removes them. */
.attendance{
  background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
  padding:12px 14px; margin:0 0 16px;
}
.att-head{display:flex; align-items:baseline; gap:10px; margin-bottom:9px; flex-wrap:wrap}
.att-lbl{font-size:.72rem; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); font-weight:800}
.att-summary{font-size:.85rem; color:var(--danger); font-weight:700}
.att-summary.att-full{color:var(--muted); font-weight:600}
.att-chips{display:flex; flex-wrap:wrap; gap:8px}
.att-chip{
  appearance:none; cursor:pointer; min-height:38px; padding:6px 14px;
  border:1px solid var(--border); background:var(--surface-2); color:var(--text);
  border-radius:999px; font-size:.9rem; font-weight:700;
  transition:background .15s ease, border-color .15s ease, color .15s ease, opacity .15s ease;
}
.att-chip::before{content:"✓ "; color:var(--accent); font-weight:800}
.att-chip.out{
  opacity:.6; text-decoration:line-through; text-decoration-color:var(--danger);
  border-color:var(--danger); color:var(--muted);
}
.att-chip.out::before{content:"✕ "; color:var(--danger); text-decoration:none}

/* Song-row flag when a singer on that song is out for the show. */
.badge.singer-out{
  background:rgba(229,103,95,.16); color:var(--danger); border-color:var(--danger);
  font-weight:700;
}

/* drag insertion line */
.sdrop{height:0; border-top:3px solid var(--accent); border-radius:2px; margin:-6px 0}

/* On desktop the drag handle is the obvious grab point; arrows stay for
   keyboard + touch. Hide the handle on coarse pointers (phones) where the
   arrows are the intended control. */
/* ⚠️ v174 — THIS RULE USED TO READ `@media (hover:none){.sdrag{display:none}}`,
   with a comment saying the ▲▼ arrows were the touch control instead. Those
   arrows were deleted a batch later and nobody swept this, so from then until
   v174 a setlist COULD NOT BE REORDERED ON AN IPAD AT ALL: the handle was
   hidden, drag does not fire on touch, and nothing errored. ⠿ is now the row
   actions button and has a job without a pointer, so on touch it gets BIGGER,
   not hidden. Do not re-hide it. → ipad_reorder_deadend */
@media (hover:none){ .sdrag{ display:inline-flex; width:38px; } }

/* ---------- picker: target + "already in" hint ---------- */
.pick-target{color:var(--muted); font-weight:700; font-size:.9rem; margin:-4px 0 10px}
.pickin{
  font-size:.72rem; font-weight:700; color:var(--muted);
  background:var(--surface); border:1px solid var(--border);
  border-radius:999px; padding:2px 8px;
}
/* Phase 27: the "wants a revisit" tally on a picker row (teal, stands out from
   the muted in-set / in-list tags), and the "who added this" attribution. */
.pickrev{
  font-size:.72rem; font-weight:800; color:var(--play-ink);
  background:var(--play); border-radius:999px; padding:2px 8px; margin-left:2px;
}
.qadder{ color:var(--muted); font-weight:700; font-style:italic; opacity:.9; }

/* =====================================================================
   Stage mode — big, dark, tap-through
   ===================================================================== */
/* =====================================================================
   Stage mode — the gig-day view. Responsive across portrait + landscape on
   iPhone and iPad. Type is sized by whichever is SMALLER, the viewport width
   or height (min(vw,vh)), so a short landscape screen shrinks the type to fit
   instead of scaling it off the edges. Height uses dynamic viewport units
   (dvh) so the layout sits under iOS Safari's chrome; on iPad / desktop /
   Android the Fullscreen API hides that chrome outright.
   ===================================================================== */
body.stage-lock{overflow:hidden}
.stage{
  --stage-scale:1;   /* text-size multiplier, set per device by the A−/A+ control */
  position:fixed; inset:0; z-index:80; display:none; flex-direction:column;
  height:100vh; height:100svh; height:100dvh;   /* progressive enhancement */
  background:var(--bg); color:var(--text); gap:8px;
  /* Phase 34: trimmed the top/bottom breathing room (was 12/14) to give the
     lyrics more height — the safe-area insets still keep buttons clear of the
     notch / home indicator when running full-screen on an iPad or iPhone. */
  padding:
    calc(env(safe-area-inset-top,0px) + 8px)
    calc(env(safe-area-inset-right,0px) + 16px)
    calc(env(safe-area-inset-bottom,0px) + 10px)
    calc(env(safe-area-inset-left,0px) + 16px);
}
.stage.open{display:flex}
.stage:fullscreen{height:100vh}

.stage-top{display:flex; align-items:center; flex-wrap:wrap; gap:8px 10px; flex:0 0 auto}
.stage-counter{color:var(--accent); font-weight:800; font-size:clamp(.8rem,3.4vw,1rem); letter-spacing:.3px; flex:0 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.stage-top-btns{margin-left:auto; display:flex; gap:8px; flex:0 0 auto}
/* Phase 34: the reading tools — text size / layout / width / 📌📝 — ride in the top
   bar, right-justified beside the ⛶/✕ buttons. The responsive wrap (dropping the
   cluster to its own right-justified row on phones) lives in the JS-injected block
   so it survives a stale stylesheet; these are the base looks. */
.stage-tools{flex:1 1 auto; display:flex; align-items:center; justify-content:flex-end; flex-wrap:wrap; gap:6px 10px; min-width:0}
.stage-tools .stage-size, .stage-tools .stage-opts{padding:0}
.stage-opt-emoji{min-width:44px; padding:0 6px; font-size:1.2rem; line-height:1}
.stage-tb{
  appearance:none; background:var(--surface-2); color:var(--text);
  border:1px solid var(--border); border-radius:12px; min-width:46px; min-height:46px;
  font-size:1.15rem; line-height:1; cursor:pointer; display:flex; align-items:center; justify-content:center;
}
.stage-tb[hidden]{display:none}
/* v171 — the way OUT reads as a button. `.stage-tb` is surface-2 on a 1px
   --border, which on the near-black theme is a ~1.2:1 box: a white ✕ floating
   on nothing. The exit gets an accent outline + accent glyph, and grows to the
   app's own 48px floor. Outline, not fill — see .stage-counter-btn above.
   The other bar buttons (⏱ ▶ ⛶ 🎧) stay quiet on purpose: they're the
   leader's and the power user's; the ✕ is everyone's. */
.stage-tb.stage-x{ border:2px solid var(--accent); color:var(--accent); min-width:48px; min-height:48px; font-weight:700; }
.stage-tb.stage-x:active{ transform:scale(.96); }

.stage-hint{
  flex:0 0 auto; display:flex; align-items:center; gap:10px;
  background:var(--surface-2); border:1px solid var(--border); border-radius:12px;
  padding:8px 12px; font-size:.85rem; color:var(--muted);
}
.stage-hint[hidden]{display:none}
.stage-hint span{flex:1}
.stage-hint-x{appearance:none; border:0; background:transparent; color:var(--muted); font-size:1rem; cursor:pointer; min-width:32px; min-height:32px}

.stage-main{
  flex:1 1 auto; min-height:0; display:flex; flex-direction:column;
  justify-content:safe center; align-items:safe center;   /* 'safe' = never clip when it overflows */
  text-align:center; gap:12px; cursor:default; overflow-y:auto;
}
/* v141 — flex:0 0 auto IS THE TAIL-PAD FIX. This column sits inside the
   scrolling .stage-main; as an ordinary flex item (shrink:1 + min-height:0) it
   COMPRESSED to the container's height and its lyrics spilled out as descendant
   overflow. That spill already covered the spot where the auto-scroll tail pad
   lands, so appending the pad to the scroller added ZERO scrollable height and
   the last lines could never ride up to the reading third. (It used to work by
   accident: the old largest-overflow scroller pick migrated the pad INSIDE this
   box. Phase 57's — correct — canScroll hardening ended that and orphaned the
   pad.) With shrink off, the column lays out at its natural height, the
   scroller owns all the overflow, and a spacer after it genuinely extends the
   scroll range. The landscape short-screen override further down sets its own
   flex and still wins the cascade — which is also why this lives HERE and is
   never set inline from JS. */
.stage-primary{display:flex; flex-direction:column; align-items:center; gap:12px; width:100%; max-width:900px; min-height:0; flex:0 0 auto}
.stage-upnext{flex:0 0 auto}
/* Phase 57 — align-self/min-width are what make UI.fitOneLine WORK, not styling.
   .stage-song is a flex item in .stage-primary (column flex, align-items:center),
   so by default its cross-axis size is fit-content. fitOneLine sets nowrap and
   then compares clientWidth to scrollWidth — but a fit-content box grows with its
   own text, so those were always equal and it concluded every title fit. The
   shrink never fired and long titles clipped off BOTH edges instead of wrapping
   (Phase 48 regression). Stretching the element to the column's width gives the
   measurement something real to compare against. Do not remove. */
.stage-song{font-size:calc(var(--stage-scale,1) * clamp(1.9rem, min(9vw,11vh), 4.5rem)); font-weight:800; line-height:1.04; letter-spacing:.2px; align-self:stretch; min-width:0}
.stage-sub{color:var(--muted); font-size:calc(var(--stage-scale,1) * clamp(1rem, min(3.6vw,4.4vh), 1.6rem)); font-weight:700}
.stage-next-label{color:var(--muted); font-size:clamp(.8rem,2.6vw,1rem); font-weight:700; margin-top:6px; text-transform:uppercase; letter-spacing:.6px}
.stage-next-song{color:var(--accent); font-size:calc(var(--stage-scale,1) * clamp(1.15rem, min(4.4vw,5vh), 2.2rem)); font-weight:800; line-height:1.1}
.stage-next-label.stage-end{color:var(--good)}

.stage-nav{display:flex; gap:12px; flex:0 0 auto}
/* Phase 34: shorter Prev/Next (was clamp(50,8vh,64)) to hand the extra height to
   the lyrics — colours unchanged (Next stays the amber accent). */
.stage-nav button{
  flex:1; appearance:none; min-height:clamp(42px,6vh,54px); border-radius:14px; font-size:1.1rem; font-weight:800;
  background:var(--surface-2); color:var(--text); border:1px solid var(--border); cursor:pointer;
}
.stage-nav .stage-next{background:var(--accent); color:var(--accent-ink); border-color:var(--accent)}
.stage-nav button:disabled{opacity:.35; cursor:default}

/* Off-the-cuff request controls. The "?" sits narrow at the left; once armed it
   flips to the accent so the confirm tap is obvious. While a guest card is up,
   Prev/Next/? give way to one wide "Return to the set" button. */
.stage-nav .stage-ask{ flex:0 0 auto; min-width:clamp(48px,8vw,64px); color:var(--muted); font-size:1.25rem }
.stage-nav .stage-ask.armed{ background:var(--accent); color:var(--accent-ink); border-color:var(--accent); font-size:1rem }
.stage-nav .stage-return{ flex:1; background:var(--accent); color:var(--accent-ink); border-color:var(--accent) }
.stage-nav.guest .stage-ask,
.stage-nav.guest .stage-prev,
.stage-nav.guest .stage-next{ display:none }

/* ---- landscape on a short screen: current song + cues on the left, up-next
   on the right, so the short height isn't fighting a tall stack ---- */
@media (orientation:landscape) and (max-height:600px){
  .stage{gap:8px}
  .stage-main{flex-direction:row; align-items:stretch; gap:16px; text-align:left; overflow:hidden}
  .stage-primary{flex:1 1 60%; justify-content:safe center; align-items:flex-start; overflow-y:auto; max-width:none}
  .stage-upnext{
    flex:0 0 34%; display:flex; flex-direction:column; justify-content:center; align-items:flex-start;
    border-left:1px solid var(--border); padding-left:16px;
  }
  .stage-song{font-size:calc(var(--stage-scale,1) * clamp(1.5rem, min(6vw,13vh), 3rem))}
  .stage-sub{font-size:calc(var(--stage-scale,1) * clamp(.9rem, min(2.4vw,5vh), 1.3rem))}
  .stage-next-label{margin-top:0}
  .stage-next-song{font-size:calc(var(--stage-scale,1) * clamp(1.1rem, min(3vw,7vh), 1.8rem))}
  .stage-nav button{min-height:42px; font-size:1rem}
}

/* =====================================================================
   Printable one-pager — light + big for a music stand
   ===================================================================== */
.print-sheet{display:none}
@media print{
  /* Show only the print sheet.
     ⚠️ THE BLANK-PAGE TRAP (fixed Aug 2026) — do NOT go back to
     `body *{visibility:hidden}` + an absolutely-positioned sheet. That was the
     old approach and it caused a pile of blank pages after every print:
       * visibility:hidden hides pixels but the element STILL OCCUPIES LAYOUT,
         so the whole app page (topbar, cards, rails, dock — several feet of it)
         kept its full height in the print flow;
       * .print-sheet was position:absolute, i.e. OUT of that flow, so it never
         governed the page count — the invisible app did.
       Result: a 16-song practice list printed 1 real page + N blank ones, where
       N was just "how tall the app page happened to be".
     The fix is to REMOVE the app from the print flow (display:none) and let the
     sheet sit in normal static flow, so the paper is exactly as long as the
     sheet. Page count is now content-driven, which is also what makes the
     break-inside rules below behave. */
  body{background:#fff}
  body > *:not(.print-sheet){display:none !important}
  .print-sheet{
    display:block; position:static; width:auto; margin:0;
    color:#000; background:#fff; padding:0 6mm; font-size:15pt;
    -webkit-print-color-adjust:exact; print-color-adjust:exact;
  }
  .print-head{text-align:center; margin-bottom:6mm; border-bottom:2pt solid #000; padding-bottom:3.5mm}
  .print-title{font-size:30pt; font-weight:800; line-height:1.1}
  .print-date{font-size:14pt; margin-top:2mm; color:#333}
  /* Let a long set flow across pages (a 15-song set + notes can exceed one
     page); keep each song row intact instead, and keep the set head with its
     first song. This avoids a set being shoved whole onto page 2 (blank page 1). */
  .print-set{margin-bottom:6mm}
  .print-set-head{
    display:flex; justify-content:space-between; align-items:baseline;
    font-size:16pt; font-weight:800; border-bottom:1pt solid #999; padding-bottom:1.5mm; margin-bottom:2.5mm;
    break-after:avoid; page-break-after:avoid;
  }
  .print-set-time{font-size:12pt; font-weight:700; color:#333}
  .print-songs{list-style:none; margin:0; padding:0}
  /* flex-WRAP is essential: without it the .pmynote (flex-basis:100%) doesn't
     drop to its own line — it competes for width on the row and crushes the
     title to one word per line. A song row keeps together across a page break. */
  .print-songs li{
    font-size:17pt; font-weight:700; line-height:1.38; display:flex; flex-wrap:wrap;
    align-items:baseline; gap:6px; break-inside:avoid; page-break-inside:avoid; margin-bottom:.8mm;
  }
  .print-songs .pnum{color:#666; font-weight:800; min-width:9mm}
  .print-songs .psong{font-weight:700}
  .print-songs .part{font-size:12pt; font-weight:600; color:#555}
  .print-songs .pmeta{font-size:11pt; font-weight:600; color:#777; margin-left:auto; white-space:nowrap}
  /* Personal cues wrap to their own line under the song (flex-basis:100% + the
     row's flex-wrap forces the break), indented to clear the number. Only present
     when My notes is on. */
  .print-songs .pmynote{flex-basis:100%; margin-left:9mm; font-size:11pt; font-weight:600; font-style:italic; color:#555; line-height:1.3}
  /* v157 — singer headings on the printed sheet. It's a <li> so it flows with
     the list, but it is not a song: no number, and it never sits alone at the
     foot of a page away from the songs it labels. */
  .print-songs li.pblock{
    font-size:13pt; font-weight:800; text-transform:uppercase; letter-spacing:1pt;
    color:#000; border-bottom:1.5pt solid #000; padding-bottom:1mm;
    margin:5mm 0 2mm; break-after:avoid; page-break-after:avoid;
  }
  .print-songs li.pblock:first-child{margin-top:0}
  .print-songs .pblock-n{margin-left:auto; font-size:10pt; font-weight:700; color:#777; letter-spacing:0; text-transform:none}
  .print-total{margin-top:3mm; font-size:14pt; font-weight:800; text-align:right; border-top:2pt solid #000; padding-top:1.5mm}
  .print-foot{margin-top:3mm; text-align:center; font-size:10pt; color:#999; letter-spacing:1pt; text-transform:uppercase}
  .fab, .toast, .topbar{display:none !important}
}

/* ---------- Offline Stage Mode (Phase 4): the "Stage Sheet" PDF parachute ----------
   A second print mode on the SAME hidden .print-sheet container, switched on by the
   .stage-sheet class. One song per page, big legible type; long lyrics FLOW to a
   second page (never clipped — no overflow:hidden). Each song is a <table> so its
   title/slate rides in a repeating <thead>: on any overflow page the header reprints,
   which is the reliable cross-browser "this song continues" marker (iPad Safari has
   no support for CSS-only per-page running headers). The running-order cover reuses
   the compact .print-* look above. */
@media print{
  /* Page 1: the whole-gig running order, then a hard break into the song pages. */
  .stage-sheet .ssheet-cover{ break-after:page; page-break-after:always; }
  .stage-sheet .ssheet-cover-tag{
    margin-top:7mm; text-align:center; font-size:11pt; font-style:italic; color:#777;
  }

  /* One song = one <table> that starts a fresh page and may spill to the next. */
  .stage-sheet table.ssheet-song{
    width:100%; border-collapse:collapse; margin:0;
    break-after:page; page-break-after:always;
    break-inside:auto; page-break-inside:auto;   /* allow the spill — never clip */
  }
  .stage-sheet table.ssheet-song:last-child{ break-after:auto; page-break-after:auto; }
  /* thead as a header GROUP → the browser reprints it atop each overflow page. */
  .stage-sheet .ssheet-song thead{ display:table-header-group; }
  .stage-sheet .ssheet-song th,
  .stage-sheet .ssheet-song td{ text-align:left; padding:0; vertical-align:top; }

  .stage-sheet .ssheet-slate{
    font-size:12pt; font-weight:700; color:#666; letter-spacing:.3pt; margin-bottom:2mm;
  }
  .stage-sheet .ssheet-title{ font-size:34pt; font-weight:800; line-height:1.05; }
  .stage-sheet .ssheet-meta{
    font-size:15pt; font-weight:600; color:#444; margin-top:1.5mm;
    padding-bottom:2.5mm; border-bottom:1.5pt solid #000; margin-bottom:4mm;
  }

  /* Leader Note (canonical, plain text) — a quiet slab, line breaks preserved. */
  .stage-sheet .ssheet-canon{
    margin:0 0 5mm; padding:3mm 4mm; border-left:3pt solid #000; background:#f1f1f1;
    -webkit-print-color-adjust:exact; print-color-adjust:exact; break-inside:avoid;
  }
  .stage-sheet .ssheet-canon-lbl{
    font-size:10.5pt; font-weight:800; text-transform:uppercase; letter-spacing:1pt;
    color:#555; margin-bottom:1mm;
  }
  .stage-sheet .ssheet-canon-tx{ font-size:16pt; font-weight:600; line-height:1.35; white-space:pre-wrap; }

  /* Personal cues / lyrics (rich text). Big + airy; flows freely across pages. */
  .stage-sheet .ssheet-cue{ font-size:19pt; font-weight:600; line-height:1.45; margin-bottom:3mm; }
  .stage-sheet .ssheet-cue:last-child{ margin-bottom:0; }
  .stage-sheet .ssheet-empty{ font-size:13pt; font-style:italic; color:#999; }
}

/* =====================================================================
   Phase 5 — leader mode + nominations (Ideas)
   ===================================================================== */

/* ---------- topbar leader toggle ---------- */
/* Icon-only now (just the padlock) — tight and square-ish so it barely takes any
   room in the phone topbar. The word "Leader" lives in the aria-label + title. */
.leader-toggle{
  white-space:nowrap; padding:0; width:38px; min-width:38px;
  display:inline-flex; align-items:center; justify-content:center;
  font-size:1.05rem; line-height:1;
}
.leader-toggle.is-leader{
  background:var(--good); color:var(--accent-ink); border-color:var(--good);
}
.leader-toggle.is-leader:hover{background:#69b559}
.leader-modal .leader-err{
  color:var(--danger); font-size:.92rem; font-weight:700; margin:-4px 0 12px;
}

/* ---------- Ideas page ---------- */
.nom-top{
  display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:16px;
}
.nom-top h1{font-size:1.3rem; margin:0}
.nom-top .spacer{flex:1}

.nom-section{margin:22px 0}
.nom-section > h2{
  font-size:1.05rem; color:var(--muted); margin:0 0 10px;
  display:flex; align-items:center; gap:8px;
}
.nom-section > h2 .nom-count{
  font-size:.8rem; font-weight:700; color:var(--muted);
  background:var(--surface-2); border:1px solid var(--border);
  border-radius:999px; padding:2px 9px;
}
.nom-list{display:flex; flex-direction:column; gap:12px}

.nom{
  display:flex; gap:14px; align-items:flex-start;
  background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
  padding:14px; border-left:5px solid var(--accent);
}
.nom.decided{border-left-color:var(--border); opacity:.9}

/* vote stack: 👍 / net score / 👎 */
.nom-vote{
  display:flex; flex-direction:column; align-items:center; gap:4px; flex-shrink:0;
}
.votebtn{
  appearance:none; border:1px solid var(--border); background:var(--surface-2);
  color:var(--text); width:var(--tap); min-height:44px; border-radius:12px;
  font-size:1.25rem; line-height:1; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
}
.votebtn:active{transform:scale(.94)}
.votebtn.up[aria-pressed=true]{background:var(--good); color:var(--accent-ink); border-color:var(--good)}
.votebtn.down[aria-pressed=true]{background:var(--danger); color:#fff; border-color:var(--danger)}
.nom-score{font-weight:800; font-size:1.1rem; min-width:2ch; text-align:center}
.nom-score.pos{color:var(--good)}
.nom-score.neg{color:var(--danger)}

.nom-main{flex:1; min-width:0}
.nom-title{font-weight:800; font-size:1.08rem}
.nom-artist{color:var(--muted); font-size:.98rem; margin-top:2px}
.nom-by{color:var(--muted); font-size:.85rem; margin-top:4px}
.nom-dupe{
  display:inline-block; margin-top:8px; font-size:.85rem; font-weight:700;
  color:var(--accent); background:rgba(232,144,63,.12);
  border:1px solid var(--accent); border-radius:10px; padding:5px 9px;
}
.nom-links{margin-top:10px}
.nom-links a.linkbtn{
  display:inline-flex; align-items:center; gap:6px; text-decoration:none;
  font-size:.9rem; font-weight:700; padding:9px 12px; border-radius:10px;
  background:var(--surface-2); border:1px solid var(--border); color:var(--text);
  min-height:40px;
}
.nom-voters{margin-top:10px; font-size:.85rem; color:var(--muted); line-height:1.5}
.nom-voters .up-names{color:var(--good); font-weight:700}
.nom-voters .down-names{color:var(--danger); font-weight:700}
.nom-actions{
  margin-top:12px; display:flex; gap:8px; flex-wrap:wrap;
  border-top:1px solid var(--border); padding-top:12px;
}
.nom-actions .btn-sm{min-height:44px}

.nom .status-tag{
  display:inline-block; font-size:.78rem; font-weight:800; letter-spacing:.5px;
  text-transform:uppercase; padding:3px 9px; border-radius:999px; margin-top:2px;
}
.nom .status-tag.added{color:var(--good); border:1px solid var(--good); background:rgba(122,195,106,.14)}
.nom .status-tag.passed{color:var(--danger); border:1px solid var(--danger); background:rgba(229,103,95,.13)}

/* --- Phase 11: richer pitch, concern flags, comments, turnout --- */
.nom-reason{
  margin-top:8px; font-size:.95rem; color:var(--text); line-height:1.45;
  white-space:pre-wrap; word-wrap:break-word;
}
.nom-lead{margin-top:6px; font-size:.9rem; color:var(--muted)}
.nom-lead strong{color:var(--text)}
.nom-passreason{
  margin-top:8px; font-size:.88rem; color:var(--muted); line-height:1.45;
  background:var(--surface-2); border:1px solid var(--border);
  border-radius:10px; padding:7px 10px;
}
.nom-passreason strong{color:var(--text)}
.nom-turnout{color:var(--muted); font-weight:700; white-space:nowrap}

/* concern flags */
.nom-flags-summary{margin-top:10px; display:flex; flex-direction:column; gap:4px}
.flag-line{font-size:.85rem; color:var(--muted); line-height:1.4}
.flag-inst{font-weight:800; color:var(--text)}
.flag-note{color:var(--muted); font-style:italic}
.nom-flagpick{margin-top:10px}
.nom-flagpick > summary{
  cursor:pointer; font-size:.85rem; font-weight:700; color:var(--accent);
  list-style:none; padding:4px 0; min-height:32px;
  display:inline-flex; align-items:center;
}
.nom-flagpick > summary::-webkit-details-marker{display:none}
.flagpick{display:flex; flex-wrap:wrap; gap:6px; margin-top:8px}
.flagchip{font-size:.82rem; min-height:40px; padding:6px 10px}
.flagchip[aria-pressed=true], .flagchip.on{
  background:var(--danger); color:#fff; border-color:var(--danger);
}
/* concern tally — counts shown like a second vote count */
.nom-concerns{margin-top:10px}
.nom-concerns-lbl{display:block; font-size:.8rem; color:var(--muted); font-weight:700; margin-bottom:6px}
.flagtally{display:flex; flex-wrap:wrap; gap:6px}
.flagcount{
  display:inline-flex; align-items:center; gap:5px; font-size:.82rem; color:var(--muted);
  background:var(--surface-2); border:1px solid var(--border); border-radius:999px; padding:4px 10px;
}
.flagcount b{color:var(--danger); font-weight:800}
.flagchip .flagn{font-weight:800; margin-left:2px}
.flagchip:not(.on) .flagn{color:var(--danger)}

/* comment thread */
.nom-comments{margin-top:12px; border-top:1px solid var(--border); padding-top:10px}
.cmt-list{display:flex; flex-direction:column; gap:8px; margin-bottom:8px}
.cmt{background:var(--surface-2); border:1px solid var(--border); border-radius:10px; padding:8px 10px}
.cmt-head{display:flex; align-items:center; gap:8px; font-size:.8rem; color:var(--muted)}
.cmt-who{font-weight:800; color:var(--text)}
.cmt-del{
  margin-left:auto; appearance:none; background:none; border:none; cursor:pointer;
  color:var(--muted); font-size:.9rem; line-height:1; padding:4px; border-radius:6px;
}
.cmt-del:hover{color:var(--danger)}
.cmt-body{margin-top:3px; font-size:.92rem; color:var(--text); line-height:1.45; white-space:pre-wrap; word-wrap:break-word}
.cmt-empty{font-size:.85rem; color:var(--muted); margin-bottom:8px}
.cmt-new{display:flex; gap:8px; align-items:flex-end}
.cmt-input{
  flex:1; min-width:0; resize:vertical; min-height:40px; font:inherit; line-height:1.4;
  color:var(--text); background:var(--surface-2); border:1px solid var(--border);
  border-radius:10px; padding:9px 10px;
}
.cmt-input:focus{outline:none; border-color:var(--accent)}
.cmt-new .btn-sm{min-height:40px; flex-shrink:0}

/* =====================================================================
   Phase 6 — polish + accessibility sweep
   Bottom tab bar, one Filters popout, styled confirm + undo toast,
   consistent motion, and the a11y trimmings. No new feature surface.
   ===================================================================== */

/* ---------- skip to content (keyboard users land here first) ---------- */
.skip-link{
  position:fixed; top:8px; left:8px; z-index:100;
  background:var(--accent); color:var(--accent-ink); font-weight:800;
  padding:10px 16px; border-radius:10px; text-decoration:none;
  transform:translateY(-160%); transition:transform .16s ease;
}
.skip-link:focus{transform:translateY(0)}

/* ---------- bottom tab bar (phones) ---------- */
.bottomnav{
  position:fixed; left:0; right:0; bottom:0; z-index:70;
  display:flex; gap:4px;
  background:var(--surface); border-top:1px solid var(--border);
  padding:6px 6px calc(6px + env(safe-area-inset-bottom,0px));
  box-shadow:0 -6px 20px rgba(0,0,0,.4);
}
.bottomnav a{
  flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px;
  min-height:var(--tap); padding:5px 4px; border-radius:12px;
  color:var(--muted); text-decoration:none; font-size:.72rem; font-weight:700;
  transition:color .15s ease, background .15s ease;
}
.bottomnav a .bn-ic{font-size:1.3rem; line-height:1}
.bottomnav a.active{color:var(--accent); background:var(--surface-2)}
.bottomnav a:active{background:var(--surface-2)}

/* Phones: the bottom bar replaces the inline top nav; give content room. */
@media (max-width:639.98px){
  .topnav{display:none}
  .topbar-inner{flex-wrap:nowrap; gap:8px}
  .whoami{max-width:96px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
  body{padding-bottom:calc(66px + env(safe-area-inset-bottom,0px))}
  .fab{bottom:calc(78px + env(safe-area-inset-bottom,0px))}
  .toast{bottom:calc(84px + env(safe-area-inset-bottom,0px))}
}
@media (max-width:479.98px){
  .brand .name{display:none}   /* keep the top bar to one calm row on small phones */
}
/* Desktop: keep the top nav, drop the bottom bar. */
@media (min-width:640px){
  .bottomnav{display:none}
}

/* ---------- one Filters popout on the repertoire ---------- */
.controls-row{display:flex; gap:10px; align-items:stretch}
.controls-row .search{flex:1; min-width:0}
.filters-btn{flex:none; gap:8px; white-space:nowrap}
.filters-btn.has-filters{border-color:var(--accent); color:var(--accent)}
.filter-count{
  background:var(--accent); color:var(--accent-ink); font-size:.78rem; font-weight:800;
  border-radius:999px; min-width:20px; height:20px; padding:0 6px;
  display:inline-flex; align-items:center; justify-content:center;
}
/* A flex/inline-flex display would otherwise beat the [hidden] attribute, so
   name the hidden case explicitly (also fixes the player's transport row). */
.filter-count[hidden],.transport[hidden]{display:none}
.filters-modal{max-width:460px}
.filter-group{margin-bottom:18px}
.filter-group .filter-label{font-size:.95rem; font-weight:800; margin:0 0 10px; color:var(--text)}

/* ---------- toast with an inline action (undo) ---------- */
.toast.has-action{display:inline-flex; align-items:center; gap:16px; pointer-events:auto}
.toast-action{
  appearance:none; background:transparent; border:0; color:var(--accent);
  font-weight:800; font-size:1rem; cursor:pointer; padding:2px 4px;
  text-decoration:underline; text-underline-offset:3px;
}

/* ---------- styled confirm (reuses the leader-modal look) ---------- */
.confirm-modal{max-width:440px}
.confirm-msg{color:var(--muted); font-size:1rem; line-height:1.5; margin:-2px 0 16px}
.confirm-suppress{display:flex; align-items:center; gap:8px; margin:-6px 0 14px; font-size:.88rem; color:var(--muted); cursor:pointer; user-select:none}
.confirm-suppress input{width:18px; height:18px; min-height:0; margin:0; flex:none; cursor:pointer; accent-color:var(--danger)}

/* ---------- consistent loading state (spinner + text) ---------- */
.empty.loading{display:flex; flex-direction:column; align-items:center; gap:12px}
.empty.loading::before{
  content:""; width:26px; height:26px; border-radius:50%;
  border:3px solid var(--border); border-top-color:var(--accent);
  animation:spin .8s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}

/* ---------- v160: the boot bar (UI.loadingStart / loadingDone) ----------
   A 3px indeterminate sweep pinned to the very top edge while a tool page is
   loading. Sits above the topbar (z 20) and below every modal/toast (90+), so
   it can never cover anything that asks for a tap. pointer-events:none so it
   never intercepts one either. `.done` fades it out before ui.js removes it. */
.boot-bar{position:fixed; top:env(safe-area-inset-top,0px); left:0; right:0; height:3px; z-index:85;
  background:rgba(232,144,63,.16); overflow:hidden; pointer-events:none;
  transition:opacity .35s ease}
.boot-bar i{position:absolute; top:0; bottom:0; left:-45%; width:45%;
  background:var(--accent); border-radius:2px; animation:bootslide 1.15s ease-in-out infinite}
.boot-bar.done{opacity:0}
@keyframes bootslide{to{left:100%}}
@media (prefers-reduced-motion:reduce){.boot-bar i{animation-duration:2.6s}}
/* The reassurance line, appended INSIDE the page's own loading block after a
   few seconds (see bootNote in ui.js), so it lands under the spinner where the
   eyes already are. Muted and small: it's a "still alive", not an alert. */
.boot-slow{font-size:.9rem; color:var(--muted); max-width:34ch; text-align:center; line-height:1.4}

/* ---------- keyboard trap: lift bottom-sheet modals above the keyboard ---------- */
/* --kb is published by ui.js from the VisualViewport; 0 on desktop. */
.modal-back{padding-bottom:var(--kb,0px)}
.modal{max-height:min(92vh, calc(100dvh - var(--kb,0px) - 20px))}
/* Pin the picker's search to the top of the sheet so the keyboard can't hide it. */
.pick-search{position:sticky; top:0; z-index:2; background:var(--surface); padding-top:2px}

/* ---------- motion (subtle; disabled under prefers-reduced-motion) ---------- */
.modal-back{opacity:0; transition:opacity .18s ease}
.modal-back.open{opacity:1}
.modal{transform:translateY(10px); transition:transform .2s ease}
.modal-back.open .modal{transform:none}
.votebtn{transition:transform .07s ease, background .15s ease, border-color .15s ease, color .15s ease}
.chip{transition:background .15s ease, border-color .15s ease, color .15s ease}
.att-chip{transition:background .15s ease, border-color .15s ease, color .15s ease}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.001ms !important; animation-iteration-count:1 !important;
    transition-duration:.001ms !important; scroll-behavior:auto !important;
  }
  .modal{transform:none}
}

/* ---------- print + stage mode: hide the new chrome too ---------- */
@media print{
  .bottomnav,.skip-link{display:none !important}
}

/* ---------- stacking order fix (Phase 6) ----------
   The bottom tab bar (z-index 70) must sit BELOW modals/toasts, or it covers a
   bottom-sheet modal's footer buttons on phones. Lift the modal layer and the
   toast above it; the attendance popover too. */
.modal-back{z-index:90}
.toast{z-index:92}
.att-pop{z-index:95}

/* =====================================================================
   Phase 6.1 — follow-up tweaks
   Sort + decade filters, search clear button, toast width fix.
   ===================================================================== */

/* ---------- toast width fix ----------
   A fixed element with only left:50% shrink-to-fits to ~half the viewport,
   so a longer "Removed …" message wrapped to several narrow lines. width:
   max-content lets it size to its text, capped so it never overflows. */
.toast{ width:max-content; max-width:calc(100vw - 32px); text-align:center; }

/* ---------- search clear (×) button ---------- */
.search-clear{
  position:absolute; right:8px; top:50%; transform:translateY(-50%);
  appearance:none; border:0; background:var(--surface-2); color:var(--muted);
  width:34px; height:34px; border-radius:50%; font-size:.95rem; line-height:1;
  cursor:pointer; display:flex; align-items:center; justify-content:center;
}
.search-clear:hover{color:var(--text)}
.search-clear[hidden]{display:none}
.search input.has-clear{padding-right:46px}

/* ---------- decade disclosure (stays compact until opened) ---------- */
.decade-toggle{gap:8px}
.decade-toggle.has-filters{border-color:var(--accent); color:var(--accent)}
.decade-toggle[aria-expanded=true]{background:var(--surface-2); border-color:var(--accent)}
.decade-chips{margin-top:10px}
.decade-chips[hidden]{display:none}

/* =====================================================================
   Phase 7 — notes overhaul (the feed, attachments, viewer, stage cues)
   ===================================================================== */

/* ---------- 📎 chart badge on song cards ---------- */
.badge.chart{color:var(--accent); border:1px solid var(--border)}

/* ---------- feed lanes + cards ---------- */
.note-feed{margin-bottom:12px}
.note-item{overflow:hidden}
.note-meta{justify-content:space-between}
.note-acts{display:flex; gap:6px; margin-left:auto}
.note-mini{
  appearance:none; background:transparent; border:0; cursor:pointer;
  color:var(--muted); font-size:.8rem; font-weight:700; padding:4px 6px; border-radius:8px;
}
.note-mini:hover{color:var(--text); background:var(--surface)}
.note-mini.note-del:hover{color:var(--danger)}

/* ---------- attachments inside a card ---------- */
.note-att{margin-top:8px}
.note-att-img{
  display:block; width:100%; padding:0; border:1px solid var(--border);
  border-radius:10px; overflow:hidden; background:var(--surface); cursor:zoom-in;
}
.note-att-img img{display:block; width:100%; max-height:340px; object-fit:cover}
.note-att-pdf{
  display:flex; align-items:center; gap:8px;
  background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:8px 10px;
}
.pdf-ic{font-size:1.2rem; flex:0 0 auto}
.pdf-name{flex:1; min-width:0; font-size:.9rem; font-weight:700; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.pdf-size{color:var(--muted); font-weight:600}
.note-att-open{flex:0 0 auto}

/* ---------- composer + inline edit ---------- */
.note-composer{border-top:1px dashed var(--border); padding-top:12px; margin-top:4px}
.attach-btn{cursor:pointer}
.attach-btn input[type=file]{display:none}
.composer-file{
  display:flex; align-items:center; gap:8px; margin-top:8px;
  background:var(--surface-2); border:1px solid var(--border); border-radius:10px; padding:6px 10px;
  font-size:.85rem; color:var(--muted);
}
.composer-file[hidden]{display:none}
.cf-name{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.cf-clear{
  appearance:none; border:0; background:transparent; color:var(--muted);
  cursor:pointer; font-size:.95rem; width:28px; height:28px; border-radius:50%;
}
.cf-clear:hover{color:var(--text); background:var(--surface)}
.note-edit{margin-top:6px}
.note-edit .note-input{min-height:60px}

/* ---------- full-screen attachment viewer (shared: notes + stage) ---------- */
body.att-lock{overflow:hidden}
.att-viewer{
  position:fixed; inset:0; z-index:110; display:none; flex-direction:column;
  background:rgba(10,8,5,.96); color:var(--text);
  padding:calc(env(safe-area-inset-top,0px) + 10px) 14px calc(env(safe-area-inset-bottom,0px) + 14px);
}
.att-viewer.open{display:flex}
.att-bar{display:flex; align-items:center; gap:10px; margin-bottom:10px}
.att-name{font-weight:800; font-size:.95rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.att-actions{margin-left:auto; display:flex; align-items:center; gap:8px}
.att-x{
  appearance:none; background:var(--surface-2); color:var(--text); border:1px solid var(--border);
  border-radius:12px; min-width:52px; min-height:52px; font-size:1.3rem; cursor:pointer;
}
.att-stage{flex:1; min-height:0; display:flex; align-items:center; justify-content:center; overflow:auto}
.att-stage.att-img{cursor:zoom-in}
.att-image{max-width:100%; max-height:100%; object-fit:contain; transition:transform .18s ease}
.att-image.zoomed{max-width:none; max-height:none; width:auto; transform:scale(2); cursor:zoom-out}
.att-frame{width:100%; height:100%; border:0; background:#fff; border-radius:8px}

/* ---------- stage-mode personal cues (Part C) ---------- */
.stage-cues{
  display:flex; flex-direction:column; align-items:center; gap:10px;
  max-width:820px; width:100%; margin:2px auto 0;
  background:var(--accent-soft, rgba(232,144,63,.08)); border:1px solid var(--accent-line, rgba(232,144,63,.35));
  border-radius:16px; padding:clamp(10px,2.4vh,16px) 16px;
}
.stage-cue{
  color:var(--accent); font-weight:800; line-height:1.2;
  font-size:calc(var(--stage-scale,1) * clamp(1.1rem, min(5vw,6vh), 2rem)); text-align:center; white-space:pre-wrap; word-break:break-word;
}
.stage-chart{
  appearance:none; cursor:pointer; min-height:clamp(48px,7vh,56px); padding:0 20px;
  background:var(--accent); color:var(--accent-ink); border:1px solid var(--accent);
  border-radius:14px; font-size:clamp(.95rem,2.4vh,1.1rem); font-weight:800;
}
.stage-peekwrap{display:flex; flex-direction:column; align-items:center; gap:10px; margin-top:12px}
.stage-peek{
  appearance:none; cursor:pointer; min-height:44px; padding:0 18px;
  background:transparent; color:var(--muted); border:1px solid var(--border);
  border-radius:12px; font-size:.9rem; font-weight:700;
}
.stage-peek:hover{color:var(--text)}
.stage-band{display:flex; flex-direction:column; gap:10px; align-items:center}
.stage-band .stage-chart{
  background:transparent; color:var(--accent); border-color:var(--accent); font-size:1rem; min-height:52px;
}

/* cues in the landscape split: left-aligned + compact */
@media (orientation:landscape) and (max-height:600px){
  .stage-cues{align-items:flex-start; padding:10px 14px; margin-top:0}
  .stage-cue{text-align:left; font-size:calc(var(--stage-scale,1) * clamp(1rem, min(3vw,7vh), 1.6rem))}
  .stage-peekwrap{align-items:flex-start; margin-top:8px}
  .stage-band{align-items:flex-start}
}

/* ---------- Phase 9: data-model sync (form groups + new badges) ---------- */
.form-section{
  font-size:.72rem; letter-spacing:.08em; text-transform:uppercase;
  color:var(--accent); font-weight:800; margin:20px 0 10px;
  padding-bottom:5px; border-bottom:1px solid var(--border);
}
.lbl-note{color:var(--muted); font-weight:400; font-size:.82rem}

.badge.bpm{color:var(--muted)}
.badge.capo{background:#26302a; color:#bfe0c0}
.badge.explicit{background:var(--danger); color:#fff; font-weight:800; padding:3px 8px}
.badge.warn{color:var(--danger); border-color:var(--danger)}
.badge.status{border:none; color:var(--accent-ink); font-weight:800}
.badge.status.s-new{background:#6b8fb5}
.badge.status.s-learning{background:#d9a441}
.badge.status.s-rough{background:var(--danger); color:#fff}
.badge.status.s-polishing{background:#9ccf8e}
.badge.status.s-gigready{background:var(--good)}
.badge.status.s-shelved{background:#6d6355; color:var(--text)}

/* ---------- Phase 10: practice-tool status ---------- */
/* `.qflags` (the row that carried key + readiness) is DELETED as of v137. Batch 135
   moved readiness into the row's expand and v137 folded the key into `.qsub`, so it
   was a flex row rendering one chip and charging a full line of height for it. If
   the practice row ever needs a second glance-level chip, put it in `.qsub` beside
   the key — don't bring back a row. */
/* Status shown as a tappable chip; the native <select> opens its menu in one tap. */
.qstatus-chip{
  width:auto; min-width:0; min-height:36px; padding:4px 8px 4px 12px;
  border-radius:999px; font-weight:800; font-size:.8rem; cursor:pointer;
  background:var(--surface-2); color:var(--text); border:1px solid var(--border);
}
.qstatus-chip.unset{border-style:dashed; color:var(--muted)}
.qstatus-chip.s-new{background:#6b8fb5; color:var(--accent-ink); border-color:transparent}
.qstatus-chip.s-learning{background:#d9a441; color:var(--accent-ink); border-color:transparent}
.qstatus-chip.s-rough{background:var(--danger); color:#fff; border-color:transparent}
.qstatus-chip.s-polishing{background:#9ccf8e; color:var(--accent-ink); border-color:transparent}
.qstatus-chip.s-gigready{background:var(--good); color:var(--accent-ink); border-color:transparent}
.qstatus-chip.s-shelved{background:#6d6355; color:var(--text); border-color:transparent}
.qarr{display:flex; align-items:center; gap:8px; font-size:.9rem; color:var(--muted); margin:10px 0 0}
.qarr input{width:22px; height:22px; min-height:0}
.qlast{margin-top:8px; color:var(--muted); font-size:.85rem}
.pick-needswork{display:flex; align-items:center; gap:8px; margin:2px 0 10px; font-weight:700; font-size:.95rem}
.pick-needswork input{width:22px; height:22px; min-height:0}
.picktitle .badge.status{margin-left:6px; font-size:.72rem; padding:2px 7px}

/* ---------- Phase 11: mobile-compact repertoire card ---------- */
/* Desktop shows everything (default). The toggle is hidden and .sec badges/actions display normally. */
.song .row1{align-items:flex-start}
.song .row1-main{flex:1; min-width:0}
.song-toggle{
  display:none; flex:0 0 auto; width:40px; min-height:40px; align-items:center; justify-content:center;
  border:1px solid var(--border); background:var(--surface-2); color:var(--muted);
  border-radius:10px; cursor:pointer; font-size:1rem; padding:0;
}
.st-caret{display:inline-block; transition:transform .12s ease}
.song.open .st-caret{transform:rotate(90deg)}

/* Phase 16: the readiness chip rides on the title row, right of the name.
   It reuses the practice planner's .qstatus-chip styling (pill shape + status
   colors); this rule only pins where it sits — top-aligned with the title,
   never stretched. row1's flex + gap handle the spacing; row1-main (flex:1,
   min-width:0) wraps the title/artist so the chip never gets crowded off. */
.song .row1 > .qstatus-chip{ flex:0 0 auto; align-self:flex-start; }

/* Phone / narrow: default to a compact card — hide secondary badges + actions until expanded. */
@media (max-width:639.98px){
  .song .badges .sec{display:none}
  .song .actions .sec{display:none}
  .song.open .badges .sec{display:inline-flex}
  .song.open .actions .sec{display:inline-flex}
  .song-toggle{display:inline-flex}
  /* Phase 32b: keep the repertoire action row on ONE line on phones — drop the
     button emoji, and shorten "Practice Log" to just "Log". */
  .song .actions .lb-ico,
  .song .actions .lb-long{display:none}
}

/* =====================================================================
   Phase 14: the working list (a wide-screen staging rail)
   A holding pool for songs chosen for the show but not yet placed in a set.
   It's a spread-out task that wants elbow room, so the whole thing is
   reserved for >=1024px; below that it's hidden and the tool is unchanged.
   ===================================================================== */
body{ --wl-w:340px; --wl-tab:36px; }
.wl-rail{ display:none; }                 /* off by default (narrow + no setlist) */
.wl-tab{ display:none; }
.sset-songs.wl-drop{                       /* a set lit up as a drop target */
  outline:3px dashed var(--accent); outline-offset:4px; border-radius:10px;
  background:rgba(232,144,63,.06);
}
/* An "in list" hint in the add-song picker, next to any "in Set N" tag. */
.pickin.pickin-list{ color:var(--accent); border-color:var(--accent); }

@media (min-width:1024px){
  /* Center the content column (setlists OR practice) in the space that's LEFT
     beside the rail — not shoved against it. Clamp its width so it fits the
     remaining band, keep it auto-centered, then nudge it by half the rail width
     so its centre lands in the middle of that band. main has no fixed-position
     children (the rail, modals and player dock all live on <body>), so the
     transform can't reparent an overlay. */
  body.has-wl.wl-left  .setlist-wrap, body.has-wl.wl-left  .planner-wrap,
  body.has-wl.wl-right .setlist-wrap, body.has-wl.wl-right .planner-wrap{
    max-width:min(var(--maxw), calc(100% - var(--wl-w) - 28px - 16px));
    margin-left:auto; margin-right:auto;
  }
  body.has-wl.wl-left  .setlist-wrap, body.has-wl.wl-left  .planner-wrap{ transform:translateX(calc((var(--wl-w) + 28px) / 2)); }
  body.has-wl.wl-right .setlist-wrap, body.has-wl.wl-right .planner-wrap{ transform:translateX(calc((var(--wl-w) + 28px) / -2)); }
  /* Collapsed: the rail shrinks to a slim edge tab (a fixed overlay), so restore
     the normal full-width centering across the whole viewport. */
  body.has-wl.wl-collapsed.wl-left  .setlist-wrap, body.has-wl.wl-collapsed.wl-left  .planner-wrap,
  body.has-wl.wl-collapsed.wl-right .setlist-wrap, body.has-wl.wl-collapsed.wl-right .planner-wrap{
    max-width:var(--maxw); margin-left:auto; margin-right:auto; transform:none;
  }

  body.has-wl .wl-rail{
    display:flex; flex-direction:column;
    position:fixed; top:70px; bottom:0; width:var(--wl-w);
    z-index:15; background:var(--surface);
  }
  body.has-wl.wl-left  .wl-rail{ left:0;  border-right:1px solid var(--border); }
  body.has-wl.wl-right .wl-rail{ right:0; border-left:1px solid var(--border); }
  body.has-wl.wl-collapsed .wl-rail{ width:var(--wl-tab); background:transparent; border:none; }

  /* Collapsed: hide the panel, show a slim vertical tab to re-open it. */
  body.wl-collapsed .wl-panel{ display:none; }
  body.has-wl.wl-collapsed .wl-tab{
    display:flex; align-items:center; justify-content:center;
    height:100%; width:100%; padding:0;
    background:var(--surface); color:var(--muted); cursor:pointer;
    border:0 solid var(--border);
  }
  body.wl-collapsed.wl-left  .wl-tab{ border-right-width:1px; }
  body.wl-collapsed.wl-right .wl-tab{ border-left-width:1px; }
  body.has-wl.wl-collapsed .wl-tab:hover{ color:var(--text); }
  .wl-tab-txt{
    writing-mode:vertical-rl; text-orientation:mixed;
    letter-spacing:.1em; font-weight:800; font-size:.78rem; text-transform:uppercase;
  }
}

.wl-panel{ display:flex; flex-direction:column; height:100%; min-height:0; padding:14px 12px 12px; }
.wl-head{ display:flex; align-items:center; gap:8px; margin-bottom:8px; }
.wl-title{ font-size:.98rem; font-weight:800; letter-spacing:.02em; }
.wl-count{ color:var(--muted); font-weight:700; }
.wl-head-btns{ margin-left:auto; display:flex; gap:4px; }
.wl-hbtn{
  appearance:none; border:1px solid var(--border); background:var(--surface-2);
  color:var(--muted); border-radius:8px; width:32px; height:32px; min-height:0;
  cursor:pointer; font-size:.95rem; display:flex; align-items:center; justify-content:center; padding:0;
}
.wl-hbtn:hover{ color:var(--text); border-color:var(--muted); }

/* =====================================================================
   BATCH 133 — the Practice check panel, at the top of the working-list rail.
   Leader-only and desktop-only (it inherits the rail's ≥1024px gate), so this
   never has to answer for a phone layout.

   IT WHISPERS. Collapsed it is one tappable line; the body only exists once
   you ask for it. A panel that shouts on every setlist load is one you stop
   reading by the third gig, and the whole point is to be trusted on the fourth.
   Nothing here is a verdict — same house rule as Phase 12's badges.
   ===================================================================== */
.pc{ margin-bottom:10px; }
.pc:empty{ display:none; }          /* nothing to say → take up no space at all */

.pc-head{
  width:100%; appearance:none; text-align:left; cursor:pointer;
  display:flex; align-items:baseline; gap:7px; flex-wrap:wrap;
  background:var(--surface-2); border:1px solid var(--border); border-radius:10px;
  padding:8px 10px; min-height:0; color:var(--text);
}
.pc-head:hover{ border-color:var(--muted); }
/* Open, the head is the LID of the body, not a separate card — square off the
   corners it shares so the two read as one box rather than two stacked ones. */
.pc-head.open{ border-radius:10px 10px 0 0; border-bottom-color:transparent; }
.pc-chev{ color:var(--muted); font-size:.8rem; transition:transform .12s ease; flex:0 0 auto; }
.pc-head.open .pc-chev{ transform:rotate(90deg); }
.pc-title{ font-size:.85rem; font-weight:800; letter-spacing:.02em; flex:0 0 auto; }
/* The summary is the feature most of the time — it wraps rather than truncates,
   because "6 worked on since Jul 27 aren't in this list" only helps if you can
   read all of it. */
.pc-sum{ font-size:.8rem; color:var(--muted); font-weight:600; flex:1 1 100%; }

.pc-body{
  border:1px solid var(--border); border-top:0;
  border-radius:0 0 10px 10px; padding:9px 10px 10px;
}
.pc-window{ font-size:.76rem; color:var(--muted); margin-bottom:9px; line-height:1.4; }
.pc-window strong{ color:var(--text); }

.pc-sec{
  font-size:.72rem; font-weight:800; letter-spacing:.06em; text-transform:uppercase;
  color:var(--muted); margin:10px 0 6px;
}
.pc-sec:first-of-type{ margin-top:0; }
.pc-coldnote{ font-size:.74rem; color:var(--muted); margin:-3px 0 7px; }

.pc-row{
  display:flex; align-items:flex-start; gap:8px;
  background:var(--surface-2); border:1px solid var(--border); border-radius:10px;
  padding:7px 8px; margin-bottom:6px;
}
.pc-main{ flex:1; min-width:0; }
.pc-song{ font-size:.87rem; font-weight:700; line-height:1.25; }
.pc-meta{ font-size:.75rem; color:var(--muted); margin-top:2px; }
/* Batch 134: a gig-check row carries TWO "date · venue" facts — the show it's
   for, and the last time it was out. On one line, separated by the same dot,
   they read as a single confused fact. Stacked, the first line is what you
   triage by and the second is the evidence. */
.pc-for{ display:block; color:var(--text); }
.pc-since{ display:block; opacity:.8; }
.pc-eviction strong{ color:var(--text); }

/* The cold half borrows --danger, the same colour the ⚑ arr and singer-out
   badges already use for "look at this before the show". Deliberately NOT the
   accent: amber means "act on this" everywhere else in the app, and the cold
   list is a risk you might reasonably accept, not a task. */
.pc-sec-cold{ color:var(--danger); }
.pc-row-cold{ border-left:3px solid var(--danger); }

.pc-btns{ display:flex; gap:4px; flex:0 0 auto; }
.pc-add, .pc-skip{
  appearance:none; border:1px solid var(--border); background:var(--chip);
  color:var(--muted); border-radius:8px; width:28px; height:28px; min-height:0;
  padding:0; cursor:pointer; font-size:.9rem; line-height:1;
  display:flex; align-items:center; justify-content:center;
}
.pc-add:hover{ background:var(--accent); color:var(--accent-ink); border-color:var(--accent); }
.pc-skip:hover{ color:var(--text); border-color:var(--muted); }

/* The shared .btn min-height is sized for a page, not a 340px rail — left alone
   this one control is taller than the rows it acts on and reads as the point of
   the panel, which it isn't. The rows are. */
.pc-addall{ margin-top:2px; font-size:.8rem; min-height:0; padding:7px 10px; }
.pc-note{ font-size:.74rem; color:var(--muted); margin-top:8px; line-height:1.4; }
.pc-link{
  appearance:none; background:none; border:0; padding:0; min-height:0;
  color:var(--accent); font-size:inherit; font-weight:700; cursor:pointer;
  text-decoration:underline; text-underline-offset:2px;
}
.pc-link:hover{ color:var(--accent-press); }

.wl-sub{ margin-bottom:9px; }
.wl-stat{ display:flex; align-items:center; gap:8px; flex-wrap:wrap; font-size:.82rem; color:var(--muted); }
.wl-toplace{ color:var(--text); font-weight:800; }
.wl-hideplaced{
  margin-left:auto; appearance:none; border:1px solid var(--border); background:var(--chip);
  color:var(--muted); border-radius:20px; padding:3px 11px; min-height:0;
  font-size:.75rem; font-weight:700; cursor:pointer;
}
.wl-hideplaced[aria-pressed=true]{ background:var(--accent); color:var(--accent-ink); border-color:var(--accent); }
.wl-tally{ display:flex; flex-wrap:wrap; gap:5px; margin-top:9px; }

.wl-list{
  flex:1; min-height:0; overflow-y:auto;
  display:flex; flex-direction:column; gap:8px; padding:2px 2px 6px;
}
.wl-alldone{
  color:var(--muted); font-size:.9rem; text-align:center; padding:16px 8px;
  border:1px dashed var(--border); border-radius:10px;
}

.wl-item{
  background:var(--surface-2); border:1px solid var(--border); border-radius:12px;
  padding:8px 10px; cursor:grab;
}
.wl-item:active{ cursor:grabbing; }
.wl-item.dragging{ opacity:.45; }
.wl-item.placed{ opacity:.6; }
.wl-row{ display:flex; align-items:flex-start; gap:8px; }
.wl-main{ flex:1; min-width:0; }
.wl-song{ font-weight:700; font-size:.95rem; line-height:1.25; word-break:break-word; }
.wl-inset{
  font-size:.66rem; font-weight:800; color:var(--good); border:1px solid var(--good);
  border-radius:20px; padding:1px 7px; margin-left:4px; white-space:nowrap; vertical-align:middle;
}
.wl-sub2{ color:var(--muted); font-size:.82rem; margin-top:3px; word-break:break-word; }
.wl-nosinger{ font-style:italic; opacity:.8; }
.wl-btns{ display:flex; flex-direction:column; gap:5px; flex:0 0 auto; }
.wl-assign, .wl-remove{
  appearance:none; border:1px solid var(--border); background:var(--surface); color:var(--muted);
  border-radius:8px; width:30px; height:30px; min-height:0; cursor:pointer;
  font-size:1rem; display:flex; align-items:center; justify-content:center; padding:0;
}
.wl-assign:hover{ color:var(--accent); border-color:var(--accent); }
.wl-remove:hover{ color:var(--danger); border-color:var(--danger); }

.wl-menu{
  display:flex; flex-wrap:wrap; gap:6px; align-items:center;
  margin-top:9px; padding-top:9px; border-top:1px dashed var(--border);
}
.wl-menu-set{
  appearance:none; border:1px solid var(--accent); background:transparent; color:var(--accent);
  border-radius:20px; padding:4px 13px; min-height:0; font-weight:800; font-size:.82rem; cursor:pointer;
}
.wl-menu-set:hover{ background:var(--accent); color:var(--accent-ink); }
.wl-menu-cancel{
  appearance:none; border:1px solid var(--border); background:transparent; color:var(--muted);
  border-radius:50%; width:30px; height:30px; min-height:0; cursor:pointer; margin-left:auto; padding:0;
}
.wl-menu-cancel:hover{ color:var(--text); border-color:var(--muted); }

.wl-add{ margin-top:10px; flex:0 0 auto; border-style:dashed; }

/* ---------- Phase 15: practice planner drag + staleness ---------- */
/* Drag handle on a practice-set row (mirrors the setlist .sdrag). */
.qdrag{
  cursor:grab; color:var(--muted); font-size:1.2rem; line-height:1; flex:none;
  padding:2px 2px 0; touch-action:none; user-select:none;
}
.qdrag:active{ cursor:grabbing; }
.qitem.dragging{ opacity:.45; }
/* v174 — same story as `.sdrag` above, same fix. → ipad_reorder_deadend */
@media (hover:none){ .qdrag{ display:inline-flex; width:38px; } }
/* The practice set lit up as a drop target for a working-list song. */
.queue.wl-drop{
  outline:3px dashed var(--accent); outline-offset:4px; border-radius:12px;
  background:rgba(232,144,63,.06);
}
/* BATCH 131 / v132 — the "last played out" pill. Picker sub-lines in BOTH
   tools, plus both working-list rails.

   It shipped in 131 as italic + dimmed, because it was then the SECOND date on
   a practice-picker row and deference was right. v132 removed the other one
   (see renderPickList in planner.js), so it is now the only date on the row —
   and a dimmed italic was actively wrong for the fact the row is chosen on.
   Promoted to the weight .pickstale used to carry.

   THE RULE THAT SURVIVED THE CHANGE: one date per row. If a second ever gets
   added back here, this styling has to be re-litigated, not just inherited.

   There is no "never played" state to style — the pill isn't rendered at all
   without gig history (the silence rule, ui.js). */
.pickplayed{ color:var(--muted); font-weight:700; }
/* In the rails it sits among singer names and out-flags rather than alone, so
   it steps back a little — the row's job there is identity, not recency. */
.wl-sub2 .pickplayed{ font-size:.92em; font-weight:600; }

/* --- Phase 17: collapsible ideas + Accepted/Declined at-a-glance --- */
/* Open idea cards collapse to a scannable row; the toggle lives top-right. */
.nom.open{position:relative}
.nom.open .nom-main{padding-right:42px}
.nom-toggle{
  position:absolute; top:12px; right:10px;
  appearance:none; background:var(--surface-2); border:1px solid var(--border);
  color:var(--muted); width:32px; height:32px; border-radius:9px; cursor:pointer;
  display:flex; align-items:center; justify-content:center; line-height:1;
}
.nom-toggle .chev{font-size:1rem; transition:transform .18s ease}
.nom.expanded .nom-toggle .chev{transform:rotate(180deg)}
.nom-toggle:active{transform:scale(.94)}
.nom.collapsed .nom-expand{display:none}

/* Collapsed warning cue — a concern is hiding below, worth a look. */
.nom-concern-cue{
  display:inline-flex; align-items:center; gap:5px; margin-top:10px;
  font-size:.82rem; font-weight:700; color:var(--danger);
  background:rgba(229,103,95,.11); border:1px solid var(--danger);
  border-radius:999px; padding:3px 10px;
}
.nom-concern-cue b{font-weight:800}
.nom.expanded .nom-concern-cue{display:none}   /* redundant once chips show */

/* Collapsed chatter cue (Phase 21) — quieter sibling of the concern cue: this
   idea has comments. Muted, no alarm color; the thread is one tap away. */
.nom-comment-cue{
  display:inline-flex; align-items:center; gap:5px; margin-top:10px; margin-left:6px;
  font-size:.82rem; font-weight:700; color:var(--muted);
  background:var(--surface-2); border:1px solid var(--border);
  border-radius:999px; padding:3px 10px;
}
.nom-comment-cue b{font-weight:800; color:var(--text)}
.nom.expanded .nom-comment-cue{display:none}   /* the thread itself shows on expand */

/* Leader-only "who voted", surfaced in the expanded card (the veil stays on
   the collapsed count). */
.nom-voternames{margin-top:10px; font-size:.85rem; color:var(--muted); line-height:1.5}
.nom-voternames .up-names{color:var(--good); font-weight:700}
.nom-voternames .down-names{color:var(--danger); font-weight:700}

/* Accepted = green, Declined = red, on the settled cards' left edge too. */
.nom.decided.added{border-left-color:var(--good)}
.nom.decided.passed{border-left-color:var(--danger)}

/* Whole open card is a click target for expand/collapse (Phase 17.1); keep the
   comment area and text inputs on their normal cursors so reading/typing there
   doesn't read as "click to fold." */
.nom.open{cursor:pointer}
.nom.open textarea, .nom.open input{cursor:text}
.nom.open .nom-comments{cursor:default}

/* =====================================================================
   Phase 19 — "what I play" picker · the stem player component · docked player
   ===================================================================== */

/* Topbar name is now a button that opens the instrument picker. */
.whoami{background:transparent; border:0; padding:6px 8px; border-radius:10px; cursor:pointer; font:inherit; color:var(--muted); max-width:180px}
.whoami:hover{background:var(--surface-2); color:var(--text)}
.whoami .whoami-name{white-space:nowrap; overflow:hidden; text-overflow:ellipsis}

/* Instrument picker chips */
.whoami-modal{max-height:82vh; overflow:auto}
.whoami-opts{display:flex; flex-wrap:wrap; gap:8px; margin:4px 0}
.whoami-chip{appearance:none; border:1px solid var(--border); background:var(--chip); color:var(--text);
  min-height:var(--tap); padding:0 16px; border-radius:var(--radius); font-weight:700; font-size:1rem; cursor:pointer}
.whoami-chip.on{background:var(--accent); color:var(--accent-ink); border-color:var(--accent)}
.whoami-chip.sm{min-height:38px; padding:0 12px; font-size:.9rem}

/* Leader-only roster: set a bandmate's instruments for them */
.whoami-roster{margin-top:14px; border-top:1px solid var(--border); padding-top:12px}
.whoami-rhd{display:flex; align-items:center; gap:8px; font-weight:800; margin-bottom:10px}
.whoami-rsub{font-size:.7rem; font-weight:700; color:var(--muted); background:var(--chip); border-radius:6px; padding:1px 6px; text-transform:uppercase}
.whoami-rrow{display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:12px}
.whoami-rname{flex:0 0 76px; font-weight:700}
.whoami-ropts{display:flex; flex-wrap:wrap; gap:6px}
.whoami-rvox{flex-basis:100%; padding-left:86px}   /* v172: the swatch row under a singer's chips */
.whoami-rloading{color:var(--muted); font-size:.9rem}
.whoami-rerr{color:var(--danger); font-size:.9rem}

/* Phase 28 — leader "play as" switcher (sits above the instruments). */
.whoami-switch{ margin-bottom:14px; }
.whoami-swap-list{ display:flex; flex-direction:column; gap:8px; }
.whoami-swap{
  appearance:none; cursor:pointer; text-align:left; width:100%;
  display:flex; align-items:center; gap:10px; min-height:var(--tap); padding:8px 14px;
  border:1px solid var(--border); background:var(--chip); color:var(--text);
  border-radius:var(--radius); font-weight:700; font-size:1rem;
  transition:background .12s ease, border-color .12s ease;
}
.whoami-swap:hover{ border-color:var(--accent); }
.whoami-swap-name{ flex:1 1 auto; min-width:0; }
.whoami-swap-go{ color:var(--muted); font-weight:800; }
.whoami-swap-you{ font-size:.7rem; font-weight:800; text-transform:uppercase; color:var(--muted);
  background:var(--surface-2); border:1px solid var(--border); border-radius:6px; padding:1px 7px; }
.whoami-swap.current{ cursor:default; opacity:.75; }
.whoami-swap.current:hover{ border-color:var(--border); }
/* Armed (first tap): highlight + a "tap again" cue so a stray tap can't swap you. */
.whoami-swap.armed{ background:var(--accent); color:var(--accent-ink); border-color:var(--accent); }
.whoami-swap.armed .whoami-swap-go{ color:var(--accent-ink); }
.whoami-swap.armed .whoami-swap-name::after{ content:" — tap again to switch"; font-weight:700; opacity:.85; }
.whoami-swap.switching{ opacity:.6; }

/* Collapsible instruments header (leader only). */
.whoami-instr-tog{
  appearance:none; cursor:pointer; width:100%;
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  min-height:44px; padding:6px 2px; margin-top:4px;
  background:transparent; border:0; border-top:1px solid var(--border);
  color:var(--text); font-weight:800; font-size:1rem; font-family:inherit;
}
.whoami-instr-tog .wi-caret{ color:var(--muted); transition:transform .15s ease; }
.whoami-instr-tog[aria-expanded="false"] .wi-caret{ transform:rotate(-90deg); }
.whoami-instr-body[hidden]{ display:none; }

/* Account dialog footer: Sign out (left, danger-tinted) + Done (right). Tapping
   Sign out swaps the footer for a small inline confirm strip. NB .modal-actions
   sets display:flex, which beats the UA [hidden] rule — so guard it explicitly. */
.whoami-signout{ color:var(--danger); }
#whoamiActions[hidden]{ display:none; }
.whoami-signout-confirm{
  display:flex; align-items:center; justify-content:space-between; gap:10px 14px;
  flex-wrap:wrap; margin-top:10px;
}
.whoami-signout-confirm[hidden]{ display:none; }
.wsc-msg{ font-weight:800; }
.wsc-btns{ display:flex; gap:8px; flex:1 1 auto; justify-content:flex-end; }

/* ---- stem player component (powers the page AND the dock) ---- */
.sp{display:block}
.sp-boot{color:var(--muted); padding:14px 2px}
.sp-err{color:var(--danger)}
.sp-empty{color:var(--muted); padding:14px 2px}
.sp-head{display:flex; align-items:flex-start; gap:10px; margin:2px 0 14px}
.sp-title{font-size:1.3rem; font-weight:800; line-height:1.2; flex:1; min-width:0}
.sp-sub{display:block; color:var(--muted); font-size:.9rem; font-weight:600; margin-top:3px}
.sp-close{appearance:none; border:1px solid var(--border); background:var(--surface-2); color:var(--text);
  width:40px; height:40px; border-radius:10px; cursor:pointer; font-size:1rem; flex:0 0 auto}
.sp-close:hover{background:var(--chip)}

.sp-load{display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:8px}
.sp-loadnote{color:var(--muted); font-size:.9rem}
.sp-loadnote.err{color:var(--danger); font-weight:700}

.sp-transport{display:flex; flex-wrap:wrap; align-items:center; gap:12px; margin:4px 0 6px}
.sp-transport-row{display:flex; gap:8px; align-items:center}
.sp-tbtn{min-height:44px; padding:0 12px}

.sp-seekwrap{position:relative; margin:8px 0 4px}
.sp-marks{position:absolute; left:0; right:0; top:-3px; height:16px; pointer-events:none}
.sp-marks .sp-mark{position:absolute; top:1px; width:2px; height:14px; background:var(--muted); opacity:.7; transform:translateX(-1px); pointer-events:auto; cursor:pointer}
.sp-marks .sp-mark:hover{background:var(--text); opacity:1}
.sp-marks .sp-ab{position:absolute; top:0; width:3px; height:16px; transform:translateX(-1px)}
.sp-marks .sp-ab.a{background:var(--good)}
.sp-marks .sp-ab.b{background:var(--accent)}
.sp-marks .sp-loopband{position:absolute; top:3px; height:10px; background:rgba(232,144,63,.22); border-radius:3px}
.sp-marks .sp-playhead{position:absolute; top:-1px; width:2px; height:18px; background:var(--text)}
.sp-seek{width:100%; height:26px; accent-color:var(--accent); cursor:pointer}
.sp-time{display:flex; justify-content:space-between; color:var(--muted); font-size:.85rem; margin-top:2px; font-variant-numeric:tabular-nums}

.sp-loopbar{display:flex; flex-wrap:wrap; align-items:center; gap:6px; margin:6px 0 10px}
.sp-lbtn{appearance:none; border:1px solid var(--border); background:var(--surface-2); color:var(--text);
  min-height:40px; padding:0 12px; border-radius:10px; font-weight:700; cursor:pointer}
.sp-lbtn:hover{background:var(--chip)}
.sp-lbtn:disabled{opacity:.5; cursor:default}
.sp-looptoggle.on{background:var(--accent); color:var(--accent-ink); border-color:var(--accent)}
.sp-loopread{color:var(--muted); font-size:.85rem; margin:0 4px; font-variant-numeric:tabular-nums}
.sp-saveloop{margin-left:auto}

.sp-tracks{display:flex; flex-direction:column; gap:10px}
.sp-track{display:flex; align-items:center; gap:12px; background:var(--surface); border:1px solid var(--border);
  border-left:5px solid var(--accent); border-radius:12px; padding:10px 12px}
.sp-track.silenced{border-left-color:var(--border); opacity:.55}
.sp-track.soloed{border-left-color:var(--focus); opacity:1}
.sp-track-name{flex:0 0 96px; font-weight:800; font-size:1rem; display:flex; align-items:center; gap:6px}
.sp-yours{font-size:.62rem; font-weight:800; text-transform:uppercase; letter-spacing:.04em;
  background:var(--chip); color:var(--muted); border-radius:6px; padding:2px 5px}
.sp-track-ctrls{display:flex; align-items:center; gap:10px; flex:1; min-width:0}
.sp-cbtn{appearance:none; border:1px solid var(--border); background:var(--surface-2); color:var(--text);
  min-height:40px; padding:0 12px; border-radius:10px; font-weight:700; cursor:pointer; min-width:62px}
.sp-cbtn.mute[aria-pressed=true]{background:var(--danger); color:#1a0605; border-color:var(--danger)}
.sp-cbtn.solo[aria-pressed=true]{background:var(--focus); color:var(--accent-ink); border-color:var(--focus)}
.sp-vol{flex:1; min-width:60px; accent-color:var(--accent); height:24px; cursor:pointer}
.sp-hint{color:var(--muted); font-size:.88rem; margin:12px 2px 0}

.sp-drawer{margin-top:14px; border-top:1px solid var(--border); padding-top:10px}
.sp-drawer-toggle{appearance:none; border:0; background:transparent; color:var(--accent); font-weight:800;
  cursor:pointer; padding:4px 0; font-size:.95rem}
.sp-drawer-toggle::before{content:'\25be  '}
.sp-drawer.collapsed .sp-drawer-toggle::before{content:'\25b8  '}
.sp-drawer.collapsed .sp-drawer-body{display:none}
.sp-drawer-body{display:flex; gap:20px; flex-wrap:wrap; margin-top:8px}
.sp-dsec{flex:1; min-width:220px}
.sp-dhd{display:flex; align-items:center; gap:8px; font-weight:800; margin-bottom:6px}
.sp-dhd-sub{font-size:.7rem; font-weight:700; color:var(--muted); background:var(--chip); border-radius:6px; padding:1px 6px; text-transform:uppercase}
.sp-dadd{margin-left:auto; appearance:none; border:1px solid var(--border); background:var(--surface-2); color:var(--text); border-radius:8px; padding:6px 10px; font-weight:700; cursor:pointer}
.sp-dadd:disabled{opacity:.5; cursor:default}
.sp-nameform{display:flex; gap:6px; margin:6px 0}
.sp-nameform input{flex:1; min-height:40px; background:var(--surface); border:1px solid var(--border); color:var(--text); border-radius:10px; padding:0 10px}
.sp-dlist{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:4px}
.sp-di{display:flex; align-items:center; gap:6px}
.sp-di-jump{flex:1; text-align:left; appearance:none; border:1px solid transparent; background:var(--surface); color:var(--text); border-radius:8px; padding:8px 10px; cursor:pointer; min-width:0}
.sp-di-jump:hover{border-color:var(--border)}
.sp-di-jump:disabled{opacity:.5; cursor:default}
.sp-di-time{color:var(--muted); font-variant-numeric:tabular-nums; font-size:.82rem; margin-right:6px}
.sp-di-acts{display:flex; gap:4px}
.sp-di-loop,.sp-di-ren,.sp-di-del{appearance:none; border:1px solid var(--border); background:var(--surface-2); color:var(--text); width:36px; height:36px; border-radius:8px; cursor:pointer}
.sp-di-loop:hover,.sp-di-ren:hover,.sp-di-del:hover{background:var(--chip)}
.sp-di-empty{color:var(--muted); font-size:.88rem; padding:6px 2px}

.player-mount{margin:4px 0 22px}

/* compact (dock) tweaks */
.sp-compact .sp-title{font-size:1.05rem}
.sp-compact .sp-track-name{flex-basis:76px; font-size:.92rem}
.sp-compact .sp-drawer{margin-top:10px}

/* ---- docked practice player ---- */
.dockplayer{position:fixed; left:0; right:0; bottom:0; z-index:60;
  background:var(--surface); border-top:1px solid var(--border);
  box-shadow:0 -8px 24px rgba(0,0,0,.35);
  padding:10px 14px calc(10px + env(safe-area-inset-bottom,0px));
  max-height:74vh; overflow-y:auto; overflow-x:hidden}
.dock-nav{display:flex; align-items:center; justify-content:center; gap:14px; margin-bottom:4px}
.dock-navbtn{appearance:none; border:1px solid var(--border); background:var(--surface-2); color:var(--text);
  min-height:40px; min-width:48px; border-radius:10px; cursor:pointer; font-size:1rem}
.dock-navbtn:disabled{opacity:.4; cursor:default}
.dock-pos{color:var(--muted); font-size:.85rem; font-variant-numeric:tabular-nums; min-width:48px; text-align:center}
.dock-body{max-width:var(--maxw); margin:0 auto}
body.has-dock{padding-bottom:44vh}

/* On phones the bottom tab bar owns the very bottom; sit the dock just above it. */
@media (max-width:639.98px){
  .dockplayer{bottom:calc(66px + env(safe-area-inset-bottom,0px)); padding-bottom:10px}
}

/* =====================================================================
   Phase 20 — Player UI pass
   Adds a dedicated TEAL "playable" lane (kept clear of the red-yellow-green
   readiness axis), a title-row practice button, a real load progress bar,
   a −30/−10/play/+10/+30 scrub row, a two-column player, quick loop/section
   chips, and a quieter per-track volume. Rules here intentionally come last
   so they override the earlier player styles they replace.
   ===================================================================== */
:root{
  --play:#37c2d4;          /* teal — "this is playable", never a readiness state */
  --play-press:#28a7b8;
  --play-ink:#04252b;      /* dark text on a teal fill */
  --play-soft:rgba(55,194,212,.14);
}

/* ---------- queue: practice button in the title row ---------- */
/* Batch 135: ▶ YouTube joined the 🎧 player up here, so the title row can now hold
   two buttons. flex-wrap + a percentage basis on the head means a long title keeps
   its width and pushes the buttons onto their own line rather than crushing them —
   Alex's call, and the right one: "Everybody Wants To Rule The World" wrapping is
   fine, a 40px-wide YouTube button is not. */
.qtop{display:flex; align-items:center; gap:10px; flex-wrap:wrap}
.qtop .qhead{flex:1 1 55%; min-width:0}
.qplay{
  flex:0 0 auto; appearance:none; cursor:pointer;
  display:inline-flex; align-items:center; gap:6px;
  min-height:44px; padding:0 14px; border-radius:999px;
  background:var(--play); color:var(--play-ink); border:1px solid var(--play);
  font-weight:800; font-size:.9rem; line-height:1;
  transition:transform .06s ease, background .15s ease, border-color .15s ease;
}
.qplay:hover{background:var(--play-press); border-color:var(--play-press)}
.qplay:active{transform:scale(.96)}
.qplay .hp-ico{width:20px; height:20px; display:block}
.qplay-txt{white-space:nowrap}
.qitem.done .qplay{opacity:.7}
@media(max-width:430px){ .qplay-txt{display:none} .qplay{padding:0 12px} }

/* ---------- player: load status / progress ---------- */
.sp-status{margin:2px 0 12px}
.sp-bar{height:8px; border-radius:999px; background:var(--surface-2); overflow:hidden; border:1px solid var(--border)}
.sp-bar-fill{display:block; position:relative; overflow:hidden; height:100%; width:0; background:var(--play); border-radius:999px; transition:width .2s ease}
/* Phase 44: a moving shimmer over the fill while loading, so a stalled chunk still
   reads as "working," never frozen. Removed the instant we flip to Ready. */
.sp-loading-anim .sp-bar-fill::after{
  content:''; position:absolute; inset:0;
  background:linear-gradient(90deg, transparent, rgba(255,255,255,.35), transparent);
  transform:translateX(-100%); animation:sp-shimmer 1.15s linear infinite;
}
@keyframes sp-shimmer{ to{ transform:translateX(100%); } }
@media (prefers-reduced-motion:reduce){ .sp-loading-anim .sp-bar-fill::after{ animation:none; } }
.sp-status-line{display:flex; align-items:center; gap:10px; margin-top:6px}
.sp-badge{font-size:.72rem; font-weight:800; padding:3px 10px; border-radius:999px; text-transform:uppercase; letter-spacing:.04em}
.sp-badge.loading{background:#3a2f1c; color:var(--focus); border:1px solid #5a4a2a}   /* amber "not ready yet" */
.sp-badge.ready{background:var(--good); color:#0e2409}
.sp-badge.err{background:var(--danger); color:#2a0906}
.sp-status-note{color:var(--muted); font-size:.82rem; font-variant-numeric:tabular-nums}
.sp-status-retry{cursor:pointer}
.sp-status-done{opacity:0; height:0; margin:0; overflow:hidden;
  transition:opacity .4s ease, height .4s ease, margin .4s ease}

/* ---------- player: transport + scrub ---------- */
.sp-transport{display:flex; flex-direction:column; gap:10px; margin:4px 0 2px}
.sp-scrub{display:flex; justify-content:center; align-items:stretch; gap:8px; flex-wrap:nowrap}
.sp-scrubbtn{
  appearance:none; cursor:pointer; border:1px solid var(--border); background:var(--surface-2); color:var(--text);
  min-height:50px; border-radius:12px; padding:0 14px; font-weight:700;
  display:inline-flex; align-items:center; justify-content:center; gap:1px; flex:0 1 auto;
}
.sp-scrubbtn:hover{background:var(--chip)}
.sp-scrubbtn:disabled{opacity:.45; cursor:default}
.sp-scrubbtn b{font-size:1.02rem; font-variant-numeric:tabular-nums}
.sp-scrubbtn span{font-size:.72rem; color:var(--muted)}
.sp-play{
  appearance:none; cursor:pointer; border:1px solid var(--play); background:var(--play); color:var(--play-ink);
  display:inline-flex; align-items:center; justify-content:center;
  min-height:50px; min-width:118px; border-radius:12px; font-weight:800; font-size:1.05rem; padding:0 18px; flex:0 0 auto;
  transition:transform .06s ease, background .15s ease, border-color .15s ease;
}
.sp-play:hover{background:var(--play-press); border-color:var(--play-press)}
.sp-play:active{transform:scale(.97)}
.sp-play:disabled{opacity:.5; cursor:default}
.sp-play.playing{background:var(--surface-2); color:var(--text); border-color:var(--border)}  /* pause reads calmer */
.sp-underrow{display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap}
.sp-kbd{color:var(--muted); font-size:.76rem}
.sp-tbtn{
  appearance:none; cursor:pointer; border:1px solid var(--border); background:transparent; color:var(--muted);
  min-height:40px; padding:0 14px; border-radius:10px; font-weight:700; font-size:.85rem;
}
.sp-tbtn:hover{background:var(--surface-2); color:var(--text)}
.sp-tbtn:disabled{opacity:.5; cursor:default}
@media(max-width:560px){
  .sp-scrub{gap:6px}
  .sp-scrubbtn{flex:1 1 0; min-width:0; padding:0 6px}
  .sp-play{flex:1.5 1 0; min-width:0; padding:0 8px}
  .sp-kbd{display:none}
}

/* seek + markers pick up the teal accent */
.sp-seek{accent-color:var(--play)}
.sp-marks .sp-ab.b{background:var(--play)}
.sp-marks .sp-loopband{background:rgba(55,194,212,.22)}

/* ---------- Phase 45: waveform seek ----------
   The seek is now a waveform box. The canvas paints the song's shape, the range
   input is a transparent full-area scrub surface on top of it, and the marks layer
   (playhead / A–B / section ticks / played fill) rides on top of both. Marks is
   pointer-events:none so a drag falls through to the input — except section ticks,
   which stay clickable to jump. */
.sp-wavebox{ position:relative; height:58px; border-radius:8px; background:var(--surface-2); border:1px solid var(--border); overflow:hidden; }
.sp-compact .sp-wavebox{ height:46px; }
.sp-wave{ position:absolute; inset:0; width:100%; height:100%; display:block; pointer-events:none; }

/* the range input: invisible, but the whole box is the grab surface */
.sp-wavebox .sp-seek{
  position:absolute; inset:0; width:100%; height:100%; margin:0; padding:0;
  background:transparent; -webkit-appearance:none; appearance:none; cursor:pointer; z-index:2;
}
.sp-wavebox .sp-seek:focus-visible{ outline:2px solid var(--play); outline-offset:-2px; }
.sp-wavebox .sp-seek::-webkit-slider-runnable-track{ background:transparent; height:100%; border:0; }
.sp-wavebox .sp-seek::-moz-range-track{ background:transparent; height:100%; border:0; }
.sp-wavebox .sp-seek::-webkit-slider-thumb{ -webkit-appearance:none; width:16px; height:100%; background:transparent; border:0; cursor:grab; }
.sp-wavebox .sp-seek::-moz-range-thumb{ width:16px; height:100%; background:transparent; border:0; cursor:grab; }

/* marks overlay spans the full height of the box now */
.sp-wavebox .sp-marks{ position:absolute; inset:0; top:0; height:100%; z-index:3; pointer-events:none; }
.sp-wavebox .sp-played{ position:absolute; left:0; top:0; height:100%; background:rgba(55,194,212,.20); pointer-events:none; }
.sp-wavebox .sp-mark{ position:absolute; top:0; height:100%; width:2px; background:var(--muted); opacity:.85; transform:translateX(-1px); pointer-events:auto; cursor:pointer; }
.sp-wavebox .sp-mark:hover{ background:var(--text); opacity:1; }
.sp-wavebox .sp-ab{ position:absolute; top:0; height:100%; width:3px; transform:translateX(-1px); }
.sp-wavebox .sp-ab.a{ background:var(--good); }
.sp-wavebox .sp-ab.b{ background:var(--play); }
/* Phase 46: explicit A / B flag chips at the top of each loop marker. */
.sp-wavebox .sp-ab b{ position:absolute; top:0; left:1px; transform:translateX(-50%); font-size:.6rem; font-weight:800; line-height:1; padding:1px 4px; border-radius:0 0 4px 4px; }
.sp-wavebox .sp-ab.a b{ background:var(--good); color:var(--accent-ink); }
.sp-wavebox .sp-ab.b b{ background:var(--play); color:var(--play-ink); }
.sp-wavebox .sp-loopband{ position:absolute; top:0; height:100%; background:rgba(55,194,212,.16); border-radius:0; }
.sp-wavebox .sp-playhead{ position:absolute; top:0; height:100%; width:2px; background:var(--text); box-shadow:0 0 0 1px rgba(0,0,0,.3); }

/* ---------- Phase 46: pitch-preserving speed control ---------- */
.sp-speed{ display:flex; align-items:center; gap:10px; margin:10px 0 0; }
.sp-speed[hidden]{ display:none; }
.sp-speed-lbl{ font-weight:800; font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); flex:0 0 auto; }
.sp-speed-range{ flex:1 1 auto; min-width:120px; height:24px; accent-color:var(--play); cursor:pointer; }
.sp-speed-val{ flex:0 0 auto; min-width:44px; text-align:right; font-weight:800; font-variant-numeric:tabular-nums; color:var(--text); }
.sp-speed-busy{ flex:0 0 auto; font-size:.78rem; color:var(--play); font-weight:700; }
.sp-speed-busy[hidden]{ display:none; }
.sp-speed.busy .sp-speed-val{ color:var(--muted); }
/* Phase 48: labelled detents under the speed slider (75 / 80 / 85 / 90 / 95 / 100). */
.sp-speed-wrap{ position:relative; flex:1 1 auto; min-width:140px; }
.sp-speed-wrap .sp-speed-range{ width:100%; margin:0; display:block; }
.sp-speed-ticks{ position:relative; height:16px; margin-top:1px; pointer-events:none; }
.sp-speed-ticks .sp-tick{ position:absolute; top:0; transform:translateX(-50%); display:flex; flex-direction:column; align-items:center; }
.sp-speed-ticks .sp-tick i{ display:block; width:2px; height:5px; background:var(--border); }
.sp-speed-ticks .sp-tick b{ margin-top:1px; font-size:.58rem; font-weight:700; color:var(--muted); font-variant-numeric:tabular-nums; }

/* ---------- Phase 47/48: use the wide screen (laptop → TV) ----------
   The dock and the Rehearsal-Mode player were capped at --maxw (900px). On a big
   display, let them span more of the room so a mirrored-to-TV practice actually uses
   the screen; the mixer (below) auto-fits its columns to the new width. The extra
   specificity here beats the unconditional .rstage-player-body rule further down the
   file. Phones/iPad unchanged. */
@media (min-width:1024px){
  .dockplayer .dock-body{ max-width:min(1500px, 94vw); }
  .rstage-player > .rstage-player-body{ max-width:min(1500px, 94vw); }
  .sp-compact .sp-wavebox{ height:72px; }
}

/* ---------- player: two-column body on wide screens ---------- */
.sp-main{display:block}
@media(min-width:760px){
  .sp-main{display:grid; grid-template-columns:minmax(240px,340px) minmax(0,1fr); gap:24px; align-items:start; margin-top:6px}
}
.sp-col{min-width:0}

/* ---------- player: loop bar ---------- */
.sp-loopbar{
  display:flex; flex-wrap:wrap; align-items:center; gap:6px; margin:0 0 12px;
  padding:10px; background:var(--surface); border:1px solid var(--border); border-radius:12px;
}
.sp-loop-lbl{width:100%; margin-bottom:2px; font-weight:800; font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--muted)}
.sp-looptoggle.on{background:var(--play); color:var(--play-ink); border-color:var(--play)}
.sp-saveloop{margin-left:auto}

/* ---------- player: quick chips (jump / your loops) ---------- */
.sp-quick{display:flex; flex-direction:column; gap:12px}
.sp-qgroup{display:flex; flex-wrap:wrap; align-items:center; gap:6px}
.sp-qlbl{width:100%; margin-bottom:2px; font-weight:800; font-size:.7rem; text-transform:uppercase; letter-spacing:.04em; color:var(--muted)}
.sp-chip{
  appearance:none; cursor:pointer; border:1px solid var(--border); background:var(--surface-2); color:var(--text);
  min-height:38px; padding:4px 12px; border-radius:999px; font-weight:700; font-size:.86rem;
  display:inline-flex; align-items:center; gap:6px;
}
.sp-chip:hover{background:var(--chip)}
.sp-chip:disabled{opacity:.5; cursor:default}
.sp-chip b{color:var(--muted); font-weight:700; font-size:.8rem; font-variant-numeric:tabular-nums}
.sp-chip-loop{border-color:var(--play); color:var(--play)}
.sp-chip-loop:hover{background:var(--play-soft)}

/* ---------- player: mixer tweaks ---------- */
.sp-track{border-left-color:var(--play)}              /* base rail teal, not amber */
.sp-vol-wrap{display:inline-flex; align-items:center; flex:1; min-width:70px}
.sp-vol{accent-color:var(--play); height:20px; opacity:.72}   /* quiet fine-tune */
.sp-vol:hover, .sp-vol:focus-visible{opacity:1}
.sp-tag-mix{font-size:.6rem; font-weight:800; text-transform:uppercase; letter-spacing:.04em;
  background:var(--play-soft); color:var(--play); border-radius:6px; padding:2px 5px}
.sp-loadingrow .sp-track-name::after{content:' · loading…'; color:var(--play); font-weight:700; font-size:.72rem}

/* ---------- player: drawer heading ---------- */
.sp-drawer-toggle{color:var(--play)}

/* ---------- Phase 20 — re-lane Repertoire + Setlist practice links to teal ---------- */
.hp-ico{width:18px; height:18px; display:block; flex:0 0 auto}
.actions .practice{background:var(--play); color:var(--play-ink); border-color:var(--play)}
.actions .practice:hover{background:var(--play-press); border-color:var(--play-press)}
.actions .practice .hp-ico{width:18px; height:18px}
.slplay{
  display:inline-flex; align-items:center; justify-content:center; vertical-align:middle;
  width:30px; height:30px; border-radius:8px;
  color:var(--play); border:1px solid var(--play); background:var(--play-soft);
}
.slplay:hover{background:var(--play); color:var(--play-ink)}
.slplay .hp-ico{width:17px; height:17px}

/* ---------- Phase 51 — practice tools on a setlist row ----------
   Two levers per song, sitting together at the end of the meta line so they read
   as one "work on this song" cluster rather than two stray icons:
     .slyt   ▶ the reference recording (floating YouTube window) — the quieter of
             the two, so it's a muted outline; it's a look-something-up action.
     .slplay 🎧 the stem player — keeps the teal --play lane it already owns
             across the repertoire and practice tools, because that's the real
             woodshedding tool.
   Same 30px square as each other so the pair never looks lopsided. These are
   never gated by freeze or leader mode — practising is everybody's job. */
.splay-tools{
  display:inline-flex; align-items:center; gap:6px;
  vertical-align:middle; margin-left:4px;
}
.slyt{
  display:inline-flex; align-items:center; justify-content:center; vertical-align:middle;
  width:30px; height:30px; border-radius:8px;
  font-size:.78rem; line-height:1; text-decoration:none;
  color:var(--muted); border:1px solid var(--border); background:transparent;
}
.slyt:hover{ color:var(--text); border-color:var(--muted); background:var(--chip); }
/* Offline, the float has nothing to load — hide the ▶ rather than hand someone a
   dead button at a gig. The stem player stays: its audio may be SW-cached. */
body.offline .slyt{ display:none; }

/* ---------- Phase 20c — full-width single-column player + tracks grid ----------
   Drops the orphaned two-column split. Transport is one full-width stack (scrub
   above the seek, A-B loop below it); the seek now fills the width (the old
   layout inherited align-items:center, which shrank it). The mixer becomes a
   grid so the six stems sit as two tidy rows of three instead of one tall pile,
   and each cell stacks its name over its controls so the volume slider stops
   hogging width. */
.sp-transport{align-items:stretch}
.sp-loopbar{margin:2px 0 0; justify-content:center}
.sp-loop-lbl{width:auto; margin:0 4px 0 0}
.sp-quick{margin-top:10px}
.sp-hintline{color:var(--muted); font-size:.78rem; text-align:center; margin-top:6px}
.sp-hintline b{color:var(--text); font-weight:800; font-variant-numeric:tabular-nums}

/* Phase 48: the mixer auto-fits as many ≥200px columns as the width allows and
   WRAPS the rest — fills a laptop/TV, collapses to one column on a phone, and never
   scrolls off the right. minmax(0,1fr) + min-width:0 lets a cell shrink below its
   content instead of forcing a horizontal overflow (the old repeat(6,1fr) bug). */
.sp-tracks{display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:10px; margin-top:14px}
.sp-track{min-width:0}
@media(min-width:680px){
  .sp-track{flex-direction:column; align-items:stretch; gap:8px}
  .sp-track-name{flex:0 0 auto}
  .sp-track-ctrls{width:100%}
  .sp-vol-wrap{min-width:0}
}

/* ---------- Phase 20d — keep the dock's close X visible while scrolling ----------
   The docked player can be taller than its 74vh cap, so the header (song title +
   the ✕) used to scroll out of reach. Pin it to the top of the dock's scroll area
   in compact (dock) mode only — the standalone page is unaffected. */
.sp-compact .sp-head{
  position:sticky; top:-10px; z-index:6;
  background:var(--surface);
  margin:0 0 12px; padding:16px 0 8px;
  border-bottom:1px solid var(--border);
}

/* ===================================================================
   Phase 21 — Repertoire: on-demand song detail view (R1)
   The card stays a lean glance row; tapping its title block opens this
   read-only "full monty" with everything we have on the song.
   =================================================================== */
.song .row1-main{ cursor:pointer; border-radius:8px; }
/* A quiet affordance that the name is tappable — no layout change to the card. */
.song .row1-main:hover .title{ text-decoration:underline; text-decoration-color:var(--border); text-underline-offset:3px; }

.detail-modal{ max-width:560px; }
.dv-scroll{ overflow:auto; -webkit-overflow-scrolling:touch; }
.dv-head{
  display:flex; align-items:flex-start; gap:12px;
  padding-bottom:14px; margin-bottom:6px; border-bottom:1px solid var(--border);
}
.dv-titleblock{ flex:1 1 auto; min-width:0; }
.dv-title{ font-size:1.35rem; line-height:1.2; }
.dv-sub{ color:var(--muted); margin-top:4px; }
.dv-status{
  flex:0 0 auto; align-self:center;
  font-size:.8rem; font-weight:700; padding:5px 11px; border-radius:999px;
  background:var(--chip); color:var(--text); white-space:nowrap;
}
.dv-status.unset{ background:transparent; color:var(--muted); border:1px dashed var(--border); }
.dv-status.s-new{ background:#6b8fb5; color:var(--accent-ink); }
.dv-status.s-learning{ background:#d9a441; color:var(--accent-ink); }
.dv-status.s-rough{ background:var(--danger); color:#fff; }
.dv-status.s-polishing{ background:#9ccf8e; color:var(--accent-ink); }
.dv-status.s-gigready{ background:var(--good); color:var(--accent-ink); }
.dv-status.s-shelved{ background:#6d6355; color:var(--text); }

.dv-sec{ margin:16px 0 0; }
.dv-h{
  font-size:.72rem; font-weight:800; letter-spacing:.8px; text-transform:uppercase;
  color:var(--muted); margin-bottom:8px;
}
.dv-row{ display:flex; gap:12px; padding:5px 0; align-items:baseline; }
.dv-k{ flex:0 0 108px; color:var(--muted); font-size:.9rem; }
.dv-v{ flex:1 1 auto; min-width:0; }
.dv-muted{ color:var(--muted); }
.dv-block{ white-space:pre-line; line-height:1.55; }
.dv-links{ display:flex; flex-wrap:wrap; gap:8px; }
.dv-link{
  display:inline-flex; align-items:center; gap:7px;
  padding:9px 14px; border-radius:10px; text-decoration:none;
  background:var(--surface-2); color:var(--text); border:1px solid var(--border);
  font-size:.92rem; font-weight:600; min-height:44px;
}
.dv-link.practice{ background:var(--play, #2f6f6a); color:#fff; border-color:transparent; }
.dv-link .hp-ico{ width:18px; height:18px; }
.dv-foot{ margin-top:16px; padding-top:12px; border-top:1px solid var(--border); color:var(--muted); font-size:.9rem; }

/* ===================================================================
   Phase 21 — Practice rows: inline reference links, arrangement note,
   the ↩ return button, and the carried-forward practice-notes thread.
   =================================================================== */
.qref{ display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }
/* Batch 135: ▶ YouTube is the one .qref-link that DIDN'T move behind the expand —
   it rides in the title row instead. flex:none so it keeps its shape when a long
   title squeezes the row; nowrap so "▶ YouTube" never breaks across two lines. */
.qtop .qref-yt{ flex:none; white-space:nowrap; }
.qref-link{
  display:inline-flex; align-items:center; gap:5px;
  font-size:.82rem; font-weight:600; line-height:1;
  padding:7px 11px; min-height:36px;
  border-radius:9px; border:1px solid var(--border);
  background:var(--surface-2); color:var(--text); text-decoration:none; cursor:pointer;
  font-family:inherit;
}
.qref-link:hover{ border-color:var(--accent); }

.qarrnote{
  margin-top:8px; padding:8px 11px;
  background:var(--surface-2); border-left:3px solid var(--accent);
  border-radius:6px; color:var(--text); font-size:.92rem; line-height:1.45;
  white-space:pre-line;
}

/* ↩ return-to-working-list, beside the ✕ cut button. */
.qbtn.qreturn{ min-height:44px; width:44px; color:var(--muted); font-size:1.05rem; }
.qbtn.qreturn:hover{ color:var(--accent); border-color:var(--accent); }

/* Practice-notes thread inside the expanded row. */
.qnotes{ margin-top:12px; }
.qn-h{ font-size:.72rem; font-weight:800; letter-spacing:.6px; text-transform:uppercase; color:var(--muted); margin-bottom:8px; }
.qn-hint{ font-weight:600; letter-spacing:0; text-transform:none; color:var(--muted); opacity:.85; }
.qn-list{ display:flex; flex-direction:column; gap:8px; }
.qn{ background:var(--surface-2); border:1px solid var(--border); border-radius:9px; padding:8px 11px; }
.qn-head{ display:flex; align-items:baseline; gap:8px; }
.qn-who{ font-weight:700; font-size:.86rem; }
.qn-when{ color:var(--muted); font-size:.76rem; }
.qn-body{ margin-top:3px; white-space:pre-line; line-height:1.45; font-size:.92rem; }
.qn-att{ display:inline-block; margin-top:5px; font-size:.82rem; color:var(--accent); text-decoration:none; }
.qn-empty{ color:var(--muted); font-size:.88rem; font-style:italic; }
.qn-loading{ color:var(--muted); font-size:.86rem; }
.qn-new{ display:flex; gap:8px; margin-top:8px; align-items:flex-end; }
.qn-input{
  flex:1 1 auto; min-height:40px; resize:vertical;
  padding:8px 11px; border-radius:9px; border:1px solid var(--border);
  background:var(--surface); color:var(--text); font-family:inherit; font-size:.92rem; line-height:1.4;
}
.qn-post{ flex:0 0 auto; }

/* ===================================================================
   Phase 21 — shared "who's in" roster strip (roster.js), used by BOTH
   the practice planner and the setlist tool. Own `roster-*` namespace
   to avoid the older .att-* attendance / attachment-viewer styles.
   =================================================================== */
.att-strip{ display:block; }   /* practice head wrapper now holds one .roster card */
.roster{
  background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
  padding:12px 14px; margin:0 0 16px;
}
.roster-head{ display:flex; align-items:baseline; gap:10px; margin-bottom:9px; flex-wrap:wrap; }
.roster-lbl{ font-size:.72rem; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); font-weight:800; }
.roster-lbl::after{ content:":"; }
.roster-summary{ font-size:.85rem; color:var(--danger); font-weight:700; }

/* Collapsible attendance (Phase 25): the head becomes a full-width toggle. When
   collapsed it's just the label + the who's-out summary; the chips (and the
   leader's remove/re-add row) tuck away. Default state is set by each tool from
   "has this member answered yet?". */
.roster-head-btn{
  width:100%; appearance:none; background:transparent; border:0; cursor:pointer;
  font-family:inherit; text-align:left; color:inherit;
}
.roster-caret{ margin-left:auto; color:var(--muted); font-size:.8rem; transition:transform .15s ease; }
.roster.is-collapsed .roster-caret{ transform:rotate(-90deg); }
.roster.is-collapsed .roster-body{ display:none; }
.roster.is-collapsed .roster-head{ margin-bottom:0; }
.roster-summary.full{ color:var(--muted); font-weight:600; }
.roster-chips{ display:flex; flex-wrap:wrap; gap:8px; }

/* Chip = a pill container holding the in/out toggle and (for a leader) a ✕. */
.roster-chip{
  display:inline-flex; align-items:stretch; overflow:hidden;
  border:1px solid var(--border); background:var(--surface-2);
  border-radius:999px; transition:background .15s ease, border-color .15s ease, opacity .15s ease;
}
.roster-toggle{
  appearance:none; cursor:pointer; border:0; background:transparent; color:var(--text);
  min-height:38px; padding:6px 14px; font-size:.9rem; font-weight:700; font-family:inherit;
}
.roster-toggle::before{ content:"✓ "; color:var(--good); font-weight:800; }
.roster-chip.out{ opacity:.7; border-color:var(--danger); }
.roster-chip.out .roster-toggle{ color:var(--muted); text-decoration:line-through; text-decoration-color:var(--danger); }
.roster-chip.out .roster-toggle::before{ content:"✕ "; color:var(--danger); text-decoration:none; }

/* Leader-only remove ✕ tucked onto the right of the chip. */
.roster-x{
  appearance:none; cursor:pointer; border:0; border-left:1px solid var(--border);
  background:transparent; color:var(--muted); min-width:34px; min-height:38px;
  font-size:1rem; line-height:1; font-family:inherit;
}
.roster-x:hover{ color:var(--danger); background:rgba(229,103,95,.12); }

/* Removed members — a leader-only row to add someone back to the roster. */
.roster-removed{ display:flex; flex-wrap:wrap; align-items:center; gap:8px; margin-top:10px; padding-top:10px; border-top:1px dashed var(--border); }
.roster-removed-lbl{ font-size:.75rem; color:var(--muted); font-weight:700; }
.roster-chip.removed{
  appearance:none; cursor:pointer; display:inline-flex; align-items:center; gap:6px;
  min-height:34px; padding:5px 12px; font-size:.85rem; font-weight:700; font-family:inherit;
  border:1px dashed var(--border); background:transparent; color:var(--muted); border-radius:999px;
}
.roster-chip.removed:hover{ color:var(--text); border-color:var(--accent); }
.roster-add{ color:var(--accent); font-weight:800; }

/* ===================================================================
   Phase 22 — freeze a setlist, collapsible picker filters, vibe on the
   meta row, and the mobile "essentials only" pass.
   =================================================================== */

/* --- Freeze (leader lock) --- */
.lock-ico{ font-size:.9em; }
.frozen-banner{
  display:flex; align-items:center; gap:6px; flex-wrap:wrap;
  background:rgba(232,144,63,.12); border:1px solid var(--accent);
  color:var(--text); border-radius:10px; padding:8px 12px; margin:0 0 12px;
  font-size:.86rem; font-weight:600;
}

/* ---------- Offline Stage Mode (Phase 3): save-for-offline + readiness ----------
   Sits on its own row under the gig toolbar. The "Save" button is a quiet
   outline; once saved, an honest readout — green "✓ Ready for tonight", a plain
   "Saved · <time>", or an amber "tap to update" nudge when the copy is stale. */
.gig-offline{
  display:flex; align-items:center; flex-wrap:wrap; gap:8px 10px;
  width:100%; margin-top:12px;
}
.gig-offline:empty{ display:none; margin:0; }
.off-ico{ width:1em; height:1em; vertical-align:-2px; margin-right:2px; }
.off-save{
  appearance:none; cursor:pointer; display:inline-flex; align-items:center; gap:6px;
  min-height:38px; padding:0 14px; font-family:inherit; font-size:.9rem; font-weight:700;
  color:var(--text); background:var(--surface-2);
  border:1px solid var(--border); border-radius:999px;
}
.off-save:hover{ border-color:var(--accent); color:var(--accent); }
.off-chip{
  display:inline-flex; align-items:center; gap:6px;
  min-height:34px; padding:4px 12px; border-radius:999px;
  font-size:.86rem; font-weight:700; border:1px solid var(--border);
}
.off-ok{ color:var(--muted); background:transparent; }
.off-saving{ color:var(--muted); background:transparent; }
.off-ready{ color:var(--good); background:rgba(122,195,106,.12); border-color:var(--good); }
.off-warn{ color:var(--muted); background:transparent; border:0; }
.off-stale{
  appearance:none; cursor:pointer; font-family:inherit;
  color:var(--accent); background:rgba(232,144,63,.12); border-color:var(--accent);
}
.off-stale:hover{ background:rgba(232,144,63,.2); }
/* BATCH 135 — the happy state is now ONE chip that is also the button. It replaced
   three elements (✓ Saved for offline · Synced 2 hours ago · Refresh) that between
   them ran most of the header's width to state one fact and offer one action.
   `.off-sub` and `.off-resave` retired with them.

   The honest cost: a green tick doesn't say "tap me" the way the word Refresh did.
   These rules are the compensation — a pointer, a hover lift, a real pressed state
   — alongside the "Saving…" label swap in the click handler. Don't flatten this
   back to a plain .off-chip: the affordance IS the styling now. */
.off-chip.off-refresh{
  appearance:none; cursor:pointer; font-family:inherit;
  transition:background .12s ease, transform .08s ease;
}
.off-chip.off-refresh:hover{ background:rgba(122,195,106,.22); }
.off-chip.off-refresh:active{ transform:scale(.97); }
.off-chip.off-refresh:disabled{ opacity:.6; cursor:default; transform:none; }
.off-chip.off-refresh:focus-visible{ outline:3px solid var(--focus); outline-offset:2px; }
/* v152: the stale chip is a real button now, so it gets the same affordances as
   the happy one. It is the state most likely to be tapped and was the state
   most likely to be missed — before this batch it rendered identically to
   "saved and current", which is how a week-old copy passed for a fresh one. */
.off-stale:active{ transform:scale(.97); }
.off-stale:focus-visible{ outline:3px solid var(--focus); outline-offset:2px; }

/* ===========================================================================
   THE OFFLINE CHECK (v152) — the line-by-line "am I ready for tonight?" report.
   Built as a .modal-back so it inherits the app's focus trap, Escape handling
   and backdrop click for free (see UI.openOfflineCheck).

   Read at arm's length in a car park, so: the verdict is big and first, each
   row leads with a shape you can read before you read any words, and the detail
   line is the instruction rather than an explanation. Nothing here scrolls
   horizontally and nothing depends on colour alone — ✓ / ! / ✕ carry the state
   for anyone who can't separate the greens from the ambers under a stage wash.
   ========================================================================= */
.oc-modal{ max-width:520px; }
.oc-verdict{
  margin:2px 0 14px; padding:10px 14px; border-radius:10px;
  font-size:1.05rem; font-weight:800; line-height:1.3;
  color:var(--muted); background:var(--surface-2); border:1px solid var(--border);
}
.oc-verdict.oc-good{ color:var(--good);   background:rgba(122,195,106,.12); border-color:var(--good); }
.oc-verdict.oc-warn-v{ color:var(--accent); background:rgba(232,144,63,.12); border-color:var(--accent); }
.oc-verdict.oc-bad{ color:var(--danger); background:rgba(229,103,95,.12); border-color:var(--danger); }
.oc-verdict.oc-busy{ font-weight:700; }
.oc-list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:10px; }
.oc-row{ display:flex; align-items:flex-start; gap:10px; }
.oc-ico{
  flex:0 0 22px; width:22px; height:22px; border-radius:50%;
  display:inline-flex; align-items:center; justify-content:center;
  font-size:.82rem; font-weight:900; line-height:1; margin-top:1px;
}
.oc-ico.oc-ok{   color:#12240e; background:var(--good); }
.oc-ico.oc-warn{ color:#2a1704; background:var(--accent); }
.oc-ico.oc-fail{ color:#fff;    background:var(--danger); }
.oc-tx{ display:flex; flex-direction:column; gap:2px; min-width:0; }
.oc-lbl{ font-weight:700; font-size:.94rem; color:var(--text); }
.oc-det{ font-size:.84rem; color:var(--muted); line-height:1.4; }
.oc-det:empty{ display:none; }
/* The list is the substance; the buttons are what you do about it. The render
   check (Chromium, 820×1180 and 390×844) showed them crowding the last row at
   the stock .modal-actions spacing, which made the final ⚠ read as a caption on
   the button rather than as the last thing to read. */
.oc-modal .modal-actions{ margin-top:18px; padding-top:16px; border-top:1px solid var(--border); }
@media (max-width:480px){
  .oc-verdict{ font-size:.98rem; }
  .oc-lbl{ font-size:.9rem; }
  /* Two full-width words don't fit side by side at 390px once "Refresh the
     saved copy" is spelled out — and shortening it to "Refresh" would lose the
     noun that says WHAT gets refreshed, which is the whole point of the panel. */
  .oc-modal .modal-actions{ flex-direction:column-reverse; }
}

/* The Stage Mode offline pill — a compact, red "Offline · <age>" that rides in
   the top bar's empty middle. Deliberately SHORT (no emoji, terse age) and hard-
   capped in width so it can never grow enough to bump the reading tools onto a
   second row — the wrap the old "Offline — saved copy from just now" wording
   caused. Red = you're on cached data; the age tells you how stale. */
.stage-offline{
  flex:0 1 auto; min-width:0; max-width:min(46vw, 15em); margin:0;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  border-radius:999px; padding:5px 12px; font-size:.78rem; font-weight:800;
  color:var(--danger); border:1px solid var(--danger); background:var(--danger-soft, rgba(229,103,95,.12));
}
.stage-offline[hidden]{ display:none; }
/* The scary one: offline AND nothing saved — a solid red flag, not a tint. */
.stage-offline-warn{ color:#fff; background:var(--danger); border-color:var(--danger); }
/* Keep the position counter intact and let the offline pill absorb any squeeze. */
.stage-top .stage-counter-btn{ flex:0 0 auto; }

/* ===========================================================================
   THE SET MAP (v153) — the counter as a door, and the grid behind it.

   ⚠️ EVERY COLOUR HERE IS A TOKEN. This panel lives INSIDE `.stage`, which the
   v149 themes re-point wholesale (`.stage.rt-paper`, `.stage.rt-contrast`), so a
   literal would look right on the near-black and wrong on Paper — and Paper is
   the one someone reaches for under stage lighting.
   ========================================================================= */
.stage-counter-btn{
  appearance:none; -webkit-appearance:none; cursor:pointer;
  /* v171 — THE DOOR NOW LOOKS LIKE A DOOR. A singer at the Sep-1 practice
     couldn't find the way to the set map: the counter was a bare readout with a
     75%-opacity glyph, and nothing about it said "tap me". It now wears the same
     outline the ✕ wears (below) — an accent BORDER, not an accent fill. Next is
     deliberately the only accent-FILLED control on this surface, and a second
     solid-orange block up here would compete with it. All tokens: rule 8. */
  background:var(--surface-2); border:2px solid var(--accent); padding:0 12px;
  font:inherit; color:inherit;
  display:inline-flex; align-items:center; gap:8px; min-width:0; min-height:48px;
  border-radius:12px; transition:opacity .12s ease, transform .08s ease;
}
/* The word beside the glyph. "List" — Alex's word: to a singer it means the
   set list, which is exactly what the map shows. Words beside glyphs was the
   Aug-27 review's standing ask for this tier of button. */
.stage-map-lbl{
  display:inline-flex; align-items:center; gap:5px; flex:0 0 auto;
  color:var(--text); font-weight:700; font-size:clamp(.8rem,3.2vw,.95rem); letter-spacing:.2px;
  padding-left:8px; border-left:1px solid var(--border);
}
.stage-counter-btn:hover{ opacity:.82; }
.stage-counter-btn:active{ transform:scale(.97); }
.stage-counter-btn:focus-visible{ outline:3px solid var(--focus); outline-offset:3px; }
/* The glyph is what tells you the counter is tappable at all — the readout
   itself gives no hint, which is the same affordance debt batch 135 had to pay
   off for the offline chip. Sized off the counter's own clamp so it tracks the
   text on a phone and on a TV alike. */
.stage-map-ico{
  width:1.15em; height:1.15em; flex:0 0 auto; opacity:1;
  color:var(--accent);
}

.setmap{
  position:absolute; inset:0; z-index:60;
  display:flex; align-items:flex-start; justify-content:center;
  padding:calc(env(safe-area-inset-top,0px) + 14px) 14px 14px;
  background:var(--scrim, rgba(0,0,0,.72));
}
.setmap[hidden]{ display:none; }
.setmap-card{
  width:100%; max-width:1100px; max-height:100%;
  display:flex; flex-direction:column; min-height:0;
  background:var(--surface); border:1px solid var(--border); border-radius:16px;
  box-shadow:0 18px 50px rgba(0,0,0,.45);
}
.setmap-head{
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  padding:12px 14px; border-bottom:1px solid var(--border); flex:0 0 auto;
}
.setmap-title{ font-weight:800; font-size:1.02rem; color:var(--text); }
.setmap-x{
  appearance:none; cursor:pointer; font:inherit; line-height:1;
  min-width:40px; min-height:40px; border-radius:10px;
  color:var(--muted); background:var(--surface-2); border:1px solid var(--border);
}
.setmap-x:hover{ color:var(--text); }
.setmap-x:focus-visible{ outline:3px solid var(--focus); outline-offset:2px; }
.setmap-body{ overflow-y:auto; -webkit-overflow-scrolling:touch; padding:12px 14px 16px; min-height:0; }
.setmap-empty{ color:var(--muted); padding:18px 4px; }
.setmap-group + .setmap-group{ margin-top:18px; }
/* STICKY, and the render check is why. The panel opens scrolled to the song
   you're on, so on a three-set night the first thing on screen was a grid of
   tiles with its "Set 1" heading already scrolled past — you could see the
   songs and not which set they were. Sticky headers mean whatever you are
   looking at is always labelled, which is the entire job of this panel. */
.setmap-group-h{
  position:sticky; top:0; z-index:1;
  display:flex; align-items:baseline; gap:10px; flex-wrap:wrap;
  margin:0 2px 8px; padding:6px 2px; border-bottom:1px solid var(--border);
  background:var(--surface);
}
.setmap-group-n{ font-weight:800; color:var(--accent); font-size:.98rem; letter-spacing:.3px; }
.setmap-group-m{ color:var(--muted); font-size:.84rem; }
/* v158 — a `tone:'quiet'` group. The practice map's no-singer block: in the full
   accent, next to "Dan" and "Kevin", it read as a third band member. */
.setmap-group-quiet .setmap-group-n{ color:var(--muted); }
/* auto-fill + minmax is what makes "five or six per row" true on a landscape
   iPad and "two" true on a phone, without a single breakpoint. */
.setmap-grid{ display:grid; grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:8px; }
.setmap-tile{
  appearance:none; cursor:pointer; font:inherit; text-align:left;
  display:flex; flex-direction:column; gap:2px;
  min-height:62px; padding:8px 10px; border-radius:11px;
  color:var(--text); background:var(--surface-2); border:1px solid var(--border);
  transition:transform .08s ease, border-color .12s ease;
}
.setmap-tile:active{ transform:scale(.97); }
.setmap-tile:hover{ border-color:var(--accent); }
.setmap-tile:focus-visible{ outline:3px solid var(--focus); outline-offset:2px; }
.setmap-n{ font-size:.72rem; font-weight:800; color:var(--muted); letter-spacing:.5px; }
.setmap-t{ font-weight:700; font-size:.92rem; line-height:1.25; overflow-wrap:anywhere; }
.setmap-s{ font-size:.74rem; color:var(--muted); }
/* Played / worked-on recedes rather than disappearing: you still want to be able
   to jump BACK, so it must stay readable and tappable. */
.setmap-tile.is-done{ opacity:.5; }
.setmap-tile.is-done .setmap-t{ font-weight:600; }
/* Where you are. The ring is deliberately heavier than a tint — this is the one
   tile you are looking for, and the panel opens scrolled to it. */
.setmap-tile.is-now{
  border-color:var(--accent); background:var(--accent-soft, rgba(232,144,63,.14));
  box-shadow:inset 0 0 0 1px var(--accent);
}
.setmap-tile.is-now .setmap-n{ color:var(--accent); }
@media (max-width:520px){
  .setmap{ padding:calc(env(safe-area-inset-top,0px) + 8px) 8px 8px; }
  .setmap-grid{ grid-template-columns:repeat(auto-fill, minmax(120px, 1fr)); }
  .setmap-tile{ min-height:56px; }
}

/* Offline = view-only. Dim the create/edit entry points as a quiet cue (they stay
   tappable so the friendly "changes are view-only right now" toast still fires). */
body.offline #addBtn,
body.offline #newSetlistBtn,
body.offline #newPracticeBtn{ opacity:.45; }

/* --- Meta row: [Key] Artist · Singer, same order as a practice row (v138) --- */
.smeta .sm:not(:first-child)::before{ content:"·"; margin:0 6px; color:var(--muted); }
/* The key leads as a CHIP (was the text "key Am"), outside .smeta so it takes no
   ·-separator — the chip's own border is the divider. `.sm-vibe` and `.sm-len` are
   gone with the badges they styled: the vibe is a set-BUILDING concern and the
   length is already summed in the header and every set heading. */
.sm-keychip{ vertical-align:-1px; margin-right:5px; }
.ssub .badge{ vertical-align:-1px; }

/* --- Collapsible picker filters ("More filters" disclosure) --- */
.pf-more-toggle{
  appearance:none; cursor:pointer; align-self:flex-start;
  background:transparent; border:0; color:var(--accent);
  font-size:.85rem; font-weight:700; font-family:inherit;
  padding:6px 2px; display:inline-flex; align-items:center; gap:6px; min-height:36px;
}
.pf-more-n{
  display:inline-flex; align-items:center; justify-content:center;
  min-width:18px; height:18px; padding:0 5px; border-radius:999px;
  background:var(--accent); color:var(--accent-ink); font-size:.72rem; font-weight:800;
}
.pf-more{ display:flex; flex-direction:column; gap:8px; margin-top:2px; }
.pf-toprow{ align-items:center; }

/* --- Mobile essentials ---
   The working list is desktop-only (≥1024px), so the ↩ "return to working list"
   button has no visible destination below that — hide it in both tools. */
@media (max-width:1023.98px){
  .qbtn.qreturn{ display:none; }
}
/* On a phone a setlist row strips to Title · Singer · Key (+ a singer-out flag if
   someone's absent) — the rest is superfluous when space is tight.
   Phase 51 EXCEPTION: the practice tools (▶ / 🎧) now STAY on phones. The reason
   the strip existed was show night, and show night is Stage Mode, not this page —
   whereas a bandmate practising on the couch is holding a phone, which is exactly
   who these buttons are for. With the meta line down to "Singer · key G" there's
   room to spare, and they wrap to their own line if there isn't. */
@media (max-width:639.98px){
  .sitem .sm-artist{ display:none; }
  /* …and with the artist hidden, the singer inherits an ORPHAN leading "·" — the
     ::before above fires on :not(:first-child) and `display:none` doesn't change
     who the first child IS. Kill it explicitly. (This bit the moment v138 put the
     artist first; under the old singer-first order the bug couldn't show.) */
  .sitem .smeta .sm-singer::before{ content:none; }
  .sitem .sbadges .badge.status,
  .sitem .sbadges .badge.warn{ display:none; }
  /* NOT hidden on a phone, unlike the two above: "no lyrics" is the one warning a
     singer needs while checking Saturday's set from the couch, and it's rare enough
     to cost nothing once the corpus is filled in. It matches neither .status nor
     .warn, so it survives that rule already — this note exists so nobody "tidies"
     it into the list above. */
  .sitem .splay-tools{ margin-left:2px; }
}

/* ===================================================================
   Phase 22b — set-relative song numbers; creation gated to leaders.
   =================================================================== */

/* Small muted "1." "2." … before each setlist row's title (restarts per set).
   The number + title share one flex item so a long title can't strand the
   number alone on a line. */
.stitle-main{ min-width:0; word-break:break-word; }
.snum{ color:var(--muted); font-weight:700; font-variant-numeric:tabular-nums; }

/* Creating and duplicating setlists/practices is a leader action, done on the
   laptop. New (setlist + practice) and Duplicate: shown only to a leader, and
   hidden on mobile even for a leader (mobile is for displaying the data).
   Freeze is leader-only too, but stays available on mobile (lock/unlock at the
   venue). Driven by the body.is-leader class from leader.js + server-enforced. */
#newSetlistBtn, #newPracticeBtn, #dupSetlistBtn, #freezeBtn{ display:none; }
body.is-leader #newSetlistBtn,
body.is-leader #newPracticeBtn,
body.is-leader #dupSetlistBtn,
body.is-leader #freezeBtn{ display:inline-flex; }
@media (max-width:639.98px){
  body.is-leader #newSetlistBtn,
  body.is-leader #newPracticeBtn,
  body.is-leader #dupSetlistBtn{ display:none; }
}
/* The practice-page "Print song list" button is leader-only (a pre-practice
   heads-up the leader sends round). Stays available at every width — printing to
   PDF works on a phone too. */
#practicePrintBtn{ display:none; }
body.is-leader #practicePrintBtn{ display:inline-flex; }

/* ===================================================================
   Phase 22c — repertoire: status chip beside the key; always-on YouTube;
   Add-song leader-gated. Practice rows: view-only + full-width on mobile.
   =================================================================== */

/* Status chip now rides in the badges row next to the key, sized to match. */
.badges{ align-items:center; }
.qstatus-chip.qstatus-badge{
  min-height:0; padding:2px 8px; font-size:.82rem; font-weight:700;
  border-radius:8px; line-height:1.25;
}

/* Leader-only filter chips (currently just 📄 No lyrics yet). Same body.is-leader
   readout gate as the practice Print button — decluttering, not security; the real
   protection is require_leader_api on set_lyrics. Unlike #addBtn this is NOT also
   desktop-gated: auditing lyrics coverage from the couch on a phone is a perfectly
   normal thing to want to do. app.js drops the filter on re-lock so it can never
   strand the list behind a chip that just disappeared. */
.chip.chip-leader{ display:none; }
body.is-leader .chip.chip-leader{ display:inline-flex; align-items:center; }

/* The coverage readout under that chip. Reuses .hint, just spaced off the one
   above it so the two don't read as one paragraph. */
.hint.lyrics-coverage{ margin-top:6px; font-weight:700; }
.hint.lyrics-coverage[hidden]{ display:none; }

/* "no lyrics" card badge — marks the EXCEPTION, the way the setlist row shows a
   status badge only when a song ISN'T gig-ready.

   Deliberately NOT .badge.warn (that's --danger red, for ⚑ arr and ⚠ singer-out —
   things that are a problem with the SHOW). A missing lyrics sheet is a to-do, so
   it's muted with a dashed border: the dash reads as "empty" without shouting, and
   a repertoire mid-sweep doesn't look like a wall of errors. */
.badge.nolyrics{
  color:var(--muted); background:transparent;
  border-style:dashed; font-weight:700;
}
/* v138 — on a SETLIST row the same chip becomes a leader's shortcut into the Lyrics
   editor, so it has to read as pressable where the repertoire's is just a label.
   It's a real <button> for a leader and an inert <span> for everyone else (see
   renderRow), which is why the affordance is a class and not a hover-on-everything
   rule: a member must never get a pointer for an action they can't perform. */
.badge.nolyrics-btn{
  appearance:none; cursor:pointer; font-family:inherit; font-size:.82rem;
  transition:color .12s ease, border-color .12s ease;
}
.badge.nolyrics-btn:hover{ color:var(--accent); border-color:var(--accent); }
.badge.nolyrics-btn:active{ transform:scale(.97); }
.badge.nolyrics-btn:focus-visible{ outline:3px solid var(--focus); outline-offset:2px; }

/* v141 — "⏱ no timing" / "⏱ re-time" card badge. Same absence-marking shape as
   .badge.nolyrics above, and the same muted-dashed look: an untimed song is a
   leader to-do, not a problem with the show. Unlike the lyrics badge this one is
   LEADER-ONLY (rendered gated in cardHtml): a member can't calibrate and, unlike
   a missing lyrics sheet, doesn't have to plan around its absence — the rail's
   presence in Stage/Rehearsal already tells them a song scrolls itself. */
.badge.notiming{
  color:var(--muted); background:transparent;
  border-style:dashed; font-weight:700;
}
/* v143 — as a leader's BUTTON the same chip reads as pressable (it opens the
   Lyrics & Timing dialog). Mirrors .nolyrics-btn above: affordance by class,
   never a hover-on-everything rule — members never receive this element. */
.badge.notiming-btn{
  appearance:none; cursor:pointer; font-family:inherit; font-size:.82rem;
  transition:color .12s ease, border-color .12s ease;
}
.badge.notiming-btn:hover{ color:var(--accent); border-color:var(--accent); }
.badge.notiming-btn:active{ transform:scale(.97); }
.badge.notiming-btn:focus-visible{ outline:3px solid var(--focus); outline-offset:2px; }

/* v144 — the leader's ⏱ Timing tools mode.
   .lt-toggle: the mode switch on the setlist + practice toolbars — CSS-gated
   leader-only exactly like #practicePrintBtn, lit when pressed.
   .lt-door: the per-row route into the Lyrics & Timing dialog. ALWAYS in the
   markup, VISIBLE only when the leader has the mode on — the compound selector
   is the whole feature: rows stay v138-lean for everyone until the leader is
   deliberately tuning, and flipping the toggle or the padlock needs no
   re-render because only this body class changes. (Decluttering, not
   security — the dialog refuses non-leaders and the server gates the writes.) */
.lt-toggle{ display:none; }
body.is-leader .lt-toggle{ display:inline-flex; }
.lt-toggle[aria-pressed="true"]{ color:var(--accent); border-color:var(--accent); }
.lt-door{
  display:none; appearance:none; cursor:pointer; flex:0 0 auto;
  border:1px dashed var(--border); background:transparent; color:var(--muted);
  border-radius:999px; min-width:34px; min-height:30px; padding:0 8px;
  font-size:.95rem; line-height:1;
  transition:color .12s ease, border-color .12s ease;
}
body.is-leader.lt-tools-on .lt-door{ display:inline-flex; align-items:center; justify-content:center; }
.lt-door:hover{ color:var(--accent); border-color:var(--accent); }
.lt-door:active{ transform:scale(.95); }
.lt-door:focus-visible{ outline:3px solid var(--focus); outline-offset:2px; }
.lyr-loading{ color:var(--muted); padding:14px 2px; }

/* "Add song" FAB is a leader action, done on the laptop (like New / Duplicate):
   shown only to a leader, and hidden on mobile even for a leader. */
#addBtn{ display:none; }
body.is-leader #addBtn{ display:inline-flex; }
@media (max-width:639.98px){
  body.is-leader #addBtn{ display:none; }
}

/* Practice rows on a phone are for consuming, not editing: drop the right-hand
   control column entirely (✕ remove, ↩ return, and the ▲▼ reorder arrows all go
   with it) so each entry — and its note composer — reflows to the full width.
   Reordering / removing stays on the laptop. */
@media (max-width:639.98px){
  /* Phase 22c hid .qref-tabs behind the expand on phones only. Batch 135 made that
     rule redundant — the WHOLE .qref cluster lives inside .qdetails now, on every
     screen size — so it's gone rather than left lying around asserting something
     that's no longer true. */
}

/* ===================================================================
   Phase 22e — a rehearsed (checked) practice row folds up to just the
   title, and everything else is suppressed (player, YouTube, arrangement,
   sub-info, ↩/✕). Keeps the list compact as you work down it. Pure CSS off
   the .done class that setWorked toggles, so it flips instantly with no
   re-render.

   BATCH 135 — this used to keep the status chip alive on a folded row, so a
   leader could re-grade a song after playing it. The chip is off the collapsed
   row entirely now, so a done row is title-and-nothing-else. That's a cleaner
   statement of what "done" means, and re-grading moved to the row's expand —
   which is why setWorked() also force-collapses on tick: without that, .done
   would hide the details in CSS while state.expanded still said "open", and
   un-ticking would spring the row back open for no reason.
   =================================================================== */
.qitem.done{ opacity:1; }
.qitem.done .qmain{ display:flex; align-items:center; gap:10px; }
.qitem.done .qtop{ flex:1; min-width:0; }
.qitem.done .qtitle{ color:var(--muted); }   /* strike-through already applies via .qitem.done .qtitle */
/* Nothing to expand once it's done — but a done song can still be LAUNCHED
   (v171): "let's run that one again" is a real thing to say at practice. */
.qitem.done .qcaret-btn{ pointer-events:none; visibility:hidden; }
.qitem.done .qsub,
.qitem.done .qcaret,
.qitem.done .qplay,
.qitem.done .qref-yt,
.qitem.done .qarrnote,
.qitem.done .qref,
.qitem.done .qdetails,

/* ============================================================= *
 * Phase 23 — notes consolidation: the pinned "How we play it"   *
 * canonical (teal --play family, to read as band reference, not *
 * a readiness/amber cue) + stage-mode show toggles.            *
 * ============================================================= */

/* Card badge: rare pin flag beside the key/status. */
.badge.canon{ color:var(--play); border:1px solid var(--play); background:var(--play-soft); }

/* Pinned canonical block at the top of the Notes surface. */
.notes-canon{
  background:var(--play-soft); border:1px solid var(--play);
  border-radius:12px; padding:12px 14px; margin-bottom:20px;
}
.canon-head{ display:flex; align-items:center; gap:8px; margin-bottom:6px; }
.canon-pin{ font-size:1rem; line-height:1; }
.canon-title{ font-size:.95rem; margin:0; font-weight:800; color:var(--play); }
.canon-lead{
  font-size:.62rem; font-weight:800; letter-spacing:.6px; text-transform:uppercase;
  color:var(--play); border:1px solid var(--play); border-radius:20px; padding:1px 8px;
}
.canon-edit{ margin-left:auto; }
.canon-body{ color:var(--text); font-size:.95rem; line-height:1.5; white-space:pre-wrap; word-break:break-word; }
.canon-empty{ color:var(--muted); font-size:.9rem; font-style:italic; }
.canon-editor{ display:flex; flex-direction:column; gap:8px; }
.canon-editor .canon-input{ width:100%; min-height:70px; }
.canon-status{ color:var(--good); font-size:.85rem; font-weight:700; }

/* Stage mode: the two "what to show" toggles. */
.stage-opts{
  display:flex; gap:8px; justify-content:center; flex-wrap:wrap;
  padding:6px 12px 0;
}
.stage-opt{
  appearance:none; cursor:pointer; min-height:38px; padding:0 14px;
  background:transparent; color:var(--muted); border:1px solid var(--border);
  border-radius:999px; font-size:.82rem; font-weight:700;
}
.stage-opt[aria-pressed="true"]{
  color:var(--play-ink); background:var(--play); border-color:var(--play);
}

/* Text-size scaler — one quiet row under the note toggles. A− shrinks, A＋ grows,
   universally, so anyone can dial the stage text to their eyes + device. */
.stage-size{
  display:flex; gap:8px; justify-content:center; align-items:center;
  padding:6px 12px 0; color:var(--muted);
}
.stage-size-lbl{ font-size:.7rem; font-weight:800; letter-spacing:.5px; text-transform:uppercase; }
.stage-size-btn{
  appearance:none; cursor:pointer; min-width:44px; min-height:34px; padding:0 12px;
  display:inline-flex; align-items:center; justify-content:center; gap:1px;
  background:transparent; color:var(--muted); border:1px solid var(--border);
  border-radius:999px; font-weight:800; line-height:1;
}
/* Nudge the glyphs onto the optical centre — the differently-sized A + operator
   center as a group rather than sitting on the baseline. */
.stage-size-btn .ss-a, .stage-size-btn .ss-op{ display:inline-flex; align-items:center; }
.stage-size-btn:hover{ color:var(--text); }
.stage-size-btn:disabled{ opacity:.35; cursor:default; }
.stage-size-btn .ss-a{ font-weight:800; }
.stage-size-btn .ss-sm{ font-size:.8rem; }
.stage-size-btn .ss-lg{ font-size:1.15rem; }
.stage-size-btn .ss-op{ font-size:.9rem; }

/* Phase 57 — the Width control, made legible.
   It used to render at roughly half the visual weight of its A−/A＋ neighbours,
   and the cause was mundane: A−/A＋ bump their glyphs via .ss-lg/.ss-sm/.ss-op
   spans, while Width held BARE ↤ ↦ characters that just inherited the ~16px page
   font. Nobody could tell at a glance what the buttons did. Fixed by giving the
   glyphs their own size, swapping in spread-apart / come-together arrows (the
   standard expand-contract shape, readable without a legend), and spending part
   of the space reclaimed from the deleted toggles on an actual word. */
/* Phase 57 batch 2: the "Width" word came back out — once the arrows were drawn
   properly they read on their own, and the word was doing nothing but taking
   room. What the cluster actually needed was separation, so A−/A＋ and the width
   arrows stop looking like one run of four buttons. */
.stage-tools-div{
  flex:0 0 auto; width:1px; align-self:stretch; min-height:26px;
  background:var(--border); margin:0 2px; border-radius:1px;
}
.stage-size-btn.stage-width-btn{ padding:0 10px; }
/* Drawn, not typed — see widthSvg() in setlists.js / planner.js for why. Sized
   in em off the button so it tracks any future button-size change. */
.wd-ico{ width:1.45rem; height:1.45rem; display:block; }

/* Stage mode: the pinned canonical line, distinct from your amber cues. */
.stage-canon{
  display:flex; flex-direction:column; align-items:center; gap:4px;
  max-width:820px; width:100%; margin:2px auto 0;
  background:var(--play-soft); border:1px solid var(--play);
  border-radius:16px; padding:clamp(10px,2.4vh,16px) 16px;
}
.stage-canon-lbl{
  font-size:.72rem; font-weight:800; letter-spacing:.6px; text-transform:uppercase; color:var(--play);
}
.stage-canon-tx{
  color:var(--play); font-weight:800; line-height:1.2; text-align:center;
  font-size:calc(var(--stage-scale,1) * clamp(1.1rem, min(5vw,6vh), 2rem)); white-space:pre-wrap; word-break:break-word;
}
@media (orientation:landscape) and (max-height:600px){
  .stage-canon{ align-items:flex-start; padding:10px 14px; margin-top:0; }
  .stage-canon-tx{ text-align:left; font-size:calc(var(--stage-scale,1) * clamp(1rem, min(3vw,7vh), 1.6rem)); }
}

/* ============================================================= *
 * Phase 28 — Rehearsal Mode: the practice big-screen. Reuses the *
 * .stage shell; these are the practice-only additions (editable  *
 * notes, the leader's member toggles + worked-on / status row).  *
 * ============================================================= */
/* This is a working surface, not a teleprompter — no click-to-advance. */
.stage.rehearsal .stage-main{ cursor:default; }
/* Give the practice screen room to spread notes into columns (the pinned
   canonical block keeps its own narrower max-width and stays centred). */
.stage.rehearsal .stage-primary{ max-width:min(1500px, 100%); }

/* Top control bar: Text Size + (leader) Worked-on / Status share one row. */
.rstage-topbar{ display:flex; gap:16px; align-items:center; justify-content:center; flex-wrap:wrap; }
.rstage-topbar .stage-size{ padding-top:0; }

/* Phase 38: the launch button now wears the orange accent (btn-primary) so it
   matches Stage mode on the setlist page — no colour override here, just the
   disabled dim. */
.rehearse-btn:disabled{ opacity:.45; cursor:default; }
/* Phase 39: the "↩ Resume" shortcut beside the launch button (practice + setlist).
   A warm secondary — clearly related to the orange primary, but quieter. */
.rehearse-resume, .stage-resume{ color:var(--accent); border-color:rgba(232,144,63,.5); font-weight:800; }
.rehearse-resume:hover, .stage-resume:hover{ background:rgba(232,144,63,.14); }

/* Leader's "show notes" member toggle strip (reuses .stage-opts / .stage-opt). */
.rstage-members-lbl{
  align-self:center; color:var(--muted); font-size:.7rem; font-weight:800;
  letter-spacing:.5px; text-transform:uppercase; margin-right:2px;
}

/* Leader worked-on + status — chrome, not content, so a fixed compact size that
   sits alongside the Text Size buttons (does NOT scale with --stage-scale). */
.rstage-ctl{ display:inline-flex; gap:14px; align-items:center; padding:6px 12px 0; }
.rstage-ctl[hidden]{ display:none; }
.rstage-check{
  display:inline-flex; gap:7px; align-items:center; color:var(--text); cursor:pointer;
  font-size:.82rem; font-weight:800;
}
.rstage-check input{ width:18px; height:18px; accent-color:var(--play); cursor:pointer; }
.rstage-statuswrap{
  display:inline-flex; gap:7px; align-items:center; color:var(--muted);
  font-size:.7rem; font-weight:800; letter-spacing:.5px; text-transform:uppercase;
}
/* The status dropdown carries the same per-status colours as the chips elsewhere. */
.rstage-status{
  appearance:none; background:var(--surface-2); color:var(--text);
  border:1px solid var(--border); border-radius:999px; padding:5px 12px;
  font:inherit; font-weight:800; font-size:.82rem; cursor:pointer; text-transform:none; letter-spacing:0;
}
.rstage-status.s-new{ background:#6b8fb5; color:var(--accent-ink); border-color:transparent; }
.rstage-status.s-learning{ background:#d9a441; color:var(--accent-ink); border-color:transparent; }
.rstage-status.s-rough{ background:var(--danger); color:#fff; border-color:transparent; }
.rstage-status.s-polishing{ background:#9ccf8e; color:var(--accent-ink); border-color:transparent; }
.rstage-status.s-gigready{ background:var(--good); color:var(--accent-ink); border-color:transparent; }
.rstage-status.s-shelved{ background:#6d6355; color:var(--text); border-color:transparent; }
.rstage-status option{ background:var(--surface-2); color:var(--text); }

/* The personal-note region: one amber block per member (or your own). Phase 38b:
   a flex layout instead of an equal grid, so the wide TV gets used gracefully — a
   LYRICS card (.rstage-mnote-lyrics) claims a wide reading lane while CUE cards
   stay compact beside it. Cards wrap to new rows when the screen is narrow. */
.rstage-notes{
  display:flex; flex-wrap:wrap; gap:14px; width:100%; margin:4px auto 0;
  justify-content:center; align-items:flex-start;
}
/* v151: in REHEARSAL the Leader Note sits directly above these cards, so it
   wears the same width — 820px-vs-460px read as mismatched furniture (Alex).
   Stage Mode is untouched: there the cue box is 820 too, so they already match.
   ⚠️ this max-width MUST equal .rstage-mnote's below — a test compares them. */
.stage.rehearsal .stage-canon{ max-width:460px; }
.rstage-mnote{
  flex:1 1 340px; max-width:460px;   /* cue card: compact, grows only to fill a gap */
  display:flex; flex-direction:column; align-items:center; gap:8px;
  background:var(--accent-soft, rgba(232,144,63,.08)); border:1px solid var(--accent-line, rgba(232,144,63,.35));
  border-radius:16px; padding:clamp(10px,2vh,14px) 16px;
}
/* A lyrics note is the centerpiece: a big flex weight so it takes the lion's share
   of the leftover width, capped at a readable line length (dial it with Width ↤↦,
   which drives --note-w inside the card). One lyrics + a couple of cues then lays
   out as a wide lyrics column with tidy cue cards alongside. */
.rstage-mnote-lyrics{ flex:100 1 620px; max-width:1100px; }
/* Phase 38: the card head is a row — owner name on the left, its controls (the
   per-card layout toggle + ✎ Edit) on the right. */
.rstage-mnote-lbl{
  display:flex; align-items:center; justify-content:space-between; gap:8px 12px;
  flex-wrap:wrap; width:100%;
}
/* v151: the owner name / "My note" label is CHROME, like .stage-canon-lbl and
   the Edit button — it does NOT track A−/A+ (Alex: the label growing with the
   note text is noise). The viewport clamp alone keeps it legible from across
   the room on a TV; A+ is for note CONTENT. This replaced a Phase 28b rule
   that multiplied by --stage-scale. */
.rstage-mnote-name{
  font-weight:800; letter-spacing:.6px; text-transform:uppercase; color:var(--accent);
  font-size:clamp(.8rem, min(2.6vw,3vh), 1.35rem);
}
.rstage-card-ctls{ display:inline-flex; align-items:center; gap:6px; flex:0 0 auto; }
/* The card's Edit button stays a comfortable, fixed tap size — it's chrome, so it
   does NOT scale with --stage-scale like the note text. (Phase 57 removed the
   per-card Auto/Cue/Lyrics toggle that shared these rules.) */
.rstage-noteedit{
  appearance:none; cursor:pointer; min-height:34px; border-radius:999px;
  padding:5px 12px; font-weight:800; font-size:.8rem; white-space:nowrap; line-height:1;
}
.rstage-noteedit{ background:transparent; color:var(--accent); border:1px solid rgba(232,144,63,.55); }
.rstage-noteedit:hover{ background:rgba(232,144,63,.16); }
/* The slim control row above a member's own lyrics teleprompter (which has no card
   box of its own). */
.rstage-mynote-ctl{
  display:flex; align-items:center; justify-content:center; gap:12px; flex-wrap:wrap;
  width:100%; margin:2px auto 0;
}
.rstage-mnote-body{ display:flex; flex-direction:column; align-items:center; gap:8px; width:100%; }

/* Phase 43: the stem player docked at the bottom of Rehearsal Mode. It's a normal
   in-flow flex child (the last one), so opening it pushes the set nav + reading
   surface up rather than covering them — the ‹ Prev / Next › buttons stay tappable
   right above it. Capped height with its own scroll so a tall mixer never eats the
   whole screen. Lives inside .stage so it paints in real fullscreen. */
.rstage-player{
  flex:0 0 auto; box-sizing:border-box; width:100%;
  max-height:52vh; overflow-y:auto; overflow-x:hidden;
  margin-top:8px; padding-top:8px; border-top:1px solid var(--border);
}
.rstage-player[hidden]{ display:none; }
.rstage-player-body{ max-width:var(--maxw); margin:0 auto; width:100%; }
/* The ▸ launch button lights up teal (the shared "play" lane) while the player is open. */
.stage-tb.stage-play.is-on{ color:var(--play); border-color:var(--play); }
.stage-tb.stage-play .hp-ico{ width:22px; height:22px; }

/* ===== AUTO-SCROLL (Phase 49) — self-contained; delete this block to unwind ===== */
/* ===== AUTO-SCROLL RAIL (Phase 57 batch 2) ==========================
   Was a horizontal pill sitting directly above ‹ Prev / Next ›. That put the
   only mid-song control on the app's worst piece of real estate: reach for
   play, miss, and you've jumped the singer off the tune in front of a room.
   Now it's a vertical rail on the right of the reading area — ＋ on top, ▶ in
   the middle, − at the bottom (volume convention, more is up).

   IT FLOATS OVER THE LYRICS ON PURPOSE, and does not reserve a gutter. That's
   safe because of the always-on tail pad (Phase 50 v111): the last line still
   scrolls up into the reading zone, so the bottom of the box is structurally
   empty for the whole song, end included. Narrowing the lyrics on 154 songs to
   protect space that is blank by design would be a bad trade.

   Two rules make the float work:
     • pointer-events are OFF on the rail and ON only on the buttons, so the gaps
       between them still pass touches through to the lyrics — where a touch
       means "pause", which is the gesture that must keep working.
     • each button carries its own scrim, because a 35%-opacity control sitting
       on text is a two-way legibility problem: the glyph gets muddy AND so does
       the word underneath it.
   Vertical placement is set from JS (positionRail) against the scroller. */
.stage-asr{
  position:absolute; z-index:6;
  transform:translateY(-50%);
  display:flex; flex-direction:column; align-items:center; gap:10px;
  pointer-events:none;                 /* gaps stay transparent to a pause tap */
  opacity:1; transition:opacity .35s ease;
}
.stage-asr[hidden]{ display:none; }
/* Fade DOWN, never out. At 0 it would be a secret; at ~35% it stays findable in
   a dark room without competing with the line being sung. Any tap on the rail
   wakes it (.is-awake) for a couple of seconds. */
.stage-asr.is-run{ opacity:.35; }
.stage-asr.is-run.is-awake{ opacity:1; }

.stage-asr-step, .stage-asr-go{
  pointer-events:auto;
  touch-action:manipulation;           /* the tap can't become a scroll gesture */
  appearance:none; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
  border-radius:14px; font-weight:800; line-height:1;
  -webkit-backdrop-filter:blur(7px); backdrop-filter:blur(7px);
  box-shadow:0 2px 10px rgba(0,0,0,.45);
}
.stage-asr-step{
  width:52px; height:48px; font-size:1.5rem;
  border:1px solid var(--border);
  /* Solid first, translucent second. color-mix is used elsewhere in this sheet,
     but only for focus rings and tints that degrade to nothing much. Here it's
     the BACKGROUND of a button floating over lyrics — if it were skipped on an
     older iPad the button would fall back to transparent and become an unreadable
     outline sitting on the words. */
  background:var(--surface-2);
  background:color-mix(in srgb, var(--surface-2) 82%, transparent);
  color:var(--text);
}
.stage-asr-step:active{ transform:scale(.94); }
/* the one thing you reach for mid-song */
.stage-asr-go{
  width:52px; height:56px; font-size:1.25rem;
  border:1px solid var(--play);
  background:var(--play);
  background:color-mix(in srgb, var(--play) 88%, transparent);
  color:var(--play-ink);
}
.stage-asr-go:active{ transform:scale(.95); }

/* The tempo readout FLASHES on a nudge and goes. It sits ABOVE the rail rather
   than beside it: level with the buttons it lands in the middle of a lyric line
   and reads as part of the words, which is worse than useless for something you
   glance at for a second. Right-aligned to the rail so a long value ("−20%")
   grows inward, away from the screen edge. Safe to be transient at all only
   because tempo resets to 100% every song change. */
.stage-asr-val{
  position:absolute; bottom:100%; right:0; margin-bottom:9px;
  pointer-events:none; white-space:nowrap;
  font-weight:800; font-variant-numeric:tabular-nums; font-size:.95rem;
  color:var(--text); background:var(--chip-scrim, rgba(0,0,0,.55)); border-radius:8px;
  -webkit-backdrop-filter:blur(7px); backdrop-filter:blur(7px);
  padding:4px 9px;
  opacity:0; transition:opacity .25s ease;
}
.stage-asr-val:empty{ display:none; }
.stage-asr-val.is-on{ opacity:1; }

/* In leader Rehearsal the note cards run to the right edge and carry an ✎ Edit
   in each card head, which the rail's buttons could sit on top of. This is a
   card inset, NOT a narrowing of the lyrics lane — it applies only while the
   rail is actually showing. */
.stage.as-rail-on .rstage-notes{ padding-right:64px; }

@media (max-width:520px){
  .stage-asr{ gap:8px; }
  .stage-asr-step{ width:46px; height:44px; font-size:1.35rem; }
  .stage-asr-go{ width:46px; height:50px; font-size:1.1rem; }
  .stage-asr-val{ font-size:.85rem; }
  .stage.as-rail-on .rstage-notes{ padding-right:56px; }
}
/* trailing spacer (auto-scroll): keep its measured height even inside a flex-column reading lane */
.stage-as-tailpad{ flex:0 0 auto; width:100%; pointer-events:none; }
.stage-cal{ position:absolute; inset:0; z-index:55; box-sizing:border-box; display:flex; align-items:flex-start; justify-content:center; padding:max(20px,4vh) 16px 16px; overflow:auto; background:rgba(0,0,0,.6); }
.stage-cal[hidden]{ display:none; }
.stage-cal-card{ width:100%; max-width:640px; box-sizing:border-box; background:var(--surface); color:var(--text); border:1px solid var(--border); border-radius:16px; box-shadow:0 24px 70px rgba(0,0,0,.5); padding:16px; }
.stage-cal-head{ display:flex; align-items:center; gap:10px; }
.stage-cal-title{ font-weight:800; font-size:1.05rem; flex:1; }
.stage-cal-hint{ color:var(--muted); font-size:.85rem; margin:8px 0 12px; line-height:1.4; }
.stage-cal-body{ display:flex; flex-direction:column; gap:2px; }
.stage-cal-row{ display:flex; align-items:center; gap:10px; padding:3px 0; }
.stage-cal-t{ flex:0 0 auto; width:64px; text-align:center; background:var(--surface-2); border:1px solid var(--border); color:var(--text); border-radius:8px; padding:6px; font-variant-numeric:tabular-nums; font-weight:700; }
.stage-cal-t:focus{ outline:2px solid var(--play); outline-offset:1px; }
.stage-cal-line{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* v141 — calibrator readability + honesty.
   .stage-cal-sect: a stanza's FIRST line (the blank-line boundaries in the sheet
   mark them) gets breathing room and full-strength text, so the verse/chorus
   starts a leader actually times are findable in a wall of lines; the rows in
   between stay slightly muted. .stage-cal-bad: a time that runs BACKWARDS
   against an earlier line is flagged red at the input — before this it was
   silently sorted and clamped into a "hold", which made typos invisible.
   .stage-cal-meta / -warn: the song-length line, and the stale-timing warning
   when the lyrics changed after calibration. */
.stage-cal-body.has-sects .stage-cal-line{ color:var(--muted); }
.stage-cal-body.has-sects .stage-cal-row.stage-cal-sect{ margin-top:14px; }
.stage-cal-body.has-sects .stage-cal-row.stage-cal-sect .stage-cal-line{ color:var(--text); font-weight:700; }
.stage-cal-body.has-sects .stage-cal-row:first-child{ margin-top:0; }
.stage-cal-t.stage-cal-bad{ border-color:var(--danger); color:var(--danger); }
/* v143 — the ▶ point row: where on the RECORDING the singer presses play. Reads
   as one sentence with the input inline, set apart from the per-line rows below
   by a hairline so the two kinds of number don't blur together. */
.stage-cal-offrow{
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  margin:12px 0 0; padding:10px 0 0; border-top:1px solid var(--border);
  font-weight:700; font-size:.92rem;
}
.stage-cal-offhint{ color:var(--muted); font-size:.82rem; margin:6px 0 0; line-height:1.4; }
.lt-edhost{ margin-top:12px; }
/* v161 — the Lyrics & Timing dialog SIDE BY SIDE (≥1100px, decided in notes.js
   openLyricsTiming): the sheet editor on the left, the calibrator on the right.
   The card is pinned to the viewport height and each column scrolls on its
   own — the editor's toolbar stays put while a long song scrolls inside the
   box, and the timing rows scroll beside it — so a surgical mid-rehearsal fix
   never means losing sight of the timing. The calibrator still paints its own
   .stage-cal-card inside the right column; that inner card is flattened here
   (no border, no shadow, no padding) so it reads as a pane, not a card in a
   card. Below the breakpoint none of this applies and the stacked flow is
   untouched. */
.stage-cal-card.lt-wide{
  max-width:1180px; height:calc(100vh - max(20px,4vh) - 16px); box-sizing:border-box;
  /* A fixed height (not max-height) is what lets the 1fr row track resolve, so
     the columns are CLIPPED to the card and scroll — with an indefinite height
     the track sizes to its content and both columns run off the bottom. */
  display:grid; grid-template-columns:minmax(0,1fr) minmax(0,1fr); grid-template-rows:minmax(0,1fr); gap:0 24px; align-items:stretch;
}
.lt-wide .lt-col{ min-width:0; min-height:0; display:flex; flex-direction:column; overflow:hidden; }
.lt-wide .lt-col-cal{ overflow:auto; border-left:1px solid var(--border); padding-left:24px; }
.lt-wide .lt-col-cal > .stage-cal-card{ max-width:none; border:0; box-shadow:none; padding:0; background:transparent; border-radius:0; }
.lt-wide .lt-col-edit .lt-edhost{ flex:1 1 auto; min-height:0; display:flex; flex-direction:column; }
.lt-wide .lt-col-edit .pnote{ flex:1 1 auto; min-height:0; display:flex; flex-direction:column; }
.lt-wide .lt-col-edit .pnote-editor{ flex:1 1 auto; min-height:14em; max-height:none; }
.lt-wide .lt-cal-empty{ margin-top:0; }
.stage-cal-meta{ color:var(--muted); font-size:.85rem; margin:8px 0 0; }
.stage-cal-warn{ color:var(--danger); font-size:.85rem; margin:8px 0 0; line-height:1.4; font-weight:700; }
.stage-cal-warn[hidden], .stage-cal-meta[hidden]{ display:none; }
/* v146 — the rehearsal-nudge telemetry line (leader-only data; renders only
   when the GET action delivers it). Quiet like the meta line, with the Retime
   button inline so the fix sits beside the evidence. */
.stage-cal-nudges{ color:var(--muted); font-size:.85rem; margin:8px 0 0; line-height:1.7; }
.stage-cal-scale{ margin-left:8px; vertical-align:middle; }
/* v146 — the flat-shift row. The input mirrors .stage-cal-t's look but is
   DELIBERATELY a different class: collect()/validate harvest row inputs by
   .stage-cal-t, and the ▶-point field already documented that trap (v143). */
.stage-cal-shiftrow{
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
  margin:10px 0 0; font-weight:700; font-size:.92rem;
}
.stage-cal-shift-in{
  flex:0 0 auto; width:72px; text-align:center; background:var(--surface-2);
  border:1px solid var(--border); color:var(--text); border-radius:8px;
  padding:6px; font-variant-numeric:tabular-nums; font-weight:700;
}
.stage-cal-shift-in:focus{ outline:2px solid var(--play); outline-offset:1px; }
.stage-cal-shifthint{ color:var(--muted); font-size:.8rem; font-weight:400; }

/* v141 — the preview return pill. While a leader previews an UNSAVED timing
   draft the panel hides and the sheet scrolls for real; this is the one control
   left on screen, floating clear of the lyrics, to get back to editing. Lives on
   .stage so it survives real fullscreen (same reasoning as .stage-as-mark). */
.stage-cal-return{
  position:absolute; right:14px; top:14px; z-index:56;
  appearance:none; border:1px solid var(--border); border-radius:999px;
  background:var(--surface); color:var(--text); font-weight:800; font-size:.95rem;
  padding:10px 16px; min-height:46px; cursor:pointer;
  box-shadow:0 10px 30px rgba(0,0,0,.45);
}
.stage-cal-return:active{ transform:scale(.97); }

/* Phase 38: the in-place Personal Note editor — a dimmed sheet OVER the stage, but
   INSIDE the stage element so it paints in real fullscreen (a body-level modal
   wouldn't). Tapping the ✎ Edit on any card opens it; Done / Esc / backdrop close. */
.rstage-sheet{
  position:absolute; inset:0; z-index:50; box-sizing:border-box;
  display:flex; align-items:flex-start; justify-content:center;
  padding:max(20px, 4vh) 16px 16px; overflow:auto; background:rgba(0,0,0,.55);
}
.rstage-sheet[hidden]{ display:none; }
.rstage-sheet-card{
  width:100%; max-width:720px; box-sizing:border-box;
  background:var(--surface); color:var(--text);
  border:1px solid var(--border); border-radius:16px;
  box-shadow:0 24px 70px rgba(0,0,0,.5); padding:16px;
}
.rstage-sheet-head{
  display:flex; align-items:center; justify-content:space-between; gap:12px; margin-bottom:12px;
}
.rstage-sheet-title{ font-weight:800; font-size:1.05rem; color:var(--text); }
.rstage-cue{
  color:var(--accent); font-weight:800; line-height:1.2; text-align:center;
  font-size:calc(var(--stage-scale,1) * clamp(1.1rem, min(5vw,6vh), 2rem));
  white-space:pre-wrap; word-break:break-word; cursor:text; border-radius:8px; padding:2px 8px;
}
.rstage-cue:hover{ background:var(--accent-soft-2, rgba(232,144,63,.16)); }
.rstage-add{
  appearance:none; cursor:pointer; background:transparent; color:var(--muted);
  border:1px dashed var(--border); border-radius:999px; padding:7px 16px; font-weight:700; font-size:.85rem;
}
.rstage-add:hover{ color:var(--text); border-color:var(--muted); }
.rstage-add-canon{ color:var(--play); border-color:var(--play); margin:2px auto 0; }
.rstage-add-canon:hover{ color:var(--play); border-color:var(--play); filter:brightness(1.15); }
.rstage-editable{ cursor:text; }
.rstage-hint-empty{ color:var(--muted); font-size:.9rem; text-align:center; margin-top:4px; }

/* The in-place editor (shared by notes + canonical). */
.rstage-edit{
  width:100%; max-width:760px; box-sizing:border-box; resize:none;
  background:var(--surface-2); color:var(--text); border:1px solid var(--accent);
  border-radius:12px; padding:10px 12px; font:inherit; font-weight:700; line-height:1.3;
  font-size:calc(var(--stage-scale,1) * clamp(1rem, min(4vw,5vh), 1.6rem));
}
.rstage-edit:focus{ outline:2px solid var(--accent); outline-offset:1px; }

/* In the short-landscape split, keep the note blocks left-aligned like the cues. */
@media (orientation:landscape) and (max-height:600px){
  .rstage-notes{ margin-top:0; }
  .rstage-mnote{ align-items:flex-start; }
  .rstage-mnote-body{ align-items:flex-start; }
  .rstage-cue{ text-align:left; font-size:calc(var(--stage-scale,1) * clamp(1rem, min(3vw,7vh), 1.6rem)); }
  .rstage-ctl{ justify-content:flex-start; }
}

/* ============================================================= *
 * Phase 29 — floating YouTube reference player (draggable, over  *
 * the page and over Rehearsal Mode). Teal ▶ launch button.       *
 * ============================================================= */
.stage-yt{ color:var(--play); }
.ytfloat{
  position:fixed; z-index:90; width:460px; max-width:calc(100vw - 8px);
  background:var(--surface-2); border:1px solid var(--border); border-radius:12px;
  box-shadow:0 14px 44px rgba(0,0,0,.55); overflow:hidden; display:flex; flex-direction:column;
}
.ytfloat[hidden]{ display:none; }
.ytfloat-bar{
  display:flex; align-items:center; gap:8px; padding:6px 6px 6px 12px;
  cursor:move; user-select:none; touch-action:none; border-bottom:1px solid var(--border);
}
.ytfloat-title{ flex:1; font-weight:800; font-size:.85rem; color:var(--text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.ytfloat-acts{ display:flex; gap:2px; }
.ytfloat-btn{
  appearance:none; border:0; background:transparent; color:var(--muted); cursor:pointer;
  width:32px; height:32px; border-radius:8px; font-size:1rem; line-height:1;
  display:flex; align-items:center; justify-content:center;
}
.ytfloat-btn:hover{ background:var(--border); color:var(--text); }
.ytfloat-body{ position:relative; width:100%; aspect-ratio:16/9; background:#000; }
.ytfloat-body > #ytFloatMount, .ytfloat-body iframe{ position:absolute; inset:0; width:100%; height:100%; border:0; }
.ytfloat-shield{ position:absolute; inset:0; z-index:2; }         /* eats pointer events over the iframe while dragging */
.ytfloat-shield[hidden]{ display:none; }
.ytfloat-fallback{
  position:absolute; inset:0; z-index:3; display:flex; flex-direction:column;
  align-items:center; justify-content:center; gap:12px; padding:16px; text-align:center;
  background:var(--surface-2); color:var(--text);
}
.ytfloat-fallback[hidden]{ display:none; }
.ytfloat-fb-msg{ font-weight:700; color:var(--muted); font-size:.9rem; }
.ytfloat-resize{
  position:absolute; right:0; bottom:0; width:18px; height:18px; z-index:4;
  cursor:nwse-resize; touch-action:none; opacity:.55;
  background:linear-gradient(135deg, transparent 55%, var(--muted) 55%, var(--muted) 64%, transparent 64%, transparent 76%, var(--muted) 76%, var(--muted) 85%, transparent 85%);
}
.ytfloat-resize:hover{ opacity:1; }

/* ============================================================= *
 * Phase 24 — personal "My notes" toggle (per device) + the      *
 * inline shorthand it reveals on repertoire cards + practice    *
 * rows. Muted/neutral so it never competes with the teal        *
 * canonical or amber status cues.                               *
 * ============================================================= */

/* BATCH 135: the .mynotes-toggle button rules are DELETED, on all three surfaces.
   The inline-shorthand styling below stays — it's what renders a note, and notes
   still render. Only the switch is gone. If you're bringing a toggle back, bring
   back a reason with it: this one survived to Phase 135 controlling one thing
   (whether your own short cues showed on a row) that nobody ever turned off. */

/* Inline shorthand on a repertoire card. */
.card-mynote{
  margin-top:10px; padding:8px 11px;
  background:var(--surface-2); border:1px dashed var(--border); border-radius:8px;
  display:flex; flex-direction:column; gap:5px;
}
/* A note line is a flex row (icon + text) so an in-place editor can sit where the
   text was. Editable on the repertoire card only — practice/setlist use their own
   .qmynote-line / .smynote-line classes and stay read-only. */
.mynote-line{
  display:flex; align-items:flex-start; gap:6px;
  color:var(--text); font-size:.9rem; line-height:1.4;
  cursor:pointer; border-radius:6px; padding:2px 5px; margin:-2px -5px;
  transition:background .12s ease;
}
.mynote-line:hover{ background:var(--surface); }
.mynote-ic{ opacity:.85; flex:none; }
.mynote-text{ flex:1 1 auto; min-width:0; white-space:pre-wrap; word-break:break-word; }
/* Armed (first click): underline the text to say "click again to edit". */
.mynote-line.armed{ background:var(--surface); }
.mynote-line.armed .mynote-text{ text-decoration:underline; text-decoration-color:var(--play); text-underline-offset:3px; }
.mynote-line.editing{ background:transparent; cursor:default; }
.mynote-edit{
  flex:1 1 auto; width:100%; min-width:0; resize:none; overflow:hidden;
  font:inherit; color:var(--text); background:var(--surface);
  border:1px solid var(--play); border-radius:6px; padding:4px 8px; line-height:1.4;
}
.mynote-edit:focus{ outline:none; box-shadow:0 0 0 2px color-mix(in srgb, var(--play) 35%, transparent); }

/* Phase 31: empty "Add note" state. No dashed frame around it (that box is for a
   real note) — just a quiet dashed button that invites the first personal note.
   When it opens an editor, the .mynote-line.editing lives inside this same wrap. */
.card-mynote.empty{ background:transparent; border:0; padding:0; margin-top:8px; }
.mynote-add{
  display:inline-flex; align-items:center; gap:6px;
  font:inherit; font-size:.88rem; color:var(--muted);
  background:transparent; border:1px dashed var(--border); border-radius:8px;
  padding:6px 11px; cursor:pointer;
  transition:color .12s ease, border-color .12s ease, background .12s ease;
}
.mynote-add:hover{ color:var(--text); border-color:var(--play); background:var(--surface-2); }
.mynote-add:focus-visible{ outline:none; border-color:var(--play);
  box-shadow:0 0 0 2px color-mix(in srgb, var(--play) 35%, transparent); }
.card-mynote.empty .mynote-line.editing{ width:100%; padding:0; margin:0; cursor:default; }

/* ===================== Phase 33: rich Personal Notes =====================
   Inline formatting carried by CLASSES (theme-safe + sanitizer-simple). A
   highlight brings its own dark text, so it reads on ANY note background. */
mark.hl-amber{ background:#ffd54a; color:#1a1400; }
mark.hl-green{ background:#a6e07a; color:#0e1a06; }
mark.hl-pink { background:#ff9ec4; color:#2a0716; }
mark.hl-blue { background:#8fd3ff; color:#05131f; }
mark[class^="hl-"]{ padding:0 .14em; border-radius:3px; }
/* v161 — A HIGHLIGHT IS BOLD. The toolbar's B button is gone: Alex never
   highlighted without bolding or bolded without highlighting, so the weight now
   comes with the mark on every surface (editor, Stage, Rehearsal, previews,
   print). Older <strong><mark> cues render exactly as before; older bare marks
   simply gain the weight. No stored note changed. */
mark[class^="hl-"]{ font-weight:700; }

/* v161 — the SECTION LABEL: a quiet structural line ("[CHORUS]", "GUITAR SOLO")
   that a singer's eye skips while still marking the song's shape. Small, spaced
   caps in the muted ink — every theme's --muted is already contrast-checked
   against its own background, so no per-theme override is needed. `.sect` is
   the stored inline wrapper (editor, cue layout, previews); `.ll-sect` is the
   lyric LINE renderLyrics stamps when a whole line is a label. Labels are
   structure, not lyrics: the timing tools skip them (UI.timingLines), so adding
   one to a calibrated song moves no anchor and flags nothing stale. */
.sect, .ll-sect{ font-size:.68em; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); }
.ll-sect .sect{ font-size:inherit; }
.stage-cue-lyrics .ll.ll-sect, .rstage-lyrics .ll.ll-sect{ padding-top:.35em; line-height:1.4; }
.pn-sect{ min-width:auto; padding:0 10px; font-size:.76rem; font-weight:700; letter-spacing:.08em; text-transform:uppercase; }
/* The calibrator shows a label as a heading over the rows it introduces. */
.stage-cal-sechead{ margin:14px 0 2px; font-size:.72rem; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); }
.stage-cal-body .stage-cal-sechead:first-child{ margin-top:0; }
.stage-cal-body.has-sects .stage-cal-sechead + .stage-cal-row.stage-cal-sect{ margin-top:0; }

/* Text colours — RETIRED from the toolbar in v161 (unreadable at stage distance)
   but kept so any stored note that used one still renders. Bright by default
   (dark surfaces); darkened under the light note theme and on Paper. */
.c-red{ color:#ff6b5e; } .c-amber{ color:#f0a83a; } .c-green{ color:#6cd67f; } .c-blue{ color:#6bb6ff; }

/* --- the rich editor (in the Notes popover) --- */
.pnote{ margin-top:6px; }
.pnote-head{ display:flex; align-items:baseline; gap:10px; }
.pnote-status{ color:var(--good); font-size:.82rem; font-weight:700; }
.pnote-toolbar{ display:flex; flex-wrap:wrap; align-items:center; gap:6px; margin:8px 0; }
.pn-tool{ min-width:32px; height:30px; border:1px solid var(--border); background:var(--surface-2);
  color:var(--text); border-radius:7px; cursor:pointer; font-size:.95rem; line-height:1; }
.pn-tool:hover{ border-color:var(--play); }
.pn-sw-row{ display:inline-flex; gap:4px; align-items:center; padding:0 2px; }
.pn-sw{ width:22px; height:22px; border-radius:6px; border:1px solid rgba(0,0,0,.3); cursor:pointer; padding:0; }
.pn-sw.hl-amber{ background:#ffd54a; } .pn-sw.hl-green{ background:#a6e07a; }
.pn-sw.hl-pink{ background:#ff9ec4; } .pn-sw.hl-blue{ background:#8fd3ff; }
/* .pn-co — DORMANT since v161 (the colour swatches left the toolbar). Kept so
   putting them back is a revert, like the .nt-* rules further down. */
.pn-sw.pn-co{ position:relative; background:var(--surface-2); }
.pn-sw.pn-co::after{ content:"A"; position:absolute; inset:0; display:flex; align-items:center;
  justify-content:center; font-weight:800; font-size:.8rem; }
.pn-sw.pn-co.c-red::after{ color:#ff6b5e; } .pn-sw.pn-co.c-amber::after{ color:#f0a83a; }
.pn-sw.pn-co.c-green::after{ color:#6cd67f; } .pn-sw.pn-co.c-blue::after{ color:#6bb6ff; }
/* Phase 39: toolbar reflects what's active under the caret. Bold presses in; the
   live highlight/colour swatch gets a ring so you can see (and toggle) it. */
.pn-tool[aria-pressed="true"]{ border-color:var(--play); color:var(--play);
  background:color-mix(in srgb, var(--play) 14%, var(--surface-2)); }
.pn-sw.is-active{ box-shadow:0 0 0 2px var(--surface-2), 0 0 0 4px var(--play); }
.pn-clearall{ min-width:auto; padding:0 10px; font-size:.8rem; font-weight:600; letter-spacing:.02em; }
.pnote-editor{ min-height:120px; max-height:52vh; overflow:auto; padding:10px 12px;
  border:1px solid var(--border); border-radius:10px; background:var(--surface-2); color:var(--text);
  line-height:1.5; white-space:pre-wrap; word-break:break-word; }
.pnote-editor:focus{ outline:none; box-shadow:0 0 0 2px color-mix(in srgb, var(--play) 35%, transparent); }
.pnote-editor.is-empty::before{ content:attr(data-placeholder); color:var(--muted); pointer-events:none; }
/* DORMANT since v149 — the editor's "Look:" row was retired with the per-note
   theme (see UI.noteThemeClass). Kept, like the .nt-* rules and the two DB
   columns, so putting it back is a revert rather than a rebuild. */
.pnote-themes{ display:flex; flex-wrap:wrap; align-items:center; gap:6px; margin-top:8px; }
.pn-lbl{ color:var(--muted); font-size:.85rem; }
.pn-theme{ padding:4px 10px; border:1px solid var(--border); background:var(--surface-2);
  color:var(--text); border-radius:999px; cursor:pointer; font-size:.85rem; }
.pn-theme[aria-pressed="true"]{ border-color:var(--play); color:var(--play); }

/* --- card + practice rich preview: clamped to 2 lines, app-themed (the note's
   own theme only kicks in on the reading surfaces below) --- */
.mynote-rich, .qmynote-rich{ display:flex; gap:6px; align-items:flex-start; }
.mynote-rich{ cursor:pointer; }
.mynote-preview, .qmynote-preview, .smynote-preview{ flex:1 1 auto; min-width:0; line-height:1.4;
  white-space:pre-wrap; word-break:break-word;
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }

/* --- note display themes: a reading surface the singer controls. Defined AFTER
   the editor + stage rules so they win for the shared bg/color properties. --- */
.nt-light{ background:#ffffff; color:#141414; }
.nt-dark{ background:#0c0c0c; color:#f4f4f4; }
.nt-contrast{ background:#000000; color:#ffe14d; }
.nt-light .c-red{ color:#d1362b; } .nt-light .c-amber{ color:#b26a00; }
.nt-light .c-green{ color:#237a30; } .nt-light .c-blue{ color:#1f6fc4; }
/* Give a themed cue/lyrics panel padding so the colour block reads as a surface. */
.stage-cue[class*="nt-"], .rstage-cue[class*="nt-"], .pnote-editor[class*="nt-"],
.stage-cue-lyrics[class*="nt-"], .rstage-lyrics[class*="nt-"]{
  padding:.5em .7em; border-radius:8px; }

/* ---- Phase 33 (3b): the teleprompter view ----
   A "Lyrics" note becomes a full-width, big-line-height panel, sized off the same
   A−/A+ text scale as the rest of the stage. Target: portrait iPad.
   (Phase 57 removed the Auto/Cue/Lyrics toggle this section used to describe —
   which layout a note gets is now auto-detected from the note itself.) */
/* A lyrics note reads as a document: left-aligned, roomy line-height, sized off
   the A−/A+ scale. It does NOT scroll inside itself — in teleprompter mode the
   WHOLE stage scrolls (below), so the header can scroll away once it's been read. */
.stage-cue.stage-cue-lyrics, .rstage-lyrics{
  width:100%; max-width:100%; text-align:left; white-space:pre-wrap; word-break:break-word;
  line-height:1.5; padding:.3em .5em; border-radius:10px;
  font-size:calc(var(--stage-scale,1) * clamp(1.05rem, min(4.6vw,4.6vh), 2.3rem));
}
.rstage-lyrics{ color:inherit; }
/* Phase 36: each authored lyric line is its own .ll block so a too-wide line
   wraps with a HANGING INDENT (the runover tucks under the line, reading as the
   same line rather than a new one). The indent + blank-line height are also set
   INLINE from UI.renderLyrics so they hold against a stale cached stylesheet;
   these rules are the declarative mirror. */
.stage-cue-lyrics .ll, .rstage-lyrics .ll{
  padding-left:1.5em; text-indent:-1.5em; white-space:pre-wrap;
}
.stage-cue-lyrics .ll:empty, .rstage-lyrics .ll:empty{ height:.55em; }

/* Teleprompter mode (a lyrics note is showing). The heavy lifting is done INLINE
   from JS now (the lyrics note's width/scroll/chrome), because that survives a
   stale stylesheet and works for both the gig stage AND rehearsal's own note box.
   This just top-aligns the scroll so the title + Leader Note scroll off cleanly. */
.stage-main.lyrics-on{ flex-direction:column; justify-content:flex-start; align-items:center; text-align:center; gap:10px; }

/* "Up next" is a FIXED footer between the scroll area and the Prev/Next buttons —
   out of the scroll flow, so it can never land on the lyrics. */
.stage-upnext-bar{
  flex:0 0 auto; text-align:center; padding:5px 12px; line-height:1.2;
  border-top:1px solid var(--border);
}
.stage-upnext-bar:empty{ display:none; }
.stage-upnext-lbl{ color:var(--muted); font-size:.82rem; font-weight:700; text-transform:uppercase; letter-spacing:.5px; }
.stage-upnext-song{ color:var(--accent); font-weight:800; }
.stage-upnext-lbl.stage-end{ color:var(--good); text-transform:none; }
/* The Width −/+ control's visibility is toggled inline from JS (robust); this is
   just a fallback so it's hidden before the first render. */
.stage-width .stage-size-lbl{ min-width:0; }

/* Inline shorthand on a practice row (matches the .qarrnote rhythm). */
.qmynote{
  margin-top:8px; padding:8px 11px;
  background:var(--surface-2); border-left:3px solid var(--muted);
  border-radius:6px; display:flex; flex-direction:column; gap:5px;
}
/* Phase 27: note lines are editable in place (like the repertoire). Flex row so
   the in-place editor can sit where the text was; armed = underline; editing =
   textarea. Setlist keeps its own read-only .smynote-line, untouched. */
.qmynote-line{
  display:flex; align-items:flex-start; gap:6px;
  color:var(--text); font-size:.92rem; line-height:1.45;
  cursor:pointer; border-radius:6px; padding:2px 5px; margin:-2px -5px;
  transition:background .12s ease;
}
.qmynote-line:hover{ background:var(--surface); }
.qmynote-ic{ opacity:.85; flex:none; }
.qmynote-text{ flex:1 1 auto; min-width:0; white-space:pre-wrap; word-break:break-word; }
.qmynote-line.armed{ background:var(--surface); }
.qmynote-line.armed .qmynote-text{ text-decoration:underline; text-decoration-color:var(--play); text-underline-offset:3px; }
.qmynote-line.editing{ background:transparent; cursor:default; }
.qmynote-edit{
  flex:1 1 auto; width:100%; min-width:0; resize:none; overflow:hidden;
  font:inherit; color:var(--text); background:var(--surface);
  border:1px solid var(--play); border-radius:6px; padding:4px 8px; line-height:1.4;
}
.qmynote-edit:focus{ outline:none; box-shadow:0 0 0 2px color-mix(in srgb, var(--play) 35%, transparent); }

/* Keep a rehearsed (folded) practice row clean — hide the shorthand like the arr note. */
.qitem.done .qmynote{ display:none; }

/* Inline shorthand on a setlist row (Phase 25) — same rhythm as the practice
   tool's .qmynote, so the two tools read the same. */
.smynote{
  margin-top:8px; padding:8px 11px;
  background:var(--surface-2); border-left:3px solid var(--muted);
  border-radius:6px; display:flex; flex-direction:column; gap:5px;
}
.smynote-line{ display:flex; align-items:flex-start; color:var(--text); font-size:.92rem; line-height:1.45; }
.smynote-ic{ margin-right:6px; opacity:.85; flex:none; }

/* ===== Leader backups & restore (backups.js) ===================== */
.whoami-tools{ margin:10px 0 4px; }
.whoami-backups{ text-align:left; }
.backups-modal{ max-width:640px; width:min(640px,94vw); }
.backups-body{ max-height:min(70vh,640px); overflow:auto; -webkit-overflow-scrolling:touch; }
.bk-intro{ margin:-2px 0 12px; }
.bk-loading, .bk-empty, .bk-empty-sm, .bk-err{ color:var(--muted); padding:12px 2px; }
.bk-err{ color:var(--danger, #c0392b); }
.bk-empty-sm{ padding:6px 2px; font-size:.9rem; }
.bk-create{ display:flex; gap:8px; margin-bottom:14px; }
.bk-create #bkLabel{ flex:1 1 auto; min-width:0; padding:9px 12px; border:1px solid var(--border); border-radius:10px; background:var(--bg); color:var(--text); font-size:.95rem; }
.bk-create .btn{ flex:none; white-space:nowrap; }
.bk-list{ display:flex; flex-direction:column; gap:8px; }
.bk-row{ display:flex; gap:10px; align-items:center; justify-content:space-between; border:1px solid var(--border); border-radius:12px; padding:10px 12px; background:var(--card, var(--bg)); }
.bk-row-main{ min-width:0; }
.bk-when{ font-weight:700; color:var(--text); }
.bk-label{ color:var(--text); font-size:.94rem; margin-top:1px; }
.bk-counts{ color:var(--muted); font-size:.82rem; margin-top:2px; }
.bk-badge{ display:inline-block; font-size:.7rem; font-weight:700; text-transform:uppercase; letter-spacing:.03em; color:var(--muted); border:1px solid var(--border); border-radius:6px; padding:0 5px; vertical-align:middle; }
.bk-row-btns{ display:flex; gap:6px; flex:none; flex-wrap:wrap; justify-content:flex-end; }
.bk-armed{ background:var(--danger, #c0392b) !important; border-color:var(--danger, #c0392b) !important; color:#fff !important; }
.bk-detail-head{ display:flex; align-items:center; gap:10px; margin-bottom:8px; }
.bk-detail-when{ color:var(--muted); font-size:.85rem; }
.bk-sec{ margin-top:14px; }
.bk-sec-h{ font-weight:800; color:var(--text); border-bottom:1px solid var(--border); padding-bottom:5px; margin-bottom:8px; }
/* v162: ~150 songs in the Songs section — the filter keeps single-song restore
   usable. type=search isn't in the base input rule's selector list, so it gets
   the same surface tokens here. */
.bk-songfilter{ width:100%; margin:2px 0 8px; padding:8px 12px; font-size:.95rem; min-height:40px;
  color:var(--text); background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); }
/* Rule 21, third sighting: .bk-item is flex, so the UA's [hidden] loses to it —
   the filter set `hidden` and every row stayed visible (the screenshot caught
   it, as ever). */
.bk-item[hidden]{ display:none !important; }
.bk-sec-n{ color:var(--muted); font-weight:600; font-size:.85rem; margin-left:4px; }
.bk-grp{ margin:8px 0; }
.bk-grp-h{ font-weight:700; color:var(--play, var(--text)); font-size:.9rem; margin:8px 0 4px; }
.bk-item{ display:flex; gap:10px; align-items:center; justify-content:space-between; padding:8px 4px; border-bottom:1px solid var(--border-soft, var(--border)); }
.bk-item-main{ min-width:0; }
.bk-item-t{ color:var(--text); font-weight:600; font-size:.94rem; }
.bk-item-sub{ color:var(--muted); font-size:.82rem; margin-top:1px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:min(60vw,420px); }
.bk-restore{ flex:none; }
.bk-done{ background:var(--play, #2e7d32) !important; border-color:var(--play, #2e7d32) !important; color:var(--play-ink,#fff) !important; }
@media (max-width:560px){
  .bk-row{ flex-direction:column; align-items:stretch; }
  .bk-row-btns{ justify-content:flex-start; }
  .bk-item-sub{ max-width:70vw; }
}

/* full rollback panel (backups detail view) */
.bk-rollback{ margin:12px 0 4px; border:1px solid var(--danger, #c0392b); border-radius:12px; padding:10px 12px; background:rgba(192,57,43,.05); }
.bk-rollback-row{ display:flex; gap:10px; align-items:center; justify-content:space-between; flex-wrap:wrap; }
.bk-rollback-txt{ display:flex; flex-direction:column; min-width:0; }
.bk-rollback-txt strong{ color:var(--danger, #c0392b); }
.bk-rollback-txt span{ color:var(--muted); font-size:.84rem; }
.bk-roll-arm{ flex:none; border-color:var(--danger,#c0392b); color:var(--danger,#c0392b); }
.bk-rollback-confirm{ margin-top:10px; border-top:1px dashed var(--danger,#c0392b); padding-top:10px; }
.bk-rollback-warn{ margin:0 0 10px; font-size:.9rem; color:var(--text); }
.bk-rollback-btns{ display:flex; gap:8px; justify-content:flex-end; }
.bk-rollback-done{ border-color:var(--play, #2e7d32); background:rgba(46,125,50,.06); }
.bk-rollback-result{ display:flex; gap:10px; align-items:center; flex-wrap:wrap; color:var(--text); font-size:.9rem; }
.bk-undo{ flex:none; }

/* =====================================================================
   PHASE 52 — the shared Lyrics sheet
   One leader-owned lyric sheet per song (songs.lyrics_note), standing in
   automatically for anyone who keeps no personal note. Visual language:
   the Leader Note owns the teal --play lane, so lyrics take a quieter
   NEUTRAL card — it's reference material, not an alert.
   ===================================================================== */

/* --- the block inside the 💬 Note popover (below the Leader Note) --- */
.notes-lyrics{
  background:var(--card, rgba(255,255,255,.03));
  border:1px solid var(--border, rgba(255,255,255,.14));
  border-radius:12px; padding:12px 14px; margin-bottom:20px;
}
.lyr-head{ display:flex; align-items:center; gap:8px; }
.lyr-ic{ font-size:1rem; line-height:1; }
.lyr-title{ font-size:.95rem; margin:0; font-weight:800; color:var(--text); }
.lyr-toggle{ margin-left:auto; }
/* When the toggle is present it takes the auto-margin; Edit then just follows it. */
.lyr-head .lyr-toggle + .lyr-edit{ margin-left:0; }
.lyr-head > .lyr-edit:first-of-type{ margin-left:auto; }
.lyr-ro{ margin-left:auto; color:var(--muted); font-size:.78rem; font-style:italic; }
.lyr-body{
  margin-top:10px; padding-top:10px; border-top:1px dashed var(--border, rgba(255,255,255,.14));
  color:var(--text); font-size:.95rem; line-height:1.6; word-break:break-word;
  max-height:46vh; overflow:auto;   /* a whole song must never push the editor off-screen */
}
.lyr-empty{ margin-top:8px; color:var(--muted); font-size:.9rem; font-style:italic; }
.lyr-editdone{ display:flex; justify-content:flex-end; margin-top:8px; }
/* the leader's in-place lyrics editor reuses the personal-note editor chrome */
.pnote.pnote-lyrics{ margin-top:10px; }
.pnote.pnote-lyrics .pnote-editor{ min-height:12em; }

/* --- "Start from the lyrics" (personal-note empty state only) --- */
.pnote-seed{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:8px; }
/* v161 — FOUND BY THE SIDE-BY-SIDE SCREENSHOT: display:flex beat the UA's
   [hidden] rule (the Phase 40 .btn[hidden] lesson, again), so this row had
   been visible in EVERY note editor since Phase 52 — including the shared
   Lyrics editor, where its click replaced the sheet with an empty body. */
.pnote-seed[hidden]{ display:none !important; }
.pnote-seed-hint{ color:var(--muted); font-size:.8rem; flex:1 1 14em; min-width:0; }

/* --- list surfaces: NOTHING. ---
   Phase 52 rendered the shared sheet on repertoire cards, practice rows and
   setlist rows with a 📄 and a quieter tone, so you could tell the band's words
   from your own. BATCH 135 removed it from all three: a list is for finding a
   song, and a song's worth of lyrics under every row made that impossible on a
   phone. The `.is-lyrics` rules that lived here are deleted with the markup.
   The fallback is untouched everywhere it belongs — Stage Mode, Rehearsal Mode,
   and the lyrics booklet, all styled below. If you're adding a list surface,
   gate its note on `my_notes_source === 'own'` and don't re-litigate this. */

/* --- reading surfaces (Stage + Rehearsal) --- */
/* A shared sheet reads the same as your own note; only a hairline start-edge marks
   it as the band's copy, so nothing about the reading experience changes. */
.stage-cue.is-shared-lyrics,
.stage-cue-lyrics.is-shared-lyrics,
.rstage-lyrics.is-shared-lyrics{ border-inline-start:2px solid var(--border, rgba(255,255,255,.22)); padding-inline-start:10px; }

/* the leader's big-screen Lyrics card, alongside the member note cards.
   v150: the cue rule below is SCOPED to the shared card now. It used to be a
   bare `.rstage-cue`, which — same specificity, later in the file — silently
   clobbered the scaled cue rule up in the Rehearsal section: every Rehearsal
   cue froze at 1em while A−/A+ moved everything around it (the owner names
   scaled, the cue text didn't). The shared sheet keeps its document look
   (text ink, calm line-height) but rides --stage-scale like everything else. */
.rstage-mnote.rstage-mnote-shared{ border-color:var(--border, rgba(255,255,255,.22)); }
.rstage-mnote-shared .rstage-mnote-name{ color:var(--text); }
.rstage-mnote-shared .rstage-cue{ color:var(--text); font-size:calc(var(--stage-scale,1) * 1em); line-height:1.5; word-break:break-word; }
.rstage-lyrbtn{ margin-inline-start:6px; }

/* --- Phase 52b: Calibrate from the note editor ---------------------------
   The timing panel is no longer welded inside the stage overlay, so it needs a
   full-viewport variant. z-index sits above .modal-back (40) but below .ytfloat
   (90) on purpose: you calibrate WHILE the reference video plays, so the video
   has to stay clickable on top of the panel. */
.stage-cal.stage-cal-fixed{ position:fixed; z-index:60; }
.lyr-cal{ flex:none; }
.stage-cal-head .btn{ flex:none; }


/* Resume marker (Phase 56, rebuilt v145) — shown only while you're dragging the
   lyrics, plus a brief lock-in flash as playback resumes. v145: the quiet
   left-edge caret grew into a FULL-WIDTH reading line — mid-drag on a stage the
   whole question is "which line will it pick up on?", and the answer has to read
   from arm's length in a dark room. The engine positions the element at the
   reading third and sets left/right to the scroller's edges; everything here is
   pointer-events:none so the marker can never eat the drag it describes.
   Colors are literal rgba of --play (#37c2d4) where a glow needs alpha —
   color-mix is NOT safe as the only declaration on the stage (older iPads). */
.stage-as-mark{
  position:absolute; left:0; right:0; z-index:50; pointer-events:none;
  height:0;
  opacity:0; transition:opacity .12s ease;
}
.stage-as-mark.on{ opacity:1; }
.stage-as-mark-line{
  position:absolute; left:0; right:0; top:0; height:3px; transform:translateY(-50%);
  background:var(--play); border-radius:2px;
  box-shadow:0 0 12px 2px var(--mark-glow, rgba(55,194,212,.55)), 0 1px 2px rgba(0,0,0,.6);
}
/* The time chip rides ABOVE the line's left end, not on it — sitting on the line
   would cover the very text the drag is trying to place. */
.stage-as-mark-chip{
  position:absolute; left:0; bottom:9px;
  display:flex; align-items:center; gap:8px;
  padding:6px 16px 6px 10px; border-radius:0 999px 999px 0;
  background:var(--play); color:var(--play-ink);
  box-shadow:0 6px 18px rgba(0,0,0,.45);
}
.stage-as-mark-tip{ font-size:1.05rem; line-height:1; }
.stage-as-mark-t{ font-weight:800; font-variant-numeric:tabular-nums; font-size:1.3rem; line-height:1; }
/* The lyric line currently under the reading line — the concrete "grab" target
   while dragging. Tint + start-edge bar, readable over any note theme. */
.ll-at-mark{
  background:var(--play-soft, rgba(55,194,212,.14));
  border-radius:6px;
  box-shadow:inset 3px 0 0 var(--play);
}
/* Lock-in: one pulse of the line as playback resumes from the marked spot. */
.stage-as-mark.is-flash .stage-as-mark-line{ animation:asMarkLock .55s ease-out; }
@keyframes asMarkLock{
  0%{ box-shadow:0 0 26px 9px var(--mark-glow-lock, rgba(55,194,212,.9)), 0 1px 2px rgba(0,0,0,.6); }
  100%{ box-shadow:0 0 12px 2px var(--mark-glow, rgba(55,194,212,.55)), 0 1px 2px rgba(0,0,0,.6); }
}

/* v147 — the BIG pre-play button (the vocalist's ask). Lives at the top of a
   song: full scroller width in the lower third (the band the tail pad keeps
   structurally free at song start), height CAPPED so top-of-song cues stay
   legible above it. Playback or a deliberate scroll (past the engine's grace
   buffer) retires it to the rail; the retreat animates toward the rail's
   corner so the hand-off is visible. `[hidden]{display:none}` is load-bearing
   with display:flex — the Phase 40 `.btn[hidden]` lesson. */
.stage-asbig{
  position:absolute; z-index:7;
  appearance:none; border:0; cursor:pointer; box-sizing:border-box;
  min-height:76px; max-height:14vh; padding:12px;
  border-radius:20px;
  background:var(--play); color:var(--play-ink);
  box-shadow:0 14px 40px rgba(0,0,0,.5), inset 0 0 0 1px rgba(255,255,255,.10);
  display:flex; align-items:center; justify-content:center;
  touch-action:manipulation;
  transform-origin:100% 60%;
  transition:opacity .22s ease, transform .22s ease;
}
.stage-asbig[hidden]{ display:none; }
.stage-asbig:active{ background:var(--play-press); }
.stage-asbig.is-retreat{ opacity:0; transform:translateX(16%) scale(.22); pointer-events:none; }
.stage-asbig-tri{ font-size:2.5rem; line-height:1; }


/* =====================================================================
   PHASE 59 — THE DRUMMER'S TEMPO CIRCLE
   ---------------------------------------------------------------------
   Colour: its OWN green (--tdot), deliberately NOT the teal --play lane.
   Teal means "audio playback" everywhere else in this suite, and this thing
   is emphatically not audio — reusing it would erode the one visual promise
   the play lane makes.

   The clock is entirely in CSS. --tdot-beat (seconds per beat) and
   --tdot-iters (how many beats one tap runs) are set inline by
   UI.tempoDotHtml from the song's effective BPM; every animation below reads
   the same pair, so the hand, the ring and the face can never drift apart.
   Compositor-only properties (transform / opacity) so a busy main thread
   can't stutter the pulse.

   SIZING IS CONTAINER-RELATIVE, NOT VIEWPORT-RELATIVE. `.tdot-wrap` sets a
   width; everything inside is a share of THAT (cqw), so the circle fills
   whatever room its lane gives it and its number scales with it. Viewport
   units would be wrong here — on a TV the viewport is huge but the circle's
   column beside a lyric sheet is not.
   ===================================================================== */
:root{
  --tdot:#5fd07a;          /* green — the tempo lane, distinct from teal --play */
  --tdot-ink:#08240f;
  --tdot-soft:rgba(95,208,122,.16);
}

/* ---------- the lane: reading column + circle ---------- */
/* Stacked by default, circle ON TOP (order:-1 — see UI.tempoLaneHtml for why
   it must be above rather than below when the two are stacked). */
.tdot-lane{
  display:flex; flex-direction:column; align-items:center;
  gap:12px; width:100%; min-width:0;
}
.tdot-lane-read{ width:100%; min-width:0; }
.tdot-lane > .tdot-wrap{ order:-1; }
/* v150: the lane knows what it holds. UI.tempoLaneHtml stamps .tdot-lane-cue
   when everything on the reading side resolved to CUE layout (no lyrics sheet
   anywhere in it). A cue is two lines, not a wall of words — above or beside
   one, the circle can take real room without squeezing anything that matters.
   Next to any lyrics layout the class is absent and the conservative split
   below still governs. */
.tdot-lane.tdot-lane-cue > .tdot-wrap{ width:min(58vw, 38vh, 420px); }

/* Wide enough to sit side by side — a TV, or an iPad in landscape. The circle
   takes a bounded share of the lane and the reading column takes the rest, so
   toggling three people's notes on can never squeeze the words to nothing. */
@media (min-width: 900px){
  .tdot-lane{ flex-direction:row; align-items:flex-start; justify-content:center;
              gap:clamp(16px, 3vw, 44px); }
  .tdot-lane-read{ flex:1 1 auto; }
  .tdot-lane > .tdot-wrap{
    order:0; flex:0 1 clamp(185px, 26%, 360px); width:auto;
    /* Ride the TOP of the lane, not its middle — beside a full lyric sheet the
       centre is halfway down a scroll. `align-self` has to be restated because
       the base rule centres it for the stacked and solo cases. */
    align-self:flex-start;
    /* …and STAY there. The reading column scrolls; without this the circle
       leaves the screen two verses in, which is the one time you can't get it
       back without scrolling away from the words you're reading. */
    position:sticky; top:0;
  }
  /* v150: beside CUE-only content the 26% share is protecting words that
     aren't there. Give the circle a real share, height-capped so it always
     fits beside the cue rather than under it. width:auto must be RESTATED:
     the stacked cue rule above outranks this media query's base rule on
     specificity, media query or not — without it the wide lane would inherit
     the stacked width and stop flexing. */
  .tdot-lane.tdot-lane-cue > .tdot-wrap{
    width:auto;
    flex:0 1 clamp(220px, 42%, min(560px, 58vh));
  }
}

/* ---------- the circle ---------- */
/* HEADROOM, done by insetting rather than padding. Both animations paint
   OUTSIDE the button box — the sweep's hand and its glow sit proud of the rim,
   the collapse ring starts wider than the face — so the circle needs empty
   space around it or it overlaps whatever is next to it.
   The obvious `padding:11%` is WRONG here: percentage padding resolves against
   the CONTAINING BLOCK's width, not the element's own, so beside a wide lyric
   sheet it computed to ~99px on a 156px circle. Sizing the button as a share of
   the wrap keeps the margin proportional to the circle, always. */
/* v150: HEIGHT-AWARE CAPS. The old fixed caps (235/340px) were tuned for
   "don't crowd a lyric sheet" — but in the solo and cue-only cases there is no
   sheet, and on the drummer's landscape iPad the circle sat at 340px inside
   ~500px of empty height. aspect-ratio:1 means width IS height, so a vh term
   is the honest ceiling: the circle grows into the height the screen actually
   has and can never overflow it. Phones are unchanged (their vw term still
   wins). NOTE this is the WRAP — everything inside the circle stays cqw
   (container-relative, Phase 59b); the wrap has always been the one place
   viewport terms belong. */
.tdot-wrap{
  box-sizing:border-box; aspect-ratio:1;
  display:grid; place-items:center;
  width:min(52vw, 34vh, 300px);    /* stacked, with reading content beside it */
  flex:0 0 auto; align-self:center;
}
/* Alone on the page (no cue written for this song) it can afford to be big —
   which is the common case for the drummer, and the best version of that
   screen: song title, tempo, nothing to look past. v150: ~475px on a landscape
   iPad (58vh), ~620px on a portrait one (76vw), capped at 640 so a TV mirror
   stays sane. */
.tdot-wrap.is-solo{ width:min(76vw, 58vh, 640px); margin:6px auto 0; }

.tdot{
  position:relative; appearance:none; cursor:pointer;
  container-type:inline-size;      /* everything inside sizes off THIS box */
  width:78%; aspect-ratio:1;       /* the remaining 22% IS the headroom */
  padding:0; border:0; background:none; border-radius:50%;
  -webkit-tap-highlight-color:transparent;
  touch-action:manipulation;
  display:grid; place-items:center;
}
.tdot:focus-visible{ outline:3px solid var(--focus); outline-offset:4px; }

/* The face — the filled disc that carries the number. */
.tdot-face{
  position:absolute; inset:0; border-radius:50%;
  background:var(--tdot-soft);
  border:3px solid var(--tdot);
  box-shadow:0 0 0 1px rgba(0,0,0,.35) inset;
}
/* The readout. ALWAYS visible, running or not — half the value of this thing
   is glancing over and seeing "112" without touching it at all. */
.tdot-read{
  position:relative; z-index:2; display:flex; flex-direction:column;
  align-items:center; line-height:1; pointer-events:none;
}
.tdot-num{
  font-size:34cqw; font-weight:800;
  font-variant-numeric:tabular-nums; color:var(--text); letter-spacing:-.02em;
}
.tdot-lbl{
  font-size:10.5cqw; font-weight:800; letter-spacing:.14em;
  color:var(--tdot); margin-top:4.5cqw;
}

/* ---------- the two animations ----------
   Both are built. Which one renders is decided by TEMPO_MODE in ui.js — one
   word, no device key, no UI. See the comment there for why. */

/* SWEEP: a hand travels the rim once per beat and the face pops as it crosses
   the top. Continuous position PLUS a sharp arrival, which is what makes a
   visual tempo followable — a bare blink gives you the arrival only, and by
   then the beat has gone. */
.tdot-hand{
  position:absolute; inset:0; border-radius:50%;
  opacity:0; pointer-events:none;
}
.tdot-hand::before{
  content:''; position:absolute; left:50%; top:-3.5cqw;
  width:15.5cqw; height:15.5cqw; margin-left:-7.75cqw;
  border-radius:50%; background:var(--tdot);
  box-shadow:0 0 9cqw var(--tdot);
}
.tdot-sweep.is-running .tdot-hand{
  opacity:1;
  animation:tdot-sweep var(--tdot-beat) linear var(--tdot-iters);
}
.tdot-sweep.is-running .tdot-face{
  animation:tdot-pop var(--tdot-beat) linear var(--tdot-iters);
}
@keyframes tdot-sweep{ from{ transform:rotate(0deg); } to{ transform:rotate(360deg); } }
/* The pop is front-loaded: it lands ON the beat (hand at 12 o'clock) and has
   decayed well before the next one, so the eye reads discrete hits rather
   than a wobble. */
@keyframes tdot-pop{
  0%   { transform:scale(1.07); background:rgba(95,208,122,.42); }
  18%  { transform:scale(1);    background:var(--tdot-soft); }
  100% { transform:scale(1);    background:var(--tdot-soft); }
}

/* COLLAPSE (SHIPPED — see TEMPO_MODE): a ring contracts and hits the centre on
   the beat. The more readable of the two — the arrival is a fixed point rather
   than a position you have to track around the rim. */
.tdot-ring{
  position:absolute; inset:0; border-radius:50%;
  border:3px solid var(--tdot); opacity:0; pointer-events:none;
}
.tdot-collapse.is-running .tdot-ring{
  animation:tdot-collapse var(--tdot-beat) linear var(--tdot-iters);
}
.tdot-collapse.is-running .tdot-face{
  animation:tdot-pop var(--tdot-beat) linear var(--tdot-iters);
}
@keyframes tdot-collapse{
  0%   { transform:scale(1.42); opacity:.12; }
  85%  { transform:scale(1.02); opacity:.85; }
  100% { transform:scale(.97);  opacity:0; }
}

/* Reduced motion is deliberately NOT honoured here: the motion is the entire
   feature, and it only ever runs because someone pressed the button. Muting
   it would leave a green circle that does nothing. */

/* ---------- "Our BPM" tap-tempo field (Phase 59) ---------- */
.bpm-tap{ display:flex; gap:8px; align-items:stretch; }
.bpm-tap input{ flex:1 1 auto; min-width:0; }
.bpm-tap .btn{ flex:0 0 auto; min-height:0; padding:0 14px; }
.bpm-tap-hint{
  min-height:1.1em; margin-top:5px;
  font-size:.82rem; color:var(--muted); font-variant-numeric:tabular-nums;
}

/* =====================================================================
   v149 — THE READING LOOK: three themes, the line wash, and the panel
   ---------------------------------------------------------------------
   A theme is a fact about the READING — the room, the light, the eyes —
   not about the song, so it belongs to the reader and not to the content
   (see UI.noteThemeClass, retired). It is expressed as a class on the
   surface ROOT (.stage — Rehearsal's root is .stage.rehearsal, so one
   selector covers both) which re-points the design tokens for everything
   inside it. Chrome included: a bright reading slab inside a dark frame
   is a glare edge on a dark stage.

   THE RULE FOR ADDING A THEME: every token below gets a value chosen
   against THAT theme's background, not flipped from another theme's. Our
   teal (#37c2d4) is 8.6:1 on the near-black and 2.1:1 on white — the same
   meaning needs a different value. Verified ratios are in the comments;
   re-run them if you touch a value.
   ===================================================================== */

/* Defaults = exactly what the stage already looked like, so "Stage" is a
   no-op theme and nothing about today's surface changes. */
:root{
  --scrim:rgba(0,0,0,.6);            /* full-panel overlays (calibrator, sheet) */
  --chip-scrim:rgba(0,0,0,.55);      /* the rail's floating readout pill        */
  --accent-soft:rgba(232,144,63,.08);
  --accent-soft-2:rgba(232,144,63,.16);
  --accent-line:rgba(232,144,63,.35);
  --danger-soft:rgba(229,103,95,.12);
  --mark-glow:rgba(55,194,212,.55);      /* literal rgba of --play: a glow needs */
  --mark-glow-lock:rgba(55,194,212,.9);  /* alpha, and color-mix is not safe as  */
                                         /* the only declaration on old iPads.   */
  --lyr-stripe:rgba(255,255,255,.045);   /* the alternating line wash            */
}

/* ---------- PAPER: warm off-white. Not #fff — pure white under stage
     lighting is a glare panel and makes large text bloom. Ratios vs the
     page: text 16.7 · muted 7.1 · accent 4.8 · play 5.3 · good 5.1 ---- */
.stage.rt-paper{
  --bg:#faf7f2;
  --surface:#ffffff;
  --surface-2:#f0ebe2;
  --border:#d8cfc0;
  --text:#1a1712;
  --muted:#5c5346;
  --accent:#a55a12;
  --accent-press:#8c4a0c;
  --accent-ink:#ffffff;
  --good:#1f7a35;
  --danger:#b3261e;
  --chip:#efe8dc;
  --focus:#8c4a0c;
  --play:#0e7180;          /* the teal, re-toned: same meaning, visible value */
  --play-press:#0a5b67;
  --play-ink:#ffffff;
  --play-soft:rgba(14,113,128,.13);
  --tdot:#1f7a35;
  --tdot-ink:#ffffff;
  --tdot-soft:rgba(31,122,53,.14);
  --scrim:rgba(0,0,0,.45);
  --chip-scrim:rgba(255,255,255,.82);   /* dark text needs a LIGHT pill here */
  /* v172 singer swatches, re-toned for white: each ≥4.5:1 on every Paper surface. */
  --vox-1:#b3245f;
  --vox-2:#6a3fc4;
  --vox-3:#175f92;
  --vox-4:#6b6a00;
  --vox-5:#0b6a4f;
  --vox-6:#9c2a9c;
  --accent-soft:rgba(165,90,18,.07);
  --accent-soft-2:rgba(165,90,18,.13);
  --accent-line:rgba(165,90,18,.32);
  --danger-soft:rgba(179,38,30,.09);
  /* A glow reads on dark and disappears on light, so on paper the reading
     line stops glowing and becomes a solid rule with a soft shadow. */
  --mark-glow:rgba(14,113,128,.30);
  --mark-glow-lock:rgba(14,113,128,.55);
  --lyr-stripe:rgba(26,23,18,.045);
}
/* The authored text colours were picked bright, for dark surfaces. On paper
   they need the darkened set (ratios vs the page: 4.6 / 5.7 / 5.0 / 4.8). */
.stage.rt-paper .c-red{ color:#d1362b; }
.stage.rt-paper .c-amber{ color:#8f5400; }
.stage.rt-paper .c-green{ color:#237a30; }
.stage.rt-paper .c-blue{ color:#1f6fc4; }
/* A highlight carries its own background AND text colour, so nothing becomes
   unreadable on paper — but a pale block on a pale page loses its edge, the
   way a real highlighter does. A hairline gives the mark back its shape. */
.stage.rt-paper mark[class^="hl-"]{ box-shadow:inset 0 0 0 1px rgba(0,0,0,.20); }

/* ---------- HIGH CONTRAST: the accessibility answer, not a taste. Black
     with the yellow we already used, and every accent pulled up to match.
     Ratios vs black: text 16.1 · muted 12.2 · accent 11.5 · play 13.4 --- */
.stage.rt-contrast{
  --bg:#000000;
  --surface:#0d0d0d;
  --surface-2:#161616;
  --border:#5a5a5a;
  --text:#ffe14d;
  --muted:#d6c76a;
  --accent:#ffb020;
  --accent-press:#e09400;
  --accent-ink:#1a1000;
  --good:#7ef08a;
  --danger:#ff8a80;
  --chip:#1a1a1a;
  --focus:#ffe14d;
  --play:#5fe0f0;
  --play-press:#3fc4d6;
  --play-ink:#00232a;
  --play-soft:rgba(95,224,240,.18);
  --tdot:#7ef08a;
  --tdot-ink:#00230a;
  --tdot-soft:rgba(126,240,138,.18);
  /* v172 singer swatches, brightened for black. */
  --vox-1:#ff9ec2;
  --vox-2:#cdbcff;
  --vox-3:#8fd3ff;
  --vox-4:#dede7a;
  --vox-5:#98f0d5;
  --vox-6:#f5a8f5;
  --scrim:rgba(0,0,0,.72);
  --chip-scrim:rgba(0,0,0,.72);
  --accent-soft:rgba(255,176,32,.10);
  --accent-soft-2:rgba(255,176,32,.18);
  --accent-line:rgba(255,176,32,.45);
  --danger-soft:rgba(255,138,128,.14);
  --mark-glow:rgba(95,224,240,.65);
  --mark-glow-lock:rgba(95,224,240,.95);
  --lyr-stripe:rgba(255,225,77,.075);
}

/* ---------- the alternating line wash ----------
   `.ll-alt` is stamped by UI.renderLyrics on every other SUNG line, with the
   alternation resetting at each blank line — so the markup is identical
   whether or not stripes are switched on, and the toggle is pure CSS.
   :not(.ll-at-mark) is load-bearing: the moving reading line is the strongest
   cue on the surface and a stripe must never sit on top of it. */
.stage.rt-stripes .stage-cue-lyrics .ll-alt:not(.ll-at-mark),
.stage.rt-stripes .rstage-lyrics .ll-alt:not(.ll-at-mark){
  background:var(--lyr-stripe);
  border-radius:6px;
}

/* ---------- the Look panel ----------
   A POPOVER, not a full-bleed modal, and its backdrop is deliberately
   transparent: every control in here changes the lyrics behind it, and a
   preference you can't watch take effect is one you set by guessing. */
.stage-look{
  position:absolute; inset:0; z-index:57;
  display:flex; align-items:flex-start; justify-content:flex-end;
  padding:calc(env(safe-area-inset-top,0px) + 64px) 14px 14px;
  overflow:auto;
}
.stage-look[hidden]{ display:none; }
.stage-look-card{
  width:min(340px, 100%); box-sizing:border-box;
  background:var(--surface); color:var(--text);
  border:1px solid var(--border); border-radius:16px;
  box-shadow:0 24px 70px rgba(0,0,0,.5);
  padding:14px;
}
.stage-look-head{ display:flex; align-items:center; gap:10px; margin-bottom:10px; }
.stage-look-title{ font-weight:800; font-size:1rem; flex:1 1 auto; min-width:0; }
.stage-look-row{
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  padding:9px 0; border-top:1px solid var(--border);
}
.stage-look-lbl{ font-weight:700; font-size:.9rem; flex:1 1 auto; }
/* v153: the tempo row names the song it applies to, on its own line. Every other
   control in this panel is global-to-you; this one is not, and a row that read
   just "Tempo circle" would be read as "everywhere" by everyone. */
.stage-look-sub{ display:block; font-weight:500; font-size:.76rem; color:var(--muted); margin-top:2px; overflow-wrap:anywhere; }
.stage-look-sub:empty{ display:none; }
.stage-look-themes{ display:flex; gap:6px; }
.stage-look-sw{
  appearance:none; cursor:pointer; padding:5px 6px 6px;
  display:flex; flex-direction:column; align-items:center; gap:4px;
  background:var(--surface-2); color:var(--text);
  border:2px solid var(--border); border-radius:11px;
  font-size:.7rem; font-weight:700; min-width:52px;
}
.stage-look-sw[aria-pressed="true"]{ border-color:var(--play); }
/* The swatch shows each theme in ITS OWN colours, literally — so the choice is
   visible before it's made, whatever theme is currently painting the surface. */
.stage-look-swin{
  display:flex; align-items:center; justify-content:center;
  width:100%; height:24px; border-radius:6px; font-weight:800; font-size:.85rem;
}
.lk-stage .stage-look-swin{ background:#17140f; color:#f7f2ea; }
.lk-paper .stage-look-swin{ background:#faf7f2; color:#1a1712; }
.lk-contrast .stage-look-swin{ background:#000000; color:#ffe14d; }
.stage-look-toggle{
  appearance:none; cursor:pointer; min-width:64px; min-height:38px;
  padding:0 14px; border-radius:999px; font-weight:800; font-size:.88rem;
  background:var(--surface-2); color:var(--muted); border:1px solid var(--border);
}
.stage-look-toggle[aria-pressed="true"]{
  background:var(--play); color:var(--play-ink); border-color:var(--play);
}
.stage-look-hint{
  margin:10px 0 0; font-size:.78rem; color:var(--muted); line-height:1.45;
}
.stage-look .stage-width{ flex:0 0 auto; }
@media (max-width:560px){
  .stage-look{ justify-content:center; padding-left:10px; padding-right:10px; }
  .stage-look-card{ width:100%; }
}
/* The door lights up while its panel is open — the same "this control is live"
   cue the 📝 toggle uses, so the bar never has two things looking pressed. */
.stage.look-open .stage-look-btn{
  background:var(--play); color:var(--play-ink); border-color:var(--play);
}

/* =====================================================================
   v163 — the availability calendar (the Ideas slot's new tenant)
   ===================================================================== */

/* The nav count: how many Saturdays are waiting on YOUR answer. The pull
   mechanic the Ideas board never had. */
.topnav .nav-badge{
  display:inline-block; margin-left:6px; padding:1px 7px; border-radius:999px;
  background:var(--accent); color:var(--accent-ink);
  font-size:.72rem; font-weight:800; line-height:1.35; vertical-align:1px;
}
.bottomnav .bn-ic{position:relative}
.bottomnav .bn-ic .nav-badge{
  position:absolute; top:-4px; right:-14px; padding:0 6px; border-radius:999px;
  background:var(--accent); color:var(--accent-ink);
  font-size:.68rem; font-weight:800; line-height:1.5;
}

.cal-head{display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:2px}
.cal-head h1{margin:0; flex:1}
.cal-tools{display:flex; gap:8px; flex-wrap:wrap}
.cal-intro{margin-top:2px}

/* ---- agenda rows ---- */
.cal-list{display:flex; flex-direction:column; gap:10px; margin:0 auto 30px; max-width:720px}
.cal-row{
  background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
  padding:12px 14px;
}
.cal-row.booked{border-color:var(--accent); background:var(--surface-2)}
.cal-row-top{display:flex; align-items:center; gap:10px; flex-wrap:wrap}
.cal-date{
  background:none; border:0; padding:2px 0; cursor:pointer;
  color:var(--text); font-weight:800; font-size:1.05rem; font-family:inherit;
  text-decoration:underline; text-decoration-color:var(--border); text-underline-offset:4px;
}
.cal-gig{
  display:inline-block; padding:3px 10px; border-radius:999px;
  background:var(--accent); color:var(--accent-ink); font-weight:700; font-size:.85rem;
  text-decoration:none;
}
.cal-practice{
  display:inline-block; padding:3px 10px; border-radius:999px;
  background:var(--chip); color:var(--muted); font-size:.8rem;
}
.cal-row-chips{display:flex; gap:6px; flex-wrap:wrap; margin:9px 0 8px}
.cal-chip{
  padding:3px 10px; border-radius:999px; font-size:.85rem; font-weight:700;
  border:1px solid; background:transparent; cursor:pointer;
}
.cal-chip.st-in{color:var(--good); border-color:var(--good)}
.cal-chip.st-out{color:var(--danger); border-color:var(--danger); text-decoration:line-through}
.cal-chip.st-maybe{color:var(--accent); border-color:var(--accent)}
/* Silence is LOUD here: the blank chip is the one that reads as unresolved. */
.cal-chip.st-blank{color:var(--muted); border-style:dashed; border-color:var(--muted); opacity:.85}
.cal-chip-note{margin-left:3px; color:inherit}
.cal-row-foot{display:flex; align-items:center; gap:12px; flex-wrap:wrap; justify-content:space-between}
.cal-rollup{color:var(--muted); font-size:.92rem}
.cal-rollup.needs{color:var(--text); font-weight:700}
.cal-rollup.muted{color:var(--muted)}

/* your In / Maybe / Out — segmented, honest tap targets */
.cal-me{display:inline-flex; border:1px solid var(--border); border-radius:999px; overflow:hidden}
.cal-seg{
  min-height:40px; padding:6px 16px; border:0; background:transparent; cursor:pointer;
  color:var(--muted); font-family:inherit; font-size:.95rem; font-weight:700;
}
.cal-seg + .cal-seg{border-left:1px solid var(--border)}
.cal-seg.on{color:var(--accent-ink)}
.cal-seg.on.st-in{background:var(--good)}
.cal-seg.on.st-out{background:var(--danger)}
.cal-seg.on.st-maybe{background:var(--accent)}

/* ---- the day dialog ---- */
.cal-day-modal{max-width:480px}
.cal-day-events{display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px}
.cal-day-rollup{color:var(--muted); margin-bottom:12px}
.cal-day-rollup.needs{color:var(--text); font-weight:700}
.cal-day-row{
  display:flex; align-items:flex-start; justify-content:space-between; gap:10px;
  padding:9px 0; border-bottom:1px solid var(--border);
}
.cal-day-who{font-weight:700; padding-top:8px}
.cal-you{color:var(--muted); font-weight:400; font-size:.85rem}
.cal-day-ans{text-align:right}
.cal-word{font-weight:700}
.cal-word.st-in{color:var(--good)}
.cal-word.st-out{color:var(--danger)}
.cal-word.st-maybe{color:var(--accent)}
.cal-word.st-blank{color:var(--muted); font-weight:400; font-style:italic}
.cal-day-note{color:var(--text); font-size:.88rem; margin-top:3px}
.cal-day-age{color:var(--muted); font-size:.78rem; margin-top:2px}
/* v165: your block leads the day dialog; everyone else collapses to sentences. */
.cal-day-you{display:flex; align-items:center; justify-content:space-between; gap:10px;
  padding:4px 0 10px}
.cal-day-grp{display:flex; gap:10px; align-items:baseline; padding:8px 0;
  border-top:1px solid var(--border)}
.cal-day-grp-h{flex:0 0 104px; font-weight:800; white-space:nowrap}
.cal-day-grp.st-in .cal-day-grp-h{color:var(--good)}
.cal-day-grp.st-out .cal-day-grp-h{color:var(--danger)}
.cal-day-grp.st-maybe .cal-day-grp-h{color:var(--accent)}
/* Silence is loud: the No-answer line is the one doing the blocking. */
.cal-day-grp.st-blank .cal-day-grp-h{color:var(--text); border-bottom:2px dashed var(--muted)}
.cal-day-grp-names{color:var(--text)}
.cal-day-grpnote{color:var(--muted); font-size:.85rem}
.cal-day-notebox{margin:0 0 14px}
.cal-day-notebox label{display:block; margin-bottom:6px; font-weight:700}
.cal-day-notebox .muted, .cal-range-modal .muted{color:var(--muted); font-weight:400}

/* ---- the grid (v164: ONE month, the page's lead — Alex's flip) ---- */
.cal-needs{
  display:block; width:100%; max-width:560px; margin:10px auto 0; padding:10px 14px;
  border:1px dashed var(--accent); border-radius:var(--radius); background:transparent;
  color:var(--accent); font-family:inherit; font-size:.95rem; font-weight:700;
  cursor:pointer; text-align:center;
}
.cal-onemonth{margin:12px auto 18px; max-width:560px}
.cal-monthnav{display:flex; align-items:center; gap:8px; margin-bottom:8px}
.cal-monthnav .cg-month-h{flex:1; text-align:center; margin:0; font-size:1.1rem}
.cal-mbtn{
  min-width:44px; min-height:44px; border:1px solid var(--border); border-radius:10px;
  background:var(--chip); color:var(--text); font-family:inherit; font-size:1.3rem;
  font-weight:800; cursor:pointer; line-height:1;
}
.cal-mbtn:disabled{opacity:.35; cursor:default}
.cg-month{background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:12px}
.cg-month-h{font-weight:800; margin-bottom:8px}
.cg-dow{display:grid; grid-template-columns:repeat(7,1fr); text-align:center;
  color:var(--muted); font-size:.72rem; margin-bottom:5px}
.cg-dow-sat{color:var(--accent); font-weight:800}
/* v166 (Alex): a real GRID, not numbers floating on a slab — 1px gaps over the
   border color read as ruled lines, like a paper calendar. */
.cg-days{display:grid; grid-template-columns:repeat(7,1fr); gap:1px;
  background:var(--border); border:1px solid var(--border);
  border-radius:10px; overflow:hidden}
.cg-cell{
  position:relative; min-height:48px; border:0; border-radius:0; background:var(--surface);
  color:var(--text); font-family:inherit; font-size:1rem; cursor:pointer; padding:3px 0 13px;
}
.cg-cell:hover{background:var(--chip)}
/* Leading/trailing blanks stay boxed (it's a grid) but read as out-of-month. */
.cg-pad{background:var(--bg); cursor:default}
/* The ring that answers "do I owe anything?" — an open Saturday with no answer
   from YOU. Dashed like the blank chip, so the two vocabularies rhyme. */
.cg-needme{box-shadow:inset 0 0 0 2px var(--accent)}
.cg-needme.cg-sat{background:var(--chip)}
/* Saturdays are the bright column — the page's whole premise. */
.cg-sat{background:var(--chip); font-weight:800}
.cg-past{color:var(--muted); opacity:.55}
.cg-today{outline:2px solid var(--focus); outline-offset:-2px}
.cg-gig{background:var(--accent); color:var(--accent-ink); font-weight:800}
.cg-gig:hover{background:var(--accent-press)}
.cg-marks{position:absolute; left:0; right:0; bottom:3px; display:flex; gap:3px; justify-content:center}
.cg-dot{width:5px; height:5px; border-radius:50%; display:inline-block}
.cg-dot-gig{background:var(--accent-ink)}
.cg-dot-practice{background:var(--muted)}
.cg-dot-ans{background:var(--good)}

/* ---- v167: lineups & the honest green dot ---- */
.cg-dot-book{background:var(--good); width:6px; height:6px}
.cal-bookchip{
  display:inline-block; padding:3px 10px; border-radius:999px;
  border:1px solid var(--good); color:var(--good);
  font-weight:700; font-size:.85rem;
}
/* The day dialog's booking read: one verdict line per lineup. */
.cal-lus{margin:0 0 12px; border:1px solid var(--border); border-radius:10px; padding:4px 12px}
.cal-lu{display:flex; justify-content:space-between; align-items:baseline; gap:10px; padding:7px 0}
.cal-lu + .cal-lu{border-top:1px solid var(--border)}
.cal-lu-name{font-weight:700}
.cal-lu-st{font-size:.92rem}
.cal-lu-st.st-in{color:var(--good); font-weight:700}
.cal-lu-st.st-out{color:var(--danger)}
.cal-lu-st.st-maybe{color:var(--accent)}
.cal-lu-st.st-blank{color:var(--muted)}
/* The leader's Lineups editor. */
.cal-lu-modal{max-width:480px}
.cal-lu-block{border-top:1px solid var(--border); padding:14px 0}
.cal-lu-block .lu-name{margin-bottom:10px}
.lu-chips{display:flex; flex-wrap:wrap; gap:6px; margin-bottom:10px}
.lu-chip{
  padding:6px 14px; min-height:40px; border-radius:999px; cursor:pointer;
  border:1px dashed var(--muted); background:transparent; color:var(--muted);
  font-family:inherit; font-size:.92rem; font-weight:700;
}
.lu-chip[aria-pressed="true"]{border:1px solid var(--good); color:var(--good); background:var(--chip)}
.lu-actions{display:flex; justify-content:flex-end; gap:8px}
.lu-del[data-armed="1"]{color:var(--danger); border-color:var(--danger)}

/* ---- v168: the yellow "close" dot, wins-first booking read, relocated tools ---- */
/* Close-to-bookable: a lineup with nobody Out, someone In, within two answers
   of complete. Amber = the Maybe color, deliberately — same "chase it" family. */
.cg-dot-close{background:var(--accent); width:6px; height:6px}
/* The wins line reads left-to-right as a sentence: "✓ Bookable as  Trio". */
.cal-lu-wins{justify-content:flex-start}
/* The tools now sit between the grid and the day list (Alex) — give them air. */
.cal-tools{margin:10px 0 14px}

/* ---- v169: the priority-asks banner + the flag ---- */
.cal-asks{display:grid; gap:8px; margin:0 0 14px}
.cal-asks[hidden]{display:none !important}   /* rule 21: authored display beats UA [hidden] */
.cal-ask{
  display:flex; align-items:center; gap:10px; flex-wrap:wrap;
  border:1px solid var(--accent); border-radius:12px; padding:10px 12px;
  background:var(--chip);
}
.cal-ask-date{
  background:none; border:0; padding:0; cursor:pointer;
  font:inherit; font-weight:700; color:var(--accent);
}
.cal-ask-note{color:var(--muted); flex:1 1 auto; min-width:0}
.cal-ask .cal-me{margin-left:auto}
/* The agenda row's flag chip — same family as the bookchip, amber = chase it. */
.cal-askchip{
  display:inline-block; padding:3px 10px; border-radius:999px;
  border:1px solid var(--accent); color:var(--accent);
  font-weight:700; font-size:.85rem;
}
/* The day dialog's flag line + the leader's pencil. */
.cal-day-ask{margin:0 0 10px; color:var(--accent); font-weight:700}
.cal-day-askctl{display:flex; gap:8px; align-items:center; margin:0 0 12px}
.cal-day-askctl input{flex:1; min-width:0}
.cal-day-flagbtn{margin:0 0 12px}

/* ======================================================================
   v174 — MOVING SONGS AROUND
   The row menu, multi-select, lift-and-place and the compact view, shared
   by the setlist rows (.sitem) and the practice rows (.qitem). Design
   decisions and the reasons behind them: memory topic v174_setlist_moves.
   ====================================================================== */

/* --- the ⠿ handle, now a real button on BOTH rows --------------------
   It keeps drag on a fine pointer and opens the row menu on a click, which
   is what finally gives touch a way to reorder anything (see the swept
   @media rules above). Sized to the same 30px as the other row furniture;
   on touch it grows to 38 and stops hiding. */
.sdrag, .qdrag{
  appearance:none; background:transparent; border:1px solid transparent;
  border-radius:8px; width:30px; height:34px; padding:0; flex:none;
  display:inline-flex; align-items:center; justify-content:center;
  color:var(--muted); font-size:1.15rem; line-height:1;
  cursor:grab; touch-action:none; user-select:none;
}
.sdrag:hover, .qdrag:hover{ color:var(--text); border-color:var(--border); background:var(--surface); }
.sdrag:focus-visible, .qdrag:focus-visible{ outline:3px solid var(--focus); outline-offset:2px; }
.sdrag:active, .qdrag:active{ cursor:grabbing; }
.sitem.menu-open .sdrag, .qitem.menu-open .qdrag{
  color:var(--accent); border-color:var(--accent); background:var(--surface);
}
/* ⚠️ SOURCE ORDER MATTERS HERE. The touch overrides sit up beside the old rules
   they replaced, so the explanation lives where the bug lived — but this base
   rule is further down the file and wins on a tie, quietly shrinking the handle
   back to 30px on an iPad. Measured at exactly that in the Chromium pass before
   this line existed. Re-stated last so it actually wins. */
@media (hover:none){ .sdrag, .qdrag{ display:inline-flex; width:38px; } }

/* --- the row menu ----------------------------------------------------
   A vertical popover in the 🖨 menu's voice (.print-menu-item), whose set
   pills are the working list's .wl-menu-set. Two idioms the band already
   reads; nothing new to learn. Positioned in viewport coords by
   UI.makeRowMenu, which flips it above the point near the bottom of a set. */
.rowmenu{
  position:fixed; z-index:70;
  background:var(--surface); border:1px solid var(--border); border-radius:12px;
  box-shadow:0 12px 30px rgba(0,0,0,.5); padding:6px; width:290px; max-width:92vw;
  display:flex; flex-direction:column; gap:2px;
}
/* ⚠️ RULE 21. .rowmenu is display:flex, so the UA's [hidden]{display:none}
   loses to it and the menu would sit open forever. Carried from birth. */
.rowmenu[hidden]{ display:none !important; }
.rm-head{
  font-size:.78rem; font-weight:800; color:var(--muted); letter-spacing:.3px;
  padding:6px 10px 4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.rm-sets{ display:flex; flex-wrap:wrap; gap:6px; align-items:center; padding:2px 8px 8px; }
.rm-lbl{ font-size:.78rem; font-weight:800; color:var(--muted); margin-right:2px; }
/* The set you're already in stays VISIBLE but dimmed and dashed. If it
   vanished, the pill row would reflow on every move and the target you
   wanted would jump out from under you. */
.wl-menu-set.is-here{ opacity:.35; cursor:default; border-style:dashed; }
.rm-item{
  appearance:none; background:transparent; border:0; border-radius:8px;
  font:inherit; font-size:.92rem; font-weight:600; color:var(--text);
  text-align:left; cursor:pointer; min-height:38px; padding:0 10px;
  display:flex; align-items:center; gap:10px; width:100%;
}
.rm-item:hover, .rm-item:focus-visible{ background:var(--surface-2); outline:none; }
.rm-item:disabled{ opacity:.4; cursor:default; }
.rm-item:disabled:hover{ background:transparent; }
.rm-ic{ width:16px; text-align:center; color:var(--muted); font-size:.95rem; flex:none; }
.rm-item:hover:not(:disabled) .rm-ic{ color:var(--accent); }
.rm-txt{ min-width:0; overflow:hidden; text-overflow:ellipsis; }
.rm-hint{ margin-left:auto; padding-left:8px; font-size:.76rem; font-weight:600; color:var(--muted); white-space:nowrap; }
.rm-tag{
  margin-left:auto; font-size:.66rem; font-weight:800; letter-spacing:.4px;
  color:var(--muted); border:1px dashed var(--border); border-radius:999px; padding:1px 7px;
}
.rm-item.danger{ color:var(--danger); }
.rm-item.danger .rm-ic{ color:var(--danger); }
.rm-item.danger:hover:not(:disabled){ background:var(--danger-soft); }
.rm-sep{ height:1px; background:var(--border); margin:4px 8px; }

/* --- selection (shift-click a run) ----------------------------------- */
.sitem.sel, .qitem.sel{ border-color:var(--accent); background:var(--accent-soft-2); }
.sitem.sel .sdrag, .qitem.sel .qdrag{ color:var(--accent); }
.selbar, .liftbar{
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
  background:var(--surface); border:1px solid var(--accent); border-radius:999px;
  padding:6px 8px 6px 16px; margin-bottom:12px; font-weight:800; font-size:.9rem;
}
.liftbar{ border-style:dashed; }
.selbar .sb-n, .liftbar .sb-n{ color:var(--accent); }
.selbar .sb-hint, .liftbar .lb-hint{ font-weight:600; color:var(--muted); font-size:.82rem; }
.selbar .sb-clear, .liftbar .sb-clear{
  appearance:none; margin-left:auto; border:1px solid var(--border); background:transparent;
  color:var(--muted); border-radius:999px; min-height:32px; padding:0 12px;
  font:inherit; font-size:.82rem; font-weight:800; cursor:pointer;
}
.selbar .sb-clear:hover, .liftbar .sb-clear:hover{ color:var(--text); border-color:var(--muted); }

/* --- lift and place --------------------------------------------------
   The carried row stays where it is but goes quiet, and every gap becomes
   a target. Deliberately NOT a drag: you can scroll, collapse a set, even
   flip to compact view mid-move — the things drag can never do.
   The hairlines are faint at rest on purpose: with ~50 gaps on a three-set
   page, a solid line between every row is louder than the songs. */
.sitem.lifted, .qitem.lifted{ opacity:.45; border-style:dashed; background:transparent; }
.sitem.lifted .sdrag, .qitem.lifted .qdrag{ color:var(--accent); }
.slot{
  appearance:none; display:block; width:100%; border:0; padding:0; margin:0;
  height:10px; border-radius:6px; background:transparent; cursor:pointer; position:relative;
}
.slot::before{
  content:""; position:absolute; left:0; right:0; top:50%; height:2px;
  transform:translateY(-50%); border-radius:2px;
  background:repeating-linear-gradient(90deg,var(--accent-line) 0 8px,transparent 8px 16px);
}
.slot:hover, .slot:focus-visible{ height:34px; background:var(--accent-soft); }
.slot:hover::before, .slot:focus-visible::before{ background:var(--accent); height:3px; }
/* The label sits on a chip rather than flat on the rule — rendered flat, the
   insertion line runs straight through the words and reads as strikethrough.
   Caught in the Chromium pass; jsdom would never have shown it. */
.slot:hover::after, .slot:focus-visible::after{
  content:"Drop here"; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
  background:var(--surface-2); border:1px solid var(--accent); border-radius:999px;
  padding:1px 10px; line-height:1.5; white-space:nowrap;
  font-size:.72rem; font-weight:800; color:var(--accent); letter-spacing:.3px;
}
.slot:focus-visible{ outline:3px solid var(--focus); outline-offset:1px; }
/* Touch has no hover, so the targets have to be reachable by size alone. */
@media (hover:none){
  .slot{ height:30px; }
  .slot::before{ background:repeating-linear-gradient(90deg,var(--accent-line) 0 8px,transparent 8px 16px); }
}

/* --- compact view ----------------------------------------------------
   One line per song: number, title, key, artist, singer, length. Everything
   a running order needs and nothing that is a BUILDING concern. Row height
   goes ~74px → ~32px, which is the difference between seeing six songs and
   seeing a whole set. It is one class on the container and nothing else —
   the rows are never re-rendered, so scroll position, an open menu and a
   live selection all survive the flip. */
.slen{ display:none; }
.sset-songs.is-compact, .queue.is-compact{ gap:3px; }
.sset-songs.is-compact .sitem,
.queue.is-compact .qitem{
  padding:2px 8px; border-radius:8px; gap:6px; align-items:center;
  background:transparent; border-color:transparent; min-height:0;
}
.sset-songs.is-compact .sitem:hover,
.queue.is-compact .qitem:hover{ background:var(--surface-2); border-color:var(--border); }
.sset-songs.is-compact .slaunch,
.queue.is-compact .qlaunch{
  min-height:30px; padding:2px 6px; display:flex; align-items:baseline; gap:10px;
}
/* No percentage cap on the title. A fixed 52% is generous on a wide setlist
   row and stingy on a practice row (which spends width on a checkbox and a
   caret), so it truncated "All The Small Thi…" while there was empty space to
   the right. The title takes what it needs and the META line yields first —
   the artist is the thing you can afford to lose the tail of. */
.sset-songs.is-compact .stitle, .queue.is-compact .qtitle{
  font-size:.94rem; font-weight:700; display:block; flex:0 1 auto; min-width:8ch;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.sset-songs.is-compact .ssub, .queue.is-compact .qsub{
  margin-top:0; font-size:.8rem; flex:1 1 auto; min-width:0;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.sset-songs.is-compact .snum{ color:var(--muted); font-weight:700; margin-right:2px; }
/* Everything that is about BUILDING the list rather than reading it. */
.sset-songs.is-compact .sbadges, .sset-songs.is-compact .smynote,
.queue.is-compact .qdetails, .queue.is-compact .qmynote,
.queue.is-compact .qarr, .queue.is-compact .badges{ display:none !important; }
.sset-songs.is-compact .slen, .queue.is-compact .slen{
  display:inline; color:var(--muted); font-size:.78rem; font-weight:700;
  flex:none; font-variant-numeric:tabular-nums;
}
/* The handle appears on hover so the resting view is nothing but songs —
   except on touch, where hover doesn't exist and it IS the only control. */
.sset-songs.is-compact .sdrag, .queue.is-compact .qdrag{ height:28px; opacity:0; }
.sset-songs.is-compact .sitem:hover .sdrag,
.sset-songs.is-compact .sitem.menu-open .sdrag,
.sset-songs.is-compact .sitem.sel .sdrag,
.queue.is-compact .qitem:hover .qdrag,
.queue.is-compact .qitem.menu-open .qdrag,
.queue.is-compact .qitem.sel .qdrag{ opacity:1; }
@media (hover:none){
  .sset-songs.is-compact .sdrag, .queue.is-compact .qdrag{ opacity:1; }
}
.sset-songs.is-compact .sitem.sel, .queue.is-compact .qitem.sel{
  background:var(--accent-soft-2); border-color:var(--accent);
}
/* --- compact, the practice row's own furniture -----------------------
   Caught in the Chromium pass: the practice row carries more than a setlist
   row does, and left alone compact was 50px to the setlist's 38 — the teal
   "Practice" pill alone was taller than the line it sat on. Everything here
   is the same judgement as above: keep what you READ off a running order,
   shrink or drop what belongs to BUILDING one. */
.queue.is-compact .qmain{ min-width:0; }
.queue.is-compact .qtop{ gap:6px; align-items:center; }
.queue.is-compact .qhead{ flex:1 1 auto; min-width:0; }
/* "you added" is attribution — a planning fact, not a reading one. */
.queue.is-compact .qadder{ display:none; }
/* The stem player keeps its teal lane but loses the word; the icon is the
   thing people aim at anyway, and the pill was the tallest object on the row. */
.queue.is-compact .qplay{ min-height:26px; padding:0 8px; border-radius:999px; }
.queue.is-compact .qplay-txt{ display:none; }
.queue.is-compact .lt-door{ min-height:26px; width:26px; }
/* Worked-on stays — it is the whole point of the practice list — just smaller. */
.queue.is-compact .qcheck{ padding-top:0; }
.queue.is-compact .qcheckbox{ width:20px; height:20px; }
.queue.is-compact .qcaret-btn{ min-height:26px; width:26px; }
.queue.is-compact .qitem{ padding:1px 8px; }

.compact-toggle .compact-ico{ font-size:1rem; line-height:1; }
.compact-toggle[aria-pressed="true"]{ color:var(--accent); border-color:var(--accent); }
