diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index 0eb70ac..9dc788f 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -4785,16 +4785,22 @@ document.addEventListener('visibilitychange', () => {
* Tiny WAV header + 1 sample of silence — the smallest valid PCM
* audio resource a browser will accept. Base64 ~80 bytes. */
const SILENCE_WAV = 'data:audio/wav;base64,UklGRiYAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQIAAAAAAA==';
-let audioPrimed = false;
+/* audioPrimed gate REMOVED — every entry click should re-prime even
+ * after a successful first prime. Mobile browsers can suspend the
+ * audio session on tab background / leave; the next entry click needs
+ * a fresh play() to re-bless audio for the new session. */
function primeAudioOnGesture(){
- if (audioPrimed) return;
const a = document.createElement('audio');
a.src = SILENCE_WAV; a.muted = false; a.volume = 0;
a.style.display = 'none';
document.body.appendChild(a);
const p = a.play();
- if (p && p.then) p.then(() => { audioPrimed = true; })
- .catch(()=>{ /* still try the rest of join */ });
+ if (p && p.then) p.then(() => {
+ /* keep the element around briefly so the play() actually emits a
+ * frame; then yank it from the DOM to avoid a growing pile of
+ * primer elements after many entry clicks. */
+ setTimeout(() => { try { a.remove(); } catch(_){} }, 500);
+ }).catch(()=>{ try { a.remove(); } catch(_){} });
/* Also try resuming any existing AudioContext (meter / chime).
* resume() on an already-running context is a no-op, and on a
* suspended one it transitions to running — granting Web Audio
@@ -4860,6 +4866,19 @@ $('btn-leave').addEventListener('click', async () => {
try { sessionStorage.removeItem(ACTIVE_CALL_KEY); } catch(_){}
try { sessionStorage.removeItem(ACTIVE_CAM_KEY); } catch(_){}
if (sigReconnect){ clearTimeout(sigReconnect); sigReconnect = null; }
+ /* tear down ALL DJ-mode HTTP streams + their