diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html
index e955888..f2325e6 100644
--- a/zebra-report/zebra-spaces.html
+++ b/zebra-report/zebra-spaces.html
@@ -1627,17 +1627,17 @@ const sfuStreamsByPubHex = new Map(); // pubHex -> MediaStream
function attachSfuTrack(uuid, stream){
let a = remoteAudio.get(uuid);
if (!a){
- a = document.createElement('audio'); a.autoplay = true;
- a.playsInline = true;
- document.body.appendChild(a); remoteAudio.set(uuid, a);
+ /* lease from the pre-blessed audio pool so Firefox Android's
+ * per-element autoplay grant carries over from the entry-button
+ * gesture. attachSfuTrack runs after several async hops (WS join,
+ * SFU subscribe negotiation, ontrack) so the click activation
+ * window has expired by now — only a pre-played pool element
+ * still has the engagement bit set. */
+ a = leaseAudioElement();
+ remoteAudio.set(uuid, a);
applySinkTo(a);
}
a.srcObject = stream;
- /* programmatic play in case autoplay policy needs the nudge after a track
- * swap — caller already had a user gesture (entered the space). On Firefox
- * Android the play() promise will reject if the gesture grace has aged
- * out; the listener's play button re-runs play() inside its own gesture
- * to recover. */
try {
const p = a.play();
if (p && p.catch) p.catch(e => logLine('err','rtc autoplay '+uuid+': '+e.message));
@@ -4743,23 +4743,65 @@ const SILENCE_WAV = 'data:audio/wav;base64,UklGRiYAAABXQVZFZm10IBAAAAABAAEARKwAA
* 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. */
+/* Pool of audio elements pre-blessed for autoplay by the user gesture
+ * (entry button click). Firefox Android grants media autoplay
+ * permission PER ELEMENT, not per document — so attachSfuTrack creating
+ * a fresh