<!-- SOLID CITY · part 2836 -->
turn a + (b - a) * t; }
    return L(
      L(L(vhash(xi, yi, zi), vhash(xi + 1, yi, zi), u), L(vhash(xi, yi + 1, zi), vhash(xi + 1, yi + 1, zi), u), v),
      L(L(vhash(xi, yi, zi + 1), vhash(xi + 1, yi, zi + 1), u), L(vhash(xi, yi + 1, zi + 1), vhash(xi + 1, yi + 1, zi + 1), u), v), w) * 2 - 1;
  }
  function fbm(x, y, z, oct) {
    var val = 0, amp = 1, sum = 0, fx = x, fy = y, fz = z;
    for (var o = 0; o < oct; o++) { val += vnoise(fx, fy, fz) * amp; sum += amp; amp *= 0.5; fx *= 2.03; fy *= 2.03; fz *= 2.03; }
    return val / sum;
  }
  function tearOf(geo, before) {
    var p = geo.attributes.position, v = new T.Vector3(), seen = {}, tear = 0;
    for (var k = 0; k < p.count; k++) {
      var key = before[k].map(function (q) { return q.toFixed(3); }).join(',');
      v.fromBufferAttribute(p, k);
      if (seen[key]) tear = Math.max(tear, v.distanceTo(seen[key])); else seen[key] = v.clone();
    }
    return tear;
  }
  function snapshot(geo) { var p = geo.attributes.position, v = new T.Vector3(), out = []; for (var i = 0; i < p.count; i++) { v.fromBufferAttribute(p, i); out.push([v.x, v.y, v.z]); } return out; }
  /* displace along the radial direction by fbm — the
   * organic lump-maker; squashY keeps crowns wide */
  function fbmDisplace(geo, freq, amp, seed, squashY) {
    var before = snapshot(geo);
    var p = geo.attributes.position, v = new T.Vector3();
    for (var i = 0; i < p.count; i++) {
      v.fromBufferAttribute(p, i);
      var len = v.length() || 1;
      var d = 1 + fbm(v.x * freq + seed, v.y * freq + seed * 1.7, v.z * freq - seed, 3) * amp;
      p.setXYZ(i, v.x * d, v.y * d * (squashY || 1), v.z * d);
    }
    geo.computeVertexNormals();
    return tearOf(geo, before);
  }
  /* the baked curve: offset grows with height squared */
  function bendGeo(geo, amt, h) {
    var p = geo.attributes.position;
    for (var i = 0; i < p.count; i++) { var y = p.getY(i); var t = 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 };
  });
  c