<!-- SOLID CITY · part 3755 -->
-2)+':'+('0'+t.getSeconds()).slice(-2);
    if (first) { logEl.textContent = ''; first = false; }
    logEl.textContent += '[' + stamp + '] ' + msg + '\n';
    logEl.scrollTop = 1e9;
  }
  function set(id, v) { $(id).textContent = v; }

  var stream = null, pc1 = null, pc2 = null, analyser = null, audioCtx = null, raf = 0;

  function meter() {
    if (!analyser) return;
    var d = new Uint8Array(analyser.fftSize);
    analyser.getByteTimeDomainData(d);
    var peak = 0;
    for (var i = 0; i < d.length; i++) { var v = Math.abs(d[i] - 128) / 128; if (v > peak) peak = v; }
    $('meterfill').style.width = Math.min(100, peak * 220) + '%';
    raf = requestAnimationFrame(meter);
  }

  $('start').addEventListener('click', function () {
    var btn = $('start');
    btn.disabled = true;
    log('requesting microphone…');
    navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true } }).then(function (st) {
      stream = st;
      set('s-mic', 'live');
      log('mic live: ' + st.getAudioTracks().length + ' track(s)');
      st.getAudioTracks()[0].addEventListener('ended', function () { log('mic track ENDED'); set('s-mic', 'ended'); });
      st.getAudioTracks()[0].addEventListener('mute', function () { log('mic track MUTED'); });

      // Level meter only — no recording anywhere.
      try {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        var src = audioCtx.createMediaStreamSource(st);
        analyser = audioCtx.createAnalyser(); analyser.fftSize = 512;
        src.connect(analyser);
        meter();
      } catch (e) { log('meter unavailable: ' + e.message); }

      // True WebRTC audio path: two peer connections, signaled in-page.
      // Real encode/decode; the only shortcut is no network between them.
      pc1 = new RTCPeerConnection();
      pc2 = new RTCPeerConnection();
      pc1.addEventListener('connectionstatechange', function () { set('s-pc', pc1.connectionState); log('pc1: ' + pc1.connectionState); });
      pc2.addEventListener('connectionstatechange', function () { log('pc2: ' + pc2.connectionState); });
      pc2.addEventListener('track', function (ev) {
        log('remote track received');
        set('s-track', 'receiving');
        var a = $('remote');
        a.srcObject = ev.streams[0];
        ev.track.addEventListener('ended', function () { log('remote track ENDED'); set('s-track', 'ended'); });
        ev.track.addEventListener('mute', function () { log('remote track MUTED'); });
        var p = a.play();
        if (p && p.catch) p.catch(function (e) { log('play blocked: ' + e.name); });
      });
      st.getTracks().forEach(function (t) { pc1.addTrack(t, st); });
      pc1.addEventListener('icecandidate', function (e) { if (e.candidate) pc2.addIceCandidate(e.candidate).catch(function(){}); });
      pc2.addEventListener('icecandidate', function (e) { if (e.candidate) pc1.addIceCandidate(e.candidate).catch(function(){}); });

      pc1.createOffer().then(function (offer) {
        return pc1.setLocalDescription(offer).then(function () { return pc2.setRemoteDescription(offer); });
      }).then(function () { return pc2.createAnswer(); })
      .then(function (answer) {
        return pc2.setLocalDescription(answer).then(function () { return pc1.setRemoteDescription(answer); });
      }).then(function () {
        log('loopback established — speak now');
        btn.style.display = 'none';
        $('stop').style.display = 'block';
      }).catch(function (e) { log('setup failed: ' + e.message); btn.disabled = false; });
    }).catch(function (e) {
      log('mic denied/blocked: ' + e.name);
      btn.disabled = false;
    });
  });

  $('stop').addEventListener('click', function () {
    log('stopping test');
    try { cancelAnimationFrame(raf); } catch (e) {}
    try { if (audioCtx) audioCtx.close(); } catch (e) {}
    try { if (pc1) pc1.close(); } catch (e) {}
    try { if (pc2) pc2.close(); } catch (e) {}
    try { if (stream) stream.getTracks().forEach(function (t) { t.stop(); }); } catch (e) {}
    set('s-mic', 'stopped'); set('s-pc', 'closed'); set('s-track', 'stopped');
    $('meterfill').style.width = '0%';
    $('stop').style.display = 'none';
    var b = $('start'); b.style.display = 'block'; b.disabled = false;
  });

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


===== FILE: public/city/workshop/index.html =====
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
  <meta name="color-scheme" content="light dark" />
  <link rel="icon" href="data:," />
  <title>AI Workshop</title>
  <style>
    @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Manrope:wght@400;500;600;700;800&display=swap');
    :root{
      color-scheme:light d