<!-- SOLIDIVISM · part 64 -->
| wallMat);
    m.position.set(x, y, z); G.add(m); return m;
  };
  const wall = (w, d, x, z) => {                       /* a collider-backed wall segment */
    addBox(w, wallH, d, x, wallH / 2, z);
    interiorWalls.push({ minX: POCKET.x + x - w / 2 - 0.15, maxX: POCKET.x + x + w / 2 + 0.15,
                         minZ: POCKET.z + z - d / 2 - 0.15, maxZ: POCKET.z + z + d / 2 + 0.15 });
  };

  /* room slots: alternating sides down the hall */
  const slots = spec.rooms.map((room, i) => {
    const side = i % 2 === 0 ? 1 : -1;                       /* +x first */
    const per = Math.floor(i / 2);
    const zc = 2.6 + per * 3.6;
    return { room, side, zc, cx: side * (hallW / 2 + room.size[0] / 2), cz: zc };
  });
  const maxRoomX = hallW / 2 + 6;

  /* floor + ceiling over the whole footprint */
  addBox(maxRoomX * 2 + 0.4, 0.15, hallLen + 0.4, 0, -0.07, hallLen / 2, floorMat);
  addBox(maxRoomX * 2 + 0.4, 0.15, hallLen + 0.4, 0, wallH + 0.07, hallLen / 2, floorMat);

  /* hall end walls: entry (z=0, door gap in the middle) + far end */
  wall(hallW / 2 - 0.55, 0.25, -(hallW / 4 + 0.275), 0); wall(hallW / 2 - 0.55, 0.25, hallW / 4 + 0.275, 0);
  wall(maxRoomX * 2, 0.25, 0, hallLen);
  /* outer room walls (far sides + shared backs) */
  wall(0.25, hallLen, -maxRoomX, hallLen / 2); wall(0.25, hallLen, maxRoomX, hallLen / 2);
  /* hall side walls WITH door gaps where rooms open */
  for (const sgn of [1, -1]) {
    const mine = slots.filter(s => s.side === sgn).sort((a, b) => a.zc - b.zc);
    let cursor = 0;
    for (const s of mine) {
      const gapA = s.zc - 0.6, gapB = s.zc + 0.6;
      if (gapA > cursor) wall(0.25, gapA - cursor, sgn * hallW / 2, cursor + (gapA - cursor) / 2);
      cursor = gapB;
      /* room back + side partitions */
      wall(s.room.size[0], 0.25, s.cx, s.cz + s.room.size[2] / 2);
      wall(0.25, s.room.size[2], s.cx + sgn * s.room.size[0] / 2, s.cz);
    }
    if (cursor < hallLen) wall(0.25, hallLen - cursor, sgn * hallW / 2, cursor + (hallLen - cursor) / 2);
    /* room fronts between this side's rooms (the strip beyond the hall wall is open plan per room) */
  }
  /* entry sign + building panel above the door, exit panel facing in */
  const sign = new THREE.Mesh(new THREE.PlaneGeometry(3.6, 2.2),
    new THREE.MeshBasicMaterial({ map: panelTexture(rec.title, 'You are inside ' + rec.title + '. The hall holds its rooms; the door behind you returns to the street.', 'https://epistles.xyz' + rec.path, colorCss), side: THREE.DoubleSide }));
  sign.position.set(0, 1.9, 0.35); G.add(sign);

  /* each room: a content panel on its back wall + a floor tint */
  for (const s of slots) {
    const tint = new THREE.Mesh(new THREE.PlaneGeometry(s.room.size[0] - 0.3, s.room.size[2] - 0.3),
      new THREE.MeshStandardMaterial({ color: rec.cat.color, roughness: 1, transparent: true, opacity: 0.13 }));
    tint.rotation.x = -Math.PI / 2; tint.position.set(s.cx, 0.02, s.cz); G.add(tint);
    const panel = new THREE.Mesh(new THREE.PlaneGeometry(4.6, 2.9),
      new THREE.MeshBasicMaterial({ map: panelTexture(s.room.label, s.room.blurb || '', s.room.contentUrl, colorCss) }));
    panel.position.set(s.cx, 1.85, s.cz + s.room.size[2] / 2 - 0.18);
    panel.rotation.y = Math.PI; G.add(panel);
    const label = new THREE.Mesh(new THREE.PlaneGeometry(2.6, 0.5),
      new THREE.MeshBasicMaterial({ map: nameplateTexture(s.room.label.toUpperCase().slice(0, 18), colorCss), side: THREE.DoubleSide }));
    label.position.set(s.cx - s.side * 0.2, 2.9, s.cz); label.rotation.y = s.side > 0 ? -Math.PI / 2 : Math.PI / 2;
    G.add(label);
  }
  scene.add(G); interiorGroup = G;
  return G;
}

/* ---------------- THE KING'S FIGURE + bindControls() ----------------- */
const you = new THREE.Group(); you.name = 'you';
{
  const cloth = new THREE.MeshStandardMaterial({ color: 0x8c1620, roughness: 0.6 });
  const dark = new THREE.MeshStandardMaterial({ color: 0x14161c, roughness: 0.5, metalness: 0.3 });
  const body = new THREE.Mesh(new THREE.CapsuleGeometry(0.34, 0.78, 6, 14), cloth); body.position.y = 0.95;
  const head = new THREE.Mesh(new THREE.SphereGeometry(0.21, 18, 14), dark); head.position.y = 1.72;
  you.add(body, head);
}
scene.add(you);

const P = { x: 0, z: 17, heading: Math.PI, bob: 0, yS: 0 };
const cam = { yaw: Math.PI, pitch: 0.32 };
const input = { sx: 0, sy: 0, keys: {} };
let mode = 'street';                 /* street | interior | fading */
let savedStreet = null, nearDoor = null, focusRec = null;

function bindControls() {
  addEventListener('keydown', e => {
    input.keys[e.key.toLowerCase()] = true;
    if (e.key.toLowerCase() === 'e' && nearDoor) activateDoor();
    if (e.key === 'Escape') focusRec = null;
  });
  addEventListener('keyup', e => { input.keys[e.key.toLowerCase()] = false; });

  /* touch: left half = walk stick, right half = look drag (dynamic origin) */
  const stick = { id: -1, ox: 0, oy: 0 }, look = { id: -1, lx: 0, ly: 0 };
  canvas.addEv