<!-- SOLUV · part 466 -->
eadBase); } }
    var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    var rec = null, listening = false;
    if (!SR) {
      mic.disabled = true;
      setMicNote('Mic off: this browser has no speech typing (try Safari or Chrome).');
    } else {
      setMicNote('Tap 🎤 to talk instead of typing.');
      mic.addEventListener('click', function () {
        if (listening) { rec.stop(); return; }
        rec = new SR();
        rec.lang = opts.lang || navigator.language || 'en-US';
        rec.interimResults = true; rec.continuous = false;
        var base = input.value ? input.value.replace(/\s*$/, ' ') : '';
        rec.onresult = function (e) {
          var s = '';
          for (var i = 0; i < e.results.length; i++) s += e.results[i][0].transcript;
          input.value = base + s; onInput();
        };
        rec.onerror = function (e) { showError('Mic error: ' + (e.error || 'unknown') + (e.error === 'not-allowed' ? ' — allow the microphone for this site.' : '')); };
        rec.onend = function () { listening = false; mic.setAttribute('aria-pressed', 'false'); setMicNote('Tap 🎤 to talk instead of typing.'); };
        try { rec.start(); listening = true; mic.setAttribute('aria-pressed', 'true'); setMicNote('Listening… tap 🎤 again to stop.'); }
        catch (e) { showError('Mic could not start: ' + e.message); }
      });
    }

    // ---------- live: EventSource, with a visible 1-second poll fallback ----------
    function poll() {
      if (pollBusy || destroyed) return;
      pollBusy = true;
      fetch(api + '?since=' + lastId, { credentials: 'same-origin', cache: 'no-store' })
        .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
        .then(function (j) { (j.msgs || []).forEach(addServerMsg); if (pollTimer) setStatus('poll', 'Live dropped — checking every 1s'); })
        .catch(function (e) { setStatus('down', 'Offline — ' + e.message + ' (retrying every 1s)'); })
        .then(function () { pollBusy = false; });
    }
    function startPoll() { if (!pollTimer) { pollTimer = setInterval(poll, 1000); poll(); } }
    function stopPoll() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } }
    function openStream() {
      if (!('EventSource' in window)) { setStatus('poll', 'No live stream here — checking every 1s'); startPoll(); return; }
      es = new EventSource(api + '/events?since=' + lastId, { withCredentials: true });
      es.onopen = function () { stopPoll(); setStatus('live', 'Live'); };
      es.onmessage = function (e) {
        var m;
        try { m = JSON.parse(e.data); } catch (err) { showError('Bad message from server: ' + err.message); return; }
        addServerMsg(m);
      };
      es.onerror = function () {
        setStatus('poll', 'Live dropped — checking every 1s');
        startPoll();                        // EventSource keeps retrying on its own; onopen stops the poll
        if (es.readyState === 2) { es.close(); setTimeout(function () { if (!destroyed) openStream(); }, 2000); }
      };
    }

    // ---------- visualViewport: keep the composer above the keyboard (fit mode) ----------
    if (opts.fitViewport) {
      var vv = window.visualViewport;
      var fit = function () {
        var hgt = vv ? vv.height : window.innerHeight;
        root.style.setProperty('--sr-vvh', Math.round(hgt) + 'px');
        if (vv && vv.offsetTop) window.scrollTo(0, 0);
        if (document.activeElement === input && nearBottom()) toBottom();
      };
      (vv || window).addEventListener('resize', fit);
      if (vv) vv.addEventListener('scroll', fit);
      cleanups.push(function () { (vv || window).removeEventListener('resize', fit); if (vv) vv.removeEventListener('scroll', fit); });
      fit();
    }

    // ---------- dense text size: A−/A+ and pinch, saved on this phone ----------
    var SIZE_KEY = 'soluvRoom.denseSize', SIZE_MIN = 12, SIZE_MAX = 26;
    var DEFAULT_DENSE = opts.denseSize || 15;
    var size = DEFAULT_DENSE;
    function applySize(px, save) {
      size = Math.max(SIZE_MIN, Math.min(SIZE_MAX, Math.round(px)));
      root.style.setProperty('--sr-dfs', size + 'px');
      aMinus.disabled = size <= SIZE_MIN; aPlus.disabled = size >= SIZE_MAX;
      if (save) {
        try { localStorage.setItem(SIZE_KEY, String(size)); }
        catch (e) { showError('Could not save your text size on this phone: ' + e.message); }
        info('Text ' + size + 'px');
      }
    }
    if (dense) {
      var saved = null;
      try { saved = localStorage.getItem(SIZE_KEY); }
      catch (e) { showError('Could not read your saved text size: ' + e.message); }
      applySize(saved ? parseInt(saved, 10) || DEFAULT_DENSE : DEFAULT_DENSE, false);
      aMinus.addEventListener('click', function () { applySize(size - 1, true); });
      aPlus.addEventListener('click', function () { applySize(size + 1, true); });
      // Pinch on the lines sets the text size (two fingers); 