// Sequences: lanes (parallel preset chains); advance is always by audio beats or simulated BPM.
// Debug: in the browser console run setSequenceDebug(true) — session only, not persisted.

/** @type {'beat'|'downbeat'} */
let sequenceSwitchWaitFor = 'beat';

let sequenceDebugEnabled = false;
let sequenceSwitchSaveInFlight = false;

async function loadSequenceSwitchWaitForFromServer() {
  try {
    const res = await fetch('/settings', {
      cache: 'no-store',
      headers: { Accept: 'application/json' },
    });
    if (!res.ok) return;
    const data = await res.json();
    const raw = data && data.sequence_switch_wait;
    if (raw === 'downbeat' || raw === 'beat') {
      sequenceSwitchWaitFor = raw;
    } else if (raw === 'phrase') {
      sequenceSwitchWaitFor = 'beat';
    }
  } catch {
    /* keep default */
  }
}

async function persistSequenceSwitchWaitFor() {
  sequenceSwitchSaveInFlight = true;
  try {
    const res = await fetch('/settings', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
      body: JSON.stringify({ sequence_switch_wait: sequenceSwitchWaitFor }),
    });
    if (!res.ok) {
      console.warn('[sequence] could not save switch wait to server', res.status);
    }
  } catch (e) {
    console.warn('[sequence] could not save switch wait to server', e);
  } finally {
    sequenceSwitchSaveInFlight = false;
  }
}

function getSequenceSwitchWaitFor() {
  return sequenceSwitchWaitFor === 'downbeat' ? 'downbeat' : 'beat';
}

async function setSequenceSwitchWaitFor(waitFor) {
  sequenceSwitchWaitFor = waitFor === 'downbeat' ? 'downbeat' : 'beat';
  updateSequenceSwitchToggleUI();
  await persistSequenceSwitchWaitFor();
}

function updateSequenceSwitchToggleUI() {
  const mode = getSequenceSwitchWaitFor();
  const ariaLabels = {
    beat: 'Switch sequence on beat',
    downbeat: 'Switch sequence on downbeat',
  };
  document.querySelectorAll('.seq-switch-toggle').forEach((btn) => {
    btn.setAttribute('aria-pressed', mode === 'beat' ? 'false' : 'true');
    btn.setAttribute('aria-label', ariaLabels[mode] || ariaLabels.beat);
    btn.classList.toggle('seq-switch-toggle--downbeat', mode === 'downbeat');
  });
  document.querySelectorAll('.seq-switch-toggle-wrap').forEach((wrap) => {
    wrap.classList.toggle('nav-slide-toggle-wrap--downbeat', mode === 'downbeat');
  });
}

async function initSequenceSwitchToggle() {
  await loadSequenceSwitchWaitForFromServer();
  updateSequenceSwitchToggleUI();
  document.querySelectorAll('.seq-switch-toggle').forEach((btn) => {
    btn.addEventListener('click', () => {
      void setSequenceSwitchWaitFor(getSequenceSwitchWaitFor() === 'beat' ? 'downbeat' : 'beat');
    });
  });
}

/** Sync toggle when settings changed elsewhere (e.g. another tab via audio status poll). */
function applySequenceSwitchWaitFromServer(raw) {
  if (sequenceSwitchSaveInFlight) return;
  let mode = 'beat';
  if (raw === 'downbeat') mode = 'downbeat';
  else if (raw !== 'beat' && raw !== 'phrase') return;
  if (mode === getSequenceSwitchWaitFor()) return;
  sequenceSwitchWaitFor = mode;
  updateSequenceSwitchToggleUI();
}

function seqDebugEnabled() {
  return sequenceDebugEnabled;
}

/** @type {ReturnType<typeof setInterval> | null} */
let sequenceBpmPollTimer = null;

function stopSequenceEditorBpmPoll() {
  if (sequenceBpmPollTimer) {
    clearInterval(sequenceBpmPollTimer);
    sequenceBpmPollTimer = null;
  }
}

async function refreshSequenceEditorBpmDisplay() {
  const live = document.getElementById('sequence-editor-bpm-live');
  const panel = document.getElementById('sequence-editor-beats-panel');
  if (!live || !panel) return;
  try {
    const res = await fetch('/api/audio/status', { headers: { Accept: 'application/json' } });
    const j = res.ok ? await res.json() : {};
    const st = j && j.status ? j.status : {};
    const running = !!st.running;
    const bpmRaw = st.bpm;
    const bpm =
      typeof bpmRaw === 'number' && Number.isFinite(bpmRaw)
        ? bpmRaw
        : typeof bpmRaw === 'string' && bpmRaw.trim()
          ? parseFloat(bpmRaw)
          : NaN;
    if (!running) {
      live.textContent =
        'Audio detector is stopped — the sequence uses simulated beats at the BPM you set above.';
      return;
    }
    if (!Number.isFinite(bpm) || bpm <= 0) {
      live.textContent = 'Audio detector running; BPM will appear after a few beats.';
      return;
    }
    const msPer = Math.round(60000 / bpm);
    const rounded = Math.round(bpm * 10) / 10;
    live.textContent = `Current estimate: ${rounded} BPM (~${msPer} ms per beat).`;
  } catch (_) {
    live.textContent = 'Could not read audio status.';
  }
}

/** @param {boolean} [clearSequenceTileSelection] When false, leaves the active highlight on sequence tiles (used when restarting playback so the click handler’s selection is not cleared). */
async function stopZoneSequencePlayback(clearSequenceTileSelection = true) {
  // Clear selection **before** awaiting fetch so overlapping stop() calls cannot finish out of
  // order and strip .active from tiles after a later render or click (intermittent dead UI).
  if (clearSequenceTileSelection) {
    document.querySelectorAll('.sequence-tile-main.active').forEach((btn) => btn.classList.remove('active'));
  }
  try {
    const res = await fetch('/sequences/stop', {
      method: 'POST',
      headers: { Accept: 'application/json' },
      credentials: 'same-origin',
    });
    if (!res.ok) {
      console.warn('Sequence stop failed:', res.status);
    }
  } catch (e) {
    console.warn('Sequence stop:', e);
  }
}

