<!-- SOLID CITY · part 3152 -->
T.Vector3(), w = new T.Vector3(), cc = new T.Color(color);
    for (var i = 0; i < p.count; i++) {
      v.fromBufferAttribute(p, i).applyMatrix4(matrix); this.pos.push(v.x, v.y, v.z);
      w.fromBufferAttribute(nr, i).applyMatrix3(nm).normalize(); this.nor.push(w.x, w.y, w.z);
      this.col.push(cc.r, cc.g, cc.b);
    }
    for (var j = 0; j < p.count; j++) this.idx.push(this.n + j);
    this.n += p.count; if (g !== geo) g.dispose();
  };
  Batch.prototype.build = function (mat) {
    var g = new T.BufferGeometry();
    g.setAttribute('position', new T.Float32BufferAttribute(this.pos, 3));
    g.setAttribute('normal', new T.Float32BufferAttribute(this.nor, 3));
    g.setAttribute('color', new T.Float32BufferAttribute(this.col, 3));
    g.setIndex(this.idx);
    var m = new T.Mesh(g, mat); m.frustumCulled = false; return m;
  };
  var solid = new Batch(), glow = new Batch(), glassB = new Batch();
  function box(b, w, h, d, m, color) { b.add(new T.BoxGeometry(w, h, d), m, color); }
  function cyl(b, rt, rb, h, s, m, color) { b.add(new T.CylinderGeometry(rt, rb, h, s), m, color); }
  function sph(b, r, m, color, sx, sy, sz) { b.add(new T.SphereGeometry(r, 10, 7), m, color); }
  function cone(b, r, h, s, m, color) { b.add(new T.ConeGeometry(r, h, s), m, color); }

  /* red and black marble — one canvas, veined by hand */
  function marbleTex(redBase) {
    var cv = document.createElement('canvas'); cv.width = 256; cv.height = 256;
    var g = cv.getContext('2d');
    g.fillStyle = redBase ? '#57101b' : '#0d0b0e'; g.fillRect(0, 0, 256, 256);
    for (var i = 0; i < 46; i++) {
      g.strokeStyle = redBase ? 'rgba(255,120,120,' + (0.05 + R() * 0.16) + ')' : 'rgba(200,30,50,' + (0.08 + R() * 0.2) + ')';
      g.lineWidth = 0.6 + R() * 1.8; g.beginPath();
      var x = R() * 256, y = R() * 256; g.moveTo(x, y);
      for (var k = 0; k < 4; k++) { x += (R() - 0.5) * 90; y += (R() - 0.5) * 90; g.lineTo(x, y); }
      g.stroke();
    }
    for (var j = 0; j < 26; j++) { g.fillStyle = 'rgba(240,220,190,' + (0.03 + R() * 0.06) + ')'; g.fillRect(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 ?