<!-- PLAYA 2.0'S WORLD · part 5 -->
, 0.36, 0.3, x, 2.15, z); k.box(M.black, 0.42, 0.14, 0.42, x, 2.4, z);
      cols.push([x - 0.2, x + 0.2, z - 0.2, z + 0.2, 2.47]);
    }
  }
  k.build(); return placeLocal(scene, g, opts, cols);
}
export default { createLanternPath };


===== FILE: playa-world/mkit.js =====
// mkit.js — shared tiny merge kit for Playa 2.0 landmark modules (min dimension 0.13, one mesh per material)
export const MIN = 0.13;
export function mkit(THREE, group) {
  const buckets = new Map(); group.userData.minDim = Infinity;
  const d = v => { if (v < MIN) throw new Error('dimension below 0.13: ' + v); group.userData.minDim = Math.min(group.userData.minDim, v); return v; };
  const add = (mat, geo, x = 0, y = 0, z = 0, rx = 0, ry = 0, rz = 0) => {
    const m = new THREE.Matrix4().compose(new THREE.Vector3(x, y, z), new THREE.Quaternion().setFromEuler(new THREE.Euler(rx, ry, rz)), new THREE.Vector3(1, 1, 1));
    const g = geo.index ? geo.toNonIndexed() : geo.clone(); g.applyMatrix4(m); geo.dispose();
    if (!buckets.has(mat)) buckets.set(mat, []); buckets.get(mat).push(g);
  };
  const box = (mat, w, h, dd, x, y, z, rx, ry, rz) => add(mat, new THREE.BoxGeometry(d(w), d(h), d(dd)), x, y, z, rx, ry, rz);
  const cyl = (mat, rt, rb, h, seg, x, y, z, rx, ry, rz) => add(mat, new THREE.CylinderGeometry(d(rt * 2) / 2, d(rb * 2) / 2, d(h), seg || 12), x, y, z, rx, ry, rz);
  const torus = (mat, R, r, x, y, z, rx = Math.PI / 2, ry = 0, rz = 0, arc = Math.PI * 2) => add(mat, new THREE.TorusGeometry(d(R * 2) / 2, d(r * 2) / 2, 8, 40, arc), x, y, z, rx, ry, rz);
  const cone = (mat, r, h, seg, x, y, z, rx, ry, rz) => add(mat, new THREE.ConeGeometry(d(r * 2) / 2, d(h), seg || 10), x, y, z, rx, ry, rz);
  const build = () => {
    for (const [mat, list] of buckets) {
      let n = 0; list.forEach(g => n += g.attributes.position.count);
      const pos = new Float32Array(n * 3), nor = new Float32Array(n * 3); let o = 0;
      list.forEach(g => { pos.set(g.attributes.position.array, o * 3); nor.set(g.attributes.normal.array, o * 3); o += g.attributes.position.count; g.dispose(); });
      const geo = new THREE.BufferGeometry(); geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); geo.setAttribute('normal', new THREE.BufferAttribute(nor, 3)); geo.computeBoundingSphere();
      group.add(new THREE.Mesh(geo, mat));
    }
    buckets.clear();
  };
  return { d, add, box, cyl, torus, cone, build };
}
export function pmats(THREE, opts = {}) {
  const red = opts.red ?? 0xc8102e;
  return {
    black: new THREE.MeshStandardMaterial({ color: 0x0b0b0d, metalness: 0.6, roughness: 0.35 }),
    red: new THREE.MeshStandardMaterial({ color: red, metalness: 0.3, roughness: 0.4 }),
    glow: new THREE.MeshStandardMaterial({ color: red, emissive: red, emissiveIntensity: opts.glow ?? 1.8, roughness: 0.3 }),
    stone: new THREE.MeshStandardMaterial({ color: 0x2a2a2f, roughness: 0.85 }),
  };
}
// place group at opts.position/rotationY; colliders given in local coords [x0,x1,z0,z1,top,bottom] -> world AABBs
export function placeLocal(scene, g, opts, cols) {
  const p = opts.position || [0, 0, 0]; g.position.set(p[0], p[1], p[2]); g.rotation.y = opts.rotationY || 0;
  const s = opts.scale ?? 1; if (s < MIN) throw new Error('scale below 0.13'); g.scale.setScalar(s);
  scene.add(g); g.updateMatrixWorld(true); const out = [];
  for (const [x0, x1, z0, z1, top, bot = 0] of cols) {
    let a = Infinity, b = -Infinity, c = Infinity, e = -Infinity;
    for (const [x, z] of [[x0, z0], [x1, z0], [x0, z1], [x1, z1]]) { const v = new g.position.constructor(x, 0, z).applyMatrix4(g.matrixWorld); a = Math.min(a, v.x); b = Math.max(b, v.x); c = Math.min(c, v.z); e = Math.max(e, v.z); }
    const col = [a, b, c, e, g.position.y + top * s, g.position.y + bot * s]; out.push(col);
    if (opts.ctx && opts.ctx.addCollider) opts.ctx.addCollider(...col);
  }
  g.userData.colliders = out; return g;
}


===== FILE: playa-world/monorail-loop.js =====
// monorail-loop.js — Playa 2.0 original: elevated glowing red monorail ring on pylons with a moving car. fn(THREE, scene, opts) -> Group
import { mkit, pmats, placeLocal } from './mkit.js';
export function createMonorailLoop(THREE, scene, opts = {}) {
  const g = new THREE.Group(); g.name = 'MonorailLoop'; const M = pmats(THREE, opts), k = mkit(THREE, g);
  const R = opts.radius ?? 46, H = opts.height ?? 8, n = opts.pylons ?? 10, a0 = opts.pylonOffset ?? Math.PI / 10;
  k.torus(M.black, R, 0.45, 0, H, 0, Math.PI / 2, 0, 0);          // beam
  k.torus(M.glow, R, 0.13, 0, H + 0.48, 0, Math.PI / 2, 0, 0);    // glowing rail
  k.torus(M.glow, R - 0.5, 0.08, 0, H - 0.3, 0, Math.PI / 2, 0, 0);
  const cols = [];
  for (let i = 0; i < n; i++) {
    const a = a0 + i / n * Math.PI * 2, x = Math.cos(a) * R, z = Math.sin(a) * R;
    k.cyl(M.black, 0.45, 0.6, H - 0.4, 8, x, (H - 0.4) / 2, z);
    k.box(M.red, 1.6, 0.4, 1.6, x, 0.2, z); k.box(M.red, 1.4, 0.3, 1.4, x, H - 0.55, z);
    k.box(M.glow, 0.14,