<!-- SOLID CITY · part 3102 -->
 = (fu - 0.85) / 0.15; h0 = h0 * (1 - wu) + imgRaw(fu - 1, fv) * wu; }
    if (fv > 0.85) { var wv = (fv - 0.85) / 0.15; h0 = h0 * (1 - wv) + imgRaw(fu, fv - 1) * wv; }
    return h0;
  }
  function reliefTexture(kind, size) {
    if (!HEIGHT) return null;
    var data = new Uint8Array(size * size * 4);
    for (var y = 0; y < size; y++) for (var x = 0; x < size; x++) {
      var i = (y * size + x) * 4;
      if (kind === 'bump') { var hv = Math.max(0, Math.min(255, Math.round((imgH(x / size, y / size) + 1.6) / 3.4 * 255))); data[i] = data[i + 1] = data[i + 2] = hv; data[i + 3] = 255; }
      else {
        var s = 1.7;
        var dx = (imgH((x - 1) / size, y / size) - imgH((x + 1) / size, y / size)) * s;
        var dy = (imgH(x / size, (y - 1) / size) - imgH(x / size, (y + 1) / size)) * s;
        var inv = 1 / Math.sqrt(dx * dx + dy * dy + 1);
        data[i] = Math.round((dx * inv * 0.5 + 0.5) * 255); data[i + 1] = Math.round((dy * inv * 0.5 + 0.5) * 255); data[i + 2] = Math.round((inv * 0.5 + 0.5) * 255); data[i + 3] = 255;
      }
    }
    var t = new T.DataTexture(data, size, size); t.needsUpdate = true; t.wrapS = t.wrapT = T.RepeatWrapping;
    return t;
  }
  var NRM = reliefTexture('normal', 256), BMP = reliefTexture('bump', 128);
  var molded = { baked: false, maxRise: 0, maxSink: 0 };
  var practiceInfo = null;
  /* STAGE 1 COMPLETED — THE LIGHT POOLS (the craft
   * research prescribed them: glow-pool decals under every
   * lamp, halo sprites at the great lights, a warm bounce
   * wash borrowed onto the floors). Every lamp registers
   * its pool as it is placed; one instanced mesh pours
   * them all — a single draw for the whole city's glow. */
  var glowSpots = [], glowMat = null, haloMats = [], glowInfo = null, glowTexShared = null;
  var heroInfo = { crystalMorph: false, redline: 0, lavaEmissive: !!lavaTex };
  var redlineMat = null;
  function glowTexture() {
    var cv = document.createElement('canvas'); cv.width = cv.height = 128;
    var ctx = cv.getContext('2d');
    try {
      var gr = ctx.createRadialGradient(64, 64, 2, 64, 64, 64);
      gr.addColorStop(0, 'rgba(255,255,255,0.9)'); gr.addColorStop(0.35, 'rgba(255,255,255,0.38)'); gr.addColorStop(1, 'rgba(255,255,255,0)');
      ctx.fillStyle = gr; ctx.fillRect(0, 0, 128, 128);
    } catch (e) { try { ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.fillRect(0, 0, 128, 128); } catch (e2) {} }
    return new T.CanvasTexture(cv);
  }
  function halo(x, y, z, s, c) {
    if (!glowTexShared) glowTexShared = glowTexture();
    var sm = new T.SpriteMaterial({ map: glowTexShared, color: c, transparent: true, opacity: 0.3, blending: T.AdditiveBlending, depthWrite: false });
    var sp = new T.Sprite(sm); sp.position.set(x, y, z); sp.scale.set(s, s, 1); sp.frustumCulled = false; group.add(sp); haloMats.push(sm);
  }
  function buildGlow() {
    if (!glowTexShared) glowTexShared = glowTexture();
    var geo = new T.CircleGeometry(1, 22); geo.rotateX(-Math.PI / 2);
    glowMat = new T.MeshBasicMaterial({ map: glowTexShared, transparent: true, blending: T.AdditiveBlending, depthWrite: false });
    var im = new T.InstancedMesh(geo, glowMat, Math.max(1, glowSpots.length));
    im.count = glowSpots.length;
    var d = new T.Object3D(), cc = new T.Color();
    for (var i = 0; i < glowSpots.length; i++) { var s = glowSpots[i]; d.position.set(s.x, s.y, s.z); d.scale.setScalar(s.r); d.updateMatrix(); im.setMatrixAt(i, d.matrix); im.setColorAt(i, cc.setHex(s.c)); }
    if (im.instanceColor) im.instanceColor.needsUpdate = true;
    im.renderOrder = 3; im.frustumCulled = false; im.name = 'glow-pools'; group.add(im);
    [[A_C, 0xff9a4d], [B_C, 0xff7a3c]].forEach(function (w) {
      var m = new T.Mesh(new T.CircleGeometry(138, 28), new T.MeshBasicMaterial({ map: glowTexShared, transparent: true, opacity: 0.13, color: w[1], depthWrite: false }));
      m.rotation.x = -Math.PI / 2; m.position.set(w[0].x, w[0].f + 0.30, w[0].z); m.renderOrder = 2; m.frustumCulled = false; group.add(m);
    });
    [1425, 1470, 1515].forEach(function (z) {
      var m2 = new T.Mesh(new T.CircleGeometry(11, 18), new T.MeshBasicMaterial({ map: glowTexShared, transparent: true, opacity: 0.10, color: 0xff8a4d, depthWrite: false }));
      m2.rotation.x = -Math.PI / 2; m2.position.set(GAL.x, galleryFloorY(z) + 0.07, z); m2.renderOrder = 2; m2.frustumCulled = false; group.add(m2);
    });
    glowInfo = { pools: glowSpots.length, washes: 5, halos: haloMats.length };
  }
  function displace(geo, rx, ry, depth) {
    if (!HEIGHT || !geo.attributes.uv || !geo.attributes.normal) return geo;
    var p = geo.attributes.position, n = geo.attributes.normal, uv = geo.attributes.uv;
    for (var i = 0; i < p.count; i++) {
      var d = imgH(uv.getX(i) * rx, uv.getY(i) * ry) * depth;
      if (d > molded.maxRise) molded.maxRise = d;
      if (d < molded.maxSink) molded.maxSink = d;
      p.setXYZ(i, p.getX(i) + n.getX(i) * d, p.getY(i) + n.getY(i) * d, p.ge