<!-- SOLID CITY · part 3122 -->
pest:
     * their vein signal outweighs their light. */
    if (!HEIGHT._norm) {
      var lums = [];
      for (var gy = 0; gy < 128; gy++) for (var gx = 0; gx < 128; gx++) {
        var gi = ((gy * HEIGHT.h / 128 | 0) * HEIGHT.w + (gx * HEIGHT.w / 128 | 0)) * (HEIGHT.data.length >= HEIGHT.w * HEIGHT.h * 4 ? 4 : 3);
        lums.push((0.35 * HEIGHT.data[gi] + 0.5 * HEIGHT.data[gi + 1] + 0.15 * HEIGHT.data[gi + 2]) / 255);
      }
      lums.sort(function (a, b) { return a - b; });
      HEIGHT._norm = { lo: lums[Math.floor(lums.length * 0.05)], hi: lums[Math.floor(lums.length * 0.95)] };
      if (HEIGHT._norm.hi - HEIGHT._norm.lo < 0.05) HEIGHT._norm.hi = HEIGHT._norm.lo + 0.05;
    }
    var ln = (lum - HEIGHT._norm.lo) / (HEIGHT._norm.hi - HEIGHT._norm.lo);
    ln = ln < 0 ? 0 : ln > 1 ? 1 : ln;
    var hh = (ln - 0.30) * 2.4 - vein * 1.5;
    return hh < -1.7 ? -1.7 : hh > 1.8 ? 1.8 : hh;
  }
  /* the heightfield made PERIODIC before it is trusted: a
   * cylinder's seam must close — the last stretch of each
   * axis is cross-faded into its first, so the folded
   * surface is one unbroken manifold, no step at the wrap.
   * (The geometry studied properly, per the King's word:
   * master-class or nothing.) */
  function imgH(u, v) {
    if (!HEIGHT) return 0;
    var fu = u - Math.floor(u), fv = v - Math.floor(v), h0 = imgRaw(fu, fv);
    if (fu > 0.85) { var wu = (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