<!-- SS · part 107 -->
t('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.chat : [{ who: 'ai', text: 'Describe the song — I\'ll write the lyrics, build every section (intro, verse, chorus, bridge, outro), arrange it and voice it.' }];
    c.innerHTML = msgs.map(function (m) { return '<div class="es-msg' + (m.who === 'me' ? ' me' : '') + '">' + esc(m.text) + '</div>'; }).join(''); c.scrollTop = c.scrollHeight;
  }
  function renderBlocks() {
    var bl = blocks();
    $('#es-blocks').innerHTML = bl.length ? bl.map(function (b, i) { return '<div class="es-block"><b>' + esc(b.label.toUpperCase()) + '</b><textarea data-bi="' + i + '" rows="' + Math.max(1, b.lines.length) + '" aria-label="' + esc(b.label) + ' lyrics">' + esc(b.lines.join('\n')) + '</textarea></div>'; }).join('')
      : '<div class="es-hint">Lyrics appear here as section blocks. You can edit any line.</div>';
    $('#es-suno').value = S.suno || '';
  }
  $('#es-blocks').addEventListener('input', function (e) {
    var t = e.target; if (!t.hasAttribute('data-bi')) return;
    t.style.height = 'auto'; t.style.height = t.scrollHeight + 'px';
    var bl = blocks(); bl[+t.getAttribute('data-bi')].lines = t.value.split('\n');
    S.lyrics = bl.map(function (b) { return '[' + b.label + ']\n' + b.lines.join('\n'); }).join('\n\n'); save();
  });
  $('#es-blocks').addEventListener('change', function () { drawTimeline(); drawCurve(); });
  $('#es-copy-suno').addEventListener('click', function () { var t = $('#es-suno'); t.select(); (navigator.clipboard ? navigator.clipboard.writeText(t.value) : Promise.reject()).then(function () { toast('Copied.'); }, function () { try { document.execCommand('copy'); toast('Copied.'); } catch (e) { toast('Select the text and copy it.'); } }); });
  ['topic', 'mood', 'genre'].forEach(function (k) { var el = $('#es-' + k); el.value = S[k] || ''; el.addEventListener('input', function () { S[k] = el.value; save(); }); });
  var remix = null;
  function applySong(s) {
    stop();
    S.title = s.title; S.lyrics = s.lyrics; S.suno = s.suno_prompt || ''; S.bpm = s.bpm; S.key = parseKey(s.key).name; S.chords = s.chords || [];
    SECS.forEach(function (n) { if (s.sections[n]) S.sections[n] = { bars: s.sections[n].bars, patterns: normPat(s.sections[n].patterns) }; });
    S.ai = clone(S.sections); S.arrangement = s.arrangement.slice(); S.sel = Math.max(0, S.arrangement.indexOf('verse'));
    VOX.clips = {}; seekPos = 0; syncTop(); renderBlocks(); refreshSection(); drawCurve(); if (E) E.T.bpm.value = S.bpm; save();
  }
  function generate(btn, withVoice) {
    var