<!-- SOLID CITY · part 2515 -->
tch, 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, 4.2 * s, 0.4 * s, C.trunk, 0, 0, 0.7);
    cyl(solid, 0.16 * s, 0.3 * s, 2.6 * s, 6, -1.1 * s, 4.2 * s, -0.3 * s, C.trunk, 0, 0, -0.7);
    sph(solid, 4.6 * s, 0, 6.4 * s, 0, C.leaf, 0.42);
    sph(solid, 3.4 * s, 2.6 * s, 5.9 * s, 1 * s, C.leafDeep, 0.4);
    sph(solid, 3.2 * s, -2.7 * s, 6.0 * s, -0.8 * s, C.leafLight, 0.4);
  }
  function palm(s) {                 // tall slender palm of the Hilo photos
    trunkBody();
    var lean = (R() - 0.5) * 0.14;
    cyl(solid, 0.16 * s, 0.26 * s, 8.5 * s, 6, lean * 4 * s, 4.25 * s, 0, C.palmTrunk, 0, 0, lean);
    for (var i = 0; i < 7; i++) {
      var a = i / 7 * Math.PI * 2 + R() * 0.4;
      box(solid, 3.6 * s, 0.05, 0.55 * s, lean * 8 * s + Math.cos(a) * 1.6 * s, 8.8 * s, Math.sin(a) * 1.6 * s, C.leafLight, -a, 0, 0.62);
    }
    sph(solid, 0.5 * s, lean * 8 * s, 8.4 * s, 0, C.leafDeep, 0.8);
  }
  function banana() {               // banana clump: broad leaf fans
    cyl(solid, 0.14, 0.2, 2.2, 5, 0, 1.1, 0, C.banana);
    for (var i = 0; i < 5; i++) {
      var a = i / 5 * Ma