<!-- NEW1 — TEST 1 · THE LIVING DISTRICT · part 1 -->
===== FILE: public/new1/citizens.js =====
/* citizens.js — THE NERVESMITH's population for the King's /new1 test.
 *
 * Computer-generated citizens whose motion is OUR NEW NERVES: every figure
 * is a jointed humanoid (hips, spine, head, shoulders, elbows, knees) and
 * every pose is COMPUTED from joint angles each frame — procedural nerves,
 * no keyframes, no assets, no network. THREE is handed in by the page.
 *
 * Feet stay honest at local y=0; the page's invisible stand layer does the
 * rest. Deterministic: one seed grows the whole crowd the same way, always.
 *
 * Behaviors: WALKERS on a sidewalk loop, RUNNERS on a park oval, BIKE RIDERS
 * pedaling a road loop, and a FOOTBALL GAME (jog, pass, kick) on a pitch.
 *
 * Also exported in pieces for the LIFE DIRECTOR, which moves rigs itself
 * and poses them with this same craft: makeFigure, makeBike, poses, gait.
 */
'use strict';

/* ---------------- deterministic seed ---------------- */
function mulberry32(a) {
  return function () {
    a |= 0; a = (a + 0x6D2B79F5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

/* ---------------- palettes ---------------- */
const SKIN = [0xf2c79b, 0xd9a066, 0xb57a4a, 0x8a5a33, 0x6e4525, 0xffd9b3];
const SHIRTS = [0xc94f4f, 0x4f79c9, 0x53a06a, 0xd9a441, 0x7a5fc0, 0xe8e4da,
                0x3d8f8a, 0xd96c9e, 0x5b6670, 0xe07b39, 0x8fae4c, 0x94543a];
const PANTS = [0x2e3a4a, 0x4a3b2c, 0x606a75, 0x7a2e35, 0x2c4a3e, 0x8a8f96, 0x3a3f8f];
const HAIR = [0x1c1512, 0x3a2617, 0x6e4525, 0xb08a4f, 0xd9d4c8, 0x8a2e2e];

function pick(rng, arr) { return arr[Math.floor(rng() * arr.length)]; }
function mat(THREE, color) { return new THREE.MeshStandardMaterial({ color, roughness: 0.85, metalness: 0.0 }); }

/* A capsule hanging DOWN from its pivot (pivot at the joint, limb below). */
function limb(THREE, len, r, material) {
  const g = new THREE.CapsuleGeometry(r, Math.max(0.01, len - 2 * r), 3, 10);
  g.translate(0, -len / 2, 0);
  const m = new THREE.Mesh(g, material);
  m.castShadow = true;
  return m;
}

/* ---------------- the jointed figure (the nerves) ----------------
 * Rig groups (pivots): hips -> spine -> head, shoulderL/R -> elbowL/R,
 * thighL/R -> kneeL/R. Poses are pure functions of joint angles. */
function makeFigure(THREE, rng, opts) {
  opts = opts || {};
  const shirt = mat(THREE, opts.shirt !== undefined ? opts.shirt : pick(rng, SHIRTS));
  const pants = mat(THREE, opts.pants !== undefined ? opts.pants : pick(rng, PANTS));
  const skin = mat(THREE, pick(rng, SKIN));
  const hairM = mat(THREE, pick(rng, HAIR));
  const scale = opts.scale || (0.94 + rng() * 0.12);

  const LEG = 0.92;            // hip height: thigh 0.47 + shin 0.45
  const root = new THREE.Group();

  const hips = new THREE.Group();
  hips.position.y = LEG;
  root.add(hips);

  const pelvis = new THREE.Mesh(new THREE.BoxGeometry(0.30, 0.17, 0.19), pants);
  pelvis.castShadow = true;
  hips.add(pelvis);

  const spine = new THREE.Group();
  spine.position.y = 0.08;
  hips.add(spine);

  const torso = new THREE.Mesh(new THREE.CapsuleGeometry(0.155, 0.34, 4, 12), shirt);
  torso.position.y = 0.30;
  torso.castShadow = true;
  spine.add(torso);

  const headG = new THREE.Group();
  headG.position.y = 0.62;
  spine.add(headG);
  const head = new THREE.Mesh(new THREE.SphereGeometry(0.115, 18, 14), skin);
  head.position.y = 0.10;
  head.castShadow = true;
  headG.add(head);
  const hair = new THREE.Mesh(new THREE.SphereGeometry(0.118, 18, 12, 0, Math.PI * 2, 0, Math.PI * 0.55), hairM);
  hair.position.y = 0.115;
  headG.add(hair);

  function arm(side) { // side: -1 left, +1 right
    const sh = new THREE.Group();
    sh.position.set(0.215 * side, 0.52, 0);
    spine.add(sh);
    sh.add(limb(THREE, 0.30, 0.05, shirt));
    const el = new THREE.Group();
    el.position.y = -0.30;
    sh.add(el);
    el.add(limb(THREE, 0.28, 0.042, skin));
    return { sh, el };
  }
  function leg(side) {
    const th = new THREE.Group();
    th.position.set(0.10 * side, -0.03, 0);
    hips.add(th);
    th.add(limb(THREE, 0.47, 0.068, pants));
    const kn = new THREE.Group();
    kn.position.y = -0.47;
    th.add(kn);
    kn.add(limb(THREE, 0.45, 0.052, opts.bareShins ? skin : pants));
    const foot = new THREE.Mesh(new THREE.BoxGeometry(0.09, 0.05, 0.22), mat(THREE, 0x2b2b2e));
    foot.position.set(0, -0.435, 0.05);
    kn.add(foot);
    return { th, kn };
  }
  const rig = {
    root, hips, spine, headG,
    armL: arm(-1), armR: arm(1),
    legL: leg(-1), legR: leg(1),
    LEG,
  };
  root.scale.setScalar(scale);
  return rig;
}

/* Pose the nerves for a gait. phase advances with distance; amp = stride
 * size; pump = arm energy; lean = forward tilt. Arms swing AGAINST legs. */
function poseGait(rig, phase, amp, lean, pump) {
  const s = Math.sin(phase), c = Math.cos(phase);
  rig.legL.th.rotatio