<!-- NEW1 — TEST 1 · THE LIVING DISTRICT · part 7 -->
idge meets bank (bridge crossings get lights)
    bg.position.set(bx, 0, riverZ);
    group.add(bg);
    stopLight(bx + STREET / 2 - 1.2, riverZ - riverW / 2 - 3.2);
    stopLight(bx + STREET / 2 - 1.2, riverZ + riverW / 2 + 3.2);
  }

  /* ---------- per-kind complex builders ---------- */
  function tree(x, z, parent) {
    const g = new THREE.Group();
    const t = new THREE.Mesh(G.trunk, M.trunk); t.position.y = 0.7; g.add(t);
    const c = new THREE.Mesh(G.canopy, M.leaf); c.position.y = 2.9; g.add(c);
    g.position.set(x, 0.12, z);
    (parent || group).add(g);
  }

  function buildDowntown(cx, cz) {
    const n = 4 + Math.floor(rng() * 3);
    for (let i = 0; i < n; i++) {
      const w = rr(10, 16), d = rr(10, 16), h = rr(26, 52);
      const x = cx + rr(-1, 1) * (BLOCK / 2 - w / 2 - 2);
      const z = cz + rr(-1, 1) * (BLOCK / 2 - d / 2 - 2);
      box(pick(towerPalette), w, h, d, x, h / 2 + 0.12, z);
    }
  }
  function buildMidrise(cx, cz) {
    for (let gx = -1; gx <= 1; gx++) for (let gz = -1; gz <= 1; gz++) {
      if (rng() < 0.25) continue;
      const w = rr(12, 17), d = rr(12, 17), h = rr(9, 20);
      box(pick(towerPalette), w, h, d, cx + gx * 20, h / 2 + 0.12, cz + gz * 20);
    }
  }
  function buildMarket(cx, cz) {
    // a walkable row of small shops along both street edges, awnings out
    for (const s of [-1, 1]) {
      for (let i = 0; i < 5; i++) {
        const w = 9.6, h = rr(5, 8), x = cx + (i - 2) * 12.4;
        const z = cz + s * (BLOCK / 2 - 6);
        box(pick(housePalette), w, h, 10, x, h / 2 + 0.12, z);
        const aw = box(M.awning, w, 0.18, 3.4, x, 3.1, z - s * 6.4);
        aw.rotation.x = s * 0.35;
      }
    }
    for (let i = 0; i < 4; i++) tree(cx + rr(-24, 24), cz + rr(-8, 8));
  }
  function buildParking(cx, cz) {
    // two open decks with stall markings and a few parked cars
    for (const s of [-1, 1]) {
      const z = cz + s * 17;
      box(M.deck, BLOCK - 8, 0.35, 26, cx, 0.3 + (s > 0 ? 2.6 : 0), z);
      if (s > 0) for (const px of [-24, 0, 24]) box(M.pole, 1.1, 2.6, 1.1, cx + px, 1.3, z);
      for (let i = 0; i < 9; i++) {
        box(M.stall, 0.26, 0.04, 4.4, cx - 26 + i * 6.4, 0.52 + (s > 0 ? 2.6 : 0), z - 8);
        if (rng() < 0.45) box(M.deckCar, 4.2, 1.3, 1.9, cx - 26 + i * 6.4 + 2.6, 1.15 + (s > 0 ? 2.6 : 0), z - 5.6);
      }
    }
  }
  function buildSuburb(cx, cz, c) {
    // houses in a loose ring + inner green; neighbors merge edge-to-edge
    for (let gx = -2; gx <= 2; gx++) for (let gz = -2; gz <= 2; gz++) {
      if (Math.abs(gx) < 2 && Math.abs(gz) < 2) continue;   // shared inner green
      if (rng() < 0.18) { tree(cx + gx * 12.4, cz + gz * 12.4); continue; }
      const w = rr(7, 9.5), d = rr(7, 9.5), h = rr(3.2, 4.6);
      const x = cx + gx * 12.4, z = cz + gz * 12.4;
      box(pick(housePalette), w, h, d, x, h / 2 + 0.12, z, group, pick([0, Math.PI / 2]));
      const roof = new THREE.Mesh(G.roofPrism, M.roof);
      roof.scale.set(w * 0.78, rr(1.8, 2.6), d * 0.78);
      roof.position.set(x, h + 0.12 + roof.scale.y / 2, z);
      roof.rotation.y = Math.PI / 4;
      group.add(roof);
      // record the address for the life director: the house, its
      // suburb, and a door point just outside on the street side —
      // outward from the block's heart, a couple of units past the
      // wall. Recording consumes no rng: the city must not change.
      houses.push({
        x, z, suburb: c.name,
        door: { x: x + Math.sign(gx) * (w / 2 + 2), z: z + Math.sign(gz) * (d / 2 + 2) },
      });
    }
    box(M.park, 26, 0.1, 26, cx, 0.14, cz); // the shared green
    tree(cx - 6, cz - 6); tree(cx + 7, cz + 5);
  }
  function buildPark(cx, cz) {
    box(M.park, BLOCK - 2, 0.14, BLOCK - 2, cx, 0.13, cz);
    // the lake: an open water disc, west side of the park
    const lake = new THREE.Mesh(new THREE.CircleGeometry(13, 24), M.water);
    lake.rotation.x = -Math.PI / 2;
    lake.position.set(cx - 13, 0.24, cz);
    lake.name = 'park-lake';
    group.add(lake);
    // a pale path loop (four thin strips)
    box(M.walk, 52, 0.05, 2, cx, 0.22, cz - 26);
    box(M.walk, 52, 0.05, 2, cx, 0.22, cz + 26);
    box(M.walk, 2, 0.05, 52, cx - 26, 0.22, cz);
    box(M.walk, 2, 0.05, 52, cx + 26, 0.22, cz);
    for (let i = 0; i < 14; i++) {
      const x = cx + rr(-28, 28), z = cz + rr(-28, 28);
      if (Math.hypot(x - (cx - 13), z - cz) < 15) continue; // not in the lake
      tree(x, z);
    }
  }

  const builders = {
    downtown: buildDowntown, midrise: buildMidrise, market: buildMarket,
    parking: buildParking, suburb: buildSuburb, park: buildPark,
  };
  const houses = [];   // filled by buildSuburb as it places each house
  const meta = { complexes: [], river: { z: riverZ, width: riverW }, bridges: BRIDGE_STREETS.map(streetX) };
  for (const c of COMPLEXES) {
    const { x, z } = blockCenter(c.col, c.row);
    builders[c.kind](x, z, c);
    meta.complexes.push({ nam