<!-- SOLID CITY · part 2450 -->
wL.shoulder.position.set(P.bodyRad * 1.15, P.bodyRad * 1.15, P.bodyLen * 0.16);
      wR.shoulder.position.set(-P.bodyRad * 1.15, P.bodyRad * 1.15, P.bodyLen * 0.16);
      body.add(wL.shoulder); body.add(wR.shoulder);
      joints.wingL = wL; joints.wingR = wR;
    }

    // legs
    var thick = sp.id === 'dodo' ? 0.055 : 0.011;
    var legSpread = sp.id === 'dodo' ? 0.115 : 0.030;
    var lL = buildLeg(1, sp, P, thick), lR = buildLeg(-1, sp, P, thick);
    lL.hip.position.set(legSpread, P.legLen, P.bodyLen * 0.06);
    lR.hip.position.set(-legSpread, P.legLen, P.bodyLen * 0.06);
    group.add(lL.hip); group.add(lR.hip);
    joints.legL = lL; joints.legR = lR;

    group.traverse(function (o) { o.frustumCulled = false; });
    return { group: group, joints: joints };
  }

  /* ================================================================
   * AnimationController — turns controller intent into joint motion.
   * Owns: wing fold (NEVER instant), flap cycle with wrist lag,
   * feather spread, head look smoothing, tail balance, gait posing,
   * breathing, blinking, grip. Pose targets come from the state
   * machine; this controller only eases and applies.
   * ================================================================ */
  function AnimationController(bird) {
    this.bird = bird;
    this.fold = 0;              // 0 folded tight, 1 fully extended
    this.flapPhase = 0;
    this.flapping = false;
    this.glideBlend = 0;        // 0 flap .. 1 glide pose
    this.headYaw = 0; this.headPitch = 0;
    this.tailPitch = 0;
    this.gaitPhase = 0;
    this.blinkT = 1.5 + Math.random() * 2;
    this.blinkPhase = -1;       // >=0 while a blink is running
    this.breathHz = bird.species.id === 'dodo' ? 0.28 : 0.55;
  }
  AnimationController.prototype.update = function (dt, t) {
    var b = this.bird, J = b.model.joints, sp = b.species;
    var pose = b.pose;
    // --- fold: eased toward target at a physical rate (no instant folding)
    var foldRate = 1 / 0.55;
    var df = pose.foldTarget - this.fold;
    var step = clamp(df, -foldRate * dt, foldRate * dt);
    this.fold += step;
    // --- flap
    if (pose.flapActive && this.fold > 0.82) {
      this.flapping = true;
      this.flapPhase += dt * sp.flight.wingbeatHz * Math.PI * 2 * (pose.flapRateMul || 1);
    } else this.flapping = false;
    this.glideBlend += clamp((pose.glide ? 1 : 0) - this.glideBlend, -dt * 3, dt * 3);
    var flapAmp = (1 - this.glideBlend) * (pose.flapAmp != null ? pose.flapAmp : 1);
    var flap = Math.sin(this.flapPhase) * 1.02 * flapAmp;
    var wristFlap = Math.sin(this.flapPhase - 0.85) * 0.55 * flapAmp;
    // --- head look (independent of torso — Part II)
    this.headYaw = angLerp(this.headYaw, pose.headYaw || 0, clamp(dt * 7, 0, 1));
    this.headPitch = lerp(this.headPitch, pose.headPitch || 0, clamp(dt * 7, 0, 1));
    this.tailPitch = lerp(this.tailPitch, pose.tailPitch || 0, clamp(dt * 5, 0, 1));
    // --- gait
    if (pose.gaitSpeed > 0.01) this.gaitPhase += dt * (pose.gaitSpeed / sp.ground.strideLengthM) * Math.PI;
    // --- blink (eyelids live in the hi-detail group; harmless if absent)
    this.blinkT -= dt;
    if (this.blinkT <= 0 && this.blinkPhase < 0) { this.blinkPhase = 0; this.blinkT = 2.2 + b.rng() * 3.6; }
    var lid = 0;
    if (this.blinkPhase >= 0) {
      this.blinkPhase += dt / 0.16;
      if (this.blinkPhase >= 1) this.blinkPhase = -1;
      else lid = Math.sin(this.blinkPhase * Math.PI);
    }
    // --- breathing (subtle, always — the bird is alive at rest)
    var breath = 1 + Math.sin(t * this.breathHz * Math.PI * 2) * 0.016;

    /* apply */
    var fold = this.fold;
    J.body.scale.set(breath, 1, breath);
    J.body.rotation.x = pose.bodyPitch || 0;
    J.body.rotation.z = (pose.bodyRoll || 0);
    // wings: fold = sweep back + wrist Z-fold; extension = span out; flap rides on top
    if (sp.capabilities.canFly) {
      [1, -1].forEach(function (side) {
        var W = side > 0 ? J.wingL : J.wingR;
        var sweep = (1 - fold) * side * 1.22;                 // folded: swept back along body
        W.shoulder.rotation.y = sweep + (pose.wingYaw || 0) * side;
        W.shoulder.rotation.z = side * (fold * 0.12 + flap * fold - (1 - fold) * 0.18);
        W.wrist.rotation.y = (1 - fold) * side * 1.85;        // Z-fold of the outer wing
        W.wrist.rotation.z = side * (wristFlap * fold + fold * 0.06);
      });
    }
    // head + neck
    J.neck.rotation.y = this.headYaw * 0.35;
    J.head.rotation.y = this.headYaw * 0.75;
    J.head.rotation.x = this.headPitch;
    // tail: balance + flight pitch control surface
    J.tail.rotation.x = this.tailPitch + (pose.tailBase || 0);
    // eyelids (groups with origin at the top of the eyeball: scale.y grows the lid downward over the eye)
    if (J.lids) for (var li = 0; li < J.lids.length; li++) J.lids[li].scale.y = 0.10 + lid * 1.05;
    // legs
    var lp = this.applyLegs(J, pose, sp);
    return lp;
  };
  