diff --git a/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte b/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte index de7100e..aea355e 100644 --- a/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte +++ b/apps/dashboard/src/lib/observatory/ObservatoryStage.svelte @@ -96,6 +96,32 @@ let projectionDays = $state(0); let liveBridge: LiveBridge | null = null; let liveDecayReady = $state(false); + + // Motion control (WCAG): the field's ambient orbit/sim drift runs >5s, so it + // needs a persistent pause control AND must honor prefers-reduced-motion. + // When paused the clock freezes (drift stops) but live event pulses still + // land — they are information, not decoration. Auto-pauses under + // reduced-motion; the user can still override with the button. + let paused = $state(false); + let userSetPause = $state(false); + function initReducedMotion() { + if (typeof window === 'undefined') return; + const mq = window.matchMedia('(prefers-reduced-motion: reduce)'); + if (mq.matches && !userSetPause) paused = true; + const onChange = (e: MediaQueryListEvent) => { + if (!userSetPause) paused = e.matches; + }; + mq.addEventListener('change', onChange); + return () => mq.removeEventListener('change', onChange); + } + function togglePause() { + userSetPause = true; + paused = !paused; + } + // Push the pause state to the engine whenever either changes. + $effect(() => { + engine?.setPaused(paused); + }); // Live contradiction-firewall verdict — set when a real MemorySuppressed / // contradiction event quarantines a memory on camera. Held ~7s then fades. let liveFirewallLabel = $state(''); @@ -354,6 +380,7 @@ onMount(() => { loadGraph(); + return initReducedMotion(); }); @@ -409,6 +436,23 @@ {/if} + + {#if live} + + {/if} +