diff --git a/src/app/InterfaceSettings.tsx b/src/app/InterfaceSettings.tsx index 37134f3..860f96c 100644 --- a/src/app/InterfaceSettings.tsx +++ b/src/app/InterfaceSettings.tsx @@ -220,12 +220,12 @@ export function InterfaceSettings({ snapshot }: { snapshot: ControlSnapshot }): set("reducedMotion")(!next)} /> p:not(.overline) { margin-top: 1rem; } } -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; } -} +/* NOTE: no global prefers-reduced-motion override here on purpose. The + site setting (.reduce-interface-motion above) is the single source of + truth and it is initialized from the OS preference, so an explicit + "Enable animations" choice is never overridden by the OS. */ /* Monochrome workspace — selected from the concept-18 exploration. */ body { height: 100vh; overflow: hidden; } @@ -2409,10 +2410,8 @@ body { height: 100vh; overflow: hidden; } from { max-height: 5rem; opacity: 1; } to { max-height: 0; padding-block: 0; opacity: 0; transform: translateY(-.2rem); } } -@media (prefers-reduced-motion: reduce) { - .toast { animation: none; } - .toast.is-leaving { animation-duration: .01s; } -} +.reduce-interface-motion .toast { animation: none; } +.reduce-interface-motion .toast.is-leaving { animation-duration: .01s; } .control-shell[data-interface-theme="liquid-glass"] .toast-stack { border-color: var(--glass-border-elevated); diff --git a/src/interface-preferences.ts b/src/interface-preferences.ts index a605ac9..6b71e2b 100644 --- a/src/interface-preferences.ts +++ b/src/interface-preferences.ts @@ -53,20 +53,34 @@ function clampGlassIntensity(value: unknown): number { return Math.min(100, Math.max(0, Math.round(number))); } +/** OS-level preference. Used only as the initial default so an explicit + site choice always wins over the system setting. */ +export function systemPrefersReducedMotion(): boolean { + try { + if (typeof window !== "undefined" && typeof window.matchMedia === "function") { + return window.matchMedia("(prefers-reduced-motion: reduce)").matches; + } + } catch { + // Ignore and fall through to the animated default below. + } + return false; +} + export function loadInterfacePreferences(storage: Storage): InterfacePreferences { try { const saved = JSON.parse(storage.getItem(STORAGE_KEY) ?? "{}") as Partial; return { theme: THEMES.includes(saved.theme as InterfaceTheme) ? saved.theme as InterfaceTheme : "Mono", colorMode: saved.colorMode === "Light" || saved.colorMode === "Dark" ? saved.colorMode : "System", - reducedMotion: saved.reducedMotion === true, + reducedMotion: + typeof saved.reducedMotion === "boolean" ? saved.reducedMotion : systemPrefersReducedMotion(), expandSections: saved.expandSections === true, showExperimental: saved.showExperimental !== false, instantFlash: saved.instantFlash === true, glassIntensity: clampGlassIntensity(saved.glassIntensity), }; } catch { - return { ...DEFAULT_INTERFACE_PREFERENCES }; + return { ...DEFAULT_INTERFACE_PREFERENCES, reducedMotion: systemPrefersReducedMotion() }; } }