<!-- SOLID CITY · part 70 -->
.add(new T.TorusGeometry(1.1, 0.16, 4, 16), 20, 4 + i * 2.6, 14, Math.PI / 2, 0, i * 0.5);
    gd.add(new T.SphereGeometry(1.2, 10, 8), 20, 20.5, 14);
  } else if (k === 'rotunda') {
    C(dk, 13, 13, 12, 0, 6.6, 0, 28);
    for (i = 0; i < 16; i++) { a = i / 16 * Math.PI * 2; C(st, 0.8, 0.9, 11, Math.sin(a) * 17, 6.1, Math.cos(a) * 17, 10); }
    C(gd, 18, 18, 1.2, 0, 12.2, 0, 32); C(st, 15, 17, 3, 0, 14.3, 0, 28); C(st, 11, 14, 3, 0, 17.2, 0, 24); st.add(new T.SphereGeometry(10.5, 24, 10, 0, Math.PI * 2, 0, Math.PI / 2), 0, 18.6, 0);
    C(gd, 0.3, 0.3, 4, 0, 30.5, 0, 6); B(gd, 6, 0.3, 0.3, 0, 32.4, 0); C(gd, 1, 1, 0.2, -3, 31.2, 0, 12); C(gd, 1, 1, 0.2, 3, 31.2, 0, 12);
    for (i = 0; i < 8; i++) { a = (i + 0.5) / 8 * Math.PI * 2; B(gl, 2, 5, 0.3, Math.sin(a) * 13.1, 6, Math.cos(a) * 13.1, a); }
  } else if (k === 'towers') {
    B(dk, 12, 40, 12, -9, 20.6, -2); B(dk, 12, 32, 12, 9, 16.6, -2); B(gd, 13, 1.2, 13, -9, 40.6, -2); B(gd, 13, 1.2, 13, 9, 32.6, -2);
    for (i = 0; i < 9; i++) { B(gl, 12.2, 1, 12.2, -9, 4 + i * 4, -2); if (i < 7) B(gl, 12.2, 1, 12.2, 9, 4 + i * 4, -2); }
    B(st, 6.2, 3, 4, 0, 24, -2); C(dk, 0.25, 0.4, 12, -9, 47, -2, 6); gd.add(new T.SphereGeometry(0.7, 8, 6), -9, 53.4, -2);
    gd.add(new T.ConeGeometry(4, 2, 18, 1, true), 9, 36, -2, -0.6, 0, 0);
  }
  // name plate + office-hours podium on the forecourt side (local +z faces the city)
  var host = HOSTNAME(hostOf(c));
  var cv = document.createElement('canvas'); cv.width = 1024; cv.height = 256; var g = cv.getContext('2d');
  g.fillStyle = '#0b0709'; g.fillRect(0, 0, 1024, 256); g.strokeStyle = '#c99a3a'; g.lineWidth = 10; g.strokeRect(8, 8, 1008, 240);
  g.textAlign = 'center'; g.textBaseline = 'middle'; g.fillStyle = '#e8c36a'; g.font = '700 30px system-ui, sans-serif'; g.fillText('SOLID CITY UNIVERSITY', 512, 52);
  var nm = c.name.toUpperCase(), fs = 70; g.font = '700 ' + fs + 'px Georgia, serif'; while (g.measureText(nm).width > 960 && fs > 34) { fs -= 4; g.font = '700 ' + fs + 'px Georgia, serif'; }
  g.fillStyle = '#ffffff'; g.fillText(nm, 512, 128); g.fillStyle = '#ff4d5e'; g.font = '700 30px system-ui, sans-serif'; g.fillText('COURSE CATALOG \u00b7 LIBRARY \u00b7 OFFICE HOURS: ' + host.toUpperCase(), 512, 204);
  var tex = new T.CanvasTexture(cv); tex.encoding = T.sRGBEncoding; tex.anisotropy = 4;
  var plate = new T.Mesh(new T.PlaneGeometry(18, 4.5), curve(new T.MeshBasicMaterial({ map: tex, side: T.DoubleSide })));
  plate.position.set(0, 6.5, 33); var stand = new T.Group(); stand.add(plate);
  B(dk, 0.5, 6.5, 0.5, -8.6, 3.25, 33); B(dk, 0.5, 6.5, 0.5, 8.6, 3.25, 33); B(gd, 18.6, 0.4, 0.6, 0, 8.9, 33);
  B(gd, 1.6, 1.2, 1.0, -12, 0.6, 52); B(dk, 1.1, 0.2, 0.8, -12, 1.3, 52);     // office-hours podium
  var grp = new T.Group(); grp.name = 'hall-' + c.id; grp.position.set(c.x, Y, c.z); grp.rotation.y = c.face;
  [[dk, M.dark], [gd, M.gold], [rd, M.red], [gl, M.glass], [st, M.stone]].forEach(function (p) { var m = p[0].mesh(p[1]); if (m) grp.add(m); });
  grp.add(stand); A.world.add(grp); return grp;
}
function addHallSolids(c) {   // rotated local boxes -> world AABBs (conservative)
  var cs = Math.cos(c.face), sn = Math.sin(c.face);
  hallSolids(c.hall).forEach(function (b) {
    var wx = c.x + b[0] * cs + b[1] * sn, wz = c.z - b[0] * sn + b[1] * cs, hx = Math.abs(cs) * b[2] / 2 + Math.abs(sn) * b[3] / 2, hz = Math.abs(sn) * b[2] / 2 + Math.abs(cs) * b[3] / 2;
    A.solids.push({ x0: wx - hx, x1: wx + hx, z0: wz - hz, z1: wz + hz, y0: Y, y1: Y + b[4], campus: true });
  });
}

/* ======================= curriculum + hosts ======================= */
var CUR = null;
function curriculum() { if (CUR) return Promise.resolve(CUR); return fetch('/city/curriculum.json?v=20261010g', { cache: 'force-cache' }).then(function (r) { return r.json(); }).then(function (j) { CUR = j; return j; }); }
function HOSTNAME(id) { if (id === 'sable') return 'SABLE'; var c = window.CityCitizens && CityCitizens.get(id); return (c && c.name) || id; }
function campusData(id) { return CUR && CUR.campuses.filter(function (c) { return c.id === id; })[0]; }

/* ======================= Hyper stops, Watch CAMPUS TOUR, radar ======================= */
function plen(p) { var l = 0; for (var i = 1; i < p.length; i++) l += Math.hypot(p[i].x - p[i - 1].x, p[i].z - p[i - 1].z); return l; }
function roadRoute(c) {   // Hyper AI Drive to a campus from inside the capital: walk to the nearest point of its University road, then follow the road
  return function (from) {
    if (Math.hypot(from.x, from.z) > 760 || !window.CityHyper) return null;   // outside the walls only the roads are walkable anyway
    var inner = ROADS.filter(function (r) { return r.id === c.id && r.kind === 'inner'; })[0]; if (!inner) return null;
    var L = inner.pts.concat([c.radial[1], c.front]), best = 1e9, bi = 0, bq = null;
    for (var i = 1; i < L.length - 1; i++) {
      var a = L[i - 1], b = L[i], dx = b.x - a.x, dz = b.z - a.z, l2 = dx * dx +