<!-- SOLID CITY · part 71 -->
 dz * dz || 1, t = Math.max(0, Math.min(1, ((from.x - a.x) * dx + (from.z - a.z) * dz) / l2));
      var q = { x: a.x + dx * t, z: a.z + dz * t }, d = Math.hypot(from.x - q.x, from.z - q.z); if (d < best) { best = d; bi = i; bq = q; }
    }
    var head = best < 3 ? [{ x: from.x, z: from.z }] : CityHyper.findPath(from, bq); if (!head) return null;
    var via = head.concat(L.slice(bi)), direct = CityHyper.findPath(from, c.front);
    return direct && plen(via) > plen(direct) * 1.5 + 60 ? null : via;
  };
}
function hyperStops() {
  var H = window.CityHyper; if (!H || !H.pois) return;
  CAMPUS.forEach(function (c) {
    if (H.pois.some(function (p) { return p.id === 'campus-' + c.id; })) return;
    H.pois.push({ id: 'campus-' + c.id, name: c.name + ' campus', short: c.short, x: c.x, z: c.z, stand: { x: c.front.x, z: c.front.z }, campus: c.id, route: roadRoute(c),
      alias: ALIAS[c.id], text: 'Solid City University \u00b7 ' + c.name + '. Walk up to the hall to open its Course Catalog and Library.' });
  });
  if (!H.pois.some(function (p) { return p.id === 'university'; }))
    H.pois.push({ id: 'university', name: 'Solid City University', short: 'UNIVERSITY', x: HUB.x, z: HUB.z, stand: { x: HUB_STAND.x, z: HUB_STAND.z },
      alias: 'university uni campus campuses college school learn', text: 'The University arch on the Ring: eight campuses, one per branch of higher learning, each with courses and a library.' });
  if (H.regrid) H.regrid();
}
function watchTour() {
  var W = window.CityWatch; if (!W || !W.routes || W.routes.campus) return;
  W.routes.campus = { name: 'CAMPUS TOUR', vk: 1, blurb: 'Solid City University: along the roads to all 8 campuses', build: function () {
    var w = [], c0 = CAMPUS[0], inner = ROADS.filter(function (r) { return r.id === c0.id && r.kind === 'inner'; })[0];
    w.push({ x: 0, z: 120, y: Y + 160, v: 160, cap: { cap: 'CAMPUS TOUR', sub: 'Solid City University \u00b7 the roads out of the capital', say: 'Campus tour. We follow the University roads out of the capital.' } });
    if (inner) inner.pts.filter(function (q, i) { return i % 2 === 0; }).forEach(function (q) { w.push({ x: q.x, z: q.z, y: Y + 130, v: 140, cap: null }); });
    w.push({ x: c0.gate.x, z: c0.gate.z, y: Y + 130, v: 140, cap: null });
    for (var d = c0.deg; d <= CAMPUS[CAMPUS.length - 1].deg + 0.1; d += 7.5) {
      var hit = CAMPUS.filter(function (c) { return Math.abs(c.deg - d) < 0.1; })[0];
      w.push({ x: Math.cos(d * Math.PI / 180) * CM.RING, z: Math.sin(d * Math.PI / 180) * CM.RING, y: Y + (hit ? 150 : 170), v: 150,
        cap: hit ? { cap: hit.name.toUpperCase(), sub: 'Solid City University \u00b7 office hours: ' + HOSTNAME(hostOf(hit)), say: hit.name + ' campus.' } : null });
    }
    var last = CAMPUS[CAMPUS.length - 1];
    w.push({ x: last.gate.x, z: last.gate.z, y: Y + 140, v: 150, cap: null });
    w.push({ x: A.PAD.x - 600, z: A.PAD.z + 300, y: Y + 260, v: 160, cap: { cap: 'HOME', sub: 'Orbital Spaceport \u00b7 landing', say: 'Back to the spaceport.' } });
    return w;
  } };
  if (W.order && W.order.indexOf('campus') < 0) W.order.push('campus');
}

/* ======================= lazy halls + forecourt triggers ======================= */
function update(dt) {
  if (U.state === 'idle') { if (A.isBuilt && !A.isBuilt()) return; U.wait -= dt; if (U.wait <= 0) init(); return; }
  var ub = $('uni-btn'); if (ub) { var hide = S.mode === 'watch' || !!document.querySelector('.watching'); if (ub.hidden !== hide) ub.hidden = hide; }
  if (U.state !== 'ready' || document.hidden) return;
  var cam = A.camera.position, cx = cam.x + A.origin.x, cz = cam.z + A.origin.z, built = 0;
  for (var i = 0; i < CAMPUS.length; i++) {
    var c = CAMPUS[i], d = Math.hypot(cx - c.x, cz - c.z), h = U.halls[c.id];
    if (!h && d < 950 && !built) { try { U.halls[c.id] = buildHall(c); } catch (e) { U.halls[c.id] = 'failed'; try { console.warn('[city] campus hall skipped:', c.id, e && e.message); } catch (x) {} } built++; }
    else if (h && h !== 'failed') h.visible = d < 4000;
  }
  if (U.roads) U.roads.visible = Math.hypot(cx, cz) < 9000;
  if (S.mode === 'walk') {   // walking into a forecourt opens the catalog once per visit
    for (i = 0; i < CAMPUS.length; i++) {
      c = CAMPUS[i]; d = Math.hypot(S.p.x - c.front.x, S.p.z - c.front.z);
      if (d < 11 && !U.said[c.id]) { U.said[c.id] = true; openCampus(c.id); if (window.CityHyper && CityHyper.narrate) CityHyper.narrate('<b>' + esc(c.name) + '</b> \u00b7 Solid City University. Course Catalog open; office hours: ' + esc(HOSTNAME(hostOf(c))) + '.', 6); }
      else if (d > 60) U.said[c.id] = false;
    }
  }
}
function actHere(x, z) { if (U.state !== 'ready') return null; for (var i = 0; i < CAMPUS.length; i++) if (Math.hypot(x - CAMPUS[i].front.x, z - CAMPUS[i].front.z) < 16) return CAMPUS[i].id; return null; }
function radar(g, blip, fl, sc, ax, az) { if (U.state !== 'ready') return;
  if (!fl && sc) { var W2 = g.canvas.width 