{
  "name": "person",
  "title": "Person",
  "type": "registry:component",
  "description": "A proportioned, clothed, articulated figure generated from a seed: style templates (anime by default, realistic as the alternate), role presets, gender/age/build silhouettes, hair, outfits, hats and accessories, swappable cosmetic skins (named presets or custom bundles), poses, idle breathing, and a head that turns to watch you. Place one at a spawn point, or use it as the Player's third-person body.",
  "dependencies": [
    "@react-three/fiber@^9.6.1",
    "@react-three/rapier@^2.2.0",
    "@runek/core@^0.12.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [
    "sign"
  ],
  "files": [
    {
      "path": "Person.tsx",
      "content": "import { useFrame } from '@react-three/fiber'\nimport { CapsuleCollider, RigidBody } from '@react-three/rapier'\nimport {\n  pick,\n  type Rng,\n  range,\n  rng,\n  useWorld,\n  type Vec3,\n  type WorldComponentProps,\n} from '@runek/core'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport { Color, DoubleSide, type Group, LatheGeometry, Vector2, Vector3 } from 'three'\nimport { Sign } from './Sign'\n\n/** Preset bundles: outfit, hat, and accessories for a role. */\nexport type PersonKind =\n  | 'villager'\n  | 'merchant'\n  | 'guard'\n  | 'sailor'\n  | 'farmer'\n  | 'noble'\n  | 'scholar'\n  | 'traveler'\n/** Silhouette cues only (shoulder-to-hip ratio, torso taper, default hair weights). */\nexport type PersonGender = 'feminine' | 'masculine' | 'neutral'\nexport type PersonAge = 'child' | 'adult' | 'elder'\nexport type PersonBuild = 'slim' | 'average' | 'stocky'\nexport type PersonHair = 'short' | 'long' | 'bun' | 'ponytail' | 'braid' | 'cropped' | 'bald'\nexport type PersonFacialHair = 'none' | 'stubble' | 'moustache' | 'beard'\nexport type PersonOutfit =\n  | 'tunic'\n  | 'shirt'\n  | 'dress'\n  | 'robe'\n  | 'coat'\n  | 'apron'\n  | 'uniform'\n  | 'vest'\nexport type PersonHat = 'none' | 'cap' | 'straw' | 'brim' | 'bandana' | 'hood'\nexport type PersonAccessory = 'bag' | 'belt' | 'scarf' | 'glasses' | 'cape' | 'staff'\nexport type PersonPose = 'stand' | 'sit' | 'lean' | 'work' | 'wave'\n/** `auto` swaps to the cheap silhouette past `LOD_FAR` units from the camera. */\nexport type PersonDetail = 'auto' | 'high' | 'low'\n\n/** Visual template the whole figure follows. */\nexport type PersonStyle = 'anime' | 'realistic'\n\n/**\n * A cosmetic loadout (a \"skin\" in the game sense): clothes, colors, headwear, hair, and\n * body accessories bundled as one plain-JSON object, so a whole look swaps in a single\n * prop write. It never touches the body itself (tone, build, age, gender). Precedence per\n * trait: an explicit component prop > this skin > the `kind` preset > the seeded roll.\n */\nexport interface PersonSkin {\n  outfit?: PersonOutfit\n  topColor?: string\n  bottomColor?: string\n  shoeColor?: string\n  hat?: PersonHat\n  hair?: PersonHair\n  hairColor?: string\n  accessories?: PersonAccessory[]\n}\n\n/**\n * The named skins that ship with the component: wardrobe, not wigs — each sets clothes,\n * colors, headwear, and accessories but never hair, so a figure keeps its identity across\n * skin swaps. Colors are fixed on purpose: a named skin looks the same in every world\n * (an explicit color always beats the palette, CONTRACT §7). Pass the name to `skin`,\n * or spread one into your own bundle to tweak it.\n */\nexport const PERSON_SKINS = {\n  /** Bright tunic and scarf for a market day or a fair. */\n  festival: {\n    outfit: 'tunic',\n    topColor: '#c8563b',\n    bottomColor: '#3f4a63',\n    shoeColor: '#5a4632',\n    hat: 'none',\n    accessories: ['scarf', 'belt'],\n  },\n  /** Field clothes under a straw hat. */\n  harvest: {\n    outfit: 'shirt',\n    topColor: '#c9a05a',\n    bottomColor: '#6b5138',\n    shoeColor: '#4a3a2a',\n    hat: 'straw',\n    accessories: ['belt'],\n  },\n  /** Hooded coat and scarf for the cold. */\n  winter: {\n    outfit: 'coat',\n    topColor: '#54626f',\n    bottomColor: '#38393f',\n    shoeColor: '#2e2a26',\n    hat: 'hood',\n    accessories: ['scarf', 'belt'],\n  },\n  /** Deck shirt, bandana, and a shoulder bag for the crossing. */\n  voyage: {\n    outfit: 'shirt',\n    topColor: '#e8e2d2',\n    bottomColor: '#31456b',\n    shoeColor: '#3a3128',\n    hat: 'bandana',\n    accessories: ['belt', 'bag'],\n  },\n  /** A deep-dyed robe and cape for rites and audiences. */\n  ceremony: {\n    outfit: 'robe',\n    topColor: '#5b3a5e',\n    bottomColor: '#2e2633',\n    shoeColor: '#241f29',\n    hat: 'none',\n    accessories: ['cape'],\n  },\n  /** Workshop apron and cap. */\n  atelier: {\n    outfit: 'apron',\n    topColor: '#7a6a55',\n    bottomColor: '#4c4238',\n    shoeColor: '#33302c',\n    hat: 'cap',\n    accessories: ['belt'],\n  },\n  /** Oxblood longcoat, dark bandana, and a plunder bag. */\n  pirate: {\n    outfit: 'coat',\n    topColor: '#5c2a2e',\n    bottomColor: '#2b2622',\n    shoeColor: '#1f1b18',\n    hat: 'bandana',\n    accessories: ['belt', 'bag'],\n  },\n  /** Rolled white sleeves under the bar apron, black trousers. */\n  bartender: {\n    outfit: 'apron',\n    topColor: '#e6e0d4',\n    bottomColor: '#26242a',\n    shoeColor: '#1d1b1f',\n    hat: 'none',\n    accessories: ['belt'],\n  },\n  /** Navy deck uniform with white trousers and a cap. */\n  sailor: {\n    outfit: 'uniform',\n    topColor: '#31456b',\n    bottomColor: '#e8e4d8',\n    shoeColor: '#26221e',\n    hat: 'cap',\n    accessories: ['belt'],\n  },\n} satisfies Record<string, PersonSkin>\n\nexport type PersonSkinName = keyof typeof PERSON_SKINS\n\nexport interface PersonProps extends WorldComponentProps {\n  /** Visual template: `anime` (default) has a bigger head, large lit eyes, a tapered chin,\n   *  longer legs, and a chunky fringe; `realistic` keeps canonical human proportions. */\n  style?: PersonStyle\n  /** Role preset: fills `outfit`, `hat`, and `accessories` unless you set them. */\n  kind?: PersonKind\n  /** Silhouette cues only; every trait it influences stays overridable. Seeded when unset. */\n  gender?: PersonGender\n  /** Head-to-body ratio, stature, and (for `elder`) a stooped spine. */\n  age?: PersonAge\n  /** Standing height, in units. Every other measurement derives from it. */\n  height?: number\n  /** Limb thickness and torso depth. Seeded when unset. */\n  build?: PersonBuild\n  /** Skin color; seeded from a curated set of tones when unset. */\n  skinTone?: string\n  /** Cosmetic loadout: a preset name from `PERSON_SKINS` (e.g. `\"winter\"`) or a custom\n   *  `PersonSkin` bundle. Individual props win over it. Plain JSON either way, so an editor\n   *  or an in-world UI can swap a figure's whole look by writing this one prop. */\n  skin?: PersonSkinName | PersonSkin\n  hair?: PersonHair\n  hairColor?: string\n  facialHair?: PersonFacialHair\n  outfit?: PersonOutfit\n  /** Shirt/dress color; defaults to a seeded shade of the palette's `fabric`. */\n  topColor?: string\n  /** Trouser/skirt color; defaults to a seeded shade of the palette's `woodDark`. */\n  bottomColor?: string\n  /** Shoe color; defaults to the palette's `metal`. */\n  shoeColor?: string\n  hat?: PersonHat\n  accessories?: PersonAccessory[]\n  /** Static joint set. `wave` also animates the raised forearm. */\n  pose?: PersonPose\n  /** Breathing, weight shift, and blinking. */\n  idle?: boolean\n  /** Turn the head toward the camera when it comes within `lookRadius`. */\n  lookAt?: boolean\n  /** How close the camera must be for `lookAt` to engage, in units. */\n  lookRadius?: number\n  /** Floating name above the head. */\n  label?: string\n  /** Capsule collider, so the figure is something you bump into. */\n  collider?: boolean\n  /** Render as a bare visual with no `RigidBody` — for a parent that owns the physics\n   *  (e.g. as `Player`'s third-person avatar). When false, `collider` is ignored. */\n  physics?: boolean\n  detail?: PersonDetail\n}\n\nconst GENDERS: readonly PersonGender[] = ['feminine', 'masculine', 'neutral']\nconst BUILDS: readonly PersonBuild[] = ['slim', 'average', 'stocky']\n\n// Skin and hair have no palette slot and no honest mapping onto one, so they roll from\n// curated sets instead (the seeded book-spine precedent, CONTRACT §7). Any explicit color wins.\nconst SKIN_TONES = [\n  '#f6ddc3',\n  '#efc9a4',\n  '#e0aa7e',\n  '#c98d63',\n  '#a9714a',\n  '#8a5a3b',\n  '#65402a',\n  '#48301f',\n]\nconst HAIR_COLORS = [\n  '#171310',\n  '#2b1d14',\n  '#4a3122',\n  '#6b4a2a',\n  '#8c6b3f',\n  '#b08d57',\n  '#c9b18a',\n  '#9a9a9a',\n  '#d9d5cd',\n  '#7d3320',\n]\n\nconst EYE_COLORS = ['#3b2a1d', '#2a2a2e', '#37455a', '#3c5038', '#584022', '#6b7a85']\n\n/** Everything a style template controls, so a new style is one record, not a rewrite.\n *  Proportions are multipliers over the human canon; face metrics are in head units. */\ninterface StyleSpec {\n  /** Scales heads-per-body: < 1 means a bigger head (anime ~6 heads, canon 7.5). */\n  headsMul: number\n  /** Cranium width as a fraction of head height. */\n  headWide: number\n  /** Cranium height scale: < 1 rounds the skull (anime), 1 keeps the long oval. */\n  headTall: number\n  /** Leg length multiplier (anime figures are leggier; the torso gives up the difference). */\n  leg: number\n  /** Limb radius multiplier. */\n  slim: number\n  shoulder: number\n  waist: number\n  neck: number\n  /** Eye half-width / half-height, eye-line height, iris radius (head units). */\n  eyeW: number\n  eyeH: number\n  eyeY: number\n  iris: number\n  /** Specular sparkle dot in the iris. */\n  highlight: boolean\n  /** Jaw ellipsoid scale: narrow it for the anime taper, keep it broad for realism. */\n  jaw: [number, number, number]\n  nose: number\n  mouth: number\n  /** Hair mass multiplier, and whether a chunky fringe hangs over the forehead. */\n  hairVol: number\n  bangs: boolean\n}\n\nconst STYLES: Record<PersonStyle, StyleSpec> = {\n  anime: {\n    headsMul: 0.82,\n    headWide: 0.84,\n    headTall: 0.94,\n    leg: 1.05,\n    slim: 0.86,\n    shoulder: 0.95,\n    waist: 0.9,\n    neck: 0.82,\n    eyeW: 0.085,\n    eyeH: 0.105,\n    eyeY: 0.0,\n    iris: 0.06,\n    highlight: true,\n    jaw: [0.5, 0.66, 0.52],\n    nose: 0.45,\n    mouth: 0.6,\n    hairVol: 1.14,\n    bangs: true,\n  },\n  realistic: {\n    headsMul: 1,\n    headWide: 0.76,\n    headTall: 1,\n    leg: 1,\n    slim: 1,\n    shoulder: 1,\n    waist: 1,\n    neck: 1,\n    eyeW: 0.062,\n    eyeH: 0.05,\n    eyeY: 0.06,\n    iris: 0.028,\n    highlight: false,\n    jaw: [0.6, 0.75, 0.62],\n    nose: 1,\n    mouth: 1,\n    hairVol: 1,\n    bangs: false,\n  },\n}\n\nconst HAIR_STYLES: Record<PersonGender, readonly PersonHair[]> = {\n  feminine: ['long', 'bun', 'ponytail', 'braid', 'short', 'long', 'ponytail'],\n  masculine: ['short', 'cropped', 'short', 'ponytail', 'bald', 'cropped'],\n  neutral: ['short', 'cropped', 'ponytail', 'bun', 'long', 'braid'],\n}\n\ninterface KindPreset {\n  outfit: PersonOutfit\n  hat: PersonHat\n  accessories: PersonAccessory[]\n}\n\nconst KINDS: Record<PersonKind, KindPreset> = {\n  villager: { outfit: 'tunic', hat: 'none', accessories: ['belt'] },\n  merchant: { outfit: 'apron', hat: 'cap', accessories: ['bag', 'belt'] },\n  guard: { outfit: 'uniform', hat: 'cap', accessories: ['belt'] },\n  sailor: { outfit: 'shirt', hat: 'bandana', accessories: ['belt'] },\n  farmer: { outfit: 'shirt', hat: 'straw', accessories: ['belt'] },\n  noble: { outfit: 'coat', hat: 'brim', accessories: ['cape', 'belt'] },\n  scholar: { outfit: 'robe', hat: 'hood', accessories: ['glasses', 'staff'] },\n  traveler: { outfit: 'coat', hat: 'brim', accessories: ['bag', 'scarf'] },\n}\n\ninterface OutfitSpec {\n  /** How far down the arm the sleeve reaches, as a fraction of the upper arm (0 = bare). */\n  sleeve: number\n  /** Skirt length as a fraction of the leg (0 = trousers instead). */\n  skirt: number\n  /** How far the top hangs below the hip, as a fraction of the thigh. */\n  hem: number\n  collar: boolean\n  apron: boolean\n}\n\nconst OUTFITS: Record<PersonOutfit, OutfitSpec> = {\n  tunic: { sleeve: 0.75, skirt: 0, hem: 0.35, collar: false, apron: false },\n  shirt: { sleeve: 0.55, skirt: 0, hem: 0.1, collar: true, apron: false },\n  dress: { sleeve: 0.35, skirt: 0.75, hem: 0, collar: false, apron: false },\n  robe: { sleeve: 1, skirt: 0.95, hem: 0, collar: true, apron: false },\n  coat: { sleeve: 1, skirt: 0, hem: 0.85, collar: true, apron: false },\n  apron: { sleeve: 0.5, skirt: 0, hem: 0.2, collar: false, apron: true },\n  uniform: { sleeve: 0.95, skirt: 0, hem: 0.45, collar: true, apron: false },\n  vest: { sleeve: 0, skirt: 0, hem: 0.15, collar: true, apron: false },\n}\n\n/** Head-to-body ratio and stature multiplier per age (a child is ~5.6 heads tall). */\nconst AGES: Record<PersonAge, { heads: number; stature: number; stoop: number }> = {\n  child: { heads: 5.6, stature: 0.72, stoop: 0 },\n  adult: { heads: 7.5, stature: 1, stoop: 0 },\n  elder: { heads: 7.4, stature: 0.97, stoop: 0.16 },\n}\n\nconst BASE_HEIGHT: Record<PersonGender, number> = {\n  feminine: 1.64,\n  masculine: 1.76,\n  neutral: 1.7,\n}\n\nconst GIRTH: Record<PersonBuild, number> = { slim: 0.85, average: 1, stocky: 1.2 }\n\n/** Fringe wedges over the forehead: [x, z, length, z-tilt] in head units (apex down). */\nconst BANGS: Array<[number, number, number, number]> = [\n  [-0.31, 0.25, 0.24, 0.2],\n  [-0.16, 0.33, 0.3, 0.08],\n  [0, 0.36, 0.27, 0],\n  [0.17, 0.32, 0.31, -0.1],\n  [0.31, 0.24, 0.23, -0.22],\n]\n\nconst LOD_FAR = 18\nconst LOD_NEAR = 15\nconst MAX_YAW = 1.2\nconst MAX_PITCH = 0.4\n\n/** Nudge a palette color per figure, so a crowd defaulting to one slot isn't a uniform. */\nfunction tint(hex: string, r: Rng): string {\n  const c = new Color(hex)\n  c.offsetHSL((r() - 0.5) * 0.05, (r() - 0.5) * 0.14, (r() - 0.5) * 0.16)\n  return `#${c.getHexString()}`\n}\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi)\n\ninterface Figure {\n  gender: PersonGender\n  hair: PersonHair\n  facialHair: PersonFacialHair\n  outfit: OutfitSpec\n  hat: PersonHat\n  accessories: PersonAccessory[]\n  skin: string\n  hairInk: string\n  top: string\n  bottom: string\n  shoe: string\n  /** Iris color and a lip shade derived from the skin tone. */\n  eye: string\n  lip: string\n  st: StyleSpec\n  stoop: number\n  /** Measurements, in world units. */\n  H: number\n  head: number\n  headW: number\n  neck: number\n  shoulderY: number\n  shoulderW: number\n  hipY: number\n  hipW: number\n  /** Torso lathe radii (chest, waist, hip) and its elliptical depth ratio. */\n  chestR: number\n  waistR: number\n  hipR: number\n  torsoZ: number\n  /** Hip-joint offset from the midline (femurs sit far inside the hip's silhouette). */\n  legX: number\n  upperArm: number\n  foreArm: number\n  armR: number\n  thigh: number\n  shin: number\n  legR: number\n  foot: number\n  /** Idle phases, so a row of figures never moves in lockstep. */\n  phase: number\n  blinkPhase: number\n  blinkEvery: number\n}\n\n/**\n * A procedural person: a proportioned, clothed, articulated figure generated from a seed.\n *\n * Every trait resolves the same way: an explicit prop wins, else the `kind` preset, else a\n * seeded roll, so `<Person seed={3} />` is a complete villager and each prop narrows it.\n * Measurements derive from `height` through canonical human ratios (a 7.5-head adult), so the\n * silhouette reads as a person rather than a toy; the body is a nested joint rig that breathes,\n * shifts its weight, blinks, and turns its head toward you.\n *\n * `position` is the spawn point. The figure is static (movement and dialogue are a later pass)\n * and stands on one capsule collider. Pass `physics={false}` for a bare visual, which is how it\n * becomes `Player`'s third-person body:\n * `<Player><Person physics={false} height={1.3} position={[0, -0.65, 0]} /></Player>`.\n *\n * Detail is parametric: past ~18 units the rig swaps to a cheap silhouette. A market square of\n * figures is affordable, but a true crowd wants the instanced component that comes later.\n */\nexport function Person({\n  position,\n  rotation = [0, 0, 0],\n  seed = 1,\n  style = 'anime',\n  kind = 'villager',\n  gender,\n  age = 'adult',\n  height,\n  build,\n  skinTone,\n  skin,\n  hair,\n  hairColor,\n  facialHair,\n  outfit,\n  topColor,\n  bottomColor,\n  shoeColor,\n  hat,\n  accessories,\n  pose = 'stand',\n  idle = true,\n  lookAt = true,\n  lookRadius = 9,\n  label,\n  collider = true,\n  physics = true,\n  detail = 'auto',\n}: PersonProps) {\n  const { unit, palette, ground } = useWorld()\n  // A placed figure stands on the world's ground baseline; a bare visual is positioned by\n  // whatever parent owns it (a `Player` capsule, a vehicle), so it starts at its own origin.\n  const at: Vec3 = position ?? (physics ? [0, ground, 0] : [0, 0, 0])\n  const accessoryKey = accessories?.join(',')\n  const skinKey = skin && JSON.stringify(skin)\n\n  // Every trait draws from the seed in a fixed order and is then overridden, so passing one\n  // prop never reshuffles the others' rolls.\n  const f = useMemo<Figure>(() => {\n    const r = rng(seed)\n    const sk: PersonSkin | undefined = typeof skin === 'string' ? PERSON_SKINS[skin] : skin\n    const g = gender ?? pick(r, GENDERS)\n    const b = build ?? pick(r, BUILDS)\n    const tone = skinTone ?? pick(r, SKIN_TONES)\n    const hairStyle = hair ?? sk?.hair ?? pick(r, HAIR_STYLES[g])\n    const hairTone = hairColor ?? sk?.hairColor ?? pick(r, HAIR_COLORS)\n    const beardRoll = r()\n    const beard =\n      facialHair ??\n      (g === 'masculine' && age !== 'child' && beardRoll > 0.55\n        ? beardRoll > 0.85\n          ? 'beard'\n          : beardRoll > 0.7\n            ? 'moustache'\n            : 'stubble'\n        : 'none')\n    const preset = KINDS[kind]\n    const cloth = OUTFITS[outfit ?? sk?.outfit ?? preset.outfit]\n    // Clothes stay palette-driven (so one palette swap re-themes the crowd), but each figure\n    // rolls which slot it wears and a shade of it, or a village ends up in uniform. An explicit\n    // color is used verbatim (CONTRACT §7). The rolls are always drawn, even when overridden,\n    // so passing a color never reshuffles the traits that roll after it.\n    const topRoll = tint(\n      pick(r, [palette.fabric, palette.accent, palette.wall, palette.foliage]),\n      r,\n    )\n    const bottomRoll = tint(pick(r, [palette.woodDark, palette.stone, palette.metal]), r)\n    const top = topColor ?? sk?.topColor ?? topRoll\n    const bottom = bottomColor ?? sk?.bottomColor ?? bottomRoll\n    const shoe = shoeColor ?? sk?.shoeColor ?? palette.metal\n\n    const a = AGES[age]\n    const st = STYLES[style]\n    const H = (height ?? BASE_HEIGHT[g] * a.stature) * unit\n    const headH = H / (a.heads * st.headsMul)\n    const girth = GIRTH[b]\n    const wide = g === 'masculine' ? 1.06 : g === 'feminine' ? 0.94 : 1\n    const hips = g === 'feminine' ? 1.09 : 1\n    const shoulderW = 0.25 * H * wide * (0.94 + girth * 0.06) * st.shoulder\n    const hipW = 0.19 * H * hips * girth\n    const chestR = shoulderW * 0.4\n    const lipTone = new Color(tone).offsetHSL(-0.02, 0.08, -0.13)\n\n    return {\n      gender: g,\n      hair: hairStyle,\n      facialHair: beard,\n      outfit: cloth,\n      hat: hat ?? sk?.hat ?? preset.hat,\n      accessories: accessories ?? sk?.accessories ?? preset.accessories,\n      skin: tone,\n      hairInk: hairTone,\n      top,\n      bottom,\n      shoe,\n      st,\n      stoop: a.stoop,\n      H,\n      head: headH,\n      headW: headH * st.headWide,\n      neck: headH * 0.28,\n      shoulderY: H - headH - headH * 0.22,\n      shoulderW,\n      hipY: 0.53 * H * st.leg,\n      hipW,\n      chestR,\n      waistR: chestR * (0.6 + girth * 0.14) * (g === 'feminine' ? 0.94 : 1) * st.waist,\n      hipR: hipW * 0.52,\n      torsoZ: 0.68 + (girth - 1) * 0.25,\n      legX: hipW * 0.3,\n      upperArm: 0.19 * H,\n      foreArm: 0.15 * H,\n      armR: 0.03 * H * girth * st.slim,\n      thigh: 0.25 * H * st.leg,\n      shin: 0.235 * H * st.leg,\n      legR: 0.045 * H * girth * st.slim,\n      foot: 0.14 * H,\n      phase: r() * Math.PI * 2,\n      blinkPhase: r() * 6,\n      blinkEvery: range(r, 3.4, 6.5),\n      eye: pick(r, EYE_COLORS),\n      lip: `#${lipTone.getHexString()}`,\n    }\n  }, [\n    seed,\n    style,\n    kind,\n    gender,\n    age,\n    height,\n    build,\n    skinTone,\n    skinKey,\n    skin,\n    hair,\n    hairColor,\n    facialHair,\n    outfit,\n    topColor,\n    bottomColor,\n    shoeColor,\n    hat,\n    accessoryKey,\n    accessories,\n    unit,\n    palette,\n  ])\n\n  const root = useRef<Group>(null)\n  const hips = useRef<Group>(null)\n  const chest = useRef<Group>(null)\n  const headRef = useRef<Group>(null)\n  const shoulderL = useRef<Group>(null)\n  const shoulderR = useRef<Group>(null)\n  const elbowR = useRef<Group>(null)\n  const eyes = useRef<Group>(null)\n  const nameTag = useRef<Group>(null)\n  const scratch = useMemo(() => new Vector3(), [])\n\n  const [far, setFar] = useState(detail === 'low')\n  const low = detail === 'low' || (detail === 'auto' && far)\n\n  // Static joint set for the pose. Sitting drops the root to a knee-height seat and folds the\n  // legs; the rest are torso and arm offsets the idle motion then rides on top of.\n  const p = useMemo(() => {\n    const sit = pose === 'sit'\n    return {\n      sit,\n      drop: sit ? -(f.hipY - f.thigh * 0.92) : 0,\n      spineX: f.stoop + (pose === 'work' ? 0.42 : pose === 'lean' ? -0.14 : sit ? 0.06 : 0),\n      armX: pose === 'work' ? -0.7 : sit ? -0.25 : 0,\n      armZ: pose === 'lean' ? 0.14 : 0.09,\n      thighX: sit ? -Math.PI / 2 : 0,\n      shinX: sit ? Math.PI / 2.1 : 0,\n      wave: pose === 'wave',\n    }\n  }, [pose, f.hipY, f.thigh, f.stoop])\n\n  useFrame((state, dt) => {\n    const t = state.clock.elapsedTime\n    const k = 1 - Math.exp(-7 * dt)\n\n    if (detail === 'auto' && root.current) {\n      const d = root.current.getWorldPosition(scratch).distanceTo(state.camera.position) / unit\n      if (!far && d > LOD_FAR) setFar(true)\n      else if (far && d < LOD_NEAR) setFar(false)\n    }\n\n    const breath = idle ? Math.sin(t * 1.6 + f.phase) : 0\n    const sway = idle ? Math.sin(t * 0.42 + f.phase) : 0\n\n    if (root.current) root.current.position.y = p.drop + breath * 0.004 * f.H\n    if (hips.current) hips.current.rotation.z = sway * 0.022\n    if (chest.current) {\n      chest.current.rotation.x = p.spineX\n      chest.current.rotation.z = -sway * 0.012\n      chest.current.scale.set(1, 1 + breath * 0.012, 1 + breath * 0.022)\n    }\n    if (shoulderL.current) {\n      shoulderL.current.rotation.x = p.armX - sway * 0.06\n      shoulderL.current.rotation.z = -p.armZ\n    }\n    if (shoulderR.current) {\n      shoulderR.current.rotation.x = p.wave ? -0.2 : p.armX + sway * 0.06\n      shoulderR.current.rotation.z = p.wave ? 2.35 : p.armZ\n    }\n    if (p.wave && elbowR.current) elbowR.current.rotation.z = -0.35 + Math.sin(t * 6) * 0.4\n\n    if (eyes.current && !low) {\n      const blink = (t + f.blinkPhase) % f.blinkEvery\n      eyes.current.scale.y = blink < 0.1 ? 0.12 : 1\n    }\n\n    // Head tracking: the camera position in the head's own parent space gives yaw and pitch\n    // directly. Outside the radius, or behind the shoulder, the head eases back to neutral.\n    if (headRef.current) {\n      const h = headRef.current\n      // Yaw before pitch, so looking up while turned doesn't roll the head.\n      if (h.rotation.order !== 'YXZ') h.rotation.order = 'YXZ'\n      let yaw = idle ? sway * 0.05 : 0\n      let pitch = 0\n      if (lookAt && h.parent) {\n        scratch.copy(state.camera.position)\n        h.parent.worldToLocal(scratch).sub(h.position)\n        const flat = Math.hypot(scratch.x, scratch.z)\n        const wanted = Math.atan2(scratch.x, scratch.z)\n        if (scratch.length() < lookRadius * unit && Math.abs(wanted) < MAX_YAW) {\n          yaw = wanted\n          pitch = clamp(-Math.atan2(scratch.y, flat), -MAX_PITCH, MAX_PITCH)\n        }\n      }\n      h.rotation.y += (yaw - h.rotation.y) * k\n      h.rotation.x += (pitch - h.rotation.x) * k\n    }\n\n    // Keep the name legible from wherever it's read.\n    if (nameTag.current?.parent) {\n      scratch.copy(state.camera.position)\n      nameTag.current.parent.worldToLocal(scratch)\n      nameTag.current.rotation.y = Math.atan2(scratch.x, scratch.z)\n    }\n  })\n\n  // ---- geometry -----------------------------------------------------------------------------\n  // Surfaces of revolution instead of primitive capsules: each limb is one smooth lathe whose\n  // radius follows the real profile (thigh tapering to the knee, the calf's bulge narrowing to\n  // the ankle), and the torso is a single hip-to-trapezius profile squashed into an elliptical\n  // cross-section. No seams, no marionette ball joints. Left and right limbs share geometry.\n  const seg = low ? 7 : 22\n  const sph = low ? 8 : 18\n  const torso = f.shoulderY - f.hipY\n  const cloth = f.outfit\n  const skirtLen = (f.thigh + f.shin) * cloth.skirt\n  const nR = f.neck * 0.62 * f.st.neck\n  const has = (a: PersonAccessory) => f.accessories.includes(a)\n\n  const geo = useMemo(() => {\n    const lathe = (pts: [number, number][]) =>\n      new LatheGeometry(\n        pts.map(([r, y]) => new Vector2(Math.max(r, 0.001), y)),\n        seg,\n      )\n    /** A limb hanging from y=0 down to -len; stations are [fraction along, radius]. */\n    const limb = (len: number, st: [number, number][]) => {\n      const rTop = st[0][1]\n      const rEnd = st[st.length - 1][1]\n      const pts: [number, number][] = [\n        [0, -len - rEnd * 0.8],\n        [rEnd * 0.7, -len - rEnd * 0.55],\n      ]\n      for (let i = st.length - 1; i >= 0; i--) pts.push([st[i][1], -st[i][0] * len])\n      pts.push([rTop * 0.72, rTop * 0.5], [0, rTop * 0.78])\n      return lathe(pts)\n    }\n\n    const t = f.shoulderY - f.hipY\n    const { chestR, waistR, hipR, neck } = f\n    const collarR = neck * 0.62 * f.st.neck\n    const sleeve = f.upperArm * f.outfit.sleeve\n    const hem = f.thigh * f.outfit.hem\n    const skirt = (f.thigh + f.shin) * f.outfit.skirt\n\n    return {\n      torso: lathe([\n        [0, -hipR * 0.7],\n        [hipR * 0.82, -hipR * 0.42],\n        [hipR, 0.04 * t],\n        [hipR * 0.96, 0.18 * t],\n        [waistR, 0.46 * t],\n        [chestR * 0.96, 0.72 * t],\n        [chestR, 0.84 * t],\n        [chestR * 0.97, 0.89 * t],\n        [collarR * 2.1, 1.0 * t],\n        [collarR * 1.4, 1.045 * t],\n        [0, 1.06 * t],\n      ]),\n      garment: lathe([\n        [0, -hem - hipR * 0.12],\n        [hipR * 1.1 + hem * 0.12, -hem],\n        [hipR * 1.09, -hem * 0.35],\n        [hipR * 1.07, 0.08 * t],\n        [hipR * 1.05, 0.2 * t],\n        [waistR * 1.12, 0.46 * t],\n        [chestR * 1.06, 0.76 * t],\n        [chestR * 1.03, 0.89 * t],\n        [collarR * 2.3, 1.01 * t],\n        [collarR * 1.2, 1.06 * t],\n        [0, 1.055 * t],\n      ]),\n      skirt:\n        skirt > 0\n          ? lathe([\n              [0, -skirt],\n              [hipR * 1.02 + skirt * 0.32, -skirt],\n              [hipR * 1.16, -skirt * 0.4],\n              [hipR * 1.13, 0.02 * t],\n              [waistR * 1.16, 0.12 * t],\n              [0, 0.13 * t],\n            ])\n          : null,\n      sleeve:\n        sleeve > 0.01\n          ? limb(sleeve, [\n              [0, f.armR * 1.3],\n              [1, f.armR * 1.18],\n            ])\n          : null,\n      upperArm: limb(f.upperArm, [\n        [0, f.armR * 1.12],\n        [0.5, f.armR],\n        [1, f.armR * 0.8],\n      ]),\n      foreArm: limb(f.foreArm, [\n        [0, f.armR * 0.82],\n        [0.25, f.armR * 0.9],\n        [1, f.armR * 0.55],\n      ]),\n      thigh: limb(f.thigh, [\n        [0, f.legR * 1.16],\n        [0.55, f.legR * 0.95],\n        [1, f.legR * 0.72],\n      ]),\n      calf: limb(f.shin, [\n        [0, f.legR * 0.74],\n        [0.3, f.legR * 0.87],\n        [1, f.legR * 0.42],\n      ]),\n    }\n  }, [f, seg])\n\n  useEffect(\n    () => () => {\n      for (const g of Object.values(geo)) g?.dispose()\n    },\n    [geo],\n  )\n\n  const skinMat = <meshStandardMaterial color={f.skin} roughness={0.75} />\n  const topMat = <meshStandardMaterial color={f.top} roughness={0.9} />\n  const bottomMat = <meshStandardMaterial color={f.bottom} roughness={0.9} />\n  const hairMat = <meshStandardMaterial color={f.hairInk} roughness={0.95} />\n  const trousers = skirtLen <= 0\n  const st = f.st\n\n  const arm = (side: -1 | 1) => (\n    <group\n      ref={side < 0 ? shoulderL : shoulderR}\n      position={[side * (f.chestR + f.armR * 0.25), torso * 0.88, 0]}\n    >\n      {/* deltoid: rounds the shoulder into the torso and rides the arm's rotation */}\n      <mesh castShadow>\n        <sphereGeometry args={[f.armR * 1.18, sph, sph / 2]} />\n        {cloth.sleeve > 0.01 ? topMat : skinMat}\n      </mesh>\n      <mesh geometry={geo.upperArm} castShadow>\n        {skinMat}\n      </mesh>\n      {geo.sleeve && (\n        <mesh geometry={geo.sleeve} castShadow>\n          {topMat}\n        </mesh>\n      )}\n      <group\n        ref={side > 0 ? elbowR : undefined}\n        position={[0, -f.upperArm, 0]}\n        rotation={[p.wave && side > 0 ? 0 : -0.18, 0, 0]}\n      >\n        <mesh>\n          <sphereGeometry args={[f.armR * 0.8, sph, sph / 2]} />\n          {skinMat}\n        </mesh>\n        <mesh geometry={geo.foreArm} castShadow>\n          {skinMat}\n        </mesh>\n        {/* hand: a mitt continuing the wrist line */}\n        <mesh\n          position={[0, -f.foreArm - f.armR * 0.85, f.armR * 0.15]}\n          scale={[0.8, 1.4, 0.5]}\n          castShadow\n        >\n          <sphereGeometry args={[f.armR * 1.05, sph, sph / 2]} />\n          {skinMat}\n        </mesh>\n        {side > 0 && has('staff') && (\n          <mesh position={[0, -f.foreArm * 0.9, f.armR * 1.4]} castShadow>\n            <cylinderGeometry args={[f.armR * 0.35, f.armR * 0.35, f.H * 1.05, 6]} />\n            <meshStandardMaterial color={palette.wood} roughness={0.9} />\n          </mesh>\n        )}\n      </group>\n    </group>\n  )\n\n  const leg = (side: -1 | 1) => (\n    <group position={[side * f.legX, f.hipY, 0]} rotation={[p.thighX, 0, 0]}>\n      <mesh geometry={geo.thigh} scale={trousers ? [1.14, 1, 1.14] : [1, 1, 1]} castShadow>\n        {trousers ? bottomMat : skinMat}\n      </mesh>\n      <group position={[0, -f.thigh, 0]} rotation={[p.shinX, 0, 0]}>\n        <mesh scale={trousers ? [1.14, 1, 1.14] : [1, 1, 1]}>\n          <sphereGeometry args={[f.legR * 0.78, sph, sph / 2]} />\n          {trousers ? bottomMat : skinMat}\n        </mesh>\n        <mesh geometry={geo.calf} scale={trousers ? [1.12, 1, 1.12] : [1, 1, 1]} castShadow>\n          {trousers ? bottomMat : skinMat}\n        </mesh>\n        <mesh\n          position={[0, -f.shin - f.legR * 0.42, f.foot * 0.32]}\n          scale={[0.82, 0.5, 1.75]}\n          castShadow\n        >\n          <sphereGeometry args={[f.legR * 1.05, sph, sph / 2]} />\n          <meshStandardMaterial color={f.shoe} roughness={0.7} />\n        </mesh>\n      </group>\n    </group>\n  )\n\n  const figure = (\n    <group ref={root}>\n      <group ref={hips}>\n        {leg(-1)}\n        {leg(1)}\n\n        {/* trouser seat: the pelvis in trouser color, so short garment hems (shirt, vest)\n            meet cloth rather than bare torso */}\n        {trousers && (\n          <mesh position={[0, f.hipY - f.hipR * 0.12, 0]} scale={[1, 0.78, f.torsoZ]} castShadow>\n            <sphereGeometry args={[f.hipR * 1.04, sph, sph / 2]} />\n            {bottomMat}\n          </mesh>\n        )}\n\n        {/* skirt: hangs from the hip so a spine lean doesn't swing the hem */}\n        {geo.skirt && (\n          <mesh\n            geometry={geo.skirt}\n            position={[0, f.hipY, 0]}\n            scale={[1, 1, (f.torsoZ + 1) / 2]}\n            castShadow\n          >\n            {topMat}\n          </mesh>\n        )}\n\n        <group ref={chest} position={[0, f.hipY, 0]}>\n          {/* torso + garment share the lathe profile, squashed to an elliptical section */}\n          <group scale={[1, 1, f.torsoZ]}>\n            <mesh geometry={geo.torso} castShadow>\n              {skinMat}\n            </mesh>\n            <mesh geometry={geo.garment} castShadow>\n              {topMat}\n            </mesh>\n            {has('belt') && (\n              <mesh position={[0, torso * 0.44, 0]} rotation={[Math.PI / 2, 0, 0]}>\n                <torusGeometry args={[f.waistR * 1.22, f.legR * 0.22, 6, low ? 8 : 20]} />\n                <meshStandardMaterial color={palette.woodDark} roughness={0.7} />\n              </mesh>\n            )}\n            {has('bag') && !low && (\n              <mesh position={[0, torso * 0.55, 0]} rotation={[0, 0, 0.55]}>\n                <torusGeometry args={[f.chestR * 1.35, f.legR * 0.16, 5, 16]} />\n                <meshStandardMaterial color={palette.woodDark} roughness={0.8} />\n              </mesh>\n            )}\n          </group>\n\n          {cloth.apron && !low && (\n            <mesh\n              position={[0, torso * 0.28, f.chestR * f.torsoZ * 1.02]}\n              rotation={[-0.06, 0, 0]}\n              castShadow\n            >\n              <boxGeometry args={[f.chestR * 1.1, torso * 0.72, f.chestR * 0.06]} />\n              <meshStandardMaterial color={palette.wall} roughness={0.95} />\n            </mesh>\n          )}\n          {has('bag') && !low && (\n            <mesh position={[f.chestR * 1.18, torso * 0.12, 0]} castShadow>\n              <boxGeometry args={[f.hipW * 0.42, f.hipW * 0.5, f.hipW * 0.26]} />\n              <meshStandardMaterial color={palette.wood} roughness={0.9} />\n            </mesh>\n          )}\n          {has('cape') && !low && (\n            <mesh\n              position={[0, torso * 0.5, -f.chestR * f.torsoZ * 0.7]}\n              rotation={[0.1, 0, 0]}\n              castShadow\n            >\n              <cylinderGeometry\n                args={[\n                  f.chestR * 1.15,\n                  f.chestR * 1.7,\n                  torso * 1.25,\n                  14,\n                  1,\n                  true,\n                  Math.PI - 1.25,\n                  2.5,\n                ]}\n              />\n              <meshStandardMaterial color={f.bottom} roughness={0.9} side={DoubleSide} />\n            </mesh>\n          )}\n          {has('scarf') && !low && (\n            <mesh position={[0, torso, 0]} rotation={[Math.PI / 2, 0, 0]}>\n              <torusGeometry args={[nR * 1.7, nR * 0.5, 6, 16]} />\n              <meshStandardMaterial color={palette.accent} roughness={0.95} />\n            </mesh>\n          )}\n\n          {arm(-1)}\n          {arm(1)}\n\n          {/* neck */}\n          <mesh position={[0, torso + f.neck * 0.3, 0]} castShadow>\n            <cylinderGeometry args={[nR * 0.95, nR * 1.2, f.neck * 1.15, 12]} />\n            {skinMat}\n          </mesh>\n\n          <group ref={headRef} position={[0, torso + f.neck + f.head * 0.42, 0]}>\n            {/* skull: cranium ellipsoid + a jaw that carries the chin below it */}\n            <mesh\n              position={[0, f.head * 0.04, -f.head * 0.01]}\n              scale={[st.headWide, st.headTall, 0.88]}\n              castShadow\n            >\n              <sphereGeometry args={[f.head * 0.5, sph, sph]} />\n              {skinMat}\n            </mesh>\n            <mesh position={[0, -f.head * 0.24, f.head * 0.04]} scale={st.jaw}>\n              <sphereGeometry args={[f.head * 0.4, sph, sph / 2]} />\n              {skinMat}\n            </mesh>\n\n            {!low && (\n              <>\n                {[-1, 1].map((s) => (\n                  <mesh\n                    key={s}\n                    position={[s * f.head * 0.36, 0, -f.head * 0.02]}\n                    scale={[0.3, 0.55, 0.4]}\n                  >\n                    <sphereGeometry args={[f.head * 0.11, 10, 8]} />\n                    {skinMat}\n                  </mesh>\n                ))}\n                {/* eyes: sclera, iris, pupil, and (per style) a specular sparkle */}\n                <group ref={eyes}>\n                  {[-1, 1].map((s) => (\n                    <group\n                      key={s}\n                      position={[s * f.head * 0.15, f.head * st.eyeY, f.head * 0.4]}\n                      rotation={[0, s * 0.1, 0]}\n                    >\n                      <mesh scale={[1, st.eyeH / st.eyeW, 0.3]}>\n                        <sphereGeometry args={[f.head * st.eyeW, 12, 10]} />\n                        <meshStandardMaterial color=\"#f4f0e8\" roughness={0.35} />\n                      </mesh>\n                      <mesh position={[0, 0, f.head * 0.014]} scale={[1, 1.3, 0.3]}>\n                        <sphereGeometry args={[f.head * st.iris, 12, 10]} />\n                        <meshStandardMaterial color={f.eye} roughness={0.25} />\n                      </mesh>\n                      <mesh position={[0, 0, f.head * 0.024]} scale={[1, 1.3, 0.3]}>\n                        <sphereGeometry args={[f.head * st.iris * 0.45, 10, 8]} />\n                        <meshStandardMaterial color=\"#181410\" roughness={0.25} />\n                      </mesh>\n                      {st.highlight && (\n                        <mesh\n                          position={[\n                            s * f.head * st.iris * 0.4,\n                            f.head * st.iris * 0.5,\n                            f.head * 0.032,\n                          ]}\n                        >\n                          <sphereGeometry args={[f.head * st.iris * 0.26, 8, 6]} />\n                          <meshStandardMaterial\n                            color=\"#ffffff\"\n                            emissive=\"#ffffff\"\n                            emissiveIntensity={0.35}\n                            roughness={0.2}\n                          />\n                        </mesh>\n                      )}\n                    </group>\n                  ))}\n                </group>\n                {[-1, 1].map((s) => (\n                  <mesh\n                    key={s}\n                    position={[\n                      s * f.head * 0.15,\n                      f.head * (st.eyeY + st.eyeH + 0.09),\n                      f.head * 0.41,\n                    ]}\n                    rotation={[0, 0, s * 0.1]}\n                  >\n                    <boxGeometry args={[f.head * 0.13, f.head * 0.022, f.head * 0.03]} />\n                    {hairMat}\n                  </mesh>\n                ))}\n                <mesh\n                  position={[0, -f.head * 0.1, f.head * 0.42]}\n                  rotation={[1.35, 0, 0]}\n                  scale={st.nose}\n                >\n                  <coneGeometry args={[f.head * 0.05, f.head * 0.15, 8]} />\n                  {skinMat}\n                </mesh>\n                <mesh position={[0, -f.head * 0.24, f.head * 0.37]}>\n                  <boxGeometry args={[f.head * 0.15 * st.mouth, f.head * 0.02, f.head * 0.02]} />\n                  <meshStandardMaterial color={f.lip} roughness={0.8} />\n                </mesh>\n                {f.facialHair !== 'none' && (\n                  <mesh\n                    position={[\n                      0,\n                      f.facialHair === 'moustache' ? -f.head * 0.14 : -f.head * 0.28,\n                      f.head * 0.16,\n                    ]}\n                    scale={[0.74, f.facialHair === 'beard' ? 0.9 : 0.3, 0.62]}\n                  >\n                    <sphereGeometry args={[f.head * 0.34, 12, 8]} />\n                    <meshStandardMaterial\n                      color={f.hairInk}\n                      roughness={0.95}\n                      transparent={f.facialHair === 'stubble'}\n                      opacity={f.facialHair === 'stubble' ? 0.55 : 1}\n                    />\n                  </mesh>\n                )}\n              </>\n            )}\n\n            {/* hair: a skull cap plus the style's own mass */}\n            {f.hair !== 'bald' && (\n              <>\n                <mesh\n                  position={[0, f.head * 0.07, -f.head * 0.03]}\n                  scale={[(st.headWide + 0.06) * st.hairVol, 0.98 * st.hairVol, 0.92 * st.hairVol]}\n                >\n                  <sphereGeometry args={[f.head * 0.52, sph, sph / 2, 0, Math.PI * 2, 0, 1.5]} />\n                  {hairMat}\n                </mesh>\n                {st.bangs &&\n                  !low &&\n                  BANGS.map(([bx, bz, len, tilt]) => (\n                    <mesh\n                      key={`${bx}:${bz}`}\n                      position={[bx * f.head, f.head * 0.18, bz * f.head]}\n                      rotation={[Math.PI - 0.24, 0, tilt]}\n                    >\n                      <coneGeometry args={[f.head * 0.088, f.head * len, 6]} />\n                      {hairMat}\n                    </mesh>\n                  ))}\n                {(f.hair === 'long' || f.hair === 'braid') && (\n                  <mesh\n                    position={[0, -f.head * 0.26, -f.head * 0.16]}\n                    scale={[\n                      (st.headWide + 0.14) * (f.hair === 'braid' ? 0.55 : 1),\n                      f.hair === 'braid' ? 2.1 : 1.7,\n                      0.6,\n                    ]}\n                    castShadow\n                  >\n                    <capsuleGeometry args={[f.head * 0.34, f.head * 0.5, 3, low ? 6 : 10]} />\n                    {hairMat}\n                  </mesh>\n                )}\n                {f.hair === 'ponytail' && (\n                  <mesh\n                    position={[0, -f.head * 0.1, -f.head * 0.5]}\n                    rotation={[0.5, 0, 0]}\n                    castShadow\n                  >\n                    <capsuleGeometry args={[f.head * 0.13, f.head * 0.65, 3, low ? 6 : 10]} />\n                    {hairMat}\n                  </mesh>\n                )}\n                {f.hair === 'bun' && (\n                  <mesh position={[0, f.head * 0.36, -f.head * 0.32]} castShadow>\n                    <sphereGeometry args={[f.head * 0.22, low ? 8 : 14, low ? 6 : 10]} />\n                    {hairMat}\n                  </mesh>\n                )}\n              </>\n            )}\n\n            {/* hat */}\n            {f.hat !== 'none' && (\n              <group position={[0, f.head * 0.42, 0]}>\n                {f.hat === 'straw' && (\n                  <>\n                    <mesh position={[0, f.head * 0.05, 0]} castShadow>\n                      <cylinderGeometry args={[f.head * 0.02, f.head * 0.46, f.head * 0.3, 12]} />\n                      <meshStandardMaterial color={palette.sand} roughness={0.95} />\n                    </mesh>\n                    <mesh position={[0, -f.head * 0.06, 0]} castShadow>\n                      <cylinderGeometry args={[f.head * 0.95, f.head * 0.95, f.head * 0.04, 14]} />\n                      <meshStandardMaterial color={palette.sand} roughness={0.95} />\n                    </mesh>\n                  </>\n                )}\n                {f.hat === 'brim' && (\n                  <>\n                    <mesh position={[0, f.head * 0.12, 0]} castShadow>\n                      <cylinderGeometry args={[f.head * 0.42, f.head * 0.45, f.head * 0.34, 12]} />\n                      {bottomMat}\n                    </mesh>\n                    <mesh position={[0, -f.head * 0.04, 0]} castShadow>\n                      <cylinderGeometry args={[f.head * 0.72, f.head * 0.72, f.head * 0.05, 14]} />\n                      {bottomMat}\n                    </mesh>\n                  </>\n                )}\n                {f.hat === 'cap' && (\n                  <>\n                    <mesh position={[0, -f.head * 0.04, 0]} scale={[0.84, 0.7, 0.95]} castShadow>\n                      <sphereGeometry args={[f.head * 0.56, 14, 8, 0, Math.PI * 2, 0, 1.6]} />\n                      <meshStandardMaterial color={palette.accent} roughness={0.9} />\n                    </mesh>\n                    <mesh position={[0, -f.head * 0.06, f.head * 0.45]} castShadow>\n                      <boxGeometry args={[f.head * 0.5, f.head * 0.04, f.head * 0.3]} />\n                      <meshStandardMaterial color={palette.accent} roughness={0.9} />\n                    </mesh>\n                  </>\n                )}\n                {f.hat === 'bandana' && (\n                  <mesh position={[0, -f.head * 0.08, 0]} scale={[0.84, 0.62, 0.95]}>\n                    <sphereGeometry args={[f.head * 0.56, 12, 8, 0, Math.PI * 2, 0, 1.6]} />\n                    <meshStandardMaterial color={palette.fabric} roughness={0.95} />\n                  </mesh>\n                )}\n                {f.hat === 'hood' && (\n                  <mesh position={[0, -f.head * 0.2, -f.head * 0.08]} scale={[0.95, 1, 1.05]}>\n                    {/* phi runs from -X, so the face (+Z) is the gap left open. */}\n                    <sphereGeometry args={[f.head * 0.64, 16, 10, 2.6, 4.2, 0, 2.1]} />\n                    <meshStandardMaterial color={f.top} roughness={0.95} side={DoubleSide} />\n                  </mesh>\n                )}\n              </group>\n            )}\n\n            {has('glasses') && !low && (\n              <group position={[0, f.head * 0.06, f.head * 0.44]}>\n                {[-1, 1].map((s) => (\n                  <mesh key={s} position={[s * f.head * 0.14, 0, 0]}>\n                    <torusGeometry args={[f.head * 0.09, f.head * 0.013, 5, 12]} />\n                    <meshStandardMaterial color={palette.metal} roughness={0.5} metalness={0.4} />\n                  </mesh>\n                ))}\n                <mesh>\n                  <boxGeometry args={[f.head * 0.13, f.head * 0.015, f.head * 0.015]} />\n                  <meshStandardMaterial color={palette.metal} roughness={0.5} metalness={0.4} />\n                </mesh>\n              </group>\n            )}\n          </group>\n        </group>\n      </group>\n\n      {label && (\n        <group ref={nameTag} position={[0, f.H + f.head * 0.55, 0]}>\n          <Sign variant=\"body\" size={0.13} color={palette.accent}>\n            {label}\n          </Sign>\n        </group>\n      )}\n    </group>\n  )\n\n  if (!physics) {\n    return (\n      <group position={at} rotation={rotation}>\n        {figure}\n      </group>\n    )\n  }\n\n  // One capsule spanning the standing (or seated) body: an obstacle you bump into, not a\n  // collider per limb (CONTRACT §5).\n  const bottom = p.sit ? f.H * 0.06 : 0\n  const top = f.H + p.drop\n  const radius = Math.min(Math.max(f.hipW, f.shoulderW * 0.62) * 0.55, (top - bottom) / 2)\n  const half = Math.max((top - bottom) / 2 - radius, 0.01 * f.H)\n\n  return (\n    <RigidBody type=\"fixed\" colliders={false} position={at} rotation={rotation}>\n      {collider && <CapsuleCollider args={[half, radius]} position={[0, (top + bottom) / 2, 0]} />}\n      {figure}\n    </RigidBody>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
