<!-- SOLID CITY · part 2444 -->
cording of its voice exists';
  var dec = bird.decisions && bird.decisions[bird.decisions.length - 1];
  L.panel.querySelector('#lw-dec').textContent = dec ? ('Last obstacle decision: ' + dec.action + ' — ' + dec.reason) : 'No obstacle decisions yet.';
  L.panel.querySelector('#lw-zlabel').textContent = L.zoom.toFixed(1) + '×';
}
function capture() {
  if (!L.renderer || !M.group) return;
  renderScope();   // a fresh render of the ACTUAL scene at this instant
  var E = Eng(); if (!E) return;
  var src = L.renderer.domElement, sc = Math.min(1, 880 / src.width);
  var c = document.createElement('canvas'); c.width = Math.round(src.width * sc); c.height = Math.round(src.height * sc);
  c.getContext('2d').drawImage(src, 0, 0, c.width, c.height);
  // subject: the bird nearest the centre of the frame
  var T = A.T, best = null, bestD = 1e9;
  M.group.userData.birds.forEach(function (b) {
    var v = birdRenderPos(b, new T.Vector3()).project(L.scopeCam);
    if (v.z > 1) return;
    var d = Math.hypot(v.x, v.y);
    if (d < bestD) { bestD = d; best = { bird: b, inFrame: Math.abs(v.x) <= 1 && Math.abs(v.y) <= 1 }; }
  });
  var label = 'Lost Wings Grove', note = 'The grove, through the lens.';
  if (best && best.inFrame) {
    var d = E.Observation.describe(best.bird);
    label = best.bird.species.commonName + ' (' + best.bird.species.latin + ')';
    note = d.text;
  }
  L.photos.unshift({ img: c.toDataURL('image/jpeg', 0.86), label: label, note: note });
  if (L.photos.length > 12) L.photos.pop();
  L.panel.querySelector('#lw-photos').textContent = 'PHOTOS (' + L.photos.length + ')';
  L.canvas.style.filter = 'brightness(2.2)'; setTimeout(function () { L.canvas.style.filter = ''; }, 90);
}
function showGallery() {
  var list = L.gallery.querySelector('#lw-glist');
  list.innerHTML = L.photos.length ? '' : '<p style="max-width:520px;margin:0 auto">No photos yet. Frame a bird in the lens and capture it.</p>';
  L.photos.forEach(function (ph) {
    var f = el('figure', 'lw-photo');
    var img = el('img'); img.src = ph.img; f.appendChild(img);
    f.appendChild(el('figcaption', null, '<b>' + ph.label + '</b><br>' + ph.note));
    list.appendChild(f);
  });
  L.gallery.classList.add('on');
}

/* ---------------- load & per-frame -------------------------------------- */
function load() {
  M.state = 'loading';
  M.baseY = (A.heightAt ? A.heightAt(POS.x, POS.z) : 22) + 0.6;
  var p; try { p = import('/city/lost-wings/lost-wings-grove.js?v=20261012'); } catch (e) { p = Promise.reject(e); }
  p.then(function (mod) {
    var make = mod.createGrove || mod.default;
    var g = make({ THREE: A.T });
    g.position.set(POS.x, M.baseY, POS.z);
    A.world.add(g); M.group = g; M.state = 'ready';
    collide(g.userData || {}); hyperStop(); watchStop();
    try { console.info('[city] Lost Wings Grove ready at (' + POS.x + ',' + POS.z + ') baseY ' + M.baseY.toFixed(1)); } catch (e) {}
  }).catch(function (e) { M.state = 'failed'; try { console.warn('[city] Lost Wings Grove skipped:', e && e.message); } catch (x) {} });
}
function update(dt) {
  if (M.state === 'idle') { if (A.isBuilt && !A.isBuilt()) return; M.wait -= dt; if (M.wait <= 0) load(); return; }
  if (M.state !== 'ready' || !M.group || document.hidden) return;
  try { M.group.userData.update(S.t, Math.min(dt, 0.05), A); } catch (e) {}
  var c = A.camera.position, og = A.origin || { x: 0, z: 0 };
  var camd = Math.hypot(c.x + og.x - POS.x, c.z + og.z - POS.z);
  M.group.visible = camd < 9000;
  var pd = Math.hypot(S.p.x - POS.x, S.p.z - POS.z);
  if (S.mode === 'walk' && pd < 170 && !M.said) {
    M.said = true;
    if (window.CityHyper && CityHyper.narrate) CityHyper.narrate('<b>Lost Wings Grove</b> — two birds the world lost, living again. The parakeet is in the great tree; the dodo is in the meadow. Watch them quietly.', 8);
  } else if (pd > 400) M.said = false;
  // the lens button lives near the grove; the panel works while you watch
  try {
    if (pd < 75 || M.panelOpen) { buildLens(); L.btn.classList.add('on'); }
    else if (L.btn) L.btn.classList.remove('on');
    if (M.panelOpen) {
      L.zoom += (L.targetZoom - L.zoom) * Math.min(1, dt * 7);
      M.group.userData.setLens(L.zoom);
      if (S.t - L.lastRender > 0.033) { L.lastRender = S.t; renderScope(); }
      if (S.t - L.lastPanel > 0.2) { L.lastPanel = S.t; refreshPanel(); }
    }
  } catch (e) {}
}
function radar(g, blip, fl) {
  if (M.state !== 'ready') return;
  blip(POS.x, POS.z, '#58a83c', fl ? 3 : 6, fl ? '' : 'WINGS');
  if (!M.group) return;
  var birds = M.group.userData.birds || [];
  if (birds[0]) blip(POS.x + birds[0].group.position.x, POS.z + birds[0].group.position.z, '#7ed957', 2.5, '');
  if (birds[1]) blip(POS.x + birds[1].group.position.x, POS.z + birds[1].group.position.z, '#c9b458', 2.5, '');
}
window.CityLostWings = {
  update: update, radar: radar, pos: POS, standY: standY,
  state: function () { return M.state; }, group: function () { return M.g