<!-- NEW1 — TEST 1 · THE LIVING DISTRICT · part 443 -->
ll = new THREE.Mesh(
    new THREE.SphereGeometry(0.22, 10, 10),
    new THREE.MeshStandardMaterial({ color: 0xf2efe6, roughness: 0.6 })
  );
  ball.visible = false;
  group.add(ball);
  const ballState = { holder: null, flight: null, wait: 2 };

  function stepBall(dtReal) {
    const players = citizens.filter((c) => c.state === 'leisure' && c.leisureKind === 'football');
    if (!players.length) { ball.visible = false; ballState.holder = null; ballState.flight = null; return; }
    ball.visible = true;
    if (!ballState.holder || !players.includes(ballState.holder)) {
      ballState.holder = players[0]; ballState.flight = null; ballState.wait = 2 + rng() * 3;
    }
    if (ballState.flight) {
      const f = ballState.flight;
      f.t += dtReal / 0.8;
      if (f.t >= 1) { ballState.holder = f.toCitizen; ballState.flight = null; ballState.wait = 2 + rng() * 4; }
      else {
        ball.position.set(
          f.from.x + (f.to.x - f.from.x) * f.t,
          0.22 + Math.sin(Math.PI * f.t) * 1.1,
          f.from.z + (f.to.z - f.from.z) * f.t
        );
      }
      return;
    }
    const h = ballState.holder;
    ball.position.set(h.x + 0.45, 0.22, h.z + 0.3);
    ballState.wait -= dtReal;
    if (ballState.wait <= 0 && players.length > 1) {
      const others = players.filter((p) => p !== h);
      const to = others[Math.floor(rng() * others.length)];
      ballState.flight = {
        t: 0,
        from: { x: ball.position.x, z: ball.position.z },
        to: { x: to.x + 0.45, z: to.z + 0.3 },
        toCitizen: to,
      };
    }
  }

  function setVisible(c, v) {
    (c.holder || c.rig.root).visible = v;
  }
  function placeAt(c, x, z) {
    c.x = x; c.z = z;
    const o = c.holder || c.rig.root;
    o.position.x = x; o.position.z = z;
  }
  function faceTo(c, dx, dz) {
    (c.holder || c.rig.root).rotation.y = Math.atan2(dx, dz);
  }

  /* ---------- a citizen's day, assessed each midnight ----------
   * Schedules vary by the work's own nature: market folk start
   * early, office folk mid-morning, a few run the late shift. */
  function assess(c, day) {
    const kind = c.job.kind;
    let workStart;
    const r = rng();
    if (kind === 'market') workStart = r < 0.6 ? 330 + rng() * 60 : 720 + rng() * 60;   // early, or late shift
    else if (kind === 'parking') workStart = 360 + rng() * 120;
    else workStart = 450 + rng() * 90;                                                   // downtown / midrise
    const workLen = 420 + rng() * 120;
    const speed = c.mode === 'bike' ? gait.bike.speed * 3 : gait.walk.speed * 4;         // life pace (see header)
    const commute = route(c.home.door, c.jobPoint);
    const travelMin = commute.length / (speed / RATE);
    const errand = rng() < 0.45;
    const leisure = !errand ? rng() < 0.65 : rng() < 0.35;
    c.departedDay = day - 1; // this morning's departure is still owed
    return {
      day, workStart, workEnd: workStart + workLen,
      depart: Math.max(240, workStart - travelMin - 10),
      errand, leisure, speed,
      groceryRoute: errand ? route(c.jobPoint, grocery.center) : null,
    };
  }

  /* ---------- movement along a route ---------- */
  function startRoute(c, to, nextState, speedMul) {
    const r = route({ x: c.x, z: c.z }, to);
    c.path = r.pts; c.seg = 0; c.segT = 0;
    c.state = 'travel'; c.after = nextState;
    c.speedMul = speedMul || 1;
    setVisible(c, true);
  }
  function stepTravel(c, dtGame, dtReal) {
    const base = c.mode === 'bike' ? gait.bike.speed * 3 : gait.walk.speed * 4;
    const speed = base * (c.speedMul || 1);                 // units per REAL second
    let dist = speed * dtReal;
    while (dist > 0 && c.seg < c.path.length - 1) {
      const a = c.path[c.seg], b = c.path[c.seg + 1];
      const segLen = Math.hypot(b.x - a.x, b.z - a.z) || 0.0001;
      const remain = segLen * (1 - c.segT);
      const step = Math.min(dist, remain);
      c.segT += step / segLen; dist -= step;
      const x = a.x + (b.x - a.x) * c.segT, z = a.z + (b.z - a.z) * c.segT;
      faceTo(c, b.x - a.x, b.z - a.z);
      placeAt(c, x, z);
      if (c.segT >= 1) { c.seg++; c.segT = 0; }
    }
    const g = c.mode === 'bike' ? gait.bike : gait.walk;
    c.phase += speed * dtReal * g.phasePerUnit;
    if (c.mode === 'bike') {
      poses.bike(c.rig, c.phase, 1);
      const spin = speed * dtReal / 0.34;
      c.bike.wF.rotation.x += spin; c.bike.wB.rotation.x += spin;
    } else poses.walk(c.rig, c.phase, 1);
    if (c.seg >= c.path.length - 1) { c.path = null; arrive(c); }
  }
  function arrive(c) {
    const s = c.after;
    if (s === 'work') { c.state = 'work'; c.workMicro = null; c.timer = 0; }
    else if (s === 'errand') { c.state = 'errand'; c.timer = 15 + rng() * 15; }
    else if (s === 'leisure') beginLeisure(c);
    else if (s === 'home') { c.state = 'sleep'; setVisible(c, false); }
  }

  /* ---------- leisure, in the park's geography ---------- */
  functi