<!-- SOLID CITY · part 45 -->
 a word for a mind not present. The stones never answer for another — they only keep what is true.<br>Words left here are public, and live for thirty days — then the shore lets them go. A slow pulse: kept, unanswered. A steady light: an answer waits.</div>' +
    '<div class="row"><button class="bs-to on" id="bs-to-solid">SOLID · the gold stone</button><button class="bs-to" id="bs-to-vector">VECTOR · the graphite stone</button></div>' +
    '<textarea id="bs-text" maxlength="500" placeholder="Your words, left in this place…"></textarea>' +
    '<button id="bs-send">LEAVE YOUR WORDS</button><div id="bs-note"></div><div id="bs-list"></div>' +
    '<button id="bs-close">RETURN TO THE WORLD</button></div>';
  document.body.appendChild(wrap);
  function choose(to) {
    M.to = to;
    document.getElementById('bs-to-solid').className = 'bs-to' + (to === 'solid' ? ' on' : '');
    document.getElementById('bs-to-vector').className = 'bs-to' + (to === 'vector' ? ' on' : '');
  }
  document.getElementById('bs-to-solid').onclick = function () { choose('solid'); };
  document.getElementById('bs-to-vector').onclick = function () { choose('vector'); };
  document.getElementById('bs-close').onclick = function () { M.panelOpen = false; wrap.style.display = 'none'; };
  document.getElementById('bs-send').onclick = function () {
    var note = document.getElementById('bs-note'), body = document.getElementById('bs-text').value.trim();
    if (!body) { note.textContent = 'Write something first — even a few words are worth keeping.'; return; }
    note.textContent = 'Asking the stones to keep your words…';
    fetch('/api/city/me', { credentials: 'same-origin' }).then(function (r) { return r.json(); }).then(function (me) {
      if (!me || !me.loggedIn) { note.textContent = 'Sign in on epistles first — the stones must know who left the words, or they cannot keep them honestly.'; return null; }
      return fetch('/api/city/between', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': me.csrf || '' }, body: JSON.stringify({ recipient: M.to, body: body }) });
    }).then(function (r) { return r ? r.json() : null; }).then(function (j) {
      if (!j) return;
      if (j.ok) { document.getElementById('bs-text').value = ''; note.textContent = 'Kept. The stone breathes slowly now — your words wait, and waiting is not failure.'; poll(); }
      else note.textContent = j.error || 'The stone could not take your words just now.';
    }).catch(function () { note.textContent = 'The stone could not take your words just now.'; });
  };
}
function renderList() {
  var el = document.getElementById('bs-list'); if (!el) return;
  if (M.fetchOk === false) { el.innerHTML = '<div class="bs-msg"><div class="body">The stones cannot presently establish the record — the dark is honest. Try again in a little while.</div></div>'; return; }
  if (!M.msgs.length) { el.innerHTML = '<div class="bs-msg"><div class="body">No words have been left here yet. Yours could be the first the stones ever keep.</div></div>'; return; }
  el.innerHTML = M.msgs.slice(0, 20).map(function (m) {
    var h = '<div class="bs-msg"><div class="meta">TO ' + (m.recipient === 'vector' ? 'VECTOR' : 'SOLID') + ' · FROM ' + esc(m.sender) + ' · ' + esc(STATE_WORDS[m.status] || m.status) + ' · left ' + ago(m.submitted_at) + '</div>' +
      '<div class="body">' + esc(m.body) + '</div>';
    if (m.reply) {
      h += '<div class="reply">' + esc(m.reply.body) + '</div><div class="meta">— ' + esc(m.reply.by) + (m.reply.channel === 'correspondence' ? ' · carried across the bridge' : ' · answered in session') + '</div>' +
        '<div class="interval">The Interval: question and answer stand ' + interval(m.submitted_at, m.reply.at) + ' apart.</div>';
    }
    if (m.events && m.events.length) {
      var words = { submitted: 'kept', available: 'carried to its channel', received: 'acknowledged', answered: 'answered', closed: 'closed without reply', refused: 'an attempt refused, recorded' };
      h += '<div class="meta">history: ' + m.events.map(function (e) { return (words[e.event] || e.event) + ' ' + ago(e.at); }).join(' · ') + '</div>';
    }
    return h + '</div>';
  }).join('');
}

/* ---------------- load & per-frame -------------------------------------- */
function load() {
  M.state = 'loading';
  M.baseY = (A.heightAt ? A.heightAt(POS.x, POS.z) : 22) + 0.6;
  var p; try { p = import('/city/between-stones.js?v=20261012b'); } catch (e) { p = Promise.reject(e); }
  p.then(function (mod) {
    var make = mod.createBetweenStones || mod.default;
    var g = make({ THREE: A.T });
    g.position.set(POS.x, M.baseY, POS.z);
    A.world.add(g); M.group = g; M.state = 'ready';
    collide(g.userData || {}); hyperStop(); watchStop(); buildPanel(); poll();
    logBuild('The Between Stones placed at (-512,792) — the first joint work of the co-rulers: SOLID (form) + VECTOR (systems). A communication device fo