diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html
index cc98b09..28430e6 100644
--- a/zebra-report/zebra-spaces.html
+++ b/zebra-report/zebra-spaces.html
@@ -1996,6 +1996,23 @@ async function sfuSubscribe(){
});
if (!ackRes.ok){ pc.close(); throw new Error('sfu subscribe-answer http '+ackRes.status); }
sfuSubPC = pc; sfuSubPeerID = offer.peer_id;
+ /* survive laptop-lid-close / network suspend. When the OS suspends the
+ * browser the SFU sub PC goes 'disconnected' first (transient) then
+ * 'failed'. On 'failed' the existing PC is dead — RTP can never
+ * recover even if we resume — so we tear it down and rebuild via
+ * sfuUnsubscribe + sfuSubscribe. The auto-rejoin code that follows
+ * a hard refresh handles the WS side; this handles the SFU side. */
+ pc.onconnectionstatechange = () => {
+ const s = pc.connectionState;
+ if (s === 'failed' || s === 'closed'){
+ logLine('err', 'sfu sub PC ' + s + ' — rebuilding');
+ if (sfuSubPC === pc){
+ sfuUnsubscribe().then(() => {
+ if (wantConnected && roomID) sfuSubscribe().catch(e => logLine('err','sfu re-subscribe: '+e.message));
+ });
+ }
+ }
+ };
/* SSE: server pushes renegotiation offers when publisher set changes.
* We answer each via POST /answer. ping events are keepalive only.
*
@@ -3128,6 +3145,25 @@ if (navigator.mediaDevices && navigator.mediaDevices.addEventListener){
* sees how many inputs exist. */
refreshMicList();
+/* laptop-lid-close / sleep / suspend recovery: when the tab comes back
+ * to visible, check whether our SFU sub PC is still in a healthy state.
+ * Some browsers (Chromium on Linux specifically) don't fire
+ * connectionstatechange on suspend → resume — the PC sits silently in
+ * 'connected' but no RTP flows. If it's in any non-live state, force
+ * the rebuild. */
+document.addEventListener('visibilitychange', () => {
+ if (document.visibilityState !== 'visible') return;
+ if (!wantConnected || !sfuSubPC) return;
+ const s = sfuSubPC.connectionState;
+ if (s === 'failed' || s === 'closed' || s === 'disconnected'){
+ logLine('err', 'visibility back, sub PC ' + s + ' — rebuilding');
+ const pc = sfuSubPC;
+ sfuUnsubscribe().then(() => {
+ if (wantConnected && roomID) sfuSubscribe().catch(e => logLine('err','sfu re-subscribe: '+e.message));
+ });
+ }
+});
+
/* ==================================================================
* leave / entry buttons
* ================================================================== */
@@ -3226,8 +3262,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');