<!-- SOLID CITY · part 3153 -->
 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 };
  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.getZ(i) + n.getZ(i) * d);
    }
    geo.computeVertexNormals(); molded.baked = true; return geo;
  }
  function lavaMat(rx, ry, side, flat) {
    var t2 = lavaTex.clone(); t2.needsUpdate = true;
    t2.wrapS = t2.wrapT = T.RepeatWrapping; t2.repeat.set(rx, ry);
    var m = new T.MeshLambertMaterial({ map: t2, side: side || T.FrontSide });
    var rel = opts.texTier === 'lo' ? BMP : NRM;
    if (rel) {
      var rc = rel.clone(); rc.needsUpdate = true; rc.wrapS = rc.wrapT = T.RepeatWrapping; rc.repeat.set(rx, ry);
      if (opts.texTier === 'lo') { m.bumpMap = rc; m.bumpScale = 0.7; }
      else { m.normalMap = rc; m.normalScale = new T.Vector2(0.85, 0.85); }
    }
    if (flat) m.flatShading = true;
    lavaMats.push(m); lavaClones.push(t2); return m;
  }
  var marbleMats = [];
  function marbleDisc(x, y, z, r, tex, rep) {
    var t2 = tex.clone(); t2.needsUpdate = true; t2.repeat.set(rep || 3, rep || 3);
    var m = new T.Mesh(new T.CylinderGeometry(r, r + 1.5, 0.5, 40), new T.MeshLambertMaterial({ map: t2 }));
    m.position.set(x, y - 0.13, z); m.frustumCulled = false; group.add(m); marbleMats.push(m);
    return m;
  }
  function textPlate(lines, w, h, fs) {
    var cv = document.createElement('canvas'); cv.width = 1024; cv.height = Math.round(1024 * h / w);
    var g = cv.getContext('2d');
    g.fillStyle = '#0d0a0c'; g.fillRect(0, 0, cv.width, cv.height);
    g.strokeStyle = '#d8a833'; g.lineWidth = 7; g.strokeRect(12, 12, cv.width - 24, cv.height - 24);
    g.fillStyle = '#ecc76e'; g.textAlign = 'center'; g.textBaseline = 'middle';
    lines.forEach(function (ln, i) {
      g.font = (i === 0 ? '800 ' : '600 ') + Math.round(fs * (i === 0 ? 1 : 0.52)) + 'px Georgia, serif';
      g.fillText(ln, cv.width / 2, cv.height * (lines.length === 1 ? 0.52 : 0.32 + i * 0.38));
    });
    var t = new T.CanvasTexture(cv); t.encoding = T.sRGBEncoding; t.anisotropy = 4;
    return new T.MeshBasicMaterial({ map: t });
  }
  /* a sign the lawful way: backing stone, plate PROUD of it,
   * a second face turned for the other journey */
  var footprints = [];
  function sign(x, y, z, ry, w, h, lines, fs, postH) {
    var g2 = new T.Group(); g2.position.set(x, y, z); g2.rotation.y = ry; group.add(g2);
    if (postH) {
      var post = new T.Mesh(new T.BoxGeometry(0.32, postH, 0.32), new T.MeshLambertMaterial({ color: C.titanium }));
      post.position.y = -postH / 2; post.frustumCulled = false; g2.add(post);
      footprints.push({ x: x, z: z, w: 0.7, d: 0.7, a: 0, y0: y - postH, y1: y });
    }
    var back = new T.Mesh(new T.BoxGeometry(w + 0.5, h + 0.5, 0.22), new T.MeshLambertMaterial({ color: C.stoneDark }));
    back.frustumCull