<!-- SOLID CITY · part 2448 -->
branches — grip surfaces (branch tops)
        { id: 'branch-1', kind: 'branch', obstacleId: 'tree-trunk', center: V3(6.9, 3.55, -5.2), radius: 0.55, angleDeg: 4, clearAbove: 2.6, stability: 0.85, material: 'wood' },
        { id: 'branch-2', kind: 'branch', obstacleId: 'tree-trunk', center: V3(9.3, 4.05, -6.9), radius: 0.5, angleDeg: 5, clearAbove: 2.8, stability: 0.85, material: 'wood' },
        { id: 'branch-3', kind: 'branch', obstacleId: 'tree-trunk', center: V3(7.6, 2.95, -7.3), radius: 0.45, angleDeg: 6, clearAbove: 2.2, stability: 0.8, material: 'wood' },
        // flat stone top of the pillar — the King's landable structure
        { id: 'pillar-top', kind: 'rock', obstacleId: 'pillar', center: V3(0, 3.42, 2), radius: 1.05, angleDeg: 0, clearAbove: 4.0, stability: 1.0, material: 'stone' },
        // snag top — small, less stable: variety for the scorer
        { id: 'snag-top', kind: 'ledge', obstacleId: 'snag', center: V3(-10, 3.12, -8), radius: 0.30, angleDeg: 2, clearAbove: 3.5, stability: 0.55, material: 'wood' },
        // the barn roof — registered so the evaluation must judge and reject it (55 deg)
        { id: 'barn-roof', kind: 'rooftop', obstacleId: 'barn', center: V3(-9, 5.35, 11), radius: 3.2, angleDeg: 55, clearAbove: 4.0, stability: 0.9, material: 'metal' }
      ],
      groundSpots: [
        { id: 'near-meadow', kind: 'meadow', pos: V3(3.5, 0, 7.5) },
        { id: 'far-meadow', kind: 'meadow', pos: V3(-13, 0, 16.5) },
        { id: 'forage-patch', kind: 'forage', pos: V3(1.5, 0, -3.5) },
        { id: 'open-ground', kind: 'meadow', pos: V3(-4, 0, -1) }
      ],
      meta: {
        tree: { x: 8, z: -6, trunkR: 0.42, trunkH: 4.6 },
        pillar: { x: 0, z: 2, r: 0.9, h: 3.4, capR: 1.08 },
        snag: { x: -10, z: -8, r: 0.22, h: 3.1 },
        barn: { minX: -16, maxX: -2, minZ: 9, maxZ: 13, wallH: 3.0, roofH: 2.2 },
        observation: { x: -3.5, z: -10.5 },
        foragePatch: { x: 1.5, z: -3.5, r: 3.2 }
      }
    };
    return new SpatialWorld(def);
  }

  /* ================================================================
   * ModelFactory — articulated birds from shared geometry/materials.
   * Origin = the feet (contact point), so "feet floating" is testable:
   * in any resting state, group.position.y == surface height.
   * Forward = +Z. Joints: neck, head, tail, wing{L,R}{shoulder,wrist},
   * leg{L,R}{hip,knee,foot}.
   * ================================================================ */
  var geoCache = {}, matCache = {};
  function geo(key, make) { if (!geoCache[key]) geoCache[key] = make(); return geoCache[key]; }
  function mat(color) {
    var k = 'm' + color;
    if (!matCache[k]) matCache[k] = new THREE.MeshLambertMaterial({ color: color });
    return matCache[k];
  }
  function mesh(g, m, x, y, z) {
    var ms = new THREE.Mesh(g, m);
    if (x != null) ms.position.set(x, y, z);
    return ms;
  }
  var SPHERE = function () { return geo('sphere', function () { return new THREE.SphereGeometry(1, 14, 11); }); };
  var BOX = function () { return geo('box', function () { return new THREE.BoxGeometry(1, 1, 1); }); };
  var CONE = function () { return geo('cone', function () { return new THREE.ConeGeometry(1, 1, 9); }); };
  var CYL = function () { return geo('cyl', function () { return new THREE.CylinderGeometry(1, 1, 1, 8); }); };

  function buildWing(side, sp, P) {
    // shoulder group at body side; inner wing extends outward, wrist folds it back
    var shoulder = new THREE.Group();
    var innerLen = sp.size.wingspanM * 0.30, outerLen = sp.size.wingspanM * 0.24;
    var inner = mesh(BOX(), mat(sp.colors.wing), side * innerLen / 2, 0, -0.01);
    inner.scale.set(innerLen, 0.012, P.bodyLen * 0.62);
    shoulder.add(inner);
    var wrist = new THREE.Group();
    wrist.position.set(side * innerLen, 0, 0);
    var outer = mesh(BOX(), mat(sp.colors.wing), side * outerLen / 2, 0, -0.03);
    outer.scale.set(outerLen, 0.010, P.bodyLen * 0.45);
    wrist.add(outer);
    // primary feather tips: three thin planes fanning from the wrist end
    for (var i = 0; i < 3; i++) {
      var f = mesh(BOX(), mat(sp.colors.wingTip), side * (outerLen + 0.02), 0, -0.05 - i * 0.028);
      f.scale.set(outerLen * 0.55, 0.006, 0.020);
      f.rotation.y = side * (0.10 + i * 0.10);
      wrist.add(f);
    }
    shoulder.add(wrist);
    return { shoulder: shoulder, wrist: wrist };
  }

  function buildLeg(side, sp, P, thick) {
    var hip = new THREE.Group();
    var upper = mesh(CYL(), mat(sp.colors.legs), 0, -P.legLen * 0.25, 0);
    upper.scale.set(thick, P.legLen * 0.5, thick);
    hip.add(upper);
    var knee = new THREE.Group(); knee.position.set(0, -P.legLen * 0.5, 0);
    var lower = mesh(CYL(), mat(sp.colors.legs), 0, -P.legLen * 0.25, 0);
    lower.scale.set(thick * 0.75, P.legLen * 0.5, thick * 0.75);
    knee.add(lower);
    var foot = new THREE.Group(); foot.position.set(0, -P.legLen * 0.5, 0);
    // three toes forward, one back
 