<!-- SOLID CITY · part 2813 -->
 Math.max(0, y) / h; p.setX(i, p.getX(i) + amt * t * t); }
    geo.computeVertexNormals();
  }
  var integrity = {};
  /* ——— STONES ———
   * Reason for the sizes: real boulder fields are a power
   * law — a hundred pebbles for every boulder. Scale runs
   * 0.25–1.85 m on the same curve (pow 2.2), so the eye
   * meets mostly small stones and, rarely, a giant. */
  var stoneGeo = new T.IcosahedronGeometry(1, 1);
  integrity.stoneTear = fbmDisplace(stoneGeo, 1.35, 0.26, 7.7, 0.84);
  var stones = scatter(hiTier ? 850 : 500, function (x, z, h) { if (h < 2.2) return null; var ry = R() * 6.28, s = 0.25 + Math.pow(R(), 2.2) * 1.6; /* TALUS SETTLE (grounding law): the body's bottom (h − 0.62·s at placement) seats toward the low rim of its own footprint, capped at 1.4·s so a crest stone embeds rather than balances */ var st = seatSettle(x, z, h - 0.62 * s, s, 0.25, 1.4 * s); /* TALUS REJECTION (grounding law, second clause): a stone smaller than the fall it would straddle does not land at all — real talus gathers BELOW the knife-edge, so the scatter walks on and the stone lands where stones gather */ if (h - ringLow(x, z, s) > 2.2 * s + 0.4) return null; return { x: x, z: z, y: h - st, ry: ry, s: s }; });
  counts.stones = stones.length; note('stones', stones);
  instanced('stones', stoneGeo, lambert(0xffffff, { flatShading: true }), stones,
    function (d, p) { d.position.set(p.x, p.y + p.s * 0.18, p.z); d.rotation.set(0, p.ry, 0); d.scale.set(p.s, p.s * 0.8, p.s); },
    function (c) { var g = 0.26 + CR() * 0.18; c.setRGB(g * 1.05, g, g * 0.94); });

  /* ——— GRASS: meadow-clustered, wind in its bones ——— */
  var meadowC = []; for (var ci = 0; ci < 70; ci++) meadowC.push({ x: -3200 + R() * 6400, z: -3300 + R() * 6600 });
  function meadowy(x, z) { for (var i = 0; i < meadowC.length; i++) { var dx = x - meadowC[i].x, dz = z - meadowC[i].z; if (dx * dx + dz * dz < 900) return true; } return false; }
  var grassGeo = new T.ConeGeometry(0.17, 0.6, 5); grassGeo.translate(0, 0.28, 0);
  var grass = scatter(hiTier ? 6500 : 2600, function (x, z, h) {
    if (h < 2.5 || h > 140 || slope(x, z, h) > 0.55) return null;
    if (!meadowy(x, z) && R() > 0.30) return null;
    /* blade reason: meadow grass stands 0.1–0.5 m; the
     * tuft cone is 0.6 m at scale 1, so scale 0.6–1.25
     * keeps every blade inside the living range */
    return { x: x, z: z, y: h, ry: R() * 6.28, s: 0.6 + R() * 0.65 };
  });
  counts.grass = grass.length; note('grass', grass);
  instanced('grass', grassGeo, sway(lambert(0xffffff), '0.0'), grass,
    function (d, p) { d.position.set(p.x, p.y - 0.03, p.z); d.rotation.set(0, p.ry, 0); d.scale.setScalar(p.s); },
    function (c) { greens(c, 0.20, 0.16); });

  /* ——— FLOWERS: stems in the wind, heads of colour ——— */
  var flowers = scatter(hiTier ? 850 : 380, function (x, z, h) {
    if (h < 2.5 || h > 120 || slope(x, z, h) > 0.5) return null;
    if (!meadowy(x, z) && R() > 0.45) return null;
    return { x: x, z: z, y: h, ry: R() * 6.28, s: 0.8 + R() * 0.5 };
  });
  counts.flowers = flowers.length; note('flowers', flowers);
  var stemGeo = new T.CylinderGeometry(0.012, 0.02, 0.34, 5); stemGeo.translate(0, 0.17, 0);
  instanced('flower-stems', stemGeo, sway(lambert(0x3d6b2f), '0.0'), flowers,
    function (d, p) { d.position.set(p.x, p.y - 0.02, p.z); d.rotation.set(0, p.ry, 0); d.scale.setScalar(p.s); }, null);
  var headGeo = new T.IcosahedronGeometry(0.06, 0);
  var PALETTE = [0xd84a5a, 0xe8b13c, 0x9a6ae0, 0xf2ead8, 0xe07ab0, 0xe2703a];
  instanced('flower-heads', headGeo, sway(lambert(0xffffff), '0.36'), flowers,
    function (d, p) { d.position.set(p.x, p.y + 0.36 * p.s, p.z); d.rotation.set(0, p.ry, 0); d.scale.setScalar(p.s); },
    function (c) { c.setHex(PALETTE[Math.floor(CR() * PALETTE.length)]); });

  /* ——— BUSHES: squashed crystal blobs of green ——— */
  var bushGeo = new T.IcosahedronGeometry(1, 1); bushGeo.scale(1, 0.62, 1); fbmDisplace(bushGeo, 0.9, 0.22, 2.9, 1);
  var bushes = scatter(hiTier ? 480 : 260, function (x, z, h) { if (h < 2.5 || h > 170 || slope(x, z, h) > 0.85) return null; var ry = R() * 6.28, s = 0.5 + R() * 1.1; var st = seatSettle(x, z, h - 0.32 * s, s * 0.9, 0.15, 0.5 * s); return { x: x, z: z, y: h - st, ry: ry, s: s }; });
  counts.bushes = bushes.length; note('bushes', bushes);
  instanced('bushes', bushGeo, lambert(0xffffff, { flatShading: true }), bushes,
    function (d, p) { d.position.set(p.x, p.y + p.s * 0.3, p.z); d.rotation.set(0, p.ry, 0); d.scale.setScalar(p.s); },
    function (c) { greens(c, 0.16, 0.12); });

  /* ——— PALMS ———
   * Researched (USDA + Bishop Museum coconut data): the
   * niu stands 10–20 m (to 30), trunk Ø 20–30 cm swelling
   * to 40–50 at the base, crown spread 6.7–10 m, and it
   * CURVES — storm and shore wind bend it. Ours: an 11 m
   * trunk (Ø 0.32/0.52 at the foot), a 6.8 m crown, lean
   * to 0.18 rad — a young giant's proportions, world-fit. */
  var palmTrunkGeo = new T.CylinderG