// ── Voice inspector (3-pane workbench) ───────────────────────────────────
let _selectedVoiceWrap = null;

function selectVoice(wrap) {
  const inspector = document.getElementById('voices-inspector');
  if (!inspector) return;

  function restoreToRow(targetWrap) {
    // Restore all extracted elements back to their original DOM parents
    (targetWrap._extracted || []).forEach(({el, target}) => {
      if (el && target) target.appendChild(el);
    });
    targetWrap._extracted = [];
    // Move main-row and optimizer back from inspector body to wrap
    inspector.querySelectorAll('.vr-main-row,.vr-optimizer').forEach(el => targetWrap.appendChild(el));
  }

  // Deselect previous voice
  if (_selectedVoiceWrap && _selectedVoiceWrap !== wrap) {
    _selectedVoiceWrap.classList.remove('vr-selected');
    restoreToRow(_selectedVoiceWrap);
  }

  if (_selectedVoiceWrap === wrap) {
    restoreToRow(wrap);
    wrap.classList.remove('vr-selected');
    _selectedVoiceWrap = null;
    inspector.innerHTML = '<div class="inspector-placeholder"><span><span class="mdi mdi-account-voice"></span></span><p>Pick a voice on the left<br>to edit it here</p></div>';
    if (typeof window.onMobileInspectorClose === 'function') window.onMobileInspectorClose();
    return;
  }

  _selectedVoiceWrap = wrap;
  wrap.classList.add('vr-selected');
  if (typeof window.onMobileInspectorOpen === 'function') window.onMobileInspectorOpen();

  const voiceId    = wrap.dataset.id || '';
  const color      = wrap.dataset.color || '#9575CD';
  const isClone    = wrap.dataset.hasRef === 'true';
  const dbfs       = wrap.dataset.dbfs || '-';
  const hasPicture = wrap.dataset.hasPicture === 'true';

  // Live voice object — authoritative for mutable fields
  const v = (_voices || []).find(vv => vv.id === voiceId) || {};
  const langCode  = v.lang  || wrap.dataset.lang   || '';
  const flagCc    = v.flag  || wrap.dataset.flagCc  || langCode;
  const rating    = v.rating || 0;
  const curGender = v.gender || 'N';

  const nameParts  = voiceId.split('_');
  // Prefer an explicit display name (e.g. the character name for Rehearser voices);
  // otherwise fall back to the last segment of the ID (EN_F_Anna → "Anna").
  const dispName   = (v.name && String(v.name).trim())
    ? String(v.name).trim()
    : (nameParts.length > 1 ? nameParts[nameParts.length - 1] : voiceId);
  const initial    = (dispName[0] || voiceId[0] || '?').toUpperCase();
  const picSrcInsp = hasPicture ? `/api/voice/picture/${encodeURIComponent(voiceId)}` : null;
  const inspFlagCc = flagCc || langCode;
  const inspFlagIcon = inspFlagCc
    ? `<span class="fi fi-${inspFlagCc.toLowerCase()}" role="img" aria-label="${langCode}"></span>` : null;

  const defaultSilClass = curGender === 'M' ? 'mdi-face-man' : (curGender === 'F' ? 'mdi-face-woman' : 'mdi-account');
  const silHtml = `<span class="mdi ${defaultSilClass}" style="color:#fff;font-size:32px;display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:${color}"></span>`;

  const inspIconHtml = window.voiceAvatarIcon ? window.voiceAvatarIcon(v.avatar, 48) : null;
  const avatarHtml = picSrcInsp
    ? `<img src="${picSrcInsp}" alt="" class="insp-avatar-img">`
    : inspIconHtml || silHtml;
  const avatarBgStyle = (picSrcInsp || inspIconHtml) ? '' : `style="background:${color}"`;

  const flagIconHtml = inspFlagIcon || '';

  const makeStars = n => [1,2,3,4,5].map(i =>
    `<span class="insp-star${i <= n ? ' on' : ''}" data-val="${i}"><span class="mdi mdi-star"></span></span>`
  ).join('');

  const inspBenchText  = typeof fmtBenchmark    === 'function' ? fmtBenchmark(v)    : '-';
  const inspBenchTitle = typeof benchmarkTitle  === 'function' ? benchmarkTitle(v)  : '';
  const inspBenchCls   = typeof benchmarkClass  === 'function' ? benchmarkClass(v)  : '';

  const LANGS = ['EN','DE','IT','ES','FR','PT','NL','PL','ZH','JA','KO','AR','RU','TR','HI','SV','DA','FI','NB','HU','CS','RO','UK'];

  // Gender maps — used in template AND in event handlers
  const _gM = {F:'♀', M:'♂', N:'⚥', '':'?'};
  const _gL = {F:'Female', M:'Male', N:'Diverse', '':'—'};
  const _gC = {F:'g-f', M:'g-m', N:'g-n', '':'g-n'};
  const genderLabelHtml = `${_gM[curGender]||'?'} ${_gL[curGender]||'—'}`;

  inspector.innerHTML = `
    <div class="inspector-header">
      <div class="insp-hd-top">
        <div class="inspector-avatar${picSrcInsp ? ' insp-avatar-photo' : ''} insp-avatar-clickable" ${avatarBgStyle} title="Click to change photo">${avatarHtml}</div>
        <div class="insp-title-stack">
          <div class="insp-hd-row1">
            <h3 class="insp-disp-name" title="Double-click to rename the display name">${escHtml(dispName)}</h3>
            <input class="insp-name-edit" value="${escHtml(voiceId)}" spellcheck="false" style="display:none" placeholder="Voice ID">
<div class="insp-actions-save"></div>
          </div>
          <div class="insp-hd-row2">
            <div class="insp-id-block">
              <span class="insp-full-id" title="Click to rename the voice ID">${escHtml(voiceId)}</span>
              <button class="insp-edit-id-btn" type="button" title="Rename voice ID"><span class="mdi mdi-pencil-outline"></span> edit ID</button>
              <button class="insp-copy-id-btn" type="button" title="Copy voice ID">copy ID</button>
            </div>
            <div class="insp-hd-row2-right" style="display:flex; flex-direction:column; align-items:flex-end; gap:4px;">
              <div class="insp-actions-active"></div>
              ${(v.seed !== undefined && v.seed !== null) ? `<div class="insp-pinned-seed" style="color:#d32f2f; font-weight:600; font-size:12px;">Pin Seed # ${v.seed}</div>` : `<div class="insp-pinned-seed" style="color:#d32f2f; font-weight:600; font-size:12px; display:none;"></div>`}
            </div>
          </div>
          <div class="insp-hd-divider"></div>
          <div class="insp-subtitle">
            <span class="insp-flag" title="Click to change accent flag">
              <span class="insp-flag-icon">${flagIconHtml}</span>
            </span>
            <span class="insp-lang-wrap" title="Click to change language">
              <span class="insp-lang-code">${escHtml(langCode)}</span>
            </span>
            <span class="insp-gender-label" title="Click to cycle gender">${escHtml(genderLabelHtml)}</span>
            <span class="vl-type-label ${isClone ? 'vl-type-clone' : 'vl-type-design'}">${isClone ? 'Clone' : 'Design'}</span>
            ${inspBenchText !== '-' ? `<span class="insp-bench-chip ${escHtml(inspBenchCls)}" title="${escHtml(inspBenchTitle)}"><span class="mdi mdi-timer-outline"></span> ${escHtml(inspBenchText)}</span>` : ''}
            <span class="insp-stars-wrap">
              <span class="insp-stars">${makeStars(rating)}</span>
              <span class="insp-rating-label">${rating}/5</span>
            </span>
            <input class="insp-tag-input" type="text" placeholder="tag1, tag2…" value="${escHtml(v.tag||'')}" autocomplete="off">
          </div>
          <div class="insp-note-delete-row">
            <div class="insp-note-slot"></div>
            <div class="insp-actions-delete"></div>
          </div>
        </div>
      </div>
    </div>
    <div class="inspector-body"></div>
  `;

  // Inject mobile back button after HTML is set (inspector DOM recreated above)
  if (window.innerWidth <= 767 && typeof window.onMobileInspectorOpen === 'function') {
    window.onMobileInspectorOpen();
  }

  const saveSlot   = inspector.querySelector('.insp-actions-save');
  const activeSlot = inspector.querySelector('.insp-actions-active');
  const deleteSlot = inspector.querySelector('.insp-actions-delete');
  const body       = inspector.querySelector('.inspector-body');

  // ── Save changes → row 1 right ───────────────────────────────────────────
  const saveBtn = document.createElement('button');
  saveBtn.className = 'btn-primary insp-save-btn';
  saveBtn.textContent = 'Save changes';
  saveBtn.addEventListener('click', () => body.querySelector('.opt-save-text')?.click());
  saveSlot.appendChild(saveBtn);

  // ── Active toggle → row 2 right ──────────────────────────────────────────
  const detailRow = wrap.querySelector('.vr-detail-row');
  const activeEl  = detailRow?.querySelector('.vr-detail-active');
  const deleteEl  = detailRow?.querySelector('.vr-detail-delete');
  if (activeEl) activeSlot.appendChild(activeEl);

  // ── Delete → note row right ───────────────────────────────────────────────
  if (deleteEl) {
    deleteSlot.appendChild(deleteEl);
    const _dBtn = deleteEl.querySelector('.delete-btn');
    const _dCnl = deleteEl.querySelector('.delete-confirm-cancel');
    _dBtn?.addEventListener('click', () => deleteEl.classList.add('delete-pending'));
    _dCnl?.addEventListener('click', () => deleteEl.classList.remove('delete-pending'));
  }

  // ── Avatar click → photo upload ───────────────────────────────────────────
  const inspAvatar = inspector.querySelector('.inspector-avatar');
  inspAvatar.addEventListener('click', () => {
    body.querySelector('.photo-input')?.click();
  });

  inspAvatar.addEventListener('dragenter', e => { e.preventDefault(); inspAvatar.classList.add('drag-over'); });
  inspAvatar.addEventListener('dragover', e => { e.preventDefault(); inspAvatar.classList.add('drag-over'); });
  inspAvatar.addEventListener('dragleave', () => inspAvatar.classList.remove('drag-over'));
  inspAvatar.addEventListener('drop', async e => {
    e.preventDefault();
    inspAvatar.classList.remove('drag-over');
    
    if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
      const photoInput = body.querySelector('.photo-input');
      if (photoInput) {
        photoInput.files = e.dataTransfer.files;
        photoInput.dispatchEvent(new Event('change'));
      }
      return;
    }
    
    let url = e.dataTransfer.getData('text/uri-list');
    if (!url) {
      const html = e.dataTransfer.getData('text/html');
      if (html) {
        const match = html.match(/src=["'](.*?)["']/);
        if (match) url = match[1];
      }
    }
    if (!url) url = e.dataTransfer.getData('text/plain');

    if (url && /^https?:\/\//i.test(url)) {
      if (typeof status === 'function') status('Downloading picture from URL...');
      try {
        const r = await fetch('/api/voice/picture-url', {
          method: 'POST', headers: {'Content-Type': 'application/json'},
          body: JSON.stringify({voice_id: voiceId, image_url: url})
        });
        if (!r.ok) throw new Error((await r.json()).detail);
        
        // Trigger the photo update in the row
        const photoCell = body.querySelector('.vr-photo');
        if (photoCell) photoCell.dispatchEvent(new Event('update-photo'));
        
        if (typeof toast === 'function') toast('Photo saved from URL', 'success');
        if (typeof status === 'function') status('Photo saved successfully');
      } catch(err) {
        if (typeof toast === 'function') toast('Photo URL download failed: ' + err.message, 'error');
        if (typeof status === 'function') status('Photo URL download failed');
      }
    }
  });

  // ── Copy ID button ────────────────────────────────────────────────────────
  inspector.querySelector('.insp-copy-id-btn').addEventListener('click', () => {
    copyText(voiceId).then(() => toast('Copied: ' + voiceId));
  });

  // ── Editing: the big header is the DISPLAY NAME (v.name); "edit ID" / the
  //    full id renames the underlying voice ID. ───────────────────────────────
  const dispNameEl = inspector.querySelector('.insp-disp-name');
  const nameEditEl = inspector.querySelector('.insp-name-edit');
  const fullIdEl   = inspector.querySelector('.insp-full-id');
  let _editMode = null;          // 'name' | 'id'
  let _editCancelled = false;

  const showEditor = (mode, val) => {
    _editMode = mode; _editCancelled = false;
    dispNameEl.style.display = 'none'; fullIdEl.style.display = 'none';
    nameEditEl.style.display = 'block';
    nameEditEl.placeholder = mode === 'name' ? 'Display name' : 'Voice ID';
    nameEditEl.value = val; nameEditEl.focus(); nameEditEl.select();
  };
  const startNameEdit   = () => showEditor('name', dispName);   // edit display name
  const startInspRename = () => showEditor('id', voiceId);      // rename voice ID

  const commitEdit = async () => {
    const mode = _editMode; _editMode = null;
    dispNameEl.style.display = ''; fullIdEl.style.display = '';
    nameEditEl.style.display = 'none';
    if (_editCancelled || !mode) return;
    const val = nameEditEl.value.trim();
    if (mode === 'id') {
      if (!val || val === voiceId) return;
      const rowInput = wrap.querySelector('.vr-name-input');
      const rowOk    = wrap.querySelector('.rename-ok');
      if (rowInput && rowOk) { rowInput.value = val; rowOk.click(); }
      return;
    }
    // mode === 'name' → save a display name to the voice's metadata
    if (val === dispName) return;
    try {
      const r = await fetch('/api/voice/meta', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ voice_id: voiceId, name: val }),
      });
      if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); }
      v.name = val;
      dispNameEl.textContent = val || (voiceId.split('_').pop());
      if (typeof toast === 'function') toast('Display name updated', 'success');
      if (typeof renderVoiceList === 'function') renderVoiceList();
    } catch (e) {
      if (typeof toast === 'function') toast('Rename failed: ' + (e.message || e), 'error');
    }
  };

  dispNameEl.addEventListener('dblclick', startNameEdit);
  fullIdEl.addEventListener('dblclick', startInspRename);
  fullIdEl.addEventListener('click', startInspRename);
  inspector.querySelector('.insp-edit-id-btn')?.addEventListener('click', startInspRename);
  nameEditEl.addEventListener('blur', commitEdit);
  nameEditEl.addEventListener('keydown', e => {
    if (e.key === 'Enter') nameEditEl.blur();
    if (e.key === 'Escape') { _editCancelled = true; nameEditEl.blur(); }
  });

  // ── Flag (accent/country) — decoupled from language ──────────────────────
  const flagSpan   = inspector.querySelector('.insp-flag');
  const flagIconEl = inspector.querySelector('.insp-flag-icon');

  const applyFlag = async (cc) => {
    v.flag = cc; wrap.dataset.flagCc = cc;
    const fi = `<span class="fi fi-${cc.toLowerCase()}" role="img" aria-label="${cc}"></span>`;
    if (flagIconEl) flagIconEl.innerHTML = fi;
    await saveMeta(voiceId, { flag: cc });
  };
  flagSpan.addEventListener('click', () => {
    // Show this language's accents first, then the complete world list — accent is
    // decoupled from language, so e.g. an English voice can still be set to German.
    const langList = FLAG_OPTIONS[langCode.toUpperCase()] || [];
    const items = uniqueFlagOptions([langList, ALL_FLAGS]).map(([cc, name]) => [cc.toLowerCase(), name]);
    createSearchablePicker(flagSpan, items, applyFlag, {
      placeholder: 'Country or accent…',
      renderItem: (cc, name) =>
        `<span class="fi fi-${cc}" style="width:20px;height:14px;background-size:cover;border-radius:2px;flex-shrink:0;display:inline-block"></span><span>${escHtml(name)}</span>`,
    });
  });

  // ── Language — click lang code to change ─────────────────────────────────
  const langCodeEl = inspector.querySelector('.insp-lang-code');
  const langWrap   = inspector.querySelector('.insp-lang-wrap');

  const applyLang = async (newLang) => {
    v.lang = newLang; wrap.dataset.lang = newLang;
    if (langCodeEl) langCodeEl.textContent = newLang;
    await saveMeta(voiceId, { lang: newLang });
  };
  langWrap?.addEventListener('click', () => {
    const items = LANGS.map(l => [l, LANGUAGE_LABELS[l] ? `${LANGUAGE_LABELS[l]} (${l})` : l]);
    createSearchablePicker(langWrap, items, applyLang, {
      placeholder: 'Language…',
      renderItem: (l, label) =>
        `<span class="ipi-code">${escHtml(l)}</span><span>${escHtml(LANGUAGE_LABELS[l] || l)}</span>`,
    });
  });

  // ── Gender label (subtitle) — click to cycle ──────────────────────────────
  const genderLabelEl = inspector.querySelector('.insp-gender-label');
  const genderSel     = inspector.querySelector('.insp-gender-sel');
  const applyGender = async (ng) => {
    v.gender = ng;
    if (genderLabelEl) genderLabelEl.textContent = `${_gM[ng]||'?'} ${_gL[ng]||'—'}`;
    if (genderSel) genderSel.value = ng;
    const gBadge = wrap.querySelector('.gender-badge');
    if (gBadge) {
      gBadge.innerHTML = `<span class="gender-sym">${_gM[ng]||'?'}</span><span class="gender-txt">${_gL[ng]||'—'}</span>`;
      gBadge.className = 'gender-badge ' + (_gC[ng]||'g-n');
    }
    await saveMeta(voiceId, { gender: ng });
  };
  genderLabelEl?.addEventListener('click', () => {
    const cycle = ['F','M','N'];
    applyGender(cycle[(cycle.indexOf(v.gender||'N')+1)%3]);
  });
  genderSel?.addEventListener('change', () => applyGender(genderSel.value));

  // ── Tag input — comma-separated, autocomplete from DB + localStorage ────────
  const tagInput = inspector.querySelector('.insp-tag-input');

  const getAllKnownTags = () => {
    const fromDb = (_voices || []).flatMap(vv =>
      (vv.tag || '').split(',').map(t => t.trim()).filter(Boolean));
    const fromStorage = getStoredTags();
    return [...new Set([...fromStorage, ...fromDb])].sort((a, b) => a.localeCompare(b));
  };
  const tagLastToken  = val => val.split(',').pop().trimStart();
  const tagReplaceLastToken = (val, rep) => {
    const parts = val.split(',');
    parts[parts.length - 1] = parts.length > 1 ? ' ' + rep : rep;
    return parts.join(',');
  };

  let _tagDrop = null;
  const hideTagDrop = () => { _tagDrop?.remove(); _tagDrop = null; };
  const showTagDrop = () => {
    hideTagDrop();
    const token = tagLastToken(tagInput.value);
    const all   = getAllKnownTags();
    const matches = all.filter(t =>
      t.toLowerCase().startsWith(token.toLowerCase()) && t.toLowerCase() !== token.toLowerCase()
    );
    if (!matches.length) return;

    _tagDrop = document.createElement('div');
    _tagDrop.className = 'tag-suggest';
    matches.slice(0, 10).forEach(tag => {
      const btn = document.createElement('button');
      btn.type = 'button'; btn.className = 'tag-suggest-item';
      btn.textContent = tag;
      btn.addEventListener('mousedown', e => {
        e.preventDefault();
        tagInput.value = tagReplaceLastToken(tagInput.value, tag);
        tagInput.dispatchEvent(new Event('input'));
        hideTagDrop();
        tagInput.focus();
      });
      _tagDrop.appendChild(btn);
    });
    document.body.appendChild(_tagDrop);
    const r = tagInput.getBoundingClientRect();
    _tagDrop.style.top  = (r.bottom + 3) + 'px';
    _tagDrop.style.left = r.left + 'px';
    _tagDrop.style.minWidth = Math.max(r.width, 140) + 'px';
  };

  tagInput.addEventListener('input',  () => showTagDrop());
  tagInput.addEventListener('focus',  () => showTagDrop());
  tagInput.addEventListener('blur',   () => setTimeout(hideTagDrop, 150));
  tagInput.addEventListener('keydown', e => {
    if (e.key === 'Escape') hideTagDrop();
    if (e.key === ',' && _tagDrop) setTimeout(showTagDrop, 10);
  });

  const saveTagValue = debounce(async () => {
    const tags = tagInput.value.split(',').map(t => t.trim()).filter(Boolean);
    tags.forEach(addStoredTag);
    v.tag = tagInput.value;
    await saveMeta(voiceId, { tag: tagInput.value });
  }, 700);
  tagInput.addEventListener('input', saveTagValue);
  tagInput.addEventListener('change', saveTagValue);

  // ── Interactive rating (subtitle + meta kept in sync) ─────────────────────
  const updateRating = async (newRating) => {
    v.rating = newRating; wrap.dataset.rating = newRating;
    inspector.querySelectorAll('.insp-star').forEach(s =>
      s.classList.toggle('on', parseInt(s.dataset.val) <= newRating));
    inspector.querySelectorAll('.insp-rating-label, .insp-meta-rating-label').forEach(el =>
      el.textContent = `${newRating}/5`);
    wrap.querySelectorAll('.vr-rating .star').forEach((s, i) =>
      s.classList.toggle('on', i < newRating));
    await saveMeta(voiceId, { rating: newRating });
  };
  ['.insp-stars', '.insp-meta-stars'].forEach(sel => {
    const stars = [...inspector.querySelectorAll(`${sel} .insp-star`)];
    stars.forEach(s => {
      s.addEventListener('click', () => {
        const val = parseInt(s.dataset.val);
        updateRating(val === (v.rating||0) ? 0 : val);
      });
      s.addEventListener('mouseenter', () => {
        const val = parseInt(s.dataset.val);
        stars.forEach(ss => ss.classList.toggle('on', parseInt(ss.dataset.val) <= val));
      });
      s.addEventListener('mouseleave', () =>
        stars.forEach(ss => ss.classList.toggle('on', parseInt(ss.dataset.val) <= (v.rating||0))));
    });
  });

  // ── Note row ──────────────────────────────────────────────────────────────
  const noteEl    = detailRow?.querySelector('.vr-note');
  const noteRowEl = inspector.querySelector('.insp-note-slot');
  if (noteEl && noteRowEl) noteRowEl.appendChild(noteEl);

  // ── Move main-row and optimizer into inspector body ───────────────────────
  const mainRow   = wrap.querySelector('.vr-main-row');
  const optimizer = wrap.querySelector('.vr-optimizer');
  if (mainRow)   body.appendChild(mainRow);
  if (optimizer) body.appendChild(optimizer);

  if (wrap._loadOptimizer) wrap._loadOptimizer().catch(e => console.warn('Auto-load waveform failed:', e));

  // ── Persona panel ─────────────────────────────────────────────────────────
  const personaVal = v.persona || '';
  const personaPanel = document.createElement('div');
  personaPanel.className = 'insp-persona-panel';
  personaPanel.innerHTML = `
    <div class="insp-persona-head">
      <span class="insp-persona-title">Character persona</span>
      <span class="insp-persona-hint">Used by LLM to rewrite text in this voice's style. Leave empty to skip.</span>
    </div>
    <textarea class="insp-persona-input" rows="3" placeholder="e.g. calm British narrator with a warm tone, measured pace, formal vocabulary…">${escHtml(personaVal)}</textarea>
    <div class="insp-persona-actions">
      <button class="btn-primary btn-sm insp-persona-save-btn">Save persona</button>
      <span class="insp-persona-status"></span>
    </div>
  `;
  body.appendChild(personaPanel);

  const personaTextarea = personaPanel.querySelector('.insp-persona-input');
  const personaStatus   = personaPanel.querySelector('.insp-persona-status');
  personaPanel.querySelector('.insp-persona-save-btn').addEventListener('click', async () => {
    const pText = personaTextarea.value.trim();
    try {
      await fetch('/api/voice/meta', { method:'POST', headers:{'Content-Type':'application/json'},
        body: JSON.stringify({ voice_id: voiceId, persona: pText }) });
      v.persona = pText;
      personaStatus.textContent = 'Saved.';
      setTimeout(() => { personaStatus.textContent = ''; }, 2000);
      toast('Persona saved', 'success');
    } catch(e) { toast('Save failed: ' + e.message, 'error'); }
  });

  const maintTitle = body.querySelector('.opt-maintenance .opt-group-title');
  if (maintTitle) {
    maintTitle.innerHTML = `<span class="opt-chevron"></span><span class="opt-title-text">Loudness <span class="opt-group-meta">Current ${escHtml(dbfs)} dBFS</span></span>`;
  }

  // ── Collapsible opt-groups ────────────────────────────────────────────────
  body.querySelectorAll('.opt-group').forEach(group => {
    const title = group.querySelector(':scope > .opt-group-title');
    if (!title) return;
    title.addEventListener('click', () => {
      group.classList.toggle('open');
    });
  });

  // ── Seed Finder panel ────────────────────────────────────────────────────
  if (typeof attachSeedFinder === 'function') {
    attachSeedFinder(voiceId, body);
  }

  // Track extracted elements for restoreToRow
  wrap._extracted = [
    activeEl ? {el: activeEl, target: detailRow} : null,
    deleteEl ? {el: deleteEl, target: detailRow} : null,
    noteEl   ? {el: noteEl,   target: detailRow}  : null,
  ].filter(Boolean);

}
