<!-- NEW8 — EMBER CITY STRUCTURES · part 3 -->
, th, 0.9, x, th / 2, s * (W / 2 + 0.3));
    for (const y of [dh + 4, th - 3, th - 0.4]) k.box(M.red, 0.6, 0.8, W + 1.5, x, y, 0);
    k.box(M.glow, 0.14, 0.14, W + 1.6, x + 0.35, th - 0.4, 0);
  }
  const cableY = x => { const ax = Math.abs(x); if (ax <= tx) { const t = x / tx; return dh + 1.2 + (th - dh - 1.2) * t * t; } const t = (ax - tx) / (L / 2 - tx); return th - (th - dh) * t; };
  const seg = 40;
  for (const s of [-1, 1]) {
    const z = s * (W / 2 + 0.3);
    for (let i = 0; i < seg; i++) { const x0 = -L / 2 + i * L / seg, x1 = x0 + L / seg; k.rod(M.black, 0.11, [x0, cableY(x0), z], [x1, cableY(x1), z]); }
    for (let x = -tx + 2; x < tx - 1; x += 2) if (cableY(x) - dh > 0.4) k.rod(M.red, 0.07, [x, dh, z], [x, cableY(x), z], 4);
  }
  if (!walk) for (const x of [-L / 2, L / 2]) k.box(M.stone, 2, dh, W + 1, x, dh / 2, 0);
  for (const x of [-tx, tx]) k.box(M.stone, 2.4, 0.6, W + 2.4, x, 0.3, 0);
  if (opts.ctx) for (const x of [-tx, tx]) k.box(M.stone, 2.4, 2.6, W + 2.4, x, -1.3, 0); // piers down to the riverbed
  const rw = s => [-L / 2, L / 2, s * (W / 2 - 0.1) - 0.1, s * (W / 2 - 0.1) + 0.1, dh + 0.6];
  const cols = [rw(-1), rw(1)];
  // railings only: no end-abutment / pier / tower-base colliders (the core deck makes the walkway passable)
  k.build(); return place(scene, g, opts, { kind: 'bridge', hx: L / 2 + 1, hz: Math.max(W / 2 + 0.75, (W + 2.4) / 2), pref: 3, cols });
}

// Arch bridge: tied red steel arch over a black deck with hangers and glowing deck lights.
export function createArchBridge(THREE, scene, opts = {}) {
  const g = new THREE.Group(); g.name = 'ArchBridge';
  const M = mats(THREE, opts), k = kit(THREE, g);
  const L = opts.length ?? 40, W = opts.width ?? 5, dh = opts.deckHeight ?? 2, rise = opts.rise ?? 9;
  const walk = walkDeck(k, M, opts, L, W, dh, W - 1, W / 2 + 0.2, 0.6), Ld = walk ? L - 2 * RAMP : L, ys = walk ? -0.14 : 0;
  if (!walk) { k.box(M.black, L, 0.5, W, 0, dh - 0.25, 0); k.box(M.graphite, L, 0.14, W - 1, 0, dh + 0.07, 0); }
  const seg = 32, arch = x => dh + rise * (1 - (2 * x / L) ** 2);
  for (const s of [-1, 1]) {
    const z = s * (W / 2 + 0.2);
    for (let i = 0; i < seg; i++) { const x0 = -L / 2 + i * L / seg, x1 = x0 + L / seg; k.rod(M.red, 0.3, [x0, arch(x0), z], [x1, arch(x1), z], 8); k.rod(M.glow, 0.07, [x0, arch(x0) + 0.32, z], [x1, arch(x1) + 0.32, z], 4); }
    for (let x = -L / 2 + 2.5; x < L / 2 - 2; x += 2.5) k.rod(M.black, 0.07, [x, dh, z], [x, arch(x), z], 4);
    k.box(M.glow, Ld, 0.14, 0.14, 0, dh + 0.05 + ys, s * (W / 2 - 0.15));
  }
  for (let x = -L / 2 + 5; x < L / 2 - 4; x += 5) k.rod(M.red, 0.15, [x, arch(x), -W / 2 - 0.2], [x, arch(x), W / 2 + 0.2], 6);
  if (!walk) for (const x of [-L / 2, L / 2]) k.box(M.stone, 2.4, dh, W + 1.2, x, dh / 2, 0);
  const cols = [];
  for (const s of [-1, 1]) cols.push([-L / 2, L / 2, s * (W / 2 + 0.2) - 0.3, s * (W / 2 + 0.2) + 0.3, dh + rise]); // arch ribs + railing line
  // railings only: no end-abutment colliders
  k.build(); return place(scene, g, opts, { kind: 'bridge', hx: L / 2 + 1.2, hz: Math.max((W + 1.2) / 2, W / 2 + 0.5), pref: 1, cols });
}
export default { createSuspensionBridge, createArchBridge };


===== FILE: public/new8/modules.json =====
{
  "modules": [
    { "src": "./structures-20.js", "export": "createTwistingSpire", "opts": { "floors": 28 }, "enabled": true },
    { "src": "./structures-20.js", "export": "createRingedObelisk", "opts": { "rings": 5 }, "enabled": true },
    { "src": "./structures-20.js", "export": "createLatticeTower", "opts": {}, "enabled": true },
    { "src": "./bridges-20.js", "export": "createSuspensionBridge", "opts": { "length": 60 }, "enabled": true },
    { "src": "./bridges-20.js", "export": "createArchBridge", "opts": { "length": 40 }, "enabled": true },
    { "src": "./plaza-20.js", "export": "createCentralPlaza", "opts": {}, "enabled": true }
  ]
}


===== FILE: public/new8/plaza-20.js =====
// plaza-20.js — central plaza piece: tiled disc, fountain, central monument, emissive red accents.
// ---- 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, 