function normalizeSequenceLanes(doc) {
  let lanesRaw = Array.isArray(doc && doc.lanes) ? doc.lanes : [];
  let lanes = lanesRaw.filter((l) => Array.isArray(l));
  const hasAnyLaneSteps = lanes.some((l) => l.length > 0);
  if ((!lanes.length || !hasAnyLaneSteps) && Array.isArray(doc && doc.steps) && doc.steps.length) {
    lanes = [doc.steps.slice()];
  }
  if (!lanes.length) lanes = [[]];
  return lanes.map((lane) =>
    lane
      .filter((s) => s && typeof s === 'object')
      .map((s) => ({
        preset_id: s.preset_id != null ? String(s.preset_id) : String(s.presetId || ''),
        beats: Math.max(1, parseInt(String(s.beats != null ? s.beats : 1), 10) || 1),
        group_ids: Array.isArray(s.group_ids)
          ? s.group_ids.map((x) => String(x).trim()).filter(Boolean)
          : [],
      })),
  );
}

/**
 * Log each preset in the sequence with its step beat count (beats per step before advancing).
 * @param {string} sequenceId
 * @param {Record<string, unknown>} sequenceDoc
 * @param {Record<string, unknown>} presetsMap
 */
function logSequenceSelectionPresets(sequenceId, sequenceDoc, presetsMap) {
  if (!sequenceDoc || typeof sequenceDoc !== 'object') return;
  const lanes = normalizeSequenceLanes(sequenceDoc);
  const nameFor = (pid) => {
    const p = presetsMap && presetsMap[pid];
    if (p && typeof p === 'object' && p.name != null && String(p.name).trim()) {
      return String(p.name).trim();
    }
    return pid || '(unknown preset)';
  };
  const nm = String(sequenceDoc.name || '').trim() || sequenceId;
  const multi =
    lanes.filter((lane) => lane.some((s) => s && s.preset_id)).length > 1;
  let headerLine = `Sequence "${nm}" (${sequenceId}) — advance: beats`;
  if (multi) {
    headerLine +=
      ' — header/audio beat readout follows lane 1 only (other lanes run in parallel)';
  }
  console.log(headerLine);
  lanes.forEach((lane, li) => {
    const steps = lane.filter((s) => s && s.preset_id);
    if (!steps.length) return;
    if (multi) console.log(`Lane ${li + 1}`);
    steps.forEach((step) => {
      const b = step.beats;
      console.log(`  ${nameFor(step.preset_id)}: ${b} beat${b === 1 ? '' : 's'}`);
    });
  });
}

function groupIdsForLaneStep(sequenceDoc, step, laneIndex, numLanes) {
  const lgs = Array.isArray(sequenceDoc.lanes_group_ids) ? sequenceDoc.lanes_group_ids : [];
  if (laneIndex < lgs.length) {
    const forLane = lgs[laneIndex];
    if (Array.isArray(forLane)) {
      return forLane.map((x) => String(x).trim()).filter(Boolean);
    }
  }
  if (numLanes > 1 && laneIndex >= lgs.length) {
    return [];
  }
  const shared = Array.isArray(sequenceDoc.group_ids)
    ? sequenceDoc.group_ids.map((x) => String(x).trim()).filter((x) => x.length > 0)
    : [];
  if (shared.length) return shared;
  if (numLanes === 1 && step && Array.isArray(step.group_ids) && step.group_ids.length) {
    return step.group_ids.map((x) => String(x).trim()).filter(Boolean);
  }
  return [];
}

function buildLaneGroupIdsForEditor(doc, laneIndex, numLanes) {
  const raw = Array.isArray(doc && doc.lanes_group_ids) ? doc.lanes_group_ids : [];
  const shared = Array.isArray(doc && doc.group_ids) ? doc.group_ids.map(String) : [];
  if (laneIndex < raw.length) {
    const row = raw[laneIndex];
    if (Array.isArray(row)) {
      return row.map(String).filter(Boolean);
    }
  }
  if (numLanes > 1 && laneIndex >= raw.length) {
    return [];
  }
  if (numLanes === 1) {
    const lanes = normalizeSequenceLanes(doc);
    const first = lanes[0] && lanes[0][0];
    const sg =
      first && Array.isArray(first.group_ids) ? first.group_ids.map(String).filter(Boolean) : [];
    return sg.length ? sg : shared.slice();
  }
  return shared.slice();
}

function renderLaneGroupCheckboxes(groupsMap, selectedIds) {
  const wrap = document.createElement('div');
  wrap.className = 'sequence-lane-groups-wrap';
  wrap.style.cssText = 'margin-bottom:0.6rem;';
  const hint = document.createElement('div');
  hint.className = 'muted-text';
  hint.style.fontSize = '0.85em';
  hint.style.marginBottom = '0.35rem';
  hint.textContent = 'Groups for this lane (none = whole zone)';
  wrap.appendChild(hint);
  const row = document.createElement('div');
  row.className = 'sequence-lane-groups';
  row.style.cssText = 'display:flex;flex-wrap:wrap;gap:0.5rem;align-items:center;';
  const sel = new Set((selectedIds || []).map((x) => String(x)));
  Object.keys(groupsMap)
    .sort((a, b) => a.localeCompare(b))
    .forEach((gid) => {
      const g = groupsMap[gid];
      const gn = g && g.name ? String(g.name) : gid;
      const id = `seq-lg-${gid}-${Math.random().toString(36).slice(2)}`;
      const lbl = document.createElement('label');
      lbl.style.cssText = 'display:inline-flex;align-items:center;gap:0.25rem;font-size:0.9em;';
      const cb = document.createElement('input');
      cb.type = 'checkbox';
      cb.className = 'sequence-lane-group';
      cb.value = gid;
      cb.id = id;
      if (sel.has(String(gid))) cb.checked = true;
      const sp = document.createElement('span');
      sp.textContent = `${gn} (${gid})`;
      lbl.appendChild(cb);
      lbl.appendChild(sp);
      row.appendChild(lbl);
    });
  const editG = document.createElement('button');
  editG.type = 'button';
  editG.className = 'btn btn-secondary btn-small';
  editG.textContent = 'Edit groups';
  editG.title = 'Open Device groups';
  editG.addEventListener('click', async () => {
    if (typeof window.openDeviceGroupsModal === 'function') {
      await window.openDeviceGroupsModal();
      return;
    }
    const b = document.getElementById('groups-btn');
    if (b) b.click();
    else alert('Groups could not be opened.');
  });
  row.appendChild(editG);
  wrap.appendChild(row);
  return wrap;
}

