<!-- SS · part 102 -->
ew Tone.Loop(function (time) {
      var ticks = T.getTicksAtTime(time), step = Math.floor((ticks + 8) / (T.PPQ / 4));
      if (step >= PLAN.bars * 16 && !T.loop) return;
      triggerStep(E, step, time);
      Tone.getDraw().schedule(function () { onStepDraw(step); }, time);
    }, '16n').start(0);
    return E;
  }
  function checkUnlock() { $('#es-unlock').hidden = !!(window.Tone && Tone.getContext().state === 'running'); }
  function unlock() { if (!window.Tone) return Promise.reject(new Error('no Tone')); return Tone.start().then(function () { engine(); checkUnlock(); }); }
  $('#es-unlock-btn').addEventListener('click', function () { unlock().then(function () { toast('Sound is on.'); }, function () { toast('Could not start audio.'); }); });
  var loopSection = false;
  var HOOKS = window.ES_HOOKS = window.ES_HOOKS || [];
  function hook(name) { var args = [].slice.call(arguments, 1), out = []; HOOKS.forEach(function (h) { if (h[name]) try { out.push(h[name].apply(null, args)); } catch (e) { console.error('hook', name, e); } }); return out; }
  function songEnd() { return Math.max.apply(null, [totalSec()].concat(hook('end'))); }
  function scheduleVox() {
    (E.players || []).forEach(function (p) { try { p.unsync(); p.dispose(); } catch (e) {} }); E.players = [];
    vocalPlan().forEach(function (v) { var c = VOX.clips[v.text]; if (!c) return; var pl = new Tone.Player(c.buf).connect(E.vin); pl.sync().start(v.t); E.players.push(pl); });
    hook('schedule', E, true);
  }
  var endId = null;
  function play(fromSec) {
    return unlock().then(function () {
      var T = E.T; T.stop(); if (endId != null) { T.clear(endId); endId = null; }
      T.bpm.value = S.bpm; T.swing = S.swing;
      scheduleVox();
      var it = PLAN.items[S.sel];
      if (loopSection && it) { T.loop = true; T.loopStart = it.start * barSec(); T.loopEnd = (it.start + it.bars) * barSec(); fromSec = it.start * barSec(); }
      else { T.loop = false; endId = T.scheduleOnce(function (t) { Tone.getDraw().schedule(function () { if (!recording) { stop(); seekTo(0); } }, t); }, songEnd() + 0.6); }
      T.start('+0.06', fromSec || 0); playing = true; setPlayUI();
    });
  }
  function stop() { if (!E) return; E.T.stop(); playing = false; setPlayUI(); onStepDraw(-1); }
  function setPlayUI() { checkUnlock(); var b = $('#es-play'); b.textContent = playing ? '❚❚' : '▶'; b.classList.toggle('is-on', playing); $('#es-vplay').textContent = playing ? '■ Stop' : '▶ Play'; }
  var seekPos = 0;
  function curPos() { if (E && playing) return E.T.seconds; return seekPos; }
  function seekTo(sec) { seekPos = Math.max(0, Math.min(totalSec(), sec)); if (E && playing) play(seekPos); updatePlayhead(); }
  $('#es-play').addEventListener('click', function () { if (playing) { seekPos = curPos(); stop(); } else play(seekPos >= totalSec() - 0.05 ? 0 : seekPos).catch(function () { toast('Tap again to start audio.'); }); });
  $('#es-stop').addEventListener('click', function () { stop(); seekTo(0); });
  $('#es-rew').addEventListener('click', function () { var was = playing; stop(); seekTo(0); if (was) play(0); });
  $('#es-vplay').addEventListener('click', function () { if (playing) stop(); else play(0).catch(function () {}); });
  $('#es-loop').addEventListener('click', function () { loopSection = !loopSection; this.classList.toggle('is-on', loopSection); this.textContent = loopSection ? '⟲ SECTION' : '⟲ SONG'; if (playing) play(); });

  function onStepDraw(step) {
    $$('.es-cell.now').forEach(function (c) { c.classList.remove('now'); });
    if (step >= 0) { var it = locate(Math.floor(step / 16)); if (it && it.i === S.sel) $$('.es-cell[data-s="' + (step % 16) + '"]').forEach(function (c) { c.classList.add('now'); }); $$('.es-secbtn').forEach(function (b) { b.classList.toggle('is-play', !!it && +b.getAttribute('data-i') === it.i); }); }
    else $$('.es-secbtn.is-play').forEach(function (b) { b.classList.remove('is-play'); });
  }
  function updatePlayhead() {
    var pos = curPos(), tot = totalSec() || 1;
    $('#es-playhead').style.left = (Math.min(1, pos / tot) * 100) + '%';
    $('#es-time').textContent = mmss(pos) + ' / ' + mmss(tot);
  }
  (function raf() { if (playing) { updatePlayhead(); drawSpectrum(); } requestAnimationFrame(raf); })();

  // ------------------------------------------------------------------ timeline
  function drawTimeline() {
    PLAN = plan();
    var wrap = $('#es-tl-wrap'), cv = $('#es-tl'); var w = wrap.clientWidth, h = 250, dpr = window.devicePixelRatio || 1;
    renderSecButtons(); updatePlayhead();
    $('#es-tl-sub').textContent = 'AI-ARRANGED · ' + PLAN.items.length + ' SECTIONS · ' + PLAN.bars + ' BARS · ' + mmss(totalSec());
    if (!w) return;
    cv.width = w * dpr; cv.height = h * dpr; var g = cv.getContext('2d'); g.setTransform(dpr, 0, 0, dpr, 0, 0);
    g.fillStyle = '#050505'; g.fillRect(0, 0, w, h);
    var bars = PLAN.bars || 1, bx = w / bars, RU = 20, LH = 46;
    PLAN.items.forEach(