<!-- SOLID CITY · part 3609 -->
       }
        speakReply(reply, function (speechMs) {
          d.speech = speechMs;
          d.total = Date.now() - turnT0;
          turnData.push(d); turnCard(turn, d);
          log('turn ' + turn + ' complete in ' + d.total + 'ms');
          setTimeout(runTurn, 800);
        });
      });
    });
  }

  $('start').addEventListener('click', function () {
    var btn = $('start');
    btn.disabled = true;
    log('fetching session…');
    fetch('/api/city/me', { credentials: 'same-origin', cache: 'no-store' })
      .then(function (r) { return r.json(); })
      .then(function (j) {
        csrf = (j && j.csrf) || '';
        if (!j || !j.loggedIn) { log('not logged in — brain calls may fail'); }
        else log('session ok');
        running = true; turn = 0; turnData = [];
        btn.style.display = 'none';
        $('stop').style.display = 'block';
        runTurn();
      })
      .catch(function (e) { log('session fetch failed: ' + e.message); btn.disabled = false; });
  });

  $('stop').addEventListener('click', function () {
    log('stopping test');
    finish();
  });

  document.addEventListener('visibilitychange', function () {
    log('page ' + (document.hidden ? 'HIDDEN' : 'VISIBLE'));
  });
})();
</script>
</body>
</html>


===== FILE: public/city/viewport-dbg.js =====
/* Viewport diagnostics — READ-ONLY instrumentation for the portrait-to-wide defect.
   Ordered by GPT-6 2026-10-11. This file OBSERVES ONLY: it never writes layout,
   never reloads, never changes behavior. Activate the on-screen panel with ?vpdbg=1.
   Every capture also goes to the console as [vpdbg]. */
(function () {
  'use strict';
  var LOG = [], MAX = 40, panel = null, pre = null;
  var ACTIVE = /[?&]vpdbg=1/.test(location.search);

  function num(n) { return Math.round((n || 0) * 10) / 10; }

  function findOverflow() {
    var out = [];
    try {
      var vw = (window.visualViewport && window.visualViewport.width) || window.innerWidth || 0;
      var els = document.querySelectorAll('body *');
      for (var i = 0; i < els.length && out.length < 8 && i < 900; i++) {
        var el = els[i];
        if (!el.getBoundingClientRect) continue;
        var cs;
        try { cs = getComputedStyle(el); } catch (e2) { continue; }
        if (cs.display === 'none' || cs.visibility === 'hidden') continue;
        var r = el.getBoundingClientRect();
        if (r.width <= 0 && r.height <= 0) continue;
        if (r.right > vw + 2 || r.left < -2) {
          var cls = (typeof el.className === 'string' ? el.className.split(' ')[0] : '');
          out.push(el.tagName.toLowerCase() + (el.id ? '#' + el.id : '') + (cls ? '.' + cls : '') +
            ' L' + Math.round(r.left) + ' R' + Math.round(r.right) + ' W' + Math.round(r.width));
        }
      }
    } catch (e) {}
    return out;
  }

  function capture(why) {
    try {
      var de = document.documentElement, vv = window.visualViewport || null;
      var e = {
        t: new Date().toISOString().slice(11, 23), why: why,
        layoutW: de.clientWidth, layoutH: de.clientHeight,
        innerW: window.innerWidth, innerH: window.innerHeight,
        vvW: vv ? num(vv.width) : null, vvH: vv ? num(vv.height) : null,
        vvScale: vv ? num(vv.scale) : null,
        scrW: window.screen ? window.screen.width : null,
        scrH: window.screen ? window.screen.height : null,
        orient: (window.screen && window.screen.orientation && window.screen.orientation.type) || null,
        dpr: window.devicePixelRatio || 1,
        docScrollW: de.scrollWidth,
        bodyScrollW: document.body ? document.body.scrollWidth : null,
        bootHidden: (function () { var b = document.getElementById('sc-boot'); return b ? !!b.hidden : null; })(),
        overflow: findOverflow()
      };
      LOG.push(e); if (LOG.length > MAX) LOG.shift();
      try { console.log('[vpdbg][' + why + '] ' + JSON.stringify(e)); } catch (e2) {}
      paint();
    } catch (e3) {}
  }

  function verdict(e) {
    if (e.docScrollW > e.layoutW + 4) return 'CHILD OVERFLOW -> ' + (e.overflow[0] || 'unidentified');
    if (e.vvW != null && Math.abs(e.layoutW - e.vvW) > 60) return 'STALE LAYOUT VIEWPORT (layout ' + e.layoutW + ' vs visual ' + e.vvW + ')';
    if (e.vvScale != null && Math.abs(e.vvScale - 1) > 0.05) return 'PAGE ZOOMED x' + e.vvScale;
    return 'healthy';
  }

  function buildPanel() {
    if (!ACTIVE || panel) return;
    panel = document.createElement('div');
    panel.id = 'vpdbg-panel';
    panel.setAttribute('style', 'position:fixed;left:8px;right:8px;bottom:8px;z-index:99999;max-height:38vh;overflow:auto;background:rgba(0,0,0,.92);border:2px solid #e8c36a;border-radius:10px;padding:8px;font:11px/1.5 monospace;color:#bff8ff;white-space:pre-wrap;');
    pre = document.createElement('div');
    var btn = document.createElement('button');
    btn.textContent = 'COPY REPORT';
    btn.setAttribute('style', 'margin-top:6px;background:#e8c36a;color:#000;border:non