<!-- SOLID CITY · part 2446 -->
ngthSq() < 0.01) return null;
    var pos = bird.group.position, vel = bird.vel;
    var speed = vel.length();
    var horizonM = Math.max(2.2, speed * (lookAheadS || 0.9));   // metres of foresight
    var clearance = bird.clearanceRadius();
    var best = null, bestD = Infinity;
    var steps = Math.max(14, Math.ceil(horizonM / 0.3));         // <=0.3m sampling: nothing threads between samples
    for (var i = 0; i < this.obstacles.length; i++) {
      var obs = this.obstacles[i];
      // skip the obstacle whose top surface is the bird's own destination
      if (bird.intentSurfaceId && obs.topSurfaceId === bird.intentSurfaceId) continue;
      for (var s = 1; s <= steps; s++) {
        var dAlong = (s / steps) * horizonM, tt = dAlong / speed;
        var px = pos.x + vel.x * tt, py = pos.y + vel.y * tt, pz = pos.z + vel.z * tt;
        if (py > obs.topY + 0.35) continue;              // passing safely above
        var d = this.distToObstacle({ x: px, z: pz }, obs);
        if (d < clearance) { if (dAlong < bestD) { bestD = dAlong; best = { obstacle: obs, distAlong: dAlong, atDist: d }; } break; }
      }
    }
    return best;
  };

  /* Is the straight segment a->b clear of all obstacles at this clearance,
   * flying at the segment's own heights? */
  SpatialWorld.prototype.isSegmentClear = function (a, b, clearance) {
    var steps = 16;
    for (var s = 0; s <= steps; s++) {
      var t = s / steps;
      var p = { x: lerp(a.x, b.x, t), y: lerp(a.y, b.y, t), z: lerp(a.z, b.z, t) };
      for (var i = 0; i < this.obstacles.length; i++) {
        var obs = this.obstacles[i];
        if (p.y > obs.topY + 0.3) continue;
        if (this.distToObstacle(p, obs) < clearance) return false;
      }
    }
    return true;
  };

  /* Score one landing surface for one species. Weighted, explainable.
   * Returns {total, size, angle, clearance, stability, kind} or null if
   * the species flatly cannot use it (glass, too steep, too small, gripless
   * branch for a non-gripping species, occupied). */
  SpatialWorld.prototype.scoreSurface = function (bird, surface) {
    var sp = bird.species, suit = sp.surfaceSuitability;
    var kindScore = (suit.kinds[surface.kind] != null) ? suit.kinds[surface.kind] : 0;
    if (kindScore <= 0) return null;
    if (surface.material === 'glass') return null;                    // glass is never a perch
    if (surface.occupiedBy && surface.occupiedBy !== bird) return null;
    if (surface.angleDeg > suit.maxSlopeDeg) return null;              // too steep for this species
    if (surface.radius < suit.minRadiusM) return null;                 // too small to stand on
    if (surface.kind === 'branch' && !suit.gripFeet) return null;      // cannot grip a branch
    var size = clamp((surface.radius - suit.minRadiusM) / 0.55, 0, 1);
    var angle = clamp(1 - surface.angleDeg / 45, 0, 1);
    var clearance = (surface.clearAbove != null) ? clamp(surface.clearAbove / 3, 0, 1) : 0.7;
    var stability = (surface.stability != null) ? surface.stability : 0.8;
    var total = 0.25 * size + 0.20 * angle + 0.20 * clearance + 0.15 * stability + 0.20 * kindScore;
    return { total: total, size: size, angle: angle, clearance: clearance, stability: stability, kind: kindScore };
  };

  /* Find a corridor around an obstacle toward a destination: try both
   * sides, honoring other obstacles and habitat bounds. Returns
   * {via, width, detourRatio} or null. */
  SpatialWorld.prototype.findCorridor = function (bird, obs, destPos) {
    var pos = bird.group.position;
    var clearance = bird.clearanceRadius();
    var need = clearance + 0.7;
    var cx = (obs.kind === 'cylinder') ? obs.x : (obs.minX + obs.maxX) / 2;
    var cz = (obs.kind === 'cylinder') ? obs.z : (obs.minZ + obs.maxZ) / 2;
    var halfW = (obs.kind === 'cylinder') ? obs.r : Math.max(obs.maxX - obs.minX, obs.maxZ - obs.minZ) / 2;
    var dirx = destPos.x - pos.x, dirz = destPos.z - pos.z;
    var dl = Math.sqrt(dirx * dirx + dirz * dirz) || 1; dirx /= dl; dirz /= dl;
    var sides = [1, -1], best = null;
    for (var i = 0; i < 2; i++) {
      var sgn = sides[i];
      var via = V3(cx - dirz * sgn * (halfW + need), Math.max(pos.y, obs.topY + 0.4), cz + dirx * sgn * (halfW + need));
      if (!this.insideBounds(via, 1.5)) continue;
      if (!this.isSegmentClear(pos, via, clearance)) continue;
      if (!this.isSegmentClear(via, destPos, clearance)) continue;
      var detour = (pos.distanceTo(via) + via.distanceTo(destPos)) / Math.max(1, dl);
      var room = 0.8; // lateral room judged by corridor validity + bounds slack
      var cand = { via: via, width: need, detourRatio: detour, room: room, side: sgn };
      if (!best || cand.detourRatio < best.detourRatio) best = cand;
    }
    return best;
  };

  /* THE DECISION. Given a bird, a detected encounter, and its intended
   * destination, weigh AVOID vs LAND vs RETREAT and return the winner
   * with full scoring for the record. Pure functio