<!-- SOLID CITY · part 2493 -->
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 mesh = new T.Mesh(g, mat);
    mesh.frustumCulled = false;
    return mesh;
  };
  var solid = new Batch(), glowB = new Batch(), glassB = new Batch();

  var _e = new T.Euler(), _m = new T.Matrix4(), _q = new T.Quaternion();
  function M4(x, y, z, ry, rx, rz) {
    _e.set(rx || 0, ry || 0, rz || 0, 'YXZ'); _q.setFromEuler(_e);
    return new T.Matrix4().compose(new T.Vector3(x, y, z), _q.clone(), new T.Vector3(1, 1, 1));
  }
  // batched primitives — all coordinates in each home's LOCAL frame, baked to world by the caller via a base matrix
  var BASE = new T.Matrix4();
  function put(batch, geo, x, y, z, ry, rx, rz, color) {
    var m = M4(x, y, z, ry || 0, rx || 0, rz || 0);
    if (BASE) m.premultiply(BASE);
    batch.add(geo, m, color);
  }
  function box(batch, w, h, d, x, y, z, color, ry, rx, rz) { put(batch, new T.BoxGeometry(w, h, d), x, y, z, ry, rx, rz, color); }
  function cyl(batch, rt, rb, h, seg, x, y, z, color, ry, rx, rz) { put(batch, new T.CylinderGeometry(rt, rb, h, seg), x, y, z, ry, rx, rz, color); }
  function cone(batch, r, h, seg, x, y, z, color, ry, rx, rz) { put(batch, new T.ConeGeometry(r, h, seg), x, y, z, ry, rx, rz, color); }
  function sph(batch, r, x, y, z, color, sy) {
    var g = new T.SphereGeometry(r, 9, 7); if (sy) g.scale(1, sy, 1);
    put(batch, g, x, y, z, 0, 0, 0, color);
  }
  function rock(batch, r, x, y, z, color) { put(batch, new T.DodecahedronGeometry(r, 0), x, y, z, R() * 3, R() * 2, 0, color); }
  // triangular gable prism: ridge along X, base half-depth hd, apex rise, thickness t (along X)
  function prismGeo(hd, rise, t) {
    var A = [-t / 2, 0, -hd], B = [t / 2, 0, -hd], Cp = [t / 2, 0, hd], D = [-t / 2, 0, hd], E = [-t / 2, rise, 0], F = [t / 2, rise, 0];
    // This prism is used ONLY for gable-end infill (thin), so build just the two end triangles + slopes shell:
    var pos = [], nor = [], idx = [];
    function tri(p1, p2, p3, n) {
      var b = pos.length / 3;
      pos.push(p1[0], p1[1], p1[2], p2[0], p2[1], p2[2], p3[0], p3[1], p3[2]);
      for (var i = 0; i < 3; i++) nor.push(n[0], n[1], n[2]);
      idx.push(b, b + 1, b + 2);
    }
    var sl = Math.hypot(hd, rise), nz = rise / sl, ny = hd / sl;
    tri(A, B, F, [0, nz, -ny]); tri(A, F, E, [0, nz, -ny]);       // back slope
    tri(D, F, Cp, [0, nz, ny]); tri(D, E, F, [0, nz, ny]);        // front slope
    tri(A, E, D, [-1, 0, 0]);                                      // left gable
    tri(B, Cp, F, [1, 0, 0]);                                      // right gable
    tri(A, Cp, B, [0, -1, 0]); tri(A, D, Cp, [0, -1, 0]);          // bottom
    var g = new T.BufferGeometry();
    g.setAttribute('position', new T.Float32BufferAttribute(pos, 3));
    g.setAttribute('normal', new T.Float32BufferAttribute(nor, 3));
    g.setAttribute('uv', new T.Float32BufferAttribute(new Float32Array((pos.length / 3) * 2), 2));
    g.setIndex(idx);
    return g;
  }
  function gable(batch, w, hd, rise, y, color) {
    // full-width gable infill: prism spanning the wall width
    var g = prismGeo(hd, rise, w);
    // prism ridge runs along its thickness axis (X) — exactly what we want
    put(batch, g, 0, y, 0, 0, 0, 0, color);
  }

  /* ---------------- terrain helpers ---------------- */
  function slopeAt(x, z, e) {
    var dx = (groundAt(x + e, z) - groundAt(x - e, z)) / (2 * e);
    var dz = (groundAt(x, z + e) - groundAt(x, z - e)) / (2 * e);
    return Math.hypot(dx, dz);
  }
  // PLACEMENT LAW: a bench is gentle ground with NO ledge/cliff break in or near the lot.
  // (Ancestors' coast: homes live between the shore h~7 and the high benches h~420.)
  function benchAt(x, z, lotR) {
    var h = groundAt(x, z);
    if (h < 7 || h > 420) return -1;
    if (slopeAt(x, z, 7) > 0.36) return -1;
    if (slopeAt(x, z, 20) > 0.52) return -1;
    for (var i = 0; i < 8; i++) {           // ring: catch terrain breaks (ledge lips, cliff edges)
      var a = i / 8 * Math.PI * 2, px = x + Math.cos(a) * lotR, pz = z + Math.sin(a) * lotR;
      if (slopeAt(px, pz, 5) > 0.85) return -1;
      if (Math.abs(groundAt(px, pz) - h) > lotR * 0.62 + 6) return -1;
    }
    return h;
  }
  function lotHeight(x, z, r) {            // pad height: near-max of the lot so the pad never buries
    var m = -1e9, i, j;
    for (i = -1; i <= 1; i++) for (j = -1; j <= 1; j++) { var h = groundAt(x + i * r, z + j * r); if (h > m) m = h; }
    return m + 0.25;
  }

  /* ================= VEGETATION & GARDENS (local frame: origin at plant base, ground y=0) ================= */
  function monkeypod(s) {           // the great umbrella tree of the compound image
    trunkBody();
    cyl(solid, 0.32 * s, 0.5 * s, 3.4 * s, 7, 0, 1.7 * s, 0, C.trunk);
    cyl(solid, 0.16 * s, 0.3 * s, 2.6 * s, 6, 1.1 * s,