<!-- NEW7 — TEST 7 · THE WOVEN CITY · part 443 -->
2);
    const gi = g.index.array;
    for (let i = 0; i < gi.length; i++) idx[io + i] = gi[i] + vo;
    vo += g.attributes.position.count; io += gi.length;
  }
  const out = new THREE.BufferGeometry();
  out.setAttribute('position', new THREE.BufferAttribute(pos, 3));
  out.setAttribute('normal', new THREE.BufferAttribute(nor, 3));
  out.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
  out.setIndex(new THREE.BufferAttribute(idx, 1));
  return out;
}

export function line(a, b, n) { const p = []; for (let i = 0; i <= n; i++) { const t = i / n; p.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]); } return p; }
export function arc(cx, cz, r, a0, a1, n) { const p = []; for (let i = 0; i <= n; i++) { const a = a0 + (a1 - a0) * (i / n); p.push([cx + Math.cos(a) * r, cz + Math.sin(a) * r]); } return p; }
export function join() { const out = []; for (const s of arguments) { for (const p of s) { const l = out[out.length - 1]; if (!l || Math.hypot(l[0] - p[0], l[1] - p[1]) > 0.01) out.push(p); } } return out; }

function mulberry32(a) { return function () { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
const M4 = (THREE, x, y, z, ry, sx, sy, sz) => { const m = new THREE.Matrix4(); const q = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, ry || 0, 0)); m.compose(new THREE.Vector3(x, y, z), q, new THREE.Vector3(sx || 1, sy || 1, sz || 1)); return m; };

// ---------- towers kit (secondary districts: waterfront rows, canal rows) --
// One shared window plate, tinted stone boxes — kin to the fashion, humbler.
function towerPlate(THREE) {
  const cv = document.createElement('canvas'); cv.width = 128; cv.height = 256; const g = cv.getContext('2d');
  g.fillStyle = '#a99c86'; g.fillRect(0, 0, 128, 256);
  for (let r = 0; r < 8; r++) for (let c = 0; c < 4; c++) { g.fillStyle = (r * 4 + c) % 7 === 0 ? '#ffd9a0' : '#66788c'; g.fillRect(10 + c * 30, 12 + r * 26, 20, 16); }
  g.fillStyle = '#35312c'; g.fillRect(0, 224, 128, 32);
  g.fillStyle = '#77726c'; g.fillRect(0, 0, 16, 16);
  const tx = new THREE.CanvasTexture(cv); tx.wrapS = tx.wrapT = THREE.RepeatWrapping; tx.colorSpace = THREE.SRGBColorSpace;
  return tx;
}
function towersKit(THREE, group, R, rnd, buildings, counts, inWater) {
  const tex = towerPlate(THREE);
  const geos = [];
  const cols = Math.max(2, Math.floor(R.w / 64)), rows = Math.max(1, Math.floor(R.d / 64));
  for (let i = 0; i < cols; i++) for (let j = 0; j < rows; j++) {
    if (rnd() < 0.18) continue;
    const x = R.x - R.w / 2 + (i + 0.5) * (R.w / cols) + (rnd() - 0.5) * 8, z = R.z - R.d / 2 + (j + 0.5) * (R.d / rows) + (rnd() - 0.5) * 8;
    if (inWater && inWater(x, z)) continue;
    const w = 20 + rnd() * 10, d = 20 + rnd() * 10, floors = 5 + Math.floor(rnd() * (R.floors || 14)), h = floors * 3.1;
    const geo = new THREE.BoxGeometry(w, h, d); const uv = geo.attributes.uv;
    for (let f = 0; f < 6; f++) for (let k = 0; k < 4; k++) { const ix = f * 4 + k; if (f === 2 || f === 3) uv.setXY(ix, 8 / 128, 1 - 8 / 256); else uv.setXY(ix, uv.getX(ix) * Math.max(1, Math.round(w / 12)), uv.getY(ix) * Math.max(1, Math.round(floors / 8))); }
    geo.applyMatrix4(M4(THREE, x, h / 2, z));
    geos.push(geo); buildings.push({ x, z, w, d, h }); counts.towers++;
  }
  if (geos.length) { const m = new THREE.Mesh(mergeGeos(THREE, geos), new THREE.MeshLambertMaterial({ map: tex })); m.name = 'TOWERS_' + R.name; group.add(m); }
}

// ---------- THE FASHION FORGE AT BLOCK SCALE ----------
// The Citywright's forge from Test 2 (buildings.js), its heart carried
// into the engine unchanged — same FASHION table, same sixteen plates,
// same dress — but striking ONE BLOCK at a time, so the weave can stand
// fashion buildings anywhere. Uniqueness is kept across the whole world.
const FASHION = {
  seed: 20261016, floorH: 3.1, tileRows: 8, windowPitch: 2.4,
  frame: '#b3a894', parapet: '#8a8175', roofGray: '#77726c',
  groundBand: '#35312c', door: '#17150f', litPane: '#ffd9a0',
  glass: ['#5d6d7a', '#77828c', '#7a6c54', '#8a7a5e', '#93a3ab', '#a7b2b4'],
  tints: [0xffffff, 0xf5eeda, 0xe8e2d4, 0xdfe5e8, 0xcfd8de, 0xf0e2cc, 0xe6d9c3, 0xd8dee2, 0xf7f3ea, 0xe2d5bd],
  crownStone: [0xb8ae9c, 0xa89e8a, 0x9aa0a2, 0xc2bba9],
  crowns: ['lip', 'cap', 'step', 'spire'], variants: 16,
};
function variantSpec(v) {
  return {
    cols: [5, 6, 7][v % 3], glass: FASHION.glass[v % FASHION.glass.length],
    lit: 0.04 + (v % 4) * 0.035, band: v % 3, shade: 0.92 + ((v * 37) % 10) * 0.016,
  };
}
const ROOF_UV = { u: 15 / 256, v: 1 - 15 / 512 };
function drawPlate(THREE, v) {
  const spec = variantSpec(v);
  const rng = mulberry32(FASHION.seed + v * 7919);
  const cv = document.createElement('canvas'); cv.width = 256; cv.height = 512;
  const g = cv.getContext('2d');
  const rows = FASHION.tileRows, rh = 512 / rows, cw = 256 / spec.cols;
  g.fillStyle = 