diff --git a/zebra-report/zebra-spaces.html b/zebra-report/zebra-spaces.html
index f5b802f..2b158d5 100644
--- a/zebra-report/zebra-spaces.html
+++ b/zebra-report/zebra-spaces.html
@@ -3272,7 +3272,14 @@ function attachAudioStreamViaWorklet(uuid, stream, targetSeconds){
return false;
}
if (!audioCtx){
- try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
+ try {
+ audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+ /* Route to picked output sink on first creation — the prime-on-
+ * gesture path already does this, but attach can also be the
+ * first user of audioCtx on auto-rejoin / desktop-resume paths
+ * where no gesture fired. fox 2026-06-06 blanka-chrome silence. */
+ applySinkToAudioCtx();
+ }
catch(e){ logLine('err','audioCtx create: '+e.message); return false; }
}
if (audioCtx.state === 'suspended'){
@@ -3357,7 +3364,8 @@ function attachAudioStreamViaWorklet(uuid, stream, targetSeconds){
if (transcribeEnabled){
startCaptureForUuid(uuid).catch(e => logLine('err', 'transcribe '+uuid.slice(0,4)+': '+e.message));
}
- logLine('', 'audio via AudioContext '+uuid.slice(0,4)+' target='+targetSeconds+'s ctxState='+audioCtx.state);
+ logLine('', 'audio via AudioContext '+uuid.slice(0,4)+' target='+targetSeconds+'s ctxState='+audioCtx.state
+ + ' sink=' + (audioCtx.sinkId === undefined ? 'n/a' : (audioCtx.sinkId || 'default')));
return true;
}
@@ -5109,10 +5117,36 @@ async function applySinkTo(el){
try { await el.setSinkId(speakerDeviceId || ''); }
catch(e){ logLine('err','setSinkId rejected: '+e.message); }
}
-/* Re-route every live remote-audio element to the newly picked sink. */
+/* AudioContext-level sink routing (Chrome ≥110, Firefox ≥116). Every
+ * listener / speaker audio path now runs through the worklet feeding
+ * audioCtx.destination — if the user picked a non-default speaker in
+ * the UI but we only apply it to