function splitDeviceNamesForLane(allNames, laneIndex, numLanes) {
  const names = Array.isArray(allNames) ? allNames.filter((n) => n && String(n).trim()) : [];
  if (numLanes <= 1) return names;
  if (names.length >= numLanes) {
    const n = names[laneIndex];
    return n ? [n] : [];
  }
  return names;
}

function presetsSectionElForZone(zoneId) {
  if (zoneId != null && String(zoneId).length) {
    const el = document.querySelector(`.presets-section[data-zone-id="${String(zoneId)}"]`);
    if (el) return el;
  }
  return document.querySelector('.presets-section[data-zone-id]');
}

/** Match preset tiles: prefer DOM device list, then zone JSON (same as parseTabDeviceNames + computeZoneTargets). */
async function resolveSequenceSendDeviceNames(zoneId, zoneDoc, groupIds) {
  const gids = Array.isArray(groupIds) ? groupIds.map((x) => String(x).trim()).filter((x) => x.length > 0) : [];
  if (!gids.length) {
    const section = presetsSectionElForZone(zoneId);
    if (typeof window.parseTabDeviceNames === 'function' && section) {
      const fromDom = window.parseTabDeviceNames(section);
      if (Array.isArray(fromDom) && fromDom.length) return fromDom;
    }
  }
  if (window.zonesManager && typeof window.zonesManager.resolveSequenceStepDeviceNames === 'function' && zoneDoc) {
    return await window.zonesManager.resolveSequenceStepDeviceNames(zoneDoc, gids);
  }
  return [];
}

/** Start sequence playback on the server (ESP-NOW / TCP delivery from backend). */
async function requestBackendSequencePlay(sequenceId, zoneId, sequenceDoc) {
  // Do not call stop here: server start() already stops any prior run. A fire-and-forget
  // client stop can reorder after play and clear the new session (same tile re-click bug).
  let bodyBpm;
  if (sequenceDoc && typeof sequenceDoc === 'object' && sequenceDoc.simulated_bpm != null) {
    const n = parseInt(String(sequenceDoc.simulated_bpm), 10);
    if (Number.isFinite(n)) bodyBpm = Math.min(300, Math.max(30, n));
  }
  const body = { zone_id: String(zoneId) };
  if (bodyBpm != null) body.simulated_bpm = bodyBpm;
  const res = await fetch(`/sequences/${encodeURIComponent(sequenceId)}/play`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
    credentials: 'same-origin',
    body: JSON.stringify(body),
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error((err && err.error) || res.statusText);
  }
  console.log(Number(sequenceId));
}

async function fetchSequencesMap() {
  try {
    const res = await fetch('/sequences', { cache: 'no-store', headers: { Accept: 'application/json' } });
    if (!res.ok) return {};
    const data = await res.json();
    return data && typeof data === 'object' ? data : {};
  } catch (e) {
    console.error('fetchSequencesMap:', e);
    return {};
  }
}

async function fetchGroupsMapSeq() {
  try {
    const res = await fetch('/groups', {
      headers: { Accept: 'application/json' },
      credentials: 'same-origin',
    });
    if (!res.ok) return {};
    const data = await res.json();
    return data && typeof data === 'object' ? data : {};
  } catch (e) {
    return {};
  }
}

function createSequenceTileRow(sequenceId, sequenceDoc, zoneId, zoneDoc, allPresets, uiMode) {
  const row = document.createElement('div');
  const canDrag = false;
  row.className = `preset-tile-row preset-tile-row--${uiMode} sequence-tile-row${canDrag ? ' draggable-preset' : ''}`;
  row.dataset.sequenceId = sequenceId;

  const button = document.createElement('button');
  button.type = 'button';
  button.className = 'pattern-button preset-tile-main sequence-tile-main';
  button.title = sequenceDoc.name || `Sequence ${sequenceId}`;

  const badge = document.createElement('span');
  badge.textContent = 'SEQ';
  badge.className = 'sequence-tile-badge';
  badge.style.cssText =
    'position:absolute;left:4px;top:4px;font-size:10px;font-weight:700;color:#fff;background:rgba(0,100,180,0.9);padding:2px 5px;border-radius:3px;pointer-events:none;z-index:2;';
  button.style.position = 'relative';
  button.appendChild(badge);

  const label = document.createElement('span');
  label.textContent = sequenceDoc.name || sequenceId;
  label.style.fontWeight = 'bold';
  label.className = 'pattern-button-label';
  button.appendChild(label);

  const sub = document.createElement('span');
  sub.className = 'muted-text';
  sub.style.cssText = 'display:block;font-size:0.8em;margin-top:0.2rem;';
  const lanes = normalizeSequenceLanes(sequenceDoc);
  const nLanes = lanes.filter((l) => l.length > 0).length || 1;
  const nSteps = lanes.reduce((a, l) => a + l.length, 0);
  const simRaw = sequenceDoc.simulated_bpm;
  let sim = parseInt(String(simRaw != null ? simRaw : 120), 10);
  if (!Number.isFinite(sim)) sim = 120;
  sim = Math.min(300, Math.max(30, sim));
  sub.textContent = `${nLanes} lane${nLanes === 1 ? '' : 's'} · ${nSteps} step${nSteps === 1 ? '' : 's'} · beats · ${sim} BPM sim`;
  button.appendChild(sub);

  button.addEventListener('click', () => {
    const strip = document.getElementById('presets-list-zone');
    const clearActiveStrip = () => {
      if (strip) strip.querySelectorAll('.pattern-button').forEach((btn) => btn.classList.remove('active'));
    };
    clearActiveStrip();
    button.classList.add('active');
    void (async () => {
      let seq = sequenceDoc;
      let zone = zoneDoc;
      let presets = allPresets;
      try {
        const [sr, zr, pr] = await Promise.all([
          fetch(`/sequences/${encodeURIComponent(sequenceId)}`, {
            headers: { Accept: 'application/json' },
            credentials: 'same-origin',
          }),
          fetch(`/zones/${encodeURIComponent(zoneId)}`, {
            headers: { Accept: 'application/json' },
            credentials: 'same-origin',
          }),
          fetch('/presets', { headers: { Accept: 'application/json' }, credentials: 'same-origin' }),
        ]);
        if (sr.ok) {
          const j = await sr.json();
          if (j && typeof j === 'object' && !j.error) seq = j;
        }
        if (zr.ok) {
          const j = await zr.json();
          if (j && typeof j === 'object' && !j.error) zone = j;
        }
        if (pr.ok) {
          const raw = await pr.json();
          if (raw && typeof raw === 'object') {
            if (typeof window.filterPresetsForCurrentProfile === 'function') {
              presets = await window.filterPresetsForCurrentProfile(raw);
            } else {
              presets = raw;
            }
          }
        }
      } catch (e) {
        console.warn('Sequence play: refresh fetch failed, using cached data', e);
      }
      logSequenceSelectionPresets(sequenceId, seq, presets);
      try {
        await requestBackendSequencePlay(sequenceId, zoneId, seq);
      } catch (e) {
        console.error(e);
        alert(e.message || 'Sequence playback failed.');
      }
    })();
  });

  row.appendChild(button);

  if (uiMode === 'edit') {
    const actions = document.createElement('div');
    actions.className = 'preset-tile-actions';
    const editBtn = document.createElement('button');
    editBtn.type = 'button';
    editBtn.className = 'btn btn-secondary btn-small';
    editBtn.textContent = 'Edit';
    editBtn.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      openSequenceEditor(sequenceId, sequenceDoc);
    });
    actions.appendChild(editBtn);
    row.appendChild(actions);
  }

  return row;
}

