<!-- NEW5 — TEST 5 · THE BEND AT THE HEART · part 6 -->
HREE.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 + (rng() * 2 - 1) * pitch.hd;
  }

  /* ---------------- per-frame nerves ---------------- */
  function update(dt /*, t */) {
    dt = Math.min(dt, 0.05);
    for (const c of crowd) {
      if (c.kind === 'walk') {
        c.s += c.speed * dt;
        c.phase += dt * c.speed * 3.4;
        const p = c.path.at(c.s);
        c.rig.root.position.set(p.x, 0, p.z);
        faceToward(c.rig.root, p.dx, p.dz);
        poseGait(c.rig, c.phase, 0.55, 0.06, 0.7);
      } else if (c.kind === 'run') {
        c.a += c.angSpeed * dt;
        c.phase += dt * 9.2;
        const p = c.path.at(c.a);
        c.rig.root.position.set(p.x, 0, p