diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index c33b982..62db2e4 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -2945,6 +2945,18 @@ function playToneRaise(){
/* single A5 bell-ish tone — light, attention-getting */
playToneBlip(880.0, 260, 'triangle');
}
+function playToneLeave(){
+ /* E5 → C5 descending major third — mirror of playToneJoin, conveys
+ * 'departed'. Fires on peer-left + peer-booted. */
+ playToneBlip(659.25, 140, 'sine');
+ setTimeout(() => playToneBlip(523.25, 200, 'sine'), 120);
+}
+function playToneStepDown(){
+ /* single G4 low blip — softer than join/leave, conveys 'still here
+ * but stepped off the stage'. Fires on canSpeak -> listener role
+ * transitions for other peers. */
+ playToneBlip(392.0, 220, 'sine');
+}
/* ==================================================================
* room state mirror (server is source of truth, we mirror locally for
@@ -3164,6 +3176,9 @@ async function handleSignal(raw){
const left = members.get(m.uuid);
const pub = pubHexFromMsg(m, 'pubkey') || (left && left.pubkey ? pubHexFromMsg({pubkey:left.pubkey}, 'pubkey') : '');
if (left || pub) logLine('', idTag(m.uuid, pub)+' left');
+ /* doorman chime — someone actually left (post hiccup-grace).
+ * server never sends peer-left for self so no self-filter. */
+ playToneLeave();
/* if they were sharing screen or camera, drop the tiles */
if (left && left.pubkey){
try { const h = hex(unb64(left.pubkey)); removeScreenTile(h); removeCameraTile(h); } catch(_){}
@@ -3293,6 +3308,9 @@ async function handleSignal(raw){
removeCameraTile(subjPub);
removeVideoTile('gameshare', subjPub);
} catch(_){}
+ /* 'stepped off the stage' chime — they're still in the
+ * room but lost mic privileges. */
+ playToneStepDown();
}
}
renderRoom();
@@ -3308,6 +3326,8 @@ async function handleSignal(raw){
|| (mm && mm.pubkey ? pubHexFromMsg({pubkey:mm.pubkey},'pubkey') : '');
const byPub = pubHexFromMsg(m, 'by_pubkey');
logLine('', idTag(m.uuid, victPub)+' was removed by '+(by||byPub?idTag(m.by, byPub):'a mod'));
+ /* same departure chime as peer-left — they're gone either way. */
+ if (m.uuid !== myUUID) playToneLeave();
if (m.uuid === myUUID){
/* server will close our socket; surface a clear notice and prevent
* the WS reconnect loop from auto-rejoining into a boot loop. Tear
@@ -4097,8 +4117,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');