<!-- SS · part 88 -->
trigger (realtime + offline)
  function triggerStep(G, step, time) {
    var bar = Math.floor(step / 16), s = step % 16, it = locate(bar); if (!it) return;
    var p = S.sections[it.name].patterns;
    DRUM_ROWS.forEach(function (r) { if (p.drums[r][s]) G.inst.drums.play(r, time, r === 'hat' ? (s % 4 === 0 ? 0.8 : 0.5) : r === 'perc' ? 0.7 : 0.95); });
    var b = p.bass[s]; if (b) G.inst.bass.triggerAttackRelease(b, '8n', time, 0.9);
    var c = p.chords[s];
    if (c) { var notes = chordNotes(c); if (notes) { var len = 1; for (var k2 = s + 1; k2 < STEPS && !p.chords[k2]; k2++) len++; G.inst.chords.triggerAttackRelease(notes, stepSec() * Math.min(len, 16) * 0.95, time, 0.6); } }
    var l = p.lead[s]; if (l) G.inst.lead.triggerAttackRelease(l, '16n', time, 0.7);
  }
  function swingOffset(s) { return s % 2 === 1 ? S.swing * stepSec() * 0.66 : 0; }

  // ------------------------------------------------------------------ realtime engine + transport
  var E = null, playing = false;
  function engine() {
    if (E) return E;
    if (!window.Tone) { toast('Audio engine failed to load. Reload the page.'); return null; }
    var T = Tone.getTransport();
    E = makeGraph(true); E.T = T; E.players = [];
    E.recorder = typeof MediaRecorder !== 'undefined' ? new Tone.Recorder() : null; if (E.recorder) E.mvol.connect(E.recorder);
    T.bpm.value = S.bpm; T.swing = S.swing; T.swingSubdivision = '16n';
    E.loop = new 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('n