diff --git a/mobile.html b/mobile.html
index 01e9b15..2ed8b28 100644
--- a/mobile.html
+++ b/mobile.html
@@ -104,12 +104,18 @@
#trackpanel .tp-msg{ color:#5fd08a; margin-left:auto; }
.lane{ display:flex; align-items:center; gap:8px; }
.lane.off{ opacity:.5; }
- .lmeta{ flex:0 0 auto; width:30%; max-width:130px; min-width:64px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left;
- background:var(--chip-bg); border:1px solid var(--chip-bd); color:var(--txt); border-radius:8px; padding:6px 8px;
+ .lmeta{ flex:0 0 auto; width:34%; max-width:150px; min-width:92px; display:flex; align-items:center; gap:5px; text-align:left;
+ background:var(--chip-bg); border:1px solid var(--chip-bd); color:var(--txt); border-radius:8px; padding:5px 8px;
font-size:clamp(10px,1.8vmin,13px); font-family:"Courier New",monospace; cursor:pointer; }
- .lmeta .lg{ color:var(--muted); }
- .pads{ flex:1 1 auto; display:flex; gap:3px; overflow-x:auto; padding-bottom:2px; min-width:0; }
- .pad{ flex:1 0 14px; min-width:14px; height:clamp(20px,3.6vmin,28px); border-radius:5px; border:1px solid var(--chip-bd); background:var(--led-off); cursor:pointer; padding:0; }
+ .lmeta .ln-name{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
+ .lmeta .lg{ flex:0 0 auto; color:var(--muted); }
+ .lmeta .rhythm{ flex:0 0 auto; color:var(--txt); display:block; }
+ /* beats line up in columns across lanes; sub-beats sit inside the beat cell, smaller */
+ .pads{ flex:1 1 auto; display:flex; gap:7px; overflow-x:auto; padding-bottom:2px; min-width:0; align-items:center; }
+ .beatcell{ flex:1 1 0; min-width:0; display:flex; gap:2px; align-items:center; }
+ .pad{ flex:1 1 0; min-width:5px; border:1px solid var(--chip-bd); background:var(--led-off); cursor:pointer; padding:0; }
+ .pad.beat{ height:clamp(20px,3.8vmin,28px); border-radius:5px; }
+ .pad.sub{ height:clamp(11px,2.3vmin,16px); border-radius:3px; }
.pad.gs{ border-color:var(--amber); }
.pad.on{ background:var(--cyan); }
.pad.acc{ background:var(--amber); }
@@ -443,25 +449,43 @@ function nudge(d){ ramp.on=false; setBpm(state.bpm+d); renderAll(); }
let laneSig=null, editLaneIdx=0;
function laneSignature(){ return meters.map(m=>m.sound+":"+m.groupsStr+"/"+m.stepsPerBeat+(m.swing?"s":"")+(m.enabled?"":"!")+(m.poly?"~":"")).join("|"); }
function lvlClass(l){ return l===2?"acc":l===3?"ghost":l===1?"on":""; }
+function padClass(m,k){ const spb=m.stepsPerBeat, isBeat=(k%spb===0), gs=isBeat&&m.groupStarts.has(k/spb), lvl=m.beatsOn[k]|0;
+ return "pad "+(isBeat?"beat":"sub")+(gs?" gs":"")+(lvl?(" "+lvlClass(lvl)):""); }
+// Small engraved rhythm figure for a beat's subdivision (1=quarter, 2=8ths, 3=triplet,
+// 4=16ths, 5/6/7=tuplets). Drawn as SVG so triplets/sextuplets render crisply.
+function rhythmSVG(spb){
+ spb=Math.max(1,spb|0);
+ const n=spb, beams = n===1?0 : (n<=3?1:2), tup = (n===3||n>=5)?n:null;
+ const LEFT=3, GAP = n<=2?8 : n<=4?6.5 : 5.5, baseY=15, topY=6;
+ const W=Math.round(LEFT*2 + (n-1)*GAP + 4); let g="", first=0, last=0;
+ for(let i=0;i';
+ g+=''; }
+ if(beams>=1) g+='';
+ if(beams>=2) g+='';
+ if(tup) g+=''+tup+'';
+ return '';
+}
function buildLanes(){
const box=$("lanes"); box.innerHTML="";
meters.forEach((m,i)=>{
const lane=document.createElement("div"); lane.className="lane"+(m.enabled?"":" off");
const meta=document.createElement("button"); meta.className="lmeta";
- meta.innerHTML=esc(m.sound)+" "+esc(m.groupsStr)+(m.stepsPerBeat>1?"/"+m.stepsPerBeat:"")+(m.poly?"~":"")+"";
+ meta.innerHTML=""+esc(m.sound)+""+rhythmSVG(m.stepsPerBeat)+""+esc(m.groupsStr)+(m.swing?" sw":"")+(m.poly?"~":"")+"";
meta.onclick=()=>openLaneSheet(i);
const pads=document.createElement("div"); pads.className="pads";
- const steps=m.beatsPerBar*m.stepsPerBeat; m._padEls=[]; m._lastPad=-1;
- for(let k=0;kcyclePad(m,k,p); pads.appendChild(p); m._padEls.push(p); }
+ const spb=m.stepsPerBeat; m._padEls=new Array(m.beatsPerBar*spb); m._lastPad=-1;
+ for(let b=0;bcyclePad(m,k,p); cell.appendChild(p); m._padEls[k]=p; }
+ pads.appendChild(cell); }
lane.appendChild(meta); lane.appendChild(pads); box.appendChild(lane);
});
const add=document.createElement("button"); add.className="addlane"; add.textContent="+ Add lane"; add.onclick=addLane; box.appendChild(add);
renderPadLevels();
}
-function renderPadLevels(){ meters.forEach(m=>{ if(!m._padEls) return; const spb=m.stepsPerBeat;
- m._padEls.forEach((p,k)=>{ const gs=(k%spb===0)&&m.groupStarts.has(k/spb); const lvl=m.beatsOn[k]|0; p.className="pad"+(gs?" gs":"")+(lvl?(" "+lvlClass(lvl)):""); }); }); }
-function cyclePad(m,k,p){ m.beatsOn[k]=((m.beatsOn[k]|0)+1)%4; if(m.orns) m.orns[k]=0; const spb=m.stepsPerBeat; const gs=(k%spb===0)&&m.groupStarts.has(k/spb); const lvl=m.beatsOn[k];
- p.className="pad"+(gs?" gs":"")+(lvl?(" "+lvlClass(lvl)):""); saveState(); }
+function renderPadLevels(){ meters.forEach(m=>{ if(!m._padEls) return;
+ m._padEls.forEach((p,k)=>{ if(p) p.className=padClass(m,k); }); }); }
+function cyclePad(m,k,p){ m.beatsOn[k]=((m.beatsOn[k]|0)+1)%4; if(m.orns) m.orns[k]=0; p.className=padClass(m,k); saveState(); }
function renderPadPlayheads(){ meters.forEach(m=>{ if(!m._padEls) return; const cur=state.running?m.currentStep:-1;
if(m._lastPad!==cur){ if(m._lastPad>=0&&m._padEls[m._lastPad]) m._padEls[m._lastPad].classList.remove("cur"); if(cur>=0&&m._padEls[cur]) m._padEls[cur].classList.add("cur"); m._lastPad=cur; } }); }
function rebuildLane(i,cfg){