<!-- SOLID CITY · part 66 -->
ds, hyper.js walks them
   (AI Drive + citizens' office hours follow the roads), and this file adds:
     roads, lamps, sign posts and the University arch (one merged mesh per material, ~7 draw calls for everything),
     a far proxy for every hall (1 draw call) + the detailed landmark hall per campus, lazy-built when you come within ~950 m,
     collision, Hyper stops ("drive me to sciences"), radar markers, the Watch CAMPUS TOUR (follows the roads),
     the Course Catalog / lesson reader / Ask the professor / Library panels, and /city/?to=university|<campus>.
   iPhone first: everything lazy, merged, no downloads but curriculum.json; any error leaves the city as it was. */
(function () {
'use strict';
if (!window.SC || !SC.api || !window.CampusMap) return;
var A = SC.api, T = A.T, S = A.S, Y = A.CITY_Y, CM = window.CampusMap, CAMPUS = CM.CAMPUS;
var QS = new URLSearchParams(location.search);
var U = { state: 'idle', cur: null, built: false, halls: {}, wait: 1.5, said: {}, open: null, me: null, csrf: '', hostName: {} };
var HUB = { x: Math.cos(5 * Math.PI / 180) * CM.RING, z: Math.sin(5 * Math.PI / 180) * CM.RING };
var HUB_STAND = { x: HUB.x + Math.sin(5 * Math.PI / 180) * 36, z: HUB.z - Math.cos(5 * Math.PI / 180) * 36 };   // on the Ring road, 36 m before the arch, looking through it
var ANCHOR = {   // the existing district each road starts from (same coordinates as the citizens' stops)
  sciences: { x: 10, z: -262, n: 'the north road' }, mathcs: { x: 80, z: -110, n: 'the Forge yard' }, engineering: { x: 372, z: -140, n: 'the Music Studio gate' },
  arts: { x: 455, z: -112, n: 'the Studio lounge' }, humanities: { x: 700, z: 165, n: 'Provolta\u2019s Sanctuary' }, medicine: { x: 172, z: 152, n: 'Soul Print Tower' },
  social: { x: 60, z: 280, n: 'the south quarter' }, business: { x: -345, z: 308, n: 'the Spaceport apron' } };
var HOSTS = { sciences: 'grok', mathcs: 'playa2', engineering: 'solid', arts: 'music', humanities: 'provolta', medicine: 'fren', social: 'mony', business: 'sable' };
function hostOf(c) { return c.host || HOSTS[c.id]; }
var RADAR = { sciences: 'SCI', mathcs: 'CS', engineering: 'ENG', arts: 'ART', humanities: 'HUM', medicine: 'MED', social: 'LAW', business: 'BIZ' };
var ALIAS = { sciences: 'science physics chemistry biology astronomy', mathcs: 'math maths mathematics computer cs ai programming', engineering: 'engineering architecture engineer',
  arts: 'arts art', humanities: 'humanities history philosophy languages literature writing', medicine: 'medicine health medical', social: 'social law economics psychology government civics',
  business: 'business communications comms media marketing' };
function $(id) { return document.getElementById(id); }
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]; }); }
var curve = A.curve || function (m) { return m; };

/* ======================= geometry helpers ======================= */
function Merger() { this.parts = []; }
Merger.prototype.add = function (geo, x, y, z, rx, ry, rz, sx, sy, sz) {
  var g = geo.index ? geo.toNonIndexed() : geo.clone();
  g.applyMatrix4(new T.Matrix4().compose(new T.Vector3(x || 0, y || 0, z || 0), new T.Quaternion().setFromEuler(new T.Euler(rx || 0, ry || 0, rz || 0)), new T.Vector3(sx || 1, sy || sx || 1, sz || sx || 1)));
  if (!g.attributes.uv) g.setAttribute('uv', new T.BufferAttribute(new Float32Array(g.attributes.position.count * 2), 2));
  this.parts.push(g); return this;
};
Merger.prototype.raw = function (pos, uv) {   // plain triangle soup, normals up
  var g = new T.BufferGeometry(); g.setAttribute('position', new T.BufferAttribute(new Float32Array(pos), 3));
  var n = new Float32Array(pos.length); for (var i = 1; i < n.length; i += 3) n[i] = 1; g.setAttribute('normal', new T.BufferAttribute(n, 3));
  g.setAttribute('uv', new T.BufferAttribute(new Float32Array(uv || new Array(pos.length / 3 * 2).fill(0)), 2)); this.parts.push(g); return this;
};
Merger.prototype.mesh = function (mat) {
  if (!this.parts.length) return null;
  var n = 0; this.parts.forEach(function (p) { n += p.attributes.position.count; });
  var P = new Float32Array(n * 3), N = new Float32Array(n * 3), UV = new Float32Array(n * 2), o = 0;
  this.parts.forEach(function (p) { var c = p.attributes.position.count; P.set(p.attributes.position.array, o * 3); N.set(p.attributes.normal.array, o * 3); UV.set(p.attributes.uv.array, o * 2); o += c; p.dispose(); });
  var g = new T.BufferGeometry(); g.setAttribute('position', new T.BufferAttribute(P, 3)); g.setAttribute('normal', new T.BufferAttribute(N, 3)); g.setAttribute('uv', new T.BufferAttribute(UV, 2));
  g.computeBoundingSphere(); this.parts = []; return new T.Mesh(g, mat);
};
function densify(pts, step) {
  var out = [pts[0]];
  for (var i = 1; i < pts.length; i++) { var a = pts[i - 1], b = pts[i], d = Math.hypot(b.x - a.x, b.z - a.z), n 