<!-- SS · part 84 -->
'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.drums || {};
    DRUM_ROWS.forEach(function (r) { for (var i = 0; i < STEPS; i++) o.drums[r][i] = d[r] && d[r][i] ? 1 : 0; });
    ['bass', 'chords', 'lead'].forEach(function (t) { for (var i = 0; i < STEPS; i++) o[t][i] = p[t] && p[t][i] ? p[t][i] : null; });
    return o;
  }
  function starter() {
    var v = emptyPat(); [0, 6, 10].forEach(function (i) { v.drums.kick[i] = 1; }); [4, 12].forEach(function (i) { v.drums.snare[i] = 1; }); for (var i = 0; i < 16; i += 2) v.drums.hat[i] = 1;
    v.bass[0] = 'C2'; v.bass[6] = 'C2'; v.bass[10] = 'Eb2'; v.bass[14] = 'G2'; v.chords[0] = 'Cm'; v.chords[8] = 'Ab';
    var c = clone(v); c.drums.perc[4] = 1; c.drums.perc[12] = 1; c.lead[0] = 'G4'; c.lead[3] = 'Eb4'; c.lead[6] = 'F4'; c.lead[8] = 'G4'; c.lead[12] = 'C5';
    var intro = emptyPat(); intro.chords[0] = 'Cm'; intro.chords[8] = 'Ab'; for (var j = 0; j < 16; j += 4) intro.drums.hat[j] = 1;
    var br = emptyPat(); br.chords[0] = 'Fm'; br.chords[8] = 'G'; br.drums.snare[12] = 1; br.bass[0] = 'F2'; br.bass[8] = 'G2';
    var out = clone(intro); out.chords[0] = 'Ab';