← Natura·Docs/Modules/Source

creatures/render.js

1162 lines · in natura-src-1.0.0.zip as natura/creatures/render.js · a module · its entry in the module reference

// creatures/render.js -- CreatureRender: the DRAWING layer.
//
// Turns a species def (from creatures/species.js) plus a pose (from creatures/behaviour.js, eased by
// the caller) into pixels on a 2D canvas context. It knows nothing about WHICH animals exist or HOW
// they decide to move -- it takes a resolved def and a name (the name only seeds stable per-animal
// texture) and draws. Two body plans, two builders: drawCreature (quadruped) and drawBird (biped).
//
// Level of detail: `det` (0 far silhouette .. 1 full model), derived from on-screen scale or forced
// via pose.lod, gates the fine work (form-shading, markings, fur, feathers, catchlight) so a far
// animal is a cheap flat shape that resolves into fur and an eye as it nears -- the dot->model idea.
//
// The operator's note (2026-08-03): the render itself is not the priority and a better pipeline will
// come one day; these canvas creatures have their own charm. So this module stays deliberately simple
// (immediate-mode 2D, no scene graph), and the value lives in the data + behaviour it renders.
//
// UMD (node test / window.CreatureRender). No dependencies; math helpers are internal.
(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory();
  else root.CreatureRender = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';

  // ---- LIGHT: the scene picks the light source so the same creature reads warm at dawn or COOL under the moon.
  // rim = the form-defining highlight over the back; dir = the directional flank warmth. A night scene calls
  // setLight('moon') for a cool blue-white rim (moonlight), so nocturnal creatures read without a warm-haze clash.
  let LIGHT = { rim:'255,240,206', dir:'255,234,196', night:false };                 // a warm low sun (the default)
  function setLight(mode){
    LIGHT = (mode==='moon') ? { rim:'212,224,244', dir:'190,206,236', night:true }   // cool moonlight
                            : { rim:'255,240,206', dir:'255,234,196', night:false };  // warm sun
  }

  // ---- internal math / texture helpers -------------------------------------------------------
  const clamp = (v,a,b)=>v<a?a:v>b?b:v;
  // 2-bone IK: knee/elbow point given hip, foot, segment lengths, and a bend sign
  function ik(hx,hy,fx,fy,l1,l2,bend){
    let dx=fx-hx, dy=fy-hy, d=Math.hypot(dx,dy);
    d=clamp(d, Math.abs(l1-l2)+0.001, l1+l2-0.001);
    const base=Math.atan2(dy,dx);
    const ca=clamp((l1*l1+d*d-l2*l2)/(2*l1*d),-1,1);
    const ang=base+bend*Math.acos(ca);
    return [hx+Math.cos(ang)*l1, hy+Math.sin(ang)*l1];
  }
  // foot position in leg-local coords over the gait cycle (x fore/aft, y lift up = negative)
  function foot(phase, off, stride, lift, duty){
    let p=(phase+off)%1; if(p<0)p+=1;
    if(p<duty){ const s=p/duty; return [stride*0.5 - s*stride, 0]; }       // stance: planted, slides back
    const s=(p-duty)/(1-duty); return [-stride*0.5 + s*stride, -lift*Math.sin(s*Math.PI)]; // swing: lifts, forward
  }
  // deterministic seeded RNG (mulberry32): mulberry(seed) returns fr, and fr() yields [0,1)
  function mulberry(seed){ let a=seed>>>0; return function(){ a=(a+0x6D2B79F5)|0; let t=Math.imul(a^(a>>>15), 1|a); t=(t+Math.imul(t^(t>>>7), 61|t))^t; return ((t^(t>>>14))>>>0)/4294967296; }; }
  // hex colour + alpha -> rgba string, for soft coat markings
  function hexa(h,a){ const n=parseInt(h.slice(1),16); return 'rgba('+(n>>16&255)+','+(n>>8&255)+','+(n&255)+','+a+')'; }

  // ============================================================================================
  // QUADRUPED -- build the animal from proportion def S (name sp seeds stable fur/spots).
  // ============================================================================================
  // a HEDGEHOG CURLED into a spiny BALL -- head and legs tucked away, the body a sphere, spines radiating outward in
  // every direction (skipping the ground contact). The signature defence; drawn instead of the standing form when curled.
  function drawCurledBall(ctx, S, gy, pose){
    const L=S.L, H=S.H, sp=S.spines, ballR=L*0.42, cy=gy-ballR, ck=pose.clock||0;
    ctx.save(); ctx.translate(0,gy+2); ctx.scale(1,0.16); const shR=ballR*0.98;      // soft contact shadow
    const shG=ctx.createRadialGradient(0,0,0,0,0,shR); shG.addColorStop(0,'rgba(0,0,0,0.34)'); shG.addColorStop(1,'rgba(0,0,0,0)');
    ctx.fillStyle=shG; ctx.beginPath(); ctx.arc(0,0,shR,0,7); ctx.fill(); ctx.restore();
    const gr=ctx.createRadialGradient(-ballR*0.35,cy-ballR*0.35,ballR*0.2, 0,cy,ballR*1.15);   // form-shaded body sphere
    gr.addColorStop(0,S.col[0]); gr.addColorStop(0.6,S.col[1]); gr.addColorStop(1,S.col[2]);
    ctx.fillStyle=gr; ctx.beginPath(); ctx.arc(0,cy,ballR,0,7); ctx.fill();
    const hh=(i,o)=>{ const s=Math.sin((i+1)*12.9898+o*78.233)*43758.5; return s-Math.floor(s); };
    ctx.lineCap='round';
    for(let i=0;i<sp.n;i++){ const a=hh(i,0)*Math.PI*2; if(Math.sin(a)>0.55) continue;     // radial spines, skipping the ground sector
      const rr=ballR*(0.9+0.08*hh(i,1)), len=sp.len*(0.72+0.5*hh(i,2))*(1+0.04*Math.sin(ck*9+i)), ca=Math.cos(a), sa=Math.sin(a);
      const bx=ca*rr, by=cy+sa*rr, tx=ca*(rr+len), ty=cy+sa*(rr+len), mx=(bx+tx)/2, my=(by+ty)/2;
      ctx.strokeStyle=hexa(S.col[2],0.92); ctx.lineWidth=Math.max(0.6,H*0.06); ctx.beginPath(); ctx.moveTo(bx,by); ctx.lineTo(mx,my); ctx.stroke();
      ctx.strokeStyle=sp.band||'#e8dcc2'; ctx.lineWidth=Math.max(0.5,H*0.045); ctx.beginPath(); ctx.moveTo(mx,my); ctx.lineTo(tx,ty); ctx.stroke(); }
  }
  // a SQUIRREL sitting UP on its haunches, holding food in its forepaws to its mouth and nibbling -- its own
  // upright FORM (like the hedgehog's curled ball), swapped in by pose.siteat. The tail arches up behind for balance.
  function drawSquirrelSit(ctx, S, sp, gy, det, pose){
    const L=S.L, H=S.H, ck=pose.clock||0, nib=Math.max(0,Math.sin(ck*7))*0.5;   // nibble: a small head/food dip
    ctx.save(); ctx.translate(0,gy+2); ctx.scale(1,0.16); const shR=L*0.5;        // contact shadow
    const shG=ctx.createRadialGradient(0,0,0,0,0,shR); shG.addColorStop(0,'rgba(0,0,0,0.32)'); shG.addColorStop(1,'rgba(0,0,0,0)');
    ctx.fillStyle=shG; ctx.beginPath(); ctx.arc(0,0,shR,0,7); ctx.fill(); ctx.restore();
    // ---- TAIL behind the body (drawn first, body overlaps its base): a squirrel's PLUME, or a mouse's thin NAKED tail ----
    const perp=(a,i)=>{ const p=a[Math.max(0,i-1)],q=a[Math.min(a.length-1,i+1)],dx=q[0]-p[0],dy=q[1]-p[1],dl=Math.hypot(dx,dy)||1; return [-dy/dl,dx/dl]; };
    if(S.nakedTail){
      // a MOUSE/RAT seated: the long thin naked tail trails DOWN off the haunch and back low along the ground, curling
      ctx.strokeStyle=S.tailNaked||S.belly; ctx.lineCap='round';
      const q=(t)=>{ const mt=1-t, bx=-L*0.04,by=gy-H*0.25, cx=-L*0.55,cy=gy+H*0.02, ex=-L*0.92,ey=gy-H*0.02;
        return [mt*mt*bx+2*mt*t*cx+t*t*ex, mt*mt*by+2*mt*t*cy+t*t*ey]; };
      for(let i=0;i<10;i++){ const a=q(i/10), b=q((i+1)/10); ctx.lineWidth=2.1*(1-0.55*i/10);
        ctx.beginPath(); ctx.moveTo(a[0],a[1]); ctx.lineTo(b[0],b[1]); ctx.stroke(); }
    } else {
      const tbx=-L*0.06,tby=gy-H*0.4, tcx=-L*0.06-H*0.5,tcy=gy-H*1.4, ttx=-L*0.02,tty=gy-H*2.5, bw=S.bushy||10;
      const seg=[]; for(let i=0;i<=9;i++){ const t=i/9, mt=1-t;
        seg.push([mt*mt*tbx+2*mt*t*tcx+t*t*ttx, mt*mt*tby+2*mt*t*tcy+t*t*tty, bw*(0.5+0.8*Math.sin(t*Math.PI))*(1.15-0.45*t)]); }
      ctx.fillStyle=S.col[1]; ctx.beginPath();
      for(let i=0;i<seg.length;i++){ const [px,py]=perp(seg,i),w=seg[i][2]; const X=seg[i][0]+px*w,Y=seg[i][1]+py*w; i?ctx.lineTo(X,Y):ctx.moveTo(X,Y); }
      for(let i=seg.length-1;i>=0;i--){ const [px,py]=perp(seg,i),w=seg[i][2]; ctx.lineTo(seg[i][0]-px*w,seg[i][1]-py*w); }
      ctx.closePath(); ctx.fill();
      if(det>0.4){ ctx.strokeStyle=hexa(S.col[2],0.32); ctx.lineWidth=0.7; ctx.lineCap='round';
        const tf=mulberry(sp.length*13+7); for(let i=0;i<12;i++){ const t=0.15+tf()*0.8, k=Math.round(t*9), b=seg[k], [px,py]=perp(seg,k);
          ctx.beginPath(); ctx.moveTo(b[0],b[1]); ctx.lineTo(b[0]-px*bw*(0.7+tf()*0.7), b[1]-py*bw*(0.7+tf()*0.7)); ctx.stroke(); } }
    }
    // ---- BODY: a plump seated spindle from the haunch up to the chest (belly bulging forward) ----
    const P_seat=[-L*0.03,gy], ctrl=[L*0.10,gy-H*0.85], P_sh=[L*0.05,gy-H*1.62];
    const NB=10, bs=[]; for(let i=0;i<=NB;i++){ const t=i/NB, mt=1-t;
      bs.push([mt*mt*P_seat[0]+2*mt*t*ctrl[0]+t*t*P_sh[0], mt*mt*P_seat[1]+2*mt*t*ctrl[1]+t*t*P_sh[1], H*(0.72-0.30*t)*(0.92+0.16*Math.sin(t*Math.PI))]); }
    const bodyPath=()=>{ ctx.beginPath();
      for(let i=0;i<=NB;i++){ const [px,py]=perp(bs,i),w=bs[i][2]; const X=bs[i][0]+px*w,Y=bs[i][1]+py*w; i?ctx.lineTo(X,Y):ctx.moveTo(X,Y); }
      for(let i=NB;i>=0;i--){ const [px,py]=perp(bs,i),w=bs[i][2]; ctx.lineTo(bs[i][0]-px*w,bs[i][1]-py*w); } ctx.closePath(); };
    const bg=ctx.createLinearGradient(P_seat[0]-H*0.7,0,P_seat[0]+H*0.7,0);     // back(-x) dark -> belly(+x) light
    bg.addColorStop(0,S.col[2]); bg.addColorStop(0.5,S.col[1]); bg.addColorStop(1,S.col[0]);
    bodyPath(); ctx.fillStyle=bg; ctx.fill();
    ctx.save(); bodyPath(); ctx.clip();
    const cg=ctx.createLinearGradient(0,gy,0,gy-H*1.3);                          // pale belly, forward+low
    cg.addColorStop(0,hexa(S.belly||'#efe7d6',0.85)); cg.addColorStop(1,hexa(S.belly||'#efe7d6',0));
    ctx.fillStyle=cg; ctx.fillRect(P_seat[0]-H*0.1,gy-H*1.6,H*1.5,H*1.7);
    if(det>0.5){ const ff=mulberry(sp.length*29+3), fn=S.fur?S.fur.n:80; ctx.strokeStyle=hexa(S.col[2],0.26); ctx.lineWidth=0.6; ctx.lineCap='round';
      for(let i=0;i<fn;i++){ const t=ff(), k=Math.min(NB,Math.round(t*NB)), c=bs[k], [px,py]=perp(bs,k), off=(ff()*2-1)*c[2];
        const bx=c[0]+px*off, by=c[1]+py*off, ln=(S.fur?S.fur.len:3)*(0.6+0.5*ff());
        ctx.beginPath(); ctx.moveTo(bx,by); ctx.lineTo(bx-(off<0?-0.3:0.3), by+ln); ctx.stroke(); } }
    ctx.restore();
    if(det>0.5){ ctx.save(); bodyPath(); ctx.clip(); ctx.strokeStyle=hexa('#ffffff',0.15); ctx.lineWidth=2.2;   // back rim light
      ctx.beginPath(); for(let i=0;i<=NB;i++){ const [px,py]=perp(bs,i),w=bs[i][2]; const X=bs[i][0]-px*w,Y=bs[i][1]-py*w; i?ctx.lineTo(X,Y):ctx.moveTo(X,Y);} ctx.stroke(); ctx.restore(); }
    // ---- a folded HIND FOOT forward at the base ----
    ctx.fillStyle=S.col[2]; ctx.beginPath(); ctx.ellipse(L*0.11,gy-2,H*0.28,H*0.12,0,0,7); ctx.fill();
    // ---- HEAD atop the chest, tipped toward the paws ----
    const hx=L*0.10, hy=gy-H*1.78+nib*H*0.06, hr=S.head*0.95;
    if(S.earRound){ ctx.fillStyle=S.col[1]; ctx.beginPath(); ctx.arc(hx-hr*0.12,hy-hr*1.12,hr*0.74,0,7); ctx.fill();   // a big ROUND ear (mouse)
      if(S.earInner){ ctx.fillStyle=hexa(S.earInner,0.85); ctx.beginPath(); ctx.arc(hx-hr*0.10,hy-hr*1.05,hr*0.46,0,7); ctx.fill(); } }
    else { ctx.fillStyle=S.col[1]; ctx.beginPath(); ctx.moveTo(hx-hr*0.5,hy-hr*0.7); ctx.lineTo(hx-hr*0.1,hy-hr*1.7); ctx.lineTo(hx+hr*0.35,hy-hr*0.8); ctx.closePath(); ctx.fill();  // pointed ear (squirrel)
      if(S.earInner){ ctx.fillStyle=hexa(S.earInner,0.8); ctx.beginPath(); ctx.moveTo(hx-hr*0.3,hy-hr*0.85); ctx.lineTo(hx-hr*0.05,hy-hr*1.45); ctx.lineTo(hx+hr*0.2,hy-hr*0.9); ctx.closePath(); ctx.fill(); } }
    if(S.earTip && det>0.4){ ctx.strokeStyle=S.earTip; ctx.lineWidth=1.4; ctx.lineCap='round'; ctx.beginPath(); ctx.moveTo(hx-hr*0.1,hy-hr*1.66); ctx.lineTo(hx-hr*0.12,hy-hr*2.06); ctx.stroke(); }  // ear tuft
    const hg=ctx.createRadialGradient(hx-hr*0.3,hy-hr*0.3,hr*0.2,hx,hy,hr*1.2); hg.addColorStop(0,S.col[0]); hg.addColorStop(1,S.col[1]);
    ctx.fillStyle=hg; ctx.beginPath(); ctx.arc(hx,hy,hr,0,7); ctx.fill();
    ctx.fillStyle=S.col[1]; ctx.beginPath(); ctx.ellipse(hx+hr*0.7,hy+hr*0.5,hr*0.62,hr*0.42,0.5,0,7); ctx.fill();  // muzzle toward the food
    if(S.nose){ ctx.fillStyle=S.nose; ctx.beginPath(); ctx.arc(hx+hr*1.12,hy+hr*0.78,hr*0.16,0,7); ctx.fill(); }
    const ex=hx+hr*0.42, ey=hy-hr*0.12, er=hr*0.34;                              // big dark eye + catchlight
    ctx.fillStyle='#160d08'; ctx.beginPath(); ctx.arc(ex,ey,er,0,7); ctx.fill();
    if(det>0.4){ ctx.fillStyle='rgba(255,255,255,0.9)'; ctx.beginPath(); ctx.arc(ex+er*0.3,ey-er*0.3,er*0.3,0,7); ctx.fill(); }
    if(det>0.55){ ctx.strokeStyle=hexa('#efe7d6',0.5); ctx.lineWidth=0.6; ctx.lineCap='round';   // whiskers forward
      for(let i=0;i<4;i++){ const a=-0.2+i*0.18; ctx.beginPath(); ctx.moveTo(hx+hr*1.0,hy+hr*0.5);
        ctx.quadraticCurveTo(hx+hr*1.9,hy+hr*0.5+a*hr*3, hx+hr*2.6,hy+hr*0.6+a*hr*5); ctx.stroke(); } }
    // ---- FORELIMBS holding the FOOD up to the mouth ----
    const paw=[hx+hr*1.15, hy+hr*1.6+nib*H*0.05];
    ctx.strokeStyle=S.col[2]; ctx.lineWidth=Math.max(1.6,H*0.11); ctx.lineCap='round';
    ctx.beginPath(); ctx.moveTo(P_sh[0]+H*0.02,P_sh[1]+H*0.3); ctx.quadraticCurveTo(hx+hr*1.3,hy+hr*1.4, paw[0]-1,paw[1]+1); ctx.stroke();  // far foreleg
    ctx.strokeStyle=S.col[1]; ctx.lineWidth=Math.max(2,H*0.14);
    ctx.beginPath(); ctx.moveTo(P_sh[0]+H*0.16,P_sh[1]+H*0.22); ctx.quadraticCurveTo(hx+hr*1.55,hy+hr*1.2, paw[0],paw[1]); ctx.stroke();     // near foreleg
    ctx.fillStyle=S.col[2]; ctx.beginPath(); ctx.arc(paw[0],paw[1],H*0.13,0,7); ctx.fill();
    if(det>0.3){ if(S.nakedTail){ ctx.fillStyle='#e6d8b0'; ctx.beginPath(); ctx.ellipse(paw[0]+H*0.04,paw[1]-H*0.04,H*0.10,H*0.15,0.4,0,7); ctx.fill(); }  // a pale SEED (mouse)
      else { ctx.fillStyle='#8a6a3a'; ctx.beginPath(); ctx.ellipse(paw[0]+H*0.05,paw[1]-H*0.05,H*0.13,H*0.18,0.3,0,7); ctx.fill();     // an acorn (squirrel)
        ctx.fillStyle='#5a4326'; ctx.beginPath(); ctx.ellipse(paw[0]+H*0.05,paw[1]-H*0.2,H*0.11,H*0.07,0.3,0,7); ctx.fill(); } }
  }
  // the UPRIGHT-PEER form -- a creature reared onto its hind feet to look around: the otter's periscope AND
  // the meerkat's sentinel stand are the same posture, so ONE form serves both, keyed on species params (the
  // tail thickness S.tailW, the snout length S.snout -> a broad otter muzzle vs a pointed meerkat one, and an
  // optional dark S.eyeMask). Tail a tripod-prop behind, forepaws clasped at the chest, head up and scanning.
  // Swapped in by pose.siteat for any species flagged periscope.
  function drawUprightPeer(ctx, S, sp, gy, det, pose){
    const L=S.L, H=S.H, ck=pose.clock||0, look=Math.sin(ck*0.8)*0.14;
    const perp=(a,i)=>{ const p=a[Math.max(0,i-1)],q=a[Math.min(a.length-1,i+1)],dx=q[0]-p[0],dy=q[1]-p[1],dl=Math.hypot(dx,dy)||1; return [-dy/dl,dx/dl]; };
    ctx.save(); ctx.translate(0,gy+2); ctx.scale(1,0.16); const shR=L*0.32;
    const shG=ctx.createRadialGradient(0,0,0,0,0,shR); shG.addColorStop(0,'rgba(0,0,0,0.34)'); shG.addColorStop(1,'rgba(0,0,0,0)');
    ctx.fillStyle=shG; ctx.beginPath(); ctx.arc(0,0,shR,0,7); ctx.fill(); ctx.restore();
    // TAIL: a tapering prop from the low rump to the floor (the tripod's third leg) -- SCALED by the creature's real
    // tail, so a long-tailed otter/meerkat braces a full prop while a stub-tailed BEAR balances on its bulk alone
    const tp=Math.min(1,(S.tail||0)/24);            // 0 = stub (bear) .. 1 = full prop (otter/meerkat)
    const tb=[-L*0.03,gy-H*0.55], tc=[-L*(0.08+0.26*tp),gy-H*0.2], tt=[-L*(0.14+0.42*tp),gy-1], tw0=H*0.6*(S.tailW||1.0)*0.6*(0.35+0.65*tp);
    const ts=[]; for(let i=0;i<=8;i++){ const t=i/8, mt=1-t; ts.push([mt*mt*tb[0]+2*mt*t*tc[0]+t*t*tt[0], mt*mt*tb[1]+2*mt*t*tc[1]+t*t*tt[1], tw0*(1-0.8*t)]); }
    ctx.fillStyle=S.col[1]; ctx.beginPath();
    for(let i=0;i<ts.length;i++){ const [px,py]=perp(ts,i),w=ts[i][2],X=ts[i][0]+px*w,Y=ts[i][1]+py*w; i?ctx.lineTo(X,Y):ctx.moveTo(X,Y); }
    for(let i=ts.length-1;i>=0;i--){ const [px,py]=perp(ts,i),w=ts[i][2]; ctx.lineTo(ts[i][0]-px*w,ts[i][1]-py*w); } ctx.closePath(); ctx.fill();
    // BODY: a tall upright chunky spindle (seat -> chest)
    const P_seat=[0,gy], ctrl=[L*0.07,gy-L*0.33], P_sh=[-L*0.01,gy-L*0.64];
    const NB=10, bs=[]; for(let i=0;i<=NB;i++){ const t=i/NB, mt=1-t;
      bs.push([mt*mt*P_seat[0]+2*mt*t*ctrl[0]+t*t*P_sh[0], mt*mt*P_seat[1]+2*mt*t*ctrl[1]+t*t*P_sh[1], H*(0.92-0.34*t)*(0.9+0.18*Math.sin(t*Math.PI))]); }
    const bodyPath=()=>{ ctx.beginPath();
      for(let i=0;i<=NB;i++){ const [px,py]=perp(bs,i),w=bs[i][2]; const X=bs[i][0]+px*w,Y=bs[i][1]+py*w; i?ctx.lineTo(X,Y):ctx.moveTo(X,Y); }
      for(let i=NB;i>=0;i--){ const [px,py]=perp(bs,i),w=bs[i][2]; ctx.lineTo(bs[i][0]-px*w,bs[i][1]-py*w); } ctx.closePath(); };
    const bg=ctx.createLinearGradient(-H,0,H,0); bg.addColorStop(0,S.col[2]); bg.addColorStop(0.5,S.col[1]); bg.addColorStop(1,S.col[0]);
    bodyPath(); ctx.fillStyle=bg; ctx.fill();
    ctx.save(); bodyPath(); ctx.clip();
    const cg=ctx.createLinearGradient(0,gy,0,gy-L*0.6); cg.addColorStop(0,hexa(S.belly||'#c2b399',0.92)); cg.addColorStop(1,hexa(S.belly||'#c2b399',0));  // pale throat down the front
    ctx.fillStyle=cg; ctx.fillRect(P_sh[0]-H*0.4,gy-L*0.62,H*0.95,L*0.62);
    if(det>0.5){ const ff=mulberry(sp.length*29+5), fn=S.fur?S.fur.n:80; ctx.strokeStyle=hexa(S.col[2],0.2); ctx.lineWidth=0.6; ctx.lineCap='round';
      for(let i=0;i<fn;i++){ const t=ff(), k=Math.min(NB,Math.round(t*NB)), c=bs[k], [px,py]=perp(bs,k), off=(ff()*2-1)*c[2];
        ctx.beginPath(); ctx.moveTo(c[0]+px*off,c[1]+py*off); ctx.lineTo(c[0]+px*off-(off<0?-0.3:0.3),c[1]+py*off+(S.fur?S.fur.len:3)*(0.5+0.4*ff())); ctx.stroke(); } }
    ctx.restore();
    // HIND FEET planted at the base
    ctx.fillStyle=S.col[2]; ctx.beginPath(); ctx.ellipse(L*0.07,gy-1,H*0.32,H*0.13,0,0,7); ctx.fill();
    ctx.beginPath(); ctx.ellipse(-L*0.06,gy-1,H*0.26,H*0.11,0,0,7); ctx.fill();
    // HEAD up top, slowly scanning
    const hx=P_sh[0]+H*0.1+look*H*1.4, hy=gy-L*0.64-S.head*0.9, hr=S.head*1.0;
    ctx.fillStyle=S.col[1]; for(const s of [-0.55,0.55]){ ctx.beginPath(); ctx.arc(hx+s*hr*0.85, hy-hr*0.78, hr*0.32,0,7); ctx.fill(); }  // small round ears
    if(S.earInner){ ctx.fillStyle=hexa(S.earInner,0.85); for(const s of [-0.55,0.55]){ ctx.beginPath(); ctx.arc(hx+s*hr*0.85, hy-hr*0.74, hr*0.17,0,7); ctx.fill(); } }
    const hg=ctx.createRadialGradient(hx-hr*0.3,hy-hr*0.3,hr*0.2,hx,hy,hr*1.2); hg.addColorStop(0,S.col[0]); hg.addColorStop(1,S.col[1]);
    ctx.fillStyle=hg; ctx.beginPath(); ctx.arc(hx,hy,hr,0,7); ctx.fill();
    const ms=(S.snout||7)/(S.head||9);                                             // snout:head ratio -> a broad otter muzzle (~0.8) vs a long pointed meerkat one (~1.4)
    ctx.fillStyle=S.col[1]; ctx.beginPath(); ctx.ellipse(hx+hr*(0.35+ms*0.4),hy+hr*0.3,hr*(0.35+ms*0.35),hr*(0.56-ms*0.16),0,0,7); ctx.fill();  // muzzle: longer + narrower as the snout lengthens
    if(S.belly){ ctx.fillStyle=hexa(S.belly,0.55); ctx.beginPath(); ctx.ellipse(hx+hr*0.18,hy+hr*0.55,hr*0.5,hr*0.32,0,0,7); ctx.fill(); }  // pale chin/throat
    if(S.nose){ ctx.fillStyle=S.nose; ctx.beginPath(); ctx.ellipse(hx+hr*(0.35+ms*0.82),hy+hr*0.24,hr*0.22,hr*0.16,0,0,7); ctx.fill(); }   // nose at the snout tip
    const ex=hx+hr*0.42, ey=hy-hr*0.2, er=hr*0.3;
    if(S.eyeMask){ ctx.fillStyle=hexa(S.eyeMask,0.9); ctx.beginPath(); ctx.ellipse(ex+hr*0.05,ey,er*2.0,er*1.55,0.15,0,7); ctx.fill(); }   // the meerkat's dark eye-mask (behind the eye)
    ctx.fillStyle='#160d08'; ctx.beginPath(); ctx.arc(ex,ey,er,0,7); ctx.fill();
    if(det>0.4){ ctx.fillStyle='rgba(255,255,255,0.9)'; ctx.beginPath(); ctx.arc(ex+er*0.3,ey-er*0.3,er*0.3,0,7); ctx.fill(); }
    if(det>0.5 && S.whiskers){ ctx.strokeStyle=hexa('#efe7d6',0.5); ctx.lineWidth=0.6; ctx.lineCap='round';
      for(let i=0;i<4;i++){ const a=-0.2+i*0.15; ctx.beginPath(); ctx.moveTo(hx+hr*0.95,hy+hr*0.38);
        ctx.quadraticCurveTo(hx+hr*1.8,hy+hr*0.4+a*hr*3, hx+hr*2.4,hy+hr*0.5+a*hr*5); ctx.stroke(); } }
    // FOREPAWS: two short forelegs hanging DOWN the chest to a pair of paws clasped together at the belly
    // (the otter's signature "hands-together" periscope look), a touch forward of the body centreline
    const paw=[P_sh[0]+H*0.16, gy-L*0.34];
    ctx.strokeStyle=S.col[2]; ctx.lineWidth=Math.max(1.8,H*0.11); ctx.lineCap='round';
    ctx.beginPath(); ctx.moveTo(P_sh[0]-H*0.02,gy-L*0.56); ctx.quadraticCurveTo(P_sh[0]+H*0.06,gy-L*0.44, paw[0]-H*0.1,paw[1]); ctx.stroke();  // far foreleg
    ctx.strokeStyle=S.col[1]; ctx.lineWidth=Math.max(2,H*0.14);
    ctx.beginPath(); ctx.moveTo(P_sh[0]+H*0.16,gy-L*0.55); ctx.quadraticCurveTo(P_sh[0]+H*0.2,gy-L*0.43, paw[0]+H*0.04,paw[1]); ctx.stroke();  // near foreleg
    ctx.fillStyle=S.col[2]; ctx.beginPath(); ctx.arc(paw[0]-H*0.1,paw[1],H*0.12,0,7); ctx.fill();   // the two clasped paws
    ctx.fillStyle=S.col[1]; ctx.beginPath(); ctx.arc(paw[0]+H*0.06,paw[1],H*0.13,0,7); ctx.fill();
  }
  function drawCreature(ctx, S, sp, x, y, dir, phase, scale, pose){
    pose = pose || {};                                       // {neck,ear,stand(0..1),...} posture from behaviour
    ctx.save(); ctx.translate(x,y); ctx.scale(dir*scale, scale);
    const det = clamp(pose.lod!=null ? pose.lod : (scale-0.26)/0.52, 0, 1);   // level of detail: 0 far silhouette .. 1 full model
    const L=S.L, H=S.H, leg=S.leg, l1=leg*S.thigh, l2=leg-l1, gy=H*0.5+leg;   // ground line (feet)
    const rest=clamp(pose.rest||0,0,1);                       // 0 standing .. 1 lying down / bedded (asleep at night) -- body sinks, legs fold, head lowers
    if((pose.curl||0)>0.5 && S.spines){ drawCurledBall(ctx, S, gy, pose); ctx.restore(); return; }   // fully curled -> the spiny ball instead of the standing form
    if((pose.siteat||0)>0.5 && (S.archTail||S.sitNibble)){ drawSquirrelSit(ctx, S, sp, gy, det, pose); ctx.restore(); return; }   // sitting up to eat -> the upright sit-nibble form (squirrel plume / mouse naked tail)
    if((pose.siteat||0)>0.5 && S.periscope){ drawUprightPeer(ctx, S, sp, gy, det, pose); ctx.restore(); return; }  // rearing up to peer -> the shared upright form (otter periscope / meerkat sentinel)
    // hips along the underside of the body
    const bh=H*0.30, backHip=[-L*0.40, bh], frontHip=[L*0.42, bh];   // hips carried UP in the haunch/shoulder (not at the belly's bottom edge), so the legs pivot high on the body
    const gt = pose.gait || S.gait, ph=phase;                // a behaviour can swap the gait (e.g. bolt -> gallop)
    // per-leg phase offsets: walk is a diagonal 4-beat; hop/gallop bring the pairs together with a suspension
    const O = gt.gallop ? {bf:0.08, ff:0.46, bn:0.0, fn:0.40}   // hinds land together, then fores, then all airborne
            : gt.trot   ? {bf:0.0,  ff:0.5,  bn:0.5, fn:0.0}    // TROT: diagonal pairs (fore-near+hind-far, then fore-far+hind-near) move together, a 2-beat with a light float
            : gt.hop    ? {bf:0.5,  ff:0.5,  bn:0.0, fn:0.0}
            :             {bf:0.25, ff:0.75, bn:0.0, fn:0.5};
    // soft contact shadow, grounds the animal
    ctx.save(); ctx.translate(0, gy+2); const shR=L*(0.55+rest*0.32); ctx.scale(1, 5.2/shR);   // a SOFT contact shadow (radial fade); WIDER when the animal lies down (more of it on the ground)
    const shG=ctx.createRadialGradient(0,0,0, 0,0,shR);
    shG.addColorStop(0,'rgba(0,0,0,0.28)'); shG.addColorStop(0.55,'rgba(0,0,0,0.13)'); shG.addColorStop(1,'rgba(0,0,0,0)');
    ctx.fillStyle=shG; ctx.beginPath(); ctx.arc(0,0,shR,0,7); ctx.fill(); ctx.restore();
    if(rest>0) ctx.translate(0, rest*leg*0.86);              // the body SINKS toward the ground as it lies down (the shadow above stays put)
    // leg render: a real 2-segment limb, tapered, knee bending the anatomically-natural way
    // (front leg 'elbow' folds toward the REAR, hind 'stifle' toward the FRONT), near pair bright, far pair dim
    function limb(a,b,wa,wb){                                  // a filled TAPERED segment (real leg, not a stick)
      const dx=b[0]-a[0],dy=b[1]-a[1],len=Math.hypot(dx,dy)||1,nx=-dy/len,ny=dx/len;
      ctx.beginPath(); ctx.moveTo(a[0]+nx*wa,a[1]+ny*wa); ctx.lineTo(b[0]+nx*wb,b[1]+ny*wb);
      ctx.lineTo(b[0]-nx*wb,b[1]-ny*wb); ctx.lineTo(a[0]-nx*wa,a[1]-ny*wa); ctx.closePath(); ctx.fill();
    }
    function drawLeg(hip, off, far, front){
      const st=pose.stand||0;                                 // 0 walking .. 1 planted stance
      const [fx,fy]=foot(ph, off, gt.stride, gt.lift, gt.duty);
      const gaitX=hip[0]+fx, gaitY=gy+fy;
      const standX=hip[0]+(front?leg*0.06:-leg*0.06), standY=gy;   // planted: front feet a touch fwd, hind back
      let footX=gaitX+(standX-gaitX)*st, footY=gaitY+(standY-gaitY)*st;
      if(rest>0){ const rfx=hip[0]+(front?leg*0.12:-leg*0.12), rfy=hip[1]+leg*0.15;   // RESTING: the leg folds UP under the body, foot tucked near the hip -- a couched limb, not an extended one
        footX=footX+(rfx-footX)*rest; footY=footY+(rfy-footY)*rest; }
      const w = far?0.82:1;                                    // far legs a touch thinner
      ctx.fillStyle = far ? S.col[2] : S.col[1];
      const sock = (S.sock && !far) ? S.sock : null;           // dark "stockings" on the lower leg (fox)
      const drop=footY-hip[1];                                 // hip-to-foot span (larger now that the hip sits high)
      if(S.columnar){
        // GRAVIPORTAL pillar leg: an elephant carries its mass on near-straight columns, not a sprung zigzag.
        // A thick, barely-tapering post from the high hip to a broad foot, with only a soft knee break -- the
        // single most iconic elephant silhouette feature. Both fore and hind read as pillars.
        const knee=[hip[0]+(front?leg*0.03:-leg*0.03), hip[1]+drop*0.50];
        const ankle=[footX+(front?-leg*0.015:leg*0.015), hip[1]+drop*0.85];
        limb(hip, knee, 8.4*w, 7.2*w);                        // upper column (heavy, near-vertical)
        limb(knee, ankle, 7.2*w, 6.1*w);                      // lower column
        limb(ankle,[footX,footY], 6.1*w, 5.4*w);              // down to the broad foot -- still thick
        ctx.beginPath(); ctx.arc(knee[0],knee[1],6.8*w,0,7); ctx.fill();    // soft joint rounding
        ctx.beginPath(); ctx.arc(ankle[0],ankle[1],5.7*w,0,7); ctx.fill();
      } else if(front){
        // FRONT leg: humerus down-and-BACK to a high elbow, then forearm + cannon fairly STRAIGHT down to the hoof
        //   -- a columnar front leg (the wrist barely breaks the line), clearly DIFFERENT from the hind's zigzag.
        const elbow=[hip[0]-leg*0.10, hip[1]+drop*0.34], carpus=[footX+leg*0.02, hip[1]+drop*0.72];
        limb(hip, elbow, 4.8*w, 3.1*w);                       // humerus
        limb(elbow, carpus, 3.1*w, 1.9*w);                    // forearm (radius/ulna)
        if(sock) ctx.fillStyle=sock; limb(carpus, [footX,footY], 1.9*w, 1.3*w);   // cannon (metacarpus), socked
        ctx.beginPath(); ctx.arc(elbow[0],elbow[1],2.4*w,0,7); ctx.fill();
        ctx.beginPath(); ctx.arc(carpus[0],carpus[1],1.6*w,0,7); ctx.fill();
      } else {
        // HIND leg: femur down-and-FORWARD to a HIGH stifle (up near the belly), gaskin down-and-BACK to the hock
        //   (carried well up the leg), then the cannon ~vertical to the hoof -- the muscled quadruped zigzag.
        const stifle=[hip[0]+leg*0.15, hip[1]+drop*0.25], hock=[footX-leg*0.02, hip[1]+drop*0.46];   // stifle HIGH near the belly, hock in the UPPER-mid of the leg -- both joints carried high, a long slender cannon below (as the operator asked)
        limb(hip, stifle, 5.3*w, 3.4*w);                      // femur, heavily muscled thigh
        limb(stifle, hock, 3.4*w, 2.0*w);                     // gaskin (tibia)
        if(sock) ctx.fillStyle=sock; limb(hock, [footX,footY], 2.0*w, 1.3*w);   // cannon (metatarsus), socked
        ctx.beginPath(); ctx.arc(stifle[0],stifle[1],2.7*w,0,7); ctx.fill();
        ctx.beginPath(); ctx.arc(hock[0],hock[1],1.9*w,0,7); ctx.fill();
      }
      // FOOT keyed to the animal: a cloven HOOF (deer/boar), a padded PAW (canid/feline/hare), or the
      // bear's long flat clawed foot. The fine shape resolves only up close; a far silhouette is a dark blob.
      const ftype = S.columnar ? 'column' : S.hoof ? 'hoof' : S.earRound ? 'flat' : 'paw';
      ctx.fillStyle = S.col[2];
      if(det < 0.5){
        ctx.beginPath(); ctx.ellipse(footX,footY,2.6*w,2.0*w,0,0,7); ctx.fill();                 // far: a simple dark foot
      } else if(ftype==='hoof'){                                                                  // cloven: two dark toes, front one lower + forward
        ctx.beginPath(); ctx.ellipse(footX+1.0*w, footY+0.3, 1.7*w, 2.3*w, 0.14, 0,7); ctx.fill();
        ctx.beginPath(); ctx.ellipse(footX-1.1*w, footY-0.1, 1.5*w, 2.0*w, -0.06, 0,7); ctx.fill();
      } else if(ftype==='flat'){                                                                  // bear: a long plantigrade foot + 3 claws at the front
        ctx.beginPath(); ctx.ellipse(footX+1.0*w, footY-0.3, 4.0*w, 2.0*w, 0, 0,7); ctx.fill();
        ctx.lineWidth=0.9*w; ctx.strokeStyle=S.col[2]; ctx.lineCap='round';
        for(let k=-1;k<=1;k++){ const cx2=footX+4.0*w+k*1.2*w; ctx.beginPath(); ctx.moveTo(cx2,footY-0.8); ctx.lineTo(cx2+0.9*w,footY+1.4); ctx.stroke(); }
      } else if(ftype==='column'){                                                                // elephant: a broad rounded foot pad, wider than tall, with pale toenail crescents at the front
        ctx.beginPath(); ctx.ellipse(footX, footY-1.4*w, 6.0*w, 3.2*w, 0, 0,7); ctx.fill();
        if(det>0.5){ ctx.fillStyle='#d8ccb6';
          for(let k=0;k<3;k++){ ctx.beginPath(); ctx.ellipse(footX+(1.8+k*1.9)*w, footY-1.6*w, 1.0*w, 1.5*w, 0, 0,7); ctx.fill(); } }
      } else {                                                                                    // paw: a rounded pad with 3 toe bumps at the front
        ctx.beginPath(); ctx.ellipse(footX, footY, 2.7*w, 1.9*w, 0, 0,7); ctx.fill();
        for(let k=0;k<3;k++){ ctx.beginPath(); ctx.arc(footX+(0.8+k*0.95)*w, footY-0.2, 1.0*w, 0,7); ctx.fill(); }
        // CLAWS: a small dark keratin hook at the front of each toe -- the dot->model detail on the foot (LOD-gated).
        // A canid's are longer + non-retractable; a cat's shorter (semi-sheathed). Bear already has its own (flat foot).
        if(det>0.55 && (S.canid||S.feline)){ ctx.fillStyle='#231710'; const clen=(S.feline?1.0:1.7)*w;
          for(let k=0;k<3;k++){ const tx=footX+(0.9+k*0.95)*w, ty=footY+0.9*w;
            ctx.beginPath(); ctx.moveTo(tx-0.45*w, ty-0.3*w); ctx.quadraticCurveTo(tx+0.7*w, ty+clen*0.3, tx+0.15*w, ty+clen);
            ctx.quadraticCurveTo(tx-0.2*w, ty+clen*0.4, tx-0.45*w, ty-0.3*w); ctx.closePath(); ctx.fill(); } }
      }
    }
    // FAR legs first (behind the body), offset from their near partners
    drawLeg(backHip, O.bf, true, false); drawLeg(frontHip, O.ff, true, true);
    // BODY: a smooth outline from rear haunch, over the back (with a slight shoulder hump), to the neck base
    const bob = (1-(pose.stand||0)) * (1-rest) * Math.sin(ph*2*Math.PI* ((gt.hop||gt.gallop)?1:2))* (gt.gallop?9:gt.hop?6:gt.trot?4:2)   // bob with the gait; a big loft at the gallop, a light float at the trot, stills when planted or bedded
      + (pose.dig||0)*Math.sin((pose.clock||0)*16)*H*0.05;   // a digging badger HEAVES -- fast forequarter effort even while planted
    ctx.translate(0,bob);
    ctx.beginPath();
    const topY=-H*0.5, botY=H*0.5;
    // back-line heights: a foreHump beast (bison) slopes from a LOW rear haunch up to a TOWERING shoulder; else a
    // uniform hump. Shared by the fill + fur-clip + fur-root beziers so the outline, its shading and its fur stay in step.
    const bkH=topY*(1+(S.foreHump?S.hump*0.28:S.hump)), bkM=topY*(1+(S.foreHump?S.hump*0.98:S.hump*0.6)),
          bk0=topY*(1+(S.foreHump?S.hump*1.55:S.hump)), bkS=topY*(1+(S.foreHump?S.hump*1.82:S.hump*1.2));
    ctx.moveTo(-L*0.5, botY*0.4);
    ctx.bezierCurveTo(-L*0.58, topY*0.6, -L*0.42, bkH, -L*0.2, bkM);   // haunch up to back
    ctx.bezierCurveTo(0, bk0, L*0.34, bkS, L*0.5, topY*0.7);           // shoulder hump to neck base
    ctx.bezierCurveTo(L*0.56, topY*0.2, L*0.55, botY*0.5, L*0.44, botY);                          // down the chest
    ctx.bezierCurveTo(L*0.15, botY*1.05, -L*0.2, botY*1.05, -L*0.42, botY*0.9);                    // belly
    ctx.bezierCurveTo(-L*0.52, botY*0.7, -L*0.54, botY*0.6, -L*0.5, botY*0.4);                     // back to haunch
    ctx.closePath();
    const grd=ctx.createLinearGradient(0,topY*1.2,0,botY*1.1);
    grd.addColorStop(0,S.col[0]); grd.addColorStop(0.6,S.col[0]); grd.addColorStop(1,S.belly);
    ctx.fillStyle=grd; ctx.fill();
    ctx.save(); ctx.clip();                                        // form-shading + fur, all clipped to the body
    if(det>0.28){                                                  // fine form-shading resolves in only as you push in
      const hi=ctx.createLinearGradient(0,topY*1.4,0,topY*0.1);    // the light source down the back (warm sun / cool moon)
      hi.addColorStop(0,'rgba('+LIGHT.rim+',0.34)'); hi.addColorStop(1,'rgba('+LIGHT.rim+',0)');
      ctx.fillStyle=hi; ctx.fillRect(-L,topY*1.6,L*2,H*1.4);
      const dl=ctx.createLinearGradient(0,topY*1.6,0,topY*0.2);    // COUNTERSHADING: darker along the spine (animals are dark on top)
      dl.addColorStop(0, S.dark||'rgba(40,26,14,0.34)'); dl.addColorStop(1,'rgba(40,26,14,0)');
      ctx.fillStyle=dl; ctx.fillRect(-L,topY*1.6,L*2,H*0.75);
      const sh=ctx.createLinearGradient(0,botY*0.1,0,botY*1.2);    // belly shadow
      sh.addColorStop(0,'rgba(20,12,6,0)'); sh.addColorStop(1,'rgba(20,12,6,0.24)');
      ctx.fillStyle=sh; ctx.fillRect(-L,botY*0.1,L*2,H*1.4);
      // DIRECTIONAL low-sun light: the sunward (world-left) flank warmly lit, the far flank cooler -- gives the body form
      const sunL=-dir, wg=ctx.createLinearGradient(sunL*L,0, -sunL*L,0);
      wg.addColorStop(0,'rgba('+LIGHT.dir+',0.16)'); wg.addColorStop(0.48,'rgba('+LIGHT.dir+',0)');
      wg.addColorStop(0.54,'rgba(28,32,44,0)'); wg.addColorStop(1,'rgba(28,32,44,0.15)');
      ctx.fillStyle=wg; ctx.fillRect(-L,topY*1.6,L*2,H*1.7);
    }
    // COAT MARKINGS: soft species patches (pale rump, dark saddle, agouti dorsal), under the fur
    if(det>0.42 && S.patches) for(const p of S.patches){
      const px=p.x*L, py=p.y*H, pr=Math.max(p.rx*L, p.ry*H)||1;
      const rg=ctx.createRadialGradient(px,py,0, px,py,pr);
      rg.addColorStop(0,hexa(p.c,p.a)); rg.addColorStop(0.62,hexa(p.c,p.a*0.5)); rg.addColorStop(1,hexa(p.c,0));
      ctx.fillStyle=rg; ctx.save(); ctx.translate(px,py); ctx.scale(p.rx*L/pr, p.ry*H/pr);
      ctx.beginPath(); ctx.arc(0,0,pr,0,7); ctx.fill(); ctx.restore();
    }
    // FAWN DAPPLE: rows of soft pale spots along the upper flank (juvenile coat), under the fur
    if(det>0.4 && S.spots){ const S2=S.spots, fr2=mulberry(sp.length*131+17); ctx.fillStyle=hexa(S2.c, S2.a);
      for(let r=0;r<S2.rows;r++){ const ry=topY*(0.82-r*0.42);
        for(let cc=0;cc<S2.cols;cc++){ const rx=-L*0.32 + cc*(L*0.64/(S2.cols-1)) + (r&1?L*0.05:0) + (fr2()-0.5)*3.5;
          ctx.beginPath(); ctx.ellipse(rx, ry+(fr2()-0.5)*3, S2.r, S2.r*0.78, 0,0,7); ctx.fill(); } } }
    // PIGLET STRIPES: pale longitudinal bands running head-to-tail along the flank (the juvenile wild-boar coat), under the fur
    if(det>0.36 && S.stripes){ const S5=S.stripes; ctx.fillStyle=hexa(S5.c, S5.a);
      for(let r=0;r<S5.n;r++){ const sy=topY*0.5 + r*(H*0.2);
        ctx.beginPath(); ctx.ellipse(-L*0.03, sy, L*0.42, Math.max(1.0,H*0.05), 0, 0,7); ctx.fill(); } }
    // FUR: short directional strokes flowing back-and-down over the coat, light + dark for real texture (stable seed)
    // per-species coat character: F.n density, F.len length, F.w stroke weight (a wolf shaggy+long, a deer sleek+short, a boar coarse)
    const F=S.fur||{n:130,len:5,w:0.8};
    const fr=mulberry(sp.length*911 + sp.charCodeAt(0)*77 + sp.charCodeAt(sp.length-1)*29 + 5);
    ctx.lineCap='round';
    for(let i=0,nFur=Math.round((F.n||130)*det*det);i<nFur;i++){  // fur thins with distance, gone at far zoom
      const fx=-L*0.56+fr()*L*1.14, fy=topY*1.25+fr()*(H*1.55), len=F.len*0.5+fr()*F.len, down=(fy>0?0.4:-0.1);
      ctx.strokeStyle = fr()<0.5 ? 'rgba(255,250,238,'+(0.04+fr()*0.06).toFixed(3)+')' : 'rgba(26,17,8,'+(0.05+fr()*0.07).toFixed(3)+')';
      ctx.lineWidth=F.w||0.8; ctx.beginPath(); ctx.moveTo(fx,fy); ctx.lineTo(fx-len*0.9, fy+down*len+(fr()-0.5)*1.4); ctx.stroke();
    }
    // RIM LIGHT: skylight catches the very topline of the coat -- a thin bright edge, still clipped to the body so it
    // HUGS the back contour and lifts the animal off itself (the crisp form cue the soft countershading can't give).
    // A horizontal gradient fades it at the haunch and neck so it reads as light grazing the back, not a drawn outline.
    if(det>0.5){ ctx.beginPath();
      ctx.moveTo(-L*0.5, botY*0.4);
      ctx.bezierCurveTo(-L*0.58, topY*0.6, -L*0.42, bkH, -L*0.2, bkM);   // haunch up over the back
      ctx.bezierCurveTo(0, bk0, L*0.34, bkS, L*0.5, topY*0.7);           // shoulder hump to the neck base
      const rg=ctx.createLinearGradient(-L*0.5,0, L*0.5,0);
      rg.addColorStop(0,'rgba(255,249,228,0)'); rg.addColorStop(0.26,'rgba(255,249,228,0.55)'); rg.addColorStop(0.72,'rgba(255,249,228,0.5)'); rg.addColorStop(1,'rgba(255,249,228,0.07)');
      ctx.strokeStyle=rg; ctx.lineWidth=Math.max(1.0, H*0.045); ctx.lineJoin='round'; ctx.lineCap='round'; ctx.stroke();
    }
    ctx.restore();
    // RAISED HACKLES (piloerection): a threatened animal's dorsal coat stands UP in a spiky ridge along the spine -- a
    // nervous quivering row of hairs rooted on the back topline, pointing up-and-back. Drawn UNCLIPPED so they break the
    // body outline. Driven by pose.bristle (0..1). A general capability; the badger raises them when a predator is near.
    if((pose.bristle||0)>0.05 && !S.spines){ const br=pose.bristle, ck=pose.clock||0;   // a SPINED creature (hedgehog) bristles its SPINES instead of hackles
      const bz=(P,t)=>{ const u=1-t,a=u*u*u,b=3*u*u*t,c=3*u*t*t,d=t*t*t; return [a*P[0][0]+b*P[1][0]+c*P[2][0]+d*P[3][0], a*P[0][1]+b*P[1][1]+c*P[2][1]+d*P[3][1]]; };
      const A=[[-L*0.5,botY*0.4],[-L*0.58,topY*0.6],[-L*0.42,bkH],[-L*0.2,bkM]];
      const B=[[-L*0.2,bkM],[0,bk0],[L*0.34,bkS],[L*0.5,topY*0.7]];
      const roots=[]; for(let i=2;i<=6;i++) roots.push(bz(A,i/7)); for(let i=0;i<=7;i++) roots.push(bz(B,i/7));
      ctx.strokeStyle=hexa(S.col[2],0.92); ctx.lineWidth=Math.max(0.7,H*0.05); ctx.lineCap='round';
      for(let i=0;i<roots.length;i++){ const [rx,ry]=roots[i], len=H*(0.30+0.18*((i*5)%3)/2)*br, sway=Math.sin(ck*12+i*1.4)*len*0.16*br;
        ctx.beginPath(); ctx.moveTo(rx,ry); ctx.quadraticCurveTo(rx-len*0.16+sway*0.5, ry-len*0.55, rx-len*0.30+sway, ry-len); ctx.stroke(); } }
    // SPINE COAT (hedgehog): the whole back dome is overlapping pale-banded QUILLS -- lie back when calm, stand ERECT when
    // threatened (the same 'bristle' piloerection channel). Dark base + pale tip. Drawn UNCLIPPED so the dome bristles.
    if(S.spines){ const sp=S.spines, br=pose.bristle||0, ck=pose.clock||0, hh=(i,o)=>{ const s=Math.sin((i+1)*12.9898+o*78.233)*43758.5; return s-Math.floor(s); };
      ctx.lineCap='round';
      for(let i=0;i<sp.n;i++){ const u=hh(i,0), v=hh(i,1), w2=hh(i,2);
        const rx=-L*0.47 + u*L*0.88, ry=topY*(1.15 - v*v*1.35);                                 // roots over the whole upper dome; v*v biases them toward the high back (spines thickest there, thinning down the sides)
        const len=sp.len*(0.7+0.5*w2), lay=-0.78 + br*0.64 + (w2-0.5)*0.5, ang=lay + Math.sin(ck*11+i)*0.05*br;   // calm: leaned back; threatened: nearer straight up; +spread + a quiver
        const mx=rx+Math.sin(ang)*len*0.5, my=ry-Math.cos(ang)*len*0.5, tx=rx+Math.sin(ang)*len, ty=ry-Math.cos(ang)*len;
        ctx.strokeStyle=hexa(S.col[2],0.92); ctx.lineWidth=Math.max(0.6,H*0.055); ctx.beginPath(); ctx.moveTo(rx,ry); ctx.lineTo(mx,my); ctx.stroke();   // dark base
        ctx.strokeStyle=sp.band||'#e8dcc2'; ctx.lineWidth=Math.max(0.5,H*0.04); ctx.beginPath(); ctx.moveTo(mx,my); ctx.lineTo(tx,ty); ctx.stroke(); } }   // pale tip
    // BRISTLY MANE (boar): a dorsal crest of stiff bristles rising from the back line, tallest over the shoulder hump
    if(det>0.3 && S.mane){ const fm=mulberry(sp.length*53+9); ctx.strokeStyle=hexa(S.col[2],0.92); ctx.lineCap='round';
      for(let i=0;i<20;i++){ const t=i/19, x=-L*0.42+t*L*0.86;
        const backY = S.foreHump ? topY*(1 + S.hump*(0.28 + 1.54*t))                       // bison: ride the SLOPED back, low haunch -> towering shoulder
                                 : topY*(1 + S.hump*(0.35+0.65*Math.sin(Math.min(1,t*1.05)*Math.PI)));   // ride the (uniform-hump) back line
        const h=(3+9*Math.sin(Math.min(1,t*0.95)*Math.PI))*(0.7+fm()*0.6);                 // tallest over the shoulder
        ctx.lineWidth=1+fm()*0.8; ctx.beginPath(); ctx.moveTo(x,backY); ctx.lineTo(x+2.4-fm()*1.4, backY-h); ctx.stroke(); }
    }
    // TAIL -- a thin flick for deer/hare, or a fat furry brush for a wolf (S.bushy).
    // A bolting deer FLAGS its white tail straight up over the rump (the flash of white as it flees).
    const flag = (gt.gallop && S.tailTip && !S.bushy) ? 1 : 0;
    const tbx=-L*0.5, tby=-H*0.15;
    let ttx=(-L*0.5-S.tail)*(1-flag) + (tbx+S.tail*0.12 + Math.sin(ph*6.283)*S.tail*0.16)*flag;   // a bolting flag flicks side to side with the stride
    let tty=(-H*0.15+S.tailAng*S.tail*2+(gt.hop?-4:0))*(1-flag) + (tby-S.tail*1.15)*flag;
    let tcx=(-L*0.5-S.tail*0.7)*(1-flag) + (tbx+S.tail*0.02)*flag;
    let tcy=(-H*0.15+S.tailAng*S.tail)*(1-flag) + (tby-S.tail*0.6)*flag;
    if(S.archTail){   // a SQUIRREL's plume: sweep UP off the rump and curl forward OVER the back (a big question-mark), with a live quiver
      const qv=Math.sin((pose.clock||ph)*3.1)*S.tail*0.06;
      tcx=-L*0.5-S.tail*0.46; tcy=-H*0.15-S.tail*0.66;                 // bows out behind the rump and rises
      ttx=-L*0.30+qv;          tty=-H*0.15-S.tail*1.32;                // tip carried high and forward, arched over the body
    }
    if(S.bushy){
      const tp=[]; for(let i=0;i<=8;i++){ const t=i/8, mt=1-t;                       // sample the tail curve
        const w = S.archTail ? S.bushy*(0.5+0.78*Math.sin(t*Math.PI))*(1.12-0.42*t)  // squirrel PLUME: fat mid, tapered to a fluffy tip
                             : S.bushy*(0.55+0.9*Math.sin(Math.min(1,t*1.15)*Math.PI*0.7)); // brush: fat through the middle
        tp.push([mt*mt*tbx+2*mt*t*tcx+t*t*ttx, mt*mt*tby+2*mt*t*tcy+t*t*tty, w]); }
      const pp=(i)=>{ const a=tp[Math.max(0,i-1)],b=tp[Math.min(tp.length-1,i+1)],dx=b[0]-a[0],dy=b[1]-a[1],dl=Math.hypot(dx,dy)||1; return [-dy/dl,dx/dl]; };
      ctx.fillStyle=S.col[1]; ctx.beginPath();
      for(let i=0;i<tp.length;i++){ const [px,py]=pp(i),w=tp[i][2],X=tp[i][0]+px*w,Y=tp[i][1]+py*w; i?ctx.lineTo(X,Y):ctx.moveTo(X,Y); }
      for(let i=tp.length-1;i>=0;i--){ const [px,py]=pp(i),w=tp[i][2]; ctx.lineTo(tp[i][0]-px*w,tp[i][1]-py*w); }
      ctx.closePath(); ctx.fill();
      if(S.tailTip){ ctx.fillStyle=S.tailTip; ctx.beginPath(); ctx.arc(ttx,tty,S.bushy*0.85,0,7); ctx.fill(); } // dark brush tip
      ctx.strokeStyle=hexa(S.col[2],0.4); ctx.lineWidth=0.7; ctx.lineCap='round';  // a few fur strands along the brush
      const tf=mulberry(99); for(let i=0;i<10;i++){ const t=0.2+tf()*0.75, mt=1-t;
        const bx=mt*mt*tbx+2*mt*t*tcx+t*t*ttx, by=mt*mt*tby+2*mt*t*tcy+t*t*tty, [px,py]=pp(Math.round(t*8));
        ctx.beginPath(); ctx.moveTo(bx,by); ctx.lineTo(bx-px*S.bushy*(0.6+tf()*0.7), by-py*S.bushy*(0.6+tf()*0.7)); ctx.stroke(); }
    } else {
      // a non-bushy tail with REAL form: sample the base->tip curve once, fill it as a tapered ribbon
      // (not a stick + ball). Its shape tells the species: a raised white FAN when a deer flags in flight,
      // a coat-coloured flat tail with a pale under-margin at rest, a fluffy white PUFF for a cottontail.
      const NT=8, tsm=[]; for(let i=0;i<=NT;i++){ const t=i/NT, mt=1-t;
        tsm.push([mt*mt*tbx+2*mt*t*tcx+t*t*ttx, mt*mt*tby+2*mt*t*tcy+t*t*tty, t]); }
      const tno=(i)=>{ const a=tsm[Math.max(0,i-1)],b=tsm[Math.min(NT,i+1)],dx=b[0]-a[0],dy=b[1]-a[1],dl=Math.hypot(dx,dy)||1; return [-dy/dl,dx/dl]; };
      const tribbon=(wfn,fill)=>{ ctx.fillStyle=fill; ctx.beginPath();
        for(let i=0;i<=NT;i++){ const [px,py]=tno(i),w=wfn(tsm[i][2]),X=tsm[i][0]+px*w,Y=tsm[i][1]+py*w; i?ctx.lineTo(X,Y):ctx.moveTo(X,Y); }
        for(let i=NT;i>=0;i--){ const [px,py]=tno(i),w=wfn(tsm[i][2]); ctx.lineTo(tsm[i][0]-px*w,tsm[i][1]-py*w); }
        ctx.closePath(); ctx.fill(); };
      if(flag){                                                                    // FLEE FLAG: a broad white fan raised over the rump, widest near the tip (the underside flashing)
        tribbon(t=> (S.tail>12?3.5:2.6)*(0.4+1.4*t), S.tailTip);
        ctx.strokeStyle=hexa(S.col[2],0.3); ctx.lineWidth=0.8; ctx.beginPath(); ctx.moveTo(tbx,tby); ctx.quadraticCurveTo(tcx,tcy,ttx,tty); ctx.stroke(); // faint coat edge so it doesn't float
      } else if(S.puffTail){                                                        // COTTONTAIL: a round fluffy white puff (a small cluster of soft tufts), not one hard ball
        const pf=mulberry(sp.length*17+3); const pr=Math.max(3,S.tail*0.7);
        ctx.fillStyle=hexa(S.col[2],0.5); ctx.beginPath(); ctx.arc(ttx,tty+0.6,pr*1.05,0,7); ctx.fill();   // a soft shadow under the puff
        ctx.fillStyle=S.tailTip||'#f4efe6'; for(let i=0;i<7;i++){ const a=pf()*6.283; ctx.beginPath(); ctx.arc(ttx+Math.cos(a)*pr*0.5, tty+Math.sin(a)*pr*0.5, pr*0.62, 0,7); ctx.fill(); }
      } else if(S.nakedTail){                                                        // a MOUSE/RAT tail: a thin BARE stroke, near-uniform + pinkish-grey (a new reusable capability, cf S.tailW)
        ctx.strokeStyle=S.tailNaked||S.belly; ctx.lineCap='round';
        for(let i=0;i<10;i++){ const t0=i/10, t1=(i+1)/10, m0=1-t0, m1=1-t1;
          const x0=m0*m0*tbx+2*m0*t0*tcx+t0*t0*ttx, y0=m0*m0*tby+2*m0*t0*tcy+t0*t0*tty;
          const x1=m1*m1*tbx+2*m1*t1*tcx+t1*t1*ttx, y1=m1*m1*tby+2*m1*t1*tcy+t1*t1*tty;
          ctx.lineWidth=(S.tail>12?2.0:1.4)*(1-0.5*t0);
          ctx.beginPath(); ctx.moveTo(x0,y0); ctx.lineTo(x1,y1); ctx.stroke(); }
      } else {                                                                       // a flat tapered tail hanging at rest, coat-coloured
        tribbon(t=> (S.tail>12?4:2.6)*(1-0.68*t)*(S.tailW||1), S.col[1]);            // S.tailW fattens the base (a thick muscular otter tail)
        if(S.flagTail){ ctx.strokeStyle=hexa(S.tailTip,0.7); ctx.lineWidth=1.2; ctx.lineCap='round';   // white underside margin, visible even at rest
          ctx.beginPath(); ctx.moveTo(tbx-0.5,tby+1.4); ctx.quadraticCurveTo(tcx,tcy+1.6, ttx,tty+0.8); ctx.stroke(); }
        else if(S.tailTip){ ctx.fillStyle=S.tailTip; ctx.beginPath(); ctx.arc(ttx,tty,2.1,0,7); ctx.fill(); }  // a small tuft tip (boar etc.)
      }
    }
    // NECK + HEAD -- posture-driven: neckAng swings the head UP (alert) or DOWN (graze)
    const neckAng = S.neckAng + (pose.neck||0);
    const nb=[L*0.46, topY*0.5];                            // neck base
    const hd=[nb[0]+Math.cos(neckAng)*S.neck, nb[1]+Math.sin(neckAng)*S.neck];
    { // a tapered, gently curved neck (throat-line bowed) sampled + offset-filled, thick at the shoulder to thin at the skull
      const mcx=(nb[0]+hd[0])/2, mcy=(nb[1]+hd[1])/2 - S.neck*0.16;
      const nw0=H*0.30, nw1=Math.max(3,H*0.15), np=[];
      for(let i=0;i<=8;i++){ const t=i/8, mt=1-t;
        np.push([mt*mt*nb[0]+2*mt*t*mcx+t*t*hd[0], mt*mt*nb[1]+2*mt*t*mcy+t*t*hd[1], nw0+(nw1-nw0)*t]); }
      const npp=(i)=>{ const a=np[Math.max(0,i-1)],b=np[Math.min(np.length-1,i+1)],dx=b[0]-a[0],dy=b[1]-a[1],dl=Math.hypot(dx,dy)||1; return [-dy/dl,dx/dl]; };
      ctx.fillStyle=S.col[0]; ctx.beginPath();
      for(let i=0;i<np.length;i++){ const [px,py]=npp(i),w=np[i][2],X=np[i][0]+px*w,Y=np[i][1]+py*w; i?ctx.lineTo(X,Y):ctx.moveTo(X,Y); }
      for(let i=np.length-1;i>=0;i--){ const [px,py]=npp(i),w=np[i][2]; ctx.lineTo(np[i][0]-px*w,np[i][1]-py*w); }
      ctx.closePath(); ctx.fill(); }
    // HEAD -- a real muzzle: skull at the back, tapering to a nose, jaw beneath, two ears, an eye, a nostril
    ctx.save(); ctx.translate(hd[0],hd[1]); ctx.rotate(neckAng+0.5);
    const hr=S.head, sn=S.snout, er=(pose.ear||0) + (pose.earTurn||0)*0.6;   // er rotates the ears: pose.ear is the alertness tilt; earTurn SWIVELS them toward a threat/target (+ ahead, - behind) even while the head grazes
    const eo=(pose.earTurn||0)*hr*0.4;                        // and SHIFTS the ear base forward/back so the tip visibly SWEEPS toward the threat (rotation alone reads too subtly on a near-symmetric ear)
    // far ear first (behind the skull, dimmer) -- triangular + erect for a canid, else a rounded ellipse
    if(S.bigEar){ const flap=Math.sin((pose.clock||0)*1.7)*0.13; ctx.fillStyle=S.col[1];   // ELEPHANT: a huge fanning ear behind the head, slowly flapping
      ctx.beginPath(); ctx.moveTo(-hr*0.15,-hr*0.72);
      ctx.quadraticCurveTo(-hr*1.5-flap*hr,-hr*0.7, -hr*1.35-flap*hr*0.6, hr*0.55);
      ctx.quadraticCurveTo(-hr*0.7, hr*1.05, hr*0.05, hr*0.5);
      ctx.quadraticCurveTo(-hr*0.35,-hr*0.1, -hr*0.15,-hr*0.72); ctx.closePath(); ctx.fill();
      ctx.strokeStyle=hexa(S.col[2],0.35); ctx.lineWidth=Math.max(0.8,hr*0.05); ctx.stroke(); }
    else if(S.canid||S.feline){ ctx.save(); ctx.translate(-hr*0.5,-hr*0.6); ctx.rotate(-0.3+er); ctx.fillStyle=S.col[2];
      ctx.beginPath(); ctx.moveTo(-S.ear*0.5,S.ear*0.1); ctx.lineTo(-S.ear*0.05,-S.ear*1.45); ctx.lineTo(S.ear*0.5,S.ear*0.05); ctx.closePath(); ctx.fill(); ctx.restore(); }
    else if(S.earRound){ ctx.fillStyle=S.col[2]; ctx.beginPath(); ctx.ellipse(-hr*0.66,-hr*0.6,S.ear*0.5,S.ear*0.5,0,0,7); ctx.fill(); } // small round ear (bear)
    else { ctx.fillStyle=S.col[2]; ctx.beginPath(); ctx.ellipse(-hr*0.55,-hr*0.75,S.ear*0.3,S.ear*0.92,-0.35+er,0,7); ctx.fill(); }
    // skull + muzzle -- a canid gets a domed skull, a brow stop and a blunt nose; a cervid/lagomorph a fine taper
    ctx.fillStyle=S.headCol||S.col[0]; ctx.beginPath();   // headCol lets the head differ from the body (a badger's white face on a grey body)
    if(S.canid){
      ctx.moveTo(-hr*0.78,-hr*0.32);                              // nape / back of skull
      ctx.quadraticCurveTo(-hr*0.45,-hr*1.02, hr*0.12,-hr*0.82);  // high domed forehead
      ctx.quadraticCurveTo(sn*0.55,-hr*0.66, sn*1.14,-hr*0.46);   // a slight stop, then the muzzle top out to the nose
      ctx.quadraticCurveTo(sn*1.54,-hr*0.22, sn*1.46, hr*0.16);   // blunt nose, near-vertical front
      ctx.quadraticCurveTo(sn*1.34, hr*0.5, sn*0.84, hr*0.48);    // lip under the nose
      ctx.quadraticCurveTo(sn*0.28, hr*0.66, -hr*0.36, hr*0.6);   // lower jaw back
      ctx.quadraticCurveTo(-hr*0.92, hr*0.42, -hr*0.78,-hr*0.32); // jowl up to the skull
    } else if(S.feline){                                          // a CAT: a domed forehead, a short rounded muzzle
      ctx.moveTo(-hr*0.8,-hr*0.28);
      ctx.quadraticCurveTo(-hr*0.5,-hr*1.02, hr*0.18,-hr*0.74);   // rounded domed forehead
      ctx.quadraticCurveTo(sn*0.7,-hr*0.55, sn*1.15,-hr*0.06);    // short muzzle top out to the nose
      ctx.quadraticCurveTo(sn*1.34, hr*0.22, sn*1.12, hr*0.44);   // small rounded nose front
      ctx.quadraticCurveTo(sn*0.5, hr*0.62, -hr*0.42, hr*0.54);   // short jaw
      ctx.quadraticCurveTo(-hr*0.92, hr*0.34, -hr*0.8,-hr*0.28);  // cheek up to the forehead
    } else {
      ctx.moveTo(-hr*0.7,-hr*0.5);                                // top of skull
      ctx.quadraticCurveTo(sn*0.9,-hr*0.85, sn*1.55,-hr*0.15);    // brow down the nose bridge to the nose tip
      ctx.quadraticCurveTo(sn*1.7, hr*0.25, sn*1.35, hr*0.5);     // nose tip round under
      ctx.quadraticCurveTo(sn*0.4, hr*0.8, -hr*0.6, hr*0.6);      // jaw underside back to the throat
      ctx.quadraticCurveTo(-hr*0.95, 0, -hr*0.7,-hr*0.5);
    }
    ctx.closePath(); ctx.fill();
    // BOVID HORNS (bison/cattle): short curved horns sweeping out from the skull crown and hooking UP, with pale
    // sharp tips -- one far (offset, dimmer), one near. Distinct from S.horns, which draws a cervid ANTLER rack.
    if(S.bovidHorns){
      const horn=(sx, col, tip)=>{ ctx.strokeStyle=col; ctx.lineCap='round'; ctx.lineWidth=hr*0.30;
        ctx.beginPath(); ctx.moveTo(-hr*0.30+sx,-hr*0.42); ctx.quadraticCurveTo(-hr*0.92+sx,-hr*0.46, -hr*1.02+sx,-hr*1.02); ctx.stroke();
        ctx.strokeStyle=tip; ctx.lineWidth=hr*0.15; ctx.beginPath(); ctx.moveTo(-hr*0.99+sx,-hr*0.76); ctx.quadraticCurveTo(-hr*1.05+sx,-hr*0.98,-hr*1.02+sx,-hr*1.13); ctx.stroke(); };
      horn(hr*0.34, S.col[2], '#a89a7e'); horn(0, '#241a12', '#d8cfb6');
    }
    // BEARD (bison): a shaggy dark tuft hanging from the lower jaw + throat
    if(S.beard){ ctx.strokeStyle=hexa(S.col[2],0.9); ctx.lineWidth=Math.max(1,hr*0.10); ctx.lineCap='round';
      const bf=mulberry(sp.length*41+5);
      for(let i=0;i<10;i++){ const bx=-hr*0.55+bf()*sn*1.0, by=hr*0.46+bf()*hr*0.22;
        ctx.beginPath(); ctx.moveTo(bx,by); ctx.lineTo(bx-hr*0.08*(bf()-0.3), by+hr*(0.55+bf()*0.8)); ctx.stroke(); } }
    // FACE STRIPE (badger): a bold black band on the white face, running from the nose back THROUGH the eye up to the ear
    if(S.faceStripe){ ctx.strokeStyle=S.faceStripe; ctx.lineWidth=hr*0.5; ctx.lineCap='round'; ctx.lineJoin='round';
      ctx.beginPath(); ctx.moveTo(sn*1.46,-hr*0.04); ctx.quadraticCurveTo(sn*0.4,-hr*0.16, -hr*0.28,-hr*0.72); ctx.stroke(); }
    // a touch of shading under the jaw (fine detail)
    if(det>0.4){ ctx.fillStyle=S.col[1]; ctx.beginPath(); ctx.moveTo(-hr*0.4,hr*0.45); ctx.quadraticCurveTo(sn*0.5,hr*0.7,sn*1.2,hr*0.42);
      ctx.quadraticCurveTo(sn*0.5,hr*0.62,-hr*0.4,hr*0.6); ctx.closePath(); ctx.fill(); }
    // MOUTH: a fine lip line where the lips close, along the lower muzzle -- completes the face. Front point keyed
    // to the muzzle type (a canid's long jaw reaches further forward than a cat's short one); scales with sn/hr.
    if(det>0.45){ ctx.strokeStyle=hexa(S.col[2],0.6); ctx.lineWidth=Math.max(0.6,hr*0.075); ctx.lineCap='round';
      const mx0 = S.feline? sn*1.0 : S.canid? sn*1.32 : sn*1.34, mxb = S.canid? -hr*0.06 : sn*0.1;   // front lip corner .. back corner
      ctx.beginPath(); ctx.moveTo(mx0, hr*0.30); ctx.quadraticCurveTo(sn*0.5, hr*0.52, mxb, hr*0.42); ctx.stroke();
      if(S.canid||S.feline){ ctx.beginPath(); ctx.arc(mxb, hr*0.42, ctx.lineWidth*0.7, 0, 7); ctx.fillStyle=hexa(S.col[2],0.5); ctx.fill(); } }  // a soft mouth corner on predators
    // PANTING TONGUE (canid/feline): when hot or blown after a run the mouth opens and the tongue LOLLS out -- a pink
    // SPATULATE lobe (narrow at the mouth, widening to a rounded spade tip) drooping down-forward, a dark open-mouth
    // gap behind it, a central groove. It QUIVERS with the pant breath (pose.clock, ~2.5Hz) -- length pulses and the
    // tip sways, so the tongue is alive, not a fixed shape. Driven by pose.pant (0..1); LOD-gated. The expressive face
    // is now ears-swivel + whiskers-protract + eye + a BREATHING panting mouth.
    if((pose.pant||0)>0 && det>0.4 && (S.canid||S.feline)){
      const pp=clamp(pose.pant,0,1), qv=Math.sin((pose.clock||0)*(10.5+8*pp));                  // pant-breath quiver: FASTER when just-blown (pp high), slowing as it recovers; 0 when no clock -> static
      const bx=(S.feline?sn*0.98:sn*1.18), by=hr*0.36;
      const tlen=hr*(0.5+0.55*pp)*(1+0.10*qv*pp), twB=hr*0.24, twT=hr*0.40;                      // SHORTER than v1 (the cougar over-hung); a spade tip; length breathes
      const sway=qv*hr*0.42*pp, tipx=bx+tlen*0.30+sway, tipy=by+tlen;                            // tip droops forward + sways with each breath
      ctx.fillStyle='#3a1712'; ctx.beginPath(); ctx.ellipse(bx-hr*0.12, by+hr*0.03, hr*0.30, hr*0.21, 0.3, 0,7); ctx.fill();   // dark open mouth
      ctx.fillStyle='#d16b73';                                                                                                 // the tongue: narrow base -> wide rounded spade tip
      ctx.beginPath(); ctx.moveTo(bx-twB, by-hr*0.02);
      ctx.quadraticCurveTo(bx+twB*0.6, by+tlen*0.42, tipx+twT*0.5, tipy-twT*0.45);
      ctx.quadraticCurveTo(tipx+twT*0.62, tipy+twT*0.28, tipx, tipy+twT*0.5);
      ctx.quadraticCurveTo(tipx-twT*0.62, tipy+twT*0.28, tipx-twT*0.5, tipy-twT*0.45);
      ctx.quadraticCurveTo(bx-twB*1.5, by+tlen*0.42, bx-twB, by-hr*0.02); ctx.closePath(); ctx.fill();
      ctx.strokeStyle='#e79aa0'; ctx.lineWidth=Math.max(0.6,twB*0.4); ctx.lineCap='round';                                     // central groove down the tongue
      ctx.beginPath(); ctx.moveTo(bx, by+tlen*0.15); ctx.quadraticCurveTo((bx+tipx)/2, by+tlen*0.6, tipx, tipy-twT*0.2); ctx.stroke();
    }
    // near ear (up and forward, with an inner) -- erect triangle for a canid, else a rounded ellipse
    if(S.canid||S.feline){ ctx.save(); ctx.translate(-hr*0.16+eo,-hr*0.64); ctx.rotate(-0.12+er);
      ctx.fillStyle=S.col[1]; ctx.beginPath(); ctx.moveTo(-S.ear*0.55,S.ear*0.1); ctx.lineTo(0,-S.ear*1.6); ctx.lineTo(S.ear*0.55,S.ear*0.05); ctx.closePath(); ctx.fill();
      ctx.fillStyle=S.col[2]; ctx.beginPath(); ctx.moveTo(-S.ear*0.30,-S.ear*0.08); ctx.lineTo(0,-S.ear*1.18); ctx.lineTo(S.ear*0.30,-S.ear*0.10); ctx.closePath(); ctx.fill(); ctx.restore(); }
    else if(S.earRound){ ctx.fillStyle=S.col[1]; ctx.beginPath(); ctx.ellipse(-hr*0.34+eo,-hr*0.68,S.ear*0.55,S.ear*0.55,0,0,7); ctx.fill();  // round bear ear + inner
      ctx.fillStyle=S.col[2]; ctx.beginPath(); ctx.ellipse(-hr*0.32+eo,-hr*0.64,S.ear*0.28,S.ear*0.28,0,0,7); ctx.fill(); }
    else {
      ctx.fillStyle=S.col[1]; ctx.beginPath(); ctx.ellipse(-hr*0.25+eo,-hr*0.85,S.ear*0.34,S.ear,-0.45+er,0,7); ctx.fill();
      ctx.fillStyle=S.earInner||S.col[2]; ctx.beginPath(); ctx.ellipse(-hr*0.22+eo,-hr*0.8,S.ear*0.16,S.ear*0.6,-0.45+er,0,7); ctx.fill();  // pale inner concha for prey (earInner)
    }
    if(S.earTip){ ctx.fillStyle=S.earTip;                                  // dark ear tips (hare) -- follow the same swivel offset so they stay on the ears
      ctx.beginPath(); ctx.ellipse(-hr*0.25-S.ear*0.30+eo,-hr*0.85-S.ear*0.62,S.ear*0.26,S.ear*0.34,-0.45+er,0,7); ctx.fill();
      ctx.beginPath(); ctx.ellipse(-hr*0.55-S.ear*0.24+eo,-hr*0.75-S.ear*0.56,S.ear*0.20,S.ear*0.28,-0.35+er,0,7); ctx.fill(); }
    if(S.nose){ ctx.fillStyle=S.nose;
      if(S.snoutDisc){                                                                    // a PIG's flat rooting disc at the muzzle tip: a tall forward-facing pad with two nostrils
        ctx.beginPath(); ctx.ellipse(sn*1.5, hr*0.13, hr*0.26, hr*0.52, 0.12, 0,7); ctx.fill();
        if(det>0.45){ ctx.fillStyle='#0b0805';
          ctx.beginPath(); ctx.ellipse(sn*1.58, hr*0.0,  hr*0.06, hr*0.13, 0.1, 0,7); ctx.fill();   // upper nostril
          ctx.beginPath(); ctx.ellipse(sn*1.58, hr*0.27, hr*0.06, hr*0.13, 0.1, 0,7); ctx.fill(); } // lower nostril
      } else { ctx.beginPath(); ctx.ellipse(sn*1.46, hr*0.14, hr*0.30, hr*0.36, 0.3, 0,7); ctx.fill(); } // dark nose pad (canid)
    }
    // EYE -- a living eye: a dark liquid orb, a warm iris + pupil, a heavy coat-coloured upper LID that gives
    // expression, and a wet catchlight. Reads even small; the iris + lid resolve up close. Predators run keener (eyeCol).
    const ex=sn*0.35, ey=-hr*0.12, erad=Math.max(1.6, hr*0.24);
    ctx.fillStyle='#0c0906'; ctx.beginPath(); ctx.arc(ex,ey,erad,0,7); ctx.fill();                    // the eye
    if(det>0.4){
      ctx.fillStyle=S.eyeCol||'#3a2413'; ctx.beginPath(); ctx.arc(ex,ey,erad*0.8,0,7); ctx.fill();     // warm iris
      ctx.fillStyle='#070504'; ctx.beginPath(); ctx.arc(ex,ey,erad*0.46,0,7); ctx.fill();               // pupil
      ctx.fillStyle=hexa(S.col[2],0.92); ctx.beginPath();                                               // heavy upper lid (coat) -> expression
      ctx.ellipse(ex, ey-erad*0.92, erad*1.3, erad*0.82, 0, 0,7); ctx.fill();
      ctx.fillStyle='rgba(255,255,255,0.9)'; ctx.beginPath(); ctx.arc(ex+erad*0.32, ey-erad*0.3, erad*0.26, 0,7); ctx.fill();  // wet catchlight
      ctx.fillStyle='#0e0a07'; ctx.beginPath(); ctx.ellipse(sn*1.4,hr*0.08,1.3,1.9,0.3,0,7); ctx.fill(); }  // nostril
    // WHISKERS (vibrissae): a LIVING whisker system, not a static fan. Fine pale hairs on a mystacial PAD beside the
    // nose, each starting near-level then curving to a drooping tip (gravity on a flexible hair). They RESPOND: an
    // alert animal PROTRACTS them forward and stiffens them (less droop), a calm one lets them relax back -- driven by
    // the same ear-alertness the ears already use (pose.ear + earTurn). Plus a faint FAR-side set behind the muzzle,
    // VARIED lengths (long macro-feelers among shorter ones), and superciliary BROW whiskers above the eye. LOD-gated
    // (fine, close-up only) so a far muzzle is bare -- the dot->model rule on the face. Canid/feline/hare (S.whiskers).
    if(det>0.55 && (S.canid||S.feline||S.whiskers)){
      ctx.lineCap='round';
      const fwd = clamp((pose.ear||0)*1.3 + Math.abs(pose.earTurn||0)*0.4, 0, 1);          // 0 relaxed .. 1 protracted (alert = ears forward)
      const baseLen = sn*(S.feline?1.2:S.canid?0.95:1.05)*(1+0.12*fwd);                     // reach a touch further when forward
      const sag = 0.15*(1-0.78*fwd), lenK=[0.82,1.20,0.92,1.24];                            // stiffen forward when alert; long macro-feelers at rows 1 & 3
      const whisk=(ox,oy,a,len,lw,al)=>{ ctx.strokeStyle='rgba(240,238,230,'+al.toFixed(2)+')'; ctx.lineWidth=Math.max(0.4,lw);
        const tx=ox+len*Math.cos(a), ty=oy+len*Math.sin(a)+len*sag, cx=ox+len*0.55*Math.cos(a*0.4), cy=oy+len*0.55*Math.sin(a*0.4);
        ctx.beginPath(); ctx.moveTo(ox,oy); ctx.quadraticCurveTo(cx,cy,tx,ty); ctx.stroke(); };
      const mAng=[-0.34,-0.13,0.08,0.29];
      for(let wI=0;wI<3;wI++)                                                                // FAR-side set: dimmer, drawn first, peeking over from the other cheek
        whisk(sn*1.02-wI*sn*0.04, -hr*0.05+wI*hr*0.08, mAng[wI]-0.08-0.30*fwd, baseLen*lenK[wI]*0.8, hr*0.04, 0.13);
      for(let wI=0;wI<4;wI++)                                                                // NEAR-side mystacial fan: protracts forward + up with alertness
        whisk(sn*1.06-wI*sn*0.05, wI*hr*0.085, mAng[wI]-0.30*fwd, baseLen*lenK[wI], hr*0.055*(1-wI*0.10), 0.30+0.10*(wI%2));
      if(S.canid||S.feline)                                                                  // SUPERCILIARY (brow) whiskers, arcing forward-up over the eye: 2 fine feelers
        for(let wI=0;wI<2;wI++) whisk(sn*0.24-wI*sn*0.14, -hr*0.44-wI*hr*0.05, -0.5-0.2*wI-0.15*fwd, baseLen*0.68, hr*0.04, 0.20);
    }
    // IVORY TUSKS (elephant): a long tusk sweeping FORWARD and down from beside the trunk root, then hooking up at
    // the tip -- a tapered ivory shape (not the boar's little up-curved jaw tush). The FAR tusk is dimmer and drawn
    // BEFORE the trunk (so the trunk hangs over it); the NEAR bright tusk is drawn AFTER, in front of the trunk.
    function drawTusk(bx,by,cxp,cyp,tx,ty,w0,col){
      const N=8, pt=[];
      for(let i=0;i<=N;i++){ const u=i/N,m=1-u;
        pt.push([ m*m*bx+2*m*u*cxp+u*u*tx, m*m*by+2*m*u*cyp+u*u*ty, w0*(1-0.90*u) ]); }
      const nrm=(i)=>{ const a=pt[Math.max(0,i-1)],b=pt[Math.min(N,i+1)],dx=b[0]-a[0],dy=b[1]-a[1],dl=Math.hypot(dx,dy)||1; return [-dy/dl,dx/dl]; };
      ctx.fillStyle=col; ctx.beginPath();
      for(let i=0;i<=N;i++){ const [nx,ny]=nrm(i),w=pt[i][2]; i?ctx.lineTo(pt[i][0]+nx*w,pt[i][1]+ny*w):ctx.moveTo(pt[i][0]+nx*w,pt[i][1]+ny*w); }
      for(let i=N;i>=0;i--){ const [nx,ny]=nrm(i),w=pt[i][2]; ctx.lineTo(pt[i][0]-nx*w,pt[i][1]-ny*w); }
      ctx.closePath(); ctx.fill();
      ctx.beginPath(); ctx.arc(pt[0][0],pt[0][1],w0,0,7); ctx.fill();          // rounded root
      ctx.beginPath(); ctx.arc(pt[N][0],pt[N][1],w0*0.10,0,7); ctx.fill();     // rounded point
    }
    if(S.ivory) drawTusk(sn*1.05, hr*0.40, sn*1.95, hr*0.88, sn*2.30, hr*0.34, hr*0.15, '#b9b09a');   // FAR tusk (dim), behind the trunk
    // TRUNK (elephant): a long muscular prehensile nose hanging from the face, tapering to two finger-lips at the tip,
    // ridged across, gently swaying at idle (or curling up with pose.trunk). Its own new render capability.
    if(S.trunk){
      const sway=Math.sin((pose.clock||0)*1.25)*0.5 - (pose.trunk||0)*2.2;
      const P0=[sn*1.0,-hr*0.04], P1=[sn*2.1+hr*0.3, hr*1.15], P2=[sn*2.05+sway*hr, hr*2.4], P3=[sn*1.3+sway*hr*2.3, hr*3.1];
      const NB=11, tp=[];
      for(let i=0;i<=NB;i++){ const u=i/NB,m=1-u;
        tp.push([ m*m*m*P0[0]+3*m*m*u*P1[0]+3*m*u*u*P2[0]+u*u*u*P3[0],
                  m*m*m*P0[1]+3*m*m*u*P1[1]+3*m*u*u*P2[1]+u*u*u*P3[1], hr*0.44*(1-0.8*u) ]); }
      const trn=(i)=>{ const a=tp[Math.max(0,i-1)],b=tp[Math.min(NB,i+1)],dx=b[0]-a[0],dy=b[1]-a[1],dl=Math.hypot(dx,dy)||1; return [-dy/dl,dx/dl]; };
      ctx.fillStyle=S.col[0]; ctx.beginPath();
      for(let i=0;i<=NB;i++){ const [px,py]=trn(i),w=tp[i][2]; i?ctx.lineTo(tp[i][0]+px*w,tp[i][1]+py*w):ctx.moveTo(tp[i][0]+px*w,tp[i][1]+py*w); }
      for(let i=NB;i>=0;i--){ const [px,py]=trn(i),w=tp[i][2]; ctx.lineTo(tp[i][0]-px*w,tp[i][1]-py*w); }
      ctx.closePath(); ctx.fill();
      if(det>0.4){ ctx.strokeStyle=hexa(S.col[2],0.45); ctx.lineWidth=Math.max(0.6,hr*0.045);   // ridges across the trunk
        for(let i=2;i<NB;i++){ const [px,py]=trn(i),w=tp[i][2]*0.8; ctx.beginPath(); ctx.moveTo(tp[i][0]-px*w,tp[i][1]-py*w); ctx.lineTo(tp[i][0]+px*w,tp[i][1]+py*w); ctx.stroke(); } }
      ctx.fillStyle=S.col[1]; ctx.beginPath(); ctx.arc(tp[NB][0]-hr*0.07,tp[NB][1],hr*0.055,0,7); ctx.arc(tp[NB][0]+hr*0.07,tp[NB][1],hr*0.055,0,7); ctx.fill();   // two finger-lips
    }
    if(S.ivory){ drawTusk(sn*1.18, hr*0.50, sn*2.15, hr*1.00, sn*2.55, hr*0.40, hr*0.17, '#efe9d6');   // NEAR tusk (bright ivory), in front of the trunk
      if(det>0.45){ ctx.strokeStyle='rgba(120,110,86,0.35)'; ctx.lineWidth=Math.max(0.5,hr*0.02);      // a faint growth-line down the near tusk
        ctx.beginPath(); ctx.moveTo(sn*1.2,hr*0.5); ctx.quadraticCurveTo(sn*2.1,hr*0.98, sn*2.5,hr*0.42); ctx.stroke(); } }
    // TUSKS (boar): two ivory tushes curving up from the lower jaw at the muzzle
    if(S.tusks){ ctx.lineCap='round';
      ctx.strokeStyle='#eae3d0'; ctx.lineWidth=hr*0.17;
      ctx.beginPath(); ctx.moveTo(sn*1.16, hr*0.44); ctx.quadraticCurveTo(sn*1.52, hr*0.16, sn*1.55,-hr*0.24); ctx.stroke(); // near tush
      ctx.strokeStyle='#d3ccb8'; ctx.lineWidth=hr*0.12;
      ctx.beginPath(); ctx.moveTo(sn*1.04, hr*0.46); ctx.quadraticCurveTo(sn*1.34, hr*0.2, sn*1.38,-hr*0.12); ctx.stroke(); } // far tush
    // ANTLERS (deer): a branched rack with real mass -- a knobby burr, a beam tapering from a thick base to a
    // fine tip through a natural up-back-then-forward sweep, a forward brow tine + two upper tines, polished pale
    // tips. The far antler is dimmer and offset (like the far legs). Drawn last so it sits above the skull.
    if(S.horns){
      const rack=(off, beamCol, tipCol)=>{
        const P=(x,y)=>[-hr*0.3+off + x, -hr*0.5 + y];                     // relative to the pedicle at the skull top-back
        const p0=P(0,0), p1=P(-hr*0.25,-hr*0.55), p2=P(-hr*0.4,-hr*1.2), p3=P(-hr*0.2,-hr*1.85), p4=P(hr*0.18,-hr*2.2);
        ctx.fillStyle=beamCol;
        ctx.beginPath(); ctx.arc(p0[0],p0[1],hr*0.17,0,7); ctx.fill();      // burr knob
        limb(p0,p1, hr*0.20, hr*0.15);                                      // main beam, tapering up the sweep
        limb(p1,p2, hr*0.15, hr*0.11);
        limb(p2,p3, hr*0.11, hr*0.07);
        limb(p3,p4, hr*0.07, hr*0.04);
        const brow=P(hr*0.55,-hr*0.5), t1=P(-hr*1.05,-hr*0.8), t2=P(-hr*0.95,-hr*1.6);
        limb(P(-hr*0.05,-hr*0.28), brow, hr*0.085, hr*0.025);              // brow tine, forward over the face
        limb(p1, t1, hr*0.09, hr*0.025);                                    // upper tine 1
        limb(p2, t2, hr*0.08, hr*0.025);                                    // upper tine 2
        if(det>0.55){ ctx.fillStyle=tipCol; for(const t of [p4,brow,t1,t2]){ ctx.beginPath(); ctx.arc(t[0],t[1],hr*0.055,0,7); ctx.fill(); } } // polished pale tips, up close
      };
      rack(hr*0.2, '#5f4d31', '#8a7a56');                                   // FAR antler (dimmer, offset)
      rack(0,      '#7a6748', '#b0a079');                                   // NEAR antler (brighter)
    }
    ctx.restore();
    // NEAR legs (in front of body)
    ctx.translate(0,-bob);
    drawLeg(backHip, O.bn, false); drawLeg(frontHip, O.fn, false);
    // SOIL flung back from the digging front paws -- a stateless spray, each speck cycling on the clock (no per-particle state)
    if((pose.dig||0)>0.4){ const dg=pose.dig, fpx=L*0.34, cl2=pose.clock||0;
      for(let i=0;i<10;i++){ const ph=((cl2*2.4 + i*0.37)%1+1)%1; if(ph>0.88) continue;
        const vv=((i*7)%3)/2, sx=fpx - ph*L*0.5*(0.7+0.5*vv), sy=gy - Math.sin(ph*Math.PI)*H*(0.33+0.28*vv) - ph*3, r=1.5*(1-ph*0.55)*dg;
        ctx.fillStyle = i%3? '#5a4632':'#6e5844'; ctx.beginPath(); ctx.ellipse(sx, sy, r, r*0.82, 0, 0,7); ctx.fill(); } }
    ctx.restore();
  }

  // ============================================================================================
  // BIRD -- a biped body plan: an ovoid body over two backward-bending legs, a folded wing with
  // primaries, a beak, a fanned tail. Build from bird def B (name sp seeds the feather stipple).
  // ============================================================================================
  // a bird's visible leg: the ankle (the "backward knee") sits BEHIND and above the foot,
  // the hip forward and above the ankle, so the leg reads as a rear-pointing Z.
  function birdLeg(ctx, hip, ft, B, w){
    const l1=B.leg*0.55, l2=B.leg*0.66, lc=B.legCol||'#3b3128', lc2=B.legCol2||B.legCol||'#332c23';
    const ank=ik(hip[0],hip[1],ft[0],ft[1], l1, l2, 1);
    ctx.strokeStyle=lc; ctx.lineCap='round'; ctx.lineJoin='round';
    ctx.lineWidth=B.legW*2.0*w;                        // feathered tibia, thicker near the body
    ctx.beginPath(); ctx.moveTo(hip[0],hip[1]); ctx.lineTo(ank[0],ank[1]); ctx.stroke();
    ctx.lineWidth=B.legW*1.0*w;                        // bare scaly tarsus, thin
    ctx.beginPath(); ctx.moveTo(ank[0],ank[1]); ctx.lineTo(ft[0],ft[1]); ctx.stroke();
    ctx.fillStyle=lc; ctx.beginPath(); ctx.arc(ank[0],ank[1],B.legW*0.9*w,0,7); ctx.fill(); // ankle knob
    // foot on the ground: three toes forward, one back
    ctx.lineWidth=B.legW*0.85*w; ctx.strokeStyle=lc2;
    for(const a of [-0.35,0.02,0.42].slice(0,B.foreToes)){ ctx.beginPath(); ctx.moveTo(ft[0],ft[1]);
      ctx.lineTo(ft[0]+B.toe*w*Math.cos(a), ft[1]+2+B.toe*0.3*w*Math.abs(Math.sin(a))); ctx.stroke(); }
    ctx.beginPath(); ctx.moveTo(ft[0],ft[1]); ctx.lineTo(ft[0]-B.toe*0.75*w, ft[1]+2); ctx.stroke(); // hind toe
  }

  // an OWL in silent GLIDING flight -- its own form (like the hedgehog's curled ball), swapped in by pose.flying.
  // Drawn in the readable "from below/behind" view: BROAD rounded wings spread wide as a gull-curve, sweeping
  // slow and deep with the beat, a round head forward, a short fanned tail, slotted primary "fingers". Head at +x.
  function drawBirdFlight(ctx, B, sp, det, phase, pose){
    const tuck=pose.tuck||0, strike=pose.strike||0;             // tuck = fold the wings for a diving STOOP; strike = flare up + reach talons
    const s=B.wing*0.62;                                          // a size unit off the wingspan
    const up=Math.sin(phase*6.2832)*0.6*(1-tuck) + strike*0.85; // strike FLARES the wings up; tuck stills them
    const wy=-up*s*0.7;
    const wtip=(1.7 - tuck*0.95)*s;                              // tuck folds the broad wings IN for the dive
    // WINGS: a filled broad gull-curve from the far tip, over the body, to the near tip (leading edge arched up)
    ctx.fillStyle=B.back; ctx.strokeStyle=hexa(B.col[2],0.6); ctx.lineWidth=Math.max(0.8,s*0.05); ctx.lineJoin='round'; ctx.lineCap='round';
    ctx.beginPath(); ctx.moveTo(-wtip, wy);
    ctx.quadraticCurveTo(-s*0.35, wy*0.4 - s*0.34, -s*0.06, -s*0.06);          // far wing leading edge to the body
    ctx.quadraticCurveTo( s*0.35, wy*0.4 - s*0.34,  wtip, wy);                 // over to the near wingtip
    ctx.quadraticCurveTo( s*0.42, wy*0.4 + s*0.10,  s*0.10, s*0.14);           // near trailing edge back
    ctx.quadraticCurveTo(-s*0.42, wy*0.4 + s*0.10, -wtip, wy); ctx.closePath(); ctx.fill();
    if(det>0.34){ ctx.fillStyle=B.col[2];                                       // slotted primary FINGERS at each wingtip
      for(const sgn of [-1,1]) for(let i=0;i<4;i++){ const u=i/3, tx=sgn*(wtip - u*s*0.30), ty=wy + Math.abs(up)*s*0.05 + u*s*0.13;
        ctx.beginPath(); ctx.moveTo(tx,ty); ctx.lineTo(tx+sgn*s*0.16, ty-s*0.03); ctx.lineTo(tx, ty+s*0.11); ctx.closePath(); ctx.fill(); } }
    ctx.fillStyle=B.col[1]; ctx.beginPath(); ctx.moveTo(-s*0.16,s*0.10); ctx.lineTo(-s*0.5,s*0.03); ctx.lineTo(-s*0.16,s*0.24); ctx.closePath(); ctx.fill();  // short tail fan (behind = -x)
    ctx.fillStyle=B.col[0]; ctx.beginPath(); ctx.ellipse(0, s*0.05, s*0.34, s*0.2, 0,0,7); ctx.fill();   // body
    if(det>0.42 && B.speckle){ ctx.fillStyle=hexa(B.speckle,0.4); const sr=mulberry(sp.length*7+3);
      for(let i=0;i<16;i++){ ctx.beginPath(); ctx.arc(-s*0.28+sr()*s*0.56, -s*0.06+sr()*s*0.22, 0.5,0,7); ctx.fill(); } }
    const hx=s*0.4, hr=s*0.2;                                                    // round head, forward
    ctx.fillStyle=B.facialDisc||B.back; ctx.beginPath(); ctx.arc(hx, 0, hr, 0,7); ctx.fill();
    if(det>0.4 && B.forwardEyes){ for(const ey of [-hr*0.4, hr*0.4]){ ctx.fillStyle=B.eyeDark?'#0a0603':'#0b0805';   // both forward eyes, seen head-on
        ctx.beginPath(); ctx.arc(hx+hr*0.35, ey, B.eye*0.6,0,7); ctx.fill();
        if(!B.eyeDark){ ctx.fillStyle=B.eyeCol||'#e2a726'; ctx.beginPath(); ctx.arc(hx+hr*0.35, ey, B.eye*0.36,0,7); ctx.fill(); } } }
    ctx.fillStyle=B.beakCol||'#3b3128'; ctx.beginPath(); ctx.arc(hx+hr*0.7, 0, hr*0.22, 0,7); ctx.fill();   // small beak between the eyes
    if(strike>0.2){ ctx.strokeStyle=B.legCol||'#c9b48c'; ctx.lineWidth=Math.max(1,B.legW*strike); ctx.lineCap='round';   // talons thrust DOWN for the grab
      for(const sgn of [-1,1]){ ctx.beginPath(); ctx.moveTo(sgn*s*0.12, s*0.12); ctx.lineTo(sgn*s*0.17, s*(0.28+0.4*strike)); ctx.stroke(); } }
  }
  // a ROOSTING bird -- the day/night REST verb in the biped body plan (a bird does not lie flat, it settles into a
  // fluffed ball, legs tucked into the belly feathers, head turned back with the beak buried over the wing, eye shut).
  function drawBirdRoost(ctx, B, sp, dir, det){
    const gy=B.leg + B.bodyH*0.42, fluff=1.16, aL=B.bodyL*0.5*fluff, aH=B.bodyH*0.5*fluff, cy=gy-aH*0.92;   // sit low, belly near the ground
    ctx.save(); ctx.translate(0, gy+3); const shR=B.bodyL*0.5; ctx.scale(1, 4.4/Math.max(1,shR));            // soft wide contact shadow
    const sg=ctx.createRadialGradient(0,0,0, 0,0,Math.max(1,shR)); sg.addColorStop(0,'rgba(0,0,0,0.24)'); sg.addColorStop(0.55,'rgba(0,0,0,0.11)'); sg.addColorStop(1,'rgba(0,0,0,0)');
    ctx.fillStyle=sg; ctx.beginPath(); ctx.arc(0,0,Math.max(1,shR),0,7); ctx.fill(); ctx.restore();
    ctx.save(); ctx.translate(0, cy);
    ctx.beginPath();                                          // a rounder, PUFFED body (roosting birds fluff into a ball)
    ctx.moveTo(-aL*0.95, aH*0.05);
    ctx.bezierCurveTo(-aL*0.95,-aH*1.0, aL*0.6,-aH*1.12, aL*1.0,-aH*0.2);
    ctx.bezierCurveTo(aL*1.12, aH*0.5, aL*0.72, aH*1.02, aL*0.1, aH*1.02);
    ctx.bezierCurveTo(-aL*0.5, aH*1.06, -aL*0.95, aH*0.7, -aL*0.95, aH*0.05);
    ctx.closePath();
    const grd=ctx.createLinearGradient(0,-aH,0,aH); grd.addColorStop(0,B.back); grd.addColorStop(0.45,B.col[0]); grd.addColorStop(0.7,B.breast); grd.addColorStop(1,B.belly);
    ctx.fillStyle=grd; ctx.fill();
    ctx.save(); ctx.clip();
    if(det>0.28){ let hi=ctx.createLinearGradient(0,-aH*1.2,0,-aH*0.1); hi.addColorStop(0,'rgba('+LIGHT.rim+',0.3)'); hi.addColorStop(1,'rgba('+LIGHT.rim+',0)'); ctx.fillStyle=hi; ctx.fillRect(-aL*1.3,-aH*1.3,aL*2.6,aH*1.3);
      let sh=ctx.createLinearGradient(0,aH*0.2,0,aH*1.2); sh.addColorStop(0,'rgba(20,12,6,0)'); sh.addColorStop(1,'rgba(20,12,6,0.24)'); ctx.fillStyle=sh; ctx.fillRect(-aL*1.3,aH*0.2,aL*2.6,aH*1.2); }
    const fr=mulberry(sp.length*613+7); for(let i=0,nf=Math.round(90*det*det);i<nf;i++){ const fx=-aL*0.9+fr()*aL*1.8, fy=-aH*0.9+fr()*aH*1.8;   // fluffed feather stipple
      ctx.strokeStyle=fr()<0.5?'rgba(255,250,238,'+(0.03+fr()*0.05).toFixed(3)+')':'rgba(26,17,8,'+(0.04+fr()*0.06).toFixed(3)+')'; ctx.lineWidth=0.7; ctx.beginPath(); ctx.moveTo(fx,fy); ctx.lineTo(fx-1.6-fr()*1.4, fy+0.8); ctx.stroke(); }
    ctx.restore();
    // HEAD turned back onto the shoulders, beak buried in the back/wing feathers -- the sleeping-bird silhouette
    const hx=-aL*0.46, hy=-aH*0.5, hr=(B.head||8)*0.66*fluff;
    ctx.fillStyle=B.col[0]; ctx.beginPath(); ctx.arc(hx,hy,hr,0,7); ctx.fill();
    if(det>0.3){ ctx.fillStyle=B.col[2]; ctx.beginPath(); ctx.moveTo(hx+hr*0.1,hy-hr*0.1); ctx.lineTo(hx+hr*1.15,hy+hr*0.45); ctx.lineTo(hx+hr*0.05,hy+hr*0.45); ctx.closePath(); ctx.fill(); }   // beak tucked back-down into the feathers
    if(det>0.45){ ctx.strokeStyle='rgba(20,14,8,0.72)'; ctx.lineWidth=Math.max(0.6,hr*0.13); ctx.beginPath(); ctx.arc(hx-hr*0.05,hy-hr*0.02,hr*0.34,0.2,1.25); ctx.stroke(); }   // a closed eye (asleep)
    ctx.restore();
  }
  function drawBird(ctx, B, sp, x, y, dir, phase, scale, pose){
    pose = pose || {};                                       // {neck,stand(0..1),peck(0..1 override)}
    ctx.save(); ctx.translate(x,y); ctx.scale(dir*scale, scale);
    const det = clamp(pose.lod!=null ? pose.lod : (scale-0.26)/0.52, 0, 1);   // 0 far silhouette .. 1 full model
    if(pose.flying){ drawBirdFlight(ctx, B, sp, det, phase, pose); ctx.restore(); return; }   // an owl on the wing -> the gliding form
    if((pose.rest||0)>0.55){ drawBirdRoost(ctx, B, sp, dir, det); ctx.restore(); return; }     // bedded at night -> the fluffed, head-tucked roost
    const st=pose.stand||0;
    const gy=B.leg + B.bodyH*0.42, gt=B.gait, ph=phase;
    // hop = both feet together and the body lofts; walk = feet alternate, a small bob
    const legOff = gt.hop ? [0.03, 0.0] : [0.5, 0.0];
    const airborne = (gt.hop && st<1) ? -foot(ph,0,gt.stride,gt.lift,gt.duty)[1]*(1-st) : 0;
    const bob = (1-st) * (gt.hop ? -airborne*0.7 : Math.sin(ph*2*Math.PI)*1.4);
    // contact shadow shrinks as the bird lofts
    ctx.save(); ctx.translate(0, gy+3); const bshR=B.bodyL*0.4*(1-airborne/gt.lift*0.5); ctx.scale(1, 4.4/Math.max(1,bshR));  // soft radial contact shadow; shrinks as the bird lofts
    const bshG=ctx.createRadialGradient(0,0,0, 0,0,Math.max(1,bshR));
    bshG.addColorStop(0,'rgba(0,0,0,0.24)'); bshG.addColorStop(0.55,'rgba(0,0,0,0.11)'); bshG.addColorStop(1,'rgba(0,0,0,0)');
    ctx.fillStyle=bshG; ctx.beginPath(); ctx.arc(0,0,Math.max(1,bshR),0,7); ctx.fill(); ctx.restore();

    // hips on the body underside; both legs from near there, slightly staggered
    const hipY=B.bodyH*0.30;
    const legPairs=[ {hip:[-B.bodyL*0.05, hipY], off:legOff[0], far:true },
                     {hip:[ B.bodyL*0.03, hipY], off:legOff[1], far:false} ];
    function footFor(off, splay){ const f=foot(ph, off, gt.stride, gt.lift, gt.duty);
      return [ f[0]*(1-st) + splay*st, gy + f[1]*(1-st) ]; }   // st blends the gaited foot toward a planted splay
    // FAR leg first (behind body)
    ctx.save(); ctx.translate(0,bob);
    birdLeg(ctx, legPairs[0].hip, footFor(legPairs[0].off, -B.legW*2.2), B, 0.82);
    ctx.restore();

    ctx.translate(0,bob);
    // ---- BODY: an egg tilted head-up, drawn in a rotated frame ----
    ctx.save(); ctx.rotate(B.bodyAng);
    const aL=B.bodyL*0.5, aH=B.bodyH*0.5;
    ctx.beginPath();
    ctx.moveTo(-aL*0.95, aH*0.1);                                    // tail base (narrow rear)
    ctx.bezierCurveTo(-aL*0.7,-aH*0.9, aL*0.2,-aH*1.05, aL*0.85,-aH*0.35); // over the back to the upper chest
    ctx.bezierCurveTo(aL*1.12, aH*0.15, aL*0.9, aH*0.9, aL*0.35, aH*0.95);  // round the full breast
    ctx.bezierCurveTo(-aL*0.2, aH*1.05, -aL*0.7, aH*0.7, -aL*0.95, aH*0.1); // belly back to the tail base
    ctx.closePath();
    const grd=ctx.createLinearGradient(0,-aH,0,aH);
    grd.addColorStop(0, B.back); grd.addColorStop(0.45, B.col[0]); grd.addColorStop(0.62, B.breast);
    grd.addColorStop(0.85, B.breast); grd.addColorStop(1, B.belly);
    ctx.fillStyle=grd; ctx.fill();
    ctx.save(); ctx.clip();
    if(det>0.28){   // form shading (fine): rim light over the back, shadow under the belly
      let hi=ctx.createLinearGradient(0,-aH*1.2,0,-aH*0.1); hi.addColorStop(0,'rgba('+LIGHT.rim+',0.32)'); hi.addColorStop(1,'rgba('+LIGHT.rim+',0)');
      ctx.fillStyle=hi; ctx.fillRect(-aL*1.3,-aH*1.3,aL*2.6,aH*1.3);
      if(B.sheen){ let ir=ctx.createLinearGradient(0,-aH,0,aH*0.4);   // iridescent blue-black sheen (corvid)
        ir.addColorStop(0,hexa(B.sheen,0)); ir.addColorStop(0.5,hexa(B.sheen,0.45)); ir.addColorStop(1,hexa(B.sheen,0));
        ctx.fillStyle=ir; ctx.fillRect(-aL*1.3,-aH,aL*2.6,aH*1.4); }
      let sh=ctx.createLinearGradient(0,aH*0.2,0,aH*1.2); sh.addColorStop(0,'rgba(20,12,6,0)'); sh.addColorStop(1,'rgba(20,12,6,0.22)');
      ctx.fillStyle=sh; ctx.fillRect(-aL*1.3,aH*0.2,aL*2.6,aH*1.2);
      const sunL=-dir, wg=ctx.createLinearGradient(sunL*aL,0,-sunL*aL,0);   // directional low-sun warmth
      wg.addColorStop(0,'rgba('+LIGHT.dir+',0.14)'); wg.addColorStop(0.5,'rgba('+LIGHT.dir+',0)');
      wg.addColorStop(0.56,'rgba(28,32,44,0)'); wg.addColorStop(1,'rgba(28,32,44,0.13)');
      ctx.fillStyle=wg; ctx.fillRect(-aL*1.3,-aH*1.3,aL*2.6,aH*2.6);
    }
    // fine feather stipple (seed-stable), thins out with distance
    const fr=mulberry(sp.length*613 + sp.charCodeAt(0)*53 + 7);
    for(let i=0,nf=Math.round(110*det*det);i<nf;i++){ const fx=-aL*0.95+fr()*aL*1.9, fy=-aH*0.95+fr()*aH*1.9;
      ctx.strokeStyle = fr()<0.5 ? 'rgba(255,250,238,'+(0.03+fr()*0.05).toFixed(3)+')' : 'rgba(26,17,8,'+(0.04+fr()*0.06).toFixed(3)+')';
      ctx.lineWidth=0.7; ctx.beginPath(); ctx.moveTo(fx,fy); ctx.lineTo(fx-1.8-fr()*1.6, fy+0.8); ctx.stroke(); }
    if(B.barred && det>0.4){ ctx.strokeStyle=hexa(B.barred,0.5); ctx.lineWidth=1;   // cryptic BARRING across the underparts (tawny owl)
      for(let i=0;i<8;i++){ const yy=-aH*0.35+i*aH*0.2; ctx.beginPath();
        for(let xx=-aL*0.85; xx<aL*0.9; xx+=3){ const yj=yy+Math.sin(xx*0.35+i*1.3)*1.3; xx<=-aL*0.85?ctx.moveTo(xx,yj):ctx.lineTo(xx,yj); } ctx.stroke(); } }
    if(B.speckle && det>0.42){ ctx.fillStyle=hexa(B.speckle,0.5);                    // fine dark SPECKLING on white underparts (barn owl)
      const sr2=mulberry(sp.length*211+9);
      for(let i=0,ns=Math.round(66*det);i<ns;i++){ const sx2=-aL*0.55+sr2()*aL*1.25, sy2=-aH*0.05+sr2()*aH*1.0;
        ctx.beginPath(); ctx.arc(sx2,sy2,0.55+sr2()*0.7,0,7); ctx.fill(); } }
    ctx.restore();
    ctx.restore(); // un-rotate

    // ---- TAIL: fanned feathers projecting back-and-down from the tail base ----
    const tb=[B.tailBase[0]*B.bodyL, B.tailBase[1]*B.bodyH];
    ctx.save(); ctx.translate(tb[0],tb[1]); ctx.rotate(B.bodyAng);
    for(let i=0;i<B.tailN;i++){ const t=(i/(B.tailN-1)-0.5)*B.tailFan;
      const ex=-Math.cos(t)*B.tail, ey=Math.sin(t)*B.tail + B.tail*B.tailAng*0.5;   // feather tip
      const cx=ex*0.5, cy=ey*0.4-2, L=Math.hypot(ex,ey)||1, nx=-ey/L, ny=ex/L;      // curve control + perpendicular for width
      const w0=0.7, w1=1.9;                                                          // a SHAPED rectrix (not a stick): base + mid half-widths, tapering to a point
      ctx.fillStyle = i%2? B.col[1] : B.col[2];
      ctx.beginPath(); ctx.moveTo(nx*w0, ny*w0);
      ctx.quadraticCurveTo(cx+nx*w1, cy+ny*w1, ex, ey);                              // one edge out to the tip
      ctx.quadraticCurveTo(cx-nx*w1, cy-ny*w1, -nx*w0, -ny*w0);                       // and back the other edge
      ctx.closePath(); ctx.fill();
      ctx.strokeStyle=hexa(B.col[2],0.5); ctx.lineWidth=0.5; ctx.stroke();
      if(det>0.55){ ctx.strokeStyle=hexa(B.back,0.55); ctx.lineWidth=0.4; ctx.beginPath(); ctx.moveTo(0,0); ctx.quadraticCurveTo(cx,cy,ex,ey); ctx.stroke(); } } // shaft, up close
    ctx.restore();

    // ---- WING: folded, layered feathers -- long primaries fanning back, coverts over the shoulder ----
    ctx.save(); ctx.rotate(B.bodyAng);
    ctx.lineJoin='round';
    const wrist=[-aL*0.16, -aH*0.24];                               // the wrist, where the primaries spring from
    // base membrane under the feathers
    ctx.fillStyle=B.col[2]; ctx.beginPath();
    ctx.moveTo(aL*0.48,-aH*0.34); ctx.quadraticCurveTo(-aL*0.2,-aH*0.5, -aL*0.98, aH*0.03);
    ctx.quadraticCurveTo(-aL*0.3, aH*0.22, aL*0.44, aH*0.02); ctx.closePath(); ctx.fill();
    // PRIMARIES: individual flight feathers (fine); far away the wing stays a simple membrane blob
    if(det>0.32) for(let i=0;i<B.prim;i++){ const u=i/(B.prim-1);
      const bx=wrist[0]+u*aL*0.10, by=wrist[1]+u*aH*0.20;           // feather root along the wrist
      const tx=-aL*(0.72+u*0.28), ty=aH*(-0.02+u*0.16);            // feather tip, fans back and down
      ctx.fillStyle = i%2 ? B.col[1] : B.col[2];
      ctx.beginPath(); ctx.moveTo(bx,by);
      ctx.quadraticCurveTo((bx+tx)*0.5, (by+ty)*0.5-aH*0.05, tx,ty);       // upper edge
      ctx.quadraticCurveTo((bx+tx)*0.5+aL*0.03,(by+ty)*0.5+aH*0.05, bx+aL*0.055,by+aH*0.05); // lower edge back
      ctx.closePath(); ctx.fill();
      ctx.strokeStyle=hexa(B.col[2],0.55); ctx.lineWidth=0.5; ctx.stroke();
      if(det>0.62){ ctx.strokeStyle=hexa(B.back,0.5); ctx.lineWidth=0.4;    // RACHIS: the central shaft down each primary
        ctx.beginPath(); ctx.moveTo(bx,by); ctx.quadraticCurveTo((bx+tx)*0.5,(by+ty)*0.5-aH*0.02, tx,ty); ctx.stroke(); }
    }
    // SECONDARIES: the shorter inner flight feathers, rooting along the forearm inboard of the primaries (fill the trailing edge)
    if(det>0.36){ const elbow=[aL*0.16,-aH*0.26], nS=Math.max(3,B.prim-1);
      for(let i=0;i<nS;i++){ const u=i/(nS-1);
        const bx=elbow[0]+u*(wrist[0]-elbow[0]), by=elbow[1]+u*(wrist[1]-elbow[1]);   // root along the forearm
        const tx=-aL*(0.34+u*0.34), ty=aH*(0.02+u*0.13);                              // shorter tips, back and down
        ctx.fillStyle = i%2 ? B.col[0] : B.col[1];
        ctx.beginPath(); ctx.moveTo(bx,by);
        ctx.quadraticCurveTo((bx+tx)*0.5,(by+ty)*0.5-aH*0.04, tx,ty);
        ctx.quadraticCurveTo((bx+tx)*0.5+aL*0.02,(by+ty)*0.5+aH*0.04, bx+aL*0.05,by+aH*0.04);
        ctx.closePath(); ctx.fill();
        ctx.strokeStyle=hexa(B.col[2],0.45); ctx.lineWidth=0.45; ctx.stroke(); }
    }
    // SPECULUM: the iridescent wing patch (a mallard's blue), white-bordered, over the inner secondaries
    if(B.speculum && det>0.38){ ctx.fillStyle=hexa(B.speculum,0.92);
      ctx.beginPath(); ctx.ellipse(-aL*0.12,-aH*0.02, aL*0.2, aH*0.13, -0.08, 0,7); ctx.fill();
      ctx.strokeStyle='rgba(244,244,240,0.7)'; ctx.lineWidth=0.9; ctx.stroke(); }
    // COVERTS: a rounded, slightly paler group over the shoulder, with a few feather edges
    ctx.fillStyle=B.back; ctx.beginPath();
    ctx.moveTo(aL*0.48,-aH*0.34); ctx.quadraticCurveTo(-aL*0.02,-aH*0.48, -aL*0.34,-aH*0.08);
    ctx.quadraticCurveTo(-aL*0.02,-aH*0.0, aL*0.44,-aH*0.08); ctx.closePath(); ctx.fill();
    if(det>0.45){ ctx.strokeStyle=hexa(B.col[2],0.4); ctx.lineWidth=0.6; ctx.lineCap='round';
      for(let i=0;i<3;i++){ const v=i/2; ctx.beginPath();
        ctx.moveTo(aL*(0.34-v*0.46),-aH*(0.28-v*0.04));
        ctx.quadraticCurveTo(-aL*(0.02+v*0.18),-aH*0.18, -aL*(0.1+v*0.24),-aH*0.04); ctx.stroke(); } }
    ctx.restore();

    // ---- NECK + HEAD + BEAK + EYE ----
    const headbob = (1-st)*Math.sin(ph*2*Math.PI)*B.gait.headbob;   // a live little head bob, stills when planted
    // peck: behaviour can drive a deliberate strike (pose.peck 0..1); else the gait's own forage dip
    const peck = (pose.peck!=null) ? pose.peck*(B.gait.peck||1) : (B.gait.peck ? (ph<0.42 ? Math.sin(ph/0.42*Math.PI) : 0)*B.gait.peck : 0);
    const neckAng = B.neckAng + (pose.neck||0);                     // posture can raise the head (alert)
    const nb=[B.bodyL*0.30, -B.bodyH*0.40];                         // neck base at the upper front of the body
    const hd=[nb[0]+Math.cos(neckAng)*B.neck + peck*B.neck*0.32,    // head center, dipping down-and-forward to peck
              nb[1]+Math.sin(neckAng)*B.neck - headbob + peck*B.neck*0.92];
    const ndx=hd[0]-nb[0], ndy=hd[1]-nb[1], nlen=Math.hypot(ndx,ndy)||1; // curve the neck into an S / hunch
    const cp=[(nb[0]+hd[0])/2 - ndy/nlen*B.neckCurve*B.neck, (nb[1]+hd[1])/2 + ndx/nlen*B.neckCurve*B.neck];
    // tapered neck: sample the curve and offset by a width that shrinks base-to-head, so it thins toward the skull
    const nw0=(B.neckW||B.bodyH*0.42)*0.55, nw1=B.head*0.26, npts=[];
    for(let i=0;i<=9;i++){ const t=i/9, mt=1-t;
      npts.push([mt*mt*nb[0]+2*mt*t*cp[0]+t*t*hd[0], mt*mt*nb[1]+2*mt*t*cp[1]+t*t*hd[1], nw0+(nw1-nw0)*t]); }
    ctx.fillStyle=B.back; ctx.beginPath();
    const perp=(i)=>{ const a=npts[Math.max(0,i-1)], b=npts[Math.min(npts.length-1,i+1)], dx=b[0]-a[0],dy=b[1]-a[1],dl=Math.hypot(dx,dy)||1; return [-dy/dl, dx/dl]; };
    for(let i=0;i<npts.length;i++){ const [px,py]=perp(i), w=npts[i][2]; const X=npts[i][0]+px*w, Y=npts[i][1]+py*w; i?ctx.lineTo(X,Y):ctx.moveTo(X,Y); }
    for(let i=npts.length-1;i>=0;i--){ const [px,py]=perp(i), w=npts[i][2]; ctx.lineTo(npts[i][0]-px*w, npts[i][1]-py*w); }
    ctx.closePath(); ctx.fill();
    // head
    ctx.fillStyle=B.headCol||B.back; ctx.beginPath(); ctx.arc(hd[0],hd[1],B.head*0.5,0,7); ctx.fill();   // a keyed headCol (a mallard's iridescent green) else the back colour
    if(det>0.4 && !B.facialDisc){
      if(B.headSheen){ ctx.fillStyle=hexa(B.headSheen,0.55); ctx.beginPath(); ctx.arc(hd[0]-B.head*0.1,hd[1]-B.head*0.17,B.head*0.3,0,7); ctx.fill(); } // iridescent sheen on the crown
      else { ctx.fillStyle=B.cheek; ctx.beginPath(); ctx.arc(hd[0]+B.head*0.14,hd[1]+B.head*0.14,B.head*0.32,0,7); ctx.fill(); } // pale cheek
    }
    if(B.neckRing && det>0.35){ const rt=0.66, rp=[nb[0]+(hd[0]-nb[0])*rt, nb[1]+(hd[1]-nb[1])*rt];   // a white collar where the head meets the neck (mallard)
      ctx.save(); ctx.translate(rp[0],rp[1]); ctx.rotate(Math.atan2(hd[1]-nb[1], hd[0]-nb[0]));
      ctx.fillStyle=B.neckRing; ctx.beginPath(); ctx.ellipse(0,0, B.head*0.15, (B.neckW||B.bodyH*0.42)*0.62, 0, 0,7); ctx.fill(); ctx.restore(); }
    if(B.crown && det>0.4){ ctx.fillStyle=B.crown; ctx.beginPath(); ctx.ellipse(hd[0]-B.head*0.08, hd[1]-B.head*0.2, B.head*0.42, B.head*0.26, -0.2, 0,7); ctx.fill(); } // a dark CROWN cap (heron)
    if(B.plume && det>0.36){ ctx.strokeStyle=B.plume; ctx.lineCap='round';                            // long NAPE PLUMES trailing back off the crown (heron)
      for(let i=0;i<2;i++){ ctx.lineWidth=1.5-i*0.5; const sx=hd[0]-B.head*0.34, sy=hd[1]-B.head*(0.22-i*0.1);
        ctx.beginPath(); ctx.moveTo(sx,sy); ctx.quadraticCurveTo(sx-B.head*0.95, sy-B.head*0.08, sx-B.head*1.8, sy+B.head*(0.18+i*0.16)); ctx.stroke(); } }
    if(B.facialDisc){
      // ---- OWL face: ear tufts, a rimmed FACIAL DISC, FORWARD-facing paired eyes, a small HOOKED beak.
      // Drawn as one group rotated by pose.faceTurn -- the owl's signature head-SWIVEL (it turns its whole face). ----
      ctx.save(); ctx.translate(hd[0],hd[1]); ctx.rotate(pose.faceTurn||0); const Hh=B.head;
      if(B.earTufts){ ctx.fillStyle=B.col[1];                                     // two feathered horns off the crown
        for(const s of [0.30,-0.12]){ ctx.beginPath(); ctx.moveTo(Hh*(s-0.06),-Hh*0.28);
          ctx.quadraticCurveTo(Hh*(s+0.12),-Hh*0.74, Hh*(s+0.22),-Hh*0.34); ctx.closePath(); ctx.fill(); }
        if(det>0.5){ ctx.strokeStyle=hexa(B.col[2],0.5); ctx.lineWidth=0.6;
          for(const s of [0.30,-0.12]){ ctx.beginPath(); ctx.moveTo(Hh*(s+0.02),-Hh*0.4); ctx.lineTo(Hh*(s+0.13),-Hh*0.64); ctx.stroke(); } } }
      const dcx=Hh*0.14, dcy=Hh*0.05, dr=Hh*0.52, heart=(B.discShape==='heart');   // the facial disc: a round tawny ruff, or a barn owl's HEART
      const discPath=(rs)=>{ ctx.beginPath();
        if(heart){ const w=dr*1.02*rs, h=dr*1.24*rs, cy2=dcy-dr*0.12;             // two top lobes tapering to a chin point
          ctx.moveTo(dcx, cy2+h*0.6);
          ctx.bezierCurveTo(dcx-w, cy2+h*0.06, dcx-w*0.52, cy2-h*0.52, dcx, cy2-h*0.06);
          ctx.bezierCurveTo(dcx+w*0.52, cy2-h*0.52, dcx+w, cy2+h*0.06, dcx, cy2+h*0.6); ctx.closePath();
        } else ctx.arc(dcx,dcy,dr*rs,0,7); };
      ctx.fillStyle=B.facialDisc; discPath(1); ctx.fill();
      if(det>0.3){ ctx.strokeStyle=B.discRim||'#5a4c38'; ctx.lineWidth=Math.max(1,Hh*0.05); discPath(0.97); ctx.stroke(); }
      if(det>0.5){ ctx.save(); discPath(1); ctx.clip(); ctx.strokeStyle=hexa(B.discRim||'#5a4c38',0.4); ctx.lineWidth=0.6;   // radial ruff, clipped to the disc shape
        for(let i=0;i<22;i++){ const a=i/22*7; ctx.beginPath(); ctx.moveTo(dcx+Math.cos(a)*dr*0.72,dcy+Math.sin(a)*dr*0.72); ctx.lineTo(dcx+Math.cos(a)*dr*1.12,dcy+Math.sin(a)*dr*1.12); ctx.stroke(); } ctx.restore(); }
      if(B.brow && det>0.45){ ctx.strokeStyle=hexa(B.brow,0.6); ctx.lineWidth=Math.max(0.8,Hh*0.035);   // pale brow line between the eyes
        ctx.beginPath(); ctx.moveTo(dcx,dcy-dr*0.5); ctx.lineTo(dcx,dcy+dr*0.14); ctx.stroke(); }
      const eyes = heart ? [[dcx+dr*0.24,dcy-dr*0.02,B.eye*0.98],[dcx-dr*0.20,dcy-dr*0.02,B.eye*0.8]]   // barn owl: small, close-set, dark
                         : [[dcx+dr*0.40,dcy-dr*0.05,B.eye*1.06],[dcx-dr*0.36,dcy-dr*0.04,B.eye*0.82]]; // tawny: big, wide, amber
      for(const e of eyes){ const ex=e[0],ey=e[1],er=e[2];
        ctx.fillStyle='#0b0805'; ctx.beginPath(); ctx.arc(ex,ey,er*(B.eyeDark?1.08:1.3),0,7); ctx.fill();  // dark socket (tight on a dark-eyed barn owl)
        ctx.fillStyle=B.eyeCol||'#e2a726'; ctx.beginPath(); ctx.arc(ex,ey,er,0,7); ctx.fill();             // iris (amber, or near-black for the barn owl)
        if(det>0.35 && !B.eyeDark){ ctx.fillStyle='#0a0603'; ctx.beginPath(); ctx.arc(ex,ey,er*0.52,0,7); ctx.fill(); }   // a pupil only shows on the light iris
        if(LIGHT.night && det>0.3){                                                                        // EYESHINE: the eye catches the moon and GLEAMS (tapetum) -- the owl's night stare
          const eg=ctx.createRadialGradient(ex,ey,0,ex,ey,er*2.3);
          eg.addColorStop(0,'rgba(255,236,188,0.5)'); eg.addColorStop(0.42,'rgba(255,214,140,0.24)'); eg.addColorStop(1,'rgba(255,214,140,0)');
          ctx.fillStyle=eg; ctx.beginPath(); ctx.arc(ex,ey,er*2.3,0,7); ctx.fill();
          ctx.fillStyle='rgba(255,244,206,0.92)'; ctx.beginPath(); ctx.arc(ex,ey,er*0.5,0,7); ctx.fill(); }  // bright reflective core
        if(det>0.4){ ctx.fillStyle='rgba(255,255,255,0.85)'; ctx.beginPath(); ctx.arc(ex+er*0.3,ey-er*0.34,er*0.24,0,7); ctx.fill(); } }  // catchlight
      const kx=dcx+dr*0.02, ky=dcy+dr*0.15;                                        // the small down-hooked beak, nested in the disc
      ctx.fillStyle=B.beakCol||'#3b3128'; ctx.beginPath();
      ctx.moveTo(kx-Hh*0.06,ky); ctx.quadraticCurveTo(kx+Hh*0.10,ky-Hh*0.02, kx+Hh*0.11,ky+Hh*0.06);
      ctx.quadraticCurveTo(kx+Hh*0.08,ky+Hh*0.17, kx+Hh*0.02,ky+Hh*0.12);
      ctx.quadraticCurveTo(kx-Hh*0.02,ky+Hh*0.06, kx-Hh*0.06,ky); ctx.closePath(); ctx.fill();
      ctx.restore();
    } else {
    // beak: a cone (finch/robin) projecting forward from the head front
    const bx=hd[0]+B.head*0.44, by=hd[1]+B.head*0.03;
    ctx.fillStyle=B.beakCol||'#d8b24e';                      // warm beak by default; the corvid's is dark
    if(B.beakType==='bill'){                                 // a broad flat spatulate DUCK bill
      ctx.beginPath(); ctx.moveTo(bx-1, by-B.beakH*0.45);
      ctx.quadraticCurveTo(bx+B.beak*0.9, by-B.beakH*0.5, bx+B.beak, by-B.beakH*0.02);
      ctx.quadraticCurveTo(bx+B.beak*1.04, by+B.beakH*0.5, bx+B.beak*0.62, by+B.beakH*0.52);
      ctx.quadraticCurveTo(bx+B.beak*0.2, by+B.beakH*0.5, bx-1, by+B.beakH*0.45); ctx.closePath(); ctx.fill();
      if(det>0.4){ ctx.fillStyle='rgba(20,14,6,0.5)'; ctx.beginPath(); ctx.arc(bx+B.beak*0.96, by+B.beakH*0.02, 0.9,0,7); ctx.fill(); } // the nail at the bill tip
    } else if(B.beakType==='hook'){                          // a RAPTOR's hooked bill (side view): a deep base, the upper mandible curving down past a shorter lower into a sharp point, with a fleshy CERE at the base
      const bh=B.beakH;
      if(B.cere){ ctx.fillStyle=B.cere; ctx.beginPath(); ctx.ellipse(bx-B.beak*0.06, by-bh*0.04, B.beak*0.26, bh*0.5, 0,0,7); ctx.fill(); }
      ctx.fillStyle=B.beakCol||'#3a3026'; ctx.beginPath();
      ctx.moveTo(bx-1, by-bh*0.5);
      ctx.quadraticCurveTo(bx+B.beak*0.9, by-bh*0.5, bx+B.beak*1.02, by+bh*0.04);   // upper culmen, forward then hooking down
      ctx.quadraticCurveTo(bx+B.beak*1.0, by+bh*0.62, bx+B.beak*0.7, by+bh*0.6);    // the tip drops below the lower edge
      ctx.quadraticCurveTo(bx+B.beak*0.52, by+bh*0.42, bx+B.beak*0.36, by+bh*0.34); // shorter lower mandible
      ctx.quadraticCurveTo(bx+B.beak*0.12, by+bh*0.3, bx-1, by+bh*0.42); ctx.closePath(); ctx.fill();
    } else {                                                 // a cone (finch) or dagger (heron/crow)
      ctx.beginPath();
      ctx.moveTo(bx, by-B.beakH*0.55);
      ctx.lineTo(bx+B.beak, by+B.beakH*0.05);                   // the tip
      ctx.lineTo(bx, by+B.beakH*0.55); ctx.closePath(); ctx.fill();
      if(det>0.4){ ctx.strokeStyle='rgba(20,14,6,0.4)'; ctx.lineWidth=0.6;   // gape line (fine)
        ctx.beginPath(); ctx.moveTo(bx,by); ctx.lineTo(bx+B.beak, by+B.beakH*0.05); ctx.stroke(); }
    }
    // EYE -- a glossy bird eye: a dark orb, an optional keyed iris + pupil (a heron's pale eye), a bright wet catchlight
    const bex=hd[0]+B.head*0.16, bey=hd[1]-B.head*0.05, ber=B.eye;
    ctx.fillStyle='#0b0805'; ctx.beginPath(); ctx.arc(bex,bey,ber,0,7); ctx.fill();
    if(det>0.4){
      if(B.eyeCol){ ctx.fillStyle=B.eyeCol; ctx.beginPath(); ctx.arc(bex,bey,ber*0.82,0,7); ctx.fill();
        ctx.fillStyle='#070504'; ctx.beginPath(); ctx.arc(bex,bey,ber*0.5,0,7); ctx.fill(); }                        // iris + pupil
      ctx.fillStyle='rgba(255,255,255,0.9)'; ctx.beginPath(); ctx.arc(bex+ber*0.34, bey-ber*0.34, ber*0.34,0,7); ctx.fill(); }  // glossy wet catchlight
    }

    if(B.malar && det>0.3){ ctx.strokeStyle=hexa(B.malar,0.8); ctx.lineWidth=Math.max(1.5,B.head*0.14); ctx.lineCap='round';   // the falcon MOUSTACHE: a dark bar dropping from below the eye down the cheek
      ctx.beginPath(); ctx.moveTo(bex-B.head*0.03, bey+ber*1.2); ctx.quadraticCurveTo(bex+B.head*0.03, bey+B.head*0.24, bex-B.head*0.02, bey+B.head*0.44); ctx.stroke(); }
    // NEAR leg (in front of the body)
    ctx.translate(0,-bob);
    ctx.save(); ctx.translate(0,bob);
    birdLeg(ctx, legPairs[1].hip, footFor(legPairs[1].off, B.legW*2.2), B, 1.0);
    ctx.restore();
    ctx.restore();
  }

  // SNAKE -- a limbless body plan. The whole body is a tapering ribbon sampled along a spine that
  // carries a TRAVELING undulation (phase drives the slither); a viper wedge head with an eye and an
  // intermittent forked TONGUE; per-species dorsal markings (adder zigzag, grass-snake collar). LOD:
  // a bare undulating spindle when far, resolving markings + eye + tongue + ventral scutes as it nears.
  function drawSnake(ctx, S, sp, x, y, dir, phase, scale, pose){
    pose = pose || {};
    ctx.save(); ctx.translate(x,y); ctx.scale(dir*scale, scale);
    const det = clamp(pose.lod!=null ? pose.lod : (scale-0.26)/0.52, 0, 1);
    const L=S.L, girth=S.girth, ph=phase, raise=pose.raise||0, coil=pose.coil||0, strike=pose.strike||0;
    const N=58, waves=S.waves||2.3, amp=(S.amp||0.14)*L;
    const by=-(girth+amp);   // baseline: lift the spine so the body's lowest point rests on y=0 (the ground)
    // girth along s (tail 0 .. head 1): a body spindle (thin tail, fat front-third) then a viper wedge head
    function girthAt(s){
      if(s<0.84) return girth*Math.pow(Math.sin(Math.PI*clamp(Math.pow(s/0.92,0.82),0,1)),0.5);
      const t=(s-0.84)/0.16, neckW=girth*0.5, cheekW=girth*0.6*(S.headW||1.2), snoutW=girth*0.06;
      return t<0.42 ? neckW+(cheekW-neckW)*(t/0.42) : cheekW+(snoutW-cheekW)*((t-0.42)/0.58);
    }
    // spine points, per-point normal, per-point half-width
    const P=[], NM=[], WD=[];
    for(let i=0;i<N;i++){
      const s=i/(N-1), env=Math.sin(Math.PI*s);
      let px=-L*0.5+s*L;
      let py=by + env*amp*Math.sin(waves*2*Math.PI*s - ph*2*Math.PI);
      if(raise>0 && s>0.68){ const u=(s-0.68)/0.32; py -= raise*girth*5.5*u*u; }                                   // alert: front third rears up
      if(coil>0 && s>0.5){ const u=(s-0.5)/0.5; py -= coil*girth*8*u*u; py += coil*girth*3.2*Math.sin(u*Math.PI); } // rear into a striking S (belly-out kink reads as a coil, not a stick)
      if(strike>0 && s>0.55){ const u=(s-0.55)/0.45; px += strike*L*0.36*u; py += strike*girth*7*u*u; }             // the head LUNGES forward + down
      P.push([px,py]); WD.push(girthAt(s));
    }
    for(let i=0;i<N;i++){ const a=P[Math.max(0,i-1)], b=P[Math.min(N-1,i+1)];
      let tx=b[0]-a[0], ty=b[1]-a[1], tl=Math.hypot(tx,ty)||1; NM.push([-ty/tl, tx/tl]); }
    // outline edges
    const top=[], bot=[];
    for(let i=0;i<N;i++){ const p=P[i], nm=NM[i], w=WD[i];
      top.push([p[0]+nm[0]*w, p[1]+nm[1]*w]); bot.push([p[0]-nm[0]*w, p[1]-nm[1]*w]); }
    function bodyPath(ox,oy){ ctx.beginPath(); ctx.moveTo(top[0][0]+ox, top[0][1]+oy);
      for(let i=1;i<N;i++) ctx.lineTo(top[i][0]+ox, top[i][1]+oy);
      for(let i=N-1;i>=0;i--) ctx.lineTo(bot[i][0]+ox, bot[i][1]+oy); ctx.closePath(); }
    // soft contact shadow (a low, offset copy of the body, on the ground under it)
    ctx.save(); ctx.fillStyle='rgba(0,0,0,0.16)'; bodyPath(2, girth*0.75); ctx.fill(); ctx.restore();
    // body fill: vertical countershading (dark dorsal -> base -> pale belly)
    let minY=1e9,maxY=-1e9; for(const p of top){ if(p[1]<minY)minY=p[1]; } for(const p of bot){ if(p[1]>maxY)maxY=p[1]; }
    const g=ctx.createLinearGradient(0,minY,0,maxY);
    g.addColorStop(0,S.col[1]); g.addColorStop(0.48,S.col[0]); g.addColorStop(1,S.belly);
    bodyPath(0,0); ctx.fillStyle=g; ctx.fill();
    ctx.strokeStyle=hexa(S.col[2]||'#333',0.5); ctx.lineWidth=Math.max(0.4,girth*0.05); ctx.lineJoin='round'; bodyPath(0,0); ctx.stroke();
    // rim light on the upper edge, resolving in with detail
    if(det>0.28){ ctx.strokeStyle=hexa('#ffffff',0.14*det); ctx.lineWidth=Math.max(0.5,girth*0.16); ctx.lineJoin='round'; ctx.lineCap='round';
      ctx.beginPath(); ctx.moveTo(top[0][0],top[0][1]); for(let i=1;i<Math.round(N*0.9);i++) ctx.lineTo(top[i][0],top[i][1]); ctx.stroke(); }
    // ADDER zigzag: a sharp dark chevron band down the dorsal midline (clipped to the body)
    if(det>0.4 && S.zigzag){ const Z=S.zigzag; ctx.save(); bodyPath(0,0); ctx.clip();
      ctx.strokeStyle=hexa(Z.c,Z.a); ctx.lineWidth=Math.max(1,girth*0.5); ctx.lineJoin='round'; ctx.lineCap='round';
      ctx.beginPath();
      for(let i=0;i<N;i++){ const s=i/(N-1); const tw=2*Math.abs(2*(((Z.n*s)%1))-1)-1;   // triangle wave -> sharp zigzag
        const p=P[i], nm=NM[i], w=WD[i], off=tw*Z.amp*w;
        const xx=p[0]+nm[0]*(off - w*0.12), yy=p[1]+nm[1]*(off - w*0.12);
        if(i===0) ctx.moveTo(xx,yy); else ctx.lineTo(xx,yy); }
      ctx.stroke(); ctx.restore(); }
    // GRASS-SNAKE collar: a bright band + a dark band across the nape (clipped)
    if(det>0.38 && S.collar){ const C=S.collar, k=Math.round(0.86*(N-1)), k2=Math.round(0.815*(N-1));
      ctx.save(); bodyPath(0,0); ctx.clip(); ctx.lineCap='butt';
      ctx.strokeStyle=hexa(C.c,C.a); ctx.lineWidth=Math.max(1,girth*0.55); ctx.beginPath(); ctx.moveTo(top[k][0],top[k][1]); ctx.lineTo(bot[k][0],bot[k][1]); ctx.stroke();
      ctx.strokeStyle=hexa(C.c2||'#20220f',C.a); ctx.lineWidth=Math.max(0.6,girth*0.3); ctx.beginPath(); ctx.moveTo(top[k2][0],top[k2][1]); ctx.lineTo(bot[k2][0],bot[k2][1]); ctx.stroke();
      ctx.restore(); }
    // CORN-SNAKE saddles: dark-edged dorsal blotches spaced evenly down the back (clipped)
    if(det>0.4 && S.saddles){ const Sa=S.saddles; ctx.save(); bodyPath(0,0); ctx.clip(); ctx.lineCap='round';
      for(let k=0;k<Sa.n;k++){ const s=0.06+(k/(Sa.n-1))*0.76, i=Math.round(s*(N-1)), t=top[i], p=P[i], nm=NM[i], w=WD[i];
        const mx=p[0]-nm[0]*w*0.12, my=p[1]-nm[1]*w*0.12;                                 // top edge down to ~mid-body
        if(Sa.edge){ ctx.strokeStyle=hexa(Sa.edge,Sa.a); ctx.lineWidth=Math.max(1.4,girth*0.6);  ctx.beginPath(); ctx.moveTo(t[0],t[1]); ctx.lineTo(mx,my); ctx.stroke(); }
        ctx.strokeStyle=hexa(Sa.c,Sa.a); ctx.lineWidth=Math.max(1,girth*0.44); ctx.beginPath(); ctx.moveTo(t[0],t[1]); ctx.lineTo(mx,my); ctx.stroke(); }
      ctx.restore(); }
    // ventral SCUTES: faint transverse belly plates along the lower edge (clipped)
    if(det>0.52){ ctx.save(); bodyPath(0,0); ctx.clip(); ctx.strokeStyle=hexa(S.belly,0.5); ctx.lineWidth=Math.max(0.4,girth*0.05);
      for(let i=3;i<Math.round(N*0.84);i+=2){ const b=bot[i], p=P[i], nm=NM[i]; ctx.beginPath(); ctx.moveTo(b[0],b[1]); ctx.lineTo(p[0]-nm[0]*WD[i]*0.45, p[1]-nm[1]*WD[i]*0.45); ctx.stroke(); }
      ctx.restore(); }
    // dorsal SCALE stipple: the snake's analog of the mammals' fur / birds' feather stipple -- a fine
    // imbricated texture, smooth far and scaled up close (det-gated, dorsal-biased, thinning to the belly)
    if(det>0.5){ const frs=mulberry(sp.length*61+7); ctx.save(); bodyPath(0,0); ctx.clip(); ctx.lineCap='round';
      const nSc=Math.round(150*det*det);
      for(let j=0;j<nSc;j++){ const s=0.02+frs()*0.94, i=Math.round(s*(N-1)), p=P[i], nm=NM[i], w=WD[i];
        const off=(frs()-0.5)*1.7*w, sx=p[0]+nm[0]*off, sy=p[1]+nm[1]*off, r=Math.max(0.5,w*0.13);
        ctx.strokeStyle = off>-w*0.15 ? hexa(S.col[2]||'#332',0.12) : hexa('#241a10',0.06);   // darker dorsal, faint ventral
        ctx.lineWidth=Math.max(0.3,girth*0.045);
        ctx.beginPath(); ctx.arc(sx, sy, r, 0.15*Math.PI, 0.85*Math.PI); ctx.stroke(); }             // an imbricated scale-edge arc
      ctx.restore(); }
    // ---- head details ----
    if(det>0.36){
      const ci=Math.round(0.93*(N-1)), cp=P[ci], cn=NM[ci], cw=WD[ci];
      const ex=cp[0]+cn[0]*cw*0.34, ey=cp[1]+cn[1]*cw*0.42, er=Math.max(1.1,girth*0.26);
      // mouth line along the lower jaw
      if(det>0.42){ ctx.strokeStyle=hexa(S.col[2]||'#2a2018',0.55); ctx.lineWidth=Math.max(0.5,girth*0.09); ctx.lineCap='round';
        ctx.beginPath(); const a=Math.round(0.855*(N-1)); ctx.moveTo(bot[a][0],bot[a][1]);
        for(let i=a+1;i<N;i++) ctx.lineTo(bot[i][0],bot[i][1]); ctx.stroke(); }
      ctx.fillStyle=S.eyeCol||'#3a3020'; ctx.beginPath(); ctx.arc(ex,ey,er,0,7); ctx.fill();      // iris
      ctx.fillStyle='#0a0805';
      if(S.pupil==='vert'){ ctx.save(); ctx.translate(ex,ey); ctx.scale(0.32,1); ctx.beginPath(); ctx.arc(0,0,er*0.8,0,7); ctx.fill(); ctx.restore(); }
      else { ctx.beginPath(); ctx.arc(ex,ey,er*0.55,0,7); ctx.fill(); }
      if(det>0.5){ ctx.fillStyle='rgba(255,255,255,0.85)'; ctx.beginPath(); ctx.arc(ex-er*0.3,ey-er*0.32,er*0.28,0,7); ctx.fill(); }  // catchlight
      if(det>0.45){ ctx.fillStyle=hexa(S.col[2]||'#333',0.7); const np=P[Math.round(0.985*(N-1))]; ctx.beginPath(); ctx.arc(np[0],np[1]-cw*0.12,Math.max(0.5,girth*0.07),0,7); ctx.fill(); }  // nostril
      // forked TONGUE, flicking intermittently (pose.tongue overrides; else a slow clock)
      const flick = pose.tongue!=null ? pose.tongue : ((Math.sin((pose.clock||0)*4.3)*0.5+0.5) > 0.8 ? 1 : 0);
      if(det>0.44 && flick>0){ const hp=P[N-1], q=P[N-3], t=[hp[0]-q[0],hp[1]-q[1]], tl=Math.hypot(t[0],t[1])||1, ux=t[0]/tl, uy=t[1]/tl;
        const len=girth*1.8*flick, base=[hp[0]+ux*girth*0.15, hp[1]+uy*girth*0.15], mid=[base[0]+ux*len*0.55, base[1]+uy*len*0.55];
        ctx.strokeStyle='#c23a44'; ctx.lineWidth=Math.max(0.5,girth*0.08); ctx.lineCap='round';
        ctx.beginPath(); ctx.moveTo(base[0],base[1]); ctx.lineTo(mid[0],mid[1]); ctx.stroke();
        const px=-uy, py=ux; for(const s of [-1,1]){ ctx.beginPath(); ctx.moveTo(mid[0],mid[1]); ctx.lineTo(mid[0]+ux*len*0.42+px*s*len*0.3, mid[1]+uy*len*0.42+py*s*len*0.3); ctx.stroke(); } }
    }
    ctx.restore();
  }

  return { drawCreature, drawBird, drawSnake, birdLeg, setLight, util: { clamp, ik, foot, mulberry, hexa } };
});