<!-- SOLID CITY · part 3689 -->
nst within=(x,z)=>x>=ward.minX && x<ward.maxX && z>=ward.minZ && z<ward.maxZ;
  const safe=(x,z)=>within(x,z) && isAllowed(x,z)===true;
  return {THREE,groundAt,safe};
}
function diskSafe(safe,x,z,r) {
  for(let j=0;j<16;j++) {const a=j*Math.PI/8;if(!safe(x+Math.cos(a)*r,z+Math.sin(a)*r))return false;}
  return safe(x,z);
}
function material(T,color) {return new T.MeshLambertMaterial({color,side:T.DoubleSide});}
function cube(T,group,mat,x,y,z,w,h,d) {
  const m=new T.Mesh(new T.BoxGeometry(w,h,d),mat);m.position.set(x,y,z);m.receiveShadow=true;group.add(m);return m;
}
function cylinder(T,group,mat,x,y,z,rTop,rBottom,height,sides=7) {
  const m=new T.Mesh(new T.CylinderGeometry(rTop,rBottom,height,sides),mat);
  m.position.set(x,y,z);group.add(m);return m;
}
function signTexture(T,label) {
  if(typeof document==='undefined') throw Error('Canvas is required to create ward sign texture in the browser');
  const canvas=document.createElement('canvas');canvas.width=512;canvas.height=192;
  const c=canvas.getContext('2d');
  c.fillStyle='#171b24';c.fillRect(0,0,512,192);
  c.strokeStyle='#d8b566';c.lineWidth=14;c.strokeRect(9,9,494,174);
  c.fillStyle='#f3d88c';c.textAlign='center';c.textBaseline='middle';
  c.font='bold 91px Georgia, serif';c.fillText(label,256,98);
  const tex=new T.CanvasTexture(canvas);tex.colorSpace=T.SRGBColorSpace;
  return tex;
}
// Roadside sign, board lies in XY plane, front points toward +Z.
// Placement requires all post/board footprint samples to be allowed.
export function createWardSign({THREE,groundAt,isAllowed},id,x,z,heading=0) {
  if(!WARDS.some(w=>w.id===id)) throw Error('Unknown ward');
  if(!THREE||typeof groundAt!=='function'||typeof isAllowed!=='function') throw Error('Missing dependencies');
  const g=new THREE.Group();g.name=`WARD ${id} sign`;
  const gold=material(THREE,0xc5a25a),post=material(THREE,0x504b43);
  const board=new THREE.Mesh(new THREE.PlaneGeometry(4.4,1.55),
    new THREE.MeshBasicMaterial({map:signTexture(THREE,`WARD ${id}`),side:THREE.DoubleSide}));
  board.position.set(0,2.3,0.02);g.add(board);
  // Simple gold border as physical frame; two posts below.
  cube(THREE,g,gold,0,3.105,0,4.55,.11,.12);
  cube(THREE,g,gold,0,1.495,0,4.55,.11,.12);
  for(const px of [-2.24,2.24]) {
    cube(THREE,g,gold,px,2.30,0,.11,1.72,.12);
    const [wx,wz]=[x+Math.cos(heading)*px,z-Math.sin(heading)*px];
    // Base at local 0, adapt each post to the actual terrain elevation.
    const relative=groundAt(wx,wz)-groundAt(x,z);
    cube(THREE,g,post,px,.80+relative/2,0,.16,1.60+Math.abs(relative),.16);
  }
  g.position.set(x,groundAt(x,z)+.03,z);g.rotation.y=heading;
  g.userData={ward:id,label:`WARD ${id}`,kind:'roadside-sign'};
  return g;
}
function addSign(args,ward,group,safe) {
  const [ax,az]=ward.anchor;
  // Search for a safe roadside location; never silently place on protected land.
  const offsets=[[0,6],[0,-6],[6,0],[-6,0],[9,9],[-9,-9],[14,0],[-14,0]];
  for(const [dx,dz] of offsets) {
    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.Float32BufferAtt