<!-- SS · part 85 -->
 out.chords[8] = 'Cm';
    return { intro: { bars: 2, patterns: intro }, verse: { bars: 4, patterns: v }, chorus: { bars: 4, patterns: c }, bridge: { bars: 2, patterns: br }, outro: { bars: 2, patterns: out } };
  }
  var S = {
    title: '', lyrics: '', suno: '', bpm: 92, key: 'C minor', chords: [], genre: '', mood: '', swing: 0.1, topic: '',
    sections: starter(), arrangement: ['intro', 'verse', 'chorus', 'verse', 'chorus', 'outro'], ai: null,
    sounds: { drums: '808', bass: 'Sub', chords: 'Pad', lead: 'Saw' },
    vols: { vocals: 0, drums: -4, bass: -6, chords: -14, lead: -12 }, mutes: {}, solos: {},
    master: { on: false, drive: 0, comp: 30, eq: 0, width: 50, verb: 25 },
    voice: { name: 'am_onyx', speed: 1, pitch: 0, emo: 'proud', aggr: 30, drama: 35, vuln: 20, pass: 70 },
    chat: [], sel: 1, rollLane: 'bass',
  };
  var VOX = { clips: {} }; // line text -> {buf, dur, peaks, rms}
  function save() { try { var o = {}; Object.keys(S).forEach(function (k) { o[k] = S[k]; }); localStorage.setItem(LS_KEY, JSON.stringify(o)); } catch (e) {} }
  (function load() {
    try {
      var d = JSON.parse(localStorage.getItem(LS_KEY) || 'null');
      if (d && d.sections) { Object.keys(d).forEach(function (k) { S[k] = d[k]; }); }
      else {
        var c = JSON.parse(localStorage.getItem('ss_state_v1') || 'null'); // classic page work carries over
        if (c && c.patterns) { ['title', 'lyrics', 'suno', 'bpm', 'key', 'chords', 'genre', 'mood', 'swing'].forEach(function (k) { if (c[k] != null) S[k] = c[k]; }); SECS.forEach(function (n) { S.sections[n].patterns = normPat(c.patterns); }); }
      }
    } catch (e) {}
    SECS.forEach(function (n) { if (!S.sections[n]) S.sections[n] = { bars: 4, patterns: emptyPat() }; S.sections[n].patterns = normPat(S.sections[n].patterns); });
    S.arrangement = (S.arrangement || []).filter(function (n) { return SECS.indexOf(n) >= 0; });
    if (!S.arrangement.length) S.arrangement = ['verse', 'chorus'];
    if (S.sel >= S.arrangement.length) S.sel = 0;
    S.mutes = S.mutes || {}; S.solos = S.solos || {};
  })();
  function selName() { return S.arrangement[S.sel] || 'verse'; }
  function selPat() { return S.sections[selName()].patterns; }

  // ------------------------------------------------------------------ timing / arrangement
  function stepSec() { return 60 / S.bpm / 4; }
  function barSec() { return stepSec() * 16; }
  function plan() { var out = [], b = 0; S.arrangement.forEach(function (n, i) { var bars = S.sections[n].bars; out.push({ i: i, name: n, start: b, bars: bars }); b += bars; }); return { items: out, bars: b }; }
  var PLAN = plan();
  function locate(bar) { for (var i = 0; i < PLAN.items.length; i++) { var it = PLAN.items[i]; if (bar >= it.start && bar < it.start + it.bars) return it; } return null; }
  function totalSec() { return PLAN.bars * barSec(); }

  // lyric blocks -> per-section vocal placements
  function blocks() {
    var out = [], cur = null;
    String(S.lyrics || '').split(/\r?\n/).forEach(function (l) {
      var t = l.trim(), m = /^\[([^\]]*)\]$/.exec(t);
      if (m) { cur = { label: m[1], type: secType(m[1]), lines: [] }; out.push(cur); return; }
      if (!t) return;
      if (!cur) { cur = { label: 'Verse', type: 'verse', lines: [] }; out.push(cur); }
      cur.lines.push(t);
    });
    return out;
  }
  function secType(label) { var l = String(label).toLowerCase(); if (/chorus|hook|refrain/.test(l)) return 'chorus'; if (/bridge|break/.test(l)) return 'bridge'; if (/intro/.test(l)) return 'intro'; if (/outro|end/.test(l)) return 'outro'; return 'verse'; }
  function vocalPlan(raw) {
    var bl = blocks(), byType = {}, used = {}, out = [];
    bl.forEach(function (b) { (byType[b.type] = byType[b.type] || []).push(b); });
    PLAN.items.forEach(function (it) {
      var list = byType[it.name]; if (!list || !list.length) return;
      var k = used[it.name] || 0; used[it.name] = k + 1;
      var blk = list[k] || (it.name === 'chorus' ? list[list.length - 1] : null); if (!blk) return;
      var n = blk.lines.length; if (!n) return;
      var per = it.bars / n, secEnd = (it.start + it.bars) * barSec(), prevEnd = 0;
      // grid-place each line on a half-bar; once voiced, pack lines so they never overlap (spill is reported, not clipped)
      var need = raw ? 0 : blk.lines.reduce(function (m, l) { var c = VOX.clips[l]; return m + (c && !c.sung ? c.dur + 0.12 : 0); }, 0), tight = need > it.bars * barSec() * 0.92;
      blk.lines.forEach(function (line, j) {
        var bar = it.start + Math.round(j * per * 2) / 2, t = tight ? it.start * barSec() + 0.05 : bar * barSec(), c = raw ? null : VOX.clips[line];
        if (c && c.sung) t = bar * barSec(); else if (j && !raw) t = Math.max(t, prevEnd + 0.12);
        if (c) prevEnd = t + c.dur; else prevEnd = t;
        out.push({ text: line, bar: bar, t: t, slot: Math.max(0.3, (j < n - 1 ? (it.start + Math.round((j + 1) * per * 2) / 2) * barSec() : secEnd) - t), sec: 