<!-- SOLID CITY · part 2878 -->
s.forEach(function (w) { var t = (lines[li] ? lines[li] + ' ' : '') + w; if (g.measureText(t).width > W - 64 && lines[li]) { if (li === 2) { lines[2] += '\u2026'; return; } lines[++li] = w; } else lines[li] = t; });
  lines = lines.slice(0, 3); var lh = 46, y0 = 120 - (lines.length - 1) * lh / 2 + 8;
  lines.forEach(function (l, i) { g.fillText(l, W / 2, y0 + i * lh); });
  bubTex.needsUpdate = true; bubUntil = S.t + (sec || 8); st.mood = kind === 'prophecy' ? 'speaking a prophecy' : 'greeting';
}

/* ---------- 'Approach the Oracle' link (only while you stand in her sanctuary) ---------- */
var chip = document.createElement('a'); chip.id = 'pv-approach'; chip.href = PAGE; chip.target = '_blank'; chip.rel = 'noopener'; chip.hidden = true;
chip.innerHTML = '<span aria-hidden="true">\u25C7</span> Approach the Oracle';
var css = document.createElement('style');
css.textContent = '#pv-approach{position:fixed;z-index:30;left:50%;transform:translateX(-50%);bottom:calc(116px + env(safe-area-inset-bottom,0px));display:inline-flex;align-items:center;gap:.5em;min-height:44px;padding:0 16px;border:1px solid rgba(113,247,217,.6);border-radius:3px;background:rgba(4,28,23,.82);color:#eafff9;font:600 14px/1 system-ui,-apple-system,sans-serif;letter-spacing:.06em;white-space:nowrap;text-decoration:none;box-shadow:0 0 18px rgba(50,221,190,.25);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}' +
  '#pv-approach span{color:#71f7d9;font-size:17px}#pv-approach:hover,#pv-approach:focus-visible{background:#0b4e43;outline:none}' +
  '@media (min-width:801px){#pv-approach{bottom:96px}}@media (max-height:500px) and (orientation:landscape){#pv-approach{bottom:12px}}';
document.head.appendChild(css); (document.getElementById('sc') || document.body).appendChild(chip);

/* ---------- /city/?to=sanctuary : arrive at her gate, facing her ---------- */
(function arrive() {
  var q; try { q = new URLSearchParams(location.search).get('to'); } catch (e) {} if (q !== 'sanctuary' && q !== 'provolta') return;
  (function wait() {
    var boot = document.getElementById('sc-boot');
    if ((boot && !boot.hidden) || S.mode !== 'walk' || CitySanctuary.state() !== 'ready') return setTimeout(wait, 500);
    var sx = HOME.x - 27, sz = HOME.z; SC.walk(sx, sz, Math.atan2(-(HOME.x - sx), -(HOME.z - sz)));
  })();
})();

/* ---------- frame (runs right after the sanctuary's own update) ---------- */
function turnTo(want, dt, k) { var dA = ((want - c.face + Math.PI * 3) % (Math.PI * 2)) - Math.PI; c.face += dA * Math.min(1, dt * k); }
function tick(dt) {
  if (!c.g || c.st === undefined) return;
  // home: the oracle's step (never wanders; a push from the player is undone gently)
  if (c.st !== 'dwell') { c.st = 'dwell'; c.t = 1e7; c.path = null; }
  c.t = 1e7; c.x += (HOME.x - c.x) * Math.min(1, dt * 2); c.z += (HOME.z - c.z) * Math.min(1, dt * 2);
  var me = S.mode === 'walk' ? S.p : S.ship, pd = Math.hypot(me.x - HOME.x, me.z - HOME.z), walk = S.mode === 'walk';
  // who to face: the player, else the nearest citizen in the sanctuary, else the city (west, through her gate)
  var tgt = null, name = 'the city';
  if (walk && pd < R_SEE) { tgt = me; name = 'you'; }
  else {
    var act = CityCitizens.active(), best = R_CZ;
    for (var i = 0; i < act.length; i++) { if (act[i] === 'provolta') continue; var o = CityCitizens.get(act[i]); if (!o || o.st === 'fly') continue; var d = Math.hypot(o.x - HOME.x, o.z - HOME.z); if (d < best) { best = d; tgt = o; name = o.name; } }
    // a citizen walks in: greet by name once a minute
    if (tgt && tgt !== me && (st.czSeen[name] || -99) < S.t - 60 && pd < 160) { st.czSeen[name] = S.t; if (bubUntil < S.t) say(CZ_GREET[Math.floor(Math.random() * CZ_GREET.length)].replace('{n}', name), 5, 'greet'); st.spike = Math.max(st.spike, 0.6); }
  }
  st.faceName = name;
  turnTo(tgt ? Math.atan2(tgt.x - c.x, tgt.z - c.z) : -Math.PI / 2, dt, tgt ? 2.6 : 0.8);
  if (c.idleK === 'look' || c.idleK === 'stretch') { c.idleK = ''; c.idle = 0; }
  // the player approaches: greeting + beacon
  if (walk && pd < R_IN && !st.inside) {
    st.inside = true; st.spike = 1;
    if (S.t - st.greetT > 40) { st.greetT = S.t; c.greetT = S.t; say(GREET[Math.floor(Math.random() * GREET.length)], 6, 'greet'); c.idleK = 'wave'; c.idle = 1.4; st.pending = S.t + 6.5; }
  } else if (st.inside && (!walk || pd > R_OUT)) st.inside = false;
  chip.hidden = !(st.inside && walk);
  // prophecies: one per period, the same line for everyone (shown when you are within earshot)
  var s = slotNow();
  if (s !== st.slot || (st.pending && S.t > st.pending && bubUntil < S.t)) {
    var fresh = s !== st.slot; st.slot = s; st.line = lineFor(s); st.pending = 0;
    if (pd < R_TALK || st.inside) {
      if (!fresh || bubUntil < S.t + 1) { say(st.line.text, 9, 'prophecy'); st.saidAt = Date.now(); st.spike = Math.max(st.spike, 0.5);
        if (window.CityHyper && CityHyper.narrate) CityHyper.narrate('<b style="color:#71f7d9">\u272