<!-- SOLID CITY · part 29 -->
soH/2,.005);box(spine,.09,torsoH*.86,.27*bulk,accent,.09,torsoH/2,.005);}if(d.top==='armor'){box(spine,shoulderW*1.7*bulk,.18,.31*bulk,accent,0,torsoH*.69,.035);}
 const neck=group(spine,0,torsoH,0);P.neck=neck;capsule(neck,.075,.075,skin,0,.05,0);
 const head=group(neck,0,.26*d.headSize,0);P.head=head;const faceShape=d.face==='angular'?[1,.94,.87]:d.face==='oval'?[.88,1.14,.92]:[1,1,1];sphere(head,.205*d.headSize,skin,0,0,0,...faceShape);
 for(const side of [-1,1]){sphere(head,.032*d.headSize,eye,side*.085*d.headSize,.025,.185*d.headSize);sphere(head,.045*d.headSize,skin,side*.205*d.headSize,-.02,0,.65,1,.5);}
 sphere(head,.055*d.headSize,skin,0,-.07,.198*d.headSize,.72,1.1,.75);
 if(d.hairStyle!=='none'){sphere(head,.21*d.headSize,hair,0,.105*d.headSize,-.02,1.03,.62,1.03);if(d.hairStyle==='long')for(const s of [-1,1])capsule(head,.07,.3*d.headSize,hair,s*.16,-.19,0);if(d.hairStyle==='mohawk')box(head,.075,.22,.28,hair,0,.25,0);}
 if(d.accessory==='visor'){box(head,.39*d.headSize,.095,.06,accent,0,.03,.19*d.headSize);}if(d.accessory==='halo'){const halo=mesh(head,new THREE.TorusGeometry(.28,.017,8,40),accent,0,.36,0);halo.rotation.x=Math.PI/2;}
 for(const s of [-1,1]){
   const shoulder=group(spine,s*shoulderW,torsoH-.07,0);P[s<0?'leftShoulder':'rightShoulder']=shoulder;
   sphere(shoulder,.105*bulk,cloth);capsule(shoulder,.085*bulk,armL*.47,cloth,0,-armL*.25,0);
   const elbow=group(shoulder,0,-armL*.52,0);P[s<0?'leftElbow':'rightElbow']=elbow;sphere(elbow,.075*bulk,accent);
   capsule(elbow,.07*bulk,armL*.38,cloth,0,-armL*.21,0);const wrist=group(elbow,0,-armL*.45,0);P[s<0?'leftWrist':'rightWrist']=wrist;sphere(wrist,.075*bulk,skin,0,-.065,0,1,1.35,.7);for(let finger=0;finger<4;finger++){capsule(wrist,.012*bulk,.065-finger*.006,skin,(finger-1.5)*.033*bulk,-.17,.012);}const thumb=capsule(wrist,.018*bulk,.055,skin,-s*.09*bulk,-.095,.015);thumb.rotation.z=-s*.65;
   if(d.armor!=='none'){sphere(shoulder,.135*bulk,accent,s*.02,.015,0,1.1,.65,1.05);if(d.armor==='full')box(elbow,.17*bulk,.24,.16*bulk,accent,0,-.16,0);}
   const hip=group(hips,s*.125*bulk,-.055,0);P[s<0?'leftHip':'rightHip']=hip;
   capsule(hip,.112*bulk,legL*.46,d.bottom==='shorts'?skin:cloth,0,-legL*.25,0);
   if(d.bottom==='shorts')box(hip,.24*bulk,.22,.24*bulk,cloth,0,-.1,0);
   const knee=group(hip,0,-legL*.53,0);P[s<0?'leftKnee':'rightKnee']=knee;sphere(knee,.085*bulk,accent);
   capsule(knee,.095*bulk,legL*.37,d.bottom==='shorts'?skin:cloth,0,-legL*.205,0);
   const ankle=group(knee,0,-legL*.44,0);P[s<0?'leftAnkle':'rightAnkle']=ankle;
   const bootHeight=d.shoes==='boots'?.19:.12;sphere(ankle,.12*bulk,dark,0,-.04,.09,1,bootHeight/.24,1.65);box(ankle,.23*bulk,.035,.37*bulk,accent,0,-.12,.095);
 }
 const skeletonColor=material('#ffce56',0,1);for(const [name,p] of Object.entries(P)){const marker=sphere(p,.025,skeletonColor);marker.name='rig_'+name;marker.visible=d.showRig;RM.push(marker);}
 root.userData={epistlesAvatar:true,version:VERSION,configuration:{...d}};return root;
 }

function clamp(v,min,max){
return Math.min(max,Math.max(min,v));
}
function validate(candidate){
const out={...defaults};
for(const [key,val] of Object.entries(defaults)){
const v=candidate?.[key];
if(typeof val==='number'){
const def=sections.flatMap(s=>s[1]).find(f=>f[0]===key);
out[key]=clamp(Number.isFinite(Number(v))?Number(v):val,def?.[2]??0,def?.[3]??10);
}else if(typeof val==='boolean'){
out[key]=typeof v==='boolean'?v:val;
}else if(options[key]){
out[key]=options[key].includes(v)?v:val;
}else if(key==='name'){
out[key]=String(v??val).slice(0,40);
}else{
out[key]=/^#[0-9a-fA-F]{6}$/.test(v)?v:val;
}
}
return out;
}

function collectParts(model) {
  const out = {};
  model.traverse(o => {
    if (o.name && o.name.startsWith('rig_') && o.parent) out[o.name.slice(4)] = o.parent;
  });
  return out;
}

/* createGestureSession({timeout, onEvent}) — the cooperative
 * protocol of the design, in miniature: a fist bump (or any paired
 * gesture) is offered under a session id, accepted or declined by
 * the partner, played, and completed — or it expires safely.
 * Proximity and facing are the caller's physics; agreement and
 * time are kept here. Stale ids are refused. */

  return { buildAvatar, validate, collectParts, sculptTorso };
}
export default createAvatarFactory;


===== FILE: public/city/avatar-king-playa.js =====
/* SOLID CITY — KING PLAYA'S AVATAR (2026-10-09)
 * "Playa, you can build yourself an amazing avatar." — the King, 9 Oct 2026, 11:24 PM HST.
 * King Playa, Councilor for all: black robe and armour, red mantle and cape, gold circlet crown
 * with a red jewel, glowing red visor, gold 'P' sigil on the chest, soft gold aura ring at the feet.
 *
 * A self-contained ES module, drop-in for a citizens.js citizen (same frame as build(c)):
 *   origin at the feet (y = 0), facing +Z, ~1.96 m to the top of the head (~2.25 m with the crown)
 *   before the roster scale (King Playa uses sc 1.12).
 *   userData.limbs = [armL, le