<!-- SS · part 104 -->
-------------------------- sequencer + piano roll
  function refreshSection() {
    PLAN = plan();
    var n = selName(); $('#es-seq-sec').textContent = n.toUpperCase(); $('#es-sec-sel').value = n; $('#es-sec-bars').value = S.sections[n].bars;
    renderSeq(); renderRoll(); drawTimeline(); save();
  }
  $('#es-sec-sel').addEventListener('change', function () { var n = this.value, i = S.arrangement.indexOf(n); if (i < 0) { S.arrangement.push(n); i = S.arrangement.length - 1; toast(cap(n) + ' added to the end of the song.'); } S.sel = i; refreshSection(); });
  function renderSeq() {
    var p = selPat(), h = '<span></span>';
    for (var i = 0; i < 16; i++) h += '<span class="es-num">' + (i + 1) + '</span>';
    DRUM_ROWS.forEach(function (r) {
      h += '<span class="es-rl">' + r.toUpperCase() + '</span>';
      for (var s = 0; s < 16; s++) h += '<button type="button" class="es-cell' + (p.drums[r][s] ? ' on' : '') + (s % 4 === 0 ? ' q' : '') + '" data-r="' + r + '" data-s="' + s + '" aria-label="' + r + ' step ' + (s + 1) + '"></button>';
    });
    $('#es-seqgrid').innerHTML = h;
  }
  $('#es-seqgrid').addEventListener('click', function (e) {
    var c = e.target.closest('.es-cell'); if (!c) return; var r = c.getAttribute('data-r'), s = +c.getAttribute('data-s'), p = selPat();
    p.drums[r][s] = p.drums[r][s] ? 0 : 1; c.classList.toggle('on'); save(); drawTimeline();
    if (p.drums[r][s] && E && !playing && Tone.getContext().state === 'running') E.inst.drums.play(r, undefined, 0.9);
  });
  function rollRows(lane) {
    if (lane === 'chords') { var ch = (S.chords || []).slice(); diatonic(S.key).forEach(function (c) { if (ch.indexOf(c) < 0) ch.push(c); }); selPat().chords.forEach(function (c) { if (c && ch.indexOf(c) < 0) ch.push(c); }); return ch.slice(0, 14); }
    var rows = scaleNotes(S.key, lane === 'bass' ? [1, 2] : [4, 5]);
    selPat()[lane].forEach(function (n) { if (n && rows.indexOf(n) < 0) rows.push(n); });
    rows.sort(function (a, b) { return noteMidi(a) - noteMidi(b); });
    return rows.reverse();
  }
  function renderRoll() {
    var lane = S.rollLane, p = selPat()[lane], rows = rollRows(lane), h = '';
    $$('#es-roll-lane button').forEach(function (b) { b.classList.toggle('is-on', b.getAttribute('data-lane') === lane); });
    $('#es-roll-snd').innerHTML = SOUNDS[lane].map(function (o) { return '<option' + (S.sounds[lane] === o ? ' selected' : '') + '>' + o + '</option>'; }).join('');
    rows.forEach(function (rv) {
      var blk = lane !== 'chords' && /^[A-G](#|b)\d$/.test(rv);
      h += '<span class="es-key' + (blk ? ' blk' : '') + '">' + esc(rv) + '</span>';
      for (var s = 0; s < 16; s++) h += '<button type="button" class="es-cell' + (p[s] === rv ? ' on' : '') + (s % 4 === 0 ? ' q' : '') + '" data-v="' + esc(rv) + '" data-s="' + s + '" aria-label="' + esc(rv) + ' step ' + (s + 1) + '"></button>';
    });
    $('#es-rollgrid').innerHTML = h;
  }
  $('#es-roll-lane').addEventListener('click', function (e) { var b = e.target.closest('button[data-lane]'); if (!b) return; S.rollLane = b.getAttribute('data-lane'); renderRoll(); save(); });
  $('#es-roll-snd').addEventListener('change', function () { S.sounds[S.rollLane] = this.value; setSound(E, S.rollLane, this.value); renderFaders(); save(); });
  $('#es-rollgrid').addEventListener('click', function (e) {
    var c = e.target.closest('.es-cell'); if (!c) return; var lane = S.rollLane, s = +c.getAttribute('data-s'), v = c.getAttribute('data-v'), p = selPat()[lane];
    p[s] = p[s] === v ? null : v; save(); renderRoll(); drawTimeline();
    if (p[s] && E && !playing && Tone.getContext().state === 'running') { try { E.inst[lane].triggerAttackRelease(lane === 'chords' ? chordNotes(v) : v, '8n'); } catch (x) {} }
  });
  $('#es-load-ai').addEventListener('click', function () {
    var n = selName(); if (!S.ai || !S.ai[n]) return toast('Generate a song first — then this restores the AI pattern for ' + cap(n) + '.');
    S.sections[n].patterns = normPat(S.ai[n].patterns); refreshSection(); toast('AI pattern restored for ' + cap(n) + '.');
  });
  $('#es-clear').addEventListener('click', function () { if (!confirm('Clear every lane in ' + cap(selName()) + '?')) return; S.sections[selName()].patterns = emptyPat(); refreshSection(); });
  function regen(track, btn) {
    busy(btn, true);
    api('POST', '/api/ss/pattern', { track: track, section: selName(), key: S.key, bpm: S.bpm, chords: S.chords, genre: S.genre, mood: S.mood, current: selPat()[track] })
      .then(function (j) { var p = selPat(); if (track === 'drums') p.drums = normPat({ drums: j.pattern }).drums; else p[track] = j.pattern; refreshSection(); toast(cap(track) + ' regenerated for ' + cap(selName()) + '.'); })
      .catch(function (e) { toast(e.message, 5000); }).then(function () { busy(btn, false); });
  }
  $$('[data-regen]').forEach(function (b) { b.addEventListener('click', function () { regen(b.getAttribute('data-regen'), b); }); });
  $('#