<!-- SOLUV · part 658 -->
le', 'female', 'default'].includes(kind)) return { ok: false, error: "voice must be 'male', 'female' or 'default'" };
    if (kind === 'default') delete T.overrides[k]; else T.overrides[k] = kind;
    localStorage.setItem(LS_OVR, JSON.stringify(T.overrides)); renderPanel();
    return { ok: true, name: k, voice: kind, deviceVoice: kind === 'default' ? 'device default' : (T.picked[kind] ? T.picked[kind].name : 'device default, pitch ' + PITCH[kind]) };
  }

  /* ---------- mic: dictation into the composer ---------- */
  function micButton(input) {
    if (!SR) return null;
    const b = document.createElement('button'); b.type = 'button'; b.className = 'sv-mic'; b.textContent = 'Mic'; b.setAttribute('aria-label', 'Speak into the text box (does not send)');
    let rec = null;
    b.addEventListener('click', () => {
      if (rec) { rec.stop(); return; }
      rec = new SR(); rec.lang = navigator.language || 'en-US'; rec.interimResults = false; rec.continuous = false;
      rec.onresult = e => { const t = Array.from(e.results).map(r => r[0].transcript).join(' ').trim(); if (t) { input.value = (input.value ? input.value.replace(/\s*$/, ' ') : '') + t; input.dispatchEvent(new Event('input')); } };
      rec.onerror = e => { if (e.error !== 'aborted' && e.error !== 'no-speech') { T.problem = 'Mic: ' + e.error; console.error('[soluv-voice] ' + T.problem); renderPanel(); } };
      rec.onend = () => { rec = null; T.listening = false; b.classList.remove('sv-on'); };
      rec.start(); T.listening = true; b.classList.add('sv-on');
    });
    return b;
  }

  /* ---------- the panel (48 px buttons) ---------- */
  injectStyle();
  const panel = document.createElement('div'); panel.className = 'sv-panel'; panel.hidden = true; panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-label', 'Read aloud and voices');
  host.appendChild(panel);
  let note = '';
  function label(kind) { return kind === 'male' ? 'Male' : kind === 'female' ? 'Female' : 'Default'; }
  function renderPanel(msg) {
    if (msg !== undefined) note = msg || '';
    if (panel.hidden) return;
    const rows = T.recent.map(m => { const v = voiceOf(m); return '<div class="sv-row"><span class="sv-name">' + esc(m.from || 'Guest') + '</span><span class="sv-why">' + esc(v.why) + '</span><button type="button" class="sv-b" data-name="' + esc(m.from || 'Guest') + '">' + label(v.kind) + '</button></div>'; }).join('');
    const dev = synth ? 'Male: ' + (T.picked.male ? T.picked.male.name : 'device voice, lower pitch') + ' · Female: ' + (T.picked.female ? T.picked.female.name : 'device voice, higher pitch') : '';
    panel.innerHTML = '<div class="sv-head"><b>Transceiver</b><button type="button" class="sv-b sv-x" data-a="close" aria-label="Close">Close</button></div>' +
      '<div class="sv-line"><button type="button" class="sv-b' + (T.on ? ' sv-on' : '') + '" data-a="on">Read aloud: ' + (T.on ? 'On' : 'Off') + '</button>' +
      '<button type="button" class="sv-b" data-a="mine">My voice: ' + (T.me && T.me.username ? (T.mine ? label(T.mine) : 'Not set') : 'Log in') + '</button></div>' +
      '<div class="sv-small">Everyone hears you in the voice you choose (saved on the site). Tap a name to change how YOU hear them (this device only). ' + esc(dev) + '</div>' +
      (T.problem || note ? '<div class="sv-bad">' + esc(note || T.problem) + '</div>' : '') +
      '<div class="sv-list">' + (rows || '<div class="sv-small">No speakers yet.</div>') + '</div>';
  }
  const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
  let wallsRef = [];
  panel.addEventListener('click', async e => {
    const b = e.target.closest('button'); if (!b) return;
    if (b.dataset.a === 'close') { panel.hidden = true; return; }
    if (b.dataset.a === 'on') { setOn(!T.on, wallsRef); return; }
    if (b.dataset.a === 'mine') {
      if (!T.me || !T.me.username) { location.href = '/login?next=' + encodeURIComponent(location.pathname + location.search); return; }
      const next = T.mine === 'male' ? 'female' : T.mine === 'female' ? 'none' : 'male';
      b.disabled = true; const r = await setMine(next); b.disabled = false; if (!r.ok) renderPanel(r.error); return;
    }
    if (b.dataset.name) { const m = T.recent.find(x => (x.from || 'Guest') === b.dataset.name); const cur = m ? voiceOf(m) : { kind: 'default', why: '' }; const nx = cur.why !== 'your setting' ? (cur.kind === 'male' ? 'female' : 'male') : cur.kind === 'male' ? 'female' : 'default'; voiceFor(b.dataset.name, nx); }
  });
  function toggle(walls) { wallsRef = walls || wallsRef; panel.hidden = !panel.hidden; if (!panel.hidden) { renderPanel(); refreshMine().catch(err => { T.problem = 'Login check failed: ' + err.message; console.error('[soluv-voice] ' + T.problem); renderPanel(); }); } }

  return {
    attach, setOn: on => setOn(on, wallsRef), setWalls: w => { wallsRef = w; }, setMine, refreshMine, voiceFor, voiceOf, micButton, 