diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index dcf8661..dc6dbde 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -3012,12 +3012,29 @@ async function handleSignal(raw){
renderShareUrl();
onRoleEntered();
renderRoom();
+ /* Ask the room to re-broadcast their current mic-state so our
+ * member list shows the right mute icons. Without this, mm.muted
+ * defaults to undefined and the UI reports every peer as
+ * 'open' until they happen to toggle. Fires for hiccup-takeover
+ * too (since the welcome path runs on every reconnect). */
+ try { send({ type: 'mic-state-req' }); } catch(_){}
break;
case 'state':
roomEpoch = m.epoch; applyState(m.state); flushSfuStreams(); renderRoom(); break;
+ case 'mic-state-req':
+ /* Another peer just (re)joined and asked the room to re-announce.
+ * Only speakers with a live mic respond — listeners have no
+ * meaningful mic-state to share. */
+ if (canSpeak(myRole) && micStream) sendMicState();
+ break;
case 'peer-joined':
members.set(m.uuid, { uuid:m.uuid, pubkey:m.pubkey, handle:m.handle, role:m.role, joined_at: Date.now()/1000 });
{ const pub = pubHexFromMsg(m, 'pubkey'); logLine('', idTag(m.uuid, pub)+' joined as '+m.role); }
+ /* re-announce our mic state so the new arrival's UI reflects
+ * reality. Server-side mic-state-req covers welcome timing; this
+ * covers the standard 'a new peer joined while we were already
+ * here' case. */
+ if (canSpeak(myRole) && micStream) sendMicState();
/* if the original host rejoined during their grace window, the room
* is rescued — drop the "space closing" status from our top bar */
if (m.role === 'host'){
@@ -3915,8 +3932,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');