diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index 8867285..77bd97d 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -7365,6 +7365,34 @@ $('rdv-code').addEventListener('keydown', e=>{
if(e.key==='Enter'){ e.preventDefault(); primeAudioOnGesture(); joinSpace(); }
});
+/* Desktop auto-rejoin (below) reaches joinSpace() without a click, so
+ * the entry-button gesture never fires. Chrome (Fedora especially)
+ * leaves audioCtx suspended until a real user gesture, and the
+ * fire-and-forget audioCtx.resume() inside attachAudioStreamViaWorklet
+ * silently fails outside a gesture → no audio, no waveform, no
+ * mic-open indicator. Fox 2026-06-05: "speaker on firefox chrome was
+ * hard restarted and cannot hear host."
+ *
+ * Document-wide one-shot listener: on first interaction anywhere (any
+ * click, any keydown), prime audio. Idempotent — primeAudioOnGesture()
+ * is safe to re-run because it checks `audioCtx` and only resumes if
+ * suspended. Unregisters itself after firing so we don't repeatedly
+ * spawn silent oscillators. */
+(function installFirstGesturePrimer(){
+ let primed = false;
+ function prime(){
+ if (primed) return;
+ primed = true;
+ try { primeAudioOnGesture(); } catch(e){ logLine('err','first-gesture prime: '+e.message); }
+ document.removeEventListener('click', prime, true);
+ document.removeEventListener('keydown', prime, true);
+ document.removeEventListener('touchstart', prime, true);
+ }
+ document.addEventListener('click', prime, true);
+ document.addEventListener('keydown', prime, true);
+ document.addEventListener('touchstart', prime, true);
+})();
+
/* ?code=… autofills the rendezvous code (used by the share URL). The code
* stays in the URL so a refresh keeps you in the same space; if you want
* to leave it cleanly, hit the leave button or close the tab. */
@@ -7504,8 +7532,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');