<!-- SOLID CITY · part 3131 -->
R() * 256, R() * 256, 1.4, 1.4); }
    var t = new T.CanvasTexture(cv); t.encoding = T.sRGBEncoding; t.wrapS = t.wrapT = T.RepeatWrapping;
    return t;
  }
  var marbleBlack = marbleTex(false), marbleRed = marbleTex(true);
  /* THE TWO CLOTHS (the King's word, 2026-10-14): "instead
   * of using color use the images to create the effects" —
   * the lava-glow marble is a real AI-struck image, woven at
   * two qualities: the high cloth for strong phones, the low
   * for phones that struggle, so everybody gets to play. The
   * placer chooses the tier and hands the texture in; the
   * cloth breathes (update) — its seams drift and its light
   * pulses, slowly, like cooling fire. */
  var lavaTex = opts.texLava || null, lavaMats = [], lavaClones = [];
  /* THE FOLD (the King's word, 2026-10-14): the image is
   * rayscanned into a heightfield — stone faces stand
   * CONVEX, lava seams sink CONCAVE — and objects are baked
   * OUT of the image: their vertices pushed along their
   * normals by the scan, then flat-shaded into crystal.
   * Relief by tier: a derived normal map for the high
   * cloth, a bump map for the low. All baked at build;
   * nothing per frame but the breathing. */
  var HEIGHT = opts.texHeight || null;
  function imgRaw(u, v) {
    var w = HEIGHT.w, h = HEIGHT.h, d = HEIGHT.data;
    var st = d.length >= w * h * 4 ? 4 : 3;
    var x = ((Math.floor(u * w) % w) + w) % w, y = ((Math.floor((1 - v) * h) % h) + h) % h;
    var i = (y * w + x) * st, r = d[i], g = d[i + 1], b = d[i + 2];
    var lum = (0.35 * r + 0.5 * g + 0.15 * b) / 255;
    var vein = Math.max(0, r - b) / 160; if (vein > 1) vein = 1;
    /* the fold is NORMALIZED to the image's own histogram
     * (measured on first use): the scan's 5th–95th luminance
     * percentiles are stretched across the relief, so the
     * brightest stone always stands convex and the darkest
     * pits sit below the face — whatever image is struck,
     * the fold fits it. The lava seams still sink deepest:
     * 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;
  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