<!-- SOLID CITY · part 117 -->
tizen ---------------- */
var group = new T.Group(); A.world.add(group);   // world space = absolute coords (floating origin handled by world)
var active = [], all = [];
function build(c) {
  var sh = bodyGeo(c.body, c.hat), g = new T.Group();
  c.glowMat = new T.MeshBasicMaterial({ color: c.accent });
  g.add(new T.Mesh(sh.body, bodyMat)); g.add(new T.Mesh(sh.glow, c.glowMat));
  var tw = c.body === 'slim' ? 0.5 : c.body === 'stout' ? 0.74 : c.body === 'bot' ? 0.66 : 0.6;
  c.limbs = [];
  [-1, 1].forEach(function (s) {
    var arm = new T.Mesh(limbGeo.arm, bodyMat); arm.position.set((tw / 2 + 0.08) * s, 1.48, 0); g.add(arm);
    var leg = new T.Mesh(limbGeo.leg, bodyMat); leg.position.set(0.15 * s, 0.76, 0); g.add(leg);
    c.limbs.push(arm, leg);
  });
  var embTex = new T.CanvasTexture(emblemCanvas(c, null));
  var emb = new T.Mesh(new T.PlaneGeometry(0.28, 0.28), new T.MeshBasicMaterial({ map: embTex, transparent: true }));
  emb.position.set(0, 1.25, (c.body === 'stout' ? 0.42 : c.body === 'bot' ? 0.48 : 0.32) / 2 + 0.012); g.add(emb); c.emb3 = emb;
  var tagTex = new T.CanvasTexture(tagCanvas(c, null)); tagTex.minFilter = T.LinearFilter;
  var tag = new T.Sprite(new T.SpriteMaterial({ map: tagTex, depthWrite: false, transparent: true }));
  tag.scale.set(2.3, 0.69, 1); tag.position.y = 2.75; tag.renderOrder = 3; g.add(tag); c.tag = tag;
  var hit = new T.Mesh(hitGeo, hitMat); hit.userData.cz = c; g.add(hit); c.hit = hit;
  if (c.img) loadImg(c.img, function (im) { tagCanvas(c, im); tagTex.needsUpdate = true; emblemCanvas(c, im); embTex.needsUpdate = true; });
  if (c.id === 'kingplaya') kingAvatar(c, g);
  g.scale.setScalar(c.sc || 1); g.visible = false; group.add(g); c.g = g;
}
/* King Playa's own avatar (public/city/avatar-king-playa.js, by King Playa, used as-is).
   Lazy import; any failure keeps the stock citizen look. Tag, hit box and bubble stay ours. */
function kingAvatar(c, g) {
  import('/city/avatar-king-playa.js?v=20261010a').then(function (m) {
    var make = m.createKingPlayaAvatar || m.default, av = make({ THREE: T, curve: A.curve }), u = av.userData || {};
    if (!u.limbs || u.limbs.length !== 4 || !u.emblem) throw new Error('unexpected avatar shape');
    var keep = [c.tag, c.hit, c.bub], oldEmb = c.emb3;
    g.children.slice().forEach(function (o) { if (keep.indexOf(o) < 0) g.remove(o); });
    if (oldEmb) { if (oldEmb.material.map) oldEmb.material.map.dispose(); oldEmb.material.dispose(); oldEmb.geometry.dispose(); }
    av.children.slice().forEach(function (o) { g.add(o); });
    c.shipMat = c.shipMat || c.glowMat;           // ship trim keeps the solid accent colour
    c.limbs = u.limbs; c.emb3 = u.emblem; if (u.glowMat) c.glowMat = u.glowMat; c.avatar = 'king-playa';
  }).catch(function (e) { c.avatar = 'stock'; console.warn('[citizens] King Playa avatar unavailable, keeping the stock look:', e && e.message); });
}
function buildShip(c) {
  var s = new T.Group(); s.add(new T.Mesh(shipGeo.hull, bodyMat)); s.add(new T.Mesh(shipGeo.trim, c.shipMat || c.glowMat));
  var f = new T.Mesh(shipGeo.flame, flameMat); s.add(f); s.userData.flame = f;
  s.scale.setScalar(MOBILE ? 0.9 : 1); s.visible = false; group.add(s); c.ship = s;
}

/* ---------------- simulation ---------------- */
var t0 = Date.now() / 1000;
function init() {
  ROSTER.forEach(function (c, n) {
    if (c.fixed) { c.x = A.SABLE_POS.x; c.z = A.SABLE_POS.z; c.y = Y0; c.st = 'idle'; return; }
    // deterministic start from the clock, so every visitor sees roughly the same city
    var cyc = c.sched.reduce(function (a, s) { return a + s[1] + 90; }, 0), ph = ((t0 + n * 137) % cyc) / cyc;
    c.i = Math.floor(ph * c.sched.length) % c.sched.length;
    var st = STOPS[c.sched[c.i][0]], j = (n * 7) % 5;
    c.x = st[0] + (j - 2) * 1.6; c.z = st[1] + ((n * 3) % 5 - 2) * 1.6; c.y = A.groundAt(c.x, c.z);
    c.face = n; c.st = 'dwell'; c.t = rnd(4, c.sched[c.i][1]); c.walkPh = 0; c.idle = 0; c.idleK = ''; c.greetT = -99; c.stuck = 0;
    c.lx = c.x; c.lz = c.z; c.vis = false;
    build(c);
  });
  all = ROSTER.filter(function (c) { return !c.fixed; });
}
function where(c) { return A.whereName(c.x, c.z) || 'Solid City'; }
function doing(c) {
  if (c.fixed) return 'standing in the Forum Plaza';
  if (c.st === 'fly') return 'flying to ' + STOPS[c.dest][2];
  if (c.st === 'walk') return 'walking to ' + STOPS[c.dest][2];
  return 'hanging out at ' + STOPS[c.sched[c.i][0]][2];
}
function nextLeg(c) {
  var from = c.sched[c.i][0]; c.i = (c.i + 1) % c.sched.length;
  var s = c.sched[c.i], to = s[0], st = STOPS[to], d = Math.hypot(st[0] - c.x, st[1] - c.z);
  c.dest = to; c.tx = st[0] + rnd(-2.5, 2.5); c.tz = st[1] + rnd(-2.5, 2.5);
  var r = s[2] === 'F' ? null : route(from, to);
  if (s[2] === 'F' || (!r && d > 160)) startFly(c);
  else { c.st = 'walk'; c.path = (r || []).concat([[c.tx, c.tz]]); c.pi = 0; c.stuck = 0; c.stuckT = 0; }
  if (Math.random() < 0.5) radioEvent(c);
}
function startFly(c) {
  c.