<!-- SS · part 27 -->
});
    }
  });

  // ---------------------------------------------------------------- boot
  fillWrite(); syncStudio();
  var h = (location.hash || '').slice(1);
  if (['write', 'studio', 'voice', 'songs'].indexOf(h) >= 0) showTab(h);
  window.addEventListener('hashchange', function () { var t = (location.hash || '').slice(1); if (['write', 'studio', 'voice', 'songs'].indexOf(t) >= 0 && !$('.ss-panel[data-panel="' + t + '"]').classList.contains('is-on')) showTab(t); });
  document.addEventListener('visibilitychange', function () { if (document.hidden && playing) stop(); });
})();


===== FILE: public/ss/studio-eq.js =====
/* Epistles Studio — EQ module: 10-band graphic + 6-band parametric, per lane / stem / master. Free, WebAudio only. */
(function () {
  'use strict';
  var FREQS = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
  var TYPES = ['peaking', 'lowshelf', 'highshelf', 'highpass', 'lowpass'];
  function defPeq() { return [{ on: false, type: 'highpass', f: 40, g: 0, q: 0.7 }, { on: true, type: 'lowshelf', f: 120, g: 0, q: 0.7 }, { on: true, type: 'peaking', f: 400, g: 0, q: 1 }, { on: true, type: 'peaking', f: 2500, g: 0, q: 1 }, { on: true, type: 'highshelf', f: 8000, g: 0, q: 0.7 }, { on: false, type: 'lowpass', f: 18000, g: 0, q: 0.7 }]; }
  function flat() { return { on: true, preset: 'Flat', geq: FREQS.map(function () { return 0; }), peq: defPeq() }; }
  function P(geq, mod) { return function () { var e = flat(); e.geq = geq.slice(); if (mod) mod(e.peq); return e; }; }
  var PRESETS = {
    'Flat': P([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
    'Vocal Presence': P([-2, -2, -1, 0, 0, 1, 3, 4, 2, 1], function (p) { p[0].on = true; p[0].f = 80; }),
    'Bass Boost': P([6, 5, 4, 2, 0, 0, 0, 0, 0, 0]),
    'Radio / Telephone': P([-12, -12, -10, -4, 2, 4, 4, 0, -10, -12], function (p) { p[0].on = true; p[0].f = 300; p[5].on = true; p[5].f = 3400; }),
    'Warm': P([2, 3, 3, 2, 0, 0, -1, -2, -2, -3]),
    'Bright': P([0, 0, 0, 0, 0, 1, 2, 3, 4, 5]),
    'Lo-Fi': P([-6, -2, 1, 2, 2, 1, -2, -6, -10, -12], function (p) { p[5].on = true; p[5].f = 5000; }),
    'De-Mud': P([0, 0, -1, -3, -4, -2, 0, 0, 0, 0]),
    'Rock': P([5, 4, 3, -1, -2, -1, 2, 4, 5, 5]),
    'Pop': P([-1, 1, 3, 4, 3, 0, -1, -1, -1, -1]),
    'Jazz': P([3, 2, 1, 2, -1, -1, 0, 1, 2, 3]),
    'Classical': P([0, 0, 0, 0, 0, 0, -2, -3, -3, -5])
  };
  function clone(o) { return JSON.parse(JSON.stringify(o)); }
  function isFlat(e) { return !e || !e.on || (e.geq.every(function (g) { return !g; }) && e.peq.every(function (b) { return !b.on || ((b.type === 'peaking' || /shelf/.test(b.type)) && !b.g); })); }
  function bands(e) {
    var out = [];
    e.geq.forEach(function (g, i) { if (g) out.push({ type: 'peaking', f: FREQS[i], g: g, q: 1.41 }); });
    e.peq.forEach(function (b) { if (b.on && !((b.type === 'peaking' || /shelf/.test(b.type)) && !b.g)) out.push(b); });
    return out;
  }
  // Wire in -> [filters] -> out. Returns the filter list (Tone.Filter) so callers can dispose.
  function wire(inNode, outNode, e, old) {
    (old || []).forEach(function (f) { try { f.dispose(); } catch (x) {} });
    try { inNode.disconnect(); } catch (x) {}
    if (isFlat(e)) { inNode.connect(outNode); return []; }
    var fl = bands(e).map(function (b) { return new Tone.Filter({ type: b.type, frequency: b.f, gain: b.g || 0, Q: b.q || 0.7, rolloff: -12 }); });
    var prev = inNode; fl.forEach(function (f) { prev.connect(f); prev = f; }); prev.connect(outNode);
    return fl;
  }
  // Frequency response (dB) at the given frequencies, using native biquads on a scratch context.
  var scratch = null;
  function response(e, freqs) {
    var tot = new Float32Array(freqs.length); if (isFlat(e)) return tot;
    if (!scratch) { var OAC = window.OfflineAudioContext || window.webkitOfflineAudioContext; scratch = new OAC(1, 128, 44100); }
    var fr = new Float32Array(freqs), mag = new Float32Array(freqs.length), ph = new Float32Array(freqs.length);
    bands(e).forEach(function (b) {
      var bq = scratch.createBiquadFilter(); bq.type = b.type; bq.frequency.value = b.f; bq.gain.value = b.g || 0; bq.Q.value = b.q || 0.7;
      bq.getFrequencyResponse(fr, mag, ph); for (var i = 0; i < freqs.length; i++) tot[i] += 20 * Math.log10(Math.max(1e-6, mag[i]));
    });
    return tot;
  }
  function applyToBuffer(buf, e) { // AudioBuffer -> Promise<AudioBuffer>
    if (isFlat(e)) return Promise.resolve(buf);
    return Tone.Offline(function () { var pl = new Tone.Player(new Tone.ToneAudioBuffer(buf)), o = new Tone.Gain(1).toDestination(), i = new Tone.Gain(1); pl.connect(i); wire(i, o, e); pl.start(0); }, buf.duration, buf.numberOfChannels, buf.sampleRate).then(function (tb) { return tb.get ? tb.get() : tb; });
  }
  // ---- state: master + lanes (stems keep their own eq in their meta)
  var KEY = 'es_eq_v1', PKEY = 'es_eq_presets_v1';
  var ST = {}; try { ST = JSON.parse(localStorage.getItem(KEY) || '{}') || {}; } catch (x) { ST = {}; }
  var CU