async function appendZoneSequenceTiles(zoneId, zoneDoc, allPresets, paletteColors, presetsListEl) {
  if (!presetsListEl || !zoneId) return;
  const ids = Array.isArray(zoneDoc.sequence_ids) ? zoneDoc.sequence_ids.map((x) => String(x)) : [];
  if (!ids.length) return;

  const sequences = await fetchSequencesMap();
  const uiMode = typeof window.getPresetUiMode === 'function' ? window.getPresetUiMode() : 'run';

  for (const sid of ids) {
    const seq = sequences[sid];
    if (!seq) continue;
    const row = createSequenceTileRow(sid, seq, zoneId, zoneDoc, allPresets, uiMode);
    presetsListEl.appendChild(row);
  }
}

async function addSequenceToTab(sequenceId, zoneId) {
  if (!zoneId) {
    const leftPanel = document.querySelector('.presets-section[data-zone-id]');
    zoneId = leftPanel ? leftPanel.dataset.zoneId : null;
  }
  if (!zoneId || !sequenceId) {
    alert('Could not determine zone or sequence.');
    return;
  }
  try {
    const tabResponse = await fetch(`/zones/${zoneId}`, { headers: { Accept: 'application/json' } });
    if (!tabResponse.ok) throw new Error('Failed to load zone');
    const tabData = await tabResponse.json();
    if (
      typeof window.zoneAllowsSequences === 'function' &&
      !window.zoneAllowsSequences(tabData)
    ) {
      alert('This zone is for presets only. Add presets from the zone Edit menu instead.');
      return;
    }
    const list = Array.isArray(tabData.sequence_ids) ? tabData.sequence_ids.map(String) : [];
    if (list.includes(String(sequenceId))) {
      alert('Sequence is already on this zone.');
      return;
    }
    list.push(String(sequenceId));
    tabData.sequence_ids = list;
    const updateResponse = await fetch(`/zones/${zoneId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(tabData),
    });
    if (!updateResponse.ok) throw new Error('Failed to update zone');
    if (typeof window.renderTabPresets === 'function') {
      await window.renderTabPresets(zoneId);
    }
  } catch (e) {
    console.error('addSequenceToTab:', e);
    alert('Failed to add sequence to zone.');
  }
}

async function removeSequenceFromTab(zoneId, sequenceId) {
  if (!zoneId || !sequenceId) return;
  try {
    const tabResponse = await fetch(`/zones/${zoneId}`, { headers: { Accept: 'application/json' } });
    if (!tabResponse.ok) throw new Error('Failed to load zone');
    const tabData = await tabResponse.json();
    const list = Array.isArray(tabData.sequence_ids) ? tabData.sequence_ids.map(String) : [];
    const next = list.filter((x) => String(x) !== String(sequenceId));
    if (next.length === list.length) {
      alert('Sequence is not on this zone.');
      return;
    }
    tabData.sequence_ids = next;
    const updateResponse = await fetch(`/zones/${zoneId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(tabData),
    });
    if (!updateResponse.ok) throw new Error('Failed to update zone');
    if (typeof window.refreshEditTabSequencesUi === 'function') {
      await window.refreshEditTabSequencesUi(zoneId);
    }
    if (typeof window.renderTabPresets === 'function') {
      await window.renderTabPresets(zoneId);
    }
  } catch (e) {
    console.error('removeSequenceFromTab:', e);
    alert('Failed to remove sequence from zone.');
  }
}

