<!-- PLAYA 2.0'S WORLD · part 2 -->
LF ?? 15, LIM = L.LIM ?? 190, RZ0 = L.RZ0 ?? 106, RZ1 = L.RZ1 ?? 134, BR = L.BRIDGES || [];
  const N = Math.floor((LIM - H) / P), blocks = [];
  for (let i = -N; i <= N; i++) for (let j = -N; j <= N; j++) {
    const x = i * P, z = j * P; if (z > RZ0 - H && z < RZ1 + H) continue; // river row has no block
    blocks.push({ i, j, type: !i && !j ? 'plaza' : Math.max(Math.abs(i), Math.abs(j)) <= 1 ? 'tower' : 'block', x0: x - H, x1: x + H, z0: z - H, z1: z + H });
  }
  const edge = N * P + H + 0.8, band = { c: (edge + LIM) / 2, hw: (LIM - edge) / 2 }; // open ring between outer blocks (+lamps) and the walk limit
  return { P, H, LIM, RZ0, RZ1, BR, N, blocks, band, road: (P - 2 * H) / 2 - 0.6 };
}
const hit = (a, b, m = 0) => a.x0 < b.x1 + m && a.x1 > b.x0 - m && a.z0 < b.z1 + m && a.z1 > b.z0 - m;
const rectAt = (x, z, hx, hz) => ({ x0: x - hx, x1: x + hx, z0: z - hz, z1: z + hz });
function spotFree(ctx, I, r, water) {
  if (I.blocks.some(b => hit(r, b, 0.5))) return false;
  if (!water && r.z1 > I.RZ0 - 2 && r.z0 < I.RZ1 + 2) return false;
  if (r.x0 < -I.LIM || r.x1 > I.LIM || r.z0 < -I.LIM || r.z1 > I.LIM) return false;
  return !(ctx.__new8Claims || []).some(c => hit(r, c, 1));
}
// kind 'land': half-extents hx,hz must fit the open outer ring (south side skipped: bus route A runs there).
// kind 'bridge': spans the river along a road line without an existing core bridge (rotated to run N-S).
function pickSpot(ctx, opts, kind, hx, hz, pref = 0) {
  const I = layoutInfo(ctx.layout); let cands = [];
  if (kind === 'bridge') {
    const zc = (I.RZ0 + I.RZ1) / 2;
    for (let k = -I.N; k < I.N; k++) { const x = (k + 0.5) * I.P; if (I.BR.some(b => Math.abs(b - x) < 12)) continue; cands.push({ x, z: zc, ry: Math.PI / 2, r: rectAt(x, zc, hz, hx) }); }
  } else {
    const c = I.band.c;
    for (let t = -I.N * I.P; t <= I.N * I.P; t += I.P / 2) cands.push({ x: t, z: -c }, { x: c, z: t }, { x: -c, z: t });
    cands = cands.map(o => ({ ...o, ry: 0, r: rectAt(o.x, o.z, hx, hz) }));
  }
  let chosen = null;
  if (opts.position) { const [x, , z] = opts.position, ry = opts.rotationY || 0, sw = Math.abs(Math.sin(ry)) > 0.5; const r = sw ? rectAt(x, z, hz, hx) : rectAt(x, z, hx, hz); if (spotFree(ctx, I, r, kind === 'bridge')) chosen = { x, z, ry, r }; else console.warn('[new8 module] position', opts.position, 'overlaps the core layout; picking an open spot'); }
  const fits = kind === 'bridge' ? Math.max(hz, 0) <= I.road : Math.max(hx, hz) <= I.band.hw;
  if (!chosen && fits) for (let n = 0; n < cands.length; n++) { const o = cands[(pref + n) % cands.length]; if (spotFree(ctx, I, o.r, kind === 'bridge')) { chosen = o; break; } }
  if (!chosen) throw new Error('no open spot for ' + kind + ' footprint ' + hx + 'x' + hz);
  (ctx.__new8Claims ||= []).push(chosen.r); return chosen;
}
// place group: with ctx -> open spot + ctx.groundY + colliders; without ctx -> opts.position as before.
function place(scene, group, opts, foot) {
  const ctx = opts.ctx, s = opts.scale ?? 1; if (s < MIN) throw new Error('scale below 0.13'); group.scale.setScalar(s);
  if (ctx && ctx.layout) {
    const sp = pickSpot(ctx, opts, foot.kind, foot.hx * s, foot.hz * s, opts.spot ?? foot.pref ?? 0);
    const gy = typeof ctx.groundY === 'function' ? ctx.groundY(sp.x, sp.z) : 0;
    group.position.set(sp.x, gy, sp.z); group.rotation.y = sp.ry;
  } else {
    const p = opts.position || [0, 0, 0]; group.position.set(p[0], p[1], p[2]); group.rotation.y = opts.rotationY || 0;
  }
  scene.add(group);
  const cols = [];
  if (ctx && typeof ctx.addCollider === 'function') {
    scene.updateMatrixWorld(true); const m = group.matrixWorld, THREE_V = group.position.constructor;
    for (const [x0, x1, z0, z1, h] of foot.cols || []) {
      let a = Infinity, b = -Infinity, c = Infinity, d = -Infinity;
      for (const [x, z] of [[x0, z0], [x1, z0], [x0, z1], [x1, z1]]) { const v = new THREE_V(x, 0, z).applyMatrix4(m); a = Math.min(a, v.x); b = Math.max(b, v.x); c = Math.min(c, v.z); d = Math.max(d, v.z); }
      const top = group.position.y + h * s; ctx.addCollider(a, b, c, d, top); cols.push([a, b, c, d, top]);
    }
  }
  group.userData.colliders = cols; group.userData.footprint = foot.kind; return group;
}

// Walkable deck for the new8 core (ctx): core groundY = spot y + dh on the deck, ramping over the last RAMP units
// down to street level (0.03) at each end. Walking surface (graphite top) sits exactly at dh; ramps replace end abutments.
const RAMP = 5, STREET = -0.1; // local y of street at deck ends (0.03 street - 0.13 spot ground)
function rampBox(k, mat, w, t, d, xEnd, z, dh, yTopOff = 0) { // slab whose top runs from (xEnd - sign*RAMP, dh) to (xEnd, STREET)
  const sg = Math.sign(xEnd), x0 = xEnd - sg * RAMP, dy = dh - STREET, len = Math.hypot(RAMP, dy), a = Math.atan2(dy, RAMP);
  const rz = sg > 0 ? -a : a, nx = -Math.sin(rz), ny = Math.cos(rz);
  const mx = (x0 + xEnd) / 2, my = (dh + STREET) / 2 + yTopOff;
  k