pm-mobile: compact tempo plate, Repeat checkbox, controls under lanes, bigger transport

- Replaced the big BPM circle with a compact tempo "plate" (♩ = N · per minute)
  that flashes on the beat — reclaims the wasted vertical space.
- Transport buttons now grow to fill the freed space (2×4 grid stretches).
- Removed the bottom-sheet note-value picker; note value is chosen by graphic
  inside the lane modal only (kept there as you liked).
- Repeat is now a checkbox (like Tempo ramp / Practice gaps); checking it reveals
  "Play N bars, then stop / next / prev". The whole control group moved BELOW the
  lanes.

Engine untouched; conformance passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-06-07 14:15:10 -05:00
parent 8402b4f92c
commit 36b7cacd3f

View file

@ -75,13 +75,14 @@
/* ---- middle: pulse + track panel + lanes + transport, centered as one block ---- */ /* ---- middle: pulse + track panel + lanes + transport, centered as one block ---- */
#mid{ flex:1 1 auto; min-height:0; display:flex; flex-direction:column; align-items:center; justify-content:safe center; gap:clamp(10px,2.4vmin,22px); } #mid{ flex:1 1 auto; min-height:0; display:flex; flex-direction:column; align-items:center; justify-content:safe center; gap:clamp(10px,2.4vmin,22px); }
#stage{ flex:0 0 auto; display:flex; flex-direction:column; align-items:center; gap:clamp(6px,1.6vmin,14px); } #stage{ flex:0 0 auto; display:flex; flex-direction:column; align-items:center; width:100%; }
#pulse{ position:relative; width:clamp(186px,46vmin,360px); height:clamp(186px,46vmin,360px); border-radius:50%; /* compact tempo "plate" (was a big circle) — flashes on the beat; tap/hold/drag target */
#pulse{ position:relative; width:100%; padding:clamp(10px,2.4vmin,20px) 16px; border-radius:16px;
display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center;
border:2px solid var(--ring); background:radial-gradient(circle at 50% 40%, rgba(127,139,154,.07), transparent 70%); border:1px solid var(--ring); background:linear-gradient(180deg, rgba(127,139,154,.06), transparent);
transition:transform .12s ease-out, box-shadow .12s ease-out, border-color .12s ease-out; touch-action:none; cursor:pointer; } transition:box-shadow .12s ease-out, border-color .12s ease-out, background .12s ease-out; touch-action:none; cursor:pointer; }
#pulse.hit{ transform:scale(1.045); border-color:var(--cyan); box-shadow:0 0 60px var(--glow); } #pulse.hit{ border-color:var(--cyan); box-shadow:0 0 22px var(--glow); background:rgba(10,179,247,.07); }
#pulse.hit.acc{ border-color:var(--amber); box-shadow:0 0 72px var(--aglow); } #pulse.hit.acc{ border-color:var(--amber); box-shadow:0 0 26px var(--aglow); background:rgba(255,209,102,.08); }
/* tempo as an engraved marking: ♩ = N */ /* tempo as an engraved marking: ♩ = N */
#bpm{ display:inline-flex; align-items:center; justify-content:center; gap:.12em; line-height:.82; } #bpm{ display:inline-flex; align-items:center; justify-content:center; gap:.12em; line-height:.82; }
#bpmNum{ font-size:clamp(44px,14vmin,128px); font-weight:800; font-variant-numeric:tabular-nums; letter-spacing:-.01em; } #bpmNum{ font-size:clamp(44px,14vmin,128px); font-weight:800; font-variant-numeric:tabular-nums; letter-spacing:-.01em; }
@ -153,12 +154,13 @@
.chip.feat.r{ border-color:var(--cyan); color:var(--cyan); } .chip.feat.g{ border-color:var(--amber); color:var(--amber); } .chip.feat.r{ border-color:var(--cyan); color:var(--cyan); } .chip.feat.g{ border-color:var(--amber); color:var(--amber); }
/* ---- transport ---- */ /* ---- transport ---- */
#transport{ flex:0 0 auto; display:flex; justify-content:center; padding-top:2px; } /* transport grows to fill the space freed by the compact tempo plate */
.tgrid{ display:grid; width:100%; gap:clamp(6px,1.6vmin,13px); #transport{ flex:1 1 auto; max-height:clamp(150px,42vh,300px); display:flex; align-items:stretch; justify-content:center; width:100%; padding-top:6px; }
grid-template-columns:1fr 1.5fr 1.5fr 1fr; grid-template-areas:"dn10 prev next up10" "dn play prac up"; } .tgrid{ display:grid; width:100%; height:100%; gap:clamp(7px,1.7vmin,14px);
grid-template-columns:1fr 1.5fr 1.5fr 1fr; grid-template-rows:1fr 1fr; grid-template-areas:"dn10 prev next up10" "dn play prac up"; }
.tbtn{ background:linear-gradient(180deg,var(--btn1),var(--btn2)); color:var(--txt); border:1px solid var(--btn-bd); .tbtn{ background:linear-gradient(180deg,var(--btn1),var(--btn2)); color:var(--txt); border:1px solid var(--btn-bd);
border-radius:14px; height:clamp(46px,11vmin,72px); font-size:clamp(18px,4.4vmin,28px); cursor:pointer; border-radius:14px; height:auto; min-height:46px; font-size:clamp(18px,4.4vmin,30px); cursor:pointer;
box-shadow:0 3px 0 rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.06); display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px; } box-shadow:0 3px 0 rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.06); display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px; }
.tbtn small{ font-size:clamp(8px,1.5vmin,11px); letter-spacing:.1em; color:inherit; opacity:.85; } .tbtn small{ font-size:clamp(8px,1.5vmin,11px); letter-spacing:.1em; color:inherit; opacity:.85; }
.tbtn:active{ transform:translateY(2px); box-shadow:0 1px 0 rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.06); } .tbtn:active{ transform:translateY(2px); box-shadow:0 1px 0 rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.06); }
#bDn10{grid-area:dn10} #bPrev{grid-area:prev} #bNext{grid-area:next} #bUp10{grid-area:up10} #bDn10{grid-area:dn10} #bPrev{grid-area:prev} #bNext{grid-area:next} #bUp10{grid-area:up10}
@ -272,20 +274,17 @@
<div id="meterline"></div> <div id="meterline"></div>
</div> </div>
<div id="detail"> <div id="detail">
<div id="lanes"></div>
<div id="trackpanel"> <div id="trackpanel">
<div class="tp-row"> <div class="tp-row">
<label>Play <input id="ipBars" type="number" inputmode="numeric" min="0" max="999" /> bars,</label> <label class="tp-chk"><input type="checkbox" id="ipRepeat" /> Repeat</label>
<label id="ipThen">then <select id="ipEnd"><option value="stop">stop</option><option value="next">next track</option><option value="prev">prev track</option></select></label>
<span id="ipLoop" class="tp-loop">then loop</span>
</div>
<div class="tp-row">
<label class="tp-chk"><input type="checkbox" id="ipRamp" /> Tempo ramp</label> <label class="tp-chk"><input type="checkbox" id="ipRamp" /> Tempo ramp</label>
<label class="tp-chk"><input type="checkbox" id="ipGap" /> Practice gaps</label> <label class="tp-chk"><input type="checkbox" id="ipGap" /> Practice gaps</label>
</div> </div>
<div class="tp-sub off" id="ipRepeatRow">Play <input id="ipBars" type="number" inputmode="numeric" min="1" max="999" /> bars, then <select id="ipEnd"><option value="stop">stop</option><option value="next">next track</option><option value="prev">prev track</option></select></div>
<div class="tp-sub off" id="ipRampRow"><input id="ipRampStart" type="number" min="30" max="300" /> → +<input id="ipRampAmt" type="number" min="1" max="50" /> bpm / <input id="ipRampEvery" type="number" min="1" max="64" /> bars</div> <div class="tp-sub off" id="ipRampRow"><input id="ipRampStart" type="number" min="30" max="300" /> → +<input id="ipRampAmt" type="number" min="1" max="50" /> bpm / <input id="ipRampEvery" type="number" min="1" max="64" /> bars</div>
<div class="tp-sub off" id="ipGapRow"><input id="ipGapPlay" type="number" min="1" max="32" /> play / <input id="ipGapMute" type="number" min="1" max="32" /> mute bars</div> <div class="tp-sub off" id="ipGapRow"><input id="ipGapPlay" type="number" min="1" max="32" /> play / <input id="ipGapMute" type="number" min="1" max="32" /> mute bars</div>
</div> </div>
<div id="lanes"></div>
</div> </div>
<div id="transport"> <div id="transport">
<div class="tgrid"> <div class="tgrid">
@ -324,13 +323,6 @@
<div class="lfoot"><button id="lsDel" class="lbtn danger">Delete lane</button><button id="lsDone" class="lbtn">Done</button></div> <div class="lfoot"><button id="lsDel" class="lbtn danger">Delete lane</button><button id="lsDone" class="lbtn">Done</button></div>
</div> </div>
<!-- note-value picker (opened by tapping a lane's rhythm icon) -->
<div id="noteSheet">
<div class="grab"></div>
<h2>Note value</h2>
<div id="noteOpts" class="noterow"></div>
</div>
<!-- share sheet: share a track or set list as a link, or paste a string to load --> <!-- share sheet: share a track or set list as a link, or paste a string to load -->
<div id="shareSheet"> <div id="shareSheet">
<div class="grab"></div> <div class="grab"></div>
@ -539,8 +531,7 @@ function laneMetaHTML(m){ const eff=laneNoteValue(m);
const ref=(meters[0]?meters[0].beatsPerBar:m.beatsPerBar); const ref=(meters[0]?meters[0].beatsPerBar:m.beatsPerBar);
const poly=m.poly?"<span class='polybadge' title='polyrhythm — "+m.beatsPerBar+" over "+ref+"'>↻"+m.beatsPerBar+":"+ref+"</span>":""; const poly=m.poly?"<span class='polybadge' title='polyrhythm — "+m.beatsPerBar+" over "+ref+"'>↻"+m.beatsPerBar+":"+ref+"</span>":"";
return "<span class='ln-name'>"+esc(m.sound)+"</span><span class='rh-host' title='Note value'>"+rhythmSVG(eff)+"</span><span class='lg'>"+esc(m.groupsStr)+"</span>"+poly; } return "<span class='ln-name'>"+esc(m.sound)+"</span><span class='rh-host' title='Note value'>"+rhythmSVG(eff)+"</span><span class='lg'>"+esc(m.groupsStr)+"</span>"+poly; }
function setLaneMeta(m){ if(!m._meta) return; m._meta.innerHTML=laneMetaHTML(m); function setLaneMeta(m){ if(!m._meta) return; m._meta.innerHTML=laneMetaHTML(m); }
const rh=m._meta.querySelector(".rh-host"); if(rh) rh.onclick=(e)=>{ e.stopPropagation(); openNotePicker(m._idx); }; }
function buildLanes(){ function buildLanes(){
const box=$("lanes"); box.innerHTML=""; const box=$("lanes"); box.innerHTML="";
meters.forEach((m,i)=>{ meters.forEach((m,i)=>{
@ -571,9 +562,6 @@ function renderNoteOpts(box, cur, pick){ if(!box) return; box.innerHTML="";
function setLaneSub(i,n){ const m=meters[i]; if(!m) return; function setLaneSub(i,n){ const m=meters[i]; if(!m) return;
rebuildLane(i,{groupsStr:m.groupsStr,stepsPerBeat:n,swing:m.swing,poly:m.poly,enabled:m.enabled,sound:m.sound,gainDb:m.gainDb,beatsOn:m.beatsOn.slice(),orns:(m.orns||[]).slice()}); rebuildLane(i,{groupsStr:m.groupsStr,stepsPerBeat:n,swing:m.swing,poly:m.poly,enabled:m.enabled,sound:m.sound,gainDb:m.gainDb,beatsOn:m.beatsOn.slice(),orns:(m.orns||[]).slice()});
laneSig=null; renderAll(); saveState(); } laneSig=null; renderAll(); saveState(); }
function openNotePicker(i){ const m=meters[i]; if(!m) return; editLaneIdx=i;
renderNoteOpts($("noteOpts"), m.stepsPerBeat, (n)=>{ setLaneSub(i,n); closeSheets(); });
$("laneSheet").classList.remove("open"); $("saveSheet").classList.remove("open"); $("shareSheet").classList.remove("open"); $("scrim").classList.add("open"); $("noteSheet").classList.add("open"); }
function refreshLaneSheetNotes(){ const m=meters[editLaneIdx]; if(!m) return; function refreshLaneSheetNotes(){ const m=meters[editLaneIdx]; if(!m) return;
renderNoteOpts($("lsNotes"), m.stepsPerBeat, (n)=>{ setLaneSub(editLaneIdx,n); refreshLaneSheetNotes(); }); } renderNoteOpts($("lsNotes"), m.stepsPerBeat, (n)=>{ setLaneSub(editLaneIdx,n); refreshLaneSheetNotes(); }); }
function renderPadPlayheads(){ meters.forEach(m=>{ if(!m._padEls) return; const cur=state.running?m.currentStep:-1; function renderPadPlayheads(){ meters.forEach(m=>{ if(!m._padEls) return; const cur=state.running?m.currentStep:-1;
@ -595,8 +583,8 @@ function gainLabel(db){ return (db>0?"+":"")+db+" dB"; }
function openLaneSheet(i){ editLaneIdx=i; const m=meters[i]; if(!m) return; function openLaneSheet(i){ editLaneIdx=i; const m=meters[i]; if(!m) return;
$("lsSound").value=m.sound; $("lsGroup").value=m.groupsStr; $("lsPoly").checked=!!m.poly; $("lsMute").checked=!m.enabled; $("lsSound").value=m.sound; $("lsGroup").value=m.groupsStr; $("lsPoly").checked=!!m.poly; $("lsMute").checked=!m.enabled;
$("lsGain").value=m.gainDb||0; $("lsGainVal").textContent=gainLabel(m.gainDb||0); refreshLaneSheetNotes(); $("lsGain").value=m.gainDb||0; $("lsGainVal").textContent=gainLabel(m.gainDb||0); refreshLaneSheetNotes();
$("saveSheet").classList.remove("open"); $("noteSheet").classList.remove("open"); $("shareSheet").classList.remove("open"); $("scrim").classList.add("open"); $("laneSheet").classList.add("open"); } $("saveSheet").classList.remove("open"); $("shareSheet").classList.remove("open"); $("scrim").classList.add("open"); $("laneSheet").classList.add("open"); }
function closeSheets(){ ["laneSheet","saveSheet","noteSheet","shareSheet","scrim"].forEach(id=>$(id).classList.remove("open")); } function closeSheets(){ ["laneSheet","saveSheet","shareSheet","scrim"].forEach(id=>$(id).classList.remove("open")); }
const closeLaneSheet=closeSheets; const closeLaneSheet=closeSheets;
function applyLane(){ const m=meters[editLaneIdx]; if(!m) return; function applyLane(){ const m=meters[editLaneIdx]; if(!m) return;
let grp=($("lsGroup").value||"").trim()||"4"; let grp=($("lsGroup").value||"").trim()||"4";
@ -615,9 +603,9 @@ function clampInt(v,lo,hi,def){ v=parseInt(v,10); if(isNaN(v)) return def; retur
function flashIp(msg){ $("ipMsg").textContent=msg; setTimeout(()=>{ if($("ipMsg").textContent===msg) $("ipMsg").textContent=""; },1600); } function flashIp(msg){ $("ipMsg").textContent=msg; setTimeout(()=>{ if($("ipMsg").textContent===msg) $("ipMsg").textContent=""; },1600); }
function buildTrackPanel(){ function buildTrackPanel(){
if(document.activeElement&&document.activeElement.closest&&document.activeElement.closest("#trackpanel")) return; // don't fight the user mid-edit if(document.activeElement&&document.activeElement.closest&&document.activeElement.closest("#trackpanel")) return; // don't fight the user mid-edit
const hasEnd=segBars>0; const rep=segBars>0;
$("ipBars").value=segBars||0; $("ipRepeat").checked=rep; $("ipRepeatRow").classList.toggle("off",!rep);
$("ipThen").style.display=hasEnd?"":"none"; $("ipLoop").style.display=hasEnd?"none":""; $("ipBars").value=segBars||4;
$("ipEnd").value = curEnd==="stop"?"stop":(curEnd===-1?"prev":"next"); $("ipEnd").value = curEnd==="stop"?"stop":(curEnd===-1?"prev":"next");
$("ipRamp").checked=ramp.on; $("ipGap").checked=trainer.on; $("ipRamp").checked=ramp.on; $("ipGap").checked=trainer.on;
$("ipRampStart").value=ramp.startBpm||80; $("ipRampAmt").value=ramp.amount||5; $("ipRampEvery").value=ramp.everyBars||4; $("ipRampStart").value=ramp.startBpm||80; $("ipRampAmt").value=ramp.amount||5; $("ipRampEvery").value=ramp.everyBars||4;
@ -625,15 +613,15 @@ function buildTrackPanel(){
$("ipRampRow").classList.toggle("off",!ramp.on); $("ipGapRow").classList.toggle("off",!trainer.on); $("ipRampRow").classList.toggle("off",!ramp.on); $("ipGapRow").classList.toggle("off",!trainer.on);
} }
function applyTrackPanel(){ function applyTrackPanel(){
segBars=Math.max(0,parseInt($("ipBars").value,10)||0); if($("ipRepeat").checked){ segBars=Math.max(1,parseInt($("ipBars").value,10)||4); const e=$("ipEnd").value; curEnd = e==="stop"?"stop":(e==="prev"?-1:1); }
if(segBars>0){ const e=$("ipEnd").value; curEnd = e==="stop"?"stop":(e==="prev"?-1:1); } else { curEnd=null; } // 0 bars = loop forever else { segBars=0; curEnd=null; } // no Repeat = loop forever
$("ipThen").style.display=segBars>0?"":"none"; $("ipLoop").style.display=segBars>0?"none":""; $("ipRepeatRow").classList.toggle("off",!$("ipRepeat").checked);
ramp.on=$("ipRamp").checked; ramp.startBpm=clampInt($("ipRampStart").value,30,300,80); ramp.amount=clampInt($("ipRampAmt").value,1,50,5); ramp.everyBars=clampInt($("ipRampEvery").value,1,64,4); ramp.on=$("ipRamp").checked; ramp.startBpm=clampInt($("ipRampStart").value,30,300,80); ramp.amount=clampInt($("ipRampAmt").value,1,50,5); ramp.everyBars=clampInt($("ipRampEvery").value,1,64,4);
trainer.on=$("ipGap").checked; trainer.playBars=clampInt($("ipGapPlay").value,1,32,2); trainer.muteBars=clampInt($("ipGapMute").value,1,32,2); trainer.on=$("ipGap").checked; trainer.playBars=clampInt($("ipGapPlay").value,1,32,2); trainer.muteBars=clampInt($("ipGapMute").value,1,32,2);
$("ipRampRow").classList.toggle("off",!ramp.on); $("ipGapRow").classList.toggle("off",!trainer.on); $("ipRampRow").classList.toggle("off",!ramp.on); $("ipGapRow").classList.toggle("off",!trainer.on);
saveState(); saveState();
} }
["ipBars","ipEnd","ipRamp","ipGap","ipRampStart","ipRampAmt","ipRampEvery","ipGapPlay","ipGapMute"].forEach(id=>$(id).addEventListener("change",applyTrackPanel)); ["ipRepeat","ipBars","ipEnd","ipRamp","ipGap","ipRampStart","ipRampAmt","ipRampEvery","ipGapPlay","ipGapMute"].forEach(id=>$(id).addEventListener("change",applyTrackPanel));
/* ---- save & library: user set lists/tracks (same store + format as the editor) ---- */ /* ---- save & library: user set lists/tracks (same store + format as the editor) ---- */
function userSetlists(){ return lsGet(LS_SETLISTS,[]); } function userSetlists(){ return lsGet(LS_SETLISTS,[]); }
@ -708,7 +696,7 @@ function openSaveSheet(){
$("saveName").value=currentName()||"My track"; buildSaveTo(); $("saveName").value=currentName()||"My track"; buildSaveTo();
const upd=$("saveUpd"); if(slKey[0]==="s"){ upd.style.display=""; upd.textContent='Update “'+(currentName()||"track")+'”'; } else upd.style.display="none"; const upd=$("saveUpd"); if(slKey[0]==="s"){ upd.style.display=""; upd.textContent='Update “'+(currentName()||"track")+'”'; } else upd.style.display="none";
$("saveMsg").textContent=""; renderLibrary(); $("saveMsg").textContent=""; renderLibrary();
$("laneSheet").classList.remove("open"); $("noteSheet").classList.remove("open"); $("shareSheet").classList.remove("open"); $("scrim").classList.add("open"); $("saveSheet").classList.add("open"); } $("laneSheet").classList.remove("open"); $("shareSheet").classList.remove("open"); $("scrim").classList.add("open"); $("saveSheet").classList.add("open"); }
$("saveNew").onclick=doSaveAsNew; $("saveUpd").onclick=doUpdate; $("saveDone").onclick=closeSheets; $("saveBtn").onclick=openSaveSheet; $("saveNew").onclick=doSaveAsNew; $("saveUpd").onclick=doUpdate; $("saveDone").onclick=closeSheets; $("saveBtn").onclick=openSaveSheet;
/* ---- share: a track or set list as a link, or paste a string to load ---- */ /* ---- share: a track or set list as a link, or paste a string to load ---- */
@ -721,7 +709,7 @@ function refreshShare(){ $("shareLink").value=shareUrl(); $("shareSeg").querySel
function flashShare(m){ $("shareMsg").textContent=m; setTimeout(()=>{ if($("shareMsg").textContent===m) $("shareMsg").textContent=""; },1600); } function flashShare(m){ $("shareMsg").textContent=m; setTimeout(()=>{ if($("shareMsg").textContent===m) $("shareMsg").textContent=""; },1600); }
function copyText(s, ok){ if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(s).then(ok,()=>{}); } else { const t=$("shareLink"); t.value=s; t.select(); try{ document.execCommand("copy"); ok(); }catch(e){} refreshShare(); } } function copyText(s, ok){ if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(s).then(ok,()=>{}); } else { const t=$("shareLink"); t.value=s; t.select(); try{ document.execCommand("copy"); ok(); }catch(e){} refreshShare(); } }
function openShareSheet(){ shareKind="p"; refreshShare(); $("sharePaste").value=""; $("shareMsg").textContent=""; function openShareSheet(){ shareKind="p"; refreshShare(); $("sharePaste").value=""; $("shareMsg").textContent="";
["laneSheet","saveSheet","noteSheet"].forEach(id=>$(id).classList.remove("open")); $("scrim").classList.add("open"); $("shareSheet").classList.add("open"); } ["laneSheet","saveSheet"].forEach(id=>$(id).classList.remove("open")); $("scrim").classList.add("open"); $("shareSheet").classList.add("open"); }
$("shareSeg").querySelectorAll("button").forEach(b=>b.onclick=()=>{ shareKind=b.dataset.k; refreshShare(); }); $("shareSeg").querySelectorAll("button").forEach(b=>b.onclick=()=>{ shareKind=b.dataset.k; refreshShare(); });
$("shareCopy").onclick=()=>copyText(shareUrl(),()=>flashShare("Link copied ✓")); $("shareCopy").onclick=()=>copyText(shareUrl(),()=>flashShare("Link copied ✓"));
$("shareCopyT").onclick=()=>copyText(shareText(),()=>flashShare("Copied ✓")); $("shareCopyT").onclick=()=>copyText(shareText(),()=>flashShare("Copied ✓"));
@ -842,8 +830,8 @@ const TOUR=[
{sel:".sels", title:"Pick what to play", text:"Choose a set list and the track within it. Tracks are your practice items — name them for whatever you're working on, even if two share the same beat."}, {sel:".sels", title:"Pick what to play", text:"Choose a set list and the track within it. Tracks are your practice items — name them for whatever you're working on, even if two share the same beat."},
{sel:"#saveBtn", title:"Save & library", text:"Save the current track — “Save as new”, or “Update” one of yours. The same sheet is your library: make set lists and rename / reorder / delete tracks. It all lives with the full editor too."}, {sel:"#saveBtn", title:"Save & library", text:"Save the current track — “Save as new”, or “Update” one of yours. The same sheet is your library: make set lists and rename / reorder / delete tracks. It all lives with the full editor too."},
{sel:"#pulse", title:"Tempo", text:"Tap the BPM to tap-tempo, press-and-hold to type an exact value, or drag up/down to scrub. ±10 / ±1 buttons nudge it."}, {sel:"#pulse", title:"Tempo", text:"Tap the BPM to tap-tempo, press-and-hold to type an exact value, or drag up/down to scrub. ±10 / ±1 buttons nudge it."},
{sel:"#lanes", title:"Edit the beat", text:"Each lane is a row of pads that blink on the beat — tap a pad to cycle rest → beat → accent → ghost. Tap the little note icon to pick the note value (eighths, triplets, sixteenths…); tap the lane name for sound, grouping, mute or polymeter. “+ Add lane” for more."}, {sel:"#lanes", title:"Edit the beat", text:"Each lane is a row of pads that blink on the beat — tap a pad to cycle rest → beat → accent → ghost. Tap a lane's label to set its note value (eighths, triplets, sixteenths…), sound, grouping, mute or polymeter. “+ Add lane” for more."},
{sel:"#trackpanel", title:"Track settings", text:"How many bars to play and what happens then (loop, stop, or jump to the next/previous track), plus a tempo ramp and practice gaps."}, {sel:"#trackpanel", title:"Track settings", text:"Optional per-track extras (under the lanes): Repeat for N bars then stop / next / prev track, a tempo ramp, and practice gaps."},
{sel:"#bPrac", title:"Practice = a timed session", text:"Play just runs the metronome. Practice times your playing and logs it (not audio): it starts a session clock and Play becomes Stop — start/pause each track, then Stop to save the session."}, {sel:"#bPrac", title:"Practice = a timed session", text:"Play just runs the metronome. Practice times your playing and logs it (not audio): it starts a session clock and Play becomes Stop — start/pause each track, then Stop to save the session."},
{sel:"#sessbar", title:"Your practice log", text:"Review past sessions, add notes, and compare a track across days."}, {sel:"#sessbar", title:"Your practice log", text:"Review past sessions, add notes, and compare a track across days."},
]; ];