import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { createPortal } from "react-dom";
import { useNavigationActions } from "./NavigationProvider";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { qk } from "../lib/queryKeys";
import {
  AlertTriangle,
  ArrowLeft,
  Check,
  CheckCheck,
  ChevronLeft,
  ChevronRight,
  ExternalLink,
  Maximize,
  Minimize,
  Minimize2,
  Repeat,
  Repeat1,
  Settings,
  Shuffle,
  SkipBack,
  SkipForward,
  Volume2,
  VolumeX,
  X,
} from "lucide-react";
import Avatar from "./Avatar";
import AddToPlaylist from "./AddToPlaylist";
import { PopoverSurface, useAnchoredPopover } from "./AnchoredPopover";
import { pushLayer, removeLayer, anyLayerAbove } from "../lib/layerStack";
import DownloadButton from "./DownloadButton";
import { api, type Video, type VideoStatus } from "../lib/api";
import type { YTNamespace, YTPlayer, YTPlayerEvent } from "../lib/youtube";
import {
  channelYouTubeUrl,
  formatDate,
  formatDuration,
  formatViews,
  relativeTime,
} from "../lib/format";
import { renderDescription } from "../lib/descriptionLinks";
import {
  exitFullscreen,
  fullscreenElement,
  onFullscreenChange,
  requestFullscreen,
} from "../lib/fullscreen";
import { useBackToClose, useBackToExit } from "../lib/history";
import { DEFAULT_VOLUME, normalizeVolume, stepVolume } from "../lib/playerVolume";
import { LS, readAccountMerged, writeAccount } from "../lib/storage";
import { useScrollFade } from "../lib/useScrollFade";

// Experiment (branch experiment/inline-player): play the video in-app via the
// YouTube IFrame Player API (not a bare embed) so we can read playback position
// and resume where the user left off. The modal closes via the in-card Close
// button, the backdrop, or ESC (ESC only while focus is on our page, not inside
// the cross-origin player iframe — a browser security boundary we can't cross).

// How close to the end (seconds) counts as "finished" → auto-mark watched.
const FINISH_MARGIN = 10;

// Force a high starting resolution. YouTube decides quality at player-CREATION time from the
// iframe's INTERNAL viewport size, then latches it. A windowed-size embed (~900×500) is stuck at
// ~360–480p, which is why the video used to open low and needed a manual fullscreen + gear trip to
// 1080p.
//
// Two phases (see applyHdPhase1 / switchToHdNative / the quality poll):
//   1. Create the iframe at this large FIXED logical size and CSS `transform: scale()` it DOWN to fit
//      the visible box. A CSS transform does NOT change the iframe's internal viewport, so YouTube
//      still sees 1920×1080 and latches 1080p — even while it paints small. (Its own controls, incl.
//      the gear, also paint scaled-down in the windowed box — the one downside of phase 1 alone.)
//   2. Once 1080p has actually latched, drop to NATIVE responsive sizing (setSize) so YouTube renders
//      its controls at full size.
//
// ⚠️ CONDITIONAL / UNDER OBSERVATION (2026-07-31): phase 2 is NOT proven safe everywhere. In dev, on
// ONE `was_live` stream, quality dropped hd1080 → large (480p) ~2s after going native (YouTube
// re-caps to the now-small internal viewport); on the author's own machine/connection it HELD 1080p
// on a normal VOD. So it's environment-/video-dependent and is being watched in prod. Two levers:
//   • HD_DROP_TO_NATIVE = false → skip phase 2, keep phase 1 forever = GUARANTEED 1080p (the viewport
//     stays 1920), at the cost of the scaled-down controls. The safe fallback if phase 2 downgrades.
//   • FORCE_HD_LOGICAL_SIZE = false → disable the whole trick, back to responsive 100%/100% sizing.
// 16:9 to match the `aspect-video` stage.
const FORCE_HD_LOGICAL_SIZE = true;
const HD_DROP_TO_NATIVE = true;
const HD_W = 1920;
const HD_H = 1080;

// Persistent playback settings (stored in users.preferences). Auto-advance = what plays when a
// video ends; loop = whether it repeats the current video ("one"), wraps the list at its ends
// ("all"), or neither ("off"). Both apply to any queued player (feed or playlist).
type AutoMode = "off" | "next" | "prev" | "random";
type LoopMode = "off" | "one" | "all";
const AUTO_MODES: AutoMode[] = ["off", "next", "prev", "random"];
const LOOP_MODES: LoopMode[] = ["off", "one", "all"];

/** The queue transport cluster — prev / counter / next, then the auto-advance and loop mode toggles.
 *  Rendered in two places with identical logic: the modal's queue bar (`variant:"modal"`, normal glass
 *  theme, labels always shown) and the big-view docked control bar (`variant:"docked"`, white-on-dark,
 *  labels hidden until a breakpoint). Only the theme + label-visibility differ; the behaviour is one,
 *  so a mode/icon change lands once. The wrapper (fragment vs. bordered row) stays at the call site. */
function PlayerTransportControls({
  variant,
  index,
  queueLength,
  autoMode,
  loopMode,
  onPrev,
  onNext,
  onCycleAuto,
  onCycleLoop,
}: {
  variant: "docked" | "modal";
  index: number;
  queueLength: number;
  autoMode: AutoMode;
  loopMode: LoopMode;
  onPrev: () => void;
  onNext: () => void;
  onCycleAuto: () => void;
  onCycleLoop: () => void;
}) {
  const { t } = useTranslation();
  const docked = variant === "docked";
  const navBtn = docked
    ? "inline-flex items-center gap-1 text-white/80 enabled:hover:text-white disabled:opacity-30 transition"
    : "inline-flex items-center gap-1.5 text-muted enabled:hover:text-fg disabled:opacity-40 transition";
  const navLabel = docked ? "hidden sm:inline" : "";
  const counter = docked ? "tabular-nums text-white/70" : "text-muted tabular-nums";
  const divider = docked ? "h-4 w-px bg-white/20" : "w-px h-4 bg-border";
  const toggle = (on: boolean) =>
    docked
      ? `inline-flex items-center gap-1 rounded-lg px-2 py-1 transition ${
          on ? "bg-white/20 text-white" : "text-white/70 hover:bg-white/10 hover:text-white"
        }`
      : `inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1 transition ${
          on ? "bg-accent/15 text-accent" : "text-muted hover:bg-surface hover:text-fg"
        }`;
  const toggleLabel = docked ? "hidden md:inline" : "";
  return (
    <>
      <button
        onClick={onPrev}
        disabled={index === 0}
        title={`${t("player.previous")} · Shift+←`}
        className={navBtn}
      >
        <SkipBack className="h-4 w-4" />
        <span className={navLabel}>{t("player.previous")}</span>
      </button>
      <span className={counter}>
        {index + 1} / {queueLength}
      </span>
      <button
        onClick={onNext}
        disabled={index === queueLength - 1}
        title={`${t("player.next")} · Shift+→`}
        className={navBtn}
      >
        <span className={navLabel}>{t("player.next")}</span>
        <SkipForward className="h-4 w-4" />
      </button>
      <span className={divider} />
      {/* Persistent playback settings — cycle on click, saved to your account. */}
      <button
        onClick={onCycleAuto}
        title={t("player.autoAdvance.hint")}
        className={toggle(autoMode !== "off")}
      >
        {autoMode === "prev" ? (
          <SkipBack className="h-3.5 w-3.5" />
        ) : autoMode === "random" ? (
          <Shuffle className="h-3.5 w-3.5" />
        ) : (
          <SkipForward className="h-3.5 w-3.5" />
        )}
        <span className={toggleLabel}>
          {t("player.autoAdvance.label")}: {t(`player.autoAdvance.${autoMode}`)}
        </span>
      </button>
      <button
        onClick={onCycleLoop}
        title={t("player.loop.hint")}
        className={toggle(loopMode !== "off")}
      >
        {loopMode === "one" ? (
          <Repeat1 className="h-3.5 w-3.5" />
        ) : (
          <Repeat className="h-3.5 w-3.5" />
        )}
        <span className={toggleLabel}>
          {t("player.loop.label")}: {t(`player.loop.${loopMode}`)}
        </span>
      </button>
    </>
  );
}

