<!-- SOLUV · part 465 -->
tion nearBottom() { return list.scrollHeight - list.scrollTop - list.clientHeight < 80; }
    function toBottom() { list.scrollTop = list.scrollHeight; newCount = 0; updateJump(); }
    function updateJump() {
      jump.disabled = newCount === 0;
      jump.textContent = dense ? ('↓' + (newCount ? newCount : '')) : (newCount ? ('Latest ↓ (' + newCount + ')') : 'Latest ↓');
    }

    function addServerMsg(m) {
      if (!m || typeof m.id !== 'number') return;
      if (m.id > lastId) lastId = m.id;
      if (byId[m.id]) return;                                   // already shown
      var pending = m.localKey && byKey[m.localKey];
      if (pending) {                                            // my echo: confirm in place, no re-render
        confirmPending(pending, m);
        return;
      }
      var stick = nearBottom();
      var node = letterNode(m, false);
      if (dense && !loadingHistory && !reduceMotion) node.classList.add('sr-fresh');
      byId[m.id] = node;
      list.appendChild(node);
      if (stick) toBottom(); else { newCount++; updateJump(); }
    }

    function confirmPending(node, m) {
      delete byKey[m.localKey];
      byId[m.id] = node;
      node.classList.remove('sr-pending', 'sr-failed');
      node.dataset.id = String(m.id);
      var king = !!m.king;
      if (dense) { node._sr.t = m.t; node._sr.state = ''; }
      else {
        node._sr.name.textContent = king ? 'The Epistolary' : m.from;
        node._sr.time.textContent = hhmm(m.t);
        node._sr.note.textContent = '';
      }
      var r = node.querySelector('.sr-retry'); if (r) r.remove();
    }

    // ---------- send ----------
    function setNote(node, text) { if (node._sr.note) node._sr.note.textContent = text; else node._sr.state = text; }
    function headers() {
      var hd = { 'Content-Type': 'application/json' };
      if (me.csrf) hd['X-CSRF-Token'] = me.csrf;
      return hd;
    }
    function postLetter(text, localKey, node) {
      setNote(node, 'Sending…');
      fetch(api, { method: 'POST', credentials: 'same-origin', headers: headers(),
        body: JSON.stringify({ msg: text, localKey: localKey }) })
        .then(function (r) {
          return r.json().catch(function () { throw new Error('HTTP ' + r.status + ' (not JSON)'); })
            .then(function (j) { if (!r.ok || !j.ok) throw new Error(j.error || j.reply || ('HTTP ' + r.status)); return j; });
        })
        .then(function (j) {
          if (j.msg) {
            if (j.msg.id > lastId) lastId = j.msg.id;
            if (byKey[localKey]) confirmPending(node, j.msg);
          }
        })
        .catch(function (e) {
          node.classList.add('sr-failed');
          setNote(node, 'Not sent: ' + e.message);
          if (!node.querySelector('.sr-retry')) {
            var b = h('button', 'sr-btn sr-retry', 'Try again'); b.type = 'button';
            b.addEventListener('click', function () { b.remove(); node.classList.remove('sr-failed'); postLetter(text, localKey, node); });
            (node.querySelector('.sr-fold') || node).appendChild(b);
          }
          showError('Your letter did not send: ' + e.message);
        });
    }

    compose.addEventListener('submit', function (ev) {
      ev.preventDefault();
      var text = input.value.trim();
      if (!text) return;
      if (text.length > MAX_LEN) { showError('Too long: ' + text.length + ' of ' + MAX_LEN + ' characters.'); return; }
      var localKey = newKey();
      var node = letterNode({ from: me.name, user: me.username, king: me.king, text: text, t: Date.now(), mine: true }, true);
      node.classList.add('sr-pending');
      byKey[localKey] = node;
      list.appendChild(node);
      toBottom();
      input.value = ''; onInput();
      postLetter(text, localKey, node);
    });
    function onInput() {
      var n = input.value.length;
      count.textContent = n + ' / ' + MAX_LEN;
      count.setAttribute('data-over', n > MAX_LEN ? '1' : '0');
      if (dense) info(n ? n + ' / ' + MAX_LEN : readBase);
      input.style.height = 'auto';
      input.style.height = Math.min(input.scrollHeight + 4, Math.round(window.innerHeight * 0.3)) + 'px';
    }
    input.addEventListener('input', onInput);
    var finePointer = window.matchMedia && window.matchMedia('(pointer: fine)').matches;
    input.addEventListener('keydown', function (ev) {
      if (ev.key === 'Enter' && !ev.shiftKey && (finePointer || ev.metaKey || ev.ctrlKey)) { ev.preventDefault(); compose.requestSubmit ? compose.requestSubmit() : send.click(); }
    });
    jump.addEventListener('click', toBottom);
    list.addEventListener('scroll', function () { if (nearBottom() && newCount) { newCount = 0; updateJump(); } }, { passive: true });

    // ---------- mic (Web Speech API) ----------
    function setMicNote(t) { micNote.textContent = t; if (dense) { mic.title = t; if (mic.disabled || /Listening/.test(t)) info(t); else if (read && /Listening/.test(read.textContent)) info(r