<!-- SOLUV · part 664 -->
= bd <= NEAR_M ? best : null;
    if (near && near !== current) { current = near; if (!near.userData.placed) history.replaceState(null, '', '/soluv/' + (walls.indexOf(near) + 1) + '/' + location.search); refreshStatus(); refreshMe(); }
    const rec = near && near.userData.placed; tearBtn.hidden = !(rec && me && me.username && (me.username === rec.creator || me.king));
    if (near !== nearW) { nearW = near; form.classList.toggle('sw-away', !near); gotoBtn.classList.toggle('sw-away', !!near); if (!near && document.activeElement === input) input.blur(); }
    if (form.classList.contains('sw-guest')) loginA.href = loginHref();   // returns you to this exact spot after logging in
    api.near = !!near; api.nearDistance = bd;
    const ar = current.wall.archive; const showAr = !!(ar && ar.messages > 0 && ar.url);
    if (showAr) { const t = '\u25b2 ' + ar.messages + ' older messages did not fit \u00b7 open the full chat'; if (archA.textContent !== t) archA.textContent = t; if (archA.getAttribute('href') !== ar.url) archA.href = ar.url; }
    archA.hidden = !showAr;
  }
  gotoBtn.addEventListener('click', () => { flyTo(views.live(), 1400); });
  async function refreshMe() {
    const src = current.wall.source;
    if (!src || src.type === 'static') { input.disabled = true; send.disabled = true; form.classList.remove('sw-guest'); input.placeholder = src ? 'Test text: posting is off' : 'No room connected to this wall yet'; return; }
    try { me = await current.wall.whoAmI(); }
    catch (e) { input.disabled = true; send.disabled = true; input.placeholder = 'Cannot check your login: ' + e.message; console.error('[soluv] ' + e.message); return; }
    const guest = !me.username; form.classList.toggle('sw-guest', guest); input.disabled = guest; send.disabled = guest; if (guest) loginA.href = loginHref();
    input.placeholder = guest ? 'Watching: log in to post' : 'Write to the ' + (src.name || 'room') + ' as ' + (me.king ? 'The Epistolary ♛' : me.name) + '…';
  }
  /* one send path for the Send button, Enter and soluvKeys.post: the wall's own POST with YOUR session + CSRF, normal limits */
  async function sendText(text, fromComposer) {
    text = String(text == null ? '' : text);
    if (!text.trim()) return { ok: false, error: 'Write something first' };
    const src = current.wall.source; if (!src || src.type === 'static') return { ok: false, error: 'This wall has no room to post to' };
    send.disabled = true; const keep = input.placeholder; input.placeholder = 'Sending…';
    const r = await current.wall.post(text).catch(err => ({ ok: false, error: err.message }));
    send.disabled = form.classList.contains('sw-guest'); input.placeholder = keep;
    if (r.login) r.error = 'Log in to post';
    api.lastPost = { ok: !!r.ok, error: r.error || null, id: r.id || null, at: Date.now() };
    if (r.ok) { if (fromComposer) input.value = ''; statusEl.textContent = 'Sent to ' + (src.name || 'the room') + ' ✓ (it is now on the wall and on the site)'; statusEl.dataset.state = 'live'; setTimeout(refreshStatus, 4000); }
    else { statusEl.textContent = 'Not sent: ' + r.error; statusEl.dataset.state = 'error'; if (r.login) refreshMe(); }
    return r.ok ? { ok: true, id: r.id || null } : { ok: false, status: r.status || null, error: r.error };
  }
  form.addEventListener('submit', e => { e.preventDefault(); sendText(input.value, true); });

  /* TRANSCEIVER: read new lines aloud in each speaker's own chosen voice; mic dictation into this box */
  const voice = createTransceiver({ container: wrap, whoAmI: () => current.wall.whoAmI() });
  walls.forEach(w => voice.attach(w)); voice.setWalls(walls);
  { const mic = voice.micButton(input); if (mic) form.insertBefore(mic, send); }

  /* ---------- PLACE ROOM: tap two gold blocks anywhere on the map, a live wall appears between them ----------
     Snapped to the 10-unit gold blocks, runs along the longer side (x or z), default height 600 (it still grows), the
     0.13 minimum holds (scale 1, same dense text). It is a portable createChatWall: solid, live, postable with your own
     login. PROTOTYPE persistence: localStorage on THIS device only. Shared walls that others see need a server store
     (see the report); nothing here writes to the server except normal lobby / room posts. */
  const PLACE_KEY = 'soluv.placed.v1', PLACE_H = 600, MAX_SPAN = 100;
  const pickEl = wrap.querySelector('.sw-pick'), toastEl = wrap.querySelector('.sw-toast'), tearBtn = wrap.querySelector('.sw-tear'), placeBtn = wrap.querySelector('.sw-place');
  let place = null, toastTimer = 0;
  const markMat = new THREE.MeshBasicMaterial({ color: 0xffc94a, transparent: true, opacity: 0.55, depthWrite: false });
  const markGeo = new THREE.PlaneGeometry(BLOCK, BLOCK).rotateX(-Math.PI / 2);
  const loadPlaced = () => { try { const v = JSON.parse(localStorage.getItem(PLACE_KEY) || '[]'); return Array.isArray(v) ? v : []; } catch (e) { console.error('[soluv] saved rooms unreadable: '