<!-- NEW7 — TEST 7 · THE WOVEN CITY · part 444 -->
FASHION.frame; g.fillRect(0, 0, 256, 512);
  for (let r = 0; r < rows - 1; r++) {
    for (let c = 0; c < spec.cols; c++) {
      const x = c * cw, y = r * rh;
      const lit = rng() < spec.lit;
      const jitter = 0.9 + rng() * 0.2;
      g.fillStyle = lit ? FASHION.litPane : spec.glass;
      g.globalAlpha = lit ? 0.95 : Math.min(1, spec.shade * jitter);
      g.fillRect(x + 5, y + 9, cw - 10, rh - 18);
      g.globalAlpha = 0.16; g.fillStyle = '#ffffff';
      g.fillRect(x + 5, y + 9, cw - 10, (rh - 18) * 0.42);
      g.globalAlpha = 1;
    }
  }
  g.fillStyle = FASHION.parapet; g.fillRect(0, 0, 256, 15);
  g.fillStyle = 'rgba(255,255,255,0.28)'; g.fillRect(0, 13, 256, 2);
  const gy = 512 - rh;
  g.fillStyle = FASHION.groundBand; g.fillRect(0, gy, 256, rh);
  if (spec.band === 1) { g.fillStyle = 'rgba(255,255,255,0.10)'; for (let c = 0; c <= spec.cols; c++) g.fillRect(c * cw - 2, gy, 4, rh); }
  if (spec.band === 2) { g.fillStyle = '#575046'; g.fillRect(0, gy + 6, 256, 7); }
  g.fillStyle = FASHION.door; g.fillRect(128 - cw * 0.32, gy + 14, cw * 0.64, rh - 14);
  g.fillStyle = 'rgba(255,255,255,0.35)'; g.fillRect(128 - cw * 0.32, gy + 14, cw * 0.64, 2);
  g.fillStyle = FASHION.roofGray; g.fillRect(0, 0, 30, 30);
  const tex = new THREE.CanvasTexture(cv);
  tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
  tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 4;
  return tex;
}
function fbucket() { return { pos: [], nor: [], uv: [], col: [], idx: [], vert: 0 }; }
function pushGeo(THREE, bk, geo, tintHex) {
  const p = geo.attributes.position, n = geo.attributes.normal,
        u = geo.attributes.uv, ix = geo.index, col = new THREE.Color(tintHex);
  for (let i = 0; i < p.count; i++) {
    bk.pos.push(p.getX(i), p.getY(i), p.getZ(i));
    bk.nor.push(n.getX(i), n.getY(i), n.getZ(i));
    bk.uv.push(u.getX(i), u.getY(i));
    bk.col.push(col.r, col.g, col.b);
  }
  for (let i = 0; i < ix.count; i++) bk.idx.push(ix.getX(i) + bk.vert);
  bk.vert += p.count;
}
function buildMeshF(THREE, bk, material) {
  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.Float32BufferAttribute(bk.pos, 3));
  geo.setAttribute('normal', new THREE.Float32BufferAttribute(bk.nor, 3));
  geo.setAttribute('uv', new THREE.Float32BufferAttribute(bk.uv, 2));
  geo.setAttribute('color', new THREE.Float32BufferAttribute(bk.col, 3));
  geo.setIndex(bk.idx);
  const mesh = new THREE.Mesh(geo, material);
  mesh.castShadow = true; mesh.receiveShadow = true;
  return mesh;
}
function dressBox(THREE, w, h, d, floors, spec) {
  const geo = new THREE.BoxGeometry(w, h, d);
  const uv = geo.attributes.uv;
  const vRep = Math.max(1, Math.round(floors / FASHION.tileRows));
  const repW = Math.max(1, Math.round(w / (spec.cols * FASHION.windowPitch)));
  const repD = Math.max(1, Math.round(d / (spec.cols * FASHION.windowPitch)));
  for (let f = 0; f < 6; f++) {
    for (let k = 0; k < 4; k++) {
      const i = f * 4 + k;
      if (f === 2 || f === 3) uv.setXY(i, ROOF_UV.u, ROOF_UV.v);
      else {
        const rep = f < 2 ? repD : repW;
        uv.setXY(i, uv.getX(i) * rep, uv.getY(i) * vRep);
      }
    }
  }
  return geo;
}
function makeFashionForge(THREE) {
  const plates = [], facadeMats = [], facadeBuckets = [];
  for (let v = 0; v < FASHION.variants; v++) {
    plates.push(drawPlate(THREE, v));
    facadeMats.push(new THREE.MeshStandardMaterial({ map: plates[v], vertexColors: true, roughness: 0.85, metalness: 0.05 }));
    facadeBuckets.push(fbucket());
  }
  const crownBucket = fbucket(), slabBucket = fbucket();
  const seen = new Set();
  let total = 0;
  // One block: 5x5 plots of 18.4 in a 92 block. peak 0..1 shapes heights.
  // `allow` is this block's share of the world's fashion quota, so the
  // quota spreads evenly through the weave instead of running dry at the
  // far corner; `hardCap` is the world total and is never passed.
  // Plots under a reserved point (a bus stop's corner) stay plaza — the
  // stop is planted proper, never inside a building.
  function buildBlock(cx, cz, rng, peak, allow, hardCap, out, reserved) {
    const slab = new THREE.BoxGeometry(92, 0.3, 92); slab.translate(cx, 0.15, cz);
    pushGeo(THREE, slabBucket, slab, 0x8f8c86);
    const plots = [];
    for (let a = 0; a < 5; a++) for (let b = 0; b < 5; b++) plots.push([cx + (a - 2) * 18.4, cz + (b - 2) * 18.4]);
    for (let i = plots.length - 1; i > 0; i--) { const k = Math.floor(rng() * (i + 1)); const t = plots[i]; plots[i] = plots[k]; plots[k] = t; }
    let built = 0;
    for (const [px, pz] of plots) {
      if (total >= hardCap || built >= allow) break;
      if (reserved && reserved.some(s => Math.hypot(s.x - px, s.z - pz) < 10.5)) {
        const s = new THREE.BoxGeometry(15.5, 0.14, 15.5); s.translate(px, 0.37, pz);
        pushGeo(THREE, slabBucket, s, 0xa39a88);
        continue;
      }
      let rec = null;
      for (let tries = 0; tries < 60 && !rec; tries++)