<!-- SS · part 79 -->
dyn]').forEach(function (r) { r.addEventListener('input', function () { S.voice[r.getAttribute('data-dyn')] = +r.value; syncVoiceUI(); applyVocalFx(E); save(); }); });
  $('#es-emo').addEventListener('click', function (e) { var b = e.target.closest('[data-emo]'); if (!b) return; var k = b.getAttribute('data-emo'); Object.assign(S.voice, EMO[k], { emo: k }); syncVoiceUI(); applyVocalFx(E); save(); toast(cap(k) + ': effects apply now; speed and pitch on the next render.'); });
  function drawCurve() {
    var cv = $('#es-curve'), w = cv.clientWidth, h = cv.clientHeight; if (!w) return; var dpr = window.devicePixelRatio || 1; 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 = '#1d1d1d'; for (var i = 1; i < 3; i++) { g.beginPath(); g.moveTo(0, h * i / 3); g.lineTo(w, h * i / 3); g.stroke(); }
    g.fillStyle = '#555'; g.font = '9px sans-serif'; g.fillText('HIGH', 3, 10); g.fillText('LOW', 3, h - 3);
    var tot = totalSec() || 1, pts = [];
    vocalPlan().forEach(function (v) { var c = VOX.clips[v.text]; if (!c) return; c.rms.forEach(function (r, j) { pts.push([(v.t + j / c.rms.length * c.dur) / tot * w, r]); }); });
    if (!pts.length) { g.fillStyle = '#777'; g.fillText('Render vocals to see the expression curve', 34, h / 2 + 3); return; }
    var bias = S.voice.pitch / 12; g.strokeStyle = '#ff3048'; g.lineWidth = 1.6; g.beginPath();
    pts.forEach(function (p, i) { var y = h - 6 - Math.max(0, Math.min(1, p[1] * 3.2 + 0.15 + bias * 0.3)) * (h - 12); if (i) g.lineTo(p[0], y); else g.moveTo(p[0], y); }); g.stroke();
  }
  function b64ToBuf(b64) { var bin = atob(b64), u = new Uint8Array(bin.length); for (var i = 0; i < bin.length; i++) u[i] = bin.charCodeAt(i); return u.buffer; }
  function analyse(buf) { var d = buf.getChannelData(0), N = 160, peaks = [], rms = [], step = Math.max(1, Math.floor(d.length / N)); for (var i = 0; i < N; i++) { var mx = 0, s = 0; for (var j = i * step; j < (i + 1) * step && j < d.length; j++) { var a = Math.abs(d[j]); if (a > mx) mx = a; s += d[j] * d[j]; } peaks.push(Math.min(1, mx * 1.4)); rms.push(Math.sqrt(s / step)); } return { peaks: peaks, rms: rms }; }
  function renderVocals(btn) {
    var lines = uniqueLines(); if (!lines.length) { toast('No lyrics placed in the song yet — generate or write lyrics first.'); return Promise.resolve(false); }
    if (lines.length > 40) { lines = lines.slice(0, 40); toast('Voicing the first 40 unique lines.'); }
    var st = $('#es-vstatus'), badge = $('#es-av-state'); busy(btn, true); badge.textContent = 'RENDERING'; badge.classList.add('busy');
    st.textContent = 'Rendering ' + lines.length + ' unique lines with ' + S.voice.name + '… about ' + Math.round(lines.length * 5 + 8) + 's.';
    return unlock().catch(function () {}).then(function () {
      return api('POST', '/api/ss/voice', { lines: lines, voice: S.voice.name, speed: S.voice.speed, pitch: S.voice.pitch });
    }).then(function (start) {
      return new Promise(function (resolve, reject) {
        var tries = 0;
        (function poll() {
          if (++tries > 250) return reject(new Error('Voice took too long. Try fewer lines.'));
          api('GET', '/api/ss/voice/' + encodeURIComponent(start.job)).then(function (j) { if (j.status === 'done') return resolve(j); st.textContent = 'Voicing line ' + Math.min(j.total, j.done + 1) + ' of ' + j.total + '…'; setTimeout(poll, 2000); }, reject);
        })();
      });
    }).then(function (j) {
      engine(); var ctx = Tone.getContext().rawContext, fresh = {};
      return Promise.all((j.clips || []).map(function (c) {
        return new Promise(function (res) { ctx.decodeAudioData(b64ToBuf(c.b64), function (buf) { var a = analyse(buf); fresh[lines[c.i]] = { buf: new Tone.ToneAudioBuffer(buf), dur: buf.duration, peaks: a.peaks, rms: a.rms }; res(); }, function () { res(); }); });
      })).then(function () {
        VOX.clips = fresh; var n = Object.keys(fresh).length, over = (VOX.clips = fresh, vocalPlan().filter(function (v) { return v.spill > 0.3; }).length);
        st.textContent = n + ' of ' + lines.length + ' lines voiced and placed on the timeline.' + (over ? ' ' + over + ' line(s) spill past their section — raise the voice speed or the section bars.' : '');
        drawTimeline(); drawCurve(); return true;
      });
    }).catch(function (e) { st.textContent = e.message; return false; })
      .then(function (ok) { busy(btn, false); badge.textContent = 'READY'; badge.classList.remove('busy'); return ok; });
  }
  $('#es-render').addEventListener('click', function () { renderVocals(this); });

  // ------------------------------------------------------------------ writer
  function chatAdd(who, text) { S.chat.push({ who: who, text: text }); S.chat = S.chat.slice(-12); renderChat(); save(); }
  function renderChat() {
    var c = $('#es-chat'), msgs = S.chat.length ? S.cha