diff --git a/mobile.html b/mobile.html
index 2ed8b28..a5e7fe4 100644
--- a/mobile.html
+++ b/mobile.html
@@ -34,14 +34,14 @@
--txt:#e7edf5; --muted:#8b96a5; --link:#6cb6ff;
--panel-bg:#161b22; --panel-bd:#2a313c; --field-bg:#0e1116; --field-bd:#222a36;
--cyan:#0AB3F7; --amber:#ffd166;
- --led-off:#1b2330; --ring:#2a3340; --glow:rgba(10,179,247,.55); --aglow:rgba(255,209,102,.55);
+ --led-off:#1b2330; --ring:#2a3340; --glow:rgba(10,179,247,.55); --aglow:rgba(255,209,102,.55); --poly:#bb8cff;
--btn1:#2b323d; --btn2:#1b212a; --btn-bd:#39424f; --chip-bg:#1b2230; --chip-bd:#2c3545;
}
:root[data-theme="light"]{
--bg1:#eef3f9; --bg2:#cfd9e6;
--txt:#10202f; --muted:#5c6776; --link:#1769c4;
--panel-bg:#ffffff; --panel-bd:#d2dae4; --field-bg:#f1f4f8; --field-bd:#cdd6e0;
- --led-off:#c4cedb; --ring:#c9d4e1; --glow:rgba(10,179,247,.40); --aglow:rgba(230,160,30,.45);
+ --led-off:#c4cedb; --ring:#c9d4e1; --glow:rgba(10,179,247,.40); --aglow:rgba(230,160,30,.45); --poly:#7a3df0;
--btn1:#ffffff; --btn2:#e7edf4; --btn-bd:#c8d2de; --chip-bg:#eef2f7; --chip-bd:#d3dbe5;
}
html,body{ height:100%; }
@@ -104,12 +104,14 @@
#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:34%; max-width:150px; min-width:92px; display:flex; align-items:center; gap:5px; text-align:left;
+ .lmeta{ flex:0 0 auto; width:36%; max-width:168px; min-width:94px; 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 .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; }
+ .lmeta .polybadge{ flex:0 0 auto; color:var(--poly); font-weight:700; letter-spacing:.01em; }
+ .lane.poly .lmeta{ border-left:3px solid var(--poly); padding-left:6px; }
/* 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; }
@@ -120,6 +122,8 @@
.pad.on{ background:var(--cyan); }
.pad.acc{ background:var(--amber); }
.pad.ghost{ background:var(--cyan); opacity:.42; }
+ .lane.poly .pad.on{ background:var(--poly); }
+ .lane.poly .pad.ghost{ background:var(--poly); opacity:.42; }
.pad.cur{ outline:2px solid var(--txt); outline-offset:-1px; box-shadow:0 0 8px var(--glow); }
.addlane{ align-self:flex-start; background:transparent; border:1px dashed var(--chip-bd); color:var(--muted); border-radius:8px; padding:5px 11px; font-size:12px; cursor:pointer; }
.chips{ display:flex; flex-wrap:wrap; gap:6px; justify-content:center; }
@@ -451,27 +455,49 @@ function laneSignature(){ return meters.map(m=>m.sound+":"+m.groupsStr+"/"+m.ste
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 '';
+// Effective note value a lane actually plays: reduce the subdivision grid to the
+// largest note that lands on every active hit (so a triplet grid that only plays
+// the beat shows a quarter, not a triplet). gcd(stepsPerBeat, all active offsets).
+function gcd(a,b){ a=Math.abs(a); b=Math.abs(b); while(b){ const t=a%b; a=b; b=t; } return a; }
+function laneNoteValue(m){
+ const spb=m.stepsPerBeat; let g=spb, any=false;
+ for(let k=0;k0){ any=true; g=gcd(g,k); } }
+ if(!any) return 1;
+ return Math.max(1, spb/g);
}
+// Small engraved rhythm figure (1=quarter, 2=8ths, 3=triplet, 4=16ths, 5/6/7=tuplets);
+// swung eighths render as the dotted-8th + 16th shuffle figure. Drawn as SVG.
+function rhythmSVG(n, swing){
+ n=Math.max(1,n|0);
+ const baseY=15, topY=6, stemH=(baseY-topY-0.5).toFixed(1);
+ const head=(cx)=>'';
+ const stem=(sx)=>'';
+ const beam=(x0,x1,y)=>'';
+ const wrap=(W,g)=>'';
+ if(swing && n===2){ // dotted-eighth + sixteenth (shuffle)
+ const LEFT=3, U=4.6, cx0=LEFT, cx1=LEFT+2*U, s0=cx0+2, s1=cx1+2, W=Math.round(cx1+8);
+ let g=head(cx0)+head(cx1)+stem(s0)+stem(s1)+beam(s0-0.45,s1+0.45,topY)+beam(s1-3.4,s1+0.45,topY+2.6)
+ +'';
+ return wrap(W,g);
+ }
+ const 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, W=Math.round(LEFT*2 + (n-1)*GAP + 4);
+ let g="", first=0, last=0;
+ for(let i=0;i=1) g+=beam(first-0.45,last+0.45,topY);
+ if(beams>=2) g+=beam(first-0.45,last+0.45,topY+2.6);
+ if(tup) g+=''+tup+'';
+ return wrap(W,g);
+}
+function laneMetaHTML(m){ const eff=laneNoteValue(m), swGlyph=m.swing&&eff===2;
+ const ref=(meters[0]?meters[0].beatsPerBar:m.beatsPerBar);
+ const poly=m.poly?"↻"+m.beatsPerBar+":"+ref+"":"";
+ return ""+esc(m.sound)+""+rhythmSVG(eff,m.swing)+""+esc(m.groupsStr)+(m.swing&&!swGlyph?" sw":"")+""+poly; }
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)+""+rhythmSVG(m.stepsPerBeat)+""+esc(m.groupsStr)+(m.swing?" sw":"")+(m.poly?"~":"")+"";
+ const lane=document.createElement("div"); lane.className="lane"+(m.enabled?"":" off")+(m.poly?" poly":"");
+ const meta=document.createElement("button"); meta.className="lmeta"; meta.innerHTML=laneMetaHTML(m); m._meta=meta;
meta.onclick=()=>openLaneSheet(i);
const pads=document.createElement("div"); pads.className="pads";
const spb=m.stepsPerBeat; m._padEls=new Array(m.beatsPerBar*spb); m._lastPad=-1;
@@ -485,7 +511,9 @@ function buildLanes(){
}
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 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);
+ if(m._meta) m._meta.innerHTML=laneMetaHTML(m); // note value can change as hits are added/removed
+ 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){