/** Can `el` (or an ancestor inside the modal) still scroll vertically in `dir` (-1 up, 1 down)?
 * Used to leave ArrowUp/ArrowDown alone while there is content to scroll — the volume shortcut
 * must not eat the only keyboard way to read a long description. Stops at `<body>`: the page
 * behind the modal is `overflow:hidden` anyway. */
function canScrollBy(el: HTMLElement | null, dir: -1 | 1): boolean {
  for (let node = el; node && node !== document.body; node = node.parentElement) {
    const style = getComputedStyle(node);
    if (!/(auto|scroll|overlay)/.test(style.overflowY)) continue;
    const room =
      dir < 0 ? node.scrollTop > 0 : node.scrollTop + node.clientHeight < node.scrollHeight - 1; // -1: sub-pixel heights
    if (room) return true;
  }
  return false;
}

// --- IFrame Player API loader (singleton) ---
let apiPromise: Promise<YTNamespace> | null = null;
function loadYouTubeApi(): Promise<YTNamespace> {
  if (apiPromise) return apiPromise;
  apiPromise = new Promise<YTNamespace>((resolve) => {
    const w = window;
    if (w.YT && w.YT.Player) return resolve(w.YT);
    const prev = w.onYouTubeIframeAPIReady;
    w.onYouTubeIframeAPIReady = () => {
      if (typeof prev === "function") prev();
      // The API-ready callback only fires once the script has installed window.YT.
      resolve(w.YT!);
    };
    const tag = document.createElement("script");
    tag.src = "https://www.youtube.com/iframe_api";
    document.head.appendChild(tag);
  });
  return apiPromise;
}

