<!-- SS · part 70 -->
 '#ff5a3c', bass: '#f2b632', chords: '#b46cff', lead: '#2fd6e0' };
  var DRUM_ROWS = ['kick', 'snare', 'hat', 'perc'];
  var SOUNDS = { drums: ['808', 'Lo-fi', 'Chip'], bass: ['Sub', 'Saw', 'FM'], chords: ['Pad', 'Keys', 'Pluck'], lead: ['Saw', 'Square', 'Bell'] };

  // ------------------------------------------------------------------ utils
  function toast(msg, ms) { var t = $('#es-toast'); t.textContent = msg; t.hidden = false; clearTimeout(toast._t); toast._t = setTimeout(function () { t.hidden = true; }, ms || 3400); }
  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]; }); }
  function busy(btn, on) { if (!btn) return; btn.disabled = !!on; btn.classList.toggle('es-busy', !!on); }
  function api(method, url, body) {
    var opt = { method: method, credentials: 'same-origin', headers: { 'x-csrf-token': CSRF, 'Accept': 'application/json' } };
    if (body instanceof FormData) opt.body = body;
    else if (body !== undefined) { opt.headers['Content-Type'] = 'application/json'; opt.body = JSON.stringify(body); }
    return fetch(url, opt).then(function (r) {
      return r.json().catch(function () { return {}; }).then(function (j) {
        if (!r.ok) { var e = new Error(j.error || (r.status === 429 ? 'Slow down — try again shortly.' : r.status === 401 ? 'Please log in again.' : r.status === 413 ? 'File too large.' : 'Request failed (' + r.status + ')')); e.status = r.status; throw e; }
        return j;
      });
    });
  }
  function clone(o) { return JSON.parse(JSON.stringify(o)); }
  function mmss(s) { s = Math.max(0, Math.floor(s)); return Math.floor(s / 60) + ':' + ('0' + (s % 60)).slice(-2); }
  function fmtDate(t) { try { return new Date(t * 1000).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); } catch (e) { return ''; } }
  function cap(s) { return s.charAt(0).toUpperCase() + s.slice(1); }

  // ------------------------------------------------------------------ theory
  var NOTES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
  var FLAT = { Db: 'C#', Eb: 'D#', Gb: 'F#', Ab: 'G#', Bb: 'A#', Cb: 'B', Fb: 'E' };
  var NICE = { 'C#': 'Db', 'D#': 'Eb', 'F#': 'F#', 'G#': 'Ab', 'A#': 'Bb' };
  function pc(n) { n = FLAT[n] || n; return NOTES.indexOf(n); }
  function nm(i) { var n = NOTES[((i % 12) + 12) % 12]; return NICE[n] || n; }
  var KEYS = []; ['C', 'Db', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B'].forEach(function (k) { KEYS.push(k + ' minor'); KEYS.push(k + ' major'); });
  function parseKey(k) {
    var m = /^\s*([A-G])([#b]?)\s*(maj(?:or)?|m(?:in(?:or)?)?)?/i.exec(String(k || ''));
    if (!m) return { root: 0, minor: true, name: 'C minor' };
    var root = pc(m[1].toUpperCase() + m[2]); if (root < 0) root = 0;
    var q = (m[3] || '').toLowerCase(); var minor = q ? q.charAt(0) === 'm' && q.indexOf('maj') !== 0 : /minor/i.test(k);
    return { root: root, minor: minor, name: nm(root) + (minor ? ' minor' : ' major') };
  }
  function scale(k) { var K = parseKey(k), iv = K.minor ? [0, 2, 3, 5, 7, 8, 10] : [0, 2, 4, 5, 7, 9, 11]; return iv.map(function (x) { return (K.root + x) % 12; }); }
  function scaleNotes(k, octs) { var sc = scale(k), K = parseKey(k), out = []; octs.forEach(function (o) { sc.forEach(function (p) { out.push(nm(p) + (o + (p < K.root ? 1 : 0))); }); }); return out; }
  function diatonic(k) { var sc = scale(k), out = []; for (var i = 0; i < 7; i++) { var a = sc[i], b = sc[(i + 2) % 7], c = sc[(i + 4) % 7], t3 = (b - a + 12) % 12, t5 = (c - a + 12) % 12; out.push(nm(a) + (t3 === 3 ? (t5 === 6 ? 'dim' : 'm') : '')); } return out; }
  var QUAL = { '': [0, 4, 7], m: [0, 3, 7], min: [0, 3, 7], maj: [0, 4, 7], maj7: [0, 4, 7, 11], m7: [0, 3, 7, 10], '7': [0, 4, 7, 10], dim: [0, 3, 6], aug: [0, 4, 8], sus2: [0, 2, 7], sus4: [0, 5, 7], add9: [0, 4, 7, 14], '6': [0, 4, 7, 9], m6: [0, 3, 7, 9], '9': [0, 4, 7, 10, 14], m9: [0, 3, 7, 10, 14] };
  function noteMidi(n) { var m = /^([A-G][#b]?)(-?\d)$/.exec(n || ''); if (!m) return null; return 12 * (+m[2] + 1) + pc(m[1]); }
  function midiName(m) { return nm(m) + (Math.floor(m / 12) - 1); }
  function chordMidis(sym) { var m = /^([A-G])([#b]?)(.*)$/.exec(String(sym || '')); if (!m) return null; var r = pc(m[1] + m[2]); var iv = QUAL[m[3]]; if (r < 0 || !iv) return null; var base = 48 + r; if (base > 55) base -= 12; return iv.map(function (x) { return base + x + 12; }); }
  function chordNotes(sym) { var ms = chordMidis(sym); return ms ? ms.map(midiName) : null; }

  // ------------------------------------------------------------------ state
  function z(v) { var a = []; for (var i = 0; i < STEPS; i++) a.push(v); return a; }
  function emptyPat() { return { drums: { kick: z(0), snare: z(0), hat: z(0), perc: z(0) }, bass: z(null), chords: z(null), lead: z(null) }; }
  function normPat(p) {
    p = p || {}; var o = emptyPat(), d = p.dr