<!-- SS · part 37 -->
x(ES ? ES.songEnd() : 60, 30);
      return '<li class="es-stem' + (s.mute ? ' is-muted' : '') + '" data-id="' + s.id + '"><div class="es-stem-h"><input type="text" data-k="name" value="' + esc(s.name) + '" maxlength="40" aria-label="Stem name"><small>' + fmt(D, 1) + 's</small></div>' +
        '<div class="es-stem-g">' + row(s, 'start', 'Start', 0, Math.ceil(tot), 0.05, 's') + row(s, 'vol', 'Volume', -30, 12, 0.5, ' dB') + row(s, 'trimIn', 'Trim start', 0, Math.max(0, D - 0.1), 0.01, 's') + row(s, 'trimOut', 'Trim end', 0, Math.max(0, D - 0.1), 0.01, 's') +
        row(s, 'fadeIn', 'Fade in', 0, 10, 0.05, 's') + row(s, 'fadeOut', 'Fade out', 0, 10, 0.05, 's') + '</div>' +
        '<div class="es-stem-row"><select data-k="preset" aria-label="FX preset">' + ['dry', 'polish', 'radio', 'room', 'echo'].map(function (p) { return '<option value="' + p + '"' + (s.fx.preset === p ? ' selected' : '') + '>FX: ' + { dry: 'Dry', polish: 'Vocal polish', radio: 'Radio comp', room: 'Room', echo: 'Echo' }[p] + '</option>'; }).join('') + '</select>' +
        '<label><input type="checkbox" data-k="level"' + (s.fx.level ? ' checked' : '') + '> Auto-level</label></div>' +
        '<div class="es-stem-g">' + ['verb', 'echo', 'comp'].map(function (k) { return '<label class="es-slider"><span>' + { verb: 'Reverb', echo: 'Echo', comp: 'Compress' }[k] + ' <b>' + s.fx[k] + '%</b></span><input type="range" data-fx="' + k + '" min="0" max="100" value="' + s.fx[k] + '"></label>'; }).join('') + '</div>' +
        '<div class="es-stem-row"><button type="button" class="es-btn es-btn-sm" data-a="here">Move to playhead</button><button type="button" class="es-btn es-btn-sm" data-a="nl">◀ beat</button><button type="button" class="es-btn es-btn-sm" data-a="nr">beat ▶</button>' +
        '<button type="button" class="es-btn es-btn-sm" data-a="mute">' + (s.mute ? 'Unmute' : 'Mute') + '</button><button type="button" class="es-btn es-btn-sm" data-a="solo">' + (s.solo ? 'Unsolo' : 'Solo') + '</button><button type="button" class="es-btn es-btn-sm" data-a="eq">EQ</button><button type="button" class="es-btn es-btn-sm" data-a="wave">Wave Studio</button><button type="button" class="es-btn es-btn-sm" data-a="dl">Download</button><button type="button" class="es-btn es-btn-sm" data-a="del">Delete</button></div></li>';
    }).join('');
  }
  function byId(id) { return STEMS.filter(function (s) { return s.id === id; })[0]; }
  var liveT = null;
  $('#es-stems').addEventListener('input', function (e) {
    var li = e.target.closest('.es-stem'); if (!li) return; var s = byId(li.getAttribute('data-id')), k = e.target.getAttribute('data-k'), fx = e.target.getAttribute('data-fx');
    if (k === 'name') { s.name = e.target.value.slice(0, 40) || 'Stem'; persist(s); if (window.ESEQ) ESEQ.refreshTargets(); ES.drawTimeline(); return; }
    var b = e.target.parentNode.querySelector('b');
    if (fx) { s.fx[fx] = +e.target.value; s.fx.preset = ''; if (b) b.textContent = s.fx[fx] + '%'; }
    else if (k && e.target.type === 'range') { s[k] = +e.target.value; if (b) b.textContent = fmt(s[k], +e.target.step < 1 ? 2 : 0) + (k === 'vol' ? ' dB' : 's'); }
    persist(s); ES.drawTimeline(); clearTimeout(liveT); liveT = setTimeout(function () { if (ES.isPlaying()) ES.play(ES.curPos()); }, 250);
  });
  $('#es-stems').addEventListener('change', function (e) {
    var li = e.target.closest('.es-stem'); if (!li) return; var s = byId(li.getAttribute('data-id')), k = e.target.getAttribute('data-k');
    if (k === 'preset') { var p = FXP[e.target.value]; s.fx = Object.assign({}, s.fx, p, { preset: e.target.value }); renderStems(); }
    else if (k === 'level') s.fx.level = e.target.checked; else return;
    persist(s); changed();
  });
  $('#es-stems').addEventListener('click', function (e) {
    var b = e.target.closest('[data-a]'); if (!b) return; var li = b.closest('.es-stem'), s = byId(li.getAttribute('data-id')), a = b.getAttribute('data-a'), beat = 60 / (ES.S.bpm || 90);
    if (a === 'here') s.start = Math.round(ES.curPos() * 100) / 100; else if (a === 'nl') s.start = Math.max(0, s.start - beat); else if (a === 'nr') s.start += beat;
    else if (a === 'mute') s.mute = !s.mute; else if (a === 'solo') s.solo = !s.solo;
    else if (a === 'eq') { if (window.innerWidth < 1100) ES.setMTab('mix'); var t = document.getElementById('es-eq-t'); if (t) { t.value = 'stem:' + s.id; t.dispatchEvent(new Event('change')); t.scrollIntoView({ block: 'center' }); } return; }
    else if (a === 'wave') { if (window.ESWAVE) ESWAVE.open({ name: s.name, buf: s.buf }); return; }
    else if (a === 'dl') { var u = URL.createObjectURL(s.blob), x = document.createElement('a'); x.href = u; x.download = s.name.replace(/[^\w\- ]+/g, '') + (/wav/.test(s.blob.type) ? '.wav' : /mp4|m4a|aac/.test(s.blob.type) ? '.m4a' : /mpeg|mp3/.test(s.blob.type) ? '.mp3' : /ogg/.test(s.blob.type) ? '.ogg' : '.webm'); document.body.appendChild(x); x.click(); x.remove(); setTimeout(function () { URL.