diff --git a/docs/design-system.md b/docs/design-system.md
index 36cde6b2..156fa42f 100644
--- a/docs/design-system.md
+++ b/docs/design-system.md
@@ -329,7 +329,7 @@ Use `.popover` as the relative anchor for floating content. Nest `.popover__pane
```
-Use the shared popover engine (`js/ui/overlay/popoverEngine.js`) instead of manual offsets. The helper attaches panels to the overlay root, flips placement when they collide with the viewport, and toggles `aria` + `data-popover-state` attributes as visibility changes. Width and height clamps can be sourced from design tokens (`maxWidthToken`, `maxHeightToken`) so surfaces inherit theme-specific limits without inline constants. 【F:js/ui/overlay/popoverEngine.js†L252-L266】【F:js/ui/overlay/popoverEngine.js†L633-L706】【F:css/tailwind.source.css†L358-L399】
+Use the shared popover engine (`js/ui/overlay/popoverEngine.js`) instead of manual offsets. The helper attaches panels to the overlay root, flips placement when they collide with the viewport, and toggles `aria` + `data-popover-state` attributes as visibility changes. Width and height clamps can be sourced from design tokens (`maxWidthToken`, `maxHeightToken`) so surfaces inherit theme-specific limits without inline constants. Consult [docs/menus.md](./menus.md) for overlay-root wiring, Tailwind overlay helpers, and the manual QA surface that exercises card and modal menus. 【F:js/ui/overlay/popoverEngine.js†L252-L266】【F:js/ui/overlay/popoverEngine.js†L633-L706】【F:css/tailwind.source.css†L358-L399】
```js
import createPopover from "../overlay/popoverEngine.js";
diff --git a/docs/menus.md b/docs/menus.md
new file mode 100644
index 00000000..d9db32f8
--- /dev/null
+++ b/docs/menus.md
@@ -0,0 +1,157 @@
+# Menu & Popover Integration
+
+Menus, quick actions, and contextual overlays all flow through the shared popover
+engine. This guide documents how to request the overlay root, create and manage
+popovers, and apply the Tailwind overlay helpers that keep menus consistent with
+the design system.
+
+## Request the overlay root
+
+Use `ensureOverlayRoot` before mounting any floating UI so the engine has a
+stable portal target. The helper resolves the right `document` instance and
+appends the root container when it is missing.
+
+```js
+import ensureOverlayRoot from "../js/ui/overlay/overlayRoot.js";
+
+const doc = trigger?.ownerDocument || document;
+const overlayRoot = ensureOverlayRoot(doc);
+
+// overlayRoot is a `
` tagged with `data-overlay-root`.
+// Popovers, sheets, and modals render inside this element.
+```
+
+`ensureOverlayRoot` is idempotent—call it during setup for each controller or
+dev surface without worrying about duplicates.
+
+## Create a popover
+
+`createPopover(trigger, render, options)` wires focus management, aria
+attributes, collision detection, and auto updates. The render callback receives
+a portal `container` inside the overlay root. Return the panel element so the
+engine can toggle `data-popover-state` values while it opens and closes.
+
+```js
+import createPopover from "../js/ui/overlay/popoverEngine.js";
+
+function attachMenu(trigger) {
+ const popover = createPopover(
+ trigger,
+ ({ container, close }) => {
+ const panel = container.ownerDocument.createElement("div");
+ panel.className = "popover__panel menu space-y-1";
+
+ [
+ { label: "Play", action: "play" },
+ { label: "Save to playlist", action: "save" },
+ { label: "Share", action: "share" },
+ ].forEach(({ label, action }) => {
+ const item = container.ownerDocument.createElement("button");
+ item.type = "button";
+ item.className = "menu__item justify-between";
+ item.dataset.action = action;
+ item.textContent = label;
+ item.addEventListener("click", () => close({ restoreFocus: true }));
+ panel.appendChild(item);
+ });
+
+ container.append(panel);
+ return panel;
+ },
+ {
+ document: trigger?.ownerDocument,
+ placement: "bottom-end",
+ restoreFocusOnClose: true,
+ },
+ );
+
+ trigger.addEventListener("click", async (event) => {
+ event.preventDefault();
+ await popover.open();
+ });
+}
+```
+
+Pass `document`, `placement`, and other floating-ui options through the third
+argument. The helper exposes `open()`, `close()`, `toggle()`, `isOpen()`, and
+`destroy()` for lifecycle control.
+
+## Tailwind overlay classes
+
+Tailwind ships token-backed overlay helpers that keep surfaces aligned with the
+palette:
+
+- `popover__panel` / `popover-panel` — rounded overlay panel with opacity and
+ scale transitions. Add `menu` and `menu__item` classes for stacked button
+ menus.
+- `overlay-scrim` and `overlay-panel` — lightweight surfaces for inline overlays
+ and frosted tooltips. Use the `data-strength` / `data-variant` modifiers to
+ switch tones.
+- `ds-overlay-backdrop` — full-screen backdrop helper for async states.
+- `bv-modal`, `bv-modal-backdrop`, and `bv-modal__panel` — modal scaffolding
+ used by upload/edit flows. Mobile breakpoints flatten the panel automatically.
+
+Avoid ad-hoc opacity or blur utilities—let the semantic classes map to overlay
+color tokens so theme swaps stay centralized in
+`css/tailwind.source.css`.
+
+## Video card actions
+
+Video cards lean on `MoreMenuController` to attach a popover to the card's
+"More" button. The controller registers the overlay root, builds the menu panel,
+and opens the popover on demand.
+
+```js
+// js/ui/moreMenuController.js
+const render = this.createPopoverRender(entry);
+const popover = createPopover(trigger, render, {
+ document: documentRef,
+ placement: "bottom-end",
+});
+
+// Inside the render callback (createVideoMoreMenuPanel):
+panel.className = "popover__panel menu space-y-1";
+button.className = "menu__item";
+```
+
+Each menu button dispatches `video:context-action` events so cards, lists, and
+player overlays can respond without duplicating menu markup.
+
+## Modal actions
+
+`VideoModal` follows the same pattern for its in-modal menu and zap actions. It
+requests the overlay root from the modal's `ownerDocument`, builds a
+`popover__panel`, and delegates action handling back to the controller.
+
+```js
+// js/ui/components/VideoModal.js
+const render = ({ document: doc, close }) => {
+ const panel = this.buildModalMoreMenuPanel({ document: doc, close });
+ return panel;
+};
+
+const popover = createPopover(this.modalMoreBtn, render, {
+ document: documentRef,
+ placement: "bottom-end",
+ restoreFocusOnClose: true,
+});
+```
+
+Modal action buttons reuse the same `menu__item` styling and call `close()` to
+collapse the overlay before dispatching the selected action.
+
+## Manual QA surface
+
+A lightweight QA harness lives at `views/dev/popover-demo.html`. Launch the
+static dev server from the repository root:
+
+```bash
+python -m http.server 8000
+# or
+npx serve
+```
+
+Then open `http://localhost:8000/views/dev/popover-demo.html` to interact with a
+sample video card and modal wired to the popover engine. The page ensures the
+overlay root exists, demonstrates Tailwind overlay helpers, and exercises both
+card and modal action menus without relying on production data.
diff --git a/docs/qa.md b/docs/qa.md
index ef6f1bbc..f57861cf 100644
--- a/docs/qa.md
+++ b/docs/qa.md
@@ -4,7 +4,7 @@ Run this checklist before publishing releases or merging changes that touch uplo
> **Note:** Align every UI styling review with the token-first rules in [`AGENTS.md`](../AGENTS.md) and the [README CSS build pipeline](../README.md#css-build-pipeline). When QA covers styling changes, run `npm run lint:css` (and any related build/test scripts) to catch regressions early.
-Before starting, open `docs/kitchen-sink.html` in a browser to review the modal surface reference. With that page as your visual baseline, perform this quick accessibility spot check in the running app:
+Before starting, open `docs/kitchen-sink.html` in a browser to review the modal surface reference. Spin up the static dev server (`python -m http.server 8000` or `npx serve`) and load `views/dev/popover-demo.html` to sanity-check overlay menus following [docs/menus.md](./menus.md). With those pages as your visual baseline, perform this quick accessibility spot check in the running app:
Automated regression coverage now exercises these flows on the unified design-system variant:
diff --git a/views/dev/popover-demo.html b/views/dev/popover-demo.html
new file mode 100644
index 00000000..8564aca0
--- /dev/null
+++ b/views/dev/popover-demo.html
@@ -0,0 +1,285 @@
+
+
+
+
+
+
+ Popover & Menu Demo • bitvid
+
+
+
+
+
+
Popover & Menu Demo
+
+ This manual QA page mounts a sample video card and modal so you can verify
+ overlay menus with the shared popover engine. Review the implementation notes in
+ docs/menus.md
+ for architectural guidance.
+
+
+
+
+
Video card
+
+
+
+
+
+ Preview thumbnail
+
+
+
+
+
+
+
Understanding overlay primitives
+
+ Tap the more button to open an actions menu rendered through
+ createPopover.
+
+
+
+
+
+
+
+
+
Modal actions
+
+ Launch the demo modal to test menus layered above a backdrop.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Modal context menu demo
+
+
+ Use the menu button to confirm popovers behave while a modal is open.
+
+
+
+
+
+
+ The popover below targets the modal action trigger. Confirm focus returns to the
+ trigger after closing the menu and that the modal backdrop remains intact.
+
+
+
+
+
QA checklist
+
+
Open the card and modal menus and verify they render inside the overlay root.
+
Resize the viewport; menus should reposition without clipping their trigger.
+
Confirm focus is restored to the trigger when each menu closes.