<!-- SOLID CITY · part 2557 -->
 numbers each): road slabs, driveway stones, paths, parking pads — what you see is what you stand on (2026-10-13)
  var homes = [];       // {x, z, y, kind, name} for narration / POI

  /* ---------------- palette (from the King's photos) ---------------- */
  var C = {
    stucco: 0xb3a184,      // earth-tone walls, Hilo photos
    stucco2: 0xc0b096,
    woodWall: 0x6e5136,     // weathered dark wood, compound image
    woodDark: 0x4a3520,
    white: 0xe8e2d4,        // plantation board-and-batten
    shingle: 0x4c4238,      // dark shingle roofs, Hilo photos
    shingle2: 0x5a4c3c,
    corrugated: 0xaeb6ba,   // silvered tin roof, compound main hale
    stone: 0x55504c,        // lava-rock foundation / walls
    stoneDark: 0x3c3835,
    lanaiWood: 0x7a5c38,
    doorFrame: 0x3d2b1a,    // dark wood door frames
    glass: 0xbfe0ea,
    warm: 0xffc06a,         // window glow
    leaf: 0x2a5c2e, leafDeep: 0x1d4523, leafLight: 0x3f7a38,
    banana: 0x4c9440, taro: 0x3e8a4c,
    trunk: 0x5a4630, palmTrunk: 0x8a7a68,
    lawn: 0x4d7d33, soil: 0x4a3826, water: 0x7fc4d8,
    road: 0x6b6259, drive: 0x8a8078, conc: 0xa89e90,   // warm paved tones, Hilo photos
    yellowStone: 0xd8b93a,  // the King's yellow path stones
    flower1: 0xe86a9a, flower2: 0xf2f0e6, flower3: 0xd84a3a,
    carA: 0xb8bcc2, carB: 0x7a3a30, carC: 0x3a5a7a, carD: 0xd8d4c8
  };

  /* ---------------- geometry batcher (indexed merge, vertex colors) ---------------- */
  function Batch() { this.pos = []; this.nor = []; this.col = []; this.idx = []; this.n = 0; }
  Batch.prototype.add = function (geo, mtx, color) {
    var p = geo.attributes.position, nr = geo.attributes.normal, ix = geo.index;
    var col = new T.Color(color);
    var nm = new T.Matrix3().getNormalMatrix(mtx);
    var v = new T.Vector3(), nv = new T.Vector3();
    var i, base = this.n;
    for (i = 0; i < p.count; i++) {
      v.fromBufferAttribute(p, i).applyMatrix4(mtx);
      this.pos.push(v.x, v.y, v.z);
      nv.fromBufferAttribute(nr, i).applyMatrix3(nm).normalize();
      this.nor.push(nv.x, nv.y, nv.z);
      this.col.push(col.r, col.g, col.b);
    }
    if (ix) { for (i = 0; i < ix.count; i++) this.idx.push(base + ix.getX(i)); }
    else { for (i = 0; i < p.count; i++) this.idx.push(base + i); }
    this.n += p.count;
    if (geo.dispose) geo.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 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, n