<!-- SS · part 101 -->
.Gain(1)); G.out.connect(G.mIn); G.mIn.connect(G.mOut);
    G.mOut.chain(G.eq, G.drive, G.comp, G.width, G.limiter, G.mvol, Tone.getDestination());
    G.lIn = {}; G.lOut = {};
    G.verb = k(new Tone.Reverb({ decay: 2.6, wet: 1 })); G.verbSend = k(new Tone.Gain(0.2)); G.verbSend.connect(G.verb); G.verb.connect(G.out);
    LANES.forEach(function (t) { G.lIn[t] = k(new Tone.Gain(1)); G.lOut[t] = k(new Tone.Gain(1)); G.lIn[t].connect(G.lOut[t]); G.lOut[t].connect(G.out); G.ch[t] = k(new Tone.Channel({ volume: S.vols[t] || 0 }).connect(G.lIn[t])); });
    var cs = k(new Tone.Gain(0.8)); G.ch.chords.connect(cs); cs.connect(G.verbSend);
    var ls = k(new Tone.Gain(0.6)); G.ch.lead.connect(ls); ls.connect(G.verbSend);
    // vocal chain: highpass -> lowpass (vulnerability) -> EQ -> drive (aggression) -> comp (passion) -> channel; reverb send (melodrama)
    G.vin = k(new Tone.Gain(1)); G.vFilter = k(new Tone.Filter(16000, 'lowpass')); G.vHp = k(new Tone.Filter(90, 'highpass'));
    G.vEq = k(new Tone.EQ3(0, 0, 2)); G.vDrive = k(new Tone.Distortion({ distortion: 0, wet: 0 }));
    G.vComp = k(new Tone.Compressor({ threshold: -20, ratio: 3, attack: 0.005, release: 0.12 })); G.vMake = k(new Tone.Gain(1));
    G.vin.chain(G.vHp, G.vFilter, G.vEq, G.vDrive, G.vComp, G.vMake, G.ch.vocals);
    G.vSend = k(new Tone.Gain(0.3)); G.vMake.connect(G.vSend); G.vSend.connect(G.verbSend);
    ['drums', 'bass', 'chords', 'lead'].forEach(function (t) { setSound(G, t, S.sounds[t]); });
    if (withMeters) { G.fft = k(new Tone.FFT(1024)); G.mvol.connect(G.fft); }
    applyMix(G); applyMaster(G); applyVocalFx(G);
    (window.ES_HOOKS || []).forEach(function (h) { if (h.graph) try { h.graph(G, !!withMeters); } catch (e) { console.error('hook graph', e); } });
    G.dispose = function () { ['drums', 'bass', 'chords', 'lead'].forEach(function (t) { try { G.inst[t].dispose(); } catch (e) {} }); G.nodes.forEach(function (n) { try { n.dispose(); } catch (e) {} }); };
    return G;
  }
  function setSound(G, t, kind) {
    if (!G) return;
    if (G.inst[t]) { try { G.inst[t].releaseAll && G.inst[t].releaseAll(); } catch (e) {} try { G.inst[t].dispose(); } catch (e) {} }
    G.inst[t] = t === 'drums' ? makeDrums(kind, G.ch.drums) : makeSynth(t, kind, G.ch[t]);
  }
  function applyMix(G) {
    if (!G) return; var anySolo = LANES.some(function (t) { return S.solos[t]; });
    LANES.forEach(function (t) { G.ch[t].volume.value = S.vols[t] || 0; G.ch[t].mute = !!S.mutes[t] || (anySolo && !S.solos[t]); });
  }
  function applyMaster(G) {
    if (!G) return; var M = S.master;
    var drive = M.drive / 100; G.drive.distortion = drive * 0.6; G.drive.wet.value = drive > 0 ? Math.min(1, 0.25 + drive) : 0;
    var c = M.comp / 100; G.comp.threshold.value = -4 - c * 26; G.comp.ratio.value = 1 + c * 5;
    var tilt = M.eq / 100; G.eq.low.value = -tilt * 6 + (M.on ? 1.5 : 0); G.eq.high.value = tilt * 6 + (M.on ? 2 : 0); G.eq.mid.value = M.on ? -1 : 0;
    G.width.width.value = M.width / 100;
    G.verbSend.gain.value = (M.verb / 100) * 0.6;
    G.limiter.threshold.value = M.on ? -0.8 : -1; G.mvol.gain.value = M.on ? 1.0 : 0.85;
  }
  function applyVocalFx(G) {
    if (!G) return; var V = S.voice;
    G.vDrive.distortion = V.aggr / 100 * 0.35; G.vDrive.wet.value = V.aggr > 15 ? 0.15 + V.aggr / 250 : 0;
    G.vSend.gain.value = 0.1 + V.drama / 100 * 0.9;
    G.vFilter.frequency.value = 16000 - V.vuln / 100 * 11000;
    G.vComp.ratio.value = 1.5 + V.pass / 100 * 5; G.vMake.gain.value = 1 + V.pass / 100 * 0.6;
  }
  // shared step 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 = n