diff --git a/frontend/src/components/ChatDock.tsx b/frontend/src/components/ChatDock.tsx
index 6304563..21e6c6e 100644
--- a/frontend/src/components/ChatDock.tsx
+++ b/frontend/src/components/ChatDock.tsx
@@ -7,6 +7,7 @@ import { onMessage, onUnread } from "../lib/messagesSocket";
import * as e2ee from "../lib/e2ee";
import Avatar from "./Avatar";
import ChatThread from "./ChatThread";
+import Overlay from "./Overlay";
// The Messenger-style floating chat dock: open conversations live here, bottom-right, across
// page navigation (state persisted per user). Always mounted (for human users) so it also owns
@@ -51,11 +52,13 @@ export default function ChatDock({ meId }: { meId: number }) {
if (chats.length === 0) return null;
return (
-
- {chats.map((c) => (
-
- ))}
-
+
+
+ {chats.map((c) => (
+
+ ))}
+
+
);
}
@@ -65,7 +68,7 @@ function ChatWindow({ chat, meId }: { chat: DockChat; meId: number }) {
{/* One-shot attention flash on a new message; keyed by the counter so it replays each time. */}
{chat.flash > 0 && (
-
+
)}
+
);
}
diff --git a/frontend/src/components/OnboardingWizard.tsx b/frontend/src/components/OnboardingWizard.tsx
index 2a7a8d7..e8ef79e 100644
--- a/frontend/src/components/OnboardingWizard.tsx
+++ b/frontend/src/components/OnboardingWizard.tsx
@@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AlertTriangle, Check, Loader2, RefreshCw, ShieldCheck, X, Youtube } from "lucide-react";
import { api, type Me } from "../lib/api";
import { beginGrant, endOnboarding } from "../lib/onboarding";
+import Overlay from "./Overlay";
// A first-login wizard that walks the user through granting YouTube access one scope at a
// time, each with a plain explanation and an up-front heads-up about Google's "unverified
@@ -122,7 +123,8 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
const stepIndex = step === "read" ? 0 : step === "write" ? 1 : 2;
return (
-
+
+
+
);
}
diff --git a/frontend/src/components/Overlay.tsx b/frontend/src/components/Overlay.tsx
new file mode 100644
index 0000000..41730ab
--- /dev/null
+++ b/frontend/src/components/Overlay.tsx
@@ -0,0 +1,14 @@
+import { type ReactNode } from "react";
+import { createPortal } from "react-dom";
+
+// The app's overlay-root convention. Any full-screen or floating overlay renders THROUGH this so it
+// escapes whatever ancestor stacking context it was declared in — a `mask-image`/`transform`/`filter`
+// on the page scroller (or a rail's `backdrop-filter`) would otherwise trap a `fixed` child and let
+// the chrome paint over it. That is the exact class of bug that buried the players in E3; the
+// modal/player family already dodges it by portaling to , and this makes that the one blessed
+// way. Stacking among overlays is then decided by DOM order + the z-index token scale (tailwind
+// `zIndex`), never by an ad-hoc magic number. Portals to (the existing de-facto target).
+export default function Overlay({ children }: { children: ReactNode }) {
+ if (typeof document === "undefined") return null;
+ return createPortal(children, document.body);
+}