diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html
index 6975f49..c47fa1d 100644
--- a/web/zebra-spaces.html
+++ b/web/zebra-spaces.html
@@ -4618,6 +4618,30 @@ async function sfuPublishCamera(){
'camera open cancelled: '+(e.name||'Error')+' — '+e.message+
' (constraints: 1280x720@30 16:9'+(exact?' deviceId='+String(exact).slice(0,8):'')+')'
);
+ /* Diagnostic snapshot — runs AFTER getUserMedia returned, so the
+ * await doesn't break transient activation for any subsequent
+ * retry. Captures (1) Permissions API state for camera (what
+ * Firefox itself says about the permission, separate from
+ * getUserMedia's return), (2) document visibility (Firefox auto-
+ * denies for hidden documents), (3) user-activation state (gesture
+ * propagation). Lets us tell apart "saved deny" / "global pref
+ * block" / "no gesture" / "hidden doc" without another roundtrip.
+ * Fox ticket 0002. */
+ let permState = 'n/a', visState = 'n/a', activation = 'n/a';
+ try {
+ if (navigator.permissions && navigator.permissions.query){
+ const p = await navigator.permissions.query({ name: 'camera' });
+ permState = p.state;
+ }
+ } catch(_){}
+ try { visState = document.visibilityState || 'n/a'; } catch(_){}
+ try {
+ if (navigator.userActivation){
+ activation = (navigator.userActivation.isActive ? 'active' : 'inactive') +
+ '/hasBeenActive=' + (navigator.userActivation.hasBeenActive ? 1 : 0);
+ }
+ } catch(_){}
+ logLine('', 'camera permission probe: state='+permState+' vis='+visState+' activation='+activation);
/* OverconstrainedError + a saved deviceId is the canonical "saved
* device gone or renamed" case — retry once without deviceId so the
* user gets default camera rather than a permanent fail. */
@@ -8161,8 +8185,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');