diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index d9dbf41..2965e30 100755
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -48,6 +48,15 @@ export const metadata: Metadata = {
},
};
+// PRD §5.6: suppressHydrationWarning on html + body because we use:
+// 1. next-themes (class on changes client-side)
+// 2. Zustand persist (view/locale loaded from localStorage client-side)
+// 3. Radix UI generates IDs with useId() which differ between server and client
+// (this is expected and the warning is cosmetic — Radix components still work correctly)
+// The alternative would be to delay rendering until after hydration (mount gate),
+// but that causes a flash of unstyled content. suppressHydrationWarning is the
+// official next-themes + Radix pattern.
+
export default function RootLayout({
children,
}: Readonly<{
@@ -57,6 +66,7 @@ export default function RootLayout({
{children}
diff --git a/src/components/aos/app-shell.tsx b/src/components/aos/app-shell.tsx
index ec5d2f3..1568f39 100755
--- a/src/components/aos/app-shell.tsx
+++ b/src/components/aos/app-shell.tsx
@@ -1,5 +1,6 @@
'use client'
+import { useState, useEffect } from 'react'
import { useAos } from '@/store/aos'
import { SidebarNav } from './sidebar-nav'
import { TopBar } from './top-bar'
@@ -36,10 +37,32 @@ import { MobileBottomNav } from './mobile-bottom-nav'
export function AppShell() {
const { view, sidebarOpen, setSidebarOpen, portalMode } = useAos()
+ const [mounted, setMounted] = useState(false)
+
+ // Hydration gate: wait for client mount before rendering interactive UI
+ // This prevents Radix UI useId() mismatches between server and client
+ useEffect(() => {
+ // eslint-disable-next-line react-hooks/set-state-in-effect
+ setMounted(true)
+ }, [])
// Portal mode renders a completely separate experience
if (portalMode) return
+ // Before mount: render a minimal skeleton to prevent hydration mismatch
+ if (!mounted) {
+ return (
+
+ )
+ }
+
return (
{/* Desktop sidebar */}