<!-- NEW7 — TEST 7 · THE WOVEN CITY · part 445 -->
 {
        const target = 4 + 55 * Math.pow(Math.max(0.06, peak), 1.5);
        const floors = Math.max(4, Math.min(60, Math.round(target * (0.8 + rng() * 0.45))));
        const w = 11 + Math.floor(rng() * 6), d = 11 + Math.floor(rng() * 6);
        const tiers = floors >= 34 && rng() < 0.5 ? 2 : (floors >= 16 && rng() < 0.55 ? 1 : 0);
        const variant = Math.floor(rng() * FASHION.variants);
        const crown = FASHION.crowns[Math.floor(rng() * (floors >= 40 ? 4 : 3))];
        const tint = FASHION.tints[Math.floor(rng() * FASHION.tints.length)];
        const sig = [variant, floors, w, d, tiers, crown, tint].join('|');
        if (seen.has(sig)) continue;
        seen.add(sig);
        rec = { floors, w, d, tiers, variant, crown, tint };
      }
      if (!rec) { // plaza plot
        const s = new THREE.BoxGeometry(15.5, 0.14, 15.5); s.translate(px, 0.37, pz);
        pushGeo(THREE, slabBucket, s, 0xa39a88);
        continue;
      }
      rec.x = px; rec.z = pz; rec.h = rec.floors * FASHION.floorH;
      out.push({ x: px, z: pz, w: rec.w, d: rec.d, h: rec.h });
      const spec = variantSpec(rec.variant);
      const splits = rec.tiers === 0 ? [rec.floors]
        : rec.tiers === 1 ? [Math.round(rec.floors * 0.6), 0]
        : [Math.round(rec.floors * 0.5), Math.round(rec.floors * 0.3), 0];
      splits[splits.length - 1] = rec.floors - splits.slice(0, -1).reduce((s, x) => s + x, 0);
      let y = 0.3, tw = rec.w, td = rec.d;
      for (const tf of splits) {
        if (tf < 1) continue;
        const th = tf * FASHION.floorH;
        const geo = dressBox(THREE, tw, th, td, tf, spec);
        geo.translate(px, y + th / 2, pz);
        pushGeo(THREE, facadeBuckets[rec.variant], geo, rec.tint);
        y += th;
        tw = Math.max(8, tw * (0.78 + rng() * 0.1));
        td = Math.max(8, td * (0.78 + rng() * 0.1));
      }
      const ct = FASHION.crownStone[Math.floor(rng() * FASHION.crownStone.length)];
      const crownBox = (cw2, ch, cd2, cy) => { const geo = new THREE.BoxGeometry(cw2, ch, cd2); geo.translate(px, cy, pz); pushGeo(THREE, crownBucket, geo, ct); };
      if (rec.crown === 'lip') crownBox(tw + 0.7, 1.1, td + 0.7, y + 0.55);
      else if (rec.crown === 'cap') crownBox(tw * 0.66, 2.6, td * 0.66, y + 1.3);
      else if (rec.crown === 'step') { crownBox(tw * 0.8, 1.6, td * 0.8, y + 0.8); crownBox(tw * 0.56, 1.6, td * 0.56, y + 2.4); }
      else { const sh = 7 + rng() * 5; crownBox(1.5, sh, 1.5, y + sh / 2); crownBox(0.8, 2, 0.8, y + sh + 1); }
      built++; total++;
    }
    return built;
  }
  function finalize(group) {
    for (let v = 0; v < FASHION.variants; v++) {
      if (facadeBuckets[v].vert === 0) continue;
      group.add(buildMeshF(THREE, facadeBuckets[v], facadeMats[v]));
    }
    if (crownBucket.vert) group.add(buildMeshF(THREE, crownBucket, new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.9, metalness: 0.02 })));
    if (slabBucket.vert) { const sm = buildMeshF(THREE, slabBucket, new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.95, metalness: 0 })); sm.castShadow = false; group.add(sm); }
  }
  return { buildBlock, finalize, get total() { return total; }, unique: () => seen.size };
}

// ---------- water geometry ----------
function ribbon(THREE, pts, width, y) {
  const pos = [], idx = [];
  for (let i = 0; i < pts.length; i++) {
    const p = pts[i], pn = pts[Math.min(i + 1, pts.length - 1)], pp = pts[Math.max(i - 1, 0)];
    let dx = pn[0] - pp[0], dz = pn[1] - pp[1]; const l = Math.hypot(dx, dz) || 1; dx /= l; dz /= l;
    const nx = -dz * width / 2, nz = dx * width / 2;
    pos.push(p[0] - nx, y, p[1] - nz, p[0] + nx, y, p[1] + nz);
    if (i > 0) { const a = (i - 1) * 2; idx.push(a, a + 1, a + 2, a + 1, a + 3, a + 2); }
  }
  const g = new THREE.BufferGeometry();
  g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
  g.setAttribute('uv', new THREE.Float32BufferAttribute(new Float32Array((pos.length / 3) * 2), 2));
  g.setIndex(idx); g.computeVertexNormals();
  return g;
}
function distToSeg(px, pz, a, b) { const vx = b[0] - a[0], vz = b[1] - a[1], wx = px - a[0], wz = pz - a[1]; const c1 = vx * wx + vz * wz, c2 = vx * vx + vz * vz; const t = c2 ? Math.max(0, Math.min(1, c1 / c2)) : 0; const dx = wx - vx * t, dz = wz - vz * t; return Math.hypot(dx, dz); }

// ---------- the build ----------
export function buildWorld(THREE, MAP) {
  const mergeGeometries = (gs) => mergeGeos(THREE, gs);
  const group = new THREE.Group(); group.name = 'WORLD_' + MAP.id.toUpperCase();
  const buildings = [], decks = [];
  const counts = { fashion: 0, towers: 0, houses: 0, bridges: 0, regions: MAP.regions.length, trees: 0, schools: 0, municipal: 0, gas: 0, busStops: (MAP.busStops || []).length };
  let forge = null;
  const bankGeos = [], waterGeos = [];
  const inRiver = (x, z) => { for (const r of MAP.water.rivers) { for (let i = 0; i < r.pts.length - 1; i++) if (distToSeg(x