<!-- NEW5 — TEST 5 · THE BEND AT THE HEART · part 441 -->
: big faint patches, lighter and darker,
   * so the ground has weather without having a pattern */
  for (var p = 0; p < 260; p++) {
    var x = rand() * px, y = rand() * px;
    var r = 18 + rand() * 60;
    var lift = Math.floor(rand() * 13) - 6;
    var grad = g.createRadialGradient(x, y, 0, x, y, r);
    var base = 'rgba(' + (26 + lift) + ',' + (29 + lift) + ',' +
               (31 + lift) + ',';
    grad.addColorStop(0, base + '0.16)');
    grad.addColorStop(1, base + '0)');
    g.fillStyle = grad;
    g.fillRect(x - r, y - r, r * 2, r * 2);
  }

  /* fine speckle: the grain of asphalt, barely there */
  for (var s = 0; s < 2600; s++) {
    var sx = rand() * px, sy = rand() * px;
    var tone = 24 + Math.floor(rand() * 22);
    g.fillStyle = 'rgb(' + tone + ',' + (tone + 2) + ',' + (tone + 4) + ')';
    g.fillRect(sx, sy, 1.4, 1.4);
  }

  /* one faint seam cross at the tile heart — a surveyor's mark the
   * district can align to, invisible unless you go looking */
  g.fillStyle = 'rgba(214,178,84,0.05)';
  g.fillRect(px / 2 - 1, 0, 2, px);
  g.fillRect(0, px / 2 - 1, px, 2);

  var tex = new THREE.CanvasTexture(canvas);
  tex.wrapS = THREE.RepeatWrapping;
  tex.wrapT = THREE.RepeatWrapping;
  var tiles = Math.max(4, Math.round(size / 300));
  tex.repeat.set(tiles, tiles);
  if ('colorSpace' in tex && THREE.SRGBColorSpace) {
    tex.colorSpace = THREE.SRGBColorSpace;
  }
  tex.anisotropy = 4;
  return tex;
}

/* ------------------------------------------------------------------ */
/* makeStage                                                           */
/* ------------------------------------------------------------------ */

function makeStage(THREE, renderer, opts) {
  opts = opts || {};
  var size = opts.size || 2600;             /* floor + apron, edge to edge */
  var site = opts.site || 'new2';
  var STAND_Y = 0.02;                       /* the contract height */

  var scene = new THREE.Scene();
  var SKY = new THREE.Color(0x0b0e12);
  scene.background = SKY;
  /* Fog matched to the sky and pushed out with the stage: from the
   * sky view the district's far edge must stay readable, so the fog
   * only takes hold well past the district — the world edge melts
   * into dark before the camera could ever frame raw void. */
  scene.fog = new THREE.Fog(SKY.getHex(), size * 1.3, size * 3.4);

  /* --- light: enough to read a city by ------------------------------ */
  scene.add(new THREE.HemisphereLight(0xbdd3ff, 0x1c241c, 0.85));
  var sun = new THREE.DirectionalLight(0xfff1d6, 1.6);
  sun.position.set(size * 0.45, size * 0.7, size * 0.25);
  scene.add(sun);

  /* --- 1. THE MAP FLOOR --------------------------------------------- */
  var floor = new THREE.Mesh(
    new THREE.PlaneGeometry(size, size),
    new THREE.MeshStandardMaterial({
      map: makeFloorTexture(THREE, size),
      roughness: 0.96,
      metalness: 0.0
    })
  );
  floor.rotation.x = -Math.PI / 2;
  floor.position.y = 0;
  floor.name = 'MAP_FLOOR';
  scene.add(floor);

  /* --- 2. THE STAND LAYER ------------------------------------------- */
  /* Invisible on purpose. Standing is resolved by the groundY()
   * contract below — never by raycast, never by decorative geometry —
   * so a character's feet have one law and it cannot be broken by a
   * badly placed floor tile. */
  var standLayer = new THREE.Mesh(
    new THREE.PlaneGeometry(size, size),
    new THREE.MeshBasicMaterial()
  );
  standLayer.rotation.x = -Math.PI / 2;
  standLayer.position.y = STAND_Y;
  standLayer.visible = false;
  standLayer.name = 'STAND_LAYER';
  scene.add(standLayer);

  function groundY(/* x, z */) { return STAND_Y; }

  /* --- 3. THE CAMERA RIG -------------------------------------------- */
  /* Anti-incursion fences (the King's word: keep the sight honest).
   *  - polar angle: never near-horizontal (would slice through towers
   *    or under the floor), never a dead top-down stare.
   *  - distance: never so close the frame is inside one building,
   *    never so far the district shrinks and void takes the frame.
   *    The fence is a share of the stage size, so at district scale
   *    the sky view still frames the whole district at once.
   *  - target: locked within a whisper of the district center.
   *  - absolute floor: camera height can never go below MIN_CAM_Y. */
  var LIMITS = {
    minPolar: 0.30,                 /* rad from vertical  (~17 deg) */
    maxPolar: 1.02,                 /* rad from vertical  (~58 deg) */
    minDist: size * 0.55,           /* 1430 at default size */
    maxDist: size * 1.12,           /* 2912 at default size */
    targetRadius: size * 0.03,      /* target may breathe with the stage */
    minCamY: 12
  };

  var camera = new THREE.PerspectiveCamera(
    opts.fov || 50, 1, 0.5, size * 6
  );

  var rig = {
    azimuth: (opts.azimuth != null) ? opts.azimuth : -0.65,
    polar: 0.62,
    dist: size * 1.04,
    orbitSpeed: (opts.orbitSpee