<!-- SS · part 52 -->
y('Saved as a new stem (the original is unchanged). Find it in Record.');
      case 'wav': var u = URL.createObjectURL(ES.wavEncode(b, b.sampleRate)), x = document.createElement('a'); x.href = u; x.download = (W.name || 'wave').replace(/[^\w\- ]+/g, '').trim() + '-edit.wav'; document.body.appendChild(x); x.click(); x.remove(); setTimeout(function () { URL.revokeObjectURL(u); }, 5000); return;
      case 'song': var t = prompt('Title for My Songs', (W.name || 'Wave edit').slice(0, 60)); if (!t) return;
        var fd = new FormData(); fd.append('title', t.trim().slice(0, 80)); fd.append('lyrics', ''); fd.append('bpm', ES.S.bpm); fd.append('key', ES.S.key); fd.append('patterns', '{}'); fd.append('studio', JSON.stringify({ v: 2, wave: true }));
        var sr = b.length * b.numberOfChannels * 2 > 44 * 1048576 ? 22050 : b.sampleRate; fd.append('audio', ES.wavEncode(b, sr), 'wave.wav'); say('Uploading…');
        return ES.api('POST', '/api/ss/save', fd).then(function (j) { say('Saved "' + j.song.title + '" to My Songs as MP3.'); }).catch(function (e) { say(e.message); });
    }
  }
  host.addEventListener('click', function (e) { var b = e.target.closest('[data-w]'); if (b) act(b.getAttribute('data-w')); });
  // sources
  var songs = null;
  function fillPick() {
    var o = '<option value="mix">Current mix (render the arrangement)</option>';
    (window.ESREC ? ESREC.list() : []).forEach(function (s) { o += '<option value="stem:' + s.id + '">Stem: ' + ES.esc(s.name) + '</option>'; });
    (songs || []).forEach(function (s) { if (s.url) o += '<option value="song:' + s.id + '">My Song: ' + ES.esc(s.title) + '</option>'; });
    var v = $('#es-wv-pick').value; $('#es-wv-pick').innerHTML = o; if (v) $('#es-wv-pick').value = v;
  }
  $('#es-wv-pick').addEventListener('focus', function () { fillPick(); if (!songs) ES.api('GET', '/api/ss/songs').then(function (j) { songs = j.songs || []; fillPick(); }).catch(function () { songs = []; }); });
  function open(o) { stopPlay(); W.buf = o.buf; W.name = o.name; W.sel = [0, 0]; W.cur = 0; W.view = [0, o.buf.length]; W.undo = []; W.redo = []; W.pending = null; $('#es-wv-pend').hidden = true; draw(); say('Loaded "' + o.name + '" — ' + o.buf.duration.toFixed(1) + 's. Your source stays untouched.'); if (window.innerWidth < 1100) ES.setMTab('wave'); else host.scrollIntoView({ block: 'start', behavior: 'smooth' }); }
  $('#es-wv-load').addEventListener('click', function () {
    var v = $('#es-wv-pick').value || 'mix', btn = this; ES.busy(btn, true);
    var p;
    if (v === 'mix') { say('Rendering the current mix…'); p = ES.unlock().catch(function () {}).then(function () { return ES.renderOffline(); }).then(function (b) { return { name: (ES.S.title || 'Mix'), buf: b }; }); }
    else if (v.indexOf('stem:') === 0) { var s = ESREC.list().filter(function (x) { return x.id === v.slice(5); })[0]; p = s ? Promise.resolve({ name: s.name, buf: s.buf }) : Promise.reject(new Error('Stem not found')); }
    else { var id = v.slice(5), sg = (songs || []).filter(function (x) { return String(x.id) === id; })[0]; say('Loading song…'); p = fetch(sg.url, { credentials: 'same-origin' }).then(function (r) { if (!r.ok) throw new Error('Could not load the song.'); return r.arrayBuffer(); }).then(function (ab) { return new Promise(function (res, rej) { ctx().decodeAudioData(ab, function (b) { res({ name: sg.title, buf: b }); }, function () { rej(new Error('Could not decode the song.')); }); }); }); }
    p.then(open).catch(function (e) { say(e.message); }).then(function () { ES.busy(btn, false); });
  });
  window.ESWAVE = { open: open };
  function boot(api) { ES = api; fillPick(); setFx('normalize'); draw(); }
  (window.ES_HOOKS = window.ES_HOOKS || []).push({ ready: boot });
  if (window.ES) boot(window.ES);
  window.addEventListener('resize', function () { setTimeout(draw, 150); });
  document.addEventListener('click', function (ev) { var b = ev.target.closest && ev.target.closest('.es-mtabs button[data-m="wave"]'); if (b) setTimeout(draw, 50); });
})();


===== FILE: public/ss/studio.css =====
/* Epistles Studio (/ss) — red / black / gold. Desktop: 3-column console. Phone: tabs, no sideways scroll. */
#es{--r:#e3122c;--r2:#ff3048;--rd:#5a0410;--g:#e8c36a;--g2:#f3d27a;--bg:#060606;--p:#0d0d0e;--p2:#151517;--ln:#2a1a1c;--tx:#efefef;--mu:#9b9b9b;
  color:var(--tx);font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;box-sizing:border-box;padding:10px 10px 96px;
  position:relative;width:min(1560px,calc(100vw - 20px));left:50%;margin-left:calc(-1 * min(780px, 50vw - 10px));max-width:none}
#es *,#es *::before,#es *::after{box-sizing:border-box}
#es button,#es select,#es input,#es textarea{font-family:inherit !important;letter-spacing:0 !important;text-transform:none !important;text-shadow:none !important}
#es input,#es select,#es textarea{background:#000 !important;color:var(--tx) !important;border:1px solid #33201f !important;border-radius:8px !import