<!-- SOLID CITY · part 535 -->
ntElement.clientWidth || innerWidth, h = el.clientHeight || document.documentElement.clientHeight || innerHeight; return [w, h]; }
renderer.setSize(innerWidth, innerHeight, false);
renderer.outputEncoding = T.sRGBEncoding;
var scene = new T.Scene();
var camera = new T.PerspectiveCamera(62, innerWidth / innerHeight, 0.3, 40000);
var world = new T.Group(); scene.add(world);            // floating-origin root
var origin = { x: 0, z: 0 };
scene.fog = new T.Fog(0x2a0d14, 2000, 22000);

var SUN_DIR = new T.Vector3(0.25, 0.85, 0.35).normalize(); // high daytime sun
var hemi = new T.HemisphereLight(0xbfd8ff, 0x4a5a3a, 0.75); scene.add(hemi);
var sun = new T.DirectionalLight(0xfff6e8, 1.4); sun.position.copy(SUN_DIR).multiplyScalar(1000); scene.add(sun);
var amb = new T.AmbientLight(0x8a9aa8, 0.5); scene.add(amb);

/* ---------------- quadtree LOD terrain ---------------- */
var TERRAIN_MAT = curve(new T.MeshLambertMaterial({ vertexColors: true }));
var ROOT = { cx: 8600, cz: 0, size: 131072 };
var SEG = MOBILE ? 24 : 32, MIN_SIZE = MOBILE ? 512 : 256, SPLIT_K = MOBILE ? 1.45 : 1.6;
var chunks = new Map();           // key -> {mesh, last}
var buildQueue = [];
function nodeKey(cx, cz, size) { return size + ':' + cx + ':' + cz; }
function buildChunk(cx, cz, size) {
  var n = SEG, n1 = n + 1, step = size / n, half = size / 2;
  var vcount = n1 * n1 + 4 * n1, pos = new Float32Array(vcount * 3), nor = new Float32Array(vcount * 3), col = new Float32Array(vcount * 3);
  var hs = new Float32Array((n1 + 2) * (n1 + 2));
  var allWater = true;
  for (var z = -1; z <= n1; z++) for (var x = -1; x <= n1; x++) {
    var hv = heightAt(cx - half + x * step, cz - half + z * step);
    hs[(z + 1) * (n1 + 2) + (x + 1)] = hv; if (hv > -8) allWater = false;
  }
  if (allWater && size <= 8192) return null; // deep ocean: the sea disc covers it
  var vi = 0, skirt = Math.max(30, size * 0.02);
  function put(x, z, yOff) {
    var hh = hs[(z + 1) * (n1 + 2) + (x + 1)];
    var hl = hs[(z + 1) * (n1 + 2) + x], hr = hs[(z + 1) * (n1 + 2) + x + 2], hd = hs[z * (n1 + 2) + x + 1], hu = hs[(z + 2) * (n1 + 2) + x + 1];
    var nx = (hl - hr) / (2 * step), nz = (hd - hu) / (2 * step), il = 1 / Math.sqrt(nx * nx + 1 + nz * nz);
    pos[vi * 3] = -half + x * step; pos[vi * 3 + 1] = hh + yOff; pos[vi * 3 + 2] = -half + z * step;
    nor[vi * 3] = nx * il; nor[vi * 3 + 1] = il; nor[vi * 3 + 2] = nz * il;
    colorAt(cx - half + x * step, cz - half + z * step, hh, Math.sqrt(nx * nx + nz * nz), tmpC);
    col[vi * 3] = tmpC.r; col[vi * 3 + 1] = tmpC.g; col[vi * 3 + 2] = tmpC.b; vi++;
  }
  for (z = 0; z < n1; z++) for (x = 0; x < n1; x++) put(x, z, 0);
  var sk = [];  // skirts hide LOD cracks
  var edges = [[0, 0, 1, 0], [0, n, 1, 0], [0, 0, 0, 1], [n, 0, 0, 1]];
  for (var e = 0; e < 4; e++) { sk.push(vi); for (var k = 0; k < n1; k++) put(edges[e][0] + edges[e][2] * k, edges[e][1] + edges[e][3] * k, -skirt); }
  var idx = [];
  for (z = 0; z < n; z++) for (x = 0; x < n; x++) { var a = z * n1 + x, b = a + 1, c = a + n1, d = c + 1; idx.push(a, c, b, b, c, d); }
  function edgeIdx(e, k) { var E = edges[e]; return (E[1] + E[3] * k) * n1 + (E[0] + E[2] * k); }
  for (e = 0; e < 4; e++) for (k = 0; k < n; k++) {
    var t0 = edgeIdx(e, k), t1 = edgeIdx(e, k + 1), s0 = sk[e] + k, s1 = s0 + 1;
    idx.push(t0, t1, s0, t1, s1, s0, t0, s0, t1, t1, s0, s1); // both windings (cheap, avoids cull holes)
  }
  var g = new T.BufferGeometry();
  g.setAttribute('position', new T.BufferAttribute(pos, 3));
  g.setAttribute('normal', new T.BufferAttribute(nor, 3));
  g.setAttribute('color', new T.BufferAttribute(col, 3));
  g.setIndex(idx);
  g.boundingSphere = new T.Sphere(new T.Vector3(0, 0, 0), size * 0.75 + 4500);
  var m = new T.Mesh(g, TERRAIN_MAT);
  m.position.set(cx, 0, cz); m.matrixAutoUpdate = false; m.updateMatrix(); m.visible = false;
  world.add(m);
  return m;
}
function getChunk(cx, cz, size) {
  var k = nodeKey(cx, cz, size), c = chunks.get(k);
  return c;
}
var wanted = new Set(), visibleNow = [];
function lodSelect(cam) {   // cam: absolute {x, y, z}
  var out = [], queue = [];
  function ready(node) { var c = getChunk(node.cx, node.cz, node.size); return c && c.built; }
  function need(node) {
    var k = nodeKey(node.cx, node.cz, node.size);
    if (!chunks.has(k)) { chunks.set(k, { built: false, mesh: null, last: 0 }); buildQueue.push(node); }
    return chunks.get(k);
  }
  function shouldSplit(node) {
    if (node.size <= MIN_SIZE) return false;
    var dx = Math.max(Math.abs(cam.x - node.cx) - node.size / 2, 0), dz = Math.max(Math.abs(cam.z - node.cz) - node.size / 2, 0);
    var dy = Math.max(cam.y - 1200, 0) * 0.6;
    var d = Math.sqrt(dx * dx + dz * dz + dy * dy);
    if (node.size > 40000 && Math.abs(node.cx - ROOT.cx) + Math.abs(node.cz - ROOT.cz) > 1 && d > 60000) return false;
    return d < node.size * SPLIT_K * (S.mode === 'walk' ? 0.36 : 0.5);
  }
  function kids(node) { var q = node.size / 4, s = node.siz