<!-- SOLID CITY · part 1724 -->
hip, startCountdown: startCountdown, board: board, R_PLANET: R_PLANET, ship: ship,
  isBuilt: function () { return cityBuilt; }, openSoul: openSoul, SOUL: SOUL, soulTab: spTab
};
})();


===== FILE: public/city/city.js.bak-warship-20261011 =====
/* SOLID CITY — epistles.xyz/city (MVP, 2026-10-09)
 * Client-side only: a red/black/gold city on a ~64 km procedural island chain
 * (a nod to the Big Island), chunked quadtree LOD terrain, curved-planet shader,
 * floating origin, sky/stars/galaxy, orbit layer, walking + arcade spaceship,
 * SABLE NPC (login-gated, via /api/city/sable -> existing SABLE route).
 * Solidivism rule kept: everything visible near the ground is solid (colliders).
 */
(function () {
'use strict';
var T = window.THREE;
var $ = function (id) { return document.getElementById(id); };
var IS_TOUCH = ('ontouchstart' in window) || navigator.maxTouchPoints > 0;
var MOBILE = IS_TOUCH && Math.min(screen.width, screen.height) < 820;
var QS = new URLSearchParams(location.search);
var R_PLANET = 1200000;            // curvature radius (m) — horizon & orbit look
var CITY_Y = 22;                   // city ground height
var SEED = 20261009;
var PERF = QS.has('perf');

/* ---------------- seeded simplex noise (2D) ---------------- */
function mulberry(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; var t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
var rnd = mulberry(SEED);
var perm = new Uint8Array(512), p0 = [];
for (var i = 0; i < 256; i++) p0[i] = i;
for (i = 255; i > 0; i--) { var j = Math.floor(rnd() * (i + 1)), tt = p0[i]; p0[i] = p0[j]; p0[j] = tt; }
for (i = 0; i < 512; i++) perm[i] = p0[i & 255];
var G2 = [[1,1],[-1,1],[1,-1],[-1,-1],[1,0],[-1,0],[0,1],[0,-1]];
var F2 = 0.5 * (Math.sqrt(3) - 1), Gk = (3 - Math.sqrt(3)) / 6;
function snoise(x, y) {
  var s = (x + y) * F2, i = Math.floor(x + s), j = Math.floor(y + s);
  var t = (i + j) * Gk, x0 = x - (i - t), y0 = y - (j - t);
  var i1 = x0 > y0 ? 1 : 0, j1 = 1 - i1;
  var x1 = x0 - i1 + Gk, y1 = y0 - j1 + Gk, x2 = x0 - 1 + 2 * Gk, y2 = y0 - 1 + 2 * Gk;
  var ii = i & 255, jj = j & 255, n = 0, g, tq;
  tq = 0.5 - x0 * x0 - y0 * y0; if (tq > 0) { g = G2[perm[ii + perm[jj]] & 7]; tq *= tq; n += tq * tq * (g[0] * x0 + g[1] * y0); }
  tq = 0.5 - x1 * x1 - y1 * y1; if (tq > 0) { g = G2[perm[ii + i1 + perm[jj + j1]] & 7]; tq *= tq; n += tq * tq * (g[0] * x1 + g[1] * y1); }
  tq = 0.5 - x2 * x2 - y2 * y2; if (tq > 0) { g = G2[perm[ii + 1 + perm[jj + 1]] & 7]; tq *= tq; n += tq * tq * (g[0] * x2 + g[1] * y2); }
  return 70 * n;
}
function fbm(x, y, oct) { var a = 0, f = 1, amp = 1, norm = 0; for (var o = 0; o < oct; o++) { a += amp * snoise(x * f, y * f); norm += amp; f *= 2.03; amp *= 0.5; } return a / norm; }
function ridged(x, y, oct) { var a = 0, f = 1, amp = 1, norm = 0; for (var o = 0; o < oct; o++) { var n = 1 - Math.abs(snoise(x * f + 31.7, y * f - 12.3)); a += amp * n * n; norm += amp; f *= 2.1; amp *= 0.5; } return a / norm; }
function clamp(v, a, b) { return v < a ? a : v > b ? b : v; }
function smooth(t) { t = clamp(t, 0, 1); return t * t * (3 - 2 * t); }
function lerp(a, b, t) { return a + (b - a) * t; }

/* ---------------- the world: island chain ---------------- */
// Main island: the capital sits on its west (Kona-side) coast at the origin.
var ISLANDS = [{ x: 8600, z: 0, r: 9800, main: true }];
var VOLCANOES = [
  { name: 'MAUNA LOA', x: 10500, z: 2600, r: 7200, h: 3300 },
  { name: 'MAUNA KEA', x: 11800, z: -3900, r: 5200, h: 3500 },
  { name: 'HUALĀLAI', x: 4600, z: 700, r: 3200, h: 1700 },
  { name: 'KOHALA', x: 7600, z: -7600, r: 3600, h: 1400 },
  { name: 'KĪLAUEA', x: 15600, z: 4200, r: 3200, h: 1100, crater: true }
];
(function () { // the rest of the chain, seeded
  var ang = [2.2, 2.75, 3.3, 3.9, 4.5, 5.2, 0.9, 1.5];
  for (var k = 0; k < ang.length; k++) {
    var d = 15500 + rnd() * 9000, r = 1300 + rnd() * 2600;
    var isl = { x: 8600 + Math.cos(ang[k]) * d, z: Math.sin(ang[k]) * d, r: r };
    ISLANDS.push(isl);
    if (r > 2400) VOLCANOES.push({ name: '', x: isl.x + (rnd() - .5) * r * .4, z: isl.z + (rnd() - .5) * r * .4, r: r * .9, h: 600 + rnd() * 1300 });
  }
})();
var WORLD_EXTENT = 32000; // ~64 km across the chain (terrain streams anywhere; beyond is open ocean)

function landMask(x, z) {
  var c = -9, wob = fbm(x * 0.00022, z * 0.00022, 4) * 0.32;
  for (var k = 0; k < ISLANDS.length; k++) {
    var I = ISLANDS[k], dx = x - I.x, dz = z - I.z;
    var d = Math.sqrt(dx * dx + dz * dz) / I.r + wob;
    var v = 1 - d; if (v > c) c = v;
  }
  return c;
}
function heightAt(x, z) {
  var c = landMask(x, z);
  var h = c > 0 ? 4 + 260 * Math.pow(c, 0.8) : c * 700;
  var land = smooth(c * 6 + 0.1);
  for (var k = 0; k < VOLCANOES.length; k++) {
    var V = VOLCANOES[k], dx = x - V.x, dz = z - V.z, dd = Math.sqrt(dx * dx + dz * dz) / V.r;
    if (dd < 1) {
      var q2 = 1 - dd * dd; h += V.h * q2 * q2 