export default function PlayerModal({
  video,
  startAt,
  queue: queueProp,
  startIndex,
  onClose,
  onState,
}: {
  video: Video;
  // Where to start the opened video: a number of seconds (0 = Restart), or null/undefined
  // to resume from the server-saved position carried on the video.
  startAt?: number | null;
  // Optional playback queue (e.g. a playlist). When present, the player advances through
  // it (auto-advance on end + prev/next), recreating per item to reuse the single-video
  // resume / auto-watch / progress logic.
  queue?: Video[];
  startIndex?: number;
  onClose: () => void;
  onState: (id: string, status: VideoStatus) => void;
  // Open the active video's channel page in-app (closes the player first). The small external
  // icon next to the name keeps the open-on-YouTube behaviour.
}) {
  const { t, i18n } = useTranslation();
  const { openChannel } = useNavigationActions();
  const qc = useQueryClient();
  const descFade = useScrollFade();
  // Browser/mouse Back closes the player instead of leaving the page.
  useBackToClose(onClose);
  // Freeze the launch queue: the player steps through the SAME filtered + sorted list it was
  // opened with, for its whole session. The live feed refetches and reorders/drops items under
  // us — e.g. auto-marking each video watched near its end removes it from a filtered feed — which
  // would otherwise shrink the queue mid-playback and break prev/next + Loop "All" (it could never
  // wrap back to the first item once earlier items dropped out). Snapshot once at mount via
  // useState's lazy initializer and ignore later prop changes. Items that turn out gone/
  // non-embeddable still surface the existing playback-error overlay when reached.
  const [queue] = useState(() => queueProp);
  // Precise upload date shown inline after the relative time (e.g. "9 yr ago · 12 Jan 2017").
  const fullDate = (d: string | null | undefined) => formatDate(d, i18n.language);
  // Track the playing item by id (not a frozen index) so it survives the queue changing
  // under us — e.g. an item removed in another tab. The index is derived from the *live*
  // queue, so the N / M counter and prev/next neighbours stay correct without disrupting
  // playback of the current video.
  const [playingId, setPlayingId] = useState<string>(
    // A caller with an explicit index (playlist) starts there; otherwise locate the opened
    // `video` in the queue (the feed passes its whole list + the clicked video, no index).
    startIndex != null ? (queue?.[startIndex]?.id ?? video.id) : video.id,
  );
  // Locate the playing item in the frozen launch queue → drives prev/next + the N/M counter.
  // Because the queue is frozen, the playing id always stays in it (unlike the old live queue,
  // where a just-watched item could drop out and snap playback to queue[0]).
  let index = queue ? queue.findIndex((v) => v.id === playingId) : 0;
  if (index < 0) index = 0;
  const active = queue?.[index] ?? video; // the queue item at index, else the opened video (never undefined)
  const hasQueue = !!queue && queue.length > 1;
  // startAt only applies to the item we opened with; advanced items resume their own pos.
  const resumeAt =
    startAt != null && active.id === video.id ? startAt : active.position_seconds || 0;
  const mountRef = useRef<HTMLDivElement | null>(null);
  // The box the (fixed HD-sized) player iframe is scaled to fit — see FORCE_HD_LOGICAL_SIZE.
  const videoBoxRef = useRef<HTMLDivElement | null>(null);
  // Rescales the HD iframe into videoBoxRef; set up once (ResizeObserver) and also called on ready.
  const fitPlayerRef = useRef<(() => void) | null>(null);
  // HD phase: false = phase 1 (fixed 1920×1080 iframe scaled down, to latch 1080p); true = phase 2
  // (native responsive sizing, so YouTube's own controls render full-size once 1080p has latched).
  const hdNativeRef = useRef(false);
  const playerRef = useRef<YTPlayer | null>(null);
  const autoMarkedRef = useRef(false);
  // Bound consecutive unplayable items so an auto-advancing queue (esp. loop=all) can't spin over a
  // run of dead videos — after the cap we stop on the error overlay instead of skipping forever.
  const MAX_CONSECUTIVE_ERRORS = 3;
  const errorStreakRef = useRef(0);
  // Keyboard/wheel controls. We keep focus on the modal card (not the cross-origin player
  // iframe) so F / Space / Esc keep working until the user clicks into YouTube's native
  // controls; the interaction overlay below also stops the iframe stealing focus on a video
  // click. `stageRef` is the element we fullscreen (so the volume overlay stays visible).
  const cardRef = useRef<HTMLDivElement | null>(null);
  const stageRef = useRef<HTMLDivElement | null>(null);
  // The whole modal (backdrop + card) — the wheel-to-volume target, so scrolling anywhere over the
  // modal adjusts volume (not just the small player area).
  const dialogRef = useRef<HTMLDivElement | null>(null);
  const volTimerRef = useRef<number | undefined>(undefined);
  // Volume level to flash in the on-player overlay (null = hidden). Auto-fades after a moment.
  const [volumeUi, setVolumeUi] = useState<number | null>(null);
  // When the user interacts with YouTube's own controls (gear/seek/CC), focus moves into the
  // cross-origin player iframe. We detect that and drop the interaction overlay's pointer-events
  // so YouTube's settings menu — which expands DOWN over the video, past our overlay — is fully
  // navigable (previously the overlay ate clicks below the top items). Moving the pointer off the
  // video, or the window regaining focus, re-arms our click/scroll/keyboard shortcuts.
  const [nativeControls, setNativeControls] = useState(false);
  // True while OUR stage element is the fullscreen element. In fullscreen the interaction overlay
  // yields entirely (see the overlay's pointer-events below): YouTube's control bar only appears
  // while the iframe itself sees mouse movement, so an armed overlay kept the bar — and with it
  // the native fullscreen button — permanently hidden after pressing F.
  const [isFullscreen, setIsFullscreen] = useState(false);
  // Our own "fills the viewport" mode (the F key). Not the Fullscreen API — see toggleFullscreen.
  const [maximized, setMaximized] = useState(false);
  const maximizedRef = useRef(false);
  maximizedRef.current = maximized;
  // Mirrored into a ref because the keydown handler is bound once (deps: [onClose]) and would
  // otherwise read `false` forever — the house pattern here (see navRef/volumeRef).
  const isFullscreenRef = useRef(false);
  // Nested Back: peel fullscreen → maximise → close, one browser/mouse Back at a time (each is a
  // history layer only while active). Mirrors the Esc ladder in the keydown handler below.
  useBackToExit(isFullscreen, exitFullscreen);
  useBackToExit(maximized, () => setMaximized(false));
  // Volume survives the modal, the next video and a reload: a YT.Player is created per video and
  // always starts at full volume. Per-account, like the Plex player's prefs. Level 0 IS the muted
  // state (that's how YouTube's own control behaves), so there's no separate `muted` flag to
  // contradict it.
  //
  // Ref + explicit read/write rather than `useAccountPersistedObject`: nothing RENDERS the level
  // (the on-player flash has its own `volumeUi` state), the once-bound wheel/key listeners need a
  // ref anyway, and the hook's write lives inside a `setState` updater — which is both a second
  // write on every settle and not something React must run while the component is unmounting.
  // Sanitised HERE, at the boundary where the untrusted value enters (`normalizeVolume`, see
  // lib/playerVolume): a stored `null` / `"80"` / missing key becomes the default once, instead of
  // travelling into the arithmetic. Read via a lazy `useState` initializer so it happens once, not
  // on every render (the house pattern — see the frozen queue above).
  const [storedVolume] = useState(() =>
    normalizeVolume(readAccountMerged(LS.ytPlayerPrefs, { volume: DEFAULT_VOLUME }).volume),
  );
  const volumeRef = useRef(storedVolume);
  const volSaveTimerRef = useRef<number | undefined>(undefined);
  // When the last volume gesture happened — see nudgeVolume (a spin must not re-read the player).
  const lastNudgeAtRef = useRef(0);

  const focusModal = () => cardRef.current?.focus();
  const togglePlay = () => {
    const p = playerRef.current;
    if (!p || typeof p.getPlayerState !== "function") return;
    if (p.getPlayerState() === 1)
      p.pauseVideo?.(); // 1 === playing
    else p.playVideo?.();
  };
  /** F = maximise, deliberately WITHOUT the Fullscreen API.
   *
   * The measured reason (Chrome/Brave/Firefox alike, so it is spec behaviour, not a browser bug):
   * a cross-origin embed reads its fullscreen state from ITS OWN document. While WE hold the
   * browser's fullscreen lock — on our wrapper or even on the iframe element — the YouTube player
   * never learns it is fullscreen, so its own fullscreen button stays in the "enter" state and a
   * click on it changes nothing visible. That is the reported bug: every other YouTube control
   * (gear, CC, volume) works, only its fullscreen button looks dead.
   *
   * So we don't take the lock at all: `maximized` is a CSS layer that fills the viewport. The page
   * stays non-fullscreen, which means the embed's own fullscreen button works exactly as it does
   * anywhere else — and OUR shortcuts and overlays (wheel volume, the level flash, the queue bar)
   * keep working, because our DOM is what's on screen. The one thing we give up is hiding the
   * browser's own chrome; YouTube's button provides that, correctly, when the user wants it.
   */
  const toggleFullscreen = () => setMaximized((m) => !m);
  // True browser fullscreen that WE own (the stage element). Unlike YouTube's own fullscreen it keeps
  // OUR overlay + control bar on screen, so wheel/arrow volume and our controls work inside it too.
  // We never surface YouTube's own fullscreen button, so the "a cross-origin embed's own button looks
  // dead while we hold the lock" problem that made F avoid the Fullscreen API does not arise.
  const toggleTrueFullscreen = () => {
    if (fullscreenElement()) exitFullscreen();
    else if (stageRef.current) requestFullscreen(stageRef.current);
  };
  // Leave the big view back to the modal card: drop fullscreen first if held, then un-maximise.
  const exitBigView = () => {
    if (fullscreenElement()) exitFullscreen();
    setMaximized(false);
  };
  const flashVolume = (level: number) => {
    setVolumeUi(level);
    window.clearTimeout(volTimerRef.current);
    volTimerRef.current = window.setTimeout(() => setVolumeUi(null), 1200);
  };
  /** Write the current level out now, cancelling any pending debounce. */
  const persistVolume = () => {
    window.clearTimeout(volSaveTimerRef.current);
    volSaveTimerRef.current = undefined;
    writeAccount(LS.ytPlayerPrefs, { volume: volumeRef.current });
  };
  /** Push a level (0–100) to the player and remember it; 0 also mutes (a player left at zero
   * volume but unmuted shows YouTube's speaker icon as ON, which reads as broken). */
  const applyVolume = (level: number, flash = true) => {
    // Callers hand over an already-normalised level (stepVolume / the stored value), but the
    // player can still answer NaN, so re-normalise here against the LAST GOOD level — never the
    // default: jumping a quiet session to full blast is the surprise this feature exists to avoid.
    const next = normalizeVolume(level, volumeRef.current);
    const p = playerRef.current;
    if (p && typeof p.setVolume === "function") {
      if (next > 0) p.unMute?.();
      p.setVolume(next);
      if (next === 0) p.mute?.();
    }
    volumeRef.current = next;
    // Debounced: one write when the spin settles, not one per notch. The unmount cleanup flushes
    // a pending write, so the last change is never lost.
    window.clearTimeout(volSaveTimerRef.current);
    volSaveTimerRef.current = window.setTimeout(persistVolume, 400);
    if (flash) flashVolume(next);
  };
  const nudgeVolume = (delta: number) => {
    // No early return when the player isn't ready yet: the level lives in `volumeRef` and onReady
    // applies it, so a wheel spin in the first second after opening (the natural reaction to a
    // video that starts loud) now lands instead of being silently dropped.
    const p = playerRef.current;
    // Whose level do we step from? Our own ref, EXCEPT when the gesture starts fresh — then read
    // the player, because the user may have moved YouTube's own slider in the meantime.
    // `getVolume()` is answered across the iframe boundary and lags our `setVolume` by a beat, so
    // re-reading it mid-spin resurrects a stale level and silently drops notches (measured: five
    // wheel notches moved 100 → 90 instead of 75).
    const now = Date.now();
    const continuing = now - lastNudgeAtRef.current < 1000;
    lastNudgeAtRef.current = now;
    const live =
      p && typeof p.getVolume === "function" ? (p.isMuted?.() ? 0 : p.getVolume()) : null;
    const cur = continuing || live === null ? volumeRef.current : live;
    applyVolume(stepVolume(cur, { delta, fallback: volumeRef.current }));
  };

  // The player can navigate to other videos (YouTube links in a description). The
  // currently-playing id may differ from the active item.
  const [currentVideoId, setCurrentVideoId] = useState(active.id);
  const currentIdRef = useRef(active.id);
  const navigated = currentVideoId !== active.id;
  // Title/author of a navigated-to video, read from the player (free, no API call).
  const [liveData, setLiveData] = useState<{ title?: string; author?: string } | null>(null);
  // IFrame Player API error code, when the current video can't play in the embed (e.g. an
  // auto-generated Topic "Art Track" with embedding disabled → 101/150, or removed/private →
  // 100). We overlay a friendly message + "Open on YouTube" instead of YouTube's bare error.
  const [playerError, setPlayerError] = useState<number | null>(null);

  // Put the HD iframe into phase 1 (fixed 1920×1080, scaled to fit) so YouTube (re)latches 1080p from
  // a full-HD internal viewport. Called on player-ready and when a NEW video is loaded into the same
  // player (a description link) — otherwise the new clip would start at the native box's low quality.
  const applyHdPhase1 = () => {
    if (!FORCE_HD_LOGICAL_SIZE) return;
    hdNativeRef.current = false;
    const f = playerRef.current?.getIframe?.();
    if (f) {
      // Anchor the fixed-HD iframe top-left of its box so the scale-transform (fitPlayerRef) fits it
      // in; `maxWidth:none` guards against a global iframe { max-width:100% } reset.
      f.style.position = "absolute";
      f.style.top = "0";
      f.style.left = "0";
      f.style.width = `${HD_W}px`;
      f.style.height = `${HD_H}px`;
      f.style.maxWidth = "none";
      f.style.transformOrigin = "0 0";
    }
    fitPlayerRef.current?.();
  };
  // Once 1080p has latched (see the quality poll), drop to native responsive sizing so YouTube renders
  // its own controls — the gear especially — at full size. Gated by HD_DROP_TO_NATIVE (see the note at
  // the top): if phase 2 proves to downgrade in the wild, flip that off to keep phase 1 forever.
  const switchToHdNative = () => {
    const f = playerRef.current?.getIframe?.();
    if (!f) return;
    hdNativeRef.current = true;
    f.style.position = "";
    f.style.top = "";
    f.style.left = "";
    f.style.transform = "none";
    f.style.transformOrigin = "";
    f.style.maxWidth = "";
    fitPlayerRef.current?.(); // now runs the native setSize branch
  };

  const loadVideo = (id: string, start: number | null) => {
    const p = playerRef.current;
    if (!p || typeof p.loadVideoById !== "function") return;
    // Explicit start wins; otherwise resume the active item, start others at 0.
    const startSeconds = start != null ? start : id === active.id ? resumeAt : 0;
    setPlayerError(null);
    p.loadVideoById({ videoId: id, startSeconds: startSeconds || 0 });
    // Re-latch 1080p for the newly-loaded clip: back to phase 1 (the quality poll flips it to native
    // once 1080p lands). Without this a description-link video would inherit the native box's low pick.
    applyHdPhase1();
    currentIdRef.current = id;
    setCurrentVideoId(id);
    setLiveData(null);
  };

  // Local mirror of watch status so the toggle reacts instantly; changes are
  // propagated to the feed (and server) via onState.
  const [status, setStatus] = useState(active.status);
  const watched = status === "watched";
  const setWatched = (on: boolean) => {
    const next = on ? "watched" : "new";
    setStatus(next);
    onState(active.id, next);
  };

  // On queue advance (active changes), sync local state to the new item.
  useEffect(() => {
    setStatus(active.status);
    setCurrentVideoId(active.id);
    currentIdRef.current = active.id;
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [active.id]);

  const seekTo = (seconds: number) => {
    const p = playerRef.current;
    if (p && typeof p.seekTo === "function") {
      p.seekTo(seconds, true);
      p.playVideo?.();
    }
  };

  // Prev/next stepping through the queue (the feed's order, or a playlist). Read the live
  // queue+index from a ref so the window keydown handler (bound once) always steps from the
  // current position, not a stale closure.
  const navRef = useRef({ queue, index });
  navRef.current = { queue, index };
  const goPrev = () => {
    const { queue: q, index: i } = navRef.current;
    if (q && i > 0) setPlayingId(q[i - 1]!.id);
  };
  const goNext = () => {
    const { queue: q, index: i } = navRef.current;
    if (q && i < q.length - 1) setPlayingId(q[i + 1]!.id);
  };
  // Relative seek within the current video (plain Arrow keys), matching YouTube's ±5s.
  const nudgeSeek = (delta: number) => {
    const p = playerRef.current;
    if (!p || typeof p.getCurrentTime !== "function") return;
    p.seekTo(Math.max(0, (p.getCurrentTime() || 0) + delta), true);
  };

  // Auto-advance + loop are persistent per-user settings. Read the current values from the cached
  // `me`, mirror them in local state for instant UI, and write both to the server + cache on change
  // so they apply to every player and survive reloads. A ref feeds the (once-bound) end handler.
  const savedPrefs = useMemo(
    () =>
      (qc.getQueryData<{ preferences?: Record<string, unknown> }>(qk.me())?.preferences ?? {}) as {
        playerAutoAdvance?: AutoMode;
        playerLoop?: LoopMode;
      },
    [qc],
  );
  const [autoMode, setAutoMode] = useState<AutoMode>(savedPrefs.playerAutoAdvance ?? "off");
  const [loopMode, setLoopMode] = useState<LoopMode>(savedPrefs.playerLoop ?? "off");
  const modeRef = useRef({ autoMode, loopMode });
  modeRef.current = { autoMode, loopMode };
  const persistPref = (patch: Record<string, string>) => {
    api.savePrefs(patch).catch(() => {});
    qc.setQueryData<{ preferences?: Record<string, unknown> } | undefined>(qk.me(), (m) =>
      m ? { ...m, preferences: { ...(m.preferences || {}), ...patch } } : m,
    );
  };
  const cycleAuto = () => {
    const next = AUTO_MODES[(AUTO_MODES.indexOf(autoMode) + 1) % AUTO_MODES.length]!;
    setAutoMode(next);
    persistPref({ playerAutoAdvance: next });
  };
  const cycleLoop = () => {
    const next = LOOP_MODES[(LOOP_MODES.indexOf(loopMode) + 1) % LOOP_MODES.length]!;
    setLoopMode(next);
    persistPref({ playerLoop: next });
  };
  const replayCurrent = () => {
    const p = playerRef.current;
    if (p && typeof p.seekTo === "function") {
      p.seekTo(0, true);
      p.playVideo?.();
    }
  };
  // Run when the current video ends, per the saved settings. Loop "one" repeats it; otherwise
  // advance in the chosen direction, wrapping at the ends only when loop is "all" (a single-item
  // list repeats). Uses the live queue + settings via refs so it's correct from the bound handler.
  const advanceOnEnd = () => {
    const { autoMode: a, loopMode: l } = modeRef.current;
    if (l === "one") return replayCurrent();
    const { queue: q, index: i } = navRef.current;
    if (!q || q.length === 0) return;
    const wrap = l === "all";
    if (a === "next") {
      if (i < q.length - 1) setPlayingId(q[i + 1]!.id);
      else if (wrap) {
        if (q.length === 1) replayCurrent();
        else setPlayingId(q[0]!.id);
      }
    } else if (a === "prev") {
      if (i > 0) setPlayingId(q[i - 1]!.id);
      else if (wrap) {
        if (q.length === 1) replayCurrent();
        else setPlayingId(q[q.length - 1]!.id);
      }
    } else if (a === "random" && q.length > 1) {
      let r = i;
      while (r === i) r = Math.floor(Math.random() * q.length);
      setPlayingId(q[r]!.id);
    } else if (a === "random" && wrap) {
      replayCurrent();
    }
  };

  // Lazy description (fetched only when the title is hovered). Anchored ABOVE the title so it grows
  // upward over the player rather than downward off-screen / behind the OS taskbar — `flip` still
  // rescues it when there is no room up there either, which the old fixed `bottom` could not. The
  // hover-intent delay (only pop it if the pointer lingers, so a quick pass doesn't flash the card)
  // and the travel grace are now the platform's, along with the safePolygon bridge that replaces the
  // pair of hand-managed timers.
  const [showDesc, setShowDesc] = useState(false);
  const descPop = useAnchoredPopover({
    open: showDesc,
    onOpenChange: setShowDesc,
    mode: "hover",
    placement: "top-start",
    offset: 8,
    hoverDelay: { open: 400, close: 150 },
  });
  const detail = useQuery({
    queryKey: qk.videoDetail(currentVideoId),
    queryFn: () => api.videoDetail(currentVideoId),
    // On hover (for the description) and eagerly when navigated, so we can link the
    // linked video's channel. Both share the one cached result.
    enabled: showDesc || navigated,
    staleTime: 5 * 60_000,
  });

  // This player is a dismissable layer: register it so a Modal or popover opened ABOVE it (e.g. the
  // download dialog, or — after S4 — an anchored menu) takes Escape first and this player defers.
  const layerIdRef = useRef<number | null>(null);
  useEffect(() => {
    const id = pushLayer();
    layerIdRef.current = id;
    return () => {
      removeLayer(id);
      layerIdRef.current = null;
    };
  }, []);

  // Keyboard shortcuts (Esc close / F fullscreen / Space play-pause) + background scroll lock.
  // These fire only while focus is on our page, not inside the cross-origin player iframe — so
  // we focus the modal card on open and again on player-ready (autoplay can grab focus).
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      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.
        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;
        // Same for our own maximise: Esc steps back out of it before it closes the player.
        if (maximizedRef.current) {
          e.preventDefault();
          setMaximized(false);
          return;
        }
        onClose();
        return;
      }
      const el = document.activeElement as HTMLElement | null;
      const tag = (el?.tagName || "").toLowerCase();
      const typing =
        tag === "input" || tag === "textarea" || tag === "select" || !!el?.isContentEditable;
      if (e.key === "f" || e.key === "F") {
        if (typing) return;
        e.preventDefault();
        toggleFullscreen();
      } else if (e.key === " " || e.code === "Space") {
        // Let Space activate a focused button/link; otherwise toggle playback (and stop the
        // page from scrolling).
        if (typing || tag === "button" || tag === "a") return;
        e.preventDefault();
        togglePlay();
      } else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
        // Volume by keyboard (YouTube/Plex-style ±5). The only volume control that survives
        // fullscreen, where the wheel goes to YouTube's own iframe. But the modal card scrolls
        // (a long description) and Up/Down is how you scroll it from the keyboard — so defer
        // whenever the focused element can still scroll that way, exactly like Space defers to a
        // focused button. NOT while the video fills the screen though — in fullscreen OR in our own
        // maximise — because the card is behind/off-screen there: deferring would scroll something
        // invisible and leave the viewer with no volume control at all. Both flags, not just
        // fullscreen: since F stopped taking the browser's lock, maximise is the common case and
        // checking only `isFullscreen` made the keys dead exactly where they matter most.
        const up = e.key === "ArrowUp";
        const covered = isFullscreenRef.current || maximizedRef.current;
        if (typing || (!covered && canScrollBy(el, up ? -1 : 1))) return;
        e.preventDefault();
        nudgeVolume(up ? 5 : -5);
      } else if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
        // Plain arrows seek within the video (YouTube-style ±5s); Shift+arrow steps to the
        // previous/next video in the queue (the feed's order or a playlist).
        if (typing) return;
        e.preventDefault();
        const forward = e.key === "ArrowRight";
        if (e.shiftKey) {
          if (forward) goNext();
          else goPrev();
        } else {
          nudgeSeek(forward ? 5 : -5);
        }
      }
    };
    window.addEventListener("keydown", onKey);
    focusModal();
    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      window.removeEventListener("keydown", onKey);
      document.body.style.overflow = prevOverflow;
      window.clearTimeout(volTimerRef.current);
      // Flush a debounced volume write — closing the player right after a wheel spin (the normal
      // way to leave) must not drop the level the user just set.
      if (volSaveTimerRef.current != null) persistVolume();
    };
  }, [onClose]);

  // Mouse-wheel volume over the WHOLE modal (backdrop + card), not just the small player area.
  // Non-passive so preventDefault stops the modal/page scrolling. Note: the cross-origin iframe
  // swallows wheel events over its own native-control strips, but the central interaction overlay
  // is our element (wheel there bubbles here), and everything outside the video works directly.
  useEffect(() => {
    const el = dialogRef.current;
    if (!el) return;
    const onWheel = (e: WheelEvent) => {
      // Ctrl/⌘+wheel is the browser's zoom gesture — never swallow it. Volume is the PLAIN wheel
      // (and ↑/↓); taking the modifier too left zoom unreachable while the player was open.
      if (e.ctrlKey || e.metaKey) return;
      e.preventDefault();
      focusModal(); // re-grab focus if it had slipped into the native controls
      nudgeVolume(e.deltaY < 0 ? 5 : -5);
    };
    el.addEventListener("wheel", onWheel, { passive: false });
    return () => el.removeEventListener("wheel", onWheel);
  }, []);

  // Track the browser's fullscreen state. WE never request it any more (F maximises via CSS), so
  // this is YouTube's own fullscreen — entered from its control bar, left by its button or Esc.
  useEffect(() => {
    const onChange = () => {
      const fsEl = fullscreenElement();
      // Refs only — this listener is bound once, so it must not close over render values.
      const p = playerRef.current;
      const frame = p && typeof p.getIframe === "function" ? p.getIframe() : null;
      const ours = !!fsEl && (fsEl === stageRef.current || fsEl === frame);
      isFullscreenRef.current = ours; // read by the once-bound keydown handler
      setIsFullscreen(ours); // drives the overlay's pointer-events
      // LEAVING fullscreen is where the user got stranded: to reach YouTube's fullscreen button
      // they had to click INTO the player, which moves keyboard focus into the cross-origin iframe
      // — and it stays there afterwards. Our window-level shortcuts then never fire (F went to
      // YouTube instead, giving a second, control-less fullscreen), so the maximised view had no
      // way out. Taking focus back here restores F/Space/arrows the moment fullscreen ends.
      if (!fsEl) focusModal();
    };
    return onFullscreenChange(onChange); // both spellings — see lib/fullscreen
  }, []);

  // Yield the interaction overlay to YouTube's native UI. Clicking a native control (gear / seek
  // bar / CC) moves focus into the player iframe — the only cross-origin signal we get. While the
  // iframe holds focus we drop the overlay's pointer-events so the (arbitrarily tall) settings
  // menu is clickable end to end. Re-arm — restoring click-to-play, wheel volume and our keyboard
  // shortcuts — when the pointer leaves the video or the window regains focus (a click back on our
  // card blurs the iframe → the parent window fires 'focus').
  useEffect(() => {
    const iframeFocused = () => {
      const p = playerRef.current;
      const f = p && typeof p.getIframe === "function" ? p.getIframe() : null;
      return !!f && document.activeElement === f;
    };
    // activeElement settles on the tick after blur, so defer the check.
    const onBlur = () => window.setTimeout(() => iframeFocused() && setNativeControls(true), 0);
    const rearm = () => {
      setNativeControls(false);
      focusModal();
    };
    const stage = stageRef.current;
    window.addEventListener("blur", onBlur);
    window.addEventListener("focus", rearm);
    stage?.addEventListener("mouseleave", rearm);
    return () => {
      window.removeEventListener("blur", onBlur);
      window.removeEventListener("focus", rearm);
      stage?.removeEventListener("mouseleave", rearm);
    };
  }, []);

  // Keep the fixed-HD player iframe scaled to fit its visible box (see FORCE_HD_LOGICAL_SIZE). The
  // iframe stays 1920×1080 internally (so YouTube serves 1080p) while we CSS-scale it DOWN to the
  // box with a letterbox-`contain` fit — one factor + centring handles both the 16:9 windowed box
  // and the non-16:9 big-view box (viewport minus the 56px control bar). Bound once; reads live refs.
  useEffect(() => {
    if (!FORCE_HD_LOGICAL_SIZE) return;
    const box = videoBoxRef.current;
    if (!box) return;
    const fit = () => {
      const p = playerRef.current;
      const f = p && typeof p.getIframe === "function" ? p.getIframe() : null;
      if (!f) return;
      const bw = box.clientWidth;
      const bh = box.clientHeight;
      if (!bw || !bh) return;
      if (hdNativeRef.current) {
        // Phase 2: native responsive sizing — the iframe IS the box, so YouTube's controls render
        // at full size. Set the inline size explicitly too: phase 1 left an inline 1920×1080px on the
        // element, which would otherwise override setSize's attributes and keep it overflowing.
        f.style.width = `${bw}px`;
        f.style.height = `${bh}px`;
        if (p && typeof p.setSize === "function") p.setSize(bw, bh);
        return;
      }
      // Phase 1: latch 1080p — scale the fixed 1920×1080 iframe DOWN to fit, letterbox-`contain`.
      const k = Math.min(bw / HD_W, bh / HD_H);
      const cx = (bw - HD_W * k) / 2;
      const cy = (bh - HD_H * k) / 2;
      f.style.transform = `translate(${cx}px, ${cy}px) scale(${k})`;
    };
    fitPlayerRef.current = fit;
    const ro = new ResizeObserver(fit);
    ro.observe(box);
    return () => {
      ro.disconnect();
      fitPlayerRef.current = null;
    };
  }, []);

  // Create the player, resume from the saved position, persist progress, and
  // auto-mark watched once playback reaches the end.
  useEffect(() => {
    let cancelled = false;
    const id = active.id;
    autoMarkedRef.current = false;
    // A fresh player starts in HD phase 1 (scaled), regardless of what the previous video ended on.
    hdNativeRef.current = false;
    setPlayerError(null);

    // Auto-watch only applies to the active item — not to other videos the player
    // navigated to via description links.
    const maybeAutoWatch = (current: number, duration: number): boolean => {
      if (autoMarkedRef.current || duration <= 0 || currentIdRef.current !== id) return false;
      // Cap the finish margin at half the clip, so a video shorter than FINISH_MARGIN isn't
      // auto-marked watched almost immediately (its threshold would otherwise go negative).
      const margin = Math.min(FINISH_MARGIN, duration / 2);
      if (current > duration - margin) {
        autoMarkedRef.current = true;
        setWatched(true);
        return true;
      }
      return false;
    };
    const persist = (): Promise<unknown> | void => {
      const p = playerRef.current;
      if (!p || typeof p.getCurrentTime !== "function") return;
      try {
        const cur = p.getCurrentTime();
        const dur = typeof p.getDuration === "function" ? p.getDuration() : 0;
        // If we just auto-marked watched, skip the near-end progress checkpoint: it would
        // only clear the resume position, and firing both at once needlessly races the
        // watched write against the progress row it deletes.
        if (maybeAutoWatch(cur, dur)) return;
        // Only checkpoint the feed video we opened with — a navigated-to video may not
        // be in this user's library, so the server would 404 on it.
        if (currentIdRef.current === id) {
          return api.saveProgress(id, cur, dur).catch(() => {});
        }
      } catch {
        /* player may be tearing down */
      }
    };

    loadYouTubeApi().then((YT) => {
      if (cancelled || !mountRef.current) return;
      playerRef.current = new YT.Player(mountRef.current, {
        // A FIXED large logical size (scaled down to fit via fitPlayerRef) so YouTube starts at
        // 1080p; falls back to the old responsive sizing if the trick is disabled. See HD_W/HD_H.
        width: FORCE_HD_LOGICAL_SIZE ? String(HD_W) : "100%",
        height: FORCE_HD_LOGICAL_SIZE ? String(HD_H) : "100%",
        videoId: id,
        playerVars: {
          autoplay: 1,
          start: resumeAt || undefined,
          rel: 0, // limit "related" to the same channel (full removal is no longer possible)
          enablejsapi: 1,
          origin: window.location.origin,
          playsinline: 1,
          // Best-effort HD hint. The IFrame API's setPlaybackQuality/suggestedQuality are hard no-ops
          // now (YouTube removed them), and the undocumented `vq` URL param is only occasionally
          // honoured — kept because it's harmless. The real HD lever is the fixed HD_W×HD_H iframe
          // size above (see FORCE_HD_LOGICAL_SIZE): YouTube reads quality from the iframe's INTERNAL
          // viewport at creation, which a CSS transform-scale does not shrink — so scaling a LARGE
          // player DOWN keeps the 1080p pick (an earlier note here that "the scale trick fails" was
          // scaling a SMALL player UP, which genuinely doesn't fool the cap; the opposite direction
          // does — measured 2026-07-31).
          vq: "hd1080",
        },
        events: {
          // Keep keyboard focus on the modal, not the iframe, so shortcuts work right away, and
          // restore the remembered volume — a fresh player always starts at 100.
          onReady: () => {
            // Start in phase 1 (fixed-HD iframe scaled to fit) so YouTube latches 1080p; the quality
            // poll below flips to native sizing once it lands.
            applyHdPhase1();
            focusModal();
            applyVolume(volumeRef.current, false);
          },
          onStateChange: (e: YTPlayerEvent) => {
            // 1 === playing → sync the navigated-to video's title/author for display.
            if (e?.data === 1) {
              errorStreakRef.current = 0; // a successful play breaks any unplayable run
              const p = playerRef.current;
              const d = p && typeof p.getVideoData === "function" ? p.getVideoData() : null;
              if (d) setLiveData({ title: d.title, author: d.author });
            }
            // 0 === ended → mark watched, then advance to the next queue item if any.
            if (e?.data === 0 && currentIdRef.current === id) {
              if (!autoMarkedRef.current) {
                autoMarkedRef.current = true;
                setWatched(true);
              }
              advanceOnEnd();
            }
          },
          // The embed couldn't play this video (embedding disabled, removed, private…).
          // Surface our own message + an "Open on YouTube" escape hatch.
          onError: (e: YTPlayerEvent) => {
            setPlayerError(typeof e?.data === "number" ? e.data : -1);
            // The IFrame API fires onError (not 'ended') for an unplayable item, so advanceOnEnd()
            // never runs and an auto-advancing queue would freeze here. Skip forward instead — but
            // bounded, so a run of dead items (or loop=all over a broken queue) can't spin. We do
            // NOT mark the broken video watched (only the ended-handler does that).
            if (currentIdRef.current !== id) return; // a navigated description-link error: leave the queue alone
            const { autoMode: a } = modeRef.current;
            const { queue: q } = navRef.current;
            if (a === "off" || !q || q.length <= 1) return;
            errorStreakRef.current += 1;
            if (errorStreakRef.current >= MAX_CONSECUTIVE_ERRORS) return; // give up; leave the overlay
            advanceOnEnd();
          },
        },
      });
    });

    // Periodic checkpoint so progress (and auto-watch) survive a crash/refresh.
    const timer = window.setInterval(persist, 5000);
    // HD phase 2 trigger: once the (phase-1, scaled) player has actually latched 1080p, drop to native
    // sizing so YouTube's controls render full-size. Only at hd1080+. Gated by HD_DROP_TO_NATIVE — the
    // conditional/under-observation switch (see the note up top): a video that never reaches 1080p, a
    // constrained network, or HD_DROP_TO_NATIVE=false all keep phase 1 (the guaranteed-1080p fallback).
    // Re-arms automatically after a description-link load (applyHdPhase1 resets the flag).
    const HD_NATIVE_QUALITIES = new Set(["hd1080", "hd1440", "hd2160"]);
    const hdPoll = window.setInterval(() => {
      if (!FORCE_HD_LOGICAL_SIZE || !HD_DROP_TO_NATIVE || hdNativeRef.current) return;
      const p = playerRef.current;
      const q = p && typeof p.getPlaybackQuality === "function" ? p.getPlaybackQuality() : "";
      if (HD_NATIVE_QUALITIES.has(q)) switchToHdNative();
    }, 700);
    // Save on page unload too (F5/close/navigate): effect cleanup doesn't run on a full reload, so
    // without this the resume point lags up to 5s (a seek right before F5 would be lost). A keepalive
    // beacon so the POST survives the unload. Only for the library item we opened with — a navigated
    // description-link video may not be in this user's library (the server would 404).
    const onHide = () => {
      const p = playerRef.current;
      if (!p || typeof p.getCurrentTime !== "function" || currentIdRef.current !== id) return;
      try {
        const cur = p.getCurrentTime();
        const dur = typeof p.getDuration === "function" ? p.getDuration() : 0;
        if (cur > 0 && dur > 0) api.saveProgressBeacon(id, cur, dur);
      } catch {
        /* player tearing down */
      }
    };
    window.addEventListener("pagehide", onHide);

    return () => {
      cancelled = true;
      window.clearInterval(timer);
      window.clearInterval(hdPoll);
      window.removeEventListener("pagehide", onHide);
      // Final flush, then refresh the feed + playlists so resume bars reflect this session.
      Promise.resolve(persist()).finally(() => {
        qc.invalidateQueries({ queryKey: qk.feed() });
        qc.invalidateQueries({ queryKey: qk.playlist() });
      });
      const p = playerRef.current;
      if (p && typeof p.destroy === "function") {
        try {
          p.destroy();
        } catch {
          /* ignore */
        }
      }
      playerRef.current = null;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [active.id]);

  // Portaled to <body>, like every other full-screen overlay here. A fixed, full-viewport modal
  // must not render inside the page scroller: once that scroller is a stacking context (its
  // edge-fade mask makes it one) this z-overlay is scoped inside it, and the nav rail (z-rail) and the
  // header (z-chrome) paint over the modal — worst at browser zoom, where the centered card is smaller
  // relative to the rail/panel. (Same class as PlexPlayer; this one was the miss in that sweep.)
  return createPortal(
    <div
      ref={dialogRef}
      data-testid="player-modal"
      className="fixed inset-0 z-overlay flex items-center justify-center p-4 sm:p-6 bg-black/80 backdrop-blur-sm"
      onClick={onClose}
      role="dialog"
      aria-modal="true"
    >
      {/* The card, flanked by full-height glassy strips that step through the feed's order (or a
          playlist). Each strip sits just outside the card, spans its full height, and fades out at
          the ends. stopPropagation so a click steps instead of closing. Hidden on narrow screens
          where there's no room beside the card. */}
      <div className="relative flex w-full max-w-4xl max-h-full">
        {hasQueue && (
          <button
            onClick={(e) => {
              e.stopPropagation();
              goPrev();
            }}
            disabled={index === 0}
            aria-label={t("player.previous")}
            title={`${t("player.previous")} · Shift+←`}
            className="absolute right-full top-0 bottom-0 mr-2 hidden w-12 place-items-center rounded-2xl bg-white/5 text-white/40 backdrop-blur-sm transition hover:bg-white/15 hover:text-white disabled:pointer-events-none disabled:opacity-0 sm:grid"
          >
            <ChevronLeft className="h-8 w-8" />
          </button>
        )}
        <div
          ref={cardRef}
          tabIndex={-1}
          className="glass-card relative flex-1 min-w-0 max-h-full overflow-y-auto rounded-2xl shadow-2xl outline-none"
          onClick={(e) => e.stopPropagation()}
        >
          <div
            ref={stageRef}
            className={`player-stage relative aspect-video w-full bg-black rounded-t-2xl overflow-hidden ${
              maximized ? "player-stage--max" : ""
            }`}
          >
            {/* Hide the iframe entirely on error so YouTube's own error screen can't bleed
              through our overlay. */}
            {/* In the big views the video area is inset by the control bar's height (`bottom-14`) so
              the bar below never overlaps it. The inset lives on THIS wrapper, not on `mountRef`:
              YT.Player REPLACES the mount element with its iframe (width/height 100% of the parent),
              which would drop any class on mountRef itself — the iframe then sizes to this wrapper. */}
            <div
              ref={videoBoxRef}
              className={
                maximized || isFullscreen
                  ? "absolute inset-x-0 top-0 bottom-14 overflow-hidden"
                  : "relative w-full h-full overflow-hidden"
              }
            >
              <div
                ref={mountRef}
                className={`w-full h-full ${playerError != null ? "invisible" : ""}`}
              />
            </div>
            {/* Interaction layer over the CENTRE of the video: catches click (play/pause) + wheel
              volume (the wheel event bubbles from here to the modal's listener — over the bare
              cross-origin iframe it would be swallowed) and stops the iframe stealing keyboard focus.
              It leaves the top AND bottom edges uncovered so YouTube's edge controls stay reachable.
              Armed in the big views too (that is what keeps wheel volume working after F, the reported
              gap) — it only yields to `nativeControls`, i.e. once the user opens YouTube's own
              settings menu (focus moves into the iframe), so that menu is navigable end to end.
              Hidden on error so the "Open on YouTube" CTA is clickable. */}
            {playerError == null && (
              <div
                onClick={() => {
                  focusModal();
                  togglePlay();
                }}
                title={t("player.shortcutsHint")}
                aria-label={t("player.shortcutsHint")}
                className={`absolute inset-x-0 top-[12%] bottom-[22%] z-base cursor-pointer ${
                  nativeControls ? "pointer-events-none" : ""
                }`}
              />
            )}
            {/* Our own control bar for the big views (maximise / our fullscreen), where the card's
              controls are off-screen. It's IN the stage (so it survives true fullscreen, where a
              <body> portal would not) and DOCKED in its own strip: the video area above is inset by
              this bar's height (see mountRef's `bottom-14`), so the bar NEVER overlaps YouTube's seek
              bar — that stays reachable by hovering the video's bottom edge, just above ours. Always
              visible (no auto-hide) so there's no guessing whether it will appear and no fight with the
              seek bar over the same pixels. stopPropagation so a click here doesn't toggle playback;
              the exit button also re-arms our shortcuts. Add-to-playlist works in true fullscreen (its
              popover portals INTO the fullscreen element); download stays hidden there because it opens
              a Modal, which still portals to <body> — outside the fullscreen element. */}
            {(maximized || isFullscreen) && (
              <div
                onClick={(e) => e.stopPropagation()}
                className="absolute inset-x-0 bottom-0 z-menu flex h-14 items-center justify-start gap-x-3 overflow-x-auto border-t border-white/10 bg-neutral-950/95 px-3 text-sm text-white"
              >
                {hasQueue && (
                  <>
                    <PlayerTransportControls
                      variant="docked"
                      index={index}
                      queueLength={queue!.length}
                      autoMode={autoMode}
                      loopMode={loopMode}
                      onPrev={goPrev}
                      onNext={goNext}
                      onCycleAuto={cycleAuto}
                      onCycleLoop={cycleLoop}
                    />
                    <span className="h-4 w-px bg-white/20" />
                  </>
                )}
                {/* Both work in fullscreen now: add-to-playlist's popover and download's Modal both
                    portal into the fullscreen element rather than <body> (a body-portaled node is
                    painted underneath the element holding the lock, i.e. invisible). */}
                {!navigated && (
                  <AddToPlaylist
                    videoId={active.id}
                    className="inline-flex h-8 w-8 items-center justify-center rounded-lg text-white/80 hover:bg-white/10 hover:text-white transition"
                  />
                )}
                {!navigated && (
                  <DownloadButton
                    videoId={active.id}
                    title={active.title}
                    className="inline-flex h-8 w-8 items-center justify-center rounded-lg text-white/80 hover:bg-white/10 hover:text-white transition"
                  />
                )}
                {!navigated && (
                  <button
                    onClick={() => setWatched(!watched)}
                    title={watched ? t("player.watchedUnmark") : t("player.markWatched")}
                    className={`inline-flex items-center gap-1 rounded-lg px-2 py-1 transition ${
                      watched
                        ? "bg-white/25 text-white"
                        : "text-white/70 hover:bg-white/10 hover:text-white"
                    }`}
                  >
                    {watched ? <CheckCheck className="h-4 w-4" /> : <Check className="h-4 w-4" />}
                    <span className="hidden md:inline">
                      {watched ? t("player.watched") : t("player.markWatched")}
                    </span>
                  </button>
                )}
                <span className="h-4 w-px bg-white/20" />
                <button
                  onClick={toggleTrueFullscreen}
                  title={isFullscreen ? t("player.exitFullscreen") : t("player.fullscreen")}
                  aria-label={isFullscreen ? t("player.exitFullscreen") : t("player.fullscreen")}
                  className="inline-flex h-8 w-8 items-center justify-center rounded-lg text-white/80 hover:bg-white/10 hover:text-white transition"
                >
                  {isFullscreen ? (
                    <Minimize className="h-4 w-4" />
                  ) : (
                    <Maximize className="h-4 w-4" />
                  )}
                </button>
                <button
                  onClick={(e) => {
                    e.stopPropagation();
                    exitBigView();
                    setNativeControls(false);
                    focusModal();
                  }}
                  title={t("player.exitMaximize")}
                  aria-label={t("player.exitMaximize")}
                  className="inline-flex items-center gap-1 rounded-lg px-2 py-1 text-white/80 hover:bg-white/10 hover:text-white transition"
                >
                  <Minimize2 className="h-4 w-4" />
                  <span className="hidden sm:inline">{t("player.exitMaximize")}</span>
                </button>
              </div>
            )}
            {/* Discreet badge while YouTube's own controls have taken over (overlay yielded). */}
            {playerError == null && nativeControls && (
              <div className="pointer-events-none absolute top-3 left-3 z-menu flex items-center gap-1.5 rounded-full bg-black/60 px-2.5 py-1 text-[11px] text-white/90 shadow-lg backdrop-blur-sm">
                <Settings className="h-3.5 w-3.5" />
                {t("player.nativeControls")}
              </div>
            )}
            {/* Volume level flash (auto-fades). pointer-events-none so it never blocks the video. */}
            {volumeUi != null && (
              <div className="pointer-events-none absolute top-3 left-1/2 -translate-x-1/2 z-menu flex items-center gap-2 rounded-full bg-black/70 px-3 py-1.5 text-xs text-white shadow-lg">
                {volumeUi === 0 ? <VolumeX className="h-4 w-4" /> : <Volume2 className="h-4 w-4" />}
                <div className="h-1.5 w-28 overflow-hidden rounded-full bg-white/25">
                  <div
                    className="h-full rounded-full bg-white transition-all"
                    style={{ width: `${volumeUi}%` }}
                  />
                </div>
                <span className="w-7 text-right tabular-nums">{volumeUi}</span>
              </div>
            )}
            {playerError != null && (
              <div className="absolute inset-0 grid place-items-center bg-bg p-6 text-center">
                <div className="max-w-sm">
                  <AlertTriangle className="w-8 h-8 text-amber-500 mx-auto mb-3" />
                  <h3 className="text-base font-semibold">{t("player.unavailableTitle")}</h3>
                  <p className="text-sm text-muted mt-1.5 mb-4">
                    {playerError === 101 || playerError === 150
                      ? t("player.embedDisabledBody")
                      : t("player.unavailableBody")}
                  </p>
                  <a
                    href={`https://www.youtube.com/watch?v=${currentVideoId}`}
                    target="_blank"
                    rel="noreferrer"
                    className="inline-flex items-center justify-center gap-2 px-4 py-2 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 transition"
                  >
                    <ExternalLink className="w-4 h-4" />
                    {t("player.openOnYoutube")}
                  </a>
                </div>
              </div>
            )}
          </div>

          {hasQueue && (
            <div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5 px-4 py-2 border-b border-border text-sm">
              <PlayerTransportControls
                variant="modal"
                index={index}
                queueLength={queue!.length}
                autoMode={autoMode}
                loopMode={loopMode}
                onPrev={goPrev}
                onNext={goNext}
                onCycleAuto={cycleAuto}
                onCycleLoop={cycleLoop}
              />
            </div>
          )}

          <div className="p-4 sm:p-5">
            {/* Title row — title (with hover description) on the left, Close on the right. */}
            <div className="flex items-start gap-3">
              <h2 className="min-w-0 flex-1 text-lg font-semibold leading-snug">
                {/* Hover target is the text itself (inline), not the whole row. */}
                <span
                  ref={descPop.refs.setReference}
                  {...descPop.getReferenceProps()}
                  className="cursor-default"
                >
                  {navigated ? (liveData?.title ?? t("player.loading")) : active.title}
                </span>
              </h2>
              {navigated && (
                <button
                  onClick={() => loadVideo(active.id, null)}
                  title={t("player.backToOriginal")}
                  className="shrink-0 inline-flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-lg text-muted hover:text-fg hover:bg-surface transition"
                >
                  <ArrowLeft className="w-4 h-4" />
                  {t("player.back")}
                </button>
              )}
              <PopoverSurface
                popover={descPop}
                className="w-[min(35rem,calc(100vw-2rem))] bg-surface border border-border rounded-xl shadow-2xl p-4"
              >
                <div className="text-xs uppercase tracking-wide text-muted mb-2">
                  {t("player.description")}
                </div>
                {detail.isLoading ? (
                  <div className="text-sm text-muted">{t("player.loading")}</div>
                ) : detail.data?.description ? (
                  <div
                    ref={descFade.ref}
                    style={descFade.style}
                    className="text-sm whitespace-pre-wrap break-words max-h-64 overflow-y-auto no-scrollbar leading-relaxed"
                  >
                    {renderDescription(detail.data.description, {
                      currentId: currentVideoId,
                      onSeek: seekTo,
                      onLoadVideo: loadVideo,
                      t,
                    })}
                  </div>
                ) : (
                  <div className="text-sm text-muted">{t("player.noDescription")}</div>
                )}
              </PopoverSurface>
              <button
                data-testid="player-close"
                onClick={onClose}
                title={t("player.closeEsc")}
                className="shrink-0 inline-flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-lg text-muted hover:text-fg hover:bg-surface transition"
              >
                {t("player.close")}
                <X className="w-4 h-4" />
              </button>
            </div>

            {/* Channel + meta on one line, with the watched toggle pushed to the right.
              When navigated to a linked video we only have its author (from the player),
              so we show that as plain text and hide feed-video-specific bits. */}
            <div className="flex items-center gap-3 mt-3">
              {!navigated && (
                <Avatar
                  src={active.channel_thumbnail}
                  fallback={active.channel_title ?? ""}
                  className="w-9 h-9 rounded-full shrink-0"
                />
              )}
              {navigated ? (
                detail.data?.channel_id ? (
                  <a
                    href={channelYouTubeUrl(detail.data.channel_id)}
                    target="_blank"
                    rel="noreferrer"
                    className="font-medium hover:text-accent shrink-0"
                  >
                    {liveData?.author ?? detail.data.channel_title ?? t("player.channel")}
                  </a>
                ) : (
                  <span className="font-medium shrink-0">{liveData?.author ?? ""}</span>
                )
              ) : (
                <div className="flex items-center gap-1.5 shrink-0 min-w-0">
                  <button
                    onClick={() => {
                      const cid = active.channel_id;
                      const cname = active.channel_title ?? undefined;
                      // Closing the player pops its own history entry (useBackToClose → history.back()).
                      // Open the channel only AFTER that pop's popstate — pushing the channel entry
                      // before it would let the back remove it again (the bug: clicking the name only
                      // closed the player). One-shot listener fires after the popstate handler.
                      const open = () => {
                        window.removeEventListener("popstate", open);
                        openChannel(cid, cname);
                      };
                      window.addEventListener("popstate", open);
                      onClose();
                    }}
                    className="font-medium hover:text-accent truncate text-left"
                  >
                    {active.channel_title}
                  </button>
                  <a
                    href={active.channel_url}
                    target="_blank"
                    rel="noreferrer"
                    title={t("channels.row.openOnYouTube")}
                    aria-label={t("channels.row.openOnYouTube")}
                    className="text-muted hover:text-accent shrink-0"
                  >
                    <ExternalLink className="w-3.5 h-3.5" />
                  </a>
                </div>
              )}
              {!navigated ? (
                <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm text-muted min-w-0">
                  {active.view_count != null && (
                    <span>
                      ·{" "}
                      {t("player.views", {
                        count: active.view_count,
                        formattedCount: formatViews(active.view_count),
                      })}
                    </span>
                  )}
                  <span>
                    · {relativeTime(active.published_at)}
                    {active.published_at && ` · ${fullDate(active.published_at)}`}
                  </span>
                  {active.duration_seconds != null && (
                    <span>· {formatDuration(active.duration_seconds)}</span>
                  )}
                  {active.live_status === "was_live" && (
                    <span className="bg-accent text-accent-fg text-[10px] font-semibold uppercase tracking-wide px-1.5 py-0.5 rounded">
                      {t("player.stream")}
                    </span>
                  )}
                </div>
              ) : (
                // Linked video's stats come from the (already-fetched) video detail.
                <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm text-muted min-w-0">
                  {detail.data?.view_count != null && (
                    <span>
                      ·{" "}
                      {t("player.views", {
                        count: detail.data.view_count,
                        formattedCount: formatViews(detail.data.view_count),
                      })}
                    </span>
                  )}
                  {detail.data?.published_at && (
                    <span>
                      · {relativeTime(detail.data.published_at)} ·{" "}
                      {fullDate(detail.data.published_at)}
                    </span>
                  )}
                  {detail.data?.duration_seconds != null && (
                    <span>· {formatDuration(detail.data.duration_seconds)}</span>
                  )}
                </div>
              )}

              {/* Reliable, discoverable path into our big view straight from the windowed player —
                the docked bar's controls (incl. our own true-fullscreen ⛶) are off-screen here, and
                the only fullscreen button previously reachable in the windowed video was YouTube's
                own (which hides our chrome). Mirrors the F key (maximise, a CSS big view); from there
                the docked bar offers OUR true fullscreen. */}
              {!navigated && (
                <button
                  onClick={toggleFullscreen}
                  title={`${t("player.fullscreen")} · F`}
                  aria-label={t("player.fullscreen")}
                  className="ml-auto shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-lg text-muted hover:text-fg hover:bg-surface border border-border transition"
                >
                  <Maximize className="w-4 h-4" />
                </button>
              )}
              {!navigated && (
                <AddToPlaylist
                  videoId={active.id}
                  className="shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-lg text-muted hover:text-fg hover:bg-surface border border-border transition"
                />
              )}
              {!navigated && (
                <DownloadButton
                  videoId={active.id}
                  title={active.title}
                  className="shrink-0 inline-flex items-center justify-center w-9 h-9 rounded-lg text-muted hover:text-fg hover:bg-surface border border-border transition"
                />
              )}
              {!navigated && (
                <button
                  onClick={() => setWatched(!watched)}
                  title={watched ? t("player.watchedUnmark") : t("player.markWatched")}
                  className={
                    "shrink-0 inline-flex items-center gap-1.5 text-sm px-3 py-1.5 rounded-lg transition " +
                    (watched
                      ? "bg-accent text-accent-fg shadow-sm hover:opacity-90"
                      : "text-muted hover:text-fg hover:bg-surface border border-border")
                  }
                >
                  {watched ? <CheckCheck className="w-4 h-4" /> : <Check className="w-4 h-4" />}
                  {watched ? t("player.watched") : t("player.markWatched")}
                </button>
              )}
            </div>
          </div>
        </div>
        {hasQueue && (
          <button
            onClick={(e) => {
              e.stopPropagation();
              goNext();
            }}
            disabled={index >= queue!.length - 1}
            aria-label={t("player.next")}
            title={`${t("player.next")} · Shift+→`}
            className="absolute left-full top-0 bottom-0 ml-2 hidden w-12 place-items-center rounded-2xl bg-white/5 text-white/40 backdrop-blur-sm transition hover:bg-white/15 hover:text-white disabled:pointer-events-none disabled:opacity-0 sm:grid"
          >
            <ChevronRight className="h-8 w-8" />
          </button>
        )}
      </div>
    </div>,
    document.body,
  );
}
