<!-- SOLIDIVISM · part 61 -->
return dedupe(FALLBACK_PAGES.concat(EXTRA_PAGES));
}
function dedupe(list) {
  const seen = new Set(), out = [];
  for (const p of list) { const k = p.path.replace(/\/$/, '') || '/'; if (!seen.has(k)) { seen.add(k); out.push({ path: k }); } }
  return out;
}

/* ---------------- RENDERER / SCENE — the smallest safe level ---------
 * pixelRatio capped at 1.5, no post-processing, no shadow maps at
 * all (the spec allows one 1024 map; clean wins), two lights total:
 * one hemisphere + one directional. Frustum culling left ON.     */
const canvas = document.getElementById('view');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 1.5));
renderer.setSize(innerWidth, innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0a1220);
scene.fog = new THREE.Fog(0x0a1220, 120, 340);

const camera = new THREE.PerspectiveCamera(58, innerWidth / innerHeight, 0.1, 900);
scene.add(new THREE.HemisphereLight(0xbdd3ea, 0x1c1a14, 0.95));
const sun = new THREE.DirectionalLight(0xffe6b0, 1.5);
sun.position.set(60, 110, 30);
scene.add(sun);

/* ---------------- THE GRID -------------------------------------------
 * Blocks of BLOCK meters, streets STREET wide between them. Lots are
 * dealt round-robin from the avenue ring outward, one building per
 * lot, every lot fronting a street to the south.                   */
const BLOCK = 34, STREET = 12, PITCH = BLOCK + STREET;
const cityGroup = new THREE.Group(); scene.add(cityGroup);
const buildings = [];          // { path, title, cat, x, z, w, d, h, door:{x,z}, aabb }
const colliders = [];          // street-mode AABBs { minX, maxX, minZ, maxZ }
const hitMeshes = [];          // invisible hover targets, one per building

/* Precomputed lot deal: every block coordinate sorted ring by ring
 * outward from the plaza (which keeps the center block), so no two
 * buildings ever share a lot and the city grows evenly.          */
let LOTS = [];
function dealLots(count) {
  const R = Math.ceil(Math.sqrt(count)) + 1, cells = [];
  for (let gz = -R; gz <= R; gz++) for (let gx = -R; gx <= R; gx++) {
    if (gx === 0 && gz === 0) continue;                       /* the plaza */
    cells.push({ gx, gz, ring: Math.max(Math.abs(gx), Math.abs(gz)) });
  }
  cells.sort((a, b) => a.ring - b.ring || a.gz - b.gz || a.gx - b.gx);
  LOTS = cells.slice(0, count).map(c => ({ x: c.gx * PITCH, z: c.gz * PITCH }));
}

/* Canvas nameplates: one texture per building (rooftop sign). */
function nameplateTexture(text, colorCss) {
  const c = document.createElement('canvas'); c.width = 512; c.height = 96;
  const g = c.getContext('2d');
  g.fillStyle = 'rgba(4,6,10,0.92)'; g.fillRect(0, 0, 512, 96);
  g.strokeStyle = colorCss; g.lineWidth = 4; g.strokeRect(4, 4, 504, 88);
  g.fillStyle = '#f5ecd2'; g.font = 'bold 40px Georgia, serif'; g.textAlign = 'center'; g.textBaseline = 'middle';
  let t = text; while (g.measureText(t).width > 470 && t.length > 4) t = t.slice(0, -2);
  if (t !== text) t += '…';
  g.fillText(t, 256, 50);
  const tex = new THREE.CanvasTexture(c); tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 4;
  return tex;
}

/* ---------------- buildCity() --------------------------------------- */
function buildCity(pages) {
  /* -- ground: one big slab, then MERGED streets + sidewalks -- */
  const groundGeo = [];
  const slab = new THREE.BoxGeometry(900, 1, 900); slab.translate(0, -0.5, 0); groundGeo.push(slab);
  const city = new THREE.Mesh(mergeGeometries(groundGeo),
    new THREE.MeshStandardMaterial({ color: 0x11161d, roughness: 1 }));
  city.name = 'the-ground'; cityGroup.add(city);

  const extent = Math.ceil(Math.sqrt(pages.length)) * PITCH + PITCH;
  const roadGeos = [], walkGeos = [];
  /* street lines sit BETWEEN block centers: at half-pitch offsets */
  for (let sLine = -extent + PITCH / 2; sLine <= extent; sLine += PITCH) {
    const r1 = new THREE.BoxGeometry(STREET, 0.06, extent * 2); r1.translate(sLine, 0.03, 0); roadGeos.push(r1);
    const r2 = new THREE.BoxGeometry(extent * 2, 0.06, STREET); r2.translate(0, 0.03, sLine); roadGeos.push(r2);
    for (const off of [-STREET / 2 - 0.8, STREET / 2 + 0.8]) {
      const w1 = new THREE.BoxGeometry(1.6, 0.1, extent * 2); w1.translate(sLine + off, 0.05, 0); walkGeos.push(w1);
      const w2 = new THREE.BoxGeometry(extent * 2, 0.1, 1.6); w2.translate(0, 0.05, sLine + off); walkGeos.push(w2);
    }
  }
  const roads = new THREE.Mesh(mergeGeometries(roadGeos),
    new THREE.MeshStandardMaterial({ color: 0x181d24, roughness: 0.95 }));
  roads.name = 'the-streets'; cityGroup.add(roads);
  const walks = new THREE.Mesh(mergeGeometries(walkGeos),
    new THREE.MeshStandardMaterial({ color: 0x2a3138, roughness: 1 }));
  walks.name = 'the