<!-- SOLID CITY · part 2451 -->
AnimationController.prototype.applyLegs = function (J, pose, sp) {
    var mode = pose.legMode || 'stand';
    var gp = this.gaitPhase;
    [1, -1].forEach(function (side) {
      var L = side > 0 ? J.legL : J.legR;
      if (mode === 'tucked') {           // flight: legs drawn up/back
        L.hip.rotation.x = -1.15; L.knee.rotation.x = 1.9; L.foot.rotation.x = -0.6;
      } else if (mode === 'extend') {    // landing: feet reaching down-forward
        L.hip.rotation.x = 0.35; L.knee.rotation.x = 0.25; L.foot.rotation.x = 0.3;
      } else if (mode === 'gait') {      // alternating steps, weight transfer
        var ph = gp + (side > 0 ? 0 : Math.PI);
        L.hip.rotation.x = Math.sin(ph) * 0.52;
        L.knee.rotation.x = Math.max(0, -Math.sin(ph)) * 0.75 + 0.05;
        L.foot.rotation.x = -Math.sin(ph) * 0.25;
      } else {                            // stand / grip: planted
        L.hip.rotation.x = 0; L.knee.rotation.x = 0.03; L.foot.rotation.x = 0;
        if (mode === 'grip') L.foot.rotation.x = 0.5;  // toes curled around the branch
      }
    });
    // body bob + roll from gait = visible weight transfer
    if (mode === 'gait') {
      var amp = sp.id === 'dodo' ? 0.030 : 0.008;
      J.body.position.y = sp.proportions.legLen + Math.abs(Math.sin(gp)) * amp;
      J.body.rotation.z += Math.sin(gp) * (sp.id === 'dodo' ? 0.045 : 0.02);
    } else {
      J.body.position.y = sp.proportions.legLen;
    }
    return mode;
  };

  /* ================================================================
   * LocomotionController — flight model (Part III): speed, turn-rate
   * limited steering, banking, flap/glide energy logic; and the ground
   * model: turn, accelerate, gait-coupled movement (no teleporting,
   * speed always matches the stride the AnimationController poses).
   * ================================================================ */
  function LocomotionController(bird) { this.bird = bird; }
  LocomotionController.prototype.steerFlight = function (dt, targetPos, opts) {
    var b = this.bird, sp = b.species, F = sp.flight;
    opts = opts || {};
    var pos = b.group.position;
    var desired = targetPos.clone().sub(pos);
    var dist = desired.length();
    desired.normalize();
    var curDir = b.vel.clone().normalize();
    // turn toward desired at the species' turn rate — axis-angle rotation,
    // stable even for full reversals (a retreat must turn, not loop upward)
    var angle = curDir.angleTo(desired);
    var maxTurn = F.turnRateRad * dt * (opts.turnMul || 1);
    var newDir;
    if (angle < 1e-4) newDir = desired;
    else if (angle <= maxTurn) newDir = desired;
    else {
      var axis = new THREE.Vector3().crossVectors(curDir, desired);
      if (axis.lengthSq() < 1e-6) {           // antiparallel: turn level, around the up-cross axis
        axis = new THREE.Vector3().crossVectors(curDir, new THREE.Vector3(0, 1, 0));
        if (axis.lengthSq() < 1e-6) axis.set(1, 0, 0);
      }
      axis.normalize();
      newDir = curDir.clone().applyAxisAngle(axis, maxTurn);
    }
    // banking from lateral turn demand
    var cross = curDir.clone().cross(newDir);
    b.bank = clamp(lerp(b.bank || 0, clamp(-cross.y * 6, -1, 1) * F.maxBankRad, clamp(dt * 4, 0, 1)), -F.maxBankRad, F.maxBankRad);
    // speed management: approach slow-down, altitude energy exchange
    var targetSpeed = opts.speed || F.cruiseSpeed;
    if (opts.approach) targetSpeed = Math.max(F.approachSpeed, Math.min(F.cruiseSpeed, dist * 0.9));
    var climbing = newDir.y > 0.12;
    var flapWanted = climbing || b.energyDemand > 0.4 || pos.y < (opts.minAlt || 0);
    if (opts.forceGlide) flapWanted = false;
    if (opts.forceFlap) flapWanted = true;
    var accel = flapWanted ? 5.2 : 1.6;
    b.speed += clamp(targetSpeed - b.speed, -3.2 * dt, accel * dt);
    if (!flapWanted) { // glide: sink, trading height for speed maintenance
      newDir.y -= F.glideSinkMps * dt / Math.max(3, b.speed) * 2.2;
      newDir.normalize();
    }
    b.vel.copy(newDir).multiplyScalar(b.speed);
    pos.addScaledVector(b.vel, dt);
    b.flapMode = flapWanted ? 'flap' : 'glide';
    // heading/pitch from velocity
    b.heading = Math.atan2(b.vel.x, b.vel.z);
    b.pitch = Math.asin(clamp(b.vel.y / Math.max(0.001, b.speed), -1, 1));
    return dist;
  };
  LocomotionController.prototype.moveGround = function (dt, targetPos, speedWanted) {
    var b = this.bird, sp = b.species;
    var pos = b.group.position;
    var dx = targetPos.x - pos.x, dz = targetPos.z - pos.z;
    var dist = Math.sqrt(dx * dx + dz * dz);
    if (dist < 0.02) { b.groundSpeed = 0; return 0; }
    var wantHeading = Math.atan2(dx, dz);
    var dh = angDiff(b.heading, wantHeading);
    var turnRate = sp.ground.turnRateRad;
    b.heading += clamp(dh, -turnRate * dt, turnRate * dt);
    var aligned = Math.abs(angDiff(b.heading, wantHeading)) < 0.5;
    var spd = aligned ? Math.min(speedWanted, dist / Math.max(dt, 0.001)) : speedWanted * 