<!-- SOLIDIVISM · part 65 -->
entListener('touchstart', e => {
    for (const t of e.changedTouches) {
      if (t.clientX < innerWidth / 2 && stick.id < 0) { stick.id = t.identifier; stick.ox = t.clientX; stick.oy = t.clientY; }
      else if (look.id < 0) { look.id = t.identifier; look.lx = t.clientX; look.ly = t.clientY; }
    }
    e.preventDefault();
  }, { passive: false });
  canvas.addEventListener('touchmove', e => {
    for (const t of e.changedTouches) {
      if (t.identifier === stick.id) {
        input.sx = Math.max(-1, Math.min(1, (t.clientX - stick.ox) / 52));
        input.sy = Math.max(-1, Math.min(1, (t.clientY - stick.oy) / 52));
      } else if (t.identifier === look.id) {
        cam.yaw -= (t.clientX - look.lx) * 0.0052; cam.pitch += (t.clientY - look.ly) * 0.004;
        cam.pitch = Math.max(-0.5, Math.min(1.1, cam.pitch));
        look.lx = t.clientX; look.ly = t.clientY;
      }
    }
    e.preventDefault();
  }, { passive: false });
  const endT = e => {
    for (const t of e.changedTouches) {
      if (t.identifier === stick.id) { stick.id = -1; input.sx = 0; input.sy = 0; }
      if (t.identifier === look.id) look.id = -1;
    }
  };
  canvas.addEventListener('touchend', endT); canvas.addEventListener('touchcancel', endT);

  /* mouse: drag to look; move to hover (raycast label); click to focus */
  let dragging = false, lx = 0, ly = 0, downAt = 0, downX = 0, downY = 0;
  canvas.addEventListener('pointerdown', e => { dragging = true; lx = e.clientX; ly = e.clientY; downAt = performance.now(); downX = e.clientX; downY = e.clientY; });
  addEventListener('pointerup', e => {
    dragging = false;
    if (performance.now() - downAt < 260 && Math.hypot(e.clientX - downX, e.clientY - downY) < 6 && e.target === canvas) pickAt(e.clientX, e.clientY, true);
  });
  canvas.addEventListener('pointermove', e => {
    if (dragging && e.pointerType === 'mouse') {
      cam.yaw -= (e.clientX - lx) * 0.0046; cam.pitch += (e.clientY - ly) * 0.0036;
      cam.pitch = Math.max(-0.5, Math.min(1.1, cam.pitch)); lx = e.clientX; ly = e.clientY;
    } else if (e.pointerType === 'mouse') pickAt(e.clientX, e.clientY, false);
  });
  canvas.addEventListener('contextmenu', e => e.preventDefault());
  document.getElementById('enterBtn').addEventListener('click', () => { if (nearDoor) activateDoor(); });
  addEventListener('resize', () => {
    camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix();
    renderer.setSize(innerWidth, innerHeight);
  });
}

/* ---------------- collision: circle vs AABB, axis-slide -------------- */
function blockedAt(x, z, walls, r) {
  for (const b of walls) if (x > b.minX - r && x < b.maxX + r && z > b.minZ - r && z < b.maxZ + r) return true;
  return false;
}
function moveWithCollision(dx, dz) {
  const walls = mode === 'interior' ? interiorWalls : colliders;
  const r = 0.42;
  let nx = P.x + dx, nz = P.z + dz;
  if (!blockedAt(nx, nz, walls, r)) { P.x = nx; P.z = nz; return; }
  if (!blockedAt(nx, P.z, walls, r)) { P.x = nx; return; }
  if (!blockedAt(P.x, nz, walls, r)) { P.z = nz; return; }
}
function clampWorld() {
  if (mode === 'interior') {
    P.x = Math.max(POCKET.x - 6.9, Math.min(POCKET.x + 6.9, P.x));
    P.z = Math.max(POCKET.z + 0.45, Math.min(POCKET.z + 11.55, P.z));
  } else {
    const L = 430; P.x = Math.max(-L, Math.min(L, P.x)); P.z = Math.max(-L, Math.min(L, P.z));
  }
}

/* ---------------- doors: fade through, never teleport the city ------- */
const fadeEl = document.getElementById('fade');
function fadeThrough(mid) {
  mode = 'fading'; fadeEl.style.opacity = '1';
  setTimeout(() => { mid(); fadeEl.style.opacity = '0'; setTimeout(() => { if (mode === 'fading') mode = interiorGroup && savedStreet ? 'interior' : 'street'; }, 470); }, 470);
}
async function enterBuilding(rec) {
  savedStreet = { x: P.x, z: P.z, heading: P.heading, yaw: cam.yaw };
  toast('Raising ' + rec.title + '…');
  const spec = await makeRoomSpec(rec);
  buildInterior(spec, rec);
  fadeThrough(() => {
    P.x = POCKET.x; P.z = POCKET.z + 1.7; P.heading = 0; cam.yaw = 0; P.yS = POCKET.y;
    toast(rec.title + ' — the hall is yours. The door behind you leads out.');
  });
}
function exitBuilding() {
  fadeThrough(() => {
    if (savedStreet) { P.x = savedStreet.x; P.z = savedStreet.z; P.heading = savedStreet.heading; cam.yaw = savedStreet.yaw; }
    P.yS = 0;
    if (interiorGroup) { scene.remove(interiorGroup); interiorGroup = null; }
    interiorWalls = []; savedStreet = null;
  });
}
function activateDoor() {
  if (mode === 'street' && nearDoor) enterBuilding(nearDoor);
  else if (mode === 'interior') exitBuilding();
}
function findNearDoor() {
  if (mode === 'interior') {
    return (Math.abs(P.x - POCKET.x) < 1.3 && P.z - POCKET.z < 1.5) ? interiorRec : null;
  }
  if (mode !== 'street') return null;
  let best = null, bd = 3.4;
  for (const b of buildings) {
    const d = Math.hypot(P.x - b.door.x, P.z - b.door.z);
    if (d < bd) { bd = d; best = b; }
  }