<!-- SS · part 28 -->
STOM = {}; try { CUSTOM = JSON.parse(localStorage.getItem(PKEY) || '{}') || {}; } catch (x) { CUSTOM = {}; }
  ['master', 'vocals', 'drums', 'bass', 'chords', 'lead'].forEach(function (t) { if (!ST[t] || !ST[t].geq) ST[t] = flat(); });
  function persist() { try { localStorage.setItem(KEY, JSON.stringify(ST)); localStorage.setItem(PKEY, JSON.stringify(CUSTOM)); } catch (x) {} }
  var stemApi = null; // set by the record module: { list(), get(id) -> eq, set(id, eq), rewire(id) }
  function getEq(t) { if (t.indexOf('stem:') === 0) return stemApi && stemApi.getEq(t.slice(5)); return ST[t]; }
  function setEq(t, e) { if (t.indexOf('stem:') === 0) { if (stemApi) stemApi.setEq(t.slice(5), e); return; } ST[t] = e; persist(); }
  function graphWire(G, t) {
    G.eqF = G.eqF || {};
    if (t === 'master') G.eqF.master = wire(G.mIn, G.mOut, ST.master, G.eqF.master);
    else if (G.lIn && G.lIn[t]) G.eqF[t] = wire(G.lIn[t], G.lOut[t], ST[t], G.eqF[t]);
  }
  window.ESEQ = { FREQS: FREQS, PRESETS: PRESETS, flat: flat, wire: wire, response: response, applyToBuffer: applyToBuffer, isFlat: isFlat, clone: clone, setStemApi: function (a) { stemApi = a; refreshTargets(); }, refreshTargets: function () { refreshTargets(); } };
  (window.ES_HOOKS = window.ES_HOOKS || []).push({ graph: function (G) { ['master', 'vocals', 'drums', 'bass', 'chords', 'lead'].forEach(function (t) { graphWire(G, t); }); } });

  // ---- UI
  var root = document.getElementById('es-eqp'); if (!root) return;
  function $(s) { return root.querySelector(s); }
  function esc(s) { return String(s).replace(/[&<>"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]; }); }
  function fLabel(f) { return f >= 1000 ? (f / 1000).toFixed(f % 1000 ? 1 : 0) + 'k' : String(Math.round(f)); }
  function f2s(f) { return Math.round(Math.log(f / 20) / Math.log(1000) * 1000); } function s2f(v) { return Math.round(20 * Math.pow(1000, v / 1000)); }
  root.innerHTML =
    '<h2 class="es-h"><span class="es-ic">≋</span> EQUALIZER <small>GRAPHIC 10-BAND · PARAMETRIC 6-BAND</small></h2>' +
    '<div class="es-eq-top"><label>Target <select id="es-eq-t"></select></label><label class="es-eq-on"><input type="checkbox" id="es-eq-on"> On</label></div>' +
    '<div class="es-eq-top"><label>Preset <select id="es-eq-p"></select></label><button type="button" class="es-btn es-btn-sm" id="es-eq-save">Save preset</button><button type="button" class="es-btn es-btn-sm" id="es-eq-del">Delete</button></div>' +
    '<canvas id="es-eq-cv" class="es-eq-cv"></canvas>' +
    '<div class="es-geq" id="es-geq"></div>' +
    '<div class="es-peq" id="es-peq"></div>';
  var tSel = $('#es-eq-t'), pSel = $('#es-eq-p'), cur = 'master';
  function refreshTargets() {
    if (!tSel) return; var o = [['master', 'Master'], ['vocals', 'Vocals lane'], ['drums', 'Drums lane'], ['bass', 'Bass lane'], ['chords', 'Chords lane'], ['lead', 'Lead lane']];
    if (stemApi) stemApi.list().forEach(function (s) { o.push(['stem:' + s.id, 'Stem: ' + s.name]); });
    if (!o.some(function (x) { return x[0] === cur; })) cur = 'master';
    tSel.innerHTML = o.map(function (x) { return '<option value="' + esc(x[0]) + '"' + (x[0] === cur ? ' selected' : '') + '>' + esc(x[1]) + '</option>'; }).join('');
  }
  function refreshPresets() { var e = getEq(cur) || flat(); pSel.innerHTML = '<option value="">— custom —</option>' + Object.keys(PRESETS).map(function (k) { return '<option' + (e.preset === k ? ' selected' : '') + '>' + esc(k) + '</option>'; }).join('') + Object.keys(CUSTOM).map(function (k) { return '<option value="c:' + esc(k) + '"' + (e.preset === 'c:' + k ? ' selected' : '') + '>★ ' + esc(k) + '</option>'; }).join(''); }
  function render() {
    var e = getEq(cur) || flat(); $('#es-eq-on').checked = !!e.on; refreshPresets();
    $('#es-geq').innerHTML = FREQS.map(function (f, i) { return '<label class="es-geq-b"><b>' + (e.geq[i] > 0 ? '+' : '') + e.geq[i] + '</b><input type="range" min="-12" max="12" step="1" value="' + e.geq[i] + '" data-g="' + i + '" aria-label="' + fLabel(f) + ' Hz"><span>' + fLabel(f) + '</span></label>'; }).join('');
    $('#es-peq').innerHTML = e.peq.map(function (b, i) {
      return '<div class="es-peq-r" data-b="' + i + '"><input type="checkbox" data-k="on"' + (b.on ? ' checked' : '') + ' aria-label="Band ' + (i + 1) + ' on"><select data-k="type">' + TYPES.map(function (t) { return '<option' + (t === b.type ? ' selected' : '') + '>' + t + '</option>'; }).join('') + '</select>' +
        '<label>F <b>' + fLabel(b.f) + '</b><input type="range" data-k="f" min="0" max="1000" value="' + f2s(b.f) + '"></label>' +
        '<label>G <b>' + (b.g > 0 ? '+' : '') + b.g + '</b><input type="range" data-k="g" min="-18" max="18" step="0.5" value="' + b.g + '"></label>' +
        '<label>Q <b>' + (+b.q).toFixed(1) + '</b><input type="range" data-k="q" min="0.3" max="10" step="0.1" value="' + b.q + '"></label></div>';
    }).join('');
    drawC