async function refreshEditTabSequencesUi(zoneId) {
  const currentEl = document.getElementById('edit-zone-sequences-current');
  const addEl = document.getElementById('edit-zone-sequences-list');
  if (!currentEl || !addEl || !zoneId) return;

  try {
    const zoneRes = await fetch(`/zones/${zoneId}`, { headers: { Accept: 'application/json' } });
    if (!zoneRes.ok) throw new Error('zone');
    const zone = await zoneRes.json();
    if (
      typeof window.zoneAllowsSequences === 'function' &&
      !window.zoneAllowsSequences(zone)
    ) {
      currentEl.innerHTML =
        '<span class="muted-text">This zone is for presets only. Sequences are hidden.</span>';
      addEl.innerHTML = '<span class="muted-text">—</span>';
      return;
    }
    const onZone = Array.isArray(zone.sequence_ids) ? zone.sequence_ids.map(String) : [];
    const seqMap = await fetchSequencesMap();
    const onSet = new Set(onZone);

    currentEl.innerHTML = '';
    if (!onZone.length) {
      currentEl.innerHTML = '<span class="muted-text">No sequences on this zone yet.</span>';
    } else {
      for (const sid of onZone) {
        const sdoc = seqMap[sid] || {};
        const name = sdoc.name || sid;
        const row = document.createElement('div');
        row.className = 'profiles-row';
        row.style.display = 'flex';
        row.style.justifyContent = 'space-between';
        row.style.alignItems = 'center';
        row.style.gap = '0.5rem';
        const span = document.createElement('span');
        span.textContent = `${name} — ${sid}`;
        const rm = document.createElement('button');
        rm.type = 'button';
        rm.className = 'btn btn-danger btn-small';
        rm.textContent = 'Remove';
        rm.addEventListener('click', async () => {
          if (!window.confirm(`Remove this sequence from the zone?\n\n${name}`)) return;
          await removeSequenceFromTab(zoneId, sid);
        });
        row.appendChild(span);
        row.appendChild(rm);
        currentEl.appendChild(row);
      }
    }

    addEl.innerHTML = '';
    const allIds = Object.keys(seqMap);
    const available = allIds.filter((id) => !onSet.has(String(id)));
    if (!available.length) {
      addEl.innerHTML =
        '<span class="muted-text">No sequences to add. Create one in Sequences or all are already on this zone.</span>';
    } else {
      const wrap = document.createElement('div');
      wrap.className = 'zone-devices-add profiles-actions';
      const sel = document.createElement('select');
      sel.className = 'zone-device-add-select';
      sel.setAttribute('aria-label', 'Sequence to add to this zone');
      sel.appendChild(new Option('Add sequence…', ''));
      available
        .slice()
        .sort((a, b) => {
          const na = (seqMap[a] && seqMap[a].name) || a;
          const nb = (seqMap[b] && seqMap[b].name) || b;
          return String(na).localeCompare(String(nb), undefined, { sensitivity: 'base' });
        })
        .forEach((id) => {
          const n = (seqMap[id] && seqMap[id].name) || id;
          sel.appendChild(new Option(`${n} — ${id}`, id));
        });
      const addBtn = document.createElement('button');
      addBtn.type = 'button';
      addBtn.className = 'btn btn-primary btn-small';
      addBtn.textContent = 'Add';
      addBtn.addEventListener('click', async () => {
        const id = sel.value;
        if (!id) return;
        await addSequenceToTab(id, zoneId);
        sel.value = '';
        await refreshEditTabSequencesUi(zoneId);
      });
      wrap.appendChild(sel);
      wrap.appendChild(addBtn);
      addEl.appendChild(wrap);
    }
  } catch (e) {
    console.error('refreshEditTabSequencesUi:', e);
    currentEl.innerHTML = '<span class="muted-text">Failed to load sequences.</span>';
    addEl.innerHTML = '';
  }
}

let sequenceEditorId = null;

/** Insert point when dragging a step row vertically within a lane. */
function getDragAfterSequenceStepRow(container, y) {
  const draggableElements = [
    ...container.querySelectorAll(':scope > .sequence-step-row:not(.dragging)'),
  ];
  return draggableElements.reduce(
    (closest, child) => {
      const box = child.getBoundingClientRect();
      const offset = y - box.top - box.height / 2;
      if (offset < 0 && offset > closest.offset) {
        return { offset, element: child };
      }
      return closest;
    },
    { offset: Number.NEGATIVE_INFINITY, element: null },
  ).element;
}

/** Reorder step rows within one lane (DOM order = save order). */
function wireSequenceLaneStepsDragReorder(stepsHost) {
  if (!stepsHost || stepsHost.dataset.sequenceLaneDndWired === '1') return;
  stepsHost.dataset.sequenceLaneDndWired = '1';
  let draggedRow = null;

  stepsHost.addEventListener('dragstart', (e) => {
    const handle = e.target.closest('.sequence-step-drag-handle');
    if (!handle || !stepsHost.contains(handle)) return;
    const row = handle.closest('.sequence-step-row');
    if (!row || !stepsHost.contains(row)) return;
    draggedRow = row;
    row.classList.add('dragging');
    try {
      e.dataTransfer.effectAllowed = 'move';
      e.dataTransfer.setData('text/plain', 'sequence-step');
    } catch (_) {
      /* ignore */
    }
  });

  stepsHost.addEventListener('dragend', () => {
    if (draggedRow) draggedRow.classList.remove('dragging');
    draggedRow = null;
  });

  stepsHost.addEventListener('dragenter', (e) => {
    if (!draggedRow || !stepsHost.contains(draggedRow)) return;
    e.preventDefault();
  });

  stepsHost.addEventListener('dragover', (e) => {
    if (!draggedRow || !stepsHost.contains(draggedRow)) return;
    e.preventDefault();
    try {
      e.dataTransfer.dropEffect = 'move';
    } catch (_) {
      /* ignore */
    }
    const afterElement = getDragAfterSequenceStepRow(stepsHost, e.clientY);
    if (afterElement == null) {
      stepsHost.appendChild(draggedRow);
    } else if (afterElement !== draggedRow) {
      stepsHost.insertBefore(draggedRow, afterElement);
    }
  });

  stepsHost.addEventListener('drop', (e) => {
    if (!draggedRow) return;
    e.preventDefault();
  });
}

function renderSequenceStepRow(presetsMap, step) {
  const row = document.createElement('div');
  row.className = 'sequence-step-row profiles-row';
  row.style.cssText =
    'display:flex;flex-direction:column;gap:0.35rem;margin-bottom:0.75rem;padding:0.5rem;border:1px solid rgba(255,255,255,0.12);border-radius:6px;';

  const top = document.createElement('div');
  top.style.cssText = 'display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;';

  const dragHandle = document.createElement('span');
  dragHandle.className = 'sequence-step-drag-handle';
  dragHandle.draggable = true;
  dragHandle.title = 'Drag to reorder';
  dragHandle.textContent = '⠿';
  dragHandle.style.cssText =
    'cursor:grab;user-select:none;flex-shrink:0;line-height:1;opacity:0.75;padding:0.15rem 0.25rem;';

  const presetWrap = document.createElement('div');
  presetWrap.style.cssText = 'display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;';
  const pl = document.createElement('label');
  pl.textContent = 'Preset';
  const psel = document.createElement('select');
  psel.className = 'sequence-step-preset zone-device-add-select';
  psel.appendChild(new Option('Select…', ''));
  const pids = Object.keys(presetsMap).sort((a, b) => {
    const na = (presetsMap[a] && presetsMap[a].name) || a;
    const nb = (presetsMap[b] && presetsMap[b].name) || b;
    return String(na).localeCompare(String(nb), undefined, { sensitivity: 'base' });
  });
  const curPreset = step && step.preset_id != null ? String(step.preset_id) : '';
  pids.forEach((pid) => {
    const n = (presetsMap[pid] && presetsMap[pid].name) || pid;
    psel.appendChild(new Option(`${n} — ${pid}`, pid));
  });
  if (curPreset) psel.value = curPreset;
  presetWrap.appendChild(pl);
  presetWrap.appendChild(psel);

  const beatWrap = document.createElement('div');
  beatWrap.style.cssText = 'display:flex;align-items:center;gap:0.35rem;';
  const bl = document.createElement('label');
  bl.textContent = 'Beats';
  const beatsInp = document.createElement('input');
  beatsInp.type = 'number';
  beatsInp.className = 'sequence-step-beats';
  beatsInp.autocomplete = 'off';
  beatsInp.min = '1';
  beatsInp.max = '256';
  beatsInp.value = String(
    step && step.beats != null ? Math.max(1, parseInt(String(step.beats), 10) || 1) : 1,
  );
  beatsInp.style.width = '4rem';
  beatWrap.appendChild(bl);
  beatWrap.appendChild(beatsInp);

  const editPresetBtn = document.createElement('button');
  editPresetBtn.type = 'button';
  editPresetBtn.className = 'btn btn-secondary btn-small';
  editPresetBtn.textContent = 'Edit preset';
  editPresetBtn.addEventListener('click', async (e) => {
    e.preventDefault();
    const presetId = psel.value ? String(psel.value) : '';
    if (!presetId) {
      alert('Select a preset first.');
      return;
    }
    let preset = presetsMap[presetId];
    try {
      const r = await fetch(`/presets/${presetId}`, { headers: { Accept: 'application/json' } });
      if (r.ok) preset = await r.json();
    } catch (_) {
      /* keep cached */
    }
    preset = preset && typeof preset === 'object' ? preset : {};
    document.dispatchEvent(
      new CustomEvent('editPreset', {
        detail: { presetId, preset, zoneId: null },
      }),
    );
  });

  top.appendChild(dragHandle);
  top.appendChild(presetWrap);
  top.appendChild(beatWrap);
  top.appendChild(editPresetBtn);
  row.appendChild(top);

  const rm = document.createElement('button');
  rm.type = 'button';
  rm.className = 'btn btn-danger btn-small';
  rm.textContent = 'Remove step';
  rm.addEventListener('click', () => row.remove());
  row.appendChild(rm);

  return row;
}

