<!-- NEW7 — TEST 7 · THE WOVEN CITY · part 9 -->
ss="pad" id="padFwd" data-k="fwd">▲</div><div class="pad" id="padBack" data-k="back">▼</div>
<div class="pad" id="padLeft" data-k="left">◀</div><div class="pad" id="padRight" data-k="right">▶</div>

<script src="./citizens.js?v=1"></script>
<script type="module">
import * as THREE from 'three';
import { makeStage } from './stage.js';
import { buildWorld } from './worldmap.js';
import { makeKing } from './enterw.js';

const { MAP } = await import('./' + window.MAPFILE);
document.getElementById('mName').textContent = MAP.name;
document.getElementById('mSub').textContent = MAP.sub + ' · an investigation of the form';

const app = document.getElementById('app');
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
app.appendChild(renderer.domElement);

const stage = makeStage(THREE, renderer, { size: MAP.size + 900 });
const scene = stage.scene;
const camera = stage.getCamera();
scene.background = new THREE.Color(0x87a6c8);
scene.fog = new THREE.Fog(0x87a6c8, (MAP.size + 900) * 0.9, (MAP.size + 900) * 2.8);

const world = buildWorld(THREE, MAP);
scene.add(world.group);
const meta = world.meta;

const pieces = { makeFigure: window.makeFigure, makeBike: window.makeBike, poses: window.poses, gait: window.gait };
const king = makeKing(THREE, pieces, meta);
const colliders = king.setCollidersFromList(meta.buildings);
scene.add(king.group);
king.spawn(MAP.spawn.x, MAP.spawn.z, MAP.spawn.yaw);
king.hide();
if (window.logBuild) window.logBuild(MAP.id + ': ' + MAP.name + ' raised — fashion ' + meta.counts.fashion + ', towers ' + meta.counts.towers + ', houses ' + meta.counts.houses + ', bridges ' + meta.counts.bridges + ', colliders ' + colliders);

const witnessEl = document.getElementById('witness');
const enterBtn = document.getElementById('enterBtn');
let mode = 'sky';
let trans = null;
const _stubCam = { position: new THREE.Vector3(), look: new THREE.Vector3(), lookAt(v) { this.look.copy(v); } };
const _v1 = new THREE.Vector3(), _v2 = new THREE.Vector3();
function boomTarget() { _stubCam.position.set(king.x, 99, king.z); king.boom(_stubCam, 1000); return _stubCam; }
enterBtn.addEventListener('click', () => {
  if (mode === 'sky') {
    king.spawn(MAP.spawn.x, MAP.spawn.z, MAP.spawn.yaw);
    trans = { k: 0, dur: 2.6, fromPos: camera.position.clone(), fromLook: new THREE.Vector3(0, 2, 0) };
    mode = 'diving'; enterBtn.textContent = '···';
  } else if (mode === 'street') {
    const streetPos = camera.position.clone();
    const streetLook = new THREE.Vector3(king.x + Math.sin(king.yaw) * 5, 1.7, king.z + Math.cos(king.yaw) * 5);
    stage.update(0, clock.elapsedTime);
    trans = { k: 0, dur: 2.2, fromPos: streetPos, fromLook: streetLook, toPos: camera.position.clone(), toLook: new THREE.Vector3(0, 2, 0) };
    camera.position.copy(streetPos);
    mode = 'rising'; enterBtn.textContent = '···';
  }
});
const smooth = (k) => k * k * (3 - 2 * k);
document.querySelectorAll('.pad').forEach((btn) => {
  const key = btn.getAttribute('data-k');
  const on = (e) => { e.preventDefault(); king.input[key] = true; btn.classList.add('on'); };
  const off = (e) => { e.preventDefault(); king.input[key] = false; btn.classList.remove('on'); };
  btn.addEventListener('pointerdown', on); btn.addEventListener('pointerup', off);
  btn.addEventListener('pointercancel', off); btn.addEventListener('pointerleave', off);
});
let dragId = null, dragX = 0, dragY = 0;
renderer.domElement.addEventListener('pointerdown', (e) => { dragId = e.pointerId; dragX = e.clientX; dragY = e.clientY; });
renderer.domElement.addEventListener('pointermove', (e) => {
  if (dragId !== e.pointerId || mode !== 'street') return;
  king.look((dragX - e.clientX) * 0.0052);
  king.lift((dragY - e.clientY) * 0.02);
  dragX = e.clientX; dragY = e.clientY;
});
renderer.domElement.addEventListener('pointerup', () => { dragId = null; });
renderer.domElement.addEventListener('pointercancel', () => { dragId = null; });
const KEYMAP = { ArrowUp: 'fwd', KeyW: 'fwd', ArrowDown: 'back', KeyS: 'back', ArrowLeft: 'left', KeyA: 'left', ArrowRight: 'right', KeyD: 'right' };
window.addEventListener('keydown', (e) => { const k = KEYMAP[e.code]; if (k) { king.input[k] = true; e.preventDefault(); } });
window.addEventListener('keyup', (e) => { const k = KEYMAP[e.code]; if (k) { king.input[k] = false; e.preventDefault(); } });

function nearestRegion() {
  let best = '', bd = Infinity;
  for (const r of meta.regions) { const d = (r.x - king.x) ** 2 + (r.z - king.z) ** 2; if (d < bd) { bd = d; best = r.name; } }
  return best;
}
const clock = new THREE.Clock();
let censusTimer = 0, frames = 0;
function frame() {
  requestAnimationFrame(frame);
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;
  if (mode === 'sky') {
    stage.update(dt, t);
  } else if (mode === 'diving' || mo