diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html
index ab5456b..231fd54 100644
--- a/zebra-report/zebra-spaces.html
+++ b/zebra-report/zebra-spaces.html
@@ -2531,10 +2531,17 @@ function handleRemoteSfuTrack(ev){
for (const [uuid, mm] of members){
try {
if (mm.pubkey && hex(unb64(mm.pubkey)) === pubHex){
- /* speakers get their peers' audio via mesh (lower latency) —
- * skip the duplicate SFU mic. screens + cameras still came
- * through above. */
- if (canSpeak(myRole) && peers.has(uuid)) return;
+ /* speakers get their peers' audio via mesh (lower latency)
+ * AT THE TIMES THE MESH PC IS CONNECTED. A stale or failing
+ * mesh PC must NOT block the SFU fallback — that's how the
+ * 'phone re-promoted but nobody hears them' regression
+ * appeared: the peers map still had an entry whose state
+ * was 'failed', so we skipped SFU and the receiver got no
+ * audio at all. */
+ if (canSpeak(myRole)){
+ const meshPC = peers.get(uuid);
+ if (meshPC && meshPC.connectionState === 'connected') return;
+ }
attachSfuTrack(uuid, ev.streams[0]);
return;
}
@@ -3433,6 +3440,11 @@ async function connectToPeer(uuid, weOffer){
if (pc.connectionState === 'failed' && peers.get(uuid) === pc){
logLine('', 'peer '+uuid+' failed — reconnecting');
tearPeer(uuid);
+ /* Mesh PC just died; the audio element for this peer was bound to
+ * the dying mesh stream and won't recover on its own. Switch back
+ * to the cached SFU stream so the user keeps hearing them while
+ * mesh reconnect attempts run in the background. */
+ try { attachCachedSfuStreamFor(uuid); } catch(_){}
/* let the offerer drive recovery */
setTimeout(()=>{ if (members.has(uuid) && canSpeak(members.get(uuid).role) && canSpeak(myRole))
connectToPeer(uuid, myUUID < uuid); }, 1500);
@@ -4010,8 +4022,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');