<!-- SOLID CITY · part 3687 -->
  const x=ax+dx,z=az+dz;
    if(!diskSafe(safe,x,z,3.4))continue;
    group.add(createWardSign(args,ward.id,x,z));
    group.userData.signPlaced=true;group.userData.signPosition=[x,z];return;
  }
  group.userData.signPlaced=false;
  group.userData.warning=`WARD ${ward.id} sign NOT placed: no verified clear footprint near anchor`;
}
function pathSlabs(T,groundAt,safe,g,route) {
  const road=material(T,0xb6a999);
  for(let i=0;i<route.length-1;i++) {
    const [ax,az]=route[i],[bx,bz]=route[i+1];const n=Math.ceil(Math.hypot(bx-ax,bz-az)/3);
    for(let j=0;j<n;j++) {
      const t=(j+.5)/n,x=ax+(bx-ax)*t,z=az+(bz-az)*t;
      if(!diskSafe(safe,x,z,1.2))continue;
      cube(T,g,road,x,groundAt(x,z)+.045,z,1.35,.09,1.35);
    }
  }
}
// Simple static triangle ribbon. Each station has three lateral vertices;
// no per-frame terrain queries. Sharp bends need a miter/bevel before shipping.
function pathRibbon(T,groundAt,safe,g,route,step=2.5) {
  const mat=material(T,0xb7ad9e);let triangles=0;
  for(let i=0;i<route.length-1;i++) {
    const a=route[i],b=route[i+1],dx=b[0]-a[0],dz=b[1]-a[1];
    const len=Math.hypot(dx,dz);if(len<1e-6)continue;
    const nx=dz/len,nz=-dx/len,n=Math.ceil(len/step),verts=[];
    const sample=(t,u)=>{const x=a[0]+dx*t+nx*u,z=a[1]+dz*t+nz*u;return [x,groundAt(x,z)+.075,z]};
    for(let j=0;j<n;j++) {
      const t0=j/n,t1=(j+1)/n;
      for(const u of [-1.6,0]) {
        const A=sample(t0,u),B=sample(t0,u+1.6),C=sample(t1,u),D=sample(t1,u+1.6);
        // Interior checks prevent a protected feature being crossed by a triangle.
        let allowed=true;
        for(const t of [t0,(t0+t1)/2,t1]) for(const v of [u,u+.8,u+1.6]) {
          const p=sample(t,v);if(!safe(p[0],p[2]))allowed=false;
        }
        if(!allowed)continue;
        verts.push(...A,...C,...B,...B,...C,...D);triangles+=2;
      }
    }
    if(!verts.length)continue;
    const geo=new T.BufferGeometry();geo.setAttribute('position',new T.Float32BufferAttribute(verts,3));
    geo.computeVertexNormals();const mesh=new T.Mesh(geo,mat);mesh.receiveShadow=true;g.add(mesh);
  }
  g.userData.triangles=triangles;
}
function landscaping(T,groundAt,safe,g,ward,amount) {
  const trunk=material(T,0x73543e),leaf=material(T,0x6c9470),seat=material(T,0x9a8367);
  const [cx,cz]=ward.anchor;
  for(let i=0;i<amount;i++) {
    const a=i*2.39996,r=12+7*Math.sqrt(i+1),x=cx+r*Math.cos(a),z=cz+r*Math.sin(a);
    if(!diskSafe(safe,x,z,3.2))continue;
    const y=groundAt(x,z);
    cylinder(T,g,trunk,x,y+1.05,z,.13,.18,2.1);
    const crown=new T.Mesh(new T.IcosahedronGeometry(1.3,0),leaf);
    crown.position.set(x,y+2.55,z);g.add(crown);
  }
  for(const [dx,dz] of [[10,7],[-12,-8]]) {
    const x=cx+dx,z=cz+dz;if(!diskSafe(safe,x,z,2.5))continue;
    const y=groundAt(x,z);cube(T,g,seat,x,y+.55,z,2.0,.16,.55);
    for(const px of [-.65,.65])cube(T,g,trunk,x+px,y+.28,z,.14,.52,.38);
  }
}
function createWard(args,id) {
  const ward=WARDS.find(w=>w.id===id);if(!ward)throw Error(`WARD ${id} not defined`);
  const {THREE,groundAt,safe}=ctx(args,ward),g=new THREE.Group();
  g.name=`WARD ${id} — ${ward.name}`;
  g.userData={ward:id,band:ward.band,bounds:{minX:ward.minX,maxX:ward.maxX,minZ:ward.minZ,maxZ:ward.maxZ},
    grossEnvelopeM2:(ward.maxX-ward.minX)*(ward.maxZ-ward.minZ),netUsableM2:'not yet measured',
    previewOnly:true,signPlaced:false};
  if(id===2)pathSlabs(THREE,groundAt,safe,g,ROADS[id]);
  else pathRibbon(THREE,groundAt,safe,g,ROADS[id],id===5?1.5:3);
  if(id>=4)landscaping(THREE,groundAt,safe,g,ward,id===4?8:id===5?12:7);
  addSign(args,ward,g,safe);
  return g;
}
export function createWard2(args){return createWard(args,2)}
export function createWard3(args){return createWard(args,3)}
export function createWard4(args){return createWard(args,4)}
export function createWard5(args){return createWard(args,5)}
export function createWard6(args){return createWard(args,6)}
export function createWard7(args){return createWard(args,7)}
// Approximate net area, measured against the supplied actual exclusion mask.
// Reports error-scale ~ cellSize times boundary perimeter, not an exact survey.
export function measureWardAreas(isAllowed,cellSize=2) {
  if(typeof isAllowed!=='function'||!(cellSize>0))throw Error('verified isAllowed and positive cellSize required');
  const result=WARDS.map(w=>{
    let count=0;
    const nx=Math.ceil((w.maxX-w.minX)/cellSize),nz=Math.ceil((w.maxZ-w.minZ)/cellSize);
    const dx=(w.maxX-w.minX)/nx,dz=(w.maxZ-w.minZ)/nz;
    for(let ix=0;ix<nx;ix++)for(let iz=0;iz<nz;iz++) {
      const x=w.minX+(ix+.5)*dx,z=w.minZ+(iz+.5)*dz;
      if(isAllowed(x,z)===true)count++;
    }
    return {ward:w.id,band:w.band,bounds:[w.minX,w.maxX,w.minZ,w.maxZ],
      grossEnvelopeM2:(w.maxX-w.minX)*(w.maxZ-w.minZ),
      estimatedUsableM2:Math.round(count*dx*dz),sampleCellM2:dx*dz};
  });
  return {wards:result,estimatedTotalM2:result.reduce((s,r)=>s+r.estimatedUsableM2,0),
    note:'Grid estimate; verif