<!-- SS · part 47 -->
== 0) { g.fillStyle = '#555'; g.font = '9px monospace'; g.fillText(nm(m), 2, y + rh - 2); } }
    for (var b = 0; b <= g0.tot; b++) { g.fillStyle = b % 4 ? '#1c1c1c' : '#3a2a10'; g.fillRect(b / g0.tot * w, 0, 1, h); }
    var t = 0; g0.ns.forEach(function (n, i) { var x = t / g0.tot * w, nw = n.beats / g0.tot * w, y = h - (n.midi - g0.lo + 1) * rh; g.fillStyle = i === selNote ? '#e8c36a' : '#e3122c'; g.fillRect(x + 1, y, Math.max(3, nw - 2), Math.max(4, rh)); g.fillStyle = '#fff'; g.font = '10px sans-serif'; g.fillText(n.syl, x + 3, y - 2); t += n.beats; });
    if (!g0.ns.length) { g.fillStyle = '#777'; g.font = '12px sans-serif'; g.fillText('No melody for this line yet — press Write melody.', 10, h / 2); }
  }
  function hit(e) { var r = cv.getBoundingClientRect(), g0 = geo(), x = e.clientX - r.left, y = e.clientY - r.top, t = 0, rh = r.height / (g0.hi - g0.lo + 1); for (var i = 0; i < g0.ns.length; i++) { var n = g0.ns[i], x0 = t / g0.tot * r.width, x1 = (t + n.beats) / g0.tot * r.width, ny = r.height - (n.midi - g0.lo + 1) * rh; if (x >= x0 && x <= x1 && y >= ny - 8 && y <= ny + rh + 8) return { i: i, edge: x1 - x < 10, x: x, y: y, rh: rh, bw: r.width / g0.tot }; t += n.beats; } return null; }
  cv.style.touchAction = 'none';
  cv.addEventListener('pointerdown', function (e) { var h0 = hit(e); if (!h0) return; cv.setPointerCapture(e.pointerId); selNote = h0.i; var n = lineNotes(curLine)[h0.i]; drag = Object.assign(h0, { midi: n.midi, beats: n.beats }); draw(); noteUI(); });
  cv.addEventListener('pointermove', function (e) { if (!drag) return; var r = cv.getBoundingClientRect(), n = lineNotes(curLine)[drag.i]; if (drag.edge) n.beats = Math.max(0.25, Math.round((drag.beats + (e.clientX - r.left - drag.x) / drag.bw) * 4) / 4); else n.midi = Math.max(24, Math.min(96, drag.midi + Math.round((drag.y - (e.clientY - r.top)) / drag.rh))); draw(); noteUI(); });
  cv.addEventListener('pointerup', function () { if (drag) { drag = null; store(MK, MEL); if (window.Tone) { var n = lineNotes(curLine)[selNote]; if (n) ES.unlock().then(function () { var s = new Tone.Synth({ envelope: { attack: 0.01, release: 0.2 } }).toDestination(); s.triggerAttackRelease(Tone.Frequency(n.midi, 'midi'), 0.25); setTimeout(function () { s.dispose(); }, 800); }).catch(function () {}); } } });
  var NK = [['say', 'TTS spelling', 'text'], ['vib', 'Vibrato ¢', 0, 150, 5], ['vibRate', 'Vib rate', 2, 9, 0.1], ['slide', 'Slide ms', 0, 400, 10], ['formant', 'Formant', -6, 6, 0.5], ['bright', 'Bright dB', -12, 12, 0.5], ['breath', 'Breath', 0, 1, 0.05], ['robot', 'Correction', 0, 1, 0.05], ['cons', 'Consonant', 0.05, 0.6, 0.05], ['tts', 'Word speed', 0.6, 1.6, 0.05]];
  function noteUI() {
    var ns = lineNotes(curLine), n = ns && ns[selNote], box = $('#es-sg-note');
    if (!n) { box.innerHTML = '<span class="es-hint">Tap a note. Drag up/down for pitch, drag its right edge for length.</span>'; return; }
    box.innerHTML = '<div class="es-sg-nh"><b>' + esc(n.syl) + '</b> ' + nm(n.midi) + ' · ' + n.beats + ' beats <button type="button" class="es-btn es-btn-sm" data-n="up">♯</button><button type="button" class="es-btn es-btn-sm" data-n="dn">♭</button><button type="button" class="es-btn es-btn-sm" data-n="longer">+¼</button><button type="button" class="es-btn es-btn-sm" data-n="shorter">−¼</button><button type="button" class="es-btn es-btn-sm" data-n="reset">Use defaults</button></div>' +
      '<div class="es-sg-ng">' + NK.map(function (k) { return k[2] === 'text' ? '<label>' + k[1] + '<input type="text" data-nk="say" maxlength="40" placeholder="(as written)" value="' + esc(n.say || '') + '"></label>' : '<label>' + k[1] + ' <i>' + (n[k[0]] == null ? 'default' : n[k[0]]) + '</i><input type="range" data-nk="' + k[0] + '" min="' + k[2] + '" max="' + k[3] + '" step="' + k[4] + '" value="' + (n[k[0]] == null ? G[k[0]] != null ? G[k[0]] : 1 : n[k[0]]) + '"></label>'; }).join('') + '</div>';
  }
  $('#es-sg-note').addEventListener('input', function (e) { var k = e.target.getAttribute('data-nk'), n = lineNotes(curLine)[selNote]; if (!k || !n) return; n[k] = k === 'say' ? e.target.value : +e.target.value; var i = e.target.parentNode.querySelector('i'); if (i) i.textContent = n[k]; store(MK, MEL); });
  $('#es-sg-note').addEventListener('click', function (e) { var a = e.target.getAttribute('data-n'), n = lineNotes(curLine) && lineNotes(curLine)[selNote]; if (!a || !n) return; if (a === 'up') n.midi++; if (a === 'dn') n.midi--; if (a === 'longer') n.beats += 0.25; if (a === 'shorter') n.beats = Math.max(0.25, n.beats - 0.25); if (a === 'reset') NK.forEach(function (k) { delete n[k[0]]; }); store(MK, MEL); draw(); noteUI(); });
  function boot(api) { ES = api; syncG(); fillLines(); }
  (window.ES_HOOKS = window.ES_HOOKS || []).push({ ready: boot });
  if (window.ES) boot(window.ES);
  window.addEventListener('resize', function () { setTimeout(draw, 150); });
  document.addEventListener('click', function (ev) { var b = ev.target.close