<!-- PLAYA 2.0'S WORLD · part 1 -->
===== FILE: playa-world/archway-gate.js =====
// archway-gate.js — Playa 2.0 original: glowing triple-ring archway gate on a stepped plinth. fn(THREE, scene, opts) -> Group
import { mkit, pmats, placeLocal } from './mkit.js';
export function createArchwayGate(THREE, scene, opts = {}) {
  const g = new THREE.Group(); g.name = 'ArchwayGate'; const M = pmats(THREE, opts), k = mkit(THREE, g);
  const W = opts.width ?? 7, Hh = opts.height ?? 7; // opening width, pillar height
  for (const s of [-1, 1]) {
    const x = s * (W / 2 + 0.6);
    k.box(M.black, 1.2, Hh, 1.2, x, Hh / 2, 0);
    k.box(M.glow, 0.14, Hh - 1, 0.14, x - s * 0.62, Hh / 2, 0.62);
    k.box(M.glow, 0.14, Hh - 1, 0.14, x - s * 0.62, Hh / 2, -0.62);
    k.box(M.red, 1.6, 0.4, 1.6, x, 0.2, 0); k.box(M.red, 1.5, 0.3, 1.5, x, Hh, 0);
  }
  const R = W / 2 + 0.6;
  k.torus(M.black, R, 0.55, 0, Hh, 0, 0, 0, 0, Math.PI);
  k.torus(M.glow, R - 0.62, 0.1, 0, Hh, 0.3, 0, 0, 0, Math.PI);
  k.torus(M.glow, R - 0.62, 0.1, 0, Hh, -0.3, 0, 0, 0, Math.PI);
  k.torus(M.red, R + 0.7, 0.15, 0, Hh, 0, 0, 0, 0, Math.PI);
  k.cone(M.glow, 0.5, 1.4, 6, 0, Hh + R + 1.2, 0);
  k.box(M.stone, W + 4, 0.14, 3.2, 0, 0.07, 0); // threshold slab (step-on)
  k.build();
  const px = W / 2 + 0.6;
  return placeLocal(scene, g, opts, [[-px - 0.6, -px + 0.6, -0.6, 0.6, Hh + 0.2], [px - 0.6, px + 0.6, -0.6, 0.6, Hh + 0.2]]);
}
export default { createArchwayGate };


===== FILE: playa-world/bridges-20.js =====
// bridges-20.js — bridges (red/black). Bridges span along X, deck top at opts.deckHeight.
// ---- shared inline helpers (standalone module, no imports) ----
const MIN = 0.13;
function kit(THREE, group) {
  group.userData.minDim = Infinity;
  const buckets = new Map();
  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);
    if (!buckets.has(mat)) buckets.set(mat, []);
    buckets.get(mat).push(g); geo.dispose();
  };
  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);
  // cable/beam between two points (diameter >= 0.13)
  const rod = (mat, r, a, b, seg = 6) => {
    const A = new THREE.Vector3(...a), B = new THREE.Vector3(...b), len = A.distanceTo(B);
    const g = new THREE.CylinderGeometry(d(r * 2) / 2, r, d(len), seg);
    const q = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), B.clone().sub(A).normalize());
    const m = new THREE.Matrix4().compose(A.clone().add(B).multiplyScalar(0.5), q, new THREE.Vector3(1, 1, 1));
    const ng = g.toNonIndexed(); ng.applyMatrix4(m); g.dispose();
    if (!buckets.has(mat)) buckets.set(mat, []); buckets.get(mat).push(ng);
  };
  const torus = (mat, R, r, x, y, z, rx = Math.PI / 2, ry = 0, rz = 0) => add(mat, new THREE.TorusGeometry(d(R * 2) / 2, d(r * 2) / 2, 8, 32), 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();
      const mesh = new THREE.Mesh(geo, mat); mesh.castShadow = mesh.receiveShadow = true; group.add(mesh);
    }
    buckets.clear();
  };
  return { d, add, box, cyl, rod, torus, build };
}
function mats(THREE, opts) {
  const red = opts.red ?? 0xc8102e, black = opts.black ?? 0x0b0b0d;
  return {
    black: new THREE.MeshStandardMaterial({ color: black, metalness: 0.6, roughness: 0.35 }),
    graphite: new THREE.MeshStandardMaterial({ color: 0x23232a, metalness: 0.4, roughness: 0.55 }),
    red: new THREE.MeshStandardMaterial({ color: red, metalness: 0.3, roughness: 0.4 }),
    glow: new THREE.MeshStandardMaterial({ color: red, emissive: red, emissiveIntensity: opts.glow ?? 1.6, roughness: 0.3 }),
    stone: new THREE.MeshStandardMaterial({ color: 0x2c2c30, roughness: 0.85 }),
  };
}
// ---- ctx-aware placement (new8 core loader passes opts.ctx). Without ctx: legacy behaviour. ----
function layoutInfo(L = {}) {
  const P = L.P ?? 40, H = L.HA