<!-- SS · part 29 -->
urve();
  }
  var applyT = null;
  function changed(e, structural) {
    e.preset = e.preset || ''; setEq(cur, e); drawCurve();
    clearTimeout(applyT); applyT = setTimeout(function () {
      var G = window.ES && window.ES.engine && window.ES.engine();
      if (cur.indexOf('stem:') === 0) { if (stemApi) stemApi.rewire(cur.slice(5)); } else if (G) graphWire(G, cur);
    }, structural ? 0 : 60);
  }
  tSel.addEventListener('change', function () { cur = tSel.value; render(); });
  $('#es-eq-on').addEventListener('change', function () { var e = getEq(cur); e.on = this.checked; changed(e, true); });
  pSel.addEventListener('change', function () {
    var v = pSel.value; if (!v) return; var e = v.indexOf('c:') === 0 ? clone(CUSTOM[v.slice(2)]) : PRESETS[v](); e.on = true; e.preset = v; setEq(cur, e); render(); changed(e, true);
  });
  $('#es-eq-save').addEventListener('click', function () { var n = prompt('Name this EQ preset'); if (!n || !n.trim()) return; n = n.trim().slice(0, 40); var e = clone(getEq(cur)); e.preset = 'c:' + n; CUSTOM[n] = e; setEq(cur, e); persist(); refreshPresets(); window.ES && ES.toast('Preset "' + n + '" saved.'); });
  $('#es-eq-del').addEventListener('click', function () { var v = pSel.value; if (v.indexOf('c:') !== 0) return window.ES && ES.toast('Pick one of your ★ presets to delete.'); if (!confirm('Delete preset ' + v.slice(2) + '?')) return; delete CUSTOM[v.slice(2)]; persist(); refreshPresets(); });
  $('#es-geq').addEventListener('input', function (ev) { var i = ev.target.getAttribute('data-g'); if (i == null) return; var e = getEq(cur); e.geq[+i] = +ev.target.value; e.preset = ''; ev.target.parentNode.querySelector('b').textContent = (e.geq[i] > 0 ? '+' : '') + e.geq[i]; changed(e, false); });
  $('#es-peq').addEventListener('input', function (ev) {
    var k = ev.target.getAttribute('data-k'); if (!k) return; var r = ev.target.closest('[data-b]'), b = getEq(cur).peq[+r.getAttribute('data-b')], e = getEq(cur);
    if (k === 'on') b.on = ev.target.checked; else if (k === 'type') b.type = ev.target.value; else if (k === 'f') b.f = s2f(+ev.target.value); else b[k] = +ev.target.value;
    var lab = ev.target.parentNode.querySelector('b'); if (lab) lab.textContent = k === 'f' ? fLabel(b.f) : k === 'g' ? (b.g > 0 ? '+' : '') + b.g : (+b.q).toFixed(1);
    e.preset = ''; changed(e, k === 'on' || k === 'type');
  });
  $('#es-peq').addEventListener('change', function (ev) { if (ev.target.getAttribute('data-k') === 'type') { var r = ev.target.closest('[data-b]'); getEq(cur).peq[+r.getAttribute('data-b')].type = ev.target.value; changed(getEq(cur), true); } });
  // curve + live spectrum
  var cv = $('#es-eq-cv'), NF = 160, LF = []; for (var i = 0; i < NF; i++) LF.push(20 * Math.pow(1000, i / (NF - 1)));
  function drawCurve() {
    var w = cv.clientWidth, h = cv.clientHeight; if (!w) return; var dpr = window.devicePixelRatio || 1; if (cv.width !== w * dpr) { 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);
    g.strokeStyle = '#1a1a1a'; g.fillStyle = '#555'; g.font = '9px ui-monospace,monospace';
    [-12, 0, 12].forEach(function (d) { var y = h / 2 - d / 18 * (h / 2 - 6); g.beginPath(); g.moveTo(0, y); g.lineTo(w, y); g.stroke(); g.fillText((d > 0 ? '+' : '') + d, 2, y - 2); });
    [100, 1000, 10000].forEach(function (f) { var x = Math.log(f / 20) / Math.log(1000) * w; g.beginPath(); g.moveTo(x, 0); g.lineTo(x, h); g.stroke(); g.fillText(fLabel(f), x + 2, h - 3); });
    var G = window.ES && ES.engine && ES.engine();
    if (G && G.fft && ES.isPlaying()) { var v = G.fft.getValue(), n = v.length, sr = Tone.getContext().sampleRate; g.fillStyle = 'rgba(227,18,44,.35)';
      for (var x = 0; x < w; x += 2) { var f = 20 * Math.pow(1000, x / w), bi = Math.min(n - 1, Math.round(f / (sr / 2) * n)), db = Math.max(-100, v[bi]); var bh = (db + 100) / 100 * h; g.fillRect(x, h - bh, 2, bh); } }
    var r = response(getEq(cur) || flat(), LF); g.strokeStyle = '#e8c36a'; g.lineWidth = 2; g.beginPath();
    for (var j = 0; j < NF; j++) { var X = j / (NF - 1) * w, Y = h / 2 - Math.max(-18, Math.min(18, r[j])) / 18 * (h / 2 - 6); if (j) g.lineTo(X, Y); else g.moveTo(X, Y); } g.stroke();
  }
  (function loop() { if (root.offsetParent && window.ES && ES.isPlaying && ES.isPlaying()) drawCurve(); requestAnimationFrame(loop); })();
  window.addEventListener('resize', function () { setTimeout(drawCurve, 200); });
  document.addEventListener('click', function (ev) { var b = ev.target.closest && ev.target.closest('.es-mtabs button[data-m="mix"]'); if (b) setTimeout(drawCurve, 50); });
  refreshTargets(); render();
})();


===== FILE: public/ss/studio-kit.js =====
/* Epistles Studio — SOUND KIT (admin-first): the creator's own samples -> sampler pads that replace the built-in synths,
   sample chopper, beat templates, "My Sounds Only" switch and a provenance note saved wi