diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index 88fc1f1..3f21c53 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -3813,7 +3813,16 @@ $('btn-leave').addEventListener('click', async () => {
try { sessionStorage.removeItem(ACTIVE_CAM_KEY); } catch(_){}
if (sigReconnect){ clearTimeout(sigReconnect); sigReconnect = null; }
for (const u of [...peers.keys()]) tearPeer(u);
- if (ws){ try { ws.close(); } catch(_){} ws = null; }
+ /* send 'bye' BEFORE closing the WS — server distinguishes a strong
+ * leave (user clicked leave / closed tab) from a hiccup disconnect
+ * (refresh, network blip). On bye, the server broadcasts peer-left
+ * immediately; without it, the server defers the broadcast behind an
+ * 8s grace so a refresh can rejoin invisibly. */
+ if (ws){
+ try { ws.send(JSON.stringify({ type: 'bye' })); } catch(_){}
+ try { ws.close(); } catch(_){}
+ ws = null;
+ }
await sfuUnpublish(); await sfuUnpublishScreen(); await sfuUnpublishCamera(); await sfuUnsubscribe();
dropMic();
members.clear(); handraise.clear(); myUUID=''; myRole=''; hostUUID=''; outstandingInvite=null;
@@ -3845,8 +3854,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');