<!-- NEW1 — TEST 1 · THE LIVING DISTRICT · part 3 -->
eometry(0.34, 0.03, 8, 22), dark);
    tire.rotation.y = Math.PI / 2;
    w.add(tire);
    const hub = new THREE.Mesh(new THREE.SphereGeometry(0.045, 8, 8), frameM);
    w.add(hub);
    for (let i = 0; i < 3; i++) {
      const sp = new THREE.Mesh(new THREE.BoxGeometry(0.015, 0.62, 0.015), frameM);
      sp.rotation.x = (i / 3) * Math.PI;
      w.add(sp);
    }
    g.add(w);
    return w;
  }
  const wF = wheel(0.56), wB = wheel(-0.56);
  function tube(x1, y1, z1, x2, y2, z2, r) {
    const a = new THREE.Vector3(x1, y1, z1), b = new THREE.Vector3(x2, y2, z2);
    const d = b.clone().sub(a), len = d.length();
    const m = new THREE.Mesh(new THREE.CylinderGeometry(r, r, len, 8), frameM);
    m.position.copy(a).addScaledVector(d, 0.5);
    m.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), d.normalize());
    g.add(m);
  }
  tube(0, 0.34, -0.56, 0, 0.86, -0.38, 0.028);  // seat stay
  tube(0, 0.34, -0.56, 0, 0.52, 0.05, 0.028);   // chain stay
  tube(0, 0.52, 0.05, 0, 0.86, -0.38, 0.03);    // seat tube->top
  tube(0, 0.52, 0.05, 0, 0.92, 0.42, 0.03);     // down tube
  tube(0, 0.92, 0.42, 0, 0.86, -0.38, 0.026);   // top tube
  tube(0, 0.34, 0.56, 0, 0.92, 0.42, 0.024);    // fork
  const bars = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.035, 0.035), dark);
  bars.position.set(0, 0.95, 0.42);
  g.add(bars);
  const seat = new THREE.Mesh(new THREE.BoxGeometry(0.16, 0.04, 0.24), dark);
  seat.position.set(0, 0.89, -0.38);
  g.add(seat);
  return { group: g, wF, wB };
}

/* ---------------- the factory ---------------- */
function makeCitizens(THREE, opts) {
  const rng = mulberry32(20261015);
  const group = new THREE.Group();
  const crowd = [];   // {kind, rig/bike, ...motion state}

  /* Activity areas: where each behavior lives. The defaults keep
   * the workshop geography; the page passes real city addresses
   * (from the city layout's meta) so the people live in the city. */
  const A = Object.assign({
    walk: { cx: 0, cz: 0, hw: 24, hd: 16 },
    run: { cx: 0, cz: 30, rx: 27, rz: 10 },
    bike: { cx: 0, cz: 0, hw: 33, hd: 23 },
    pitch: { cx: 30, cz: -22, hw: 9, hd: 6.5 },
  }, (opts && opts.areas) || {});
  function shifted(path, cx, cz) {
    return { length: path.length, at(s) { const p = path.at(s); return { x: p.x + cx, z: p.z + cz, dx: p.dx, dz: p.dz }; } };
  }

  /* WALKERS — sidewalk rectangle loop */
  const walkPath = shifted(rectPath(A.walk.hw, A.walk.hd), A.walk.cx, A.walk.cz);
  const N_WALK = 10;
  for (let i = 0; i < N_WALK; i++) {
    const rig = makeFigure(THREE, rng);
    group.add(rig.root);
    crowd.push({
      kind: 'walk', rig, path: walkPath,
      s: (i / N_WALK) * walkPath.length + rng() * 4,
      speed: 1.25 + rng() * 0.5, phase: rng() * Math.PI * 2,
    });
  }

  /* RUNNERS — oval around the park */
  const runPath = ellipsePath(A.run.cx, A.run.cz, A.run.rx, A.run.rz);
  const N_RUN = 6;
  for (let i = 0; i < N_RUN; i++) {
    const rig = makeFigure(THREE, rng, { bareShins: true });
    group.add(rig.root);
    crowd.push({
      kind: 'run', rig, path: runPath,
      a: (i / N_RUN) * Math.PI * 2 + rng() * 0.4,
      angSpeed: 0.16 + rng() * 0.05, phase: rng() * Math.PI * 2,
    });
  }

  /* BIKE RIDERS — the road loop */
  const bikePath = shifted(rectPath(A.bike.hw, A.bike.hd), A.bike.cx, A.bike.cz);
  const N_BIKE = 5;
  for (let i = 0; i < N_BIKE; i++) {
    const rig = makeFigure(THREE, rng);
    const bike = makeBike(THREE, rng);
    const holder = new THREE.Group();
    holder.add(bike.group);
    rig.root.position.set(0, 0.30, -0.30); // seated over the frame
    holder.add(rig.root);
    group.add(holder);
    crowd.push({
      kind: 'bike', rig, bike, holder, path: bikePath,
      s: (i / N_BIKE) * bikePath.length + rng() * 6,
      speed: 5.2 + rng() * 1.1, phase: rng() * Math.PI * 2,
    });
  }

  /* FOOTBALL — a pitch on the east green: jog, pass, kick */
  const pitch = { cx: A.pitch.cx, cz: A.pitch.cz, hw: A.pitch.hw, hd: A.pitch.hd };
  const N_FOOT = 8;
  const footies = [];
  for (let i = 0; i < N_FOOT; i++) {
    const team = i % 2;
    const rig = makeFigure(THREE, rng, {
      shirt: team ? 0xe8e4da : 0xc22e2e, pants: team ? 0x2e3a4a : 0xe8e4da, bareShins: true,
    });
    group.add(rig.root);
    const home = {
      x: pitch.cx + (rng() * 2 - 1) * pitch.hw,
      z: pitch.cz + (rng() * 2 - 1) * pitch.hd,
    };
    const c = {
      kind: 'foot', rig, x: home.x, z: home.z, tx: home.x, tz: home.z,
      phase: rng() * Math.PI * 2, wait: rng() * 1.5, speed: 2.6 + rng() * 0.9,
    };
    footies.push(c); crowd.push(c);
  }
  const ball = new THREE.Mesh(
    new THREE.SphereGeometry(0.16, 14, 12),
    new THREE.MeshStandardMaterial({ color: 0xf4f1ea, roughness: 0.5 })
  );
  ball.castShadow = true;
  group.add(ball);
  const game = { holder: footies[0], flying: null, rest: 1.2 };

  function newTarget(c) {
    c.tx = pitch.cx + (rng() * 2 - 1) * pitch.hw;
    c.tz = pitch.cz