Video Enhancer

Enhance any HTML5 video: playback speed, volume boost, seek, screenshot, PiP, filters, transforms, progress memory & more. Works on YouTube, Twitch, Vimeo and all sites.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Video Enhancer
// @name:en      Video Enhancer - Speed Control, Filters, Screenshot & More
// @namespace    video-enhancer-compact-ui
// @version      1.0.0
// @description  Enhance any HTML5 video: playback speed, volume boost, seek, screenshot, PiP, filters, transforms, progress memory & more. Works on YouTube, Twitch, Vimeo and all sites.
// @author       Agent-324
// @icon         data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%236366f1'/%3E%3Cstop offset='100%25' stop-color='%23a855f7'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect rx='20' width='100' height='100' fill='url(%23g)'/%3E%3Cpolygon points='38,25 38,75 78,50' fill='white'/%3E%3C/svg%3E
// @match        *://*/*
// @grant        unsafeWindow
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// @license      GPL
// ==/UserScript==

(function () {
  'use strict';

  /* ═══════════════════════════════════════════
     §1  CONFIGURATION
  ═══════════════════════════════════════════ */
  const CONF = {
    cache: {},
    get(key, fallback) {
      if (this.cache[key] !== undefined) return this.cache[key];
      try { const v = GM_getValue('ve_' + key); this.cache[key] = (v === undefined ? fallback : v); return this.cache[key]; } catch { return fallback; }
    },
    set(key, val) {
      this.cache[key] = val;
      try { GM_setValue('ve_' + key, val); } catch { /* silent */ }
    }
  };

  const defaults = {
    playbackRate: 1,
    volume: 1,
    lastRate: 1,
    skipStep: 5,
    enabled: true,
    guiEnabled: true,
    progressEnabled: true,
    progressMap: {},
    globalMode: true,
  };

  function cfg(key, val) {
    let storageKey = key;
    if (key === 'enabled' || key === 'guiEnabled' || key === 'progressEnabled') {
      storageKey = key + '_' + location.hostname;
    }
    if (val !== undefined) { CONF.set(storageKey, val); return val; }
    return CONF.get(storageKey, defaults[key]);
  }

  /* ═══════════════════════════════════════════
     §2  UTILITIES
  ═══════════════════════════════════════════ */
  function clamp(val, min, max) { return Math.min(Math.max(val, min), max); }
  function formatTime(sec) {
    if (isNaN(sec) || !isFinite(sec)) return "0:00";
    const h = Math.floor(sec / 3600);
    const m = Math.floor((sec % 3600) / 60);
    const s = Math.floor(sec % 60);
    if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
    return `${m}:${s.toString().padStart(2, '0')}`;
  }
  function round1(n) { return Math.round(n * 10) / 10; }
  function round2(n) { return Math.round(n * 100) / 100; }
  function isEditable(el) {
    if (!el || !el.tagName) return false;
    if (/^(INPUT|TEXTAREA|SELECT)$/i.test(el.tagName)) return true;
    return el.isContentEditable;
  }

  function debounce(fn, ms) {
    let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
  }

  /* ═══════════════════════════════════════════
     §3  PLAYER DETECTION & MANAGEMENT
  ═══════════════════════════════════════════ */
  let activePlayer = null;
  let allPlayers = [];

  function isVideoEl(el) { return el instanceof HTMLVideoElement; }

  function findVideos() {
    const vids = Array.from(document.querySelectorAll('video'));
    /* Also check shadow DOMs using a fast loop */
    const hosts = document.querySelectorAll('*');
    for (let i = 0; i < hosts.length; i++) {
      const sr = hosts[i].shadowRoot;
      if (sr) {
        const nested = sr.querySelectorAll('video');
        for (let j = 0; j < nested.length; j++) {
          if (!vids.includes(nested[j])) vids.push(nested[j]);
        }
      }
    }
    return vids;
  }

  function getBestPlayer(vids) {
    if (!vids.length) return null;
    if (vids.length === 1) return vids[0];

    /* Prefer the largest visible video */
    let best = null, bestArea = 0;
    for (const v of vids) {
      const r = v.getBoundingClientRect();
      if (r.width < 50 || r.height < 50) continue;
      const area = r.width * r.height;
      if (area > bestArea) { bestArea = area; best = v; }
    }
    return best || vids[0];
  }

  function refreshPlayers() {
    const found = findVideos();
    for (const v of found) {
      if (!allPlayers.includes(v)) {
        allPlayers.push(v);
        initPlayerEvents(v);
      } else {
        tryAttachGUI(v); /* Ensure GUI stays attached even if DOM changes */
      }
    }
    /* Remove detached */
    allPlayers = allPlayers.filter(v => {
      if (!v.isConnected) {
        const t = veRetryTimers.get(v);
        if (t) { clearTimeout(t); veRetryTimers.delete(v); }
        return false;
      }
      return true;
    });

    if (!activePlayer || !activePlayer.isConnected) {
      activePlayer = getBestPlayer(allPlayers);
    }
  }

  function initPlayerEvents(video) {
    video.addEventListener('mouseenter', () => { setActivePlayer(video); });
    video.addEventListener('playing', () => {
      if (video.duration && video.duration > 5) setActivePlayer(video);
    });
    video.addEventListener('play', () => tryAttachGUI(video));
    video.addEventListener('pause', () => tryAttachGUI(video));
    video.addEventListener('loadeddata', () => tryAttachGUI(video));

    /* Sync rate on play */
    video.addEventListener('playing', () => {
      const rate = cfg('playbackRate');
      if (rate && rate !== 1 && video.playbackRate !== rate) {
        video.playbackRate = rate;
      }
    });

    /* Restore playback progress when video metadata is ready */
    const tryRestore = () => {
      if (video.duration && video.duration > 120) {
        restoreProgress(video);
      }
    };
    video.addEventListener('loadedmetadata', tryRestore);
    video.addEventListener('canplay', tryRestore);
    video.addEventListener('durationchange', tryRestore);

    /* Restore saved volume (including gain) */
    const savedVol = cfg('volume');
    if (savedVol !== undefined && savedVol !== null) {
      if (savedVol > 1) {
        video.volume = 1;
        const amp = getAmplifier(video);
        if (amp) amp.gain.value = savedVol;
      } else if (savedVol >= 0) {
        video.volume = savedVol;
      }
    }

    tryAttachGUI(video);

    /* Watch for video becoming visible / resized (e.g. YouTube SPA transition, theater mode) */
    try {
      const ro = new ResizeObserver(() => tryAttachGUI(video));
      ro.observe(video);
    } catch { }
  }

  function setActivePlayer(v) {
    if (activePlayer === v) return;
    activePlayer = v;
    const rate = cfg('playbackRate');
    if (rate && v.playbackRate !== rate) v.playbackRate = rate;
  }

  function player() {
    if (activePlayer && activePlayer.isConnected) return activePlayer;
    refreshPlayers();
    return activePlayer;
  }

  /* ═══════════════════════════════════════════
     §4  TIPS / NOTIFICATION OVERLAY
  ═══════════════════════════════════════════ */
  let tipsTimer = null;
  let tipsEl = null;

  function tips(msg) {
    const v = player();
    if (!v) return;

    const container = v.parentElement || document.body;

    if (!tipsEl || !tipsEl.isConnected) {
      tipsEl = document.createElement('div');
      tipsEl.className = 've-tips';
      container.appendChild(tipsEl);
    } else if (tipsEl.parentElement !== container) {
      container.appendChild(tipsEl);
    }

    /* Ensure container is positioned */
    const pos = getComputedStyle(container).position;
    if (!pos || pos === 'static') container.style.position = 'relative';

    tipsEl.textContent = msg;
    tipsEl.style.opacity = '1';
    tipsEl.style.transform = 'translateY(0)';

    clearTimeout(tipsTimer);
    tipsTimer = setTimeout(() => {
      if (tipsEl) {
        tipsEl.style.opacity = '0';
        tipsEl.style.transform = 'translateY(-8px)';
      }
    }, 1800);
  }

  /* ═══════════════════════════════════════════
     §5  PLAYER CONTROLS
  ═══════════════════════════════════════════ */

  /* ---- Speed ---- */
  function setRate(rate) {
    const v = player(); if (!v) return;
    rate = clamp(round1(rate), 0.1, 16);
    v.playbackRate = rate;
    cfg('playbackRate', rate);
    tips(`Speed: ${rate}x`);
  }

  function rateUp(step = 0.1) {
    const v = player(); if (!v) return;
    setRate(v.playbackRate + step);
  }

  function rateDown(step = 0.1) {
    const v = player(); if (!v) return;
    setRate(v.playbackRate - step);
  }

  function resetRate() {
    const v = player(); if (!v) return;
    const cur = round1(v.playbackRate);
    if (cur === 1) {
      const last = cfg('lastRate') || 1;
      setRate(last);
    } else {
      cfg('lastRate', cur);
      setRate(1);
    }
  }

  /* Quick rate set with acceleration: pressing same key rapidly increases step */
  const ratePlusInfo = {};
  function setRatePlus(num) {
    num = Number(num); if (!num) return;
    const key = num;
    const now = Date.now();
    if (!ratePlusInfo[key]) ratePlusInfo[key] = { time: 0, value: num };

    if (now - ratePlusInfo[key].time < 300) {
      ratePlusInfo[key].value += num;
    } else {
      ratePlusInfo[key].value = num;
    }
    ratePlusInfo[key].time = now;
    setRate(ratePlusInfo[key].value);
  }

  /* ---- Volume (with gain boost beyond 100%) ---- */
  const ampMap = new WeakMap();

  function getAmplifier(video) {
    if (ampMap.has(video)) return ampMap.get(video);
    try {
      const ctx = new (window.AudioContext || window.webkitAudioContext)();
      const source = ctx.createMediaElementSource(video);
      const gainNode = ctx.createGain();
      source.connect(gainNode);
      gainNode.connect(ctx.destination);
      ampMap.set(video, gainNode);
      return gainNode;
    } catch (e) {
      console.warn('[VE] AudioContext gain failed:', e);
      return null;
    }
  }

  /* Current effective volume (including gain) */
  let currentGainLevel = 1;

  function setVolume(val) {
    const v = player(); if (!v) return;
    val = round2(val);

    if (val > 1) {
      /* Boost mode: set native volume to 1, use gain for amplification */
      val = clamp(val, 0, 6); /* Max 600% */
      v.volume = 1;
      v.muted = false;
      const amp = getAmplifier(v);
      if (amp) {
        amp.gain.value = val;
        currentGainLevel = val;
        cfg('volume', val);
        tips(`🔊 Volume: ${Math.round(val * 100)}%`);
      } else {
        tips('Volume boost unavailable (cross-origin)');
      }
    } else {
      /* Normal mode */
      val = clamp(val, 0, 1);
      v.volume = val;
      v.muted = false;
      /* Reset gain if previously amplified */
      if (currentGainLevel > 1) {
        const amp = ampMap.get(v);
        if (amp) amp.gain.value = 1;
        currentGainLevel = 1;
      }
      cfg('volume', val);
      tips(`Volume: ${Math.round(val * 100)}%`);
    }
  }

  function getEffectiveVolume() {
    const v = player(); if (!v) return 1;
    if (currentGainLevel > 1) return currentGainLevel;
    return v.volume;
  }

  function volumeUp(step = 0.05) {
    const v = player(); if (!v) return;
    const cur = getEffectiveVolume();
    /* At 100%, start using larger steps for gain boost */
    if (cur >= 1 && step <= 0.1) step = 0.2;
    setVolume(cur + step);
  }

  function volumeDown(step = 0.05) {
    const v = player(); if (!v) return;
    const cur = getEffectiveVolume();
    /* When above 100%, use larger steps for gain reduction */
    if (cur > 1 && step <= 0.1) step = 0.2;
    setVolume(cur - step);
  }

  /* ---- Seeking ---- */
  function seekBy(sec) {
    const v = player(); if (!v) return;
    const newTime = clamp(v.currentTime + sec, 0, v.duration || Infinity);
    v.currentTime = newTime;
    const label = sec > 0 ? `Forward: ${sec}s` : `Backward: ${Math.abs(sec)}s`;
    tips(label);
  }

  /* ---- Play/Pause ---- */
  function togglePlay() {
    const v = player(); if (!v) return;
    if (v.paused) {
      v.play().catch(() => { });
      tips('▶ Play');
    } else {
      v.pause();
      tips('⏸ Pause');
    }
  }

  /* ---- Fullscreen ---- */
  function toggleFullscreen() {
    const v = player(); if (!v) return;
    if (document.fullscreenElement) {
      document.exitFullscreen().catch(() => { });
    } else {
      /* Try fullscreening the player's container for better controls */
      const container = v.closest('[class*="player"]') || v.parentElement || v;
      (container.requestFullscreen || container.webkitRequestFullscreen || container.msRequestFullscreen)
        ?.call(container)
        ?.catch(() => v.requestFullscreen?.().catch(() => { }));
    }
  }

  /* ---- Web Fullscreen (fills page) ---- */
  let webFsActive = false;
  let webFsOrigStyles = null;

  function toggleWebFullscreen() {
    const v = player(); if (!v) return;
    const container = v.parentElement;
    if (!container) return;

    if (!webFsActive) {
      webFsOrigStyles = {
        vStyle: v.getAttribute('style') || '',
        cStyle: container.getAttribute('style') || '',
        overflow: document.body.style.overflow
      };
      container.style.cssText = 'position:fixed!important;top:0!important;left:0!important;width:100vw!important;height:100vh!important;z-index:2147483647!important;background:#000!important;';
      v.style.cssText += ';width:100%!important;height:100%!important;object-fit:contain!important;';
      document.body.style.overflow = 'hidden';
      webFsActive = true;
      tips('Web Fullscreen: ON');
    } else {
      container.setAttribute('style', webFsOrigStyles.cStyle);
      v.setAttribute('style', webFsOrigStyles.vStyle);
      document.body.style.overflow = webFsOrigStyles.overflow;
      webFsActive = false;
      tips('Web Fullscreen: OFF');
    }
  }

  /* ---- Picture in Picture ---- */
  function togglePiP() {
    const v = player(); if (!v) return;
    if (document.pictureInPictureElement) {
      document.exitPictureInPicture().catch(() => { });
    } else {
      v.requestPictureInPicture?.().catch(() => { });
    }
  }

  /* ---- Frame-by-frame ---- */
  function freezeFrame(dir = 1) {
    const v = player(); if (!v) return;
    if (!v.paused) v.pause();
    v.currentTime += dir / 30; /* assume 30fps */
    tips(dir > 0 ? 'Next Frame →' : '← Previous Frame');
  }

  /* ---- Screenshot ---- */
  function captureFrame() {
    const v = player(); if (!v) return;
    try {
      const canvas = document.createElement('canvas');
      canvas.width = v.videoWidth;
      canvas.height = v.videoHeight;
      canvas.getContext('2d').drawImage(v, 0, 0);
      canvas.toBlob(blob => {
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `screenshot_${Date.now()}.png`;
        a.click();
        setTimeout(() => URL.revokeObjectURL(url), 5000);
      }, 'image/png');
      tips('📸 Screenshot saved');
    } catch (e) {
      tips('Screenshot failed (cross-origin)');
    }
  }

  /* ═══════════════════════════════════════════
     §6  VIDEO FILTERS & TRANSFORMS
  ═══════════════════════════════════════════ */
  const filters = { brightness: 1, contrast: 1, saturate: 1, hueRotate: 0, blur: 0 };
  let transform = { scale: 1, rotate: 0, rotateY: 0, rotateX: 0, x: 0, y: 0 };

  function applyFilter() {
    const v = player(); if (!v) return;
    v.style.filter = `brightness(${filters.brightness}) contrast(${filters.contrast}) saturate(${filters.saturate}) hue-rotate(${filters.hueRotate}deg) blur(${filters.blur}px)`;
  }

  function applyTransform() {
    const v = player(); if (!v) return;
    const t = transform;
    const mirror = t.rotateX === 180 ? `rotateX(180deg)` : (t.rotateY === 180 ? `rotateY(180deg)` : '');
    v.style.transform = `scale(${t.scale}) translate(${t.x}px, ${t.y}px) rotate(${t.rotate}deg) ${mirror}`;
  }

  function adjustFilter(key, delta, label) {
    filters[key] = round2(filters[key] + delta);
    if (key !== 'hueRotate' && filters[key] < 0) filters[key] = 0;
    applyFilter();
    const display = key === 'hueRotate' ? `${filters[key]}°` : key === 'blur' ? `${filters[key]}px` : `${Math.round(filters[key] * 100)}%`;
    tips(`${label}: ${display}`);
  }

  function resetFilterAndTransform() {
    filters.brightness = 1; filters.contrast = 1; filters.saturate = 1;
    filters.hueRotate = 0; filters.blur = 0;
    transform = { scale: 1, rotate: 0, rotateY: 0, rotateX: 0, x: 0, y: 0 };
    const v = player();
    if (v) { v.style.filter = ''; v.style.transform = ''; }
    tips('🔄 Filters & Transform reset');
  }

  function setRotate90() {
    transform.rotate = (transform.rotate + 90) % 360;
    applyTransform();
    tips(`Rotation: ${transform.rotate}°`);
  }

  function toggleMirrorH() {
    transform.rotateY = transform.rotateY === 0 ? 180 : 0;
    applyTransform();
    tips(`H-Mirror: ${transform.rotateY ? 'ON' : 'OFF'}`);
  }

  function toggleMirrorV() {
    transform.rotateX = transform.rotateX === 0 ? 180 : 0;
    applyTransform();
    tips(`V-Mirror: ${transform.rotateX ? 'ON' : 'OFF'}`);
  }

  function scaleUp() { transform.scale = round2(transform.scale + 0.05); applyTransform(); tips(`Zoom: ${Math.round(transform.scale * 100)}%`); }
  function scaleDown() { transform.scale = round2(Math.max(0.1, transform.scale - 0.05)); applyTransform(); tips(`Zoom: ${Math.round(transform.scale * 100)}%`); }
  function resetTransform() { transform = { scale: 1, rotate: 0, rotateY: 0, rotateX: 0, x: 0, y: 0 }; applyTransform(); tips('Transform reset'); }
  function translateR() { transform.x += 10; applyTransform(); tips(`Move X: ${transform.x}px`); }
  function translateL() { transform.x -= 10; applyTransform(); tips(`Move X: ${transform.x}px`); }
  function translateU() { transform.y -= 10; applyTransform(); tips(`Move Y: ${transform.y}px`); }
  function translateD() { transform.y += 10; applyTransform(); tips(`Move Y: ${transform.y}px`); }

  /* ═══════════════════════════════════════════
     §7  PLAYBACK PROGRESS SAVE / RESTORE
  ═══════════════════════════════════════════ */
  let hasRestoredFor = '';

  function getProgressKey(v) {
    if (location.hostname.includes('youtube.com')) {
      try {
        const url = new URL(location.href);
        const vId = url.searchParams.get('v') || url.pathname.split('/shorts/')[1] || url.pathname.split('/embed/')[1];
        if (vId) return 'yt_' + vId;
      } catch { }
    }
    return location.href.split('#')[0].split('?')[0] + '::' + Math.round(v.duration || 0);
  }

  function saveProgress() {
    const v = player();
    if (!v || !v.duration || v.duration < 120 || !cfg('progressEnabled')) return;

    const map = cfg('progressMap') || {};
    const key = getProgressKey(v);
    map[key] = { time: v.currentTime, dur: v.duration, ts: Date.now() };

    /* Keep only last 100 entries */
    const entries = Object.entries(map);
    if (entries.length > 100) {
      entries.sort((a, b) => a[1].ts - b[1].ts);
      entries.slice(0, entries.length - 100).forEach(([k]) => delete map[k]);
    }
    cfg('progressMap', map);
  }

  function restoreProgress(v) {
    if (!v || !v.duration || v.duration < 120 || !cfg('progressEnabled')) return;
    const key = getProgressKey(v);
    if (hasRestoredFor === key) return;

    const map = cfg('progressMap') || {};
    const saved = map[key];
    if (!saved || saved.time < 10 || saved.time >= v.duration - 5) return;
    if (Math.abs(saved.time - v.currentTime) < 3) return;

    v.currentTime = saved.time - 1.5;
    hasRestoredFor = key;
    tips('📍 Playback progress restored');
  }

  function toggleProgressSave() {
    const newVal = !cfg('progressEnabled');
    cfg('progressEnabled', newVal);
    tips(newVal ? '📍 Progress save: ON' : '📍 Progress save: OFF');
  }

  /* Periodically save progress */
  setInterval(saveProgress, 1000);

  /* ═══════════════════════════════════════════
     §8  CONFIGURABLE KEYBOARD SHORTCUTS
  ═══════════════════════════════════════════ */

  /* Action registry: all bindable actions */
  const ACTIONS = {
    seekForward: { fn: () => seekBy(cfg('skipStep')), label: 'Seek Forward' },
    seekBackward: { fn: () => seekBy(-cfg('skipStep')), label: 'Seek Backward' },
    seekForward30: { fn: () => seekBy(30), label: 'Seek Forward 30s' },
    seekBackward30: { fn: () => seekBy(-30), label: 'Seek Backward 30s' },
    volumeUp: { fn: () => volumeUp(0.05), label: 'Volume Up' },
    volumeDown: { fn: () => volumeDown(0.05), label: 'Volume Down' },
    volumeUpBig: { fn: () => volumeUp(0.2), label: 'Volume Up (Big)' },
    volumeDownBig: { fn: () => volumeDown(0.2), label: 'Volume Down (Big)' },
    togglePlay: { fn: () => togglePlay(), label: 'Play / Pause' },
    rateUp: { fn: () => rateUp(), label: 'Speed Up' },
    rateDown: { fn: () => rateDown(), label: 'Speed Down' },
    rateReset: { fn: () => resetRate(), label: 'Speed Reset / Toggle' },
    nextFrame: { fn: () => freezeFrame(1), label: 'Next Frame' },
    prevFrame: { fn: () => freezeFrame(-1), label: 'Previous Frame' },
    brightnessUp: { fn: () => adjustFilter('brightness', 0.1, 'Brightness'), label: 'Brightness +' },
    brightnessDown: { fn: () => adjustFilter('brightness', -0.1, 'Brightness'), label: 'Brightness −' },
    contrastUp: { fn: () => adjustFilter('contrast', 0.1, 'Contrast'), label: 'Contrast +' },
    contrastDown: { fn: () => adjustFilter('contrast', -0.1, 'Contrast'), label: 'Contrast −' },
    saturationUp: { fn: () => adjustFilter('saturate', 0.1, 'Saturation'), label: 'Saturation +' },
    saturationDown: { fn: () => adjustFilter('saturate', -0.1, 'Saturation'), label: 'Saturation −' },
    hueUp: { fn: () => adjustFilter('hueRotate', 1, 'Hue'), label: 'Hue +' },
    hueDown: { fn: () => adjustFilter('hueRotate', -1, 'Hue'), label: 'Hue −' },
    blurUp: { fn: () => adjustFilter('blur', 1, 'Blur'), label: 'Blur +' },
    blurDown: { fn: () => adjustFilter('blur', -1, 'Blur'), label: 'Blur −' },
    resetAll: { fn: () => resetFilterAndTransform(), label: 'Reset Filters' },
    rotate90: { fn: () => setRotate90(), label: 'Rotate 90°' },
    mirrorH: { fn: () => toggleMirrorH(), label: 'Mirror Horizontal' },
    mirrorV: { fn: () => toggleMirrorV(), label: 'Mirror Vertical' },
    fullscreen: { fn: () => toggleFullscreen(), label: 'Fullscreen' },
    webFullscreen: { fn: () => toggleWebFullscreen(), label: 'Web Fullscreen' },
    pip: { fn: () => togglePiP(), label: 'Picture-in-Picture' },
    capture: { fn: () => captureFrame(), label: 'Screenshot' },
    toggleProgress: { fn: () => toggleProgressSave(), label: 'Toggle Progress Save' },
    scaleUp: { fn: () => scaleUp(), label: 'Zoom In' },
    scaleDown: { fn: () => scaleDown(), label: 'Zoom Out' },
    resetTransform: { fn: () => resetTransform(), label: 'Reset Transform' },
    translateRight: { fn: () => translateR(), label: 'Move Right' },
    translateLeft: { fn: () => translateL(), label: 'Move Left' },
    translateUp: { fn: () => translateU(), label: 'Move Up' },
    translateDown: { fn: () => translateD(), label: 'Move Down' },
  };

  /* Default key bindings: "modifier+key" → actionName */
  const DEFAULT_BINDINGS = {
    'arrowright': 'seekForward',
    'arrowleft': 'seekBackward',
    'ctrl+arrowright': 'seekForward30',
    'ctrl+arrowleft': 'seekBackward30',
    'arrowup': 'volumeUp',
    'arrowdown': 'volumeDown',
    'ctrl+arrowup': 'volumeUpBig',
    'ctrl+arrowdown': 'volumeDownBig',
    ' ': 'togglePlay',
    'x': 'rateDown',
    'c': 'rateUp',
    'z': 'rateReset',
    'f': 'nextFrame',
    'd': 'prevFrame',
    'e': 'brightnessUp',
    'w': 'brightnessDown',
    't': 'contrastUp',
    'r': 'contrastDown',
    'u': 'saturationUp',
    'y': 'saturationDown',
    'o': 'hueUp',
    'i': 'hueDown',
    'k': 'blurUp',
    'j': 'blurDown',
    'q': 'resetAll',
    's': 'rotate90',
    'm': 'mirrorH',
    'enter': 'fullscreen',
    'shift+enter': 'webFullscreen',
    'shift+p': 'pip',
    'shift+s': 'capture',
    'shift+r': 'toggleProgress',
    'shift+m': 'mirrorV',
    'shift+c': 'scaleUp',
    'shift+x': 'scaleDown',
    'shift+z': 'resetTransform',
    'shift+arrowright': 'translateRight',
    'shift+arrowleft': 'translateLeft',
    'shift+arrowup': 'translateUp',
    'shift+arrowdown': 'translateDown',
  };

  function getBindings() {
    return cfg('keyBindings') || { ...DEFAULT_BINDINGS };
  }

  function saveBindings(bindings) {
    cfg('keyBindings', bindings);
  }

  /* Convert a KeyboardEvent into a binding string like "ctrl+shift+a" */
  function eventToBindingKey(e) {
    const parts = [];
    if (e.ctrlKey || e.metaKey) parts.push('ctrl');
    if (e.altKey) parts.push('alt');
    if (e.shiftKey) parts.push('shift');
    const key = e.key.toLowerCase();
    if (!['control', 'alt', 'shift', 'meta'].includes(key)) parts.push(key);
    return parts.join('+');
  }

  function handleKeydown(e) {
    if (!cfg('enabled')) return;
    /* Don't intercept when settings modal is open */
    if (document.querySelector('.ve-settings-overlay')) return;

    const target = e.composedPath?.()?.[0] || e.target;
    if (isEditable(target)) return;

    const v = player();
    if (!v) return;

    const bindingKey = eventToBindingKey(e);
    const bindings = getBindings();
    const actionName = bindings[bindingKey];

    /* Handle 1-4 quick speed (always active, not rebindable) */
    if (!actionName && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
      const code = e.keyCode;
      if ((code >= 49 && code <= 52) || (code >= 97 && code <= 100)) {
        setRatePlus(e.key);
        e.stopPropagation();
        e.preventDefault();
        return;
      }
    }

    if (actionName && ACTIONS[actionName]) {
      e.stopPropagation();
      e.preventDefault();
      ACTIONS[actionName].fn();
    }
  }

  /* ═══════════════════════════════════════════
     §9  STYLES
  ═══════════════════════════════════════════ */
  function injectStyles() {
    const css = `
/* ─── Tips notification ─── */
.ve-tips {
  position: absolute;
  top: 16px;
  left: 16px;
  z-index: 2147483640;
  padding: 8px 16px;
  font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
  font-size: 14px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: #fff;
  background: linear-gradient(135deg, rgba(99,102,241,0.85), rgba(168,85,247,0.85));
  backdrop-filter: blur(12px) saturate(1.5);
  -webkit-backdrop-filter: blur(12px) saturate(1.5);
  border: 1px solid rgba(255,255,255,0.15);
  border-radius: 10px;
  box-shadow: 0 8px 32px rgba(99,102,241,0.3), 0 0 0 1px rgba(255,255,255,0.05) inset;
  pointer-events: none;
  user-select: none;
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity 0.35s cubic-bezier(.4,0,.2,1), transform 0.35s cubic-bezier(.4,0,.2,1);
  white-space: nowrap;
  text-shadow: 0 1px 2px rgba(0,0,0,0.2);
}

/* ─── GUI Toolbar ─── */
.ve-toolbar-wrap {
  position: fixed;
  z-index: 2147483647; /* Max int */
  display: flex;
  justify-content: center;
  pointer-events: none;
  padding-bottom: 12px;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.4s cubic-bezier(.4,0,.2,1), transform 0.4s cubic-bezier(.4,0,.2,1);
}

.ve-toolbar-wrap.ve-visible {
  opacity: 1 !important;
  transform: translateY(0) !important;
}

.ve-toolbar {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 5px 8px;
  background: linear-gradient(135deg, rgba(15,15,35,0.75), rgba(30,20,60,0.7));
  backdrop-filter: blur(20px) saturate(1.8);
  -webkit-backdrop-filter: blur(20px) saturate(1.8);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 14px;
  box-shadow:
    0 8px 32px rgba(0,0,0,0.35),
    0 0 0 1px rgba(255,255,255,0.03) inset,
    0 1px 0 rgba(255,255,255,0.06) inset;
  pointer-events: auto;
  user-select: none;
  font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
  transition: all 0.25s cubic-bezier(.4,0,.2,1);
}

/* Compact Mode / Single Icon Mode */
.ve-toolbar.ve-compact {
  padding: 0;
  width: 44px;
  height: 44px;
  border-radius: 22px;
  opacity: 0.8;
  cursor: pointer;
  justify-content: center;
}
.ve-toolbar.ve-compact:hover {
  opacity: 1;
}
.ve-toolbar.ve-compact > :not(.ve-brand) {
  display: none !important;
}
.ve-toolbar.ve-compact .ve-brand {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  line-height: 1;
  letter-spacing: normal;
  display: flex !important;
  align-items: center;
  justify-content: center;
  font-size: 15px;
}

/* Toolbar buttons */
.ve-btn {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 32px;
  border: none;
  background: transparent;
  color: rgba(255,255,255,0.8);
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.2s cubic-bezier(.4,0,.2,1);
  padding: 0;
  outline: none;
  font-size: 13px;
  font-weight: 600;
  font-family: inherit;
  line-height: 1;
}
.ve-btn:hover {
  background: rgba(255,255,255,0.1);
  color: #fff;
  transform: translateY(-1px);
}
.ve-btn:active {
  transform: translateY(0) scale(0.95);
  background: rgba(255,255,255,0.15);
}
.ve-btn svg {
  width: 18px;
  height: 18px;
  fill: currentColor;
  flex-shrink: 0;
}

/* Playback Control Panel */
.ve-panel-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.ve-panel-row:last-child { margin-bottom: 0; }
.ve-panel-btn { background: rgba(255,255,255,0.1); border: none; border-radius: 6px; color: #fff; padding: 6px 10px; cursor: pointer; font-weight: 600; font-size: 13px; flex: 1; text-align: center; transition: all 0.2s; white-space: nowrap; outline: none; }
.ve-panel-btn:hover { background: rgba(255,255,255,0.2); transform: translateY(-1px); }
.ve-panel-btn:active { transform: translateY(0); }
.ve-slider { flex: 1; -webkit-appearance: none; height: 4px; background: rgba(255,255,255,0.2); border-radius: 2px; outline: none; cursor: pointer; }
.ve-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; background: #c084fc; border-radius: 50%; cursor: pointer; box-shadow: 0 0 5px rgba(0,0,0,0.5); }
.ve-panel-label { color: rgba(255,255,255,0.8); font-size: 12px; font-weight: 700; min-width: 45px; text-align: right; font-variant-numeric: tabular-nums; }

/* Dropdown */
.ve-dropdown-wrap {
  position: relative;
}
.ve-dropdown {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%) scale(0.95);
  background: linear-gradient(180deg, rgba(20,15,45,0.95), rgba(10,8,30,0.98));
  backdrop-filter: blur(24px) saturate(1.6);
  -webkit-backdrop-filter: blur(24px) saturate(1.6);
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 12px;
  padding: 6px;
  min-width: 120px;
  box-shadow:
    0 12px 40px rgba(0,0,0,0.5),
    0 0 0 1px rgba(255,255,255,0.04) inset;
  opacity: 0;
  pointer-events: none;
  transition: all 0.25s cubic-bezier(.4,0,.2,1);
  z-index: 2147483641;
  max-height: var(--ve-max-h, 140px);
  overflow-y: auto;
  scrollbar-width: none;
}
.ve-dropdown::-webkit-scrollbar {
  display: none;
}
.ve-dropdown.ve-open {
  opacity: 1;
  transform: translateX(-50%) scale(1);
  pointer-events: auto;
}
.ve-dropdown.ve-drop-down {
  top: calc(100% + 8px);
  bottom: auto;
}
.ve-dropdown-item {
  display: block;
  width: 100%;
  padding: 7px 14px;
  border: none;
  background: transparent;
  color: rgba(255,255,255,0.8);
  font-size: 13px;
  font-weight: 500;
  font-family: inherit;
  text-align: left;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.15s;
  white-space: nowrap;
  outline: none;
}
.ve-dropdown-item:hover {
  background: linear-gradient(135deg, rgba(99,102,241,0.3), rgba(168,85,247,0.3));
  color: #fff;
}
.ve-dropdown-item.ve-active {
  background: linear-gradient(135deg, rgba(99,102,241,0.5), rgba(168,85,247,0.5));
  color: #fff;
  font-weight: 700;
}

.ve-separator {
  width: 1px;
  height: 20px;
  background: rgba(255,255,255,0.08);
  margin: 0 3px;
  flex-shrink: 0;
}

/* ─── Branding ─── */
.ve-brand {
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: linear-gradient(135deg, #818cf8, #c084fc);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  padding: 0 5px;
  cursor: default;
  line-height: 1;
}

/* ─── Responsive: small video ─── */
.ve-toolbar.ve-compact .ve-btn-label {
  display: none;
}
.ve-toolbar.ve-compact {
  gap: 1px;
  padding: 4px 6px;
}
.ve-toolbar.ve-compact .ve-btn {
  width: 30px;
  height: 28px;
}
.ve-toolbar.ve-compact .ve-speed-badge {
  min-width: 36px;
  font-size: 11px;
  padding: 3px 5px;
}

/* ─── Settings Modal (Shortcuts) ─── */
.ve-settings-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.6);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  z-index: 2147483647;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif;
  opacity: 0;
  animation: ve-fade-in 0.2s forwards;
}

@keyframes ve-fade-in { to { opacity: 1; } }

.ve-settings-modal {
  background: linear-gradient(180deg, rgba(20,15,45,0.95), rgba(10,8,30,0.98));
  border: 1px solid rgba(255,255,255,0.1);
  border-radius: 16px;
  width: 90%;
  max-width: 500px;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  box-shadow: 0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04) inset;
  transform: scale(0.95);
  animation: ve-scale-up 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes ve-scale-up { to { transform: scale(1); } }

.ve-settings-header {
  padding: 20px 24px;
  border-bottom: 1px solid rgba(255,255,255,0.08);
}
.ve-settings-title {
  display: block;
  font-size: 18px;
  font-weight: 700;
  color: #fff;
  margin-bottom: 4px;
}
.ve-settings-subtitle {
  display: block;
  font-size: 13px;
  color: rgba(255,255,255,0.6);
}

.ve-settings-list {
  padding: 12px 24px;
  overflow-y: auto;
  flex-grow: 1;
}
.ve-settings-list::-webkit-scrollbar {
  width: 8px;
}
.ve-settings-list::-webkit-scrollbar-thumb {
  background: rgba(255,255,255,0.2);
  border-radius: 4px;
}

.ve-settings-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 0;
  border-bottom: 1px solid rgba(255,255,255,0.05);
}
.ve-settings-row:last-child {
  border-bottom: none;
}
.ve-settings-label {
  font-size: 14px;
  color: rgba(255,255,255,0.85);
  font-weight: 500;
}
.ve-settings-key {
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.1);
  color: #c084fc;
  font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
  font-size: 13px;
  font-weight: 600;
  padding: 6px 12px;
  border-radius: 6px;
  cursor: pointer;
  transition: all 0.2s;
  min-width: 100px;
  text-align: center;
}
.ve-settings-key:hover {
  background: rgba(255,255,255,0.15);
  color: #fff;
}
.ve-settings-key.ve-recording {
  background: linear-gradient(135deg, rgba(239,68,68,0.2), rgba(249,115,22,0.2));
  border-color: rgba(239,68,68,0.5);
  color: #fca5a5;
  box-shadow: 0 0 12px rgba(239,68,68,0.3);
  animation: ve-pulse 1.5s infinite;
}
@keyframes ve-pulse {
  0% { box-shadow: 0 0 0 0 rgba(239,68,68,0.4); }
  70% { box-shadow: 0 0 0 10px rgba(239,68,68,0); }
  100% { box-shadow: 0 0 0 0 rgba(239,68,68,0); }
}

.ve-settings-footer {
  padding: 16px 24px;
  border-top: 1px solid rgba(255,255,255,0.08);
  display: flex;
  justify-content: flex-end;
  gap: 12px;
}
.ve-settings-btn {
  padding: 8px 16px;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
  font-family: inherit;
  outline: none;
}
.ve-reset-btn {
  background: transparent;
  color: rgba(255,255,255,0.6);
  border: 1px solid rgba(255,255,255,0.2);
  margin-right: auto;
}
.ve-reset-btn:hover {
  background: rgba(255,255,255,0.1);
  color: #fff;
}
.ve-close-btn {
  background: linear-gradient(135deg, #6366f1, #a855f7);
  color: #fff;
  border: none;
  box-shadow: 0 4px 12px rgba(168,85,247,0.3);
}
.ve-close-btn:hover {
  box-shadow: 0 6px 16px rgba(168,85,247,0.5);
  transform: translateY(-1px);
}
`;
    if (typeof GM_addStyle === 'function') {
      GM_addStyle(css);
    } else {
      const style = document.createElement('style');
      style.textContent = css;
      (document.head || document.documentElement).appendChild(style);
    }
  }

  /* ═══════════════════════════════════════════
     §10  SVG ICONS
  ═══════════════════════════════════════════ */
  const ICONS = {
    capture: `<svg viewBox="0 0 24 24"><path d="M9 2L7.17 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2h-3.17L15 2H9zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/></svg>`,
    pip: `<svg viewBox="0 0 24 24"><path d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z"/></svg>`,
    speed: `<svg viewBox="0 0 24 24"><path d="M20.38 8.57l-1.23 1.85a8 8 0 0 1-.22 7.58H5.07A8 8 0 0 1 15.58 6.85l1.85-1.23A10 10 0 0 0 3.35 19a2 2 0 0 0 1.72 1h13.85a2 2 0 0 0 1.74-1 10 10 0 0 0-.27-10.44zm-9.79 6.84a2 2 0 0 0 2.83 0l5.66-8.49-8.49 5.66a2 2 0 0 0 0 2.83z"/></svg>`,
    filter: `<svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM12 18c-1.3 0-2.5-.4-3.4-1.1l1.5-1.5c.5.4 1.2.6 1.9.6 1.7 0 3-1.3 3-3s-1.3-3-3-3c-.7 0-1.4.2-1.9.6L8.6 9.1C9.5 8.4 10.7 8 12 8c2.8 0 5 2.2 5 5s-2.2 5-5 5z"/></svg>`,
    menu: `<svg viewBox="0 0 24 24"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg>`,
    reset: `<svg viewBox="0 0 24 24"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/></svg>`,
    fullscreen: `<svg viewBox="0 0 24 24"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>`,
    rotate: `<svg viewBox="0 0 24 24"><path d="M7.11 8.53L5.7 7.11C4.8 8.27 4.24 9.61 4.07 11h2.02c.14-.87.49-1.72 1.02-2.47zM6.09 13H4.07c.17 1.39.72 2.73 1.62 3.89l1.41-1.42c-.52-.75-.87-1.59-1.01-2.47zm1.01 5.32c1.16.9 2.51 1.44 3.9 1.61V17.9c-.87-.15-1.71-.49-2.46-1.03L7.1 18.32zM13 4.07V1L8.45 5.55 13 10V6.09c2.84.48 5 2.94 5 5.91s-2.16 5.43-5 5.91v2.02c3.95-.49 7-3.85 7-7.93s-3.05-7.44-7-7.93z"/></svg>`,
    mirror: `<svg viewBox="0 0 24 24"><path d="M15 21h2v-2h-2v2zm4-12h2V7h-2v2zM3 5v14c0 1.1.9 2 2 2h4v-2H5V5h4V3H5c-1.1 0-2 .9-2 2zm16-2v2h2c0-1.1-.9-2-2-2zm-8-2h2v22h-2V1zm8 10h2v-2h-2v2zm0 8c1.1 0 2-.9 2-2h-2v2zm0-12h2V5h-2v2zm0 8h2v-2h-2v2z"/></svg>`,
  };

  /* ═══════════════════════════════════════════
     §11  GUI TOOLBAR
  ═══════════════════════════════════════════ */
  const guiMap = new WeakMap();
  const veRetryTimers = new WeakMap();
  let openDropdown = null;
  const isYouTube = location.hostname.includes('youtube.com');
  let lastYTUrl = location.href;

  function closeAllDropdowns() {
    document.querySelectorAll('.ve-dropdown.ve-open').forEach(d => d.classList.remove('ve-open'));
    openDropdown = null;
  }

  /* Find the best container to insert our toolbar into */
  function getGUIContainer(video) {
    if (isYouTube) {
      /* YouTube: #movie_player is the most stable container */
      const mp = document.getElementById('movie_player')
        || video.closest('#movie_player')
        || video.closest('.html5-video-player')
        || video.closest('[id^="movie_player"]');
      if (mp) return mp;
    }
    /* Generic: try to find a player-like wrapper, fallback to parentElement */
    return video.closest('[class*="player"][style*="position"]')
      || video.closest('[class*="player"]')
      || video.parentElement;
  }

  function tryAttachGUI(video, _retryCount) {
    if (!cfg('guiEnabled')) return;
    if (!video || !video.isConnected) return;

    const r = video.getBoundingClientRect();
    if (r.width < 120 || r.height < 80) {
      /* Video not sized yet (common during YouTube SPA transitions) — retry with backoff */
      if (veRetryTimers.has(video)) return; /* already retrying */
      if (_retryCount === undefined) _retryCount = 0;
      if (_retryCount < 10) {
        const delay = 200 + _retryCount * 200;
        veRetryTimers.set(video, setTimeout(() => {
          veRetryTimers.delete(video);
          tryAttachGUI(video, _retryCount + 1);
        }, delay));
      }
      return;
    }

    if (guiMap.has(video)) {
      const existing = guiMap.get(video);
      if (existing.isConnected) return;
      existing.remove();
      guiMap.delete(video);
    }

    createGUI(video);
  }

  /* YouTube SPA: re-attach GUI when URL changes */
  if (isYouTube) {
    document.addEventListener('yt-navigate-finish', () => {
      setTimeout(refreshPlayers, 500);
      setTimeout(refreshPlayers, 1500);
      setTimeout(refreshPlayers, 3000);
      /* Robust resume on YouTube via SPA */
      setTimeout(() => restoreProgress(player()), 1000);
      setTimeout(() => restoreProgress(player()), 2500);
    });

    setInterval(() => {
      if (location.href !== lastYTUrl) {
        lastYTUrl = location.href;
        setTimeout(refreshPlayers, 800);
        setTimeout(refreshPlayers, 2000);
      }
    }, 500);
  }

  function createGUI(video) {
    const container = getGUIContainer(video);
    if (!container) return;

    /* Ensure container is positioned */
    const pos = getComputedStyle(container).position;
    if (!pos || pos === 'static') container.style.position = 'relative';

    const wrap = document.createElement('div');
    wrap.className = 've-toolbar-wrap';
    wrap.style.cssText = 'position: absolute !important; z-index: 2147483647 !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; pointer-events: none !important;';

    const toolbar = document.createElement('div');
    toolbar.className = 've-toolbar';
    toolbar.style.cssText = 'pointer-events: auto !important; position: absolute !important; z-index: 2147483647 !important;';

    const updateCompact = () => {
      toolbar.classList.toggle('ve-compact', cfg('forceCompact'));
    };
    updateCompact();

    /* ─── Draggable Toolbar Logic ─── */
    let isDragging = false;
    let hasDragged = false;
    let dragStartX = 0, dragStartY = 0;
    let initialDragX = 0, initialDragY = 0;
    let currentDragX = 0;
    let currentDragY = 0;

    toolbar.style.cursor = 'grab';

    toolbar.addEventListener('mousedown', (e) => {
      if (e.target.closest('button, input, .ve-slider, .ve-dropdown') && !e.target.closest('.ve-brand')) return;
      e.preventDefault();
      isDragging = true;
      hasDragged = false;
      toolbar.style.cursor = 'grabbing';
      
      // Convert current display anchors back to physical top/left for pure free-drag manipulation
      toolbar.style.transition = 'none';
      const tbBase = toolbar.getBoundingClientRect();
      const wpBase = wrap.getBoundingClientRect();
      currentDragX = tbBase.left - wpBase.left;
      currentDragY = tbBase.top - wpBase.top;
      
      dragStartX = e.clientX - currentDragX;
      dragStartY = e.clientY - currentDragY;
      initialDragX = currentDragX;
      initialDragY = currentDragY;
      
      // Override flex anchors to absolute coordinates during dragging
      toolbar.style.bottom = 'auto'; toolbar.style.right = 'auto'; toolbar.style.transform = 'none';
      toolbar.style.left = currentDragX + 'px';
      toolbar.style.top = currentDragY + 'px';
    });

    window.addEventListener('mousemove', (e) => {
      if (!isDragging) return;
      currentDragX = e.clientX - dragStartX;
      currentDragY = e.clientY - dragStartY;
      if (Math.abs(currentDragX - initialDragX) > 3 || Math.abs(currentDragY - initialDragY) > 3) {
        hasDragged = true;
      }
      toolbar.style.left = currentDragX + 'px';
      toolbar.style.top = currentDragY + 'px';
    });

    window.addEventListener('mouseup', () => {
      if (!isDragging) return;
      isDragging = false;
      toolbar.style.cursor = 'grab';
      toolbar.style.transition = '';
      
      const wr = wrap.getBoundingClientRect();
      const cx = currentDragX + (toolbar.offsetWidth / 2);
      const cy = currentDragY + (toolbar.offsetHeight / 2);
      
      let vAnchor = 'middle';
      if (cy < wr.height / 3) vAnchor = 'top';
      else if (cy > wr.height * 2 / 3) vAnchor = 'bottom';
      
      let hAnchor = 'center';
      if (cx < wr.width / 3) hAnchor = 'left';
      else if (cx > wr.width * 2 / 3) hAnchor = 'right';
      
      cfg('anchor', (vAnchor === 'middle' && hAnchor === 'center') ? 'bottom-center' : `${vAnchor}-${hAnchor}`);
      veUpdatePos();
    });

    /* ─── Brand ─── */
    const brand = document.createElement('span');
    brand.className = 've-brand';
    brand.textContent = 'VE';
    brand.title = 'Drag to move, click to toggle size';
    brand.style.cursor = 'grab';
    brand.addEventListener('click', (e) => {
      e.stopPropagation();
      if (hasDragged) { hasDragged = false; return; }
      cfg('forceCompact', !cfg('forceCompact'));
      updateCompact();
      closeAllDropdowns();
    });
    toolbar.appendChild(brand);

    addSep(toolbar);

    /* ─── Capture button ─── */
    addBtn(toolbar, ICONS.capture, 'Screenshot (Shift+S)', () => captureFrame());

    /* ─── Playback Control Panel ─── */
    const playWrap = document.createElement('div');
    playWrap.className = 've-dropdown-wrap';

    const playBtn = document.createElement('button');
    playBtn.className = 've-btn';
    playBtn.title = 'Playback Controls';
    playBtn.innerHTML = secureHTML(ICONS.speed);
    playWrap.appendChild(playBtn);

    const playPanel = document.createElement('div');
    playPanel.className = 've-dropdown';
    playPanel.style.minWidth = '220px';
    playPanel.style.padding = '12px';
    playPanel.innerHTML = secureHTML(`
      <div class="ve-panel-row" style="margin-bottom: 12px;" title="Seek Video">
        <span class="ve-panel-label ve-time-cur" style="opacity:0.8; min-width: 35px; text-align:left; font-size:11px;">0:00</span>
        <input type="range" class="ve-slider ve-progress-slider" min="0" max="1000" step="1" value="0" style="background: rgba(255,255,255,0.3);">
        <span class="ve-panel-label ve-time-max" style="opacity:0.8; min-width: 35px; font-size:11px;">0:00</span>
      </div>
      <div class="ve-panel-row">
        <button class="ve-panel-btn ve-rw">⏪ -10s</button>
        <button class="ve-panel-btn ve-pp">⏯️ Play</button>
        <button class="ve-panel-btn ve-fw">+10s ⏩</button>
      </div>
      <div class="ve-panel-row" title="Playback Speed">
        <span style="font-size:12px; opacity:0.8;">🚀</span>
        <input type="range" class="ve-slider ve-speed-slider" min="0.1" max="5" step="0.1" value="1">
        <span class="ve-panel-label ve-speed-val">1.0x</span>
      </div>
      <div class="ve-panel-row" title="Volume Amplification">
        <span style="font-size:12px; opacity:0.8;">🔊</span>
        <input type="range" class="ve-slider ve-vol-slider" min="0" max="6" step="0.05" value="1">
        <span class="ve-panel-label ve-vol-val">100%</span>
      </div>
    `);

    // Attach logic seamlessly to the new DOM elements inside playPanel
    playPanel.querySelector('.ve-rw').addEventListener('click', (e) => { e.stopPropagation(); seekBy(-10); });
    playPanel.querySelector('.ve-pp').addEventListener('click', (e) => { e.stopPropagation(); togglePlay(); });
    playPanel.querySelector('.ve-fw').addEventListener('click', (e) => { e.stopPropagation(); seekBy(10); });

    const progressSlider = playPanel.querySelector('.ve-progress-slider');
    const timeCur = playPanel.querySelector('.ve-time-cur');
    const timeMax = playPanel.querySelector('.ve-time-max');
    let isScrubbing = false;

    progressSlider.addEventListener('input', (e) => {
      e.stopPropagation();
      isScrubbing = true;
      if (video.duration) {
        const targetTime = (parseFloat(progressSlider.value) / 1000) * video.duration;
        timeCur.textContent = formatTime(targetTime);
      }
    });
    progressSlider.addEventListener('change', (e) => {
      e.stopPropagation();
      isScrubbing = false;
      if (video.duration) {
        video.currentTime = (parseFloat(progressSlider.value) / 1000) * video.duration;
      }
    });

    video.addEventListener('timeupdate', () => {
      if (playPanel.classList.contains('ve-open') && !isScrubbing && video.duration) {
        const p = (video.currentTime / video.duration) * 1000;
        progressSlider.value = p;
        timeCur.textContent = formatTime(video.currentTime);
        timeMax.textContent = formatTime(video.duration);
      }
    });

    const speedSlider = playPanel.querySelector('.ve-speed-slider');
    const speedVal = playPanel.querySelector('.ve-speed-val');
    speedSlider.addEventListener('input', (e) => {
      e.stopPropagation();
      const val = parseFloat(speedSlider.value);
      setRate(val);
      speedVal.textContent = round1(val) + 'x';
    });

    const volSlider = playPanel.querySelector('.ve-vol-slider');
    const volVal = playPanel.querySelector('.ve-vol-val');
    volSlider.addEventListener('input', (e) => {
      e.stopPropagation();
      const val = parseFloat(volSlider.value);
      setVolume(val);
      volVal.textContent = Math.round(val * 100) + '%';
    });

    playWrap.appendChild(playPanel);
    toolbar.appendChild(playWrap);

    playBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      const isOpen = playPanel.classList.contains('ve-open');
      closeAllDropdowns();
      if (!isOpen) {
        speedSlider.value = round1(video.playbackRate || 1);
        speedVal.textContent = speedSlider.value + 'x';
        volSlider.value = round2(getEffectiveVolume());
        volVal.textContent = Math.round(volSlider.value * 100) + '%';
        syncDropdownDirection(playPanel);
        playPanel.classList.add('ve-open');
        openDropdown = playPanel;
      }
    });

    addSep(toolbar);

    /* ─── PiP button ─── */
    addBtn(toolbar, ICONS.pip, 'Picture-in-Picture (Shift+P)', () => togglePiP());

    /* ─── Fullscreen button ─── */
    addBtn(toolbar, ICONS.fullscreen, 'Fullscreen (Enter)', () => toggleFullscreen());

    addSep(toolbar);

    /* ─── Menu dropdown (filters, transform, etc.) ─── */
    const menuWrap = document.createElement('div');
    menuWrap.className = 've-dropdown-wrap';

    const menuBtn = document.createElement('button');
    menuBtn.className = 've-btn';
    menuBtn.title = 'More Options';
    menuBtn.innerHTML = secureHTML(ICONS.menu);
    menuWrap.appendChild(menuBtn);

    const menuDropdown = document.createElement('div');
    menuDropdown.className = 've-dropdown';
    menuDropdown.style.minWidth = '160px';

    const syncDropdownDirection = (dropdown) => {
      const anchor = String(cfg('anchor') || 'bottom-center');
      let shouldDropDown;
      if (anchor.includes('top')) {
        shouldDropDown = true;
      } else if (anchor.includes('bottom')) {
        shouldDropDown = false;
      } else {
        /* Middle anchor — fall back to space-based detection */
        const wrapRect = wrap.getBoundingClientRect();
        const parentRect = dropdown.parentElement.getBoundingClientRect();
        const dropdownHeight = Math.max(dropdown.scrollHeight || 0, 220);
        const spaceAbove = parentRect.top - wrapRect.top;
        const spaceBelow = wrapRect.bottom - parentRect.bottom;
        shouldDropDown = spaceAbove < dropdownHeight + 16 && spaceBelow > spaceAbove;
      }
      dropdown.classList.toggle('ve-drop-down', shouldDropDown);
    };

    const menuItems = [
      { label: '↻ Rotate 90°', fn: () => setRotate90() },
      { label: '↔ Mirror Horizontal', fn: () => toggleMirrorH() },
      { label: '↕ Mirror Vertical', fn: () => toggleMirrorV() },
      { label: '🔆 Brightness +', fn: () => adjustFilter('brightness', 0.1, 'Brightness') },
      { label: '🔅 Brightness −', fn: () => adjustFilter('brightness', -0.1, 'Brightness') },
      { label: '◐ Contrast +', fn: () => adjustFilter('contrast', 0.1, 'Contrast') },
      { label: '◑ Contrast −', fn: () => adjustFilter('contrast', -0.1, 'Contrast') },
      { label: '🎨 Saturation +', fn: () => adjustFilter('saturate', 0.1, 'Saturation') },
      { label: '🎨 Saturation −', fn: () => adjustFilter('saturate', -0.1, 'Saturation') },
      { label: '🔄 Reset All', fn: () => resetFilterAndTransform() },
      { label: '📍 Toggle Progress Save', fn: () => toggleProgressSave() },
      { label: '✅ Toggle Video Enhancer', fn: () => toggleExtension() },
      { label: '🎛 Toggle GUI Toolbar', fn: () => toggleGUI() },
      { label: '⚙ Shortcuts', fn: () => { closeAllDropdowns(); openSettingsModal(); } },
    ];

    menuItems.forEach(({ label, fn }) => {
      const item = document.createElement('button');
      item.className = 've-dropdown-item';
      item.textContent = label;
      item.addEventListener('click', (e) => {
        e.stopPropagation();
        fn();
      });
      menuDropdown.appendChild(item);
    });

    menuWrap.appendChild(menuDropdown);
    toolbar.appendChild(menuWrap);

    menuBtn.addEventListener('click', (e) => {
      e.stopPropagation();
      const isOpen = menuDropdown.classList.contains('ve-open');
      closeAllDropdowns();
      if (!isOpen) {
        syncDropdownDirection(menuDropdown);
        menuDropdown.classList.add('ve-open');
        openDropdown = menuDropdown;
      }
    });

    wrap.appendChild(toolbar);
    container.appendChild(wrap);
    guiMap.set(video, wrap);

    /* ─── Position tracker: keep toolbar locked over the video ─── */
    let vePosRAF = 0, vePosDirty = true;
    const veUpdatePos = () => {
      const r = video.getBoundingClientRect();
      if (r.width >= 120 && r.height >= 80) {
        wrap.style.display = 'block';
        wrap.style.setProperty('--ve-max-h', Math.max(140, r.height - 90) + 'px');
        if (container.tagName === 'BODY' || container.tagName === 'HTML') {
          wrap.style.position = 'fixed';
          wrap.style.top = r.top + 'px';
          wrap.style.left = r.left + 'px';
          wrap.style.width = r.width + 'px';
          wrap.style.height = r.height + 'px';
        }

        if (!isDragging) {
          const anchor = String(cfg('anchor') || 'bottom-center');
          toolbar.style.top = ''; toolbar.style.bottom = '';
          toolbar.style.left = ''; toolbar.style.right = '';
          toolbar.style.transform = '';
          const p = '20px', vp = '60px'; // padding

          if (anchor.includes('top')) toolbar.style.top = p;
          else if (anchor.includes('bottom')) toolbar.style.bottom = vp;
          else { toolbar.style.top = '50%'; toolbar.style.transform = 'translateY(-50%)'; }

          if (anchor.includes('left')) toolbar.style.left = p;
          else if (anchor.includes('right')) toolbar.style.right = p;
          else { 
            toolbar.style.left = '50%'; 
            toolbar.style.transform += toolbar.style.transform ? ' translateX(-50%)' : 'translateX(-50%)';
          }

          syncDropdownDirection(menuDropdown);
          syncDropdownDirection(playPanel);
        }
      } else {
        wrap.style.display = 'none';
      }
    };
    const vePosLoop = () => {
      if (vePosDirty) { vePosDirty = false; veUpdatePos(); }
      if (video.isConnected && wrap.isConnected) vePosRAF = requestAnimationFrame(vePosLoop);
    };
    const veMarkDirty = () => { vePosDirty = true; };
    vePosRAF = requestAnimationFrame(vePosLoop);
    window.addEventListener('scroll', veMarkDirty, { passive: true });
    window.addEventListener('resize', veMarkDirty, { passive: true });
    let veResizeObs;
    try { veResizeObs = new ResizeObserver(veMarkDirty); veResizeObs.observe(video); } catch { }

    /* ─── Show/hide logic ─── */
    let hideTimer = null;
    let isHovering = false;
    const alwaysShow = false;
    let usingNativeSync = false;

    function showBar() {
      if (!wrap.classList.contains('ve-visible')) wrap.classList.add('ve-visible');
    }

    function hideBar() {
      if (wrap.classList.contains('ve-visible')) {
        wrap.classList.remove('ve-visible');
        closeAllDropdowns();
      }
    }

    /* Sync perfectly with native YouTube controls if available */
    if (isYouTube && container.classList.contains('html5-video-player')) {
      usingNativeSync = true;
      const nativeObserver = new MutationObserver(() => {
        if (!container.classList.contains('ytp-autohide')) {
          showBar();
        } else {
          hideBar();
        }
      });
      nativeObserver.observe(container, { attributes: true, attributeFilter: ['class'] });
      if (!container.classList.contains('ytp-autohide')) showBar(); else hideBar();
    }

    function scheduleHide(ms = 2500) {
      if (usingNativeSync) return;
      clearTimeout(hideTimer);
      hideTimer = setTimeout(() => {
        if (!isHovering && !video.paused && !alwaysShow) hideBar();
      }, ms);
    }

    /* Show on pause */
    video.addEventListener('pause', () => { if (!usingNativeSync) showBar(); });
    video.addEventListener('ended', () => { if (!usingNativeSync) showBar(); });

    /* Show briefly on play, then auto-hide */
    video.addEventListener('play', () => {
      if (usingNativeSync) return;
      showBar();
      scheduleHide(2000);
    });

    /* Show on mouse move over video, hide after idle */
    const onMouseMove = debounce(() => {
      if (usingNativeSync) return;
      showBar();
      scheduleHide(3000);
    }, 50);

    container.addEventListener('mousemove', onMouseMove, { capture: true, passive: true });
    container.addEventListener('mouseenter', () => {
      showBar();
      scheduleHide(3000);
    }, { capture: true, passive: true });
    container.addEventListener('mouseleave', () => {
      if (!video.paused && !alwaysShow) {
        scheduleHide(800);
      }
    });

    /* Keep visible when hovering toolbar buttons */
    toolbar.addEventListener('mouseenter', () => {
      isHovering = true;
      clearTimeout(hideTimer);
      showBar();
    });
    toolbar.addEventListener('mouseleave', () => {
      isHovering = false;
      if (!video.paused) scheduleHide(1500);
    });

    /* Close dropdowns when clicking outside */
    document.addEventListener('click', (e) => {
      if (openDropdown && !wrap.contains(e.target)) {
        closeAllDropdowns();
      }
    });

    /* Sync speed badge when rate changes via keyboard */
    const rateObserver = setInterval(() => {
      if (!video.isConnected || !wrap.isConnected) {
        clearInterval(rateObserver);
        cancelAnimationFrame(vePosRAF);
        window.removeEventListener('scroll', veMarkDirty);
        window.removeEventListener('resize', veMarkDirty);
        if (veResizeObs) veResizeObs.disconnect();
        wrap.remove();
        return;
      }
      if (playPanel && playPanel.classList.contains('ve-open')) {
        const curRate = round1(video.playbackRate);
        const curVol = round2(getEffectiveVolume());
        if (parseFloat(speedSlider.value) !== curRate) {
          speedSlider.value = curRate;
          speedVal.textContent = curRate + 'x';
        }
        if (parseFloat(volSlider.value) !== curVol) {
          volSlider.value = curVol;
          volVal.textContent = Math.round(curVol * 100) + '%';
        }
      }
      updateCompact();
    }, 500);

    /* Restore progress on first play */
    video.addEventListener('playing', function onFirstPlay() {
      restoreProgress(video);
      video.removeEventListener('playing', onFirstPlay);
    }, { once: true });

    /* If already paused with content, show immediately */
    if (video.paused && video.currentTime > 0) {
      showBar();
    }

    /* Also show if video has loaded data */
    if (video.readyState >= 2 && video.paused) {
      showBar();
    }

    /* Reveal once on attach so the toolbar does not depend on a missed hover/play event. */
    showBar();
    if (!video.paused) {
      scheduleHide(2200);
    }
  }

  function secureHTML(str) {
    if (window.trustedTypes && window.trustedTypes.createPolicy) {
      if (!window.veTrustPolicy) {
        try { window.veTrustPolicy = window.trustedTypes.createPolicy('ve-trust', { createHTML: s => s }); } catch (e) { }
      }
      return window.veTrustPolicy ? window.veTrustPolicy.createHTML(str) : str;
    }
    return str;
  }

  function addBtn(parent, iconSvg, title, onClick) {
    const btn = document.createElement('button');
    btn.className = 've-btn';
    btn.title = title;
    btn.innerHTML = secureHTML(iconSvg);
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      onClick();
    });
    parent.appendChild(btn);
    return btn;
  }

  function addSep(parent) {
    const sep = document.createElement('div');
    sep.className = 've-separator';
    parent.appendChild(sep);
  }

  /* ═══════════════════════════════════════════
     §11b  SETTINGS MODAL (Keyboard Shortcuts)
  ═══════════════════════════════════════════ */
  function prettifyKey(k) {
    return k.split('+').map(p => {
      if (p === ' ') return 'Space';
      if (p === 'arrowright') return '→';
      if (p === 'arrowleft') return '←';
      if (p === 'arrowup') return '↑';
      if (p === 'arrowdown') return '↓';
      return p.charAt(0).toUpperCase() + p.slice(1);
    }).join(' + ');
  }

  function openSettingsModal() {
    if (document.querySelector('.ve-settings-overlay')) return;

    const bindings = getBindings();

    /* Build a reverse map: actionName → key */
    const actionToKey = {};
    for (const [key, action] of Object.entries(bindings)) {
      actionToKey[action] = key;
    }

    const overlay = document.createElement('div');
    overlay.className = 've-settings-overlay';

    const modal = document.createElement('div');
    modal.className = 've-settings-modal';

    /* Header */
    modal.innerHTML = secureHTML(`
      <div class="ve-settings-header">
        <span class="ve-settings-title">⚙ Keyboard Shortcuts</span>
        <span class="ve-settings-subtitle">Click a key field and press your new shortcut (Press Backspace, Delete or Escape to disable)</span>
      </div>
      <div class="ve-settings-list"></div>
      <div class="ve-settings-footer">
        <button class="ve-settings-btn ve-reset-btn">Reset to Defaults</button>
        <button class="ve-settings-btn ve-close-btn">Close</button>
      </div>
    `);

    const list = modal.querySelector('.ve-settings-list');

    /* Sorted actions by category */
    const orderedActions = Object.keys(ACTIONS);

    orderedActions.forEach(actionName => {
      const action = ACTIONS[actionName];
      const currentKey = actionToKey[actionName] || '';

      const row = document.createElement('div');
      row.className = 've-settings-row';

      const label = document.createElement('span');
      label.className = 've-settings-label';
      label.textContent = action.label;

      const keyBtn = document.createElement('button');
      keyBtn.className = 've-settings-key';
      keyBtn.textContent = currentKey ? prettifyKey(currentKey) : '—';
      keyBtn.dataset.action = actionName;
      keyBtn.dataset.currentKey = currentKey;

      keyBtn.addEventListener('click', () => {
        /* Reset any other recording buttons */
        list.querySelectorAll('.ve-settings-key.ve-recording').forEach(b => {
          b.classList.remove('ve-recording');
          b.textContent = b.dataset.currentKey ? prettifyKey(b.dataset.currentKey) : '—';
        });
        keyBtn.classList.add('ve-recording');
        keyBtn.textContent = '⏺ Press a key...';
      });

      row.appendChild(label);
      row.appendChild(keyBtn);
      list.appendChild(row);
    });

    /* Global key listener for recording */
    function onRecordKey(e) {
      const recording = list.querySelector('.ve-settings-key.ve-recording');
      if (!recording) return;

      e.preventDefault();
      e.stopPropagation();

      const key = e.key.toLowerCase();
      if (['control', 'alt', 'shift', 'meta'].includes(key)) return; /* wait for actual key */

      const actionName = recording.dataset.action;

      /* Remove old binding for this action */
      const oldKey = recording.dataset.currentKey;

      if (key === 'escape' || key === 'backspace' || key === 'delete') {
        if (oldKey && bindings[oldKey] === actionName) delete bindings[oldKey];
        recording.dataset.currentKey = '';
        recording.textContent = '—';
        recording.classList.remove('ve-recording');
        saveBindings(bindings);
        return;
      }

      const newBinding = eventToBindingKey(e);

      if (oldKey && bindings[oldKey] === actionName) delete bindings[oldKey];

      /* Remove any existing binding for this key combo */
      if (bindings[newBinding]) {
        const conflictAction = bindings[newBinding];
        const conflictBtn = list.querySelector(`[data-action="${conflictAction}"]`);
        if (conflictBtn) { conflictBtn.textContent = '—'; conflictBtn.dataset.currentKey = ''; }
        delete bindings[newBinding];
      }

      /* Set new binding */
      bindings[newBinding] = actionName;
      recording.dataset.currentKey = newBinding;
      recording.textContent = prettifyKey(newBinding);
      recording.classList.remove('ve-recording');

      saveBindings(bindings);
    }

    document.addEventListener('keydown', onRecordKey, true);

    /* Reset to defaults */
    modal.querySelector('.ve-reset-btn').addEventListener('click', () => {
      Object.keys(bindings).forEach(k => delete bindings[k]);
      Object.assign(bindings, { ...DEFAULT_BINDINGS });
      saveBindings(bindings);

      /* Refresh the key display */
      const newActionToKey = {};
      for (const [key, action] of Object.entries(bindings)) newActionToKey[action] = key;
      list.querySelectorAll('.ve-settings-key').forEach(btn => {
        const k = newActionToKey[btn.dataset.action] || '';
        btn.textContent = k ? prettifyKey(k) : '—';
        btn.dataset.currentKey = k;
        btn.classList.remove('ve-recording');
      });
      tips('🔄 Shortcuts reset to defaults');
    });

    /* Close */
    function closeModal() {
      document.removeEventListener('keydown', onRecordKey, true);
      overlay.remove();
    }
    modal.querySelector('.ve-close-btn').addEventListener('click', closeModal);
    overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); });

    overlay.appendChild(modal);
    document.body.appendChild(overlay);
  }

  /* ═══════════════════════════════════════════
     §12  TAMPERMONKEY MENUS & GLOBAL TOGGLES
  ═══════════════════════════════════════════ */
  function toggleExtension() {
    const enabled = !cfg('enabled');
    cfg('enabled', enabled);
    tips(enabled ? '✅ Video Enhancer: ON' : '❌ Video Enhancer: OFF');
  }

  function toggleGUI() {
    const guiEnabled = !cfg('guiEnabled');
    cfg('guiEnabled', guiEnabled);
    if (!guiEnabled) {
      document.querySelectorAll('.ve-toolbar-wrap').forEach(el => el.remove());
    } else {
      refreshPlayers();
    }
    tips(guiEnabled ? '🎛 GUI: ON' : '🎛 GUI: OFF');
  }

  function registerMenus() {
    try {
      GM_registerMenuCommand('Toggle Video Enhancer', toggleExtension);
      GM_registerMenuCommand('Toggle GUI Toolbar', toggleGUI);
      GM_registerMenuCommand('Toggle Progress Save', toggleProgressSave);
      GM_registerMenuCommand('Configure Shortcuts', () => openSettingsModal());
    } catch { }
  }

  /* ═══════════════════════════════════════════
     §13  INITIALIZATION
  ═══════════════════════════════════════════ */
  function init() {
    if (!document.documentElement) {
      setTimeout(init, 50);
      return;
    }

    injectStyles();
    registerMenus();

    /* Bind keyboard events */
    document.addEventListener('keydown', handleKeydown, true);

    /* Detect videos via MutationObserver */
    const observer = new MutationObserver(debounce(() => {
      refreshPlayers();
    }, 500));

    function startObserving() {
      if (!document.body) {
        setTimeout(startObserving, 50);
        return;
      }

      observer.observe(document.body, { childList: true, subtree: true });

      /* Initial scan */
      refreshPlayers();

      /* Periodic fallback for SPAs and dynamic content */
      setInterval(refreshPlayers, 2500);
    }

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', startObserving);
    } else {
      startObserving();
    }
  }

  /* Start */
  try {
    init();
  } catch (e) {
    console.error('[Video Enhancer] Init error:', e);
    setTimeout(init, 500);
  }

})();