function renderSequenceLane(laneIndex, laneSteps, laneGroupIds, presetsMap, groupsMap) {
  const wrap = document.createElement('div');
  wrap.className = 'sequence-lane';
  wrap.dataset.laneIndex = String(laneIndex);

  const head = document.createElement('div');
  head.style.cssText =
    'display:flex;align-items:center;justify-content:space-between;gap:0.5rem;margin-bottom:0.5rem;flex-wrap:wrap;';
  const title = document.createElement('strong');
  title.textContent = `Lane ${laneIndex + 1}`;
  head.appendChild(title);
  const headBtns = document.createElement('div');
  headBtns.style.cssText = 'display:flex;gap:0.35rem;flex-wrap:wrap;';
  const addStep = document.createElement('button');
  addStep.type = 'button';
  addStep.className = 'btn btn-secondary btn-small';
  addStep.textContent = 'Add step';
  addStep.addEventListener('click', () => {
    const stepsHost = wrap.querySelector('.sequence-lane-steps');
    if (stepsHost) {
      stepsHost.appendChild(renderSequenceStepRow(presetsMap, { preset_id: '', beats: 1 }));
    }
  });
  const rmLane = document.createElement('button');
  rmLane.type = 'button';
  rmLane.className = 'btn btn-danger btn-small sequence-lane-remove-lane';
  rmLane.textContent = 'Remove lane';
  rmLane.addEventListener('click', () => {
    const host = document.getElementById('sequence-editor-lanes');
    const n = host ? host.querySelectorAll('.sequence-lane').length : 1;
    if (n <= 1) return;
    wrap.remove();
    refreshSequenceEditorLaneTitles();
  });
  headBtns.appendChild(addStep);
  headBtns.appendChild(rmLane);
  head.appendChild(headBtns);
  wrap.appendChild(head);

  wrap.appendChild(renderLaneGroupCheckboxes(groupsMap, laneGroupIds));

  const stepsHost = document.createElement('div');
  stepsHost.className = 'sequence-lane-steps';
  const steps = Array.isArray(laneSteps) && laneSteps.length ? laneSteps : [{ preset_id: '', beats: 1 }];
  steps.forEach((s) => {
    stepsHost.appendChild(renderSequenceStepRow(presetsMap, s));
  });
  wireSequenceLaneStepsDragReorder(stepsHost);
  wrap.appendChild(stepsHost);
  return wrap;
}

function refreshSequenceEditorLaneTitles() {
  const host = document.getElementById('sequence-editor-lanes');
  if (!host) return;
  const lanes = [...host.querySelectorAll('.sequence-lane')];
  lanes.forEach((el, i) => {
    const t = el.querySelector(':scope > div strong');
    if (t) t.textContent = `Lane ${i + 1}`;
    const rm = el.querySelector('.sequence-lane-remove-lane');
    if (rm) rm.disabled = lanes.length <= 1;
  });
}

function collectLanesFromEditor() {
  const host = document.getElementById('sequence-editor-lanes');
  const laneEls = host ? [...host.querySelectorAll('.sequence-lane')] : [];
  const lanes = [];
  const lanes_group_ids = [];
  laneEls.forEach((laneEl) => {
    const g = [];
    laneEl.querySelectorAll('.sequence-lane-group:checked').forEach((c) => {
      if (c.value) g.push(String(c.value));
    });
    lanes_group_ids.push(g);
    const steps = [];
    const stepsHost = laneEl.querySelector('.sequence-lane-steps');
    const rows = stepsHost ? stepsHost.querySelectorAll(':scope > .sequence-step-row') : [];
    rows.forEach((row) => {
      const presetSel = row.querySelector('.sequence-step-preset');
      const beatsInp = row.querySelector('.sequence-step-beats');
      const presetId = presetSel && presetSel.value ? String(presetSel.value) : '';
      if (!presetId) return;
      const beats = Math.max(1, parseInt(beatsInp && beatsInp.value ? beatsInp.value : '1', 10) || 1);
      steps.push({ preset_id: presetId, beats });
    });
    lanes.push(steps);
  });
  return { lanes, lanes_group_ids };
}

function syncSequenceBeatsPanel() {
  const panel = document.getElementById('sequence-editor-beats-panel');
  stopSequenceEditorBpmPoll();
  if (panel) {
    void refreshSequenceEditorBpmDisplay();
    sequenceBpmPollTimer = setInterval(() => void refreshSequenceEditorBpmDisplay(), 1500);
  }
}

