diff --git a/frontend/src/components/Carousel.tsx b/frontend/src/components/Carousel.tsx
index 31834026..46f4affa 100644
--- a/frontend/src/components/Carousel.tsx
+++ b/frontend/src/components/Carousel.tsx
@@ -77,8 +77,10 @@ export function Carousel({
const label = ariaLabel ?? (typeof title === "string" ? title : undefined);
+ // Mouse-only: hidden until the carousel is hovered or the arrow is keyboard-focused; `reveal-hover`
+ // removes it on touch, where the strip scrolls by swipe — so an invisible edge button never eats a tap.
const arrowBtn =
- "absolute top-1/2 -translate-y-1/2 z-10 grid h-9 w-9 place-items-center rounded-full " +
+ "reveal-hover absolute top-1/2 -translate-y-1/2 z-10 grid h-9 w-9 place-items-center rounded-full " +
"bg-black/55 text-white backdrop-blur-xs border border-white/15 shadow-lg transition " +
"opacity-0 group-hover/car:opacity-100 focus-visible:opacity-100 hover:bg-black/75";
diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/components/PlayerModal.tsx
index 77adbcab..67ca30ad 100644
--- a/frontend/src/components/PlayerModal.tsx
+++ b/frontend/src/components/PlayerModal.tsx
@@ -31,7 +31,7 @@ import {
import Avatar from "./Avatar";
import AddToPlaylist from "./AddToPlaylist";
import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover";
-import { pushLayer, removeLayer, anyLayerAbove } from "../lib/layerStack";
+import { pushLayer, removeLayer, anyLayerAbove, escapeHandled } from "../lib/layerStack";
import DownloadButton from "./DownloadButton";
import { api, type Video, type VideoStatus } from "../lib/api";
import type { YTPlayer } from "../lib/youtube";
@@ -337,7 +337,10 @@ export default function PlayerModal({
if (e.key === "Escape") {
// Defer to any layer opened above the player FIRST (a Modal like the download dialog, or an
// anchored popover): it takes the Escape and we do nothing — so we don't also step out of
- // maximise or close the player underneath it.
+ // maximise or close the player underneath it. The consumed-event flag is the reliable signal
+ // (UX-8): the layer's `document` listener runs before ours and its close can pop it off the
+ // stack synchronously, so `anyLayerAbove` alone would miss it and close the player too.
+ if (escapeHandled(e)) return;
if (layerIdRef.current != null && anyLayerAbove(layerIdRef.current)) return;
// In fullscreen, let the browser's Esc exit fullscreen only — don't also close the modal.
if (fullscreenElement()) return;
diff --git a/frontend/src/components/PlexPlayer.tsx b/frontend/src/components/PlexPlayer.tsx
index 1fe3eef3..76a7d8de 100644
--- a/frontend/src/components/PlexPlayer.tsx
+++ b/frontend/src/components/PlexPlayer.tsx
@@ -53,7 +53,7 @@ import { LS, useAccountPersistedObject } from "../lib/storage";
import { useDismiss } from "../lib/useDismiss";
import { useScrollFade } from "../lib/useScrollFade";
import { useBackToClose } from "../lib/history";
-import { pushLayer, removeLayer, anyLayerAbove } from "../lib/layerStack";
+import { pushLayer, removeLayer, anyLayerAbove, escapeHandled } from "../lib/layerStack";
// The rich info overlay (poster/cast/ratings) reuses the same component as the card's info page.
const PlexInfo = lazy(() => import("./PlexInfo"));
@@ -602,7 +602,10 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
case "Escape":
// Defer to any layer opened above the player (Modal or anchored popover) — let it take the
// Escape, not us. A player MENU registers as a layer too, so it closes first and the next
- // Escape reaches the auto-skip-cancel below (the old blanket-stopPropagation bug).
+ // Escape reaches the auto-skip-cancel below (the old blanket-stopPropagation bug). Check the
+ // consumed-event flag first (UX-8): a layer's `document` listener runs before this `window`
+ // one and its close can pop it off the stack synchronously, so `anyLayerAbove` alone misses it.
+ if (escapeHandled(e)) break;
if (layerIdRef.current != null && anyLayerAbove(layerIdRef.current)) break;
if (skipProgressRef.current != null) cancelAutoSkipRef.current();
else if (menuOpenRef.current) {
diff --git a/frontend/src/components/VideoCard.tsx b/frontend/src/components/VideoCard.tsx
index f4b40dab..e502722c 100644
--- a/frontend/src/components/VideoCard.tsx
+++ b/frontend/src/components/VideoCard.tsx
@@ -262,9 +262,12 @@ function Thumb({