<!-- SOLUV · part 665 -->
 + e.message); return []; } };
  const savePlaced = () => localStorage.setItem(PLACE_KEY, JSON.stringify(walls.filter(w => w.userData.placed).map(w => w.userData.placed)));
  function say(text, undo) { toastEl.querySelector('.sw-toast-t').textContent = text; toastEl.querySelector('.sw-undo').hidden = !undo; toastEl.hidden = false; toastEl.undo = undo || null; clearTimeout(toastTimer); toastTimer = setTimeout(() => { toastEl.hidden = true; toastEl.undo = null; }, undo ? 10000 : 6000); }
  toastEl.querySelector('.sw-undo').addEventListener('click', () => { const u = toastEl.undo; toastEl.hidden = true; toastEl.undo = null; if (u) u(); });
  function clearMarks() { if (place) for (const m of place.marks) scene.remove(m); }
  function endPlace(msg) { clearMarks(); place = null; pickEl.hidden = true; placeBtn.classList.remove('sw-on'); if (msg) say(msg); need = true; }
  function togglePlace() {
    if (place) return endPlace('Placing cancelled');
    if (!me || !me.username) { say('Log in to place a room (your name goes on it, so only you or the King can tear it down)'); return; }
    stopMotion(); place = { cells: [], marks: [] }; placeBtn.classList.add('sw-on');
    say('Place a room: tap the first gold block (a corner)');
    if (st.pitch < 0.5) flyTo({ ...st, pitch: 0.95, dist: Math.max(st.dist, 140) }, 900);   // look down at the gold blocks
  }
  const _ray = new THREE.Raycaster(), _ground = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), _hit = new THREE.Vector3();
  function placeTap(cx, cy) {
    const r = canvas.getBoundingClientRect();
    _ray.setFromCamera(new THREE.Vector2(((cx - r.left) / r.width) * 2 - 1, -((cy - r.top) / r.height) * 2 + 1), camera);
    if (!_ray.ray.intersectPlane(_ground, _hit)) return say('Tap the ground (the gold blocks), not the sky');
    if (Math.abs(_hit.x) > MAP / 2 || Math.abs(_hit.z) > MAP / 2) return say('That is off the 2000-block map');
    const cell = [Math.floor(_hit.x / BLOCK), Math.floor(_hit.z / BLOCK)];
    const m = new THREE.Mesh(markGeo, markMat); m.position.set((cell[0] + 0.5) * BLOCK, 0.04, (cell[1] + 0.5) * BLOCK); m.renderOrder = 2; scene.add(m);
    place.cells.push(cell); place.marks.push(m); need = true;
    if (place.cells.length === 1) return say('Now tap the second gold block (the other corner)');
    const plan = planWall(place.cells[0], place.cells[1]);
    if (plan.error) { scene.remove(place.marks.pop()); place.cells.pop(); return say(plan.error + ': tap another second block'); }
    place.plan = plan; pickEl.hidden = false; say(plan.blocks + ' gold block' + (plan.blocks > 1 ? 's' : '') + ' wide (' + plan.width + ' units), ' + PLACE_H + ' high: pick its room');
  }
  function planWall(a, b) {
    const dx = Math.abs(b[0] - a[0]), dz = Math.abs(b[1] - a[1]), alongX = dx >= dz;
    const blocks = (alongX ? dx : dz) + 1; if (blocks > MAX_SPAN) return { error: 'Too wide (max ' + MAX_SPAN + ' blocks)' };
    const width = blocks * BLOCK;
    const x = alongX ? (Math.min(a[0], b[0]) + blocks / 2) * BLOCK : (a[0] + 0.5) * BLOCK;
    const z = alongX ? (a[1] + 0.5) * BLOCK : (Math.min(a[1], b[1]) + blocks / 2) * BLOCK;
    // face the person placing it
    const rotY = alongX ? (camera.position.z >= z ? 0 : Math.PI) : (camera.position.x >= x ? Math.PI / 2 : -Math.PI / 2);
    const half = new THREE.Vector3(alongX ? width / 2 : BLOCK / 2, PLACE_H / 2, alongX ? BLOCK / 2 : width / 2);
    const box = new THREE.Box3(new THREE.Vector3(x, PLACE_H / 2, z).sub(half), new THREE.Vector3(x, PLACE_H / 2, z).add(half));
    for (const w of walls) if (!w.wall.canPlace(box)) return { error: 'That would stand inside ' + (w.userData.placed ? 'another placed room' : 'wall ' + w.wall.room) };
    return { x, z, rotY, width, blocks, alongX };
  }
  function makePlaced(rec) {
    const lobby = rec.room === 'lobby';
    const w = createChatWall({ THREE, room: lobby ? 1 : 2, width: rec.width, minHeight: rec.height, position: [rec.x, 0, rec.z], rotationY: rec.rotY,
      title: (lobby ? 'The Lobby' : 'The Soluv Room') + ' \u00b7 placed by ' + rec.creatorName, route: 'placed by ' + rec.creatorName,
      source: lobby ? { type: 'lobby', base: '/api/chat/lobby', name: 'Lobby' } : { type: 'api', base: '/api/soluv-room', name: 'Soluv room' } });
    w.userData.placed = rec; voice.attach(w);
    w.addEventListener('change', () => { need = true; }); w.addEventListener('status', () => { if (current === w) refreshStatus(); });
    scene.add(w); walls.push(w); need = true; return w;
  }
  function tearDown(w, why) {
    const i = walls.indexOf(w); if (i < 0) return; walls.splice(i, 1); scene.remove(w); w.wall.dispose();
    if (current === w) current = walls[0]; savePlaced(); refreshStatus(); refreshMe(); tearBtn.hidden = true; need = true; nearW = undefined;
    say(why || 'Torn down');
  }
  pickEl.addEventListener('click', e => {
    const b = e.target.closest('[data-room]'); if (!b || b.disabled || !place) return;
    if (b.dataset.room === 'cancel