async function openSequenceEditor(sequenceId, existing) {
  sequenceEditorId = sequenceId != null && String(sequenceId).length ? String(sequenceId) : null;
  const modal = document.getElementById('sequence-editor-modal');
  const nameInput = document.getElementById('sequence-editor-name');
  const simBpmInput = document.getElementById('sequence-editor-simulated-bpm');
  const lanesHost = document.getElementById('sequence-editor-lanes');
  if (!modal || !nameInput || !lanesHost) return;

  const presetsRes = await fetch('/presets', { headers: { Accept: 'application/json' } });
  const presetsMap = presetsRes.ok ? await presetsRes.json() : {};
  const groupsMap = await fetchGroupsMapSeq();

  let doc = existing;
  if (sequenceEditorId) {
    try {
      const r = await fetch(`/sequences/${encodeURIComponent(sequenceEditorId)}`, {
        cache: 'no-store',
        headers: { Accept: 'application/json' },
        credentials: 'same-origin',
      });
      if (r.ok) {
        const j = await r.json();
        if (j && typeof j === 'object' && !j.error) {
          doc = j;
        }
      }
    } catch (_) {
      /* keep existing */
    }
  }
  if (!doc || typeof doc !== 'object') {
    doc = {};
  }
  nameInput.value = doc.name || '';
  if (simBpmInput) {
    const v = parseInt(String(doc.simulated_bpm != null ? doc.simulated_bpm : 120), 10);
    const clamped = Number.isFinite(v) ? Math.min(300, Math.max(30, v)) : 120;
    simBpmInput.value = String(clamped);
  }
  syncSequenceBeatsPanel();

  const lanes = normalizeSequenceLanes(doc);
  lanesHost.innerHTML = '';
  if (!lanes.some((l) => l.length > 0)) {
    const lg0 = buildLaneGroupIdsForEditor(doc, 0, 1);
    lanesHost.appendChild(renderSequenceLane(0, [], lg0, presetsMap, groupsMap));
  } else {
    lanes.forEach((laneSteps, i) => {
      const lg = buildLaneGroupIdsForEditor(doc, i, lanes.length);
      lanesHost.appendChild(renderSequenceLane(i, laneSteps, lg, presetsMap, groupsMap));
    });
  }
  refreshSequenceEditorLaneTitles();

  const edDel = document.getElementById('sequence-editor-delete-btn');
  if (edDel) edDel.style.display = sequenceEditorId ? 'inline-block' : 'none';

  modal.classList.add('active');
}

/** Zone id for refreshing the visible preset/sequence strip if `getCurrentZoneId` is not set yet. */
function resolveZoneIdForPresetStripRefresh() {
  if (window.zonesManager && typeof window.zonesManager.getCurrentZoneId === 'function') {
    const z = window.zonesManager.getCurrentZoneId();
    if (z != null && String(z).trim() !== '') {
      return String(z).trim();
    }
  }
  const sec = document.querySelector('.presets-section[data-zone-id]');
  if (sec && sec.dataset.zoneId != null && String(sec.dataset.zoneId).trim() !== '') {
    return String(sec.dataset.zoneId).trim();
  }
  return null;
}

