<!-- NEW5 — TEST 5 · THE BEND AT THE HEART · part 442 -->
d != null) ? opts.orbitSpeed
                                          : (Math.PI * 2) / 300,
    target: new THREE.Vector3(0, 0, 0)
  };

  function placeCamera(t) {
    /* gentle drift: a slow orbit, a slow breathing of height/reach */
    var polar = clamp(
      rig.polar + Math.sin(t * 0.045) * 0.05,
      LIMITS.minPolar, LIMITS.maxPolar);
    var dist = clamp(
      rig.dist + Math.sin(t * 0.03 + 1.7) * size * 0.035,
      LIMITS.minDist, LIMITS.maxDist);

    /* target breathes, fenced to a small disc around the center */
    var tx = Math.sin(t * 0.021) * LIMITS.targetRadius;
    var tz = Math.cos(t * 0.017) * LIMITS.targetRadius;
    rig.target.set(tx, 0, tz);

    var sp = Math.sin(polar), y = Math.cos(polar);
    camera.position.set(
      rig.target.x + dist * sp * Math.sin(rig.azimuth),
      dist * y,
      rig.target.z + dist * sp * Math.cos(rig.azimuth)
    );
    if (camera.position.y < LIMITS.minCamY) {
      camera.position.y = LIMITS.minCamY;
    }
    camera.lookAt(rig.target.x, 0, rig.target.z);
  }

  function setAspect(w, h) {
    if (h > 0) { camera.aspect = w / h; camera.updateProjectionMatrix(); }
  }
  if (renderer && renderer.domElement) {
    var el = renderer.domElement;
    setAspect(el.clientWidth || window.innerWidth,
              el.clientHeight || window.innerHeight);
  } else if (typeof window !== 'undefined') {
    setAspect(window.innerWidth, window.innerHeight);
  }

  placeCamera(0);

  function update(dt, t) {
    rig.azimuth += rig.orbitSpeed * dt;
    placeCamera(t);
  }

  function getCamera() { return camera; }

  /* --- birth certificate -------------------------------------------- */
  memoryLog(site, 'stage born: map floor ' + size + 'x' + size +
    ', stand layer at y=' + STAND_Y +
    ', camera rig fenced (polar ' + LIMITS.minPolar + '-' +
    LIMITS.maxPolar + ' rad, dist ' +
    Math.round(LIMITS.minDist) + '-' + Math.round(LIMITS.maxDist) + ')');

  return {
    scene: scene,
    floor: floor,
    standLayer: standLayer,
    standY: STAND_Y,
    groundY: groundY,
    camera: camera,
    getCamera: getCamera,
    update: update,
    setAspect: setAspect,
    limits: LIMITS,
    size: size
  };
}

/* Exports: ES module first, plain script tag as a fallback. */
if (typeof module !== 'undefined' && module.exports) {
  module.exports = { makeStage: makeStage };
}
if (typeof window !== 'undefined') {
  window.makeStage = makeStage;
}
export { makeStage };
export default makeStage;


===== FILE: public/new5/worldmap.js =====
// worldmap.js — ONE RECIPE, MANY MAPS. The shared engine for the investigation
// maps (new3-new6). A map is DATA: { water, streets, regions, spawn } and this
// engine renders it whole: rivers and lakes with banks, streets that become
// bridges where they cross water (deck at 4.5, piers, rails, ramps), region
// kits (fashion districts of unique buildings, suburbs of houses, markets,
// parks, waterfronts, canal rows). meta carries the census, the colliders,
// deckAt(x,z) and inWater(x,z) — the ground law for the King's feet.
import { buildDistrict } from './buildings.js';

// mergeGeometries, written for this engine: concatenates indexed geometries
// that all carry position/normal/uv (boxes, cones, cylinders, circles, and
// this file's ribbons) into one geometry — the recipe's merging hand.
function mergeGeos(THREE, geos) {
  let vTotal = 0, iTotal = 0;
  for (const g of geos) { vTotal += g.attributes.position.count; iTotal += g.index.count; }
  const pos = new Float32Array(vTotal * 3), nor = new Float32Array(vTotal * 3), uv = new Float32Array(vTotal * 2);
  const idx = vTotal > 65535 ? new Uint32Array(iTotal) : new Uint16Array(iTotal);
  let vo = 0, io = 0;
  for (const g of geos) {
    pos.set(g.attributes.position.array, vo * 3);
    nor.set(g.attributes.normal.array, vo * 3);
    if (g.attributes.uv) uv.set(g.attributes.uv.array, vo * 2);
    const gi = g.index.array;
    for (let i = 0; i < gi.length; i++) idx[io + i] = gi[i] + vo;
    vo += g.attributes.position.count; io += gi.length;
  }
  const out = new THREE.BufferGeometry();
  out.setAttribute('position', new THREE.BufferAttribute(pos, 3));
  out.setAttribute('normal', new THREE.BufferAttribute(nor, 3));
  out.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
  out.setIndex(new THREE.BufferAttribute(idx, 1));
  return out;
}

export function line(a, b, n) { const p = []; for (let i = 0; i <= n; i++) { const t = i / n; p.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]); } return p; }
export function arc(cx, cz, r, a0, a1, n) { const p = []; for (let i = 0; i <= n; i++) { const a = a0 + (a1 - a0) * (i / n); p.push([cx + Math.cos(a) * r, cz + Math.sin(a) * r]); } return p; }
export function join() { const out = []; for (const s of arguments) { for (const p of s) { const l = out[out.length - 1]; if (!l || Math.hypot(l[0] - p[0], l[1] - p[1]) > 0.01) out.push(p); } } return out; }

function mulberry32