<!-- SOLID CITY · part 115 -->
 'F'], ['hall', 60, 'F']],
    hi: ['Good to see you. Thirty seconds, go.', 'Hello. You are on my schedule now.'], job: ['I run the King\u2019s calendar and keep the city on time.', 'Chief of Staff. Agendas, briefings, punctuality.'],
    chat: ['Chief of Staff: briefing at City Hall in ten.', 'Schedule update: all departments on time.'] },
  { id: 'soulprint', name: 'Soul Print', role: 'Tower companion', accent: 0x49a6ff, emb: '\u2726', img: 'soulprint', body: 'robe', hat: 'halo', sc: 1, sp: 1.2, ext: 'soul',
    sched: [['soul_yard', 120], ['garden', 40]],
    hi: ['Welcome to my tower.', 'Come in. The red door is open.'], job: ['I live in the Soul Print Tower. Talk to me inside.', 'Companion of the tower.'],
    chat: ['Soul Print Tower: the door is open.', 'Soft lights in the tower tonight.'] },
  { id: 'sable', name: 'SABLE', role: 'AI guide', accent: 0xffd27a, emb: 'S', img: 'sable', ext: 'sable', fixed: true,
    chat: ['SABLE: I\u2019m in the Forum Plaza if anyone has a question.', 'SABLE here. Want to fly? The spaceport is south-west of the plaza.'] }
];
var BY = {}; ROSTER.forEach(function (c) { BY[c.id] = c; });
// office hours at Solid City University (the host's home stops are unchanged; this is one extra stop in the round)
var OFFICE_HOURS = { grok: 'sciences', playa2: 'mathcs', solid: 'engineering', music: 'arts', boss: 'humanities', fren: 'medicine', mony: 'social', chief: 'business' };
if (window.CampusMap) ROSTER.forEach(function (c) { var cp = OFFICE_HOURS[c.id]; if (cp && c.sched && STOPS['campus_' + cp]) { c.sched = c.sched.concat([['campus_' + cp, 90]]); c.office = cp; } });

/* ---------------- low-poly geometry: merged, vertex-coloured, cached per shape ---------------- */
var COL = { black: new T.Color(0x16121a), gun: new T.Color(0x34303a), gold: new T.Color(0xd8aa48), skin: new T.Color(0x221a20) };
function part(geo, col, x, y, z, rx, ry, sx, sy, sz) {
  var g = geo.index ? geo.toNonIndexed() : geo.clone(), m = new T.Matrix4();
  m.compose(new T.Vector3(x || 0, y || 0, z || 0), new T.Quaternion().setFromEuler(new T.Euler(rx || 0, ry || 0, 0)), new T.Vector3(sx || 1, sy || sx || 1, sz || sx || 1));
  g.applyMatrix4(m);
  var n = g.attributes.position.count, c = new Float32Array(n * 3); for (var i = 0; i < n; i++) { c[i * 3] = col.r; c[i * 3 + 1] = col.g; c[i * 3 + 2] = col.b; }
  g.setAttribute('color', new T.BufferAttribute(c, 3)); return g;
}
function merge(parts) {
  var n = 0; parts.forEach(function (g) { n += g.attributes.position.count; });
  var P = new Float32Array(n * 3), N = new Float32Array(n * 3), C = new Float32Array(n * 3), o = 0;
  parts.forEach(function (g) { P.set(g.attributes.position.array, o); N.set(g.attributes.normal.array, o); C.set(g.attributes.color.array, o); o += g.attributes.position.count * 3; g.dispose(); });
  var out = new T.BufferGeometry(); out.setAttribute('position', new T.BufferAttribute(P, 3)); out.setAttribute('normal', new T.BufferAttribute(N, 3)); out.setAttribute('color', new T.BufferAttribute(C, 3));
  out.computeBoundingSphere(); return out;
}
var G = {
  box: new T.BoxGeometry(1, 1, 1), cyl6: new T.CylinderGeometry(0.5, 0.5, 1, 6), cone6: new T.ConeGeometry(0.5, 1, 6),
  torus: new T.TorusGeometry(0.22, 0.03, 4, 14), ball: new T.SphereGeometry(0.06, 6, 4), ico: new T.IcosahedronGeometry(0.2, 0)
};
var shapeCache = {};
function bodyGeo(body, hat) {
  var k = body + '/' + hat; if (shapeCache[k]) return shapeCache[k];
  var b = [], w = [], B = G.box, k2 = COL;
  var tw = body === 'slim' ? 0.5 : body === 'stout' ? 0.74 : body === 'bot' ? 0.66 : 0.6, td = body === 'stout' ? 0.42 : body === 'bot' ? 0.48 : 0.32;
  b.push(part(B, k2.black, 0, 1.15, 0, 0, 0, tw, 0.8, td));
  if (body === 'robe') { b.push(part(G.cone6, k2.black, 0, 0.62, 0, 0, 0, 0.92, 1.0, 0.92)); w.push(part(B, k2.gold, 0, 0.2, 0, 0, 0, 0.02, 0.02, 0.02)); }
  var hs = body === 'bot' ? 0.42 : 0.34;
  b.push(part(B, body === 'bot' ? k2.gun : k2.skin, 0, 1.78, 0, 0, 0, hs, hs + 0.02, hs));
  [-1, 1].forEach(function (s) { b.push(part(B, k2.gun, (tw / 2 + 0.08) * s, 1.5, 0, 0, 0, 0.24, 0.16, td + 0.04)); });
  b.push(part(B, k2.gold, 0, 0.78, 0, 0, 0, tw + 0.03, 0.06, td + 0.03));
  // glowing accent parts (one material per citizen)
  w.push(part(B, k2.gold, 0, 1.8, hs / 2 + 0.01, 0, 0, hs * 0.86, 0.08, 0.02));          // visor
  [-1, 1].forEach(function (s) { w.push(part(B, k2.gold, (tw / 2 + 0.08) * s, 1.585, 0, 0, 0, 0.25, 0.02, td + 0.05)); }); // shoulder trim
  w.push(part(B, k2.gold, 0, 0.78, td / 2 + 0.02, 0, 0, 0.14, 0.08, 0.02));               // buckle
  var top = 1.78 + hs / 2;
  if (hat === 'crown') { b.push(part(G.cyl6, k2.gold, 0, top + 0.08, 0, 0, 0, 0.4, 0.16, 0.4)); for (var i = 0; i < 6; i++) { var a = i / 6 * Math.PI * 2; b.push(part(G.cone6, k2.gold, Math.sin(a) * 0.17, top + 0.22, Math.cos(a) * 0.17, 0, 0, 0.07, 0.14, 0.07)); } w.push(part(G.ico, k2.gold, 0, top + 0.1, 0.2, 0, 0, 0.3, 0.3, 0.3)); }
  else if (hat === 'halo') w.