<!-- NEW6 — TEST 6 · THE FIVE SHORES · part 2 -->
0, 0, 256, 15);
  g.fillStyle = 'rgba(255,255,255,0.28)'; g.fillRect(0, 13, 256, 2);

  // ground band — dark stone, a door recessed at the tile's heart
  const gy = 512 - rh;
  g.fillStyle = FASHION.groundBand; g.fillRect(0, gy, 256, rh);
  if (spec.band === 1) {                           // pilaster strips
    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) {                           // an awning line over the shops
    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); // the lintel's lit edge

  // THE ROOF PATCH — painted last, flat, at the known corner
  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;
}

/* ================= GEOMETRY ECONOMY =================
 * Buckets gather transformed box geometry — positions, normals,
 * uvs, vertex colors, indices — and become ONE mesh each. */
function bucket() { 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 buildMesh(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;
}
/* dress a tier box in its plate: side faces scaled so windows stay
 * one true size (u by windows-across, v by floors); roof and sole
 * point at the plate's flat roof patch */
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;
}

/* ================= THE DISTRICT ================= */
const PITCH = 110, BLOCK = 92, COLS = 10, ROWS = 9;
const PLOT = 92 / 3, SLAB_TOP = 0.3;

function buildDistrict(THREE) {
  const rng = mulberry32(FASHION.seed);
  const group = new THREE.Group();

  /* the wardrobe, struck once */
  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(bucket());
  }
  const crownBucket = bucket();
  const slabBucket = bucket();

  /* ---- the plots: 90 blocks x 3x3, shuffled; 800 built, 10 plaza ---- */
  const plots = [];
  for (let i = 0; i < COLS; i++) for (let j = 0; j < ROWS; j++) {
    const bx = (i - (COLS - 1) / 2) * PITCH, bz = (j - (ROWS - 1) / 2) * PITCH;
    for (let a = 0; a < 3; a++) for (let b = 0; b < 3; b++) {
      plots.push({ x: bx + (a - 1) * PLOT, z: bz + (b - 1) * PLOT, bx, bz });
    }
  }
  for (let i = plots.length - 1; i > 0; i--) {     // seeded shuffle
    const k = Math.floor(rng() * (i + 1));
    const t = plots[i]; plots[i] = plots[k]; plots[k] = t;
  }
  const built = plots.slice(0, 800), plazas = plots.slice(800);

  /* block slabs (the sidewalk fabric) + the bare plaza plots */
  const slabSeen = new Set();
  for (const p of plots) {
    const key = p.bx + ',' + p.bz;
    if (slabSeen.has(key)) continue;
    slabSeen.add(key);
    const s = new THREE.BoxGeometry(BLOCK, SLAB_TOP, BLOCK);
    s.translate(p.bx, SLAB_TOP / 2, p.bz);
    pushGeo(THREE, slabBucket, s, 0x8f8c86);
  }
  for (const p of plazas) {
    const s = new THREE.BoxGeome