diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index 6bd0736..b28d625 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -2911,15 +2911,23 @@ async function sfuSubscribe(){
* a hard refresh handles the WS side; this handles the SFU side. */
pc.onconnectionstatechange = () => {
/* 'failed' is terminal ICE failure (we should rebuild).
- * 'closed' is OUR OWN sfuUnsubscribe() — never rebuild on that
- * (Chrome fires the state change synchronously before sfuSubPC is
- * null'd, which used to trigger a subscribe→close→subscribe loop
- * every time anyone deliberately tore down the sub).
+ * 'closed' coming from our OWN sfuUnsubscribe() must NOT rebuild
+ * (subscribe→close→subscribe loop). sfuUnsubscribe nulls
+ * sfuSubPC BEFORE pc.close(), so the `sfuSubPC === pc` guard
+ * catches self-teardown — the guard fails and we early-return.
+ * 'closed' coming from the SFU (server-side close — e.g. our
+ * own wedge-recovery in renegotiateLocked) DOES need a rebuild
+ * because sfuSubPC === pc is still true (we didn't tear down).
+ * Without this branch the host's sub stays at sub=none after
+ * any server-driven close, every subsequent track add goes
+ * nowhere. Fox 2026-06-04: "lost cohost camera / screen
+ * share" was downstream of this missing rebuild.
* 'disconnected' is transient — let WebRTC try to recover before
- * we yank the rug. */
- if (pc.connectionState !== 'failed') return;
+ * we yank the rug. */
+ const s = pc.connectionState;
+ if (s !== 'failed' && s !== 'closed') return;
if (sfuSubPC !== pc) return;
- logLine('err', 'sfu sub PC failed — rebuilding');
+ logLine('err', 'sfu sub PC '+s+' (remote-driven) — rebuilding');
sfuUnsubscribe().then(() => {
if (wantConnected && roomID) sfuSubscribe().catch(e => logLine('err','sfu re-subscribe: '+e.message));
});
@@ -5568,8 +5576,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');