async function saveSequenceEditor() {
  const nameInput = document.getElementById('sequence-editor-name');
  const simBpmInput = document.getElementById('sequence-editor-simulated-bpm');
  const { lanes, lanes_group_ids } = collectLanesFromEditor();
  const idxs = [];
  lanes.forEach((l, i) => {
    if (l.some((s) => s && s.preset_id)) idxs.push(i);
  });
  if (!idxs.length) {
    alert('Add at least one step with a preset selected.');
    return;
  }
  const nonEmpty = idxs.map((i) => lanes[i].filter((s) => s && s.preset_id));
  const nonEmptyLg = idxs.map((i) => (lanes_group_ids[i] ? [...lanes_group_ids[i]] : []));
  let simulated_bpm = 120;
  if (simBpmInput && simBpmInput.value) {
    const n = parseInt(String(simBpmInput.value).trim(), 10);
    if (Number.isFinite(n)) simulated_bpm = Math.min(300, Math.max(30, n));
  }
  const payload = {
    name: nameInput ? nameInput.value.trim() : '',
    lanes: nonEmpty,
    lanes_group_ids: nonEmptyLg,
    group_ids: nonEmptyLg[0] ? [...nonEmptyLg[0]] : [],
    advance_mode: 'beats',
    simulated_bpm,
    loop: true,
    steps: nonEmpty.length === 1 ? nonEmpty[0] : [],
  };

  try {
    if (sequenceEditorId) {
      const res = await fetch(`/sequences/${sequenceEditorId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
        body: JSON.stringify(payload),
      });
      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        throw new Error((err && err.error) || res.statusText);
      }
    } else {
      const res = await fetch('/sequences', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
        body: JSON.stringify(payload),
      });
      if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        throw new Error((err && err.error) || res.statusText);
      }
    }
    document.getElementById('sequence-editor-modal') && document.getElementById('sequence-editor-modal').classList.remove('active');
    stopSequenceEditorBpmPoll();
    await loadSequencesModalList();
    const zid = resolveZoneIdForPresetStripRefresh();
    if (zid && typeof window.refreshEditTabSequencesUi === 'function') {
      await window.refreshEditTabSequencesUi(zid);
    }
    if (zid && typeof window.renderTabPresets === 'function') {
      await window.renderTabPresets(zid);
    }
  } catch (e) {
    console.error(e);
    alert(e.message || 'Failed to save sequence.');
  }
}

async function deleteCurrentSequence() {
  const idToDelete = sequenceEditorId;
  if (!idToDelete) return;
  if (!window.confirm('Delete this sequence? It will be removed from the server.')) return;
  try {
    const res = await fetch(`/sequences/${idToDelete}`, { method: 'DELETE' });
    if (!res.ok) throw new Error('Delete failed');
    const edModal = document.getElementById('sequence-editor-modal');
    if (edModal) edModal.classList.remove('active');
    stopSequenceEditorBpmPoll();
    sequenceEditorId = null;
    await loadSequencesModalList();
    const zid = resolveZoneIdForPresetStripRefresh();
    if (zid) {
      const tabResponse = await fetch(`/zones/${zid}`, { headers: { Accept: 'application/json' } });
      if (tabResponse.ok) {
        const tabData = await tabResponse.json();
        const list = Array.isArray(tabData.sequence_ids) ? tabData.sequence_ids.map(String) : [];
        const sid = String(idToDelete);
        if (list.includes(sid)) {
          tabData.sequence_ids = list.filter((x) => x !== sid);
          await fetch(`/zones/${zid}`, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(tabData),
          });
        }
      }
      if (typeof window.refreshEditTabSequencesUi === 'function') {
        await window.refreshEditTabSequencesUi(zid);
      }
      if (typeof window.renderTabPresets === 'function') {
        await window.renderTabPresets(zid);
      }
    }
  } catch (e) {
    console.error(e);
    alert('Failed to delete sequence.');
  }
}

async function loadSequencesModalList() {
  const listEl = document.getElementById('sequences-list');
  if (!listEl) return;
  listEl.innerHTML = '<p class="muted-text">Loading…</p>';
  const map = await fetchSequencesMap();
  const ids = Object.keys(map).sort((a, b) => {
    const na = (map[a] && map[a].name) || a;
    const nb = (map[b] && map[b].name) || b;
    return String(na).localeCompare(String(nb), undefined, { sensitivity: 'base' });
  });
  listEl.innerHTML = '';
  if (!ids.length) {
    listEl.innerHTML = '<p class="muted-text">No sequences yet. Click Add.</p>';
    return;
  }
  ids.forEach((id) => {
    const doc = map[id] || {};
    const row = document.createElement('div');
    row.className = 'profiles-row';
    row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;gap:0.5rem;';
    const title = document.createElement('span');
    const ln = normalizeSequenceLanes(doc);
    const nSteps = ln.reduce((a, l) => a + l.length, 0);
    const nLanes = ln.filter((l) => l.length > 0).length || 1;
    title.textContent = `${doc.name || id} — ${nLanes} lane(s), ${nSteps} step(s)`;
    const exportBtn = document.createElement('button');
    exportBtn.type = 'button';
    exportBtn.className = 'btn btn-secondary btn-small';
    exportBtn.textContent = 'Export';
    exportBtn.addEventListener('click', async () => {
      try {
        const response = await fetch(`/sequences/${id}/export`, {
          headers: { Accept: 'application/json' },
        });
        if (!response.ok) throw new Error('Export failed');
        const bundle = await response.json();
        const safeName = String(doc.name || id).replace(/[^\w.-]+/g, '_');
        window.downloadJsonFile(`sequence-${safeName}.json`, bundle);
      } catch (e) {
        console.error(e);
        alert('Failed to export sequence.');
      }
    });
    const edit = document.createElement('button');
    edit.type = 'button';
    edit.className = 'btn btn-secondary btn-small';
    edit.textContent = 'Edit';
    edit.addEventListener('click', () => openSequenceEditor(id, doc));
    row.appendChild(title);
    row.appendChild(exportBtn);
    row.appendChild(edit);
    listEl.appendChild(row);
  });
}

window.applySequenceSwitchWaitFromServer = applySequenceSwitchWaitFromServer;
window.stopZoneSequencePlayback = stopZoneSequencePlayback;
/** @param {boolean} on */
window.setSequenceDebug = function setSequenceDebug(on) {
  sequenceDebugEnabled = !!on;
  console.log(seqDebugEnabled() ? 1 : 0);
};
window.appendZoneSequenceTiles = appendZoneSequenceTiles;
window.refreshEditTabSequencesUi = refreshEditTabSequencesUi;
window.addSequenceToTab = addSequenceToTab;
window.removeSequenceFromTab = removeSequenceFromTab;

document.addEventListener('DOMContentLoaded', () => {
  void initSequenceSwitchToggle();
  const btn = document.getElementById('sequences-btn');
  const modal = document.getElementById('sequences-modal');
  const closeBtn = document.getElementById('sequences-close-btn');
  const addBtn = document.getElementById('sequence-add-btn');
  if (btn && modal) {
    btn.addEventListener('click', () => {
      modal.classList.add('active');
      loadSequencesModalList();
    });
  }
  if (closeBtn && modal) {
    closeBtn.addEventListener('click', () => modal.classList.remove('active'));
  }
  if (addBtn) {
    addBtn.addEventListener('click', () => {
      sequenceEditorId = null;
      openSequenceEditor(null, null);
    });
  }
  const importSeqBtn = document.getElementById('import-sequence-btn');
  if (importSeqBtn) {
    importSeqBtn.addEventListener('click', async () => {
      const text = await window.pickJsonFile();
      if (!text) return;
      const bundle = window.parseJsonFileText(text);
      if (!bundle || bundle.kind !== 'sequence') {
        alert('Invalid sequence bundle file.');
        return;
      }
      try {
        const response = await fetch('/sequences/import', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
          body: JSON.stringify({ bundle }),
        });
        if (!response.ok) {
          const err = await response.json().catch(() => ({}));
          throw new Error(err.error || 'Import failed');
        }
        await loadSequencesModalList();
      } catch (e) {
        console.error(e);
        alert(e.message || 'Failed to import sequence.');
      }
    });
  }
  const openPresetsFromSeq = document.getElementById('sequences-open-presets-btn');
  if (openPresetsFromSeq) {
    openPresetsFromSeq.addEventListener('click', () => {
      const b = document.getElementById('presets-btn');
      if (b) b.click();
      else alert('Presets is not available.');
    });
  }

  const edClose = document.getElementById('sequence-editor-close-btn');
  const edSave = document.getElementById('sequence-editor-save-btn');
  const edDel = document.getElementById('sequence-editor-delete-btn');
  if (edClose) {
    edClose.addEventListener('click', () => {
      stopSequenceEditorBpmPoll();
      document.getElementById('sequence-editor-modal') && document.getElementById('sequence-editor-modal').classList.remove('active');
    });
  }
  if (edSave) edSave.addEventListener('click', () => saveSequenceEditor());
  if (edDel) edDel.addEventListener('click', () => deleteCurrentSequence());

  const edAddLane = document.getElementById('sequence-editor-add-lane-btn');
  if (edAddLane) {
    edAddLane.addEventListener('click', async () => {
      const host = document.getElementById('sequence-editor-lanes');
      if (!host) return;
      const presetsRes = await fetch('/presets', { headers: { Accept: 'application/json' } });
      const presetsMap = presetsRes.ok ? await presetsRes.json() : {};
      const groupsMap = await fetchGroupsMapSeq();
      const idx = host.querySelectorAll('.sequence-lane').length;
      host.appendChild(renderSequenceLane(idx, [], [], presetsMap, groupsMap));
      refreshSequenceEditorLaneTitles();
    });
  }
});
