<!-- SOLUV · part 467 -->
one finger still scrolls.
      var pinch = null;
      var dist = function (t) { return Math.hypot(t[0].clientX - t[1].clientX, t[0].clientY - t[1].clientY); };
      list.addEventListener('touchstart', function (e) { if (e.touches.length === 2) pinch = { d: dist(e.touches), s: size }; }, { passive: true });
      list.addEventListener('touchmove', function (e) {
        if (!pinch || e.touches.length !== 2) return;
        e.preventDefault();
        applySize(pinch.s * dist(e.touches) / pinch.d, false);
      }, { passive: false });
      list.addEventListener('touchend', function (e) { if (pinch && e.touches.length < 2) { pinch = null; applySize(size, true); } });
      list.addEventListener('gesturestart', function (e) { e.preventDefault(); });   // iOS: pinch sizes text, not the page
    }

    // ---------- start: welcome letter, who am I, history, live ----------
    var welcome = h('article', 'sr-letter sr-welcome' + (dense ? ' sr-welcome-dense' : ''));
    var wseal = h('div', 'sr-seal'); wseal.setAttribute('aria-hidden', 'true'); wseal.appendChild(h('span', null, 'S'));
    var wfold = h('div', 'sr-fold');
    wfold.appendChild(h('p', 'sr-text', WELCOME));
    wfold.appendChild(h('span', 'sr-sign', dense ? '— Provolta' : '— Provolta, for the room'));
    welcome.appendChild(wseal); welcome.appendChild(wfold);
    list.appendChild(welcome);
    sealIt(welcome);

    fetch(api + '/me', { credentials: 'same-origin', cache: 'no-store' })
      .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
      .then(function (j) {
        me = j;
        meLine.textContent = '';
        readBase = j.username ? 'Signed as ' + (j.king ? 'The Epistolary ♛' : j.name) : 'Guest — log in to sign';
        if (dense) { info(mic.disabled ? micNote.textContent : readBase); return; }
        if (j.username) {
          meLine.appendChild(document.createTextNode('Signed as '));
          meLine.appendChild(h('strong', null, j.king ? 'The Epistolary ♛' : j.name));
        } else {
          meLine.appendChild(document.createTextNode('You are Guest. '));
          var a = h('a', null, 'Log in'); a.href = '/login?next=' + encodeURIComponent(location.pathname);
          meLine.appendChild(a);
          meLine.appendChild(document.createTextNode(' to sign your letters.'));
        }
      })
      .catch(function (e) { meLine.textContent = 'Could not check your login (' + e.message + '); posting as Guest.'; readBase = 'Guest (login check failed)'; info(readBase); showError('Login check failed: ' + e.message); })
      .then(function () { return fetch(api + '?since=0', { credentials: 'same-origin', cache: 'no-store' }); })
      .then(function (r) { if (!r.ok) throw new Error('history HTTP ' + r.status); return r.json(); })
      .then(function (j) { (j.msgs || []).forEach(addServerMsg); loadingHistory = false; toBottom(); })
      .catch(function (e) { loadingHistory = false; showError('Could not load earlier letters: ' + e.message); })
      .then(function () { if (!destroyed) openStream(); });

    return {
      el: root,
      destroy: function () {
        destroyed = true; stopPoll(); if (es) es.close(); if (io) io.disconnect();
        if (rec && listening) rec.stop();
        cleanups.forEach(function (f) { f(); });
        root.remove();
      }
    };
  }

  window.SoluvRoom = { mount: mount, version: '1' };
})();


===== FILE: public/soluv/space/chat-wall-space.js =====
/*
 * chat-wall-space.js: COPY of /soluv/world/chat-wall.js for /soluv/space, plus opts.textWidthEm (screen-width wrap). /soluv uses the original.
 * chat-wall.js: Playa's portable, live, dense chat wall for the /soluv world.
 * One wall = one chat room. It is a plain THREE.Group you can drop into ANY three.js scene
 * (the /soluv world, Solid's saga, /playa ...). It does not own the page, the renderer, the camera or the loop.
 *
 *   import { createChatWall } from '/soluv/world/chat-wall.js';
 *   const wall = createChatWall({ THREE, room: 1, position: [0, 0, -40], rotationY: 0, scale: 1 });
 *   scene.add(wall);            // that's all: it updates itself in onBeforeRender, loads its room when you look at it
 *   wall.wall.dispose();        // when you are done
 *
 * THREE is injected (works with the page's own three: r128 global in the saga, r186 module in /soluv).
 * THE LAW, Minimum 0.13: window.MINIMUM = 0.13 (an avatar's eye). One text em = exactly MINIMUM world units,
 * so the smallest glyph is eye-sized; every solid part is >= MINIMUM on every axis; scale is clamped to >= 1.
 * Data thrift: nothing is fetched until the wall is on screen and close enough to see; text is rasterized
 * at the resolution the camera can actually read (4 / 8 / 16 / 32 px per em), full detail only where readable.
 * No silent fallbacks: every failure is written on the wall itself (and console.error).
 */
if (typeof window !== 'undefined' && window.MINIMUM === undefined) window.MINIMUM = 0.13;
export