Greasy Fork is available in English.

YouTube Uppspelning Plox

Sparar och återupptar automatiskt videoframsteg på YouTube utan att behöva logga in.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         YouTube Playback Plox
// @name:en      YouTube Playback Plox
// @name:es      YouTube Reproducción Plox
// @name:fr      YouTube Lecture Plox
// @name:de      YouTube Wiedergabe Plox
// @name:it      YouTube Riproduzione Plox
// @name:pt-BR   YouTube Reprodução Plox
// @name:nl      YouTube Afspelen Plox
// @name:pl      YouTube Odtwarzanie Plox
// @name:sv      YouTube Uppspelning Plox
// @name:da      YouTube Afspilning Plox
// @name:no      YouTube Avspilling Plox
// @name:fi      YouTube Toisto Plox
// @name:cs      YouTube Přehrávání Plox
// @name:sk      YouTube Prehrávanie Plox
// @name:hu      YouTube Lejátszás Plox
// @name:ro      YouTube Redare Plox
// @name:be      YouTube Воспроизведение Plox
// @name:bg      YouTube Възпроизвеждане Plox
// @name:el      YouTube Αναπαραγωγή Plox
// @name:sr      YouTube Репродукција Plox
// @name:hr      YouTube Reprodukcija Plox
// @name:sl      YouTube Predvajanje Plox
// @name:lt      YouTube Grotuvas Plox
// @name:lv      YouTube Atskaņošana Plox
// @name:uk      YouTube Відтворення Plox
// @name:ru      YouTube Воспроизведение Plox
// @name:tr      YouTube Oynatma Plox
// @name:ar      يوتيوب بلايباك Plox
// @name:fa      پخش یوتیوب Plox
// @name:he      YouTube השמעה Plox
// @name:hi      YouTube प्लेबैक Plox
// @name:bn      YouTube প্লেব্যাক Plox
// @name:te      YouTube ప్లేబ్యాక్ Plox
// @name:ta      YouTube பிளேபாக் Plox
// @name:mr      YouTube प्लेबॅक Plox
// @name:zh-CN   YouTube 播放 Plox
// @name:zh-TW   YouTube 播放 Plox
// @name:zh-HK   YouTube 播放 Plox
// @name:ja      YouTube 再生 Plox
// @name:ko      YouTube 재생 Plox
// @name:th      YouTube เล่นต่อ Plox
// @name:vi      YouTube Phát lại Plox
// @name:id      YouTube Pemutaran Plox
// @name:ms      YouTube Main Semula Plox
// @name:tl      YouTube Playback Plox
// @name:my      YouTube ဖလေ့ဘက် Plox
// @name:sw      YouTube Uchezesha Plox
// @name:am      የYouTube ተጫዋች Plox
// @name:ha      YouTube Playback Plox
// @name:ur      YouTube پلے بیک Plox
// @name:ca      YouTube Reproducció Plox
// @name:zu      YouTube Playback Plox
// @name:yue      YouTube 播放 Plox
// @name:es-419      YouTube Reproducción Plox
// @description  Guarda y retoma automáticamente el progreso de vídeos en YouTube sin necesidad de iniciar sesión.
// @description:en  Automatically saves and resumes video playback progress on YouTube without needing to log in.
// @description:es  Guarda y retoma automáticamente el progreso de vídeos en YouTube sin necesidad de iniciar sesión.
// @description:fr  Enregistre et reprend automatiquement la progression de la lecture des vidéos sur YouTube sans avoir besoin de se connecter.
// @description:de  Speichert und setzt den Fortschritt von YouTube-Videos automatisch fort, ohne dass eine Anmeldung erforderlich ist.
// @description:it  Salva e riprende automaticamente la riproduzione dei video su YouTube senza bisogno di accedere.
// @description:pt-BR  Salva e retoma automaticamente o progresso da reprodução de vídeos no YouTube sem precisar fazer login.
// @description:nl  Slaat automatisch de voortgang van video's op YouTube op en hervat deze zonder in te loggen.
// @description:pl  Automatycznie zapisuje i wznawia postęp odtwarzania wideo na YouTube bez logowania.
// @description:sv  Sparar och återupptar automatiskt videoframsteg på YouTube utan att behöva logga in.
// @description:da  Gemmer og genoptager automatisk videoafspilning på YouTube uden at logge ind.
// @description:no  Lagrer og gjenopptar automatisk videofremdrift på YouTube uten å logge inn.
// @description:fi  Tallentaa ja jatkaa automaattisesti YouTube-videoiden toistopistettä ilman kirjautumista.
// @description:cs  Automaticky ukládá a obnovuje postup přehrávání videí na YouTube bez nutnosti přihlášení.
// @description:sk  Automaticky ukladá a obnovuje priebeh prehrávania videí na YouTube bez potreby prihlásenia.
// @description:hu  Automatikusan menti és folytatja a YouTube-videók lejátszási előrehaladását bejelentkezés nélkül.
// @description:ro  Salvează și reia automat progresul redării videoclipurilor pe YouTube fără a fi nevoie să te conectezi.
// @description:be  Автоматично зберігає та відновлює прогрес відтворення відео на YouTube без входу в акаунт.
// @description:bg  Автоматично записва и възобновява прогреса на видеото в YouTube без нужда от вход.
// @description:el  Αποθηκεύει και συνεχίζει αυτόματα την πρόοδο αναπαραγωγής βίντεο στο YouTube χωρίς να χρειάζεται σύνδεση.
// @description:sr  Аутоматски чува и наставља напредак репродукције видео записа на YouTube-у без пријављивања.
// @description:hr  Automatski sprema i nastavlja napredak reprodukcije videozapisa na YouTubeu bez prijave.
// @description:sl  Samodejno shrani in nadaljuje napredek predvajanja videoposnetkov na YouTubu brez prijave.
// @description:lt  Automatiškai išsaugo ir atnaujina YouTube vaizdo įrašų atkūrimo pažangą be prisijungimo.
// @description:lv  Automātiski saglabā un atsāk video atskaņošanas progresu YouTube bez pieteikšanās.
// @description:uk  Автоматично зберігає та відновлює прогрес відтворення відео на YouTube без входу в акаунт.
// @description:ru  Автоматически сохраняет и возобновляет прогресс воспроизведения видео на YouTube без входа в аккаунт.
// @description:tr  YouTube'daki video oynatma ilerlemesini otomatik olarak kaydeder ve devam ettirir, giriş yapmaya gerek yok.
// @description:ar  يقوم بحفظ واستئناف تقدم تشغيل الفيديوهات على يوتيوب تلقائيًا دون الحاجة لتسجيل الدخول.
// @description:fa  پیشرفت پخش ویدیوها در یوتیوب را به صورت خودکار ذخیره و ادامه می‌دهد بدون نیاز به ورود.
// @description:he  שומר ומחדש אוטומטית את התקדמות הניגון של סרטונים ביוטיוב ללא צורך בהתחברות.
// @description:hi  YouTube पर वीडियो प्लेबैक की प्रगति को स्वचालित रूप से सहेजें और पुनः प्रारंभ करें, लॉगिन की आवश्यकता नहीं।
// @description:bn  YouTube ভিডিও প্লেব্যাকের অগ্রগতি স্বয়ংক্রিয়ভাবে সংরক্ষণ এবং পুনরায় শুরু করুন, লগইনের প্রয়োজন নেই।
// @description:te  YouTube వీడియో ప్లేబ్యాక్ పురోగతిని ఆటోమేటిక్‌గా సేవ్ చేసి, తిరిగి ప్రారంభిస్తుంది, లాగిన్ అవసరం లేదు.
// @description:ta  YouTube வீடியோக்களின் பிளேபாக் முன்னேற்றத்தை தானாகச் சேமித்து மீண்டும் தொடங்கும், உள்நுழைவு தேவையில்லை.
// @description:mr  YouTube व्हिडिओ प्लेबॅक प्रगती आपोआप जतन करते आणि पुन्हा सुरू करते, लॉगिन आवश्यक नाही.
// @description:zh-CN 自动保存并恢复 YouTube 视频的播放进度,无需登录。
// @description:zh-TW  自動儲存及繼續 YouTube 影片播放進度,無需登入。
// @description:zh-HK  自動儲存及繼續 YouTube 影片播放進度,無需登入。
// @description:ja  YouTube の動画再生の進行状況を自動で保存・再開します。ログインは不要です。
// @description:ko  YouTube 동영상 재생 진행 상황을 자동으로 저장하고 이어서 재생합니다. 로그인 불필요.
// @description:th  บันทึกและเล่นต่อความคืบหน้าของวิดีโอบน YouTube โดยอัตโนมัติ โดยไม่ต้องเข้าสู่ระบบ.
// @description:vi  Tự động lưu và tiếp tục tiến trình phát video trên YouTube mà không cần đăng nhập.
// @description:id  Menyimpan dan melanjutkan kemajuan pemutaran video di YouTube secara otomatis tanpa perlu login.
// @description:ms  Menyimpan dan menyambung semula kemajuan main balik video di YouTube secara automatik tanpa perlu log masuk.
// @description:tl  Awtomatikong ini-save at ipinagpapatuloy ang progreso ng video playback sa YouTube nang hindi nagla-log in.
// @description:my  YouTube ဗီဒီယိုဖလေ့ဘက် တိုးတက်မှုကို အလိုအလျောက် သိမ်းဆည်းပြီး ထပ်မံစတင်နိုင်သည်။ ဝင်ရောက်ရန် မလိုအပ်ပါ။
// @description:sw  Hifadhi na endelea kwa kiotomatiki maendeleo ya uchezaji wa video kwenye YouTube bila kuingia.
// @description:am  በYouTube ላይ የቪዲዮ መጫወቻ እድገትን በራሱ ያስቀምጣል እና ያቀጥላል በመግባት ያስፈልጋል።
// @description:ha  Ajiye kuma ci gaba da ci gaban kallon bidiyo a YouTube ta atomatik ba tare da shiga ba.
// @description:ur  YouTube پر ویڈیوز کی پلے بیک کی پیش رفت کو خودکار طریقے سے محفوظ اور دوبارہ شروع کریں، لاگ ان کی ضرورت نہیں۔
// @description:ca  Desa i reprèn automàticament el progrés de reproducció de vídeos a YouTube sense necessitat d'iniciar sessió.
// @description:zu  Igcina futhi uqhubeke ngokuzenzakalelayo nokuqhubeka kwevidiyo ku-YouTube ngaphandle kokungena.
// @description:yue  自動儲存及繼續 YouTube 影片播放進度,無需登入。
// @description:es-419  Guarda y reanuda automáticamente el progreso de reproducción de videos en YouTube sin necesidad de iniciar sesión.
// @homepage     https://github.com/Alplox/Youtube-Playback-Plox
// @supportURL   https://github.com/Alplox/Youtube-Playback-Plox/issues
// @version      0.0.7-2
// @author       Alplox
// @match        https://www.youtube.com/*
// @icon         https://raw.githubusercontent.com/Alplox/StartpagePlox/refs/heads/main/assets/favicon/favicon.ico
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// @namespace    youtube-playback-plox
// @license      MIT
// @require      https://update.greatest.deepsurf.us/scripts/549881/1684270/YouTube%20Helper%20API.js
// ==/UserScript==

// ------------------------------------------
// MARK: 🔍 SISTEMA DE LOGGING
// ------------------------------------------

(function () {
    'use strict';

    // Sistema de niveles: silent(0), error(1), warn(2), info(3), debug(4)
    const LEVELS = { silent: 0, error: 1, warn: 2, info: 3, debug: 4 };
    let currentLevel = LEVELS.silent; // Cambiar a 'debug' para ver todo, o 'warn'/'error' para menos

    const styleFor = (kind) => {
        switch (kind) {
            case 'info': return 'color: #4FC1FF;';
            case 'debug': return 'color: #6a9955;';
            case 'warn': return 'color: #ce9178; font-weight: bold;';
            case 'error': return 'color: #f44747; font-weight: bold;';
            default: return '';
        }
    };

    window.MyScriptLogger = {
        // Debug detallado
        log: (context, ...args) => {
            if (currentLevel >= LEVELS.debug) {
                console.log(`%c[${context}]`, styleFor('debug'), ...args);
            }
        },
        debug: (context, ...args) => {
            if (currentLevel >= LEVELS.debug) {
                console.log(`%c[${context}]`, styleFor('debug'), ...args);
            }
        },
        // Informativo de etapas/éxitos
        info: (context, ...args) => {
            if (currentLevel >= LEVELS.info) {
                console.info(`%c[${context}]`, styleFor('info'), ...args);
            }
        },
        warn: (context, ...args) => {
            if (currentLevel >= LEVELS.warn) {
                console.warn(`%c[${context}]`, styleFor('warn'), ...args);
            }
        },
        error: (context, ...args) => {
            // Los errores siempre se muestran
            console.error(`%c[${context}]`, styleFor('error'), ...args);
        }
    };

})();

// Atajo para no tener que escribir window.MyScriptLogger cada vez
const { log, info, warn, error: conError } = window.MyScriptLogger;

// --- INICIO CARGA LÓGICA PRINCIPAL DEL USERSCRIPT ---

(() => {
    'use strict';

    /**
     * Polyfill ligero para CustomEvent en navegadores antiguos.
     * Crea window.CustomEvent si no existe o no es una función nativa.
     * @returns {void}
     */
    (function polyfillCustomEvent() {
        try {
            if (typeof window.CustomEvent === 'function') return;
        } catch (_) { /* noop */ }
        try {
            function CustomEventPolyfill(event, params) {
                params = params || { bubbles: false, cancelable: false, detail: null };
                const evt = document.createEvent('CustomEvent');
                evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
                return evt;
            }
            CustomEventPolyfill.prototype = (window.Event || function () { }).prototype;
            window.CustomEvent = CustomEventPolyfill;
        } catch (_) { /* noop */ }
    })();

    // ------------------------------------------
    // MARK: 🌐 Carga de Traducciones
    // ------------------------------------------

    // URL del archivo de traducciones
    const TRANSLATIONS_URL = 'https://raw.githubusercontent.com/Alplox/Youtube-Playback-Plox/refs/heads/main/translations.json';
    const TRANSLATIONS_URL_BACKUP = 'https://cdn.jsdelivr.net/gh/Alplox/Youtube-Playback-Plox@refs/heads/main/translations.json';
    const TRANSLATIONS_EXPECTED_VERSION = '0.0.7-2';

    // Variables globales para las traducciones
    let TRANSLATIONS = {};
    let LANGUAGE_FLAGS = {};

    // Traducciones básicas de fallback en caso de error
    const FALLBACK_FLAGS = {
        "en-US": {
            "emoji": "🇺🇸",
            "code": "en-US",
            "name": "English (US)"
        },
        "es-ES": {
            "emoji": "🇪🇸",
            "code": "es-ES",
            "name": "Español"
        },
        "fr": {
            "emoji": "🇫🇷",
            "code": "fr",
            "name": "Français"
        }
    };

    const FALLBACK_TRANSLATIONS = {
        "en-US": {
            "settings": "Settings",
            "savedVideos": "View saved videos",
            "close": "Close",
            "save": "Save",
            "cancel": "Cancel",
            "delete": "Delete",
            "undo": "Undo",
            "enableSavingFor": "Enable saving for",
            "regularVideos": "Regular videos",
            "shorts": "Shorts",
            "liveStreams": "Live streams",
            "live": "Live",
            "showNotifications": "Show save notifications",
            "minSecondsBetweenSaves": "Minimum seconds between saves",
            "showFloatingButton": "Show floating button",
            "language": "Language",
            "alertStyle": "Alert style in playback bar",
            "alertIconText": "Icon + Text",
            "alertIconOnly": "Icon Only",
            "alertTextOnly": "Text Only",
            "alertHidden": "Hidden",
            "noSavedVideos": "No saved videos.",
            "sortBy": "Sort by",
            "mostRecent": "Most recent",
            "oldest": "Oldest",
            "titleAZ": "Title (A-Z)",
            "filterByType": "Filter by type",
            "all": "All",
            "videos": "Videos",
            "playlist": "Playlist",
            "searchByTitleOrAuthor": "Search by title or author...",
            "export": "Export",
            "import": "Import",
            "progressSaved": "Progress saved",
            "dataExported": "Data exported",
            "itemsImported": "Imported {count} items",
            "importError": "Error importing. Make sure the file is valid.",
            "exportError": "Error exporting data",
            "invalidFormat": "Invalid format",
            "invalidJson": "Invalid JSON",
            "invalidDatabase": "Invalid database",
            "noValidVideos": "No valid videos found to import",
            "allDataCleared": "All data cleared",
            "noDataToRestore": "No data to restore",
            "allDataRestored": "All data restored",
            "clearAllDataConfirm": "Are you sure you want to delete all data?",
            "omitedVideos": "Omitted videos",
            "fileTooLarge": "File is too large (max {size})",
            "importingFromFreeTube": "Importing from FreeTube...",
            "importingFromFreeTubeAsSQLite": "Importing from FreeTube as SQLite...",
            "videosImported": "videos imported",
            "noVideosImported": "no videos could be imported",
            "errors": "errors",
            "noVideosFoundInFreeTubeDB": "No videos found in FreeTube database",
            "videosImportedFromFreeTubeDB": "videos imported from FreeTube database",
            "noVideosImportedFromFreeTubeDB": "no videos could be imported from FreeTube database",
            "fileEmpty": "File is empty",
            "processingFile": "Processing file...",
            "configurationSaved": "Configuration saved",
            "startTimeSet": "Start time set to",
            "fixedTimeRemoved": "Fixed time removed.",
            "itemDeleted": "deleted.",
            "unknownError": "Unknown error",
            "retryNow": "Retry now",
            "retryCompleted": "Retry completed",
            "progress": "Progress",
            "alwaysStartFrom": "Always start from",
            "resumedAt": "Resumed at",
            "percentWatched": "% watched",
            "remaining": "remaining",
            "setStartTime": "Set start time",
            "changeOrRemoveStartTime": "Always start from {time} (Click to change or remove)",
            "enterStartTime": "Enter the start time you always want to use (example: 1:23)",
            "enterStartTimeOrEmpty": "Enter the start time you always want to use (example: 1:23) or leave empty to remove",
            "deleteEntry": "Delete entry",
            "youtubePlaybackPlox": "YouTube Playback Plox",
            "playlistPrefix": "Playlist",
            "unknown": "Unknown",
            "notAvailable": "N/A",
            "clearAll": "Clear all",
            "clearAllConfirm": "Are you sure you want to delete ALL saved videos? This action can be undone.",
            "allItemsCleared": "All items cleared",
            "viewAllHistory": "View all history",
            "viewCompletedVideos": "View completed videos",
            "completed": "Completed",
            "completedVideos": "Completed videos",
            "videosWithFixedTime": "Videos with fixed time",
            "views": "Views",
            "enableProgressBarGradient": "Enable color gradient in progress bar",
            "staticFinishPercent": "Percentage to mark video as completed",
            "openChannel": "Open channel",
            "openPlaylist": "Open playlist",
            "createPlaylist": "Create playlist",
            "selectVideos": "Select videos",
            "selectedVideos": "Selected videos",
            "generatePlaylistLink": "Generate playlist link",
            "playlistLinkGenerated": "Playlist link generated",
            "copyLink": "Copy link",
            "linkCopied": "Link copied to clipboard",
            "selectAtLeastOne": "Select at least one video",
            "tooManyVideos": "Too many videos selected (max 200)",
            "inlinePreviews": "Inline previews (Home)"
        },
        "es-ES": {
            "settings": "Configuración",
            "savedVideos": "Ver videos guardados",
            "close": "Cerrar",
            "save": "Guardar",
            "cancel": "Cancelar",
            "delete": "Eliminar",
            "undo": "Deshacer",
            "enableSavingFor": "Activar guardado para",
            "regularVideos": "Videos regulares",
            "shorts": "Shorts",
            "liveStreams": "Directos (Livestreams)",
            "live": "Directo",
            "showNotifications": "Mostrar notificaciones de guardado",
            "minSecondsBetweenSaves": "Intervalo segundos mínimos entre guardados",
            "showFloatingButton": "Mostrar botón flotante",
            "language": "Idioma",
            "alertStyle": "Estilo de alertas en la barra de reproducción",
            "alertIconText": "Icono + Texto",
            "alertIconOnly": "Solo Icono",
            "alertTextOnly": "Solo Texto",
            "alertHidden": "Oculto",
            "noSavedVideos": "No hay videos guardados.",
            "sortBy": "Ordenar por",
            "mostRecent": "Más recientes",
            "oldest": "Más antiguos",
            "titleAZ": "Título (A-Z)",
            "filterByType": "Filtrar por tipo",
            "all": "Todos",
            "videos": "Videos",
            "playlist": "Playlist",
            "searchByTitleOrAuthor": "Buscar por título o autor...",
            "export": "Exportar",
            "import": "Importar",
            "progressSaved": "Progreso guardado",
            "dataExported": "Datos exportados",
            "itemsImported": "Importados {count} elementos",
            "importError": "Error al importar. Asegúrate de que el archivo sea válido.",
            "exportError": "Error al exportar datos",
            "invalidFormat": "Formato inválido",
            "invalidJson": "JSON inválido",
            "invalidDatabase": "Base de datos inválida",
            "noValidVideos": "No se encontraron videos válidos para importar",
            "allDataCleared": "Todos los datos eliminados",
            "noDataToRestore": "No hay datos para restaurar",
            "allDataRestored": "Todos los datos restaurados",
            "clearAllDataConfirm": "¿Estás seguro de que quieres eliminar todos los datos?",
            "omitedVideos": "Videos omitidos",
            "fileTooLarge": "El archivo es demasiado grande (máx {size})",
            "importingFromFreeTube": "Importando desde FreeTube...",
            "importingFromFreeTubeAsSQLite": "Importando desde FreeTube como SQLite...",
            "videosImported": "videos importados",
            "noVideosImported": "no se pudo importar ningún video",
            "errors": "errores",
            "noVideosFoundInFreeTubeDB": "No se encontraron videos en la base de datos de FreeTube",
            "videosImportedFromFreeTubeDB": "videos importados desde la base de datos de FreeTube",
            "noVideosImportedFromFreeTubeDB": "no se pudo importar ningún video desde la base de datos de FreeTube",
            "fileEmpty": "El archivo está vacío",
            "processingFile": "Procesando archivo...",
            "configurationSaved": "Configuración guardada",
            "startTimeSet": "Tiempo de inicio establecido en",
            "fixedTimeRemoved": "Tiempo fijo eliminado.",
            "itemDeleted": "eliminado.",
            "unknownError": "Error desconocido",
            "retryNow": "Reintentar ahora",
            "retryCompleted": "Reintentos completados",
            "progress": "Progreso",
            "alwaysStartFrom": "Siempre desde",
            "resumedAt": "Reanudado en",
            "percentWatched": "% visto",
            "remaining": "restantes",
            "setStartTime": "Establecer tiempo de inicio",
            "changeOrRemoveStartTime": "Siempre empezar en {time} (Click para cambiar o eliminar)",
            "enterStartTime": "Introduce el tiempo de inicio que siempre quieres usar (ejemplo: 1:23)",
            "enterStartTimeOrEmpty": "Introduce el tiempo de inicio que siempre quieres usar (ejemplo: 1:23) o deja vacío para eliminar",
            "deleteEntry": "Eliminar entrada",
            "youtubePlaybackPlox": "YouTube Playback Plox",
            "playlistPrefix": "Playlist",
            "unknown": "Desconocido",
            "notAvailable": "N/A",
            "clearAll": "Eliminar todo",
            "clearAllConfirm": "¿Estás seguro de que quieres eliminar TODOS los videos guardados? Esta acción se puede deshacer.",
            "allItemsCleared": "Todos los elementos eliminados",
            "viewAllHistory": "Ver todo el historial",
            "viewCompletedVideos": "Ver videos completados",
            "completed": "Completado",
            "completedVideos": "Videos completados",
            "videosWithFixedTime": "Videos con tiempo fijo",
            "views": "Vistas",
            "enableProgressBarGradient": "Habilitar degradado de colores en barra de progreso",
            "staticFinishPercent": "Porcentaje para marcar video como completado",
            "openChannel": "Abrir canal",
            "openPlaylist": "Abrir playlist",
            "createPlaylist": "Crear playlist",
            "selectVideos": "Seleccionar videos",
            "selectedVideos": "Videos seleccionados",
            "generatePlaylistLink": "Generar enlace de playlist",
            "playlistLinkGenerated": "Enlace de playlist generado",
            "copyLink": "Copiar enlace",
            "linkCopied": "Enlace copiado al portapapeles",
            "selectAtLeastOne": "Selecciona al menos un video",
            "tooManyVideos": "Demasiados videos seleccionados (máx 200)",
            "inlinePreviews": "Previsualizaciones en inicio (Home)"
        },
        "fr": {
            "settings": "Paramètres",
            "savedVideos": "Voir les vidéos enregistrées",
            "close": "Fermer",
            "save": "Enregistrer",
            "cancel": "Annuler",
            "delete": "Supprimer",
            "undo": "Annuler",
            "enableSavingFor": "Activer la sauvegarde pour",
            "regularVideos": "Vidéos régulières",
            "shorts": "Shorts",
            "liveStreams": "Diffusions en direct",
            "live": "Diffusions en direct",
            "showNotifications": "Afficher les notifications de sauvegarde",
            "minSecondsBetweenSaves": "Secondes minimales entre les sauvegardes",
            "showFloatingButton": "Afficher le bouton flottant",
            "language": "Langue",
            "alertStyle": "Style d'alerte dans la barre de lecture",
            "alertIconText": "Icône + Texte",
            "alertIconOnly": "Icône uniquement",
            "alertTextOnly": "Texte uniquement",
            "alertHidden": "Masqué",
            "noSavedVideos": "Aucune vidéo enregistrée.",
            "sortBy": "Trier par",
            "mostRecent": "Plus récent",
            "oldest": "Plus ancien",
            "titleAZ": "Titre (A-Z)",
            "filterByType": "Filtrer par type",
            "all": "Tous",
            "videos": "Vidéos",
            "playlist": "Playlist",
            "searchByTitleOrAuthor": "Rechercher par titre ou auteur...",
            "export": "Exporter",
            "import": "Importer",
            "progressSaved": "Progrès enregistré",
            "dataExported": "Données exportées",
            "itemsImported": "{count} éléments importés",
            "importError": "Erreur lors de l'importation. Assurez-vous que le fichier est valide.",
            "exportError": "Erreur lors de l'exportation des données",
            "invalidFormat": "Format invalide",
            "invalidJson": "JSON invalide",
            "invalidDatabase": "Base de données invalide",
            "noValidVideos": "Aucune vidéo valide trouvée à importer",
            "allDataCleared": "Toutes les données ont été effacées",
            "noDataToRestore": "Aucune donnée à restaurer",
            "allDataRestored": "Toutes les données restaurées",
            "clearAllDataConfirm": "Êtes-vous sûr de vouloir supprimer toutes les données ?",
            "omitedVideos": "Vidéos omises",
            "fileTooLarge": "Le fichier est trop volumineux (max {size})",
            "importingFromFreeTube": "Importation depuis FreeTube...",
            "importingFromFreeTubeAsSQLite": "Importation depuis FreeTube en tant que SQLite...",
            "videosImported": "vidéos importées",
            "noVideosImported": "aucune vidéo n'a pu être importée",
            "errors": "erreurs",
            "noVideosFoundInFreeTubeDB": "Aucune vidéo trouvée dans la base de données FreeTube",
            "videosImportedFromFreeTubeDB": "vidéos importées depuis la base de données FreeTube",
            "noVideosImportedFromFreeTubeDB": "aucune vidéo n'a pu être importée depuis la base de données FreeTube",
            "fileEmpty": "Le fichier est vide",
            "processingFile": "Traitement du fichier...",
            "configurationSaved": "Configuration enregistrée",
            "startTimeSet": "Heure de début définie à",
            "fixedTimeRemoved": "Heure fixe supprimée.",
            "itemDeleted": "supprimé.",
            "unknownError": "Erreur inconnue",
            "retryNow": "Réessayer maintenant",
            "retryCompleted": "Réessais terminés",
            "progress": "Progrès",
            "alwaysStartFrom": "Toujours commencer à",
            "resumedAt": "Repris à",
            "percentWatched": "% regardé",
            "remaining": "restant",
            "setStartTime": "Définir l'heure de début",
            "changeOrRemoveStartTime": "Toujours commencer à {time} (Cliquez pour changer ou supprimer)",
            "enterStartTime": "Entrez l'heure de début que vous souhaitez toujours utiliser (exemple: 1:23)",
            "enterStartTimeOrEmpty": "Entrez l'heure de début que vous souhaitez toujours utiliser (exemple: 1:23) ou laissez vide pour supprimer",
            "deleteEntry": "Supprimer l'entrée",
            "youtubePlaybackPlox": "YouTube Playback Plox",
            "playlistPrefix": "Playlist",
            "unknown": "Inconnu",
            "notAvailable": "N/A",
            "clearAll": "Tout effacer",
            "clearAllConfirm": "Êtes-vous sûr de vouloir supprimer TOUTES les vidéos enregistrées ? Cette action peut être annulée.",
            "allItemsCleared": "Tous les éléments effacés",
            "viewAllHistory": "Voir tout l'historique",
            "viewCompletedVideos": "Voir les vidéos terminées",
            "completed": "Terminé",
            "completedVideos": "Vidéos terminées",
            "videosWithFixedTime": "Vidéos avec un temps fixe",
            "views": "Vues",
            "enableProgressBarGradient": "Activer le dégradé de couleurs dans la barre de progression",
            "staticFinishPercent": "Pourcentage pour marquer la vidéo comme terminée",
            "openChannel": "Ouvrir la chaîne",
            "openPlaylist": "Ouvrir la playlist",
            "createPlaylist": "Créer une playlist",
            "selectVideos": "Sélectionner des vidéos",
            "selectedVideos": "Vidéos sélectionnées",
            "generatePlaylistLink": "Générer le lien de la playlist",
            "playlistLinkGenerated": "Lien de la playlist généré",
            "copyLink": "Copier le lien",
            "linkCopied": "Lien copié dans le presse-papiers",
            "selectAtLeastOne": "Sélectionnez au moins une vidéo",
            "tooManyVideos": "Trop de vidéos sélectionnées (max 200)",
            "inlinePreviews": "Aperçus intégrés (Accueil)"
        }
    };

    // Función para cargar las traducciones desde el archivo JSON externo
    async function loadTranslations() {
        const CACHE_KEY = `${CONFIG.storagePrefix}translations_cache_v1`;
        const TTL_MS = 6 * 60 * 60 * 1000; // 6 horas

        // 1) Intentar usar caché (GM_* preferido; luego localStorage)
        try {
            if (typeof GM_getValue === 'function') {
                const raw = await GM_getValue(CACHE_KEY, null);
                if (raw) {
                    const cached = JSON.parse(raw);
                    const isFresh = cached?.ts && (Date.now() - cached.ts) < TTL_MS;
                    const cachedVersion = cached?.version ?? cached?.data?.VERSION;
                    const versionMatches = !TRANSLATIONS_EXPECTED_VERSION || cachedVersion === TRANSLATIONS_EXPECTED_VERSION;
                    if (isFresh && cached?.data && versionMatches) {
                        info('loadTranslations', 'Usando traducciones desde caché GM_*');
                        return cached.data;
                    }
                }
            }
        } catch (_) { }
        try {
            const raw = localStorage.getItem(CACHE_KEY);
            if (raw) {
                const cached = JSON.parse(raw);
                const isFresh = cached?.ts && (Date.now() - cached.ts) < TTL_MS;
                const cachedVersion = cached?.version ?? cached?.data?.VERSION;
                const versionMatches = !TRANSLATIONS_EXPECTED_VERSION || cachedVersion === TRANSLATIONS_EXPECTED_VERSION;
                if (isFresh && cached?.data && versionMatches) {
                    info('loadTranslations', 'Usando traducciones desde caché localStorage');
                    return cached.data;
                }
            }
        } catch (_) { }

        // 2) Helper para cargar desde URL con GM_xmlhttpRequest o fetch
        const fetchUrl = async (url) => {
            if (typeof GM_xmlhttpRequest === 'function') {
                return await new Promise((resolve, reject) => {
                    try {
                        GM_xmlhttpRequest({
                            method: 'GET',
                            url,
                            timeout: 5000,
                            onload: (response) => {
                                try {
                                    resolve(JSON.parse(response.responseText));
                                } catch (e) { reject(e); }
                            },
                            onerror: (e) => reject(e),
                            ontimeout: () => reject(new Error('timeout'))
                        });
                    } catch (err) { reject(err); }
                });
            }
            // Fallback a fetch nativo
            if (typeof fetch === 'function') {
                const resp = await fetch(url, { cache: 'no-store' });
                const text = await resp.text();
                return JSON.parse(text);
            }
            throw new Error('No hay método de red disponible');
        };

        // 3) Intentar URLs primarias/secundarias
        const urls = [TRANSLATIONS_URL, TRANSLATIONS_URL_BACKUP];
        let data = null;
        for (const url of urls) {
            try {
                const candidate = await fetchUrl(url);
                if (candidate?.LANGUAGE_FLAGS && Object.keys(candidate.LANGUAGE_FLAGS).length > 0 &&
                    candidate?.TRANSLATIONS && Object.keys(candidate.TRANSLATIONS).length > 0) {
                    info('loadTranslations', 'Traducciones externas cargadas correctamente desde: ' + url);
                    data = candidate;
                    break;
                } else {
                    warn('loadTranslations', 'Traducciones inválidas desde: ' + url);
                }
            } catch (e) {
                warn('loadTranslations', 'Fallo al cargar traducciones desde ' + url, e);
            }
        }

        if (!data) {
            conError('loadTranslations', 'No se pudieron cargar traducciones externas, usando fallback');
            data = { LANGUAGE_FLAGS: FALLBACK_FLAGS, TRANSLATIONS: FALLBACK_TRANSLATIONS };
        }

        // 4) Guardar en caché
        const cachePayload = JSON.stringify({ ts: Date.now(), version: data?.VERSION ?? TRANSLATIONS_EXPECTED_VERSION ?? null, data });
        try { if (typeof GM_setValue === 'function') await GM_setValue(CACHE_KEY, cachePayload); } catch (_) { }
        try { localStorage.setItem(CACHE_KEY, cachePayload); } catch (_) { }

        return data;
    }

    // ------------------------------------------
    // MARK: 📦 Config
    // ------------------------------------------

    const CONFIG = {
        /** Diferencia mínima (en segundos) para considerar un cambio de posición como válido */
        minSeekDiff: 1.5,

        /** Prefijo para claves en localStorage */
        storagePrefix: 'YT_PLAYBACK_PLOX_',

        /** Enumeración de estilos de alerta */
        alertStylesSettings: {
            icon_only: 'iconOnly',
            text_only: 'textOnly',
            icon_and_text: 'iconText',
            no_icon_no_text: 'hidden'
        },

        /** Clave para guardar configuraciones del usuario en GM_* */
        userSettingsKey: 'YT_PLAYBACK_PLOX_userSettings',

        /** Valores predeterminados para configuraciones del usuario */
        defaultSettings: {
            showNotifications: true,
            minSecondsBetweenSaves: 1,
            showFloatingButtons: false,
            saveRegularVideos: true, // Por defecto, guardar videos regulares
            saveShorts: false, // Por defecto, no guardar Shorts
            saveLiveStreams: false, // Por defecto, no guardar directos de URL tipo "/live" o "/watch" con player en directo, si ya es VOD lo toma como regular
            language: 'en-US', // Idioma predeterminado
            alertStyle: 'iconText', // Estilo de alerta predeterminado
            enableProgressBarGradient: true, // Por defecto, habilitar degradado de colores en barra de progreso
            staticFinishPercent: 95, // Porcentaje desde el final para considerar video como completado (95% = 5% antes del final)
            saveInlinePreviews: false, // Guardar previsualizaciones inline (Homepage) desactivado por defecto
        },

        /** Clave para guardar filtros del usuario en GM_* */
        userFiltersKey: 'YT_PLAYBACK_PLOX_userFilters',

        /** Valores predeterminados para filtros del usuario */
        defaultFilters: {
            orderBy: "recent",
            filterBy: "all",
            searchQuery: ""
        }
    };

    // ------------------------------------------
    // MARK: 🌐 Funciones de traducción
    // ------------------------------------------

    let currentLanguage = CONFIG.defaultSettings.language; // Idioma predeterminado

    // Función para obtener el texto traducido
    function t(key, params = {}) {
        if (!TRANSLATIONS[currentLanguage] || !TRANSLATIONS[currentLanguage][key]) {
            // Si no hay traducción, intentar con el idioma por defecto (ej: en-US)
            const fallbackLang = CONFIG.defaultSettings.language;
            if (TRANSLATIONS[fallbackLang] && TRANSLATIONS[fallbackLang][key]) {
                return replaceParams(TRANSLATIONS[fallbackLang][key], params);
            }
            // Si no hay ni en el idioma por defecto, devolver la clave
            return key;
        }
        return replaceParams(TRANSLATIONS[currentLanguage][key], params);
    }

    // Función para reemplazar parámetros en las traducciones
    function replaceParams(text, params) {
        if (!text || typeof text !== 'string') return text;
        return text.replace(/{(\w+)}/g, (match, param) => {
            return params[param] !== undefined ? params[param] : match;
        });
    }

    /**
     * Fusiona profundamente mapas de traducciones por idioma, priorizando las externas.
     * @param {Object} base - Traducciones base/fallback (por idioma)
     * @param {Object} override - Traducciones externas (por idioma)
     * @returns {Object} Mapa de traducciones resultante por idioma
     */
    function deepMergeTranslations(base, override) {
        try {
            const result = { ...(base || {}) };
            const over = override && typeof override === 'object' ? override : {};
            for (const lang of Object.keys(over)) {
                const baseLang = result[lang] || {};
                const overLang = over[lang] || {};
                result[lang] = { ...baseLang, ...overLang };
            }
            return result;
        } catch (_) {
            return { ...(base || {}) };
        }
    }

    // Función para cambiar el idioma
    async function setLanguage(lang, options = { persist: true }) {
        log('setLanguage', 'lang que llega:', lang);
        let validLang = lang;

        if (!TRANSLATIONS[validLang]) {
            const primary = lang.split('-')[0];
            validLang = Object.keys(TRANSLATIONS).find(k => k === primary || k.startsWith(primary + '-'));
        }

        if (!validLang) validLang = CONFIG.defaultSettings.language;

        currentLanguage = validLang;

        // Persistir solo si se solicita (evitar escrituras redundantes durante init)
        if (options?.persist) {
            try {
                const settings = await Settings.get();
                settings.language = validLang;
                await Settings.set(settings);
            } catch (e) {
                conError('setLanguage', 'Error persistiendo idioma', e);
            }
        }

        log('setLanguage', 'lang que sale:', validLang);
        return true;
    }

    // Función para detectar el idioma del navegador
    function detectBrowserLanguage() {
        const primaryLang = navigator.language || navigator.userLanguage; // "es-ES" o "en"
        const candidates = (Array.isArray(navigator.languages) && navigator.languages.length)
            ? navigator.languages
            : (primaryLang ? [primaryLang] : []);

        log('detectBrowserLanguage', 'candidates:', candidates);

        // Coincidencia exacta priorizando navigator.languages[0]
        for (const lang of candidates) {
            if (TRANSLATIONS[lang]) return lang;
        }

        // Coincidencia por prefijo (ejemplo: "es" -> "es-ES" o "es-419")
        for (const lang of candidates) {
            const prefix = (lang || '').split('-')[0];
            const matched = Object.keys(TRANSLATIONS).find(k => k === prefix || k.startsWith(prefix + '-'));
            if (matched) {
                log('detectBrowserLanguage', 'matched by prefix:', matched);
                return matched;
            }
        }

        warn(`Idioma del navegador '${primaryLang}' no soportado, usando default.`);
        return CONFIG.defaultSettings.language;
    }

    // ------------------------------------------
    // MARK: 🎨 Styles
    // ------------------------------------------

    function injectStyles() {
        if (document.getElementById('youtube-playback-plox-styles')) return; // evitar duplicados

        const style = document.createElement('style');
        style.id = 'youtube-playback-plox-styles';
        style.textContent = `
:root {
  /* Base (Light) - Solo variables --ypp- */
  --ypp-bg: #ffffff;
  --ypp-text: #222222;
  --ypp-muted: #555555;
  --ypp-light: #888888;
  --ypp-link: #065fd4;
  --ypp-danger: #dc2626;
  --ypp-success: #16a34a;
  --ypp-success-dark: #15803d;
  --ypp-overlay: rgba(0, 0, 0, 0.4);
  --ypp-toast: #333333;
  --ypp-primary: #2563eb;
  --ypp-primary-dark: #1e40af;
  --ypp-border: #cccccc;
  --ypp-playlist-bg: #f0f8ff; /* Fondo sutil para items de playlist */
  --ypp-bg-secondary: #f5f5f5;

  /* Tipografía */
  --ypp-white: #ffffff;
  --ypp-font-base: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;

  /* Espaciado */
  --ypp-spacing-sm: 0.5rem;
  --ypp-spacing-md: 1rem;
  --ypp-spacing-lg: 1.5rem;

  /* Sombra */
  --ypp-shadow-md: 0 4px 20px rgba(0, 0, 0, 0.2);
  --ypp-shadow-modal: 0 4px 16px rgba(0, 0, 0, 0.25);
  --ypp-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);

  /* Z-index */
  --ypp-z-overlay: 9999;
  --ypp-z-modal: 10000;
  --ypp-z-toast: 10001;

  /* Inputs */
  --ypp-input: #f5f5f5;
  --ypp-input-border: #cccccc;
  --ypp-input-focus: #1a73e8; /* Light */
}

html[dark], body.dark-theme {
  --ypp-bg: #0f0f0f;
  --ypp-text: #f1f1f1;
  --ypp-muted: #aaaaaa;
  --ypp-light: #aaaaaa;
  --ypp-link: #3ea6ff;
  --ypp-border: #303030;
  --ypp-bg-secondary: #1a1a1a;
  --ypp-overlay: rgba(0, 0, 0, 0.8);
  --ypp-input: #1a1a1a;
  --ypp-input-border: #303030;
  /* Overrides específicas para UI en oscuro */
  --ypp-input-focus: #065fd4;
  --ypp-shadow: 0 12px 24px rgba(0, 0, 0, 0.5);
}

.ypp-sombra {
    box-shadow:
        0.8px 0.8px 2.7px rgba(0, 0, 0, 0.062),
        2.1px 2.1px 6.9px rgba(0, 0, 0, 0.089),
        4.3px 4.3px 14.2px rgba(0, 0, 0, 0.111),
        8.8px 8.8px 29.2px rgba(0, 0, 0, 0.138),
        24px 24px 80px rgba(0, 0, 0, 0.2);

    -webkit-box-shadow:
        0.8px 0.8px 2.7px rgba(0, 0, 0, 0.062),
        2.1px 2.1px 6.9px rgba(0, 0, 0, 0.089),
        4.3px 4.3px 14.2px rgba(0, 0, 0, 0.111),
        8.8px 8.8px 29.2px rgba(0, 0, 0, 0.138),
        24px 24px 80px rgba(0, 0, 0, 0.2);
}

.ypp-svgFolderIcon,
.ypp-svgSaveIcon,
.ypp-svgPinIcon,
.ypp-svgTimerIcon,
.ypp-svgPlayOrPauseIcon {
  vertical-align: middle;
  height: 100%;
  margin: 0 0px 2px 0px;
}

.ypp-d-flex {
  display: flex;
}

.ypp-d-none {
    display: none !important;
}

/* =========================
   Contenedores y Overlays
========================= */

.ypp-overlay,
.ypp-modalOverlay {
  position: fixed;
  top: 0;
  left: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100vw;
  height: 100vh;
  background: var(--ypp-overlay);
  z-index: var(--ypp-z-overlay);
}

.ypp-videosContainer {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: var(--ypp-bg, #0f0f0f);
  border: 1px solid var(--ypp-border, #303030);
  border-radius: 12px;
  width: 90%;
  max-width: 800px;
  max-height: 85vh;
  color: var(--ypp-text, #f1f1f1);
  box-shadow: var(--ypp-shadow, 0 12px 24px rgba(0, 0, 0, 0.5));
  z-index: var(--ypp-z-modal);
  display: flex;
  flex-direction: column;
  opacity: 0;
  transform: translate(-50%, -50%) translateY(20px) scale(0.95);
  animation: videosModalSlideIn 0.3s ease-out forwards;
}

@keyframes videosModalSlideIn {
  to {
    opacity: 1;
    transform: translate(-50%, -50%) translateY(0) scale(1);
  }
}

.ypp-container {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: var(--ypp-bg);
  border-radius: 8px;
  box-shadow: var(--ypp-shadow-md);
  padding: 0; /* Padding manejado por hijos */
  z-index: var(--ypp-z-modal);
  width: 550px; /* Un poco más ancho para los nuevos botones */
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  font-family: var(--ypp-font-base);
  color: var(--ypp-text);
}

.ypp-time-display {
  color: var(--ypp-white);
  font-weight: bold;
  margin-left: 10px;
  font-size: 1.4rem;
  background: /* #4a4a4a91; */ hsla(0,0%,6.7%,0.4);
  padding: var(--ypp-spacing-sm) var(--ypp-spacing-md);
  border-radius: 14px;
  cursor: pointer;
  transition: all 0.2s ease;

  &:hover {
    background: var(--ypp-success-dark);
  }
}

/* Estilo específico para mensajes en Shorts - integrado en el player */
.ypp-shorts-time-display {
  background: hsla(109.7, 56.1%, 22.4%, 0.7);
  color: #fff;
  padding: 4px 0px;
  font-size: 13px;
  font-weight: 700;
  /* backdrop-filter: blur(10px); */
  cursor: pointer;
  transition: all 0.2s ease;
  pointer-events: auto;
  justify-content: center;
  display: flex;
  gap: 6px;
  position: relative; /* asegurar stacking context */
  z-index: var(--ypp-z-toast, 10001); /* asegurar visibilidad por encima de overlays transitorios */
  margin: 4px auto 0; /* centrar en metapanel */
  border-radius: var(--ypp-spacing-md);

  /* Truncado de texto */
  white-space: nowrap;
  overflow: hidden;

  &:hover {
    background: var(--ypp-success-dark, #15803d);
    transform: translateY(-1px);
  }
}

/* Fallback flotante cuando el metapanel está oculto */
.ypp-shorts-time-display.ypp-floating {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: 64px; /* por encima de botones de acción */
  z-index: var(--ypp-z-toast, 10001);
}


/* =========================
   Header, Footer, Layout
========================= */

.ypp-header,
.ypp-modalHeader {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 6px 12px;
  border-bottom: 1px solid var(--ypp-border);
  flex-shrink: 0;
}

.ypp-filters {
  padding: var(--ypp-spacing-md) var(--ypp-spacing-lg);
  border-bottom: 1px solid var(--ypp-border);
  display: flex;
  flex-direction: column;
  gap: var(--ypp-spacing-md);
  flex-shrink: 0;
  gap: 0;
}

.ypp-footer {
  padding: var(--ypp-spacing-md) var(--ypp-spacing-lg);
  border-top: 2px solid var(--ypp-border);
  display: flex;
  flex-direction: column;
  gap: var(--ypp-spacing-sm);
  z-index: 10;
  flex-shrink: 0;
}

.ypp-footer-row {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--ypp-spacing-sm);
  flex-wrap: wrap;
}

.ypp-footer-row-bottom {
  justify-content: space-between;
}

#video-list-container {
  flex-grow: 1; /* Ocupar el espacio restante */
  overflow-y: auto; /* Hacer scrollable solo esta parte */
  padding: var(--ypp-spacing-md) var(--ypp-spacing-lg);
}

.ypp-settingsContent {
  display: flex;
  flex-direction: column;
  gap: var(--ypp-spacing-md);
  max-height: 60vh;
  overflow-y: auto;
}

.ypp-btnGroup {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 12px;
  padding: 16px 24px;
  background: var(--ypp-bg, #0f0f0f);
  border-radius: 0 0 12px 12px;
  flex-shrink: 0;
  margin-top: auto;
}

.ypp-saving-options{
  display: flex;
  flex-direction: column;
  background: var(--ypp-border);
  border-radius: 6px;
  padding: 10px;
}

.ypp-container-saving-options {
  background: var(--ypp-bg-secondary);
  border-radius: 6px;
  padding: 6px;
  gap: 8px;
  display: flex;
  flex-direction: column;
  margin-bottom: 10px;

}

.ypp-label-save-type{
    margin: 0 0 0 10px
}

/* =========================
   Tipografía
========================= */

.ypp-emptyMsg {
  text-align: center;
  color: #aaa;
  padding: 40px 24px;
  font-size: 1.4rem;
}

.ypp-playlistTitle {
  margin: 8px 0 4px;
  color: #065fd4;
  cursor: pointer;
  text-decoration: none;
  display: block;
  font-size: 1.2rem;
  font-weight: 500;
}

.ypp-playlistTitle:hover {
  color: #0550b3;
  text-decoration: underline;
}

.ypp-titleLink {
  font-weight: 600;
  font-size: 1.4rem;
  color: var(--ypp-link);
  text-decoration: none;
  display: block;
  margin-bottom: 2px;
}

.ypp-titleLink:hover {
  text-decoration: underline;
}

.ypp-author,
.ypp-views {
  font-size: 1.1rem;
  color: var(--ypp-muted);
}

.ypp-author-link {
  color: var(--ypp-link);
  text-decoration: none;
  transition: color 0.2s;
}

.ypp-author-link:hover {
  color: var(--ypp-primary-dark);
  text-decoration: underline;
}

.ypp-timestamp,
.ypp-progressInfo {
  font-size: 1.3rem;
  margin-top: 4px;
  display: flex;
  align-items: center;
  gap: 4px;
}

.ypp-timestamp {
  color: var(--ypp-muted);
}

.ypp-timestamp.forced {
    color: var(--ypp-primary-dark);
    font-weight: bold;
}

.ypp-timestamp.completed {
    color: var(--ypp-success);
    font-weight: bold;
}

.ypp-timestamp.forced.completed {
    /* Video con tiempo fijo Y completado: color mixto */
    color: #15803d;
    font-weight: bold;
    background: linear-gradient(90deg, var(--ypp-primary-dark) 0%, var(--ypp-success) 100%);
    background-clip: text;
}

/* =========================
   Video List
========================= */

.ypp-videoWrapper {
  display: flex;
  align-items: center;
  margin-bottom: var(--ypp-spacing-md);
  border-bottom: 1px solid var(--ypp-border);
  padding: var(--ypp-spacing-md);
}

.ypp-videoWrapper.playlist-item {
  border-radius: 6px;
  margin-bottom: var(--ypp-spacing-sm);
  transition: all 0.2s ease;
}

.ypp-videoWrapper.regular-item {
  background-color: var(--ypp-bg-secondary);
  border-left: 4px solid var(--ypp-border);
}

.ypp-playlist-indicator {
  display: flex;
  align-items: center;
  margin: 4px 0;
  font-size: 0.85em;
  opacity: 1;
  background: rgba(0, 0, 0, 0.75);
  color: #ffffff;
  padding: 3px 8px;
  border-radius: 6px;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
  backdrop-filter: blur(3px);
  font-weight: 600;
  border: 1px solid rgba(255, 255, 255, 0.2);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}

.ypp-playlist-link {
  display: inline-flex;
  align-items: center;
  margin-left: 8px;
  opacity: 0.7;
  transition: opacity 0.2s ease;
}

.ypp-playlist-link:hover {
  opacity: 1;
}

/* Estilos para modo de selección */
.ypp-videoWrapper.selection-mode {
  cursor: pointer;
  transition: all 0.2s ease;
}

.ypp-videoWrapper.selection-mode:hover {
  background-color: var(--ypp-bg) !important;
  /* transform: translateX(-1px); */
}

.ypp-video-checkbox {
  min-width: 15px;
  margin: 0 10px;
  transform: scale(1.2);
  cursor: pointer;
}

/* Estilos para el área de playlist integrada */
.ypp-playlist-creation-area {
  margin-top: 12px;
  padding: 15px;
  background-color: var(--ypp-bg-secondary);
  border: 1px solid var(--ypp-border);
  border-radius: 6px;
  display: none;
}

.ypp-playlist-creation-area.active {
  display: block;
}

.ypp-playlist-textarea {
  width: 100%;
  height: 50px;
  max-height: 40px;
  border: 1px solid var(--ypp-border);
  border-radius: 4px;
  font-family: 'Courier New', monospace;
  font-size: 11px;
  line-height: 1.3;
  background-color: var(--ypp-bg);
  color: var(--ypp-text);
  resize: none;
  overflow-y: auto;
  word-wrap: break-word;
}

.ypp-playlist-actions {
  display: flex;
  gap: 10px;
  margin-top: 10px;
  justify-content: center;
}

.ypp-footer-row.hidden {
  display: none;
}

.ypp-thumb {
  max-width: 110px;
  max-height: 80px;
  object-fit: cover;
  border-radius: 4px;
  margin-right: var(--ypp-spacing-sm);
  flex-shrink: 0;
}

.ypp-infoDiv {
  flex-grow: 1;
  min-width: 0; /* Permite que el contenedor se encoja correctamente */
}

.ypp-containerButtonsTime {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}

.ypp-sort-select, .ypp-filter-select {
  background: #1a1a1a;
  border: 1px solid #303030;
  color: #f1f1f1;
  padding: 8px 12px;
  border-radius: 6px;
  font-size: 1.3rem;
  cursor: pointer;
  transition: border-color 0.2s ease, background-color 0.2s ease;
  flex: 1;
  width: auto;
  margin-bottom: 8px;
}

.ypp-sort-select:focus, .ypp-filter-select:focus {
  outline: none;
  border-color: #065fd4;
  background: #252525;
}

.ypp-sort-select option, .ypp-filter-select option {
  background: #1a1a1a;
  color: #f1f1f1;
}


.ypp-search-input {
  background: #1a1a1a;
  border: 1px solid #303030;
  color: #f1f1f1;
  padding: 8px 12px;
  border-radius: 6px;
  font-size: 1.3rem;
  transition: border-color 0.2s ease, background-color 0.2s ease;
  flex: 1;
  min-width: 200px;
}

.ypp-search-input:focus {
  outline: none;
  border-color: #065fd4;
  background: #252525;
}

.ypp-search-input::placeholder {
  color: #888;
}

/* =========================
   Botones
========================= */

.ypp-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 5px 14px;
  font-weight: 500;
  font-size: 1.4rem;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.2s ease;
  border: none;
  outline: none;
  position: relative;
  overflow: hidden;
  min-height: 20px;
  gap: 8px;
  background: var(--ypp-primary);
  color: var(--ypp-white);
}

.ypp-btn::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(255, 255, 255, 0.1);
  opacity: 0;
  transition: opacity 0.2s ease;
}

.ypp-btn:hover::before {
  opacity: 1;
}

.ypp-btn:active {
  transform: scale(0.98);
}

.ypp-btn:hover {
  background: var(--ypp-primary-dark);
}

.ypp-btn:active {
  background: #0441a1;
}


.ypp-btn-outlined {
  background: transparent;
  border: 1px solid #065fd4;
    color: inherit;

  &:hover {
    background: rgba(6, 95, 212, 0.3);
    color: inherit;
  }
}

.ypp-save-button {
  background: transparent;
  border: 1px solid var(--ypp-success);
    color: inherit;

  &:hover {
    background: rgba(22, 212, 6, 0.3);
    color: inherit;
  }

  &:active {
    background: #008855;
  }
}

.ypp-btn-secondary {
  background: #f1f1f1;
  color: #0f0f0f;
}

.ypp-btn-secondary:hover {
  background: var(--ypp-success-dark);
  color: var(--ypp-bg);
}

.ypp-btn-secondary:active {
  background: #d9d9d9;
}

.ypp-btn-delete {
  background: transparent;
  border: 1px solid var(--ypp-danger);
  color: var(--ypp-danger);;
}

.ypp-btn-delete:hover {
  background: var(--ypp-danger);
  color: inherit;
}

.ypp-btn-delete:active {
  background: rgba(255, 68, 68, 0.2);
}


.ypp-btn-danger {
  background: #ff4444;
  color: #ffffff;
}

.ypp-btn-danger:hover {
  background: var(--ypp-danger);
}

.ypp-btn-danger:active {
  background: #dd2222;
}

.ypp-btn-small {
    padding: 8px;
    width: 36px;
    height: 36px;
    min-height: 36px;
    flex-shrink: 0;
    border-radius: 18px;
}

.ypp-btn-close{
  background: var(--ypp-text);
  border: 1px solid #303030;
  color: var(--ypp-bg);
}

.ypp-btn-close:hover {
background: var(--ypp-danger);
}





@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(20px) scale(0.95);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* =========================
   Toasts
========================= */

.ypp-toast-container {
  position: fixed;
  top: var(--ypp-spacing-md);
  right: var(--ypp-spacing-md);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  z-index: var(--ypp-z-toast);
  pointer-events: none;
}

.ypp-toast {
  display: flex;
  gap: 10px;
  justify-content: center;
  align-items: center;
  background: var(--ypp-bg, #0f0f0f);
  color: var(--ypp-text, #f1f1f1);
  padding: 12px 16px;
  border-radius: 8px;
  border: 1px solid var(--ypp-border, #303030);
  font-size: 1.4rem;
  max-width: 300px;
  animation: slideInRight 0.3s ease-out;
  transition: opacity 0.2s ease;
  backdrop-filter: blur(10px);
  pointer-events: auto;
}

.ypp-toast.persistent {
  position: relative;
}

.ypp-toast-close {
  background: var(--ypp-text);
  border: 1px solid #303030;
  color: var(--ypp-bg);
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background-color 0.2s ease;
  font-size: 12px;
  padding: 0;
}

.ypp-toast-close:hover {
  background: var(--ypp-danger);
}

.ypp-toast-action {
    background: var(--ypp-primary);
    border: none;
    color: white;
    padding: 4px 8px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 12px;
    margin-left: auto;
}

/* =========================
   Modal
========================= */

.ypp-modalOverlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: var(--ypp-overlay, rgba(0, 0, 0, 0.8));
  backdrop-filter: blur(4px);
  z-index: var(--ypp-z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  animation: fadeIn 0.2s ease-out;
}

.ypp-modalBox {
  background: var(--ypp-bg, #0f0f0f);
  border: 1px solid var(--ypp-border, #303030);
  border-radius: 12px;
  padding: 0;
  color: var(--ypp-text, #f1f1f1);
  max-width: 500px;
  width: 90%;
  max-height: 85vh;
  overflow: hidden;
  box-shadow: var(--ypp-shadow, 0 12px 24px rgba(0, 0, 0, 0.5));
  animation: slideUp 0.3s ease-out;
  display: flex;
  flex-direction: column;
  opacity: 0;
  transform: translateY(20px) scale(0.95);
  animation: modalSlideIn 0.3s ease-out forwards;
}

@keyframes modalSlideIn {
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

.ypp-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 6px 12px;
  border-bottom: 1px solid var(--ypp-border, #303030);
  background: var(--ypp-bg, #0f0f0f);
  border-radius: 12px 12px 0 0;
  flex-shrink: 0;
}

.ypp-header h2 {
  margin: 0;
  color: var(--ypp-text, #f1f1f1);
  font-size: 1.8rem;
  font-weight: 500;
}

.ypp-modalTitle {
  font-weight: 500;
  color: var(--ypp-text, #f1f1f1);
  font-size: 1.6rem;
  margin: 0;
  flex: 1;
}

.ypp-modalBody {
  font-size: 1.4rem;
  padding: 10px 24px;
  flex: 1;
  background: var(--ypp-bg, #0f0f0f);
  min-height: 0;
}

/* =========================
   Inputs y Forms
========================= */

.ypp-label {
  display: flex;
  align-items: center;
  color: var(--ypp-text, #f1f1f1);
  font-size: 1.4rem;
  transition: color 0.2s ease;
  white-space: nowrap;
  margin: 8px 0;
}

.ypp-label input[type="checkbox"] {
  margin-right: 12px;
  width: 18px;
  height: 18px;
  accent-color: var(--ypp-input-focus, #065fd4);
}

.ypp-label-language {
    gap: 12px;
}

.ypp-label-filters {
    margin: 0 8px 0 0;
}

.ypp-input {
  width: 100%;
  padding: 12px 16px;
  margin-bottom: 16px;
  background: var(--ypp-input, #1a1a1a);
  border: 1px solid var(--ypp-input-border, #303030);
  border-radius: 8px;
  color: var(--ypp-text, #f1f1f1);
  font-size: 1.4rem;
  transition: border-color 0.2s ease, background-color 0.2s ease;
}

.ypp-input:focus {
  outline: none;
  border-color: var(--ypp-input-focus, #065fd4);
  background: var(--ypp-bg-secondary, #252525);
}

.ypp-input::placeholder {
  color: var(--ypp-text-secondary, #888);
}

.ypp-percent-symbol {
    margin-left: 6px;
}

.ypp-select {
  width: 100%;
  padding: 12px 16px;
  background: var(--ypp-input, #1a1a1a);
  border: 1px solid var(--ypp-input-border, #303030);
  border-radius: 8px;
  color: var(--ypp-text, #f1f1f1);
  font-size: 1.4rem;
  cursor: pointer;
  transition: border-color 0.2s ease, background-color 0.2s ease;
}

.ypp-select:focus {
  outline: none;
  border-color: var(--ypp-input-focus, #065fd4);
  background: var(--ypp-bg-secondary, #252525);
}

.ypp-select option {
  background: var(--ypp-input, #1a1a1a);
  color: var(--ypp-text, #f1f1f1);
}

.ypp-input-small {
  margin-left: 10px;
  border-radius: 10px;
  padding: 2px 16px;
}

/* =========================
   Floating Button
========================= */

.ypp-floatingBtnContainer {
  position: fixed;
  bottom: var(--ypp-spacing-md);
  right: var(--ypp-spacing-md);
  z-index: var(--ypp-z-overlay);
  display: flex;
  gap: 10px;
}

/* =========================
   Selector de Idioma con Banderas
========================= */

.ypp-language-selector {
  display: flex;
  align-items: center;
  gap: 8px;
}

.ypp-language-flag {
  font-size: 1.2em;
  margin-right: 5px;
}
`;
        document.head.appendChild(style);
    }

    // ------------------------------------------
    // MARK: 🎨 Theme
    // ------------------------------------------

    function isYouTubeDarkTheme() {
        // Detectar si YouTube está en modo oscuro
        const htmlElement = document.documentElement;
        const computedStyle = getComputedStyle(htmlElement);

        // Verificar tema oscuro
        return (
            htmlElement.getAttribute('dark') === 'true' ||
            htmlElement.hasAttribute('dark') ||
            computedStyle.getPropertyValue('--yt-spec-base-background') === '#0f0f0f' ||
            computedStyle.getPropertyValue('--yt-spec-text-primary') === '#f1f1f1' ||
            document.body.classList.contains('dark-theme') ||
            document.querySelector('ytd-masthead')?.getAttribute('dark') === 'true'
        );
    }

    // ------------------------------------------
    // MARK: 🎨 SVG Icons
    // ------------------------------------------

    // SVGs como strings para reemplazar emojis
    const SVG_ICONS = {
        folder: '<svg class="ypp-svgFolderIcon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2h-8l-2-2z"/></svg>',
        timer: '<svg class="ypp-svgTimerIcon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M15 1H9v2h6V1zm-4 13h2V8h-2v6zm8.03-6.61l1.42-1.42c-.43-.51-.9-.99-1.41-1.41l-1.42 1.42A8.962 8.962 0 0 0 12 4c-4.97 0-9 4.03-9 9s4.02 9 9 9a9 9 0 0 0 7.03-14.61zM12 20c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/></svg>',
        check: '<svg width="16" height="16" viewBox="0 0 24 24" fill="var(--ypp-success)"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>',
        save: '<svg class="ypp-svgSaveIcon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>',
        chart: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><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-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"/></svg>',
        settings: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>',
        close: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="vertical-align: text-bottom;"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>',
        // play: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>',
        trash: '<svg id="svgTrashIcon" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>',
        download: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>',
        upload: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16h6v-6h4l-7-7-7 7h4zm-4 2h14v2H5z"/></svg>',
        externalLink: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>',
        playlist: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M15 6H3v2h12V6zm0 4H3v2h12v-2zM3 16h8v-2H3v2zM17 6v8.18c-.31-.11-.65-.18-1-.18-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3V8h3V6h-5z"/></svg>',
        copy: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>',
        // calendar: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"/></svg>',
        // sort: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z"/></svg>',
        locked: '<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" version="1.1" viewBox="0 0 30 30"><path d="M9 16V8c0-3.3 2.7-6 6-6h0c3.3 0 6 2.7 6 6v8" style="fill:none;stroke:#6a83ba;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10"/><path d="M22 29H8c-1.1 0-2-.9-2-2V16c0-1.1.9-2 2-2h14c1.1 0 2 .9 2 2v11c0 1.1-.9 2-2 2z" style="fill:#f2bb41;stroke:#f2bb41;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10"/><path d="M15 24h0c-.6 0-1-.4-1-1v-3c0-.6.4-1 1-1h0c.6 0 1 .4 1 1v3c0 .6-.4 1-1 1z" style="fill:#354c75;stroke:#354c75;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10"/></svg>',
        unlocked: '<svg idth="16" height="16" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path fill="#c9d0d7" d="M484.17 244.87H417.4a16.7 16.7 0 0 1-16.7-16.7v-77.9c0-27.63-22.46-50.1-50.08-50.1s-50.09 22.47-50.09 50.1v111.3a16.7 16.7 0 0 1-16.7 16.7h-66.78a16.7 16.7 0 0 1-16.7-16.7v-111.3C200.35 67.4 267.76 0 350.62 0s150.26 67.4 150.26 150.26v77.91a16.7 16.7 0 0 1-16.7 16.7z"/><path fill="#b8c2c9" d="M400.7 150.26v77.91a16.7 16.7 0 0 0 16.7 16.7h66.78a16.7 16.7 0 0 0 16.7-16.7v-77.9C500.86 67.4 433.46 0 350.6 0v100.17a50.14 50.14 0 0 1 50.09 50.1z"/><path fill="#e79d2e" d="M328.35 512H61.22a50.14 50.14 0 0 1-50.09-50.09V294.96a50.14 50.14 0 0 1 50.09-50.09h267.13a50.14 50.14 0 0 1 50.08 50.09V461.9A50.14 50.14 0 0 1 328.35 512z"/><path fill="#d8842a" d="M378.44 461.91V294.96a50.14 50.14 0 0 0-50.1-50.09H194.79V512h133.57a50.14 50.14 0 0 0 50.08-50.09z"/><g fill="#6e6057"><path d="M194.78 445.22a16.7 16.7 0 0 1-16.7-16.7v-66.78a16.7 16.7 0 0 1 33.4 0v66.78a16.7 16.7 0 0 1-16.7 16.7z"/><path d="M194.78 378.44c-18.41 0-33.39-14.98-33.39-33.4s14.98-33.39 33.4-33.39 33.38 14.98 33.38 33.4-14.97 33.38-33.39 33.38zm0-33.4h.11-.1zm0 0h.11-.1zm0 0h.11-.1zm0 0zm0 0h.11-.1zm0-.01h.11-.1zm0 0h.11-.1z"/></g><g fill="#615349"><path d="M211.48 428.52v-66.78a16.7 16.7 0 0 0-16.7-16.7v100.18a16.7 16.7 0 0 0 16.7-16.7z"/><path d="M194.78 378.44c18.42 0 33.4-14.98 33.4-33.4s-14.98-33.39-33.4-33.39v66.79z"/></g></svg>',
        pin: '<svg class="ypp-svgPinIcon" width="16" height="16" viewBox="0 0 508.901 508.901" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve"><defs><path id="a" fill="#a31c09" d="m342.08 279.177 58.606-58.606c-24.594-6.727-48.746-21.389-69.853-42.496-21.116-21.116-35.257-45.789-41.366-70.991l-59.727 59.719-48.719 48.719c6.118 25.212 22.581 47.554 43.697 68.661 21.107 21.116 44.067 36.97 68.661 43.697l48.701-48.703z"/></defs><path fill="#a31c09" d="M505.605 190.556c-13.789 13.789-66.887-16.949-118.599-68.661s-82.45-104.81-68.661-118.599 66.887 16.949 118.599 68.661 82.45 104.811 68.661 118.599"/><path fill="#d9dbe8" d="m0 508.9 112.358-162.295 49.937 49.938z"/><path fill="#ce3929" d="M387.007 121.894c-51.712-51.712-82.45-104.81-68.661-118.599-49.991 49.991-39.23 123.065 12.482 174.777s121.671 65.589 171.652 15.607l-.786-.821c-18.069 6.577-66.93-23.207-114.687-70.964"/><use xlink:href="#a"/><path fill="#ce3929" d="M311.324 389.978c2.348-21.486-1.607-44.226-11.829-68.22l-6.118 6.126c-24.594-6.735-47.554-22.59-68.661-43.697-21.116-21.107-37.579-43.458-43.697-68.661l6.241-6.241-.274-.282c-24.143-10.346-47.016-14.345-68.626-11.979-40.157 4.378-64.071 45.877-47.634 82.785 12.509 28.072 35.566 60.734 66.322 91.489 30.746 30.747 63.417 53.813 91.489 66.313 36.901 16.437 78.4-7.477 82.787-47.633"/></svg>',
        playOrPause: '<svg class="ypp-svgPlayOrPauseIcon" width="16"height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#3B88C3" d="M36 32a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4h28a4 4 0 0 1 4 4v28z"></path><path fill="#FFF" d="m6 7 13 11L6 29zm20 0h4v22h-4zm-7 0h4v22h-4z"></path></svg>',
        warning: '<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#FFCC4D" d="M2.65 35C.81 35 0 33.66.85 32.03l15.6-30.06c.86-1.63 2.24-1.63 3.1 0l15.6 30.06c.85 1.63.04 2.97-1.8 2.97H2.65z"/><path fill="#231F20" d="M15.58 28.95A2.42 2.42 0 0 1 18 26.53a2.42 2.42 0 0 1 2.42 2.42A2.42 2.42 0 0 1 18 31.37a2.42 2.42 0 0 1-2.42-2.42zm.19-18.29c0-1.3.96-2.1 2.23-2.1 1.24 0 2.23.83 2.23 2.1V22.6c0 1.27-.99 2.1-2.23 2.1-1.27 0-2.23-.8-2.23-2.1V10.66z"/></svg>',
        import: '<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke="#1C274C" stroke-linecap="round" stroke-width="1.5" d="M4 12a8 8 0 1 0 16 0" opacity=".5"/><path stroke="#1C274C" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 4v10m0 0 3-3m-3 3-3-3"/></svg>',
        export: '<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke="#1C274C" stroke-linecap="round" stroke-width="1.5" d="M4 12a8 8 0 1 0 16 0" opacity=".5"/><path stroke="#1C274C" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 14V4m0 0 3 3m-3-3L9 7"/></svg>',
        error: '<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><path fill="red" d="M13 10.66q0 .4-.28.68l-1.38 1.38q-.28.28-.68.28t-.69-.28L7 9.75l-2.97 2.97q-.28.28-.69.28-.4 0-.68-.28l-1.38-1.38Q1 11.06 1 10.66t.28-.69L4.25 7 1.28 4.03Q1 3.75 1 3.34q0-.4.28-.68l1.38-1.38Q2.94 1 3.34 1t.69.28L7 4.25l2.97-2.97q.28-.28.69-.28.4 0 .68.28l1.38 1.38q.28.28.28.68t-.28.69L9.75 7l2.97 2.97q.28.28.28.69z"/></svg>'
    };

    // ------------------------------------------
    // MARK: 🎨 Estilo barra progreso
    // ------------------------------------------
    /**
    * Aplica degradado de colores a la barra de progreso del reproductor de YouTube usando CSS
    * @param {number} currentTime - Tiempo actual del video en segundos
    * @param {number} duration - Duración total del video en segundos
    * @param {string} type - Tipo de video ('shorts' o 'watch')
    */
    function updateProgressBarGradient(currentTime, duration, type = 'watch') {
        try {
            // Verificar si la funcionalidad está deshabilitada en la configuración
            if (!cachedSettings.enableProgressBarGradient) {
                return;
            }

            if (!duration || duration <= 0) return;

            const percent = Math.min(100, Math.round((currentTime / duration) * 100));
            const progressColor = getProgressColor(percent);

            // Detectar si estamos en shorts
            const isShorts = type === 'shorts' || window.location.pathname.includes('/shorts/');

            if (isShorts) {
                // Selectores específicos para shorts con estructura correcta
                const shortsProgressHost = document.querySelector('.desktopShortsPlayerControlsHost .ytPlayerProgressBarHost, .ytPlayerProgressBarHost');
                const shortsPlayedBar = document.querySelector('.ytProgressBarLineProgressBarPlayed');
                const shortsHoveredBar = document.querySelector('.ytProgressBarLineProgressBarHovered');
                const shortsPlayheadDot = document.querySelector('.ytProgressBarPlayheadProgressBarPlayheadDot');

                if (shortsProgressHost) {
                    // Aplicar variables CSS para el degradado en shorts
                    shortsProgressHost.style.setProperty('--ytp-progress-color', progressColor, 'important');
                    shortsProgressHost.style.setProperty('--ytp-progress-percent', `${percent}%`, 'important');

                    // Aplicar estilos directamente a los elementos de la barra de shorts
                    if (shortsPlayedBar) {
                        shortsPlayedBar.style.backgroundColor = progressColor;
                        shortsPlayedBar.style.setProperty('background', progressColor, 'important');
                    }

                    if (shortsHoveredBar) {
                        shortsHoveredBar.style.backgroundColor = progressColor;
                        shortsHoveredBar.style.setProperty('background', progressColor, 'important');
                    }

                    if (shortsPlayheadDot) {
                        shortsPlayheadDot.style.backgroundColor = progressColor;
                        shortsPlayheadDot.style.setProperty('background', progressColor, 'important');
                    }
                }
            } else {
                // Selectores para videos regulares
                const progressContainer = document.querySelector('.ytp-progress-bar');
                const playProgress = document.querySelector('.ytp-play-progress');
                const hoverProgress = document.querySelector('.ytp-hover-progress');

                if (progressContainer) {
                    // Aplicar variables CSS para el degradado
                    progressContainer.style.setProperty('--ytp-progress-color', progressColor, 'important');
                    progressContainer.style.setProperty('--ytp-progress-percent', `${percent}%`, 'important');

                    // Aplicar estilos directamente a la barra de progreso
                    if (playProgress) {
                        playProgress.style.backgroundColor = progressColor;
                        playProgress.style.setProperty('background', progressColor, 'important');
                    }

                    if (hoverProgress) {
                        hoverProgress.style.backgroundColor = progressColor;
                        hoverProgress.style.setProperty('background', progressColor, 'important');
                    }
                }
            }
        } catch (error) {
            // Silenciar errores para no afectar el funcionamiento principal
        }
    }

    // Inyecta CSS personalizado para la barra de progreso de YouTube (regular y shorts)
    function injectProgressBarCSS() {
        // Verificar si la funcionalidad está deshabilitada en la configuración
        if (!cachedSettings.enableProgressBarGradient) {
            log('injectProgressBarCSS', 'Degradado de barra de progreso deshabilitado en configuración');
            return;
        }

        // Verificar si ya existe el estilo para evitar duplicados
        if (document.getElementById('ypp-progress-bar-styles')) {
            log('injectProgressBarCSS', 'CSS ya existe, omitiendo inyección');
            return;
        }

        const css = `
            /* Barra de progreso personalizada con degradado de colores - Videos regulares */
            .ytp-progress-bar {
                --ytp-progress-color: #ff4533;
                --ytp-progress-percent: 0%;
            }

            .ytp-play-progress {
                background: var(--ytp-progress-color) !important;
                transition: background 0.3s ease !important;
            }

            .ytp-hover-progress {
                background: var(--ytp-progress-color) !important;
                transition: background 0.3s ease !important;
            }

            .ytp-progress-bar-container {
                background: linear-gradient(to right,
                    var(--ytp-progress-color) 0%,
                    var(--ytp-progress-color) var(--ytp-progress-percent),
                    rgba(255, 255, 255, 0.2) var(--ytp-progress-percent),
                    rgba(255, 255, 255, 0.2) 100%) !important;
                background-size: 100% 100% !important;
                transition: background 0.3s ease !important;
            }

            .ytp-load-progress {
                background: rgba(255, 255, 255, 0.3) !important;
            }

            /* Shorts - barra de progreso específica con estructura correcta */
            .desktopShortsPlayerControlsHost .ytPlayerProgressBarHost,
            .ytPlayerProgressBarHost {
                --ytp-progress-color: #ff4533;
                --ytp-progress-percent: 0%;
            }

            /* Barra de progreso principal de shorts */
            .ytProgressBarLineProgressBarPlayed {
                background: var(--ytp-progress-color) !important;
                transition: background 0.3s ease !important;
            }

            /* Barra de hover en shorts */
            .ytProgressBarLineProgressBarHovered {
                background: var(--ytp-progress-color) !important;
                transition: background 0.3s ease !important;
            }

            /* Contenedor principal de la barra de shorts */
            .ytProgressBarLineProgressBarLine {
                background: linear-gradient(to right,
                    var(--ytp-progress-color) 0%,
                    var(--ytp-progress-color) var(--ytp-progress-percent),
                    rgba(255, 255, 255, 0.2) var(--ytp-progress-percent),
                    rgba(255, 255, 255, 0.2) 100%) !important;
                background-size: 100% 100% !important;
                transition: background 0.3s ease !important;
            }

            /* Fondo de carga en shorts */
            .ytProgressBarLineProgressBarLoaded {
                background: rgba(255, 255, 255, 0.3) !important;
            }

            /* Punto del seek (playhead) en shorts */
            .ytProgressBarPlayheadProgressBarPlayheadDot {
                background: var(--ytp-progress-color) !important;
                transition: background 0.3s ease !important;
            }

            /* Asegurar que los estilos se apliquen sobre los de YouTube */
            .ytp-progress-bar .ytp-play-progress,
            .ytp-chrome-controls .ytp-progress-bar .ytp-play-progress {
                background: var(--ytp-progress-color) !important;
            }

            .ytp-progress-bar .ytp-hover-progress,
            .ytp-chrome-controls .ytp-progress-bar .ytp-hover-progress {
                background: var(--ytp-progress-color) !important;
            }

            /* Para el punto del seek (thumb) - regular */
            .ytp-scrubber-container .ytp-scrubber {
                background: var(--ytp-progress-color) !important;
            }

            .ytp-scrubber-button {
                background: var(--ytp-progress-color) !important;
            }
        `;

        try {
            // Crear y añadir el estilo
            const style = document.createElement('style');
            style.id = 'ypp-progress-bar-styles';
            style.textContent = css;
            document.head.appendChild(style);

            log('injectProgressBarCSS', 'CSS inyectado para barra de progreso (regular y shorts)');
        } catch (error) {
            conError('injectProgressBarCSS', 'Error al inyectar CSS:', error);
        }
    }

    /**
    * Calcula el color del progreso basado en el porcentaje y el tema (rojo -> naranja -> verde)
    * @param {number} percent - Porcentaje de progreso (0-100)
    * @returns {string} Color en formato hexadecimal
    */
    function getProgressColor(percent) {
        if (percent === null || percent === undefined) return '#666666';

        // Detectar si estamos en tema claro
        const isLightTheme = !isYouTubeDarkTheme();

        // Rangos de color para tema oscuro (colores oscuros para mejor contraste en playlists):
        // 0-33%: Rojo oscuro (#dd4444 -> #ff8844)
        // 34-66%: Naranja oscuro (#ff8844 -> #ffcc44)
        // 67-100%: Verde oscuro (#ffcc44 -> #00cc00)

        // Rangos de color para tema claro (colores oscuros con mejor contraste):
        // 0-33%: Rojo oscuro (#cc0000 -> #dd6600)
        // 34-66%: Naranja oscuro (#dd6600 -> #cc9900)
        // 67-100%: Verde oscuro (#cc9900 -> #008800)

        let color;
        if (isLightTheme) {
            // Colores para tema claro (más oscuros para mejor contraste)
            if (percent <= 33) {
                // Rojo oscuro a naranja oscuro
                const ratio = percent / 33;
                const r = Math.round(204 - (204 - 221) * ratio); // 204 -> 221
                const g = Math.round(0 + (102 - 0) * ratio); // 0 -> 102
                const b = 0;
                color = `rgb(${r}, ${g}, ${b})`;
            } else if (percent <= 66) {
                // Naranja oscuro a amarillo oscuro
                const ratio = (percent - 33) / 33;
                const r = Math.round(221 - (221 - 204) * ratio); // 221 -> 204
                const g = Math.round(102 + (153 - 102) * ratio); // 102 -> 153
                const b = 0;
                color = `rgb(${r}, ${g}, ${b})`;
            } else if (percent <= 95) {
                // Amarillo oscuro a verde oscuro
                const ratio = (percent - 66) / 29;
                const r = Math.round(204 - (204 - 0) * ratio); // 204 -> 0
                const g = Math.round(153 + (136 - 153) * ratio); // 153 -> 136
                const b = 0;
                color = `rgb(${r}, ${g}, ${b})`;
            } else {
                // Verde oscuro (casi completado o completado)
                color = '#008800';
            }
        } else {
            // Colores mejorados para tema oscuro (más oscuros para contraste en playlists)
            if (percent <= 33) {
                // Rojo oscuro a naranja
                const ratio = percent / 33;
                const r = Math.round(221 - (221 - 255) * ratio); // 221 -> 255
                const g = Math.round(68 + (136 - 68) * ratio); // 68 -> 136
                const b = 68;
                color = `rgb(${r}, ${g}, ${b})`;
            } else if (percent <= 66) {
                // Naranja a amarillo
                const ratio = (percent - 33) / 33;
                const r = 255;
                const g = Math.round(136 + (204 - 136) * ratio); // 136 -> 204
                const b = Math.round(68 + (68 - 68) * ratio); // 68 -> 68
                color = `rgb(${r}, ${g}, ${b})`;
            } else if (percent <= 95) {
                // Amarillo a verde oscuro
                const ratio = (percent - 66) / 29;
                const r = Math.round(255 - (255 - 0) * ratio); // 255 -> 0
                const g = Math.round(204 + (204 - 204) * ratio); // 204 -> 204
                const b = Math.round(68 + (68 - 68) * ratio); // 68 -> 68
                color = `rgb(${r}, ${g}, ${b})`;
            } else {
                // Verde oscuro (casi completado o completado) - mejor contraste en fondos celestes
                color = '#00cc00';
            }
        }

        return color;
    }

    // ------------------------------------------
    // MARK: 💾 Storage + Settings
    // ------------------------------------------
    /**
    * Objeto Storage para gestionar el almacenamiento local del navegador.
    * Proporciona métodos para guardar, obtener y eliminar datos,
    * así como para listar claves almacenadas con un prefijo específico.
    */
    // Backend de almacenamiento con fallback: localStorage -> GM_* (sync) -> memoria
    let storageBackend = 'local';
    let storageWarned = false;
    const STORAGE_INDEX_KEY = `${CONFIG.storagePrefix}INDEX_v1`;
    const memoryStore = {};
    const memoryIndex = new Set();

    function supportsSyncGM() {
        try {
            if (typeof GM_getValue !== 'function' || typeof GM_setValue !== 'function') return false;
            const probe = GM_getValue('__ypp_probe__', null);
            // Si devuelve una promesa, no es síncrono (no compatible con API de Storage)
            if (probe && typeof probe.then === 'function') return false;
            return true;
        } catch (_) { return false; }
    }

    function detectStorageBackend() {
        try {
            const testKey = `${CONFIG.storagePrefix}__test__`;
            localStorage.setItem(testKey, '1');
            localStorage.removeItem(testKey);
            return 'local';
        } catch (_) {
            return supportsSyncGM() ? 'gm' : 'memory';
        }
    }

    function warnOnceBackend() {
        if (!storageWarned && storageBackend !== 'local') {
            try { warn('Storage', `Usando backend alternativo: ${storageBackend}`); } catch (_) { }
            storageWarned = true;
        }
    }

    function gmIndexGet() {
        try {
            const raw = GM_getValue(STORAGE_INDEX_KEY, '[]');
            const arr = JSON.parse(raw || '[]');
            return Array.isArray(arr) ? arr : [];
        } catch (_) { return []; }
    }
    function gmIndexSet(arr) {
        try { GM_setValue(STORAGE_INDEX_KEY, JSON.stringify(arr || [])); } catch (_) { }
    }

    storageBackend = detectStorageBackend();

    const Storage = {
        /**
         * Obtiene un valor del almacenamiento con backend disponible.
         */
        get(key) {
            try {
                if (storageBackend === 'local') {
                    const raw = localStorage.getItem(`${CONFIG.storagePrefix}${key}`);
                    return raw ? JSON.parse(raw) : null;
                }
                warnOnceBackend();
                if (storageBackend === 'gm') {
                    const raw = GM_getValue(`${CONFIG.storagePrefix}${key}`, null);
                    return raw ? JSON.parse(raw) : null;
                }
                return Object.prototype.hasOwnProperty.call(memoryStore, key) ? memoryStore[key] : null;
            } catch (error) {
                conError('Storage', `Storage.get: Error al obtener la clave "${key}"`, error);
                return null;
            }
        },

        /**
         * Guarda un valor en el backend disponible.
         */
        set(key, value) {
            try {
                if (storageBackend === 'local') {
                    localStorage.setItem(`${CONFIG.storagePrefix}${key}`, JSON.stringify(value));
                    return;
                }
                warnOnceBackend();
                if (storageBackend === 'gm') {
                    GM_setValue(`${CONFIG.storagePrefix}${key}`, JSON.stringify(value));
                    const idx = new Set(gmIndexGet());
                    idx.add(key);
                    gmIndexSet(Array.from(idx));
                    return;
                }
                memoryStore[key] = value;
                memoryIndex.add(key);
            } catch (error) {
                conError('Storage', `Storage.set: Error al guardar la clave "${key}"`, error);
            }
        },

        /**
         * Elimina un valor del almacenamiento local.
         * @param {string} key - La clave del valor que se eliminará.
         */
        del(key) {
            try {
                if (storageBackend === 'local') {
                    localStorage.removeItem(`${CONFIG.storagePrefix}${key}`);
                    return;
                }
                warnOnceBackend();
                if (storageBackend === 'gm') {
                    // No hay GM_deleteValue declarado; limpiar índice y sobrescribir con null
                    GM_setValue(`${CONFIG.storagePrefix}${key}`, null);
                    const idx = new Set(gmIndexGet());
                    idx.delete(key);
                    gmIndexSet(Array.from(idx));
                    return;
                }
                delete memoryStore[key];
                memoryIndex.delete(key);
            } catch (error) {
                conError('Storage', `Storage.del: Error al eliminar la clave "${key}"`, error);
            }
        },

        /**
         * Lista claves según backend disponible.
         */
        keys() {
            try {
                if (storageBackend === 'local') {
                    return Object.keys(localStorage)
                        .filter((fullKey) => fullKey.startsWith(CONFIG.storagePrefix))
                        .map((fullKey) => fullKey.slice(CONFIG.storagePrefix.length));
                }
                warnOnceBackend();
                if (storageBackend === 'gm') {
                    return gmIndexGet();
                }
                return Array.from(memoryIndex);
            } catch (error) {
                conError('Storage', 'Storage.keys: Error al listar claves', error);
                return [];
            }
        }
    };

    /**
    * Objeto Settings para gestionar la configuración del usuario.
    * Proporciona métodos asíncronos para obtener y establecer
    * la configuración del usuario utilizando GM_getValue y GM_setValue.
    */
    const Settings = {
        /**
         * Obtiene la configuración del usuario.
         * @returns {Promise<Object>} Una promesa que resuelve un objeto con
         * los ajustes del usuario, combinando los ajustes por defecto
         * con los ajustes almacenados.
         */
        async get() {
            try {
                const raw = await GM_getValue(CONFIG.userSettingsKey, null);
                const parsed = raw ? JSON.parse(raw) : {};
                return { ...CONFIG.defaultSettings, ...parsed };
            } catch (error) {
                conError('Settings', 'Error al cargar configuración del usuario:', error);
                return { ...CONFIG.defaultSettings };
            }
        },

        /**
         * Establece la configuración del usuario.
         * @param {Object} settings - Un objeto que contiene los nuevos ajustes del usuario.
         * @returns {Promise<void>} Una promesa que resuelve cuando la configuración es guardada.
         */
        async set(settings) {
            try {
                const serialized = JSON.stringify(settings);
                await GM_setValue(CONFIG.userSettingsKey, serialized);
            } catch (error) {
                conError('Settings', 'Error al guardar configuración del usuario:', error);
            }
        }
    };

    // ------------------------------------------
    // MARK: 📊 Variables Globales
    // ------------------------------------------

    // Variables para controlar el estado de inicialización
    let YTHelper = null; // YouTube Helper API, Redeclarada en waitForHelper
    let isNavigating = false; // Variable global para registrar navegación
    let isResuming = false; // Variable global para registrar reanudación
    let navigationDebounceTimeout = null; // Timeout para evitar múltiples navegaciones
    let currentPageType = null; // Tipo de página actual (home, watch, playlist, etc.)
    // Ad Monitor
    let isAdPlaying = false;    // Estado global de anuncios (para compartir entre módulos)
    let isScriptPaused = false; // Variable global para controlar pausa total del script
    // let lastAdEndTime = 0; // Timestamp de cuando terminó el último anuncio
    // Estados de anuncios por tipo
    let isAdShortsPlaying = false; // Anuncio activo en Shorts
    let isAdWatchPlaying = false;  // Anuncio activo en reproductor principal (watch/embed/miniplayer)

    // Helper para decidir bloqueo por anuncios según tipo
    const isAdBlockedFor = (type) => {
        try {
            const t = (type || '').toLowerCase();
            if (t === 'shorts') return !!isAdShortsPlaying || !!isScriptPaused;
            // watch/embed/home/miniplayer/preview
            return !!isAdWatchPlaying || !!isScriptPaused;
        } catch (_) {
            return !!isAdPlaying || !!isScriptPaused;
        }
    };

    // ------------------------------------------
    // MARK: 🔧 Utils
    // ------------------------------------------

    // MARK: 🔧 Formateo de Tiempo
    /**
    * Formatea un valor de tiempo (en segundos o string) a un string en formato "MM:SS" o "HH:MM:SS".
    *
    * @param {number|string} input - Valor de tiempo a formatear.
    * @returns {string} - String con el tiempo formateado.
    * Ejemplos:
    * formatTime(65)         // "01:05"
    * formatTime("5:30")     // "05:30"
    * formatTime("1:05:30")  // "01:05:30"
    * formatTime("invalid")  // "00:00"
    */
    const formatTime = (input) => {
        let seconds;

        // Si es un número, lo usa directamente
        if (typeof input === 'number' && !isNaN(input)) {
            seconds = input;
        }
        // Si es un string, intenta convertirlo
        else if (typeof input === 'string') {
            // Maneja formatos como "5:30" o "05:30"
            if (input.includes(':')) {
                const parts = input.split(':').map(part => parseInt(part, 10));

                // Si es MM:SS
                if (parts.length === 2) {
                    seconds = parts[0] * 60 + parts[1];
                }
                // Si es HH:MM:SS
                else if (parts.length === 3) {
                    seconds = parts[0] * 3600 + parts[1] * 60 + parts[2];
                } else {
                    conError('Formato de tiempo no válido:', input);
                    return '00:00';
                }
            }
            // Intenta convertir directamente a número
            else {
                seconds = parseFloat(input);
            }
        }
        // Caso por defecto
        else {
            conError('Valor de entrada no válido:', input);
            return '00:00';
        }

        // Validación final
        if (typeof seconds !== 'number' || isNaN(seconds) || seconds < 0) {
            conError('Valor de segundos no válido:', input);
            return '00:00';
        }

        const hours = Math.floor(seconds / 3600);
        const minutes = Math.floor((seconds % 3600) / 60);
        const secs = Math.floor(seconds % 60);

        return hours > 0
            ? `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
            : `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
    };

    /**
    * Parsea un string de tiempo en formato "MM:SS" o "HH:MM:SS" a segundos.
    *
    * @param {string} timeStr - String con el tiempo en formato "MM:SS" o "HH:MM:SS".
    * @returns {number} Número de segundos correspondiente al string. Retorna 0 si el formato es inválido.
    *
    * @example
    * // Formato MM:SS → minutos y segundos
    * parseTimeToSeconds("5:30");      // → 330
    *
    * @example
    * // Formato HH:MM:SS → horas, minutos y segundos
    * parseTimeToSeconds("1:05:30");   // → 3930
    *
    * @example
    * // Formato inválido → 0
    * parseTimeToSeconds("invalid");   // → 0
    *
    * @example
    * // Cadena vacía o no string → 0
    * parseTimeToSeconds("");          // → 0
    * parseTimeToSeconds(null);        // → 0
    */
    const parseTimeToSeconds = (timeStr) => {
        if (typeof timeStr !== 'string' || !timeStr.includes(':')) return 0;

        const parts = timeStr.split(':').map(Number);

        // Retorna 0 si algún valor es NaN
        if (parts.some(isNaN)) return 0;

        if (parts.length === 2) return parts[0] * 60 + parts[1];
        if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];

        return 0;
    };

    /**
     * Convierte una duración en formato ISO 8601 (PT4M35S) a segundos
     * @param {string} isoDuration - Duración en formato ISO (ej: "PT4M35S", "PT1H2M3S")
     * @returns {number} Duración en segundos
     * @example
     * parseISODuration("PT4M35S");    // → 275
     * parseISODuration("PT1H2M3S");   // → 3723
     * parseISODuration("PT30S");      // → 30
     * parseISODuration("invalid");    // → 0
     */
    const parseISODuration = (isoDuration) => {
        if (typeof isoDuration !== 'string' || !isoDuration.startsWith('PT')) return 0;

        // Expresión regular para extraer horas, minutos y segundos
        const regex = /PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/;
        const matches = isoDuration.match(regex);

        if (!matches) return 0;

        const hours = parseInt(matches[1]) || 0;
        const minutes = parseInt(matches[2]) || 0;
        const seconds = parseInt(matches[3]) || 0;

        return hours * 3600 + minutes * 60 + seconds;
    };

    /**
    * Normaliza un valor de tiempo a segundos.
    *
    * @param {number|string} value - Valor de tiempo a normalizar.
    *                              Puede ser un número (ya en segundos)
    *                              o una cadena en formato "SS", "MM:SS" o "HH:MM:SS".
    * @returns {number} Número de segundos (0 si el valor es inválido o no existe).
    *
    * @example
    * // Número directo → devuelve el mismo número
    * normalizeSeconds(65);        // → 65
    *
    * @example
    * // "MM:SS" → minutos y segundos
    * normalizeSeconds("5:30");    // → 330
    *
    * @example
    * // "HH:MM:SS" → horas, minutos y segundos
    * normalizeSeconds("1:05:30"); // → 3930
    *
    * @example
    * // Sin argumento o null → 0
    * normalizeSeconds();          // → 0
    * normalizeSeconds(null);      // → 0
    *
    * @example
    * // Valor inválido → 0
    * normalizeSeconds("invalid"); // → 0
    */
    const normalizeSeconds = (value) => {
        if (!value) return 0;
        if (typeof value === 'number') return value;
        if (typeof value === 'string') return parseTimeToSeconds(value.trim());
        return 0;
    };

    // MARK: 🔧 SetInnerHTML
    /**
    * Asigna HTML de forma segura para compatibilidad con Trusted Types (Chrome)
    *
    * @param {HTMLElement} element - Elemento HTML al que se le asignará el HTML.
    * @param {string} html - HTML a asignar en su innerHTML.
    */
    function setInnerHTML(element, html) {
        if (window.trustedTypes && window.trustedTypes.createPolicy) {
            try {
                const policy = window.trustedTypes.createPolicy('youtube-playback-plox', {
                    createHTML: (string) => string
                });
                element.innerHTML = policy.createHTML(html);
            } catch (e) {
                // Si la creación de la política falla, usar innerHTML directamente
                element.innerHTML = html;
            }
        } else {
            // Si TrustedHTML no está soportado, usar innerHTML
            element.innerHTML = html;
        }
    }

    // MARK: 🔧 Crear Elemento
    /**
    * Crea un elemento HTML con varias opciones de configuración.
    *
    * @param {string} tag - Nombre del tag HTML a crear, e.g., 'div', 'span'.
    * @param {Object} [options] - Opciones para configurar el elemento.
    * @param {string} [options.className] - Clases CSS del elemento.
    * @param {string} [options.id] - ID del elemento.
    * @param {string} [options.text] - Texto interno del elemento.
    * @param {string} [options.html] - HTML interno del elemento (usa setInnerHTML seguro).
    * @param {Function} [options.onClickEvent] - Función legacy para el evento click.
    * @param {Object.<string, Function>} [options.events] - Eventos a añadir, e.g., { click: fn, mouseover: fn }.
    * @param {Object.<string, string>} [options.atribute] - Atributos HTML a añadir, e.g., { src: 'img.png' }.
    * @param {Object.<string, any>} [options.props] - Propiedades del elemento, e.g., { value: '123' }.
    * @param {Object.<string, string>} [options.styles] - Estilos CSS a aplicar, e.g., { color: 'red', fontSize: '14px' }.
    * @param {Array<string|Node>} [options.children] - Hijos a añadir al elemento, strings o nodos.
    * @returns {HTMLElement} - El elemento HTML creado y configurado.
    */
    function createElement(tag, {
        className = '',
        id = '',
        text = '',
        html = '',
        onClickEvent = null,
        events = {},
        atribute = {},
        props = {},
        styles = {},
        children = []
    } = {}) {
        const el = document.createElement(tag);

        if (className) el.className = className;
        if (id) el.id = id;
        if (text) el.textContent = text;
        if (html) setInnerHTML(el, html);

        // Soporte legacy (función onClickEvent)
        if (onClickEvent && typeof onClickEvent === 'function') {
            el.addEventListener('click', onClickEvent);
        }

        // Soporte para múltiples eventos
        if (events && typeof events === 'object') {
            Object.entries(events).forEach(([event, handler]) => {
                if (typeof handler === 'function') {
                    el.addEventListener(event, handler);
                }
            });
        }

        // Atributos
        if (atribute && typeof atribute === 'object') {
            Object.entries(atribute).forEach(([k, v]) => el.setAttribute(k, v));
        }

        // Propiedades directas
        if (props && typeof props === 'object') {
            Object.entries(props).forEach(([k, v]) => {
                if (k in el) el[k] = v;
            });
        }

        // Estilos CSS
        if (styles && typeof styles === 'object') {
            Object.entries(styles).forEach(([property, value]) => {
                el.style[property] = value;
            });
        }

        // Añadir children
        if (Array.isArray(children)) {
            children.forEach(child => {
                if (typeof child === 'string') {
                    el.appendChild(document.createTextNode(child));
                } else if (child instanceof Node) {
                    el.appendChild(child);
                }
            });
        }

        return el;
    }

    // MARK: 🔧 YouTube Helper API
    /**
    * Espera a que YouTube Helper API esté listo.
    *
    * @param {number} retries - Número de reintentos (opcional, por defecto 0).
    * @returns {Promise} - Promesa que se resuelve cuando YouTube Helper API está listo.
    */
    function waitForHelper(retries = 0) {
        return new Promise((resolve, reject) => {
            const MAX_RETRIES = 10;
            const RETRY_INTERVAL = 1000;

            const helper = window.youtubeHelperApi;

            if (helper) {
                // Si ya está inicializado completamente
                if (helper.player?.api) return resolve(helper);

                // Si existe pero aún no se inicializó
                helper.eventTarget.addEventListener('yt-helper-api-ready', (e) => {
                    resolve(e.detail);
                }, { once: true });
                return;
            }

            // Si no existe todavía, reintenta
            if (retries < MAX_RETRIES) {
                warn('init', `[YTHelper] No disponible, reintentando... (${retries + 1}/${MAX_RETRIES})`);
                setTimeout(() => resolve(waitForHelper(retries + 1)), RETRY_INTERVAL);
            } else {
                reject(new Error("YouTube Helper API no disponible tras varios intentos"));
            }
        });
    }

    // MARK: 🔧 YouTube Thumbnail Loading
    /**
     * Devuelve la URL de la miniatura de un video de YouTube.
     * Utiliza un enfoque progresivo para obtener la miniatura más alta disponible.
     * @param {string} videoId - ID del video de YouTube
     * @returns {Promise<string>} - URL de la miniatura o placeholder
     */
    function loadYouTubeThumbnail(videoId) {
        const base = `https://i.ytimg.com/vi/${videoId}`;
        const levels = ['maxresdefault', 'hqdefault', 'mqdefault']; // fallback progresivo
        const placeholder = 'data:image/svg+xml;base64,CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjcyIiB2aWV3Qm94PSIwIDAgMTI4IDcyIj4KICA8cmVjdCB3aWR0aD0iMTI4IiBoZWlnaHQ9IjcyIiBmaWxsPSIjMjAyMDIwIi8+CiAgPHBvbHlnb24gcG9pbnRzPSI1MCwyMiA1MCw1MCA3OCwzNiIgZmlsbD0iI2ZmZiIvPgo8L3N2Zz4=';

        return new Promise((resolve) => {
            let index = 0;

            function tryNext() {
                if (index >= levels.length) {
                    resolve(placeholder); // no se encontró ninguna válida
                    return;
                }

                const url = `${base}/${levels[index]}.jpg`;
                const img = new Image();
                img.onload = () => {
                    // YouTube placeholder de maxresdefault suele tener tamaño 120x90
                    if (img.naturalWidth <= 120 || img.naturalHeight <= 90) {
                        index++;
                        tryNext();
                    } else {
                        resolve(url); // imagen válida encontrada
                    }
                };
                img.onerror = () => {
                    index++;
                    tryNext(); // intentar siguiente nivel
                };
                img.src = url;
            }

            tryNext();
        });
    }

    // MARK: 🔧 Debounce
    /**
    * Crea una función "debounceada" que retrasa la ejecución de la función original
    * hasta que haya pasado un tiempo determinado sin que se vuelva a invocar.
    *
    * @param {Function} fn - La función que se quiere ejecutar con retraso.
    * @param {number} delay - Tiempo de espera (en milisegundos) antes de ejecutar `fn`.
    * @returns {Function} - Una nueva función que, al llamarse repetidamente,
    *                       solo ejecutará `fn` una vez pasado el tiempo indicado.
    */
    const debounce = (fn, delay) => {
        // Variable para almacenar el identificador del temporizador
        let timer;

        // Retorna una nueva función que "envuelve" a la original
        return (...args) => {
            // Si el temporizador ya estaba activo, se cancela
            clearTimeout(timer);

            // Se crea un nuevo temporizador que ejecutará la función después del delay
            timer = setTimeout(() => fn(...args), delay);
        };
    };

    // MARK: 📤 Import/Export JSON
    // Exportación/Importación JSON nativo del userscript (preserva videoType de shorts)
    const exportDataToFile = () => {
        try {
            const exportData = {};
            const keys = Storage.keys().filter(k =>
                !k.startsWith('userSettings') &&
                !k.startsWith('userFilters') &&
                !k.startsWith('playlist_meta_') &&
                k !== 'translations_cache_v1'
            );

            keys.forEach(k => {
                const data = Storage.get(k);
                if (data) exportData[k] = data;
            });

            const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            const timestamp = new Date().toISOString().split('T')[0];
            a.download = `youtube-playback-plox-backup-${timestamp}.json`;
            document.body.appendChild(a);
            a.click();
            a.remove();
            URL.revokeObjectURL(url);

            const count = Object.keys(exportData).length;
            showFloatingToast(`${SVG_ICONS.upload} ${t('itemsImported', { count })}`);
            log('exportDataToFile', `Exportados ${count} videos en formato JSON nativo`);
        } catch (error) {
            conError('exportDataToFile', 'Error al exportar:', error);
            showFloatingToast(`${SVG_ICONS.error} ${t('exportError')}`);
        }
    };

    const importDataFromFile = () => {
        let inputFile = document.getElementById('ypp-import-file');
        if (!inputFile) {
            inputFile = createElement('input', {
                id: 'ypp-import-file',
                atribute: { type: 'file', accept: '.json' },
                style: { display: 'none' }
            });
            document.body.appendChild(inputFile);
        }

        inputFile.onchange = async (e) => {
            const file = e.target.files[0];
            if (!file) return;

            try {
                const text = await file.text();
                const data = JSON.parse(text);

                if (typeof data !== 'object' || data === null) {
                    showFloatingToast(`${SVG_ICONS.error} ${t('invalidFormat')}`);
                    return;
                }

                let importCount = 0;
                let skipped = 0;

                Object.entries(data).forEach(([key, value]) => {
                    // Evitar importar configuraciones
                    if (key.startsWith('userSettings') || key.startsWith('userFilters')) {
                        skipped++;
                        return;
                    }

                    // Validar que el valor tenga estructura mínima de video
                    if (value && typeof value === 'object' && (value.videoId || value.timestamp !== undefined)) {
                        Storage.set(key, value);
                        importCount++;
                    } else {
                        log('importDataFromFile', `Entrada inválida ignorada: ${key}`);
                        skipped++;
                    }
                });

                updateVideoList();

                if (importCount > 0) {
                    showFloatingToast(`${SVG_ICONS.check} ${t('itemsImported', { count: importCount })} ${skipped > 0 ? ` (${skipped} ${t('omitedVideos')})` : ''}`);
                    log('importDataFromFile', `Importados ${importCount} videos, ${skipped} omitidos`);
                } else {
                    showFloatingToast(`${SVG_ICONS.warning} ${t('noValidVideos')}`);
                }
            } catch (error) {
                conError('importDataFromFile', 'Error al importar:', error);
                showFloatingToast(`${SVG_ICONS.error} ${t('importError')}`);
            } finally {
                inputFile.value = '';
            }
        };

        inputFile.click();
    };

    // MARK: 📤 Import/Export FreeTube
    // Exportación/Importación FreeTube (no preserva videoType de shorts)
    const exportToFreeTube = () => {
        // Usar la función centralizada para exportar en formato FreeTube
        // para asegurar que todos los campos estén correctamente mapeados
        (async () => {
            try {
                const exportData = await exportToFreeTubeFormat();
                // FreeTube imports as JSON Lines / .db where each line is a JSON object.
                // Generar JSON Lines (NDJSON) - cada línea debe ser un objeto JSON completo
                // JSON.stringify serializa sin saltos de línea por defecto, pero nos aseguramos
                const ndjson = exportData
                    .map(obj => {
                        // Asegurar que no haya saltos de línea internos en el JSON serializado
                        const jsonLine = JSON.stringify(obj);
                        // Verificar que sea válido (debugging)
                        if (jsonLine.includes('\n') || jsonLine.includes('\r')) {
                            warn('exportToFreeTube', 'JSON con saltos de línea detectado, limpiando...');
                            // Esto no debería ocurrir con JSON.stringify, pero por seguridad
                            return jsonLine.replace(/\r?\n/g, '\\n');
                        }
                        return jsonLine;
                    })
                    .join('\n');

                const blob = new Blob([ndjson], { type: 'application/json' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                const timestamp = new Date().toISOString().split('T')[0];
                // Usar extensión .db porque FreeTube a veces espera ese sufijo (incluso si es JSONL)
                a.download = `youtube-playback-plox-backup-${timestamp}-freetube-compatible.db`;
                document.body.appendChild(a);
                a.click();
                a.remove();
                URL.revokeObjectURL(url);
                showFloatingToast(`${SVG_ICONS.upload} FreeTube ${t('dataExported')}`);
            } catch (err) {
                conError('exportToFreeTube', 'Error exporting to FreeTube format:', err);
                showFloatingToast(`${SVG_ICONS.error} ${t('exportError')}`);
            }
        })();
    };

    const importFromFreeTube = () => {
        let inputFile = document.getElementById('ypp-import-freetube-file');
        if (!inputFile) {
            inputFile = createElement('input', {
                id: 'ypp-import-freetube-file',
                atribute: { type: 'file', accept: '.json, .db' },
                style: { display: 'none' }
            });
            document.body.appendChild(inputFile);
        }
        inputFile.onchange = async (e) => {
            const file = e.target.files[0];
            const fileName = file?.name || '';
            if (!file) return;

            if (file.size > 10 * 1024 * 1024) { // 10MB limit
                const fileSizeMB = `${(file.size / (1024 * 1024)).toFixed(2)}MB`;
                showFloatingToast(`${SVG_ICONS.error} ${t('fileTooLarge', { size: fileSizeMB })}`);
                return;
            }

            // Si es un archivo .db de FreeTube, puede ser:
            //  - un SQLite binario (real .db)
            //  - un .db renombrado que contiene JSON o JSON Lines (caso observado)
            if (fileName.endsWith('.db')) {
                // Intentar leer como texto primero (JSON o JSON Lines)
                try {
                    showFloatingToast(`${SVG_ICONS.download} ${t('importingFromFreeTube')}`);
                    const text = await file.text();

                    // Intentar parsear como JSON array
                    let data = null;
                    try {
                        data = JSON.parse(text);
                    } catch (e) {
                        // Intentar JSON Lines
                        try {
                            const lines = text.trim().split('\n').filter(l => l.trim());
                            data = lines.map(l => JSON.parse(l));
                        } catch (e2) {
                            data = null;
                        }
                    }

                    if (Array.isArray(data) && data.length > 0) {
                        const result = await importFromFreeTubeFormat(data);
                        updateVideoList();
                        if (result.imported > 0) {
                            showFloatingToast(`${SVG_ICONS.check} ${result.imported} ${t('videosImported')}${result.failed > 0 ? ` (${result.failed} ${t('errors')})` : ''}`);
                        } else {
                            showFloatingToast(`${SVG_ICONS.error} ${t('noVideosImported')}${result.failed > 0 ? ` (${result.failed} ${t('errors')})` : ''}`);
                        }
                        return;
                    }
                    // Si llegamos aquí, el archivo .db no era JSON válido -> intentar parsear como SQLite
                    showFloatingToast(`${SVG_ICONS.download} ${t('importingFromFreeTubeAsSQLite')}`);
                } catch (textErr) {
                    // Si leer como texto falla por cualquier motivo, continuamos intentando parsear como SQLite
                    log('importFromFreeTube', 'No se pudo procesar .db como texto, intentando SQLite', textErr);
                }

                // Intentar parsear como SQLite DB (binario)
                try {
                    const arrayBuffer = await file.arrayBuffer();
                    const data = await parseFreeTubeDB(arrayBuffer);

                    if (!data || data.length === 0) {
                        showFloatingToast(`${SVG_ICONS.warning} ${t('noVideosFoundInFreeTubeDB')}`);
                        return;
                    }

                    const result = await importFromFreeTubeFormat(data);
                    updateVideoList();

                    if (result.imported > 0) {
                        showFloatingToast(`${SVG_ICONS.check} ${result.imported} ${t('videosImportedFromFreeTubeDB')} ${result.failed > 0 ? ` (${result.failed} ${t('errors')})` : ''}`);
                    } else {
                        showFloatingToast(`${SVG_ICONS.error} ${t('noVideosImportedFromFreeTubeDB')} ${result.failed > 0 ? ` (${result.failed} ${t('errors')})` : ''}`);
                    }
                } catch (error) {
                    conError('importFromFreeTube', 'Error procesando archivo .db:', error);
                    showFloatingToast(`${SVG_ICONS.error} ${t('importError')}`);
                }

                return;
            }

            try {
                showFloatingToast(`${SVG_ICONS.download} ${t('processingFile')}`);
                const text = await file.text();

                // Validar que el archivo no esté vacío
                if (!text.trim()) {
                    showFloatingToast(`${SVG_ICONS.warning} ${t('fileEmpty')}`);
                    return;
                }

                let data;

                // Intentar parsear como JSON array estándar primero
                try {
                    data = JSON.parse(text);
                } catch (standardError) {
                    // Si falla, intentar parsear como JSON Lines (formato FreeTube)
                    try {
                        data = [];
                        const lines = text.trim().split('\n').filter(line => line.trim());

                        for (const line of lines) {
                            if (line.trim()) {
                                const obj = JSON.parse(line);
                                data.push(obj);
                            }
                        }

                        log('importFromFreeTube', `Parseado como JSON Lines: ${data.length} objetos encontrados`);
                    } catch (linesError) {
                        throw new SyntaxError('El archivo no tiene un formato JSON válido ni JSON Lines (formato FreeTube)');
                    }
                }

                // Validar que sea un array
                if (!Array.isArray(data)) {
                    showFloatingToast(`${SVG_ICONS.warning} ${t('invalidFormat')}`);
                    return;
                }

                if (data.length === 0) {
                    showFloatingToast(`${SVG_ICONS.warning} ${t('noValidVideos')}`);
                    return;
                }

                const result = await importFromFreeTubeFormat(data);
                updateVideoList();

                if (result.imported > 0) {
                    showFloatingToast(`${SVG_ICONS.check} ${result.imported} ${t('videosImportedFromFreeTubeDB')} ${result.failed > 0 ? ` (${result.failed} ${t('errors')})` : ''}`);
                } else {
                    showFloatingToast(`${SVG_ICONS.error} ${t('noVideosImportedFromFreeTubeDB')} ${result.failed > 0 ? ` (${result.failed} ${t('errors')})` : ''}`);
                }
            } catch (error) {
                conError('importFromFreeTube', 'Error importando:', error);
                if (error instanceof SyntaxError) {
                    showFloatingToast(`${SVG_ICONS.error} ${t('importError')}: ${error.message}`);
                } else {
                    showFloatingToast(`${SVG_ICONS.error} ${t('importError')}`);
                }
            } finally {
                // Limpiar el input para permitir seleccionar el mismo archivo nuevamente
                inputFile.value = '';
            }
        };
        inputFile.click();
    };

    /**
    * Convierte el formato interno de YouTube Playback Plox a formato FreeTube
    * @param {Object} internalData - Datos en formato interno del script
    * @returns {Object} Datos en formato FreeTube
    */
    function toFreeTubeFormat(internalData) {
        // Redondear valores de tiempo para que FreeTube los muestre correctamente
        const timestamp = internalData.timestamp || 0;
        const duration = internalData.duration || 0;

        // Redondear watchProgress a 2 decimales
        const watchProgress = Math.round(timestamp * 100) / 100;
        // Redondear lengthSeconds a entero (FreeTube espera segundos completos)
        const lengthSeconds = Math.round(duration);

        const result = {
            videoId: internalData.videoId,
            title: internalData.title || t('unknown'),
            author: internalData.author || t('unknown'),
            authorId: internalData.authorId || '',
            published: internalData.published || null,
            description: internalData.description || '',
            viewCount: typeof internalData.viewsNumber === 'string'
                ? parseInt(internalData.viewsNumber.replace(/[,\.\s]/g, '')) || 0
                : (internalData.viewsNumber || 0),
            lengthSeconds: lengthSeconds, // Redondeado a entero
            watchProgress: watchProgress, // Redondeado a 2 decimales
            timeWatched: internalData.lastUpdated || internalData.savedAt || Date.now(),
            isLive: internalData.isLive || false,
            type: 'watch', // FreeTube siempre usa 'watch', incluso para shorts
            // Metadatos de playlist (FreeTube los incluye siempre, aunque sean null)
            lastViewedPlaylistId: internalData.lastViewedPlaylistId || null,
            lastViewedPlaylistType: internalData.lastViewedPlaylistType || '',
            lastViewedPlaylistItemId: internalData.lastViewedPlaylistItemId || null
        };

        return result;
    }

    /**
    * Parsea un archivo SQLite de FreeTube para extraer el historial
    * @param {ArrayBuffer} arrayBuffer - Datos del archivo .db
    * @returns {Array} Array de videos en formato FreeTube
    */
    async function parseFreeTubeDB(arrayBuffer) {
        try {
            // Convertir a string para buscar patrones de texto
            const uint8Array = new Uint8Array(arrayBuffer);
            let text = '';

            // Extraer texto legible del archivo binario
            for (let i = 0; i < uint8Array.length; i++) {
                const byte = uint8Array[i];
                if (byte >= 32 && byte <= 126) { // Caracteres imprimibles
                    text += String.fromCharCode(byte);
                } else {
                    text += ' '; // Reemplazar bytes no imprimibles
                }
            }

            // Buscar patrones JSON en el texto
            const jsonObjects = [];
            const jsonPattern = /\{[^}]*"videoId"[^}]*\}/g;

            let match;
            while ((match = jsonPattern.exec(text)) !== null) {
                try {
                    // Limpiar y parsear el objeto JSON
                    let cleanJson = match[0];

                    // Intentar parsear directamente
                    const obj = JSON.parse(cleanJson);
                    if (obj.videoId) {
                        jsonObjects.push(obj);
                    }
                } catch (e) {
                    // Si falla, intentar reparar JSON común
                    try {
                        let cleanJson = match[0]
                            .replace(/,\s*}/g, '}') // Remove trailing commas
                            .replace(/,\s*]/g, ']'); // Remove trailing commas in arrays

                        const obj = JSON.parse(cleanJson);
                        if (obj.videoId) {
                            jsonObjects.push(obj);
                        }
                    } catch (e2) {
                        // Ignorar objetos que no se pueden parsear
                    }
                }
            }

            // Si no se encontraron objetos con el patrón anterior, buscar más ampliamente
            if (jsonObjects.length === 0) {
                // Buscar cualquier objeto que parezca un video de YouTube
                const broaderPattern = /\{[^}]*"videoId"\s*:\s*"[^"]*"[^}]*\}/g;
                let broaderMatch;
                while ((broaderMatch = broaderPattern.exec(text)) !== null) {
                    try {
                        const obj = JSON.parse(broaderMatch[0]);
                        if (obj.videoId && obj.videoId.length > 5) {
                            jsonObjects.push(obj);
                        }
                    } catch (e) {
                        // Ignorar errores
                    }
                }
            }

            log('parseFreeTubeDB', `Encontrados ${jsonObjects.length} videos en la base de datos`);
            return jsonObjects;

        } catch (error) {
            conError('parseFreeTubeDB', 'Error parseando DB:', error);
            return [];
        }
    }

    /**
     * Convierte el formato FreeTube a formato interno
     * @param {Object} freeTubeData - Datos en formato FreeTube
     * @returns {Object} Datos en formato interno del script
     */
    function fromFreeTubeFormat(freeTubeData) {
        // Generar thumbnail URL directamente sin depender del contexto de la página
        const videoId = freeTubeData.videoId;
        let thumbnailUrl;

        if (videoId && typeof videoId === 'string' && videoId.length >= 11) {
            // Usar URL directa de YouTube como fallback confiable
            thumbnailUrl = `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`;
        } else {
            // Fallback adicional si el videoId es inválido
            thumbnailUrl = 'https://i.ytimg.com/vi/default/maxresdefault.jpg';
        }

        // Determinar si el video está completado basado en el progreso
        const watchProgress = freeTubeData.watchProgress || 0;
        const lengthSeconds = freeTubeData.lengthSeconds || 0;
        let isCompleted = false;

        if (lengthSeconds > 0) {
            // Considerar completado si el progreso es >= 95% o si quedan menos de 30 segundos
            const progressPercent = (watchProgress / lengthSeconds) * 100;
            const remainingSeconds = lengthSeconds - watchProgress;
            isCompleted = progressPercent >= CONFIG.defaultSettings.staticFinishPercent || remainingSeconds <= 30;
        }

        return {
            videoId: freeTubeData.videoId,
            title: freeTubeData.title,
            author: freeTubeData.author,
            authorId: freeTubeData.authorId,
            thumb: thumbnailUrl,
            viewsNumber: freeTubeData.viewCount ? freeTubeData.viewCount.toLocaleString() : 'N/D',
            savedAt: freeTubeData.timeWatched,
            duration: freeTubeData.lengthSeconds,
            timestamp: freeTubeData.watchProgress,
            lastUpdated: freeTubeData.timeWatched,
            videoType: freeTubeData.type === 'short' ? 'shorts' : 'watch',
            isCompleted: isCompleted,
            published: freeTubeData.published,
            description: freeTubeData.description,
            isLive: freeTubeData.isLive || false,
            // Preservar metadatos de playlist
            lastViewedPlaylistId: freeTubeData.lastViewedPlaylistId || null,
            lastViewedPlaylistType: freeTubeData.lastViewedPlaylistType || '',
            lastViewedPlaylistItemId: freeTubeData.lastViewedPlaylistItemId || null
        };
    }

    /**
    * Exporta todos los videos guardados en formato FreeTube
    * @returns {Array} Array de videos en formato FreeTube
    */
    async function exportToFreeTubeFormat() {
        const videoKeys = Storage.keys().filter(key =>
            !key.includes('userSettings') &&
            !key.includes('userFilters') &&
            !key.startsWith('playlist_meta_') && // Excluir metadata de playlists
            key !== 'translations_cache_v1'
        );

        const freeTubeData = [];
        let videoCount = 0;
        let shortCount = 0;

        let iter = 0;
        for (const key of videoKeys) {
            const data = Storage.get(key);
            if (!data) continue;

            // Compatibilidad con formato antiguo (playlists anidadas)
            if (data.videos) {
                log('exportToFreeTubeFormat', `Exportando playlist antigua ${key} con ${Object.keys(data.videos).length} videos`);
                Object.entries(data.videos).forEach(([vidKey, videoObj]) => {
                    const internal = Object.assign({}, videoObj, { videoId: videoObj.videoId || vidKey });
                    const formatted = toFreeTubeFormat(internal);
                    freeTubeData.push(formatted);
                    if (formatted.type === 'short') shortCount++;
                    else videoCount++;
                });
            } else {
                // Formato FreeTube: el video ya está en el formato correcto, solo mapear campos si es necesario
                const internal = Object.assign({}, data, { videoId: data.videoId || key });
                const formatted = toFreeTubeFormat(internal);
                freeTubeData.push(formatted);
                if (formatted.type === 'short') {
                    shortCount++;
                    log('exportToFreeTubeFormat', `Short detectado: ${formatted.videoId} | videoType: ${internal.videoType}`);
                } else {
                    videoCount++;
                }
            }
            // Rendición cooperativa para no bloquear el hilo principal
            if ((++iter % 50) === 0) { await new Promise(r => setTimeout(r, 0)); }
        }

        log('exportToFreeTubeFormat', `Exportando ${freeTubeData.length} items: ${videoCount} videos, ${shortCount} shorts`);
        return freeTubeData;
    }

    /**
    * Importa videos desde formato FreeTube
    * @param {Array} freeTubeData - Array de videos en formato FreeTube
    * @returns {Object} Resultado de la importación { imported: number, failed: number }
    */
    async function importFromFreeTubeFormat(freeTubeData) {
        let imported = 0;
        let failed = 0;

        // Validar que los datos sean un array
        if (!Array.isArray(freeTubeData)) {
            conError('importFromFreeTubeFormat', 'Los datos no son un array válido');
            return { imported: 0, failed: 0, total: 0 };
        }

        for (const video of freeTubeData) {
            try {
                // Validar que el video tenga los campos mínimos requeridos
                if (!video || typeof video !== 'object') {
                    conError('importFromFreeTubeFormat', 'Video inválido: no es un objeto');
                    failed++;
                    continue;
                }

                if (!video.videoId) {
                    conError('importFromFreeTubeFormat', 'Video inválido: no tiene videoId');
                    failed++;
                    continue;
                }

                // Validar que el videoId tenga un formato válido
                if (typeof video.videoId !== 'string' || video.videoId.length < 5) {
                    conError('importFromFreeTubeFormat', `VideoId inválido: ${video.videoId}`);
                    failed++;
                    continue;
                }

                const internalFormat = fromFreeTubeFormat(video);

                // Validación adicional del formato interno
                if (!internalFormat || !internalFormat.videoId) {
                    conError('importFromFreeTubeFormat', 'Error al convertir formato interno');
                    failed++;
                    continue;
                }

                // Si ya existe el video en Storage, conservar el mayor viewCount como más reciente
                try {
                    const existing = Storage.get(video.videoId);
                    if (existing && (existing.viewsNumber != null || internalFormat.viewsNumber != null)) {
                        const parseViews = (val) => {
                            if (typeof val === 'number') return val;
                            if (typeof val === 'string') {
                                const cleaned = val.replace(/[^0-9]/g, '');
                                if (!cleaned) return null;
                                const n = parseInt(cleaned, 10);
                                return Number.isNaN(n) ? null : n;
                            }
                            return null;
                        };

                        const existingViews = parseViews(existing.viewsNumber);
                        const importedViews = parseViews(internalFormat.viewsNumber);
                        const winner = (existingViews || 0) > (importedViews || 0) ? existingViews : importedViews;

                        if (winner != null && winner > 0) {
                            try {
                                internalFormat.viewsNumber = winner.toLocaleString();
                            } catch (_) {
                                internalFormat.viewsNumber = String(winner);
                            }
                        }
                    }
                } catch (_) { }

                Storage.set(video.videoId, internalFormat);
                imported++;
                log('importFromFreeTubeFormat', `✅ Importado: ${video.videoId} - ${video.title || 'Sin título'}`);
            } catch (error) {
                conError('importFromFreeTubeFormat', `Error importando ${video?.videoId || 'desconocido'}:`, error);
                failed++;
            }
        }

        log('importFromFreeTubeFormat', `Importación completada: ${imported} exitosos, ${failed} fallidos, total ${freeTubeData.length}`);
        return { imported, failed, total: freeTubeData.length };
    }

    // ------------------------------------------
    // MARK: 📺 Helpers
    // ------------------------------------------
    // MARK: 📺 Obtiene datos guardados de un video
    /**
    * Obtiene datos guardados de un video, intentando todas las combinaciones posibles.
    * Soporta tanto videos individuales como en playlist.
    *
    * @param {string} videoId - ID del video
    * @param {string|null} playlistId - ID de la playlist (opcional)
    * @returns {Object|null} - Datos guardados o null si no se encuentra
    */
    function getSavedVideoData(videoId, playlistId = null) {
        log('getSavedVideoData', `Buscando datos guardados para ID: ${videoId} | Playlist ID: ${playlistId}`);
        if (!videoId) return null;

        // En el formato FreeTube, todos los videos se guardan con video_id como clave
        // independientemente de si fueron vistos en una playlist o no
        const videoData = Storage.get(videoId);

        if (videoData) {
            // Si encontramos el video y se especificó un playlistId, verificar compatibilidad
            if (playlistId && videoData.lastViewedPlaylistId === playlistId) {
                log('getSavedVideoData', `✅ Video encontrado con playlist coincidente`);
                return videoData;
            } else if (!playlistId) {
                log('getSavedVideoData', `✅ Video encontrado (sin filtro de playlist)`);
                return videoData;
            } else {
                log('getSavedVideoData', `⚠ Video encontrado pero en contexto diferente (guardado: ${videoData.lastViewedPlaylistId}, buscado: ${playlistId}) - usando fallback base`);
                return videoData;
            }
        }

        // Compatibilidad con formato antiguo (playlists anidadas)
        if (playlistId) {
            const oldPlaylistData = Storage.get(playlistId);
            if (oldPlaylistData?.videos?.[videoId]) {
                log('getSavedVideoData', `✅ Video encontrado en formato antiguo (playlist anidada)`);
                return oldPlaylistData.videos[videoId];
            }
        }

        // Búsqueda flexible: por si alguna vez se guardó con prefijos raros
        const keys = Storage.keys?.() || [];
        const altKey = keys.find(k => k.endsWith(videoId) || k.includes(videoId));
        if (altKey && altKey !== `playlist_meta_${videoId}`) {
            log('getSavedVideoData', `✅ Video encontrado con clave alternativa: ${altKey}`);
            return Storage.get(altKey);
        }

        log('getSavedVideoData', `✗ No se encontraron datos para el video`);
        return null;
    }

    // MARK: 📺 Normaliza las claves de almacenamiento de YouTube
    /**
    * @function normalizeYouTubeStorageKeys
    * @description
    * Normaliza las claves almacenadas en `Storage` que estén relacionadas con YouTube.
    * Revisa todas las claves, intenta extraer o corregir los IDs de video,
    * y migra los datos a claves normalizadas, evitando duplicados.
    *
    * @returns {void} No devuelve ningún valor. Realiza operaciones directamente sobre `Storage`.
    *
    * @example
    * // Ejemplo de uso:
    * normalizeYouTubeStorageKeys();
    *
    * // Este proceso:
    * //  - Lee todas las claves del almacenamiento
    * //  - Detecta las que contienen identificadores de videos no normalizados
    * //  - Crea nuevas claves con el formato correcto y migra los datos
    */
    async function normalizeYouTubeStorageKeys() {
        const NORMALIZE_VERSION = 1; // Incrementar si cambia la lógica de normalización
        const NORMALIZE_KEY = 'ypp_normalize_storage_keys_version';

        // Evitar re-ejecución si ya se aplicó esta versión
        try {
            const last = await GM_getValue(NORMALIZE_KEY, 0);
            if (last >= NORMALIZE_VERSION) {
                log('normalizeYouTubeStorageKeys', `✅ Normalización ya aplicada (versión ${last})`);
                return;
            }
        } catch (e) {
            // Si falla GM_getValue, continuar sin bloquear la normalización
            warn('normalizeYouTubeStorageKeys', 'No se pudo leer versión de normalización, continuando...', e);
        }
        // Verifica si el objeto Storage tiene disponible el método keys()
        if (typeof Storage?.keys !== 'function') {
            conError('normalizeYouTubeStorageKeys', 'Storage.keys() no disponible.');
            return;
        }

        // Obtiene todas las claves almacenadas
        const allKeys = Storage.keys();
        // Contador para llevar registro de cuántas claves se han migrado
        let changes = 0;

        // Recorre todas las claves encontradas en el almacenamiento
        let iter = 0;
        for (const key of allKeys) {
            // Intenta extraer o normalizar el ID del video desde la clave
            const newKey = extractOrNormalizeVideoId(key)?.id;

            // Registra la operación en el log
            log('normalizeYouTubeStorageKeys', `Clave original: ${key} | Clave nueva: ${newKey}`);

            // Si se obtuvo un ID válido y diferente de la clave original
            if (newKey && newKey !== key) {
                // Obtiene los datos asociados a la clave antigua
                const data = Storage.get(key);

                // Solo migra si la nueva clave aún no existe (para evitar sobrescribir datos)
                if (!Storage.get(newKey)) {
                    Storage.set(newKey, data);  // Guarda los datos bajo la nueva clave
                    Storage.del(key);           // Elimina la clave antigua
                    log('normalizeYouTubeStorageKeys', `✅ Migrado: "${key}" -> "${newKey}"`);
                    changes++;
                } else {
                    // Si la nueva clave ya existe, registra un aviso de duplicado
                    log('normalizeYouTubeStorageKeys', `⚠️ Duplicado detectado: "${key}" ya existe como "${newKey}"`);
                }
            }
        }

        // Muestra un resumen al final del proceso
        log('normalizeYouTubeStorageKeys', `🔁 Normalización completa. ${changes} claves migradas.`);

        // Marcar como aplicada esta versión de normalización para evitar re-ejecuciones
        try {
            await GM_setValue(NORMALIZE_KEY, NORMALIZE_VERSION);
            log('normalizeYouTubeStorageKeys', `📝 Versión de normalización guardada: ${NORMALIZE_VERSION}`);
        } catch (e) {
            warn('normalizeYouTubeStorageKeys', 'No se pudo guardar versión de normalización', e);
        }
    }


    // MARK: 📺 Get YouTube Page Type
    /**
    * Determina el tipo de página actual en YouTube según la URL.
    *
    * Analiza la ruta (`window.location.pathname`) y devuelve un identificador
    * descriptivo del tipo de página (por ejemplo: "home", "shorts", "watch", "channel", etc.).
    *
    * @returns {string} - Tipo de página detectada. Puede ser uno de:
    *   - `'home'` — Página principal de YouTube
    *   - `'shorts'` — Página de YouTube Shorts
    *   - `'watch'` — Página de reproducción de un video
    *   - `'embed'` — Página de video embebido
    *   - `'playlist'` — Página de una lista de reproducción
    *   - `'search'` — Página de resultados de búsqueda
    *   - `'music'` — Página de YouTube Music (canal oficial)
    *   - `'gaming'` — Página de YouTube Gaming
    *   - `'news'` — Página de noticias
    *   - `'sports'` — Página de deportes
    *   - `'learning'` — Página de aprendizaje
    *   - `'you'` — Página "Tu espacio" (feed personal)
    *   - `'history'` — Página del historial de reproducciones
    *   - `'subscriptions'` — Página de suscripciones
    *   - `'live'` — Transmisión en vivo o enlace directo a live
    *   - `'channel'` — Página de canal (personalizada, por ID o nombre)
    *   - `'unknown'` — Si no coincide con ninguno de los anteriores
    */
    function getYouTubePageType() {
        // Obtener la ruta actual de la URL (sin dominio)
        const path = window.location.pathname;

        // Comprobaciones directas de ruta
        if (path === '/') return 'home';
        if (path.startsWith('/shorts')) return 'shorts';
        if (path.startsWith('/watch')) return 'watch';
        if (path.startsWith('/embed')) return 'embed';
        if (path.startsWith('/playlist')) return 'playlist';
        if (path.startsWith('/results')) return 'search';

        // Comprobaciones por canal o categoría especial (IDs fijos de YouTube)
        if (path.endsWith('/UC-9-kyTW8ZkZNDHQJ6FgpwQ')) return 'music';     // Canal oficial de YouTube Music
        if (path.startsWith('/gaming')) return 'gaming';                    // Sección de Gaming
        if (path.endsWith('/UCYfdidRxbB8Qhf0Nx7ioOYw')) return 'news';      // Canal de Noticias
        if (path.endsWith('/UCEgdi0XIXXZ-qJOFPf4JSKw')) return 'sports';    // Canal de Deportes
        if (path.endsWith('/UCtFRv9O2AHqOZjjynzrv-xg')) return 'learning';  // Canal de Aprendizaje

        // Feeds personales del usuario
        if (path.endsWith('/feed/you')) return 'you';
        if (path.endsWith('/feed/history')) return 'history';
        if (path.endsWith('/feed/subscriptions')) return 'subscriptions';

        // Detección de videos en vivo o enlaces con "/live"
        // Ejemplo: https://www.youtube.com/@NASA/live
        // Para raros casos, ya que Youtube usa "/watch" igual para directos.
        if (path.includes('/live')) return 'live';

        // Posibles rutas de canales de usuario
        const channelPaths = [
            '/@',        // Canal personalizado (nuevo formato: /@nombre)
            '/channel',  // Canal por ID (formato: /channel/UCxxxx)
            '/c',        // Canal personalizado (antiguo: /c/nombre)
            '/user',     // Canal de usuario clásico (muy antiguo)
            '/UC'        // Canal directo por ID (raro)
        ];

        // Si la ruta coincide con alguno de los prefijos de canal, se considera canal
        if (channelPaths.some(prefix => path.startsWith(prefix))) return 'channel';

        // Si no se reconoce la ruta, devolvemos "unknown"
        return 'unknown';
    }

    // ------------------------------------------
    // MARK: 📺 Get Video Element
    // ------------------------------------------
    // Helper para encontrar el <video> actual
    async function getActiveVideoElement() {
        // Prioridad: YouTube Helper API
        if (YTHelper?.player?.videoElement) {
            try {
                const el = YTHelper.player.videoElement;
                const pt = getYouTubePageType();
                const container = el.closest('#movie_player, #shorts-player');
                const isShortsContainer = container?.id === 'shorts-player';
                // En home/watch/embed evita usar el shorts-player; en Shorts sí debe usarse
                if ((pt === 'shorts' && isShortsContainer) || (pt !== 'shorts' && !isShortsContainer)) {
                    // En Shorts con guardado de shorts desactivado, evitar retorno inmediato del helper de Shorts.
                    // Además, si hay miniplayer visible/reproduciendo, preferir DOM para seleccionarlo.
                    let blockShortsHelper = false;
                    try {
                        if (pt === 'shorts' && isShortsContainer) {
                            const mpEl = document.querySelector('#movie_player video.html5-main-video, #miniplayer video.html5-main-video, ytd-miniplayer video');
                            const mpPlaying = (() => { try { return !!mpEl && !mpEl.paused; } catch (_) { return false; } })();
                            const shortsDisabled = (() => { try { return cachedSettings?.saveShorts === false; } catch (_) { return false; } })();
                            blockShortsHelper = (!!mpEl && (shortsDisabled || mpPlaying));
                        }
                    } catch (_) { }
                    if (blockShortsHelper) {
                        log('getActiveVideoElement', '⏭ Evitando helper de Shorts (saveShorts=false o miniplayer activo); buscando en DOM para priorizar miniplayer');
                    } else {
                        // En páginas home-like, no devolvemos aún: buscamos DOM primero para privilegiar previews reales
                        if (pt !== 'home' && pt !== 'search' && pt !== 'channel' && pt !== 'unknown') {
                            log('getActiveVideoElement', '✅ Usando YTHelper.player.videoElement');
                            return el;
                        } else {
                            log('getActiveVideoElement', 'ℹ️ En home-like no se retorna helper de inmediato; se buscará en DOM');
                        }
                    }
                }
                log('getActiveVideoElement', '⚠️ Ignorando YTHelper.videoElement por desajuste de contexto');
            } catch (_) { }
        }

        log('getActiveVideoElement', '⚠️ YouTube Helper API no disponible, usando búsqueda DOM');

        const selectors = [
            // === SHORTS (activo primero) ===
            'ytd-reel-video-renderer[is-active] #short-video-container ytd-player div.html5-video-container video',
            'ytd-reel-video-renderer[is-active] video.reel-video-player-element',
            'ytd-reel-player-overlay-renderer #shorts-player video',
            'ytd-reel-player-overlay-renderer video.html5-main-video',
            'ytd-reel-video-renderer #short-video-container ytd-player div.html5-video-container video',
            'ytd-reel-video-renderer video.reel-video-player-element',
            'ytd-shorts video.html5-main-video',
            '#shorts-player video',

            // === VIDEOS REGULARES (en página de reproducción) ===
            '#movie_player video.html5-main-video',
            '.html5-video-player video.html5-main-video',

            // === MINIPLAYER FLOTANTE (Picture-in-Picture / Miniplayer) ===
            // Este es el clave para cuando el usuario vuelve al homepage o cambia a shorts desde video regular reproduciendose
            '#movie_player',
            '.ytp-miniplayer-ui video.html5-main-video',
            '#miniplayer video.html5-main-video',
            'ytd-miniplayer video',
            '.html5-video-container video', // más genérico, pero útil

            // === FALLBACKS GENÉRICOS (evitando previews) ===
            'video:not([data-no-fullscreen])',
            'video'
        ];

        const candidates = selectors.flatMap(selector => {
            const elements = Array.from(document.querySelectorAll(selector));
            info('getActiveVideoElement', `Selector "${selector}": ${elements.length} elementos`);
            return elements;
        });
        log('getActiveVideoElement', `Total de candidatos: ${candidates.length}`);

        // Excluir candidatos dentro de contextos de anuncios (in-feed o overlays del player)
        const isAdContextNode = (node) => {
            try {
                if (!node || !node.closest) return false;
                const adNode = node.closest('.ytp-ad-module, .ytp-ad-player-overlay, .video-ads, ytd-in-feed-ad-layout-renderer, ytd-ad-slot-renderer, ytd-display-ad-renderer, ytd-promoted-sparkles-web-renderer, #player-ads');
                if (adNode) return true;
                // Si el contenedor del movie_player marca ad-showing, considerarlo anuncio
                try {
                    const po = (node.closest('#movie_player, .html5-video-player'));
                    if (po && (po.classList?.contains('ad-showing') || po.classList?.contains('ad-interrupting'))) return true;
                } catch (_) { }
                return false;
            } catch (_) { return false; }
        };

        const visibleVideos = candidates.filter(video => {
            if (!video) return false;
            if (((video.tagName || '').toUpperCase()) !== 'VIDEO') return false;
            if (isAdContextNode(video)) { info('getActiveVideoElement', 'Descartado por contexto de anuncio'); return false; }

            const rect = video.getBoundingClientRect();
            // Considerar contenedor de inline preview para la visibilidad
            let inlineContainerEl = null;
            try { inlineContainerEl = video.closest('#inline-preview-player, .ytp-inline-preview-ui, ytd-thumbnail-overlay-inline-playback-renderer'); } catch (_) { inlineContainerEl = null; }
            const contRect = (() => { try { return inlineContainerEl?.getBoundingClientRect?.(); } catch (_) { return null; } })();
            const visByVideoRect = rect.width > 50 && rect.height > 50 && rect.bottom > 0 && rect.top < (window.innerHeight || 99999);
            const visByContRect = !!(contRect && contRect.width > 50 && contRect.height > 50 && contRect.bottom > 0 && contRect.top < (window.innerHeight || 99999));
            // Chequeo de estilo (display/visibility/opacity) del contenedor si existe, de lo contrario del video
            let visByStyle = true;
            try {
                const styleNode = inlineContainerEl || video;
                const cs = window.getComputedStyle(styleNode);
                const op = parseFloat(cs.opacity || '1');
                visByStyle = cs.display !== 'none' && cs.visibility !== 'hidden' && op > 0.05;
            } catch (_) { visByStyle = true; }
            const isVisible = (visByVideoRect || visByContRect) && visByStyle;
            const hasSource = !!(video.currentSrc || video.src || video.querySelector('source'));
            const isPlaying = (() => { try { return !video.paused; } catch (_) { return false; } })();
            const hasDuration = Number.isFinite(video.duration) && video.duration > 0;
            const ready = (video.readyState || 0) >= 2; // HAVE_CURRENT_DATA
            const progressed = (Number.isFinite(video.currentTime) && video.currentTime > 0);
            const playable = ready || progressed || hasDuration || isPlaying;

            log('getActiveVideoElement', `Video: ${video.currentSrc?.substring(0, 50)}... | Visible: ${isVisible} | Tamaño: ${rect.width}x${rect.height} | Reproduciendo: ${isPlaying}`);

            let acceptByInline = false;
            try {
                const inlineEl = inlineContainerEl || video.closest('#inline-preview-player, .ytp-inline-preview-ui, ytd-thumbnail-overlay-inline-playback-renderer');
                if (inlineEl) {
                    const ptNow = getYouTubePageType();
                    const isHomeLike = (ptNow === 'home' || ptNow === 'search' || ptNow === 'channel' || ptNow === 'unknown');
                    const allowInline = !!(cachedSettings && cachedSettings.saveInlinePreviews === true);
                    // Permitir selección si:
                    // - El usuario habilitó guardar previews inline, o
                    // - Ya está reproduciendo o avanzó (para detección no intrusiva incluso si guardar está deshabilitado)
                    acceptByInline = isHomeLike && (allowInline || isPlaying || progressed);
                    // Sticky: recordar último inline playing para tolerar reflows rápidos
                    try {
                        if (acceptByInline && (isPlaying || progressed) && isVisible) {
                            window.__ypp_lastInlinePreview__ = { el: video, ts: Date.now() };
                        }
                    } catch (_) { }
                }
            } catch (_) { acceptByInline = false; }

            return isVisible && ((hasSource && playable) || acceptByInline);
        });

        log('getActiveVideoElement', `Videos visibles y reproducibles: ${visibleVideos.length}`);

        if (visibleVideos.length === 0) {
            // Fallback: usar último inline preview detectado recientemente (< 1500ms)
            try {
                const last = window.__ypp_lastInlinePreview__;
                if (last && last.el && (Date.now() - (last.ts || 0) <= 1500) && document.contains(last.el) && !isAdContextNode(last.el)) {
                    log('getActiveVideoElement', '↩️ Usando inline preview reciente (sticky) tras reflow');
                    return last.el;
                }
            } catch (_) { }
            return null;
        }

        // Preferir el video que esté reproduciéndose actualmente
        const playingVideos = visibleVideos.filter(v => {
            try { return !v.paused; } catch (_) { return false; }
        });

        const pickByPriority = (list) => {
            const mp = list.find(v => v.closest('.ytp-miniplayer-ui') || v.closest('#miniplayer') || v.closest('ytd-miniplayer'));
            if (mp) return mp;
            const main = list.find(v => v.closest('#movie_player') || v.closest('.html5-video-player'));
            if (main) return main;
            const shorts = list.find(v => v.closest('ytd-reel-video-renderer') || v.closest('ytd-shorts') || v.closest('#shorts-player'));
            if (shorts) return shorts;
            return list[0];
        };

        // Si estamos en Shorts y el guardado de Shorts está deshabilitado,
        // priorizar el miniplayer incluso cuando ambos están reproduciéndose.
        let preferMiniplayer = false;
        try { preferMiniplayer = (getYouTubePageType() === 'shorts') && (cachedSettings?.saveShorts === false); } catch (_) { }
        // Si estamos en Shorts y el guardado de Shorts está habilitado, preferir Shorts sobre miniplayer
        let preferShorts = false;
        try { preferShorts = (getYouTubePageType() === 'shorts') && (cachedSettings?.saveShorts !== false); } catch (_) { }

        if (playingVideos.length > 0) {
            if (preferMiniplayer) {
                const mpPlaying = playingVideos.find(v => v.closest('.ytp-miniplayer-ui') || v.closest('#miniplayer') || v.closest('ytd-miniplayer'));
                if (mpPlaying) {
                    log('getActiveVideoElement', 'Seleccionado miniplayer (preferido) por reproducción activa en Shorts con guardado desactivado');
                    return mpPlaying;
                }
            }
            if (preferShorts) {
                const spPlaying = playingVideos.find(v => v.closest('ytd-reel-video-renderer') || v.closest('ytd-shorts') || v.closest('#shorts-player'));
                if (spPlaying) {
                    log('getActiveVideoElement', 'Seleccionado Shorts (preferido) por reproducción activa en Shorts con guardado habilitado');
                    return spPlaying;
                }
            }
            const chosen = pickByPriority(playingVideos);
            log('getActiveVideoElement', 'Seleccionado por reproducción activa');
            return chosen;
        }

        // Si no hay videos reproduciéndose, pero preferimos miniplayer, escogerlo si es visible
        if (preferMiniplayer) {
            const mpVisible = visibleVideos.find(v => v.closest('.ytp-miniplayer-ui') || v.closest('#miniplayer') || v.closest('ytd-miniplayer'));
            if (mpVisible) {
                log('getActiveVideoElement', 'Seleccionado miniplayer (preferido) por visibilidad en Shorts con guardado desactivado');
                return mpVisible;
            }
        }
        // Si no hay videos reproduciéndose, pero preferimos Shorts, escogerlo si es visible
        if (preferShorts) {
            const spVisible = visibleVideos.find(v => v.closest('ytd-reel-video-renderer') || v.closest('ytd-shorts') || v.closest('#shorts-player'));
            if (spVisible) {
                log('getActiveVideoElement', 'Seleccionado Shorts (preferido) por visibilidad en Shorts con guardado habilitado');
                return spVisible;
            }
        }
        const chosen = pickByPriority(visibleVideos);
        log('getActiveVideoElement', 'Seleccionado por prioridad de visibilidad');
        return chosen;
    }

    // ------------------------------------------
    // MARK: 📺 Get Container
    // ------------------------------------------

    // Helper para encontrar el <container> actual
    function getActiveContainer() {
        // Prioridad: YouTube Helper API
        if (YTHelper?.player?.playerObject) {
            try {
                const co = YTHelper.player.playerObject;
                const pt = currentPageType || getYouTubePageType();
                const id = co?.id || co?.getAttribute?.('id');
                const isShorts = id === 'shorts-player';
                if (isShorts && pt !== 'shorts') {
                    log('getActiveContainer', '⚠️ Ignorando playerObject de Shorts fuera de Shorts');
                } else {
                    log('getActiveContainer', '✅ Usando YTHelper.player.playerObject');
                    return co;
                }
            } catch (_) { }
        }

        // Helpers locales para validación estricta
        const isElementReasonablyVisible = (el) => {
            try {
                if (!el || !el.getBoundingClientRect) return false;
                const r = el.getBoundingClientRect();
                const cs = getComputedStyle(el);
                return r.width > 100 && r.height > 50 && r.bottom > 0 && r.top < (window.innerHeight || 0) + 1 && cs.display !== 'none' && cs.visibility !== 'hidden';
            } catch (_) { return false; }
        };

        const isAdContext = (node) => {
            try {
                if (!node || !node.closest) return false;
                const adNode = node.closest('.ytp-ad-module, .ytp-ad-player-overlay, .video-ads, ytd-in-feed-ad-layout-renderer, ytd-ad-slot-renderer, ytd-display-ad-renderer, ytd-promoted-sparkles-web-renderer, #player-ads');
                if (adNode) return true;
                try {
                    const po = YTHelper?.player?.playerObject;
                    if (po && (po.classList?.contains('ad-showing') || po.classList?.contains('ad-interrupting'))) return true;
                } catch (_) { }
                return false;
            } catch (_) { return false; }
        };

        const isInlinePreviewContext = (node) => {
            try {
                const el = node && node.closest && node.closest('#inline-preview-player, .ytp-inline-preview-ui, ytd-thumbnail-overlay-inline-playback-renderer');
                return !!(el && !isAdContext(node));
            } catch (_) { return false; }
        };

        const isLikelyShortsContainer = (el) => {
            try {
                if (!el) return false;
                const id = el.id || el.getAttribute?.('id') || '';
                if (id === 'shorts-player') return true;
                if (el.matches?.('ytd-reel-video-renderer, ytd-shorts, #shorts-container')) return true;
                const cl = (el.className || '').toString();
                return /reel|shorts/i.test(cl);
            } catch (_) { return false; }
        };

        const isValidContainerForPage = (candidate, pageType) => {
            if (!candidate || typeof candidate.querySelector !== 'function') return false;
            if (isAdContext(candidate)) return false;
            const allowInline = cachedSettings?.saveInlinePreviews === true;
            if (isInlinePreviewContext(candidate) && !allowInline) return false;
            const isShorts = pageType === 'shorts';
            const looksLikeShorts = isLikelyShortsContainer(candidate);
            // Permitir Shorts fuera de /shorts solo si es inline preview y el usuario lo habilitó
            if (!isShorts && looksLikeShorts) {
                if (!allowInline) return false;
                const inlineOk = isInlinePreviewContext(candidate) || isInlinePreviewContext(candidate.querySelector?.('video'));
                if (!inlineOk) return false;
            }
            const videoDesc = candidate.querySelector('video.html5-main-video') || candidate.querySelector('video');
            const hasKnownPlayerMark = candidate.id === 'movie_player' || candidate.classList?.contains('html5-video-player') || candidate.matches?.('#miniplayer, ytd-miniplayer, .ytp-miniplayer-ui');
            if (videoDesc) {
                if (isAdContext(videoDesc)) return false;
                if (isInlinePreviewContext(videoDesc) && !allowInline) return false;
                if (!isElementReasonablyVisible(videoDesc)) return false;
                return true;
            }
            return !!hasKnownPlayerMark;
        };

        const pickFirstValidContainerLocal = (candidates, pageType) => {
            for (const c of candidates) {
                if (c && isValidContainerForPage(c, pageType)) {
                    try {
                        // Marcar miniplayer de forma estable para poder seguirlo aunque cambie el video interno
                        if (c.matches?.('#miniplayer, ytd-miniplayer, .ytp-miniplayer-ui')) {
                            c.dataset.yppMiniplayer = '1';
                        }
                    } catch (_) { }
                    return c;
                }
            }
            return null;
        };

        // Fallback a selectores DOM
        const pt2 = currentPageType || getYouTubePageType();
        let candidates = [];
        if (pt2 === 'shorts') {
            candidates = [
                document.querySelector('#shorts-player'),
                document.querySelector('ytd-reel-video-renderer'),
                document.querySelector('#reel-video-renderer'),
                document.querySelector('ytd-shorts'),
                document.querySelector('#shorts-container'),
                document.querySelector('#movie_player'),
                document.querySelector('.html5-video-player:not(#inline-preview-player)')
            ];
        } else {
            candidates = [
                document.querySelector('#player-container-inner #player-container #movie_player'),
                document.querySelector('#movie_player'),
                document.querySelector('.html5-video-player:not(#inline-preview-player)'),
                document.querySelector('#miniplayer'),
                document.querySelector('ytd-miniplayer'),
                document.querySelector('.ytp-miniplayer-ui')
            ];
            // Si el usuario habilitó guardar previews inline, considerar su contenedor explícito
            try { if (cachedSettings?.saveInlinePreviews === true) { candidates.unshift(document.querySelector('#inline-preview-player')); } } catch (_) { }
            const shortsEls = [
                document.querySelector('ytd-reel-video-renderer'),
                document.querySelector('#reel-video-renderer'),
                document.querySelector('ytd-shorts'),
                document.querySelector('#shorts-container')
            ];
            candidates = candidates.concat(shortsEls);
        }

        // Intentar también con el <video> como último recurso, mapeando a su contenedor
        const looseVideo = document.querySelector('video:not([data-no-fullscreen])');
        if (looseVideo) {
            const mapped = looseVideo.closest('#movie_player, .html5-video-player, .ytp-miniplayer-ui, #miniplayer, ytd-miniplayer, #inline-preview-player, .ytp-inline-preview-ui') || looseVideo.parentElement;
            if (mapped) candidates.push(mapped);
        }

        const picked = pickFirstValidContainerLocal(candidates.filter(Boolean), pt2);
        if (picked) {
            log('getActiveContainer', `✅ Container encontrado via DOM: ${picked.tagName}${picked.id ? '#' + picked.id : ''}`);
            return picked;
        }

        warn('getActiveContainer', '⚠️ No se encontró container activo');
        return null;
    }

    // ------------------------------------------
    // MARK: 🎬 Get Video Tittle
    // ------------------------------------------

    /**
     * Valida que un título sea el actual comparando con document.title
     * @param {string} candidateTitle - Título candidato a validar
     * @returns {boolean} - true si el título es válido
     */
    function validateTitle(candidateTitle) {
        if (!candidateTitle) return false;

        // Durante navegación, ser más permisivo ya que document.title puede estar desactualizado
        if (isNavigating) {
            // Validaciones básicas durante navegación
            const isReasonableLength = candidateTitle.length > 3 && candidateTitle.length < 500;
            const hasValidChars = /[a-zA-Z0-9]/.test(candidateTitle);
            const isNotGeneric = !['YouTube', 'Loading...', 'Untitled', ''].includes(candidateTitle.trim());

            const isValid = isReasonableLength && hasValidChars && isNotGeneric;

            if (isValid) {
                log('validateTitle', `✅ Título validado durante navegación: "${candidateTitle}"`);
            } else {
                log('validateTitle', `❌ Título rechazado durante navegación: "${candidateTitle}" (longitud: ${candidateTitle.length}, hasValidChars: ${hasValidChars}, isNotGeneric: ${isNotGeneric})`);
            }

            return isValid;
        }

        // Validación normal cuando no estamos navegando
        const docTitle = document.title.replace(/ - YouTube$/, '').trim();
        if (!docTitle) {
            // Si no hay document.title, usar validación básica como fallback
            const isReasonableLength = candidateTitle.length > 3 && candidateTitle.length < 500;
            const hasValidChars = /[a-zA-Z0-9]/.test(candidateTitle);
            const isNotGeneric = !['YouTube', 'Loading...', 'Untitled', ''].includes(candidateTitle.trim());
            return isReasonableLength && hasValidChars && isNotGeneric;
        }

        try {
            const pt = getYouTubePageType();
            const isNonWatch = pt !== 'watch';
            const docIsGeneric = ['YouTube', 'Loading...', 'Untitled'].includes(docTitle);
            if (isNonWatch || docIsGeneric) {
                const isReasonableLength = candidateTitle.length > 3 && candidateTitle.length < 500;
                const hasValidChars = /[a-zA-Z0-9]/.test(candidateTitle);
                const isNotGeneric = !['YouTube', 'Loading...', 'Untitled', ''].includes(candidateTitle.trim());
                const isValidBasic = isReasonableLength && hasValidChars && isNotGeneric;
                if (isValidBasic) {
                    log('validateTitle', `✅ Título validado (modo no-watch): "${candidateTitle}"`);
                } else {
                    log('validateTitle', `❌ Título rechazado (modo no-watch): "${candidateTitle}"`);
                }
                return isValidBasic;
            }
        } catch (_) { }

        // Normalizar títulos para comparación (eliminar espacios extras, convertir a minúsculas)
        const normalize = (str) => str.toLowerCase().replace(/\s+/g, ' ').trim();
        const normalizedCandidate = normalize(candidateTitle);
        const normalizedDocTitle = normalize(docTitle);

        // Coincidencia exacta o si uno contiene al otro (para casos de títulos truncados)
        const isValid = normalizedDocTitle === normalizedCandidate ||
            normalizedDocTitle.includes(normalizedCandidate) ||
            normalizedCandidate.includes(normalizedDocTitle);

        if (isValid) {
            log('validateTitle', `✅ Título validado: "${candidateTitle}" coincide con document.title: "${docTitle}"`);
        } else {
            log('validateTitle', `❌ Título NO validado: "${candidateTitle}" no coincide con document.title: "${docTitle}"`);
        }

        return isValid;
    }

    function getVideoTittle(player) {
        // Prioridad: YouTube Helper API (evitar en Shorts por posible miniplayer)
        try {
            const pt0 = getYouTubePageType();
            if (pt0 !== 'shorts' && YTHelper?.video?.title) {
                log('getVideoTittle', `Título encontrado en YTHelper: ${YTHelper.video.title}`);
                if (validateTitle(YTHelper.video.title)) {
                    return YTHelper.video.title;
                }
                warn('getVideoTittle', '⚠️ Título de YTHelper no validado, puede ser de video previo');
            }
        } catch (_) {
            if (YTHelper?.video?.title && validateTitle(YTHelper.video.title)) return YTHelper.video.title;
        }

        try {
            const pt = getYouTubePageType();
            if (pt !== 'shorts') {
                const mp = document.querySelector('#movie_player');
                const mpTitle = mp?.getVideoData?.()?.title;
                if (mpTitle) {
                    log('getVideoTittle', `Título encontrado en movie_player.getVideoData(): ${mpTitle}`);
                    if (validateTitle(mpTitle)) {
                        return mpTitle;
                    }
                    warn('getVideoTittle', '⚠️ Título de movie_player no validado');
                }
            }
        } catch (_) { }

        // Título en Shorts: preferir encabezado propio de Shorts. Evitar #anchored-panel (pertenece a watch/miniplayer)
        try {
            const ptS = getYouTubePageType();
            if (ptS === 'shorts') {
                // Usar helper dedicado que prioriza ytd-reel-video-renderer[is-active] #metapanel #title
                const shortsTitleDom = extractShortsTitle();
                if (shortsTitleDom && typeof shortsTitleDom === 'string' && shortsTitleDom.trim().length > 0) {
                    log('getVideoTittle', `Título encontrado en Shorts DOM (sin validar contra document.title): ${shortsTitleDom}`);
                    return shortsTitleDom.trim();
                }
                // En Shorts, si no se encontró un título válido en su propio DOM, no usar fallbacks de watch/miniplayer
                return t('unknown');
            } else {
                const anchorTitleShorts =
                    document.querySelector('yt-formatted-string.ytd-video-description-header-renderer')?.textContent?.trim() ||
                    document.querySelector('#anchored-panel yt-formatted-string.style-scope.ytd-video-description-header-renderer')?.textContent?.trim();
                if (anchorTitleShorts) {
                    log('getVideoTittle', `Título encontrado en #anchored-panel: ${anchorTitleShorts}`);
                    if (validateTitle(anchorTitleShorts)) {
                        return anchorTitleShorts;
                    }
                    warn('getVideoTittle', '⚠️ Título de Shorts anchor no validado');
                }
            }
        } catch (_) { }

        // Selectores meta tag y window.ytInitialPlayerResponse pueden tener/regresar datos de videos previos!

        // Si el título está en el meta tag, usarlo
        const metaTitleTag = document.querySelector('meta[name="title"]');
        const metaTitle = metaTitleTag ? metaTitleTag.getAttribute('content') : null;
        if (metaTitle) {
            log('getVideoTittle', `Título encontrado en meta tag: ${metaTitle}`);
            if (validateTitle(metaTitle)) {
                return metaTitle;
            }
            warn('getVideoTittle', '⚠️ Título de meta tag no validado, puede ser de video previo');
        }

        // Fallback si no está en meta[name=title]
        const metaOgTitleTag = document.querySelector('meta[property="og:title"]');
        const metaOgTitle = metaOgTitleTag ? metaOgTitleTag.getAttribute('content') : null;
        if (metaOgTitle) {
            log('getVideoTittle', `Título encontrado en meta og:title: ${metaOgTitle}`);
            if (validateTitle(metaOgTitle)) {
                return metaOgTitle;
            }
            warn('getVideoTittle', '⚠️ Título de og:title no validado, puede ser de video previo');
        }

        // Fallback si no está en meta[property="og:title"]
        const metaTwitterTitleTag = document.querySelector('meta[name="twitter:title"]');
        const metaTwitterTitle = metaTwitterTitleTag ? metaTwitterTitleTag.getAttribute('content') : null;
        if (metaTwitterTitle) {
            log('getVideoTittle', `Título encontrado en meta twitter:title: ${metaTwitterTitle}`);
            if (validateTitle(metaTwitterTitle)) {
                return metaTwitterTitle;
            }
            warn('getVideoTittle', '⚠️ Título de twitter:title no validado, puede ser de video previo');
        }

        // Fallback a JSON incrustado (CUIDADO: puede tener datos de videos previos)
        const title = window.ytInitialPlayerResponse?.videoDetails?.title;
        if (title) {
            log('getVideoTittle', `Título encontrado en JSON incrustado: ${title}`);
            if (validateTitle(title)) {
                return title;
            }
            warn('getVideoTittle', '⚠️ Título de JSON incrustado no validado, puede ser de video previo');
        }

        /*
        player.getVideoData() = {
                "video_id": "iy4mXZN1Zzk",
                "author": "robbiewilliamsvevo",
                "title": "Robbie Williams - Feel",
                "isPlayable": true,
                "errorCode": null,
                "video_quality": "medium",
                "video_quality_features": [],
                "list": "RDiy4mXZN1Zzk",
                "backgroundable": false,
                "eventId": "Q-AHae30KbiZ4dUPnbDByQk",
                "cpn": "m6WhMHqg6WQd25ET",
                "isLive": false,
                "isWindowedLive": false,
                "isManifestless": false,
                "allowLiveDvr": false,
                "isListed": true,
                "isMultiChannelAudio": false,
                "hasProgressBarBoundaries": false,
                "isPremiere": false,
                "itct": "CAAQu2kiEwitooPuxtSQAxW4TLgEHR1YMJk=",
                "playerResponseCpn": "",
                "progressBarStartPositionUtcTimeMillis": null,
                "progressBarEndPositionUtcTimeMillis": null,
                "paidContentOverlayDurationMs": 0
            }
        */

        const videoData = player?.getVideoData?.();
        if (videoData != null) {
            log('getVideoTittle', `Título encontrado en player.getVideoData(): ${videoData?.title}`);
            if (validateTitle(videoData.title)) {
                return videoData.title;
            }
            warn('getVideoTittle', '⚠️ Título de player.getVideoData() no validado, puede ser de video previo');
        }

        // Fallback al DOM
        const DOMSelectors = [
            'ytd-watch-metadata h1 yt-formatted-string',
            'h1.ytd-watch-metadata yt-formatted-string',
            '#title h1 yt-formatted-string',
            'h1 yt-formatted-string',
            '#title h1',
            'h1.title'
            //'h2 span.yt-core-attributed-string' // entrega el nombre pero si existe traducción activa devuelve esa version, No original.
        ];

        for (const sel of DOMSelectors) {
            const el = document.querySelector(sel);
            const t = el?.textContent.trim();
            if (t) {
                log('getVideoTittle', `Título encontrado en DOM: ${t}`);
                if (validateTitle(t)) {
                    return t;
                }
                warn('getVideoTittle', '⚠️ Título de DOM no validado, puede ser de video previo');
            }
        }

        // Fallback al <title> del documento, limpiando "- YouTube"
        const docTitle = document.title.replace(/ - YouTube$/, '');
        log('getVideoTittle', `Título encontrado en <title>: ${docTitle}`);
        if (validateTitle(docTitle)) {
            return docTitle;
        }
        warn('getVideoTittle', '⚠️ Título de <title> no validado, puede ser de video previo');

        // Si llegamos aquí, no pudimos validar ningún título
        // Retornar el primer título encontrado con advertencia
        warn('getVideoTittle', '⚠️ No se pudo validar ningún título, usando primer título disponible');
        return YTHelper?.video?.title ||
            anchorTitleShorts ||
            metaTitle ||
            metaOgTitle ||
            metaTwitterTitle ||
            title ||
            videoData?.title ||
            docTitle ||
            t('unknown');
    }

    // Función para obtener el autor del video
    function getVideoAuthor(player) {
        const pt = getYouTubePageType();
        // 1) Preferir author del player (movie_player) en watch/embed/home
        try {
            if (pt !== 'shorts') {
                // Forzar movie_player si existe
                const mp = document.querySelector('#movie_player');
                const mpAuthor = mp?.getVideoData?.()?.author;
                if (mpAuthor && typeof mpAuthor === 'string' && mpAuthor.trim().length > 0) {
                    log('getVideoAuthor', `Autor encontrado en movie_player: ${mpAuthor}`);
                    return mpAuthor.trim();
                }
            }
        } catch (_) { }

        // 1b) videoDetails.author (suele estar disponible en watch y es confiable)
        try {
            if (pt !== 'shorts') {
                const vdAuthor = window.ytInitialPlayerResponse?.videoDetails?.author;
                if (vdAuthor && typeof vdAuthor === 'string' && vdAuthor.trim().length > 0) {
                    log('getVideoAuthor', `Autor encontrado en videoDetails: ${vdAuthor}`);
                    return vdAuthor.trim();
                }
            }
        } catch (_) { }

        // 2) Player pasado por parámetro (evitar placeholders y evitar Shorts)
        try {
            const rawPlayerAuthor = player?.getVideoData?.()?.author;
            const playerAuthor = typeof rawPlayerAuthor === 'string' ? rawPlayerAuthor.trim() : '';
            const isPlaceholderAuthor = !playerAuthor || playerAuthor.toLowerCase() === 'unknown';
            if (pt !== 'shorts' && playerAuthor && !isPlaceholderAuthor) {
                log('getVideoAuthor', `Autor encontrado en player: ${playerAuthor}`);
                return playerAuthor;
            }
        } catch (_) { }

        // 3) En Shorts, usar solo el encabezado del reproductor de Shorts; evitar YTHelper (puede ser del miniplayer)
        if (pt === 'shorts') {
            try {
                const shortsAuthorText =
                    // Preferir metapanel del Short ACTIVO
                    document.querySelector('ytd-reel-video-renderer[is-active] #metapanel #channel-name a')?.textContent?.trim() ||
                    // Overlay del reproductor del Short activo
                    document.querySelector('ytd-reel-player-overlay-renderer #metapanel #channel-name a')?.textContent?.trim() ||
                    // Otras ubicaciones válidas
                    document.querySelector('#shorts-player ytd-reel-player-header-renderer #channel-name a')?.textContent?.trim() ||
                    document.querySelector('ytd-reel-player-header-renderer #channel-name a')?.textContent?.trim() ||
                    document.querySelector('ytd-reel-video-renderer[is-active] #metapanel a[href^="/@"]')?.textContent?.trim() ||
                    document.querySelector('#shorts-player a[href^="/@"]')?.textContent?.trim();
                if (shortsAuthorText && shortsAuthorText.length > 0) {
                    log('getVideoAuthor', `Autor encontrado en Shorts DOM: ${shortsAuthorText}`);
                    return shortsAuthorText;
                }
            } catch (_) { }
            // Si no se encuentra, retornar desconocido para evitar contaminación desde miniplayer
            return t('unknown');
        }

        // 4) DOM selectors (fallback)
        const domAuthor =
            document.querySelector('link[itemprop="name"]')?.getAttribute('content') ||
            document.querySelector('yt-button-shape#subscribe-button-shape button.yt-spec-button-shape-next')?.getAttribute('aria-label')?.replace(/^Subscribe to /, '').replace(/\.$/, '').trim() ||
            document.querySelector('ytd-subscription-notification-toggle-button-renderer-next button.yt-spec-button-shape-next')?.getAttribute('aria-label')?.match(/for (.+)$/)?.[1]?.trim();

        if (domAuthor) {
            log('getVideoAuthor', `Autor encontrado en DOM: ${domAuthor}`);
            return domAuthor;
        }

        // 5) Último intento: YTHelper fuera de Shorts si no hay nada más
        if (YTHelper?.video?.channel) {
            log('getVideoAuthor', `Autor fallback YTHelper: ${YTHelper.video.channel}`);
            return ('' + YTHelper.video.channel).trim();
        }

        log('getVideoAuthor', 'No se pudo obtener autor, usando fallback');
        return t('unknown');
    }

    // ------------------------------------------
    // MARK: 📺 Get Thumbnail
    // ------------------------------------------

    function getVideoThumbnail(vid) {
        if (!vid) return null;
        try {
            const ptThumb = getYouTubePageType();
            if (ptThumb === 'home' || ptThumb === 'search' || ptThumb === 'channel' || ptThumb === 'unknown') {
                return `https://i.ytimg.com/vi/${vid}/maxresdefault.jpg`;
            }
        } catch (_) { }

        let thumb = YTHelper?.video?.thumbnails?.[0]?.url;
        if (thumb && thumbnailHasVideoId(thumb, vid)) return cleanThumbnailUrl(thumb);

        // Desde DOM
        /*
        <link rel="image_src" href="https://i.ytimg.com/vi/${vid}/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&amp;rs=AOn4CLBug6b83Q3GnLn_dzMjPVf_3phVNA">
        <meta property="og:image" content="https://i.ytimg.com/vi/${vid}/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&amp;rs=AOn4CLBug6b83Q3GnLn_dzMjPVf_3phVNA">
        <meta name="twitter:image" content="https://i.ytimg.com/vi/${vid}/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&amp;rs=AOn4CLBug6b83Q3GnLn_dzMjPVf_3phVNA">
        <link itemprop="thumbnailUrl" href="https://i.ytimg.com/vi/${vid}/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&amp;rs=AOn4CLBug6b83Q3GnLn_dzMjPVf_3phVNA">
        <link itemprop="url" href="https://i.ytimg.com/vi/${vid}/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=&amp;rs=AOn4CLBug6b83Q3GnLn_dzMjPVf_3phVNA">
        */
        const selectors = [
            'link[rel="image_src"]',
            'meta[property="og:image"]',
            'meta[name="twitter:image"]',
            'link[itemprop="thumbnailUrl"]',
            'link[itemprop="url"]'
        ];

        for (const sel of selectors) {
            const el = document.querySelector(sel);
            const url = el?.content || el?.href;
            if (url && thumbnailHasVideoId(url, vid)) return cleanThumbnailUrl(url);
        }

        // Desde el div de YouTube player
        /*
        <div class="ytp-cued-thumbnail-overlay-image" style="background-image: url(&quot;https://i.ytimg.com/vi_webp/${vid}/maxresdefault.webp&quot;);"></div>
        */
        const thumbDiv = document.querySelector('.ytp-cued-thumbnail-overlay-image');
        if (thumbDiv) {
            const style = thumbDiv.style.backgroundImage;
            const match = style.match(/url\((['"]?)(.*?)\1\)/);
            if (match && thumbnailHasVideoId(match[2], vid)) return match[2];
        }

        // Desde JSON incrustado
        /*
        "microformat":{"playerMicroformatRenderer":{"thumbnail":{"thumbnails":[{"url":"https://i.ytimg.com/vi/${vid}/oar2.jpg?sqp=-oaymwEdCJUDENAFSFWQAgHyq4qpAwwIARUAAIhCcAHAAQY=\u0026rs=AOn4CLBug6b83Q3GnLn_dzMjPVf_3phVNA","width":405,"height":720}],
        */
        try {

            const url = window.ytInitialPlayerResponse?.microformat?.playerMicroformatRenderer?.thumbnail?.thumbnails?.[0]?.url;

            if (url != null && vid && thumbnailHasVideoId(url, vid)) {
                return cleanThumbnailUrl(url);
            }
        } catch (err) {
            warn('Error al obtener thumbnail desde JSON:', err);
        }

        // Fallback final
        return `https://i.ytimg.com/vi/${vid}/maxresdefault.jpg`;
    }

    // Limpia parámetros innecesarios de la URL
    function cleanThumbnailUrl(url) {
        const match = url.match(/^(https:\/\/i\.ytimg\.com\/vi(?:_webp)?\/[A-Za-z0-9_-]+\/[^?]+)/);
        return match ? match[1] : url;
    }

    function thumbnailHasVideoId(url, vid) {
        if (!url || !vid) return false;
        try {
            const m = url.match(/https:\/\/i\.ytimg\.com\/vi(?:_webp)?\/([A-Za-z0-9_-]+)\//);
            if (m && m[1]) return m[1] === vid;
            return url.includes(vid);
        } catch (e) {
            return false;
        }
    }

    /**
     * Parsea etiquetas de vistas abreviadas (K/M/B, mil/millones) a número y lo formatea como string localizado.
     * @param {string} label - Texto o aria-label que contiene la cantidad de vistas
     * @returns {string|null} String con separadores de miles o null si no se puede determinar
     */
    function formatViewsFromLabel(label) {
        if (!label || typeof label !== 'string') return null;
        const lower = label.toLowerCase();
        // Detectar multiplicadores comunes
        let mult = 1;
        if (/\b(k|mil)\b/.test(lower)) mult = 1e3;
        else if (/\b(m|mill|millón|millones)\b/.test(lower)) mult = 1e6;
        else if (/\b(b)\b/.test(lower)) mult = 1e9;

        // Extraer número (con posible decimal)
        const numMatch = lower.match(/(\d+[\.,]?\d*)/);
        if (!numMatch) return null;
        let numStr = numMatch[1].replace(/\s/g, '');
        // Normalizar decimal: si hay coma y no hay punto, tratar coma como punto
        if (numStr.includes(',') && !numStr.includes('.')) {
            numStr = numStr.replace(',', '.');
        } else {
            // Eliminar separadores de miles comunes
            numStr = numStr.replace(/,/g, '');
        }
        const base = parseFloat(numStr);
        if (isNaN(base)) return null;
        const value = Math.round(base * mult);
        try { return Number(value).toLocaleString(); } catch (_) { return String(value); }
    }

    /**
     * Obtiene las vistas desde la UI de Shorts.
     * @returns {string|null} Vistas formateadas o null si no se encuentran
     */
    function extractShortsViews() {
        const candidates = [
            // Metadatos del Short ACTIVO en el feed (evita contaminación)
            'ytd-reel-video-renderer[is-active] #metapanel [aria-label*="views" i]',
            'ytd-reel-video-renderer[is-active] #metapanel [aria-label*="visualizaciones" i]',
            // Overlay del reproductor de Shorts (UI visible del activo)
            'ytd-reel-player-overlay-renderer #metapanel [aria-label*="views" i]',
            'ytd-reel-player-overlay-renderer #metapanel [aria-label*="visualizaciones" i]',
            // Otras ubicaciones válidas para el Short activo
            '#shorts-player [aria-label*="views" i]',
            '#shorts-player [aria-label*="view" i]',
            '#shorts-player [aria-label*="visualizaciones" i]',
            '#shorts-player [aria-label*="reproducciones" i]',
            'ytd-reel-player-header-renderer [aria-label*="views" i]',
            'ytd-reel-player-header-renderer [aria-label*="visualizaciones" i]',
            '#metapanel [aria-label*="views" i]',
            '#metapanel [aria-label*="visualizaciones" i]'
        ];
        for (const sel of candidates) {
            const el = document.querySelector(sel);
            const label = el?.getAttribute?.('aria-label') || el?.textContent || '';
            const fv = formatViewsFromLabel(label);
            if (fv) return fv;
        }
        return null;
    }

    /**
     * Obtiene el título desde la UI de Shorts.
     * @returns {string|null} Título del Short o null si no se encuentra
     */
    function extractShortsTitle() {
        const sels = [
            // Preferir el metapanel del Short ACTIVO
            'ytd-reel-video-renderer[is-active] #metapanel #title',
            // Overlay del reproductor (corresponde al activo)
            'ytd-reel-player-overlay-renderer #metapanel #title',
            'ytd-reel-player-header-renderer #title',
            'ytd-reel-player-header-renderer h2',
            'ytd-reel-player-header-renderer h2 span.yt-core-attributed-string',
            'ytd-reel-video-renderer[is-active] #metapanel h2 span.yt-core-attributed-string',
            '#shorts-player #title',
            '#metapanel #title'
        ];
        for (const s of sels) {
            const t = document.querySelector(s)?.textContent?.trim();
            if (t && t.length > 1) return t;
        }
        return null;
    }

    /**
     * Extrae el channelId del Short ACTIVO desde el metapanel/overlay.
     * En caso de no encontrarlo, retornar null (fallbacks ocurren en el caller).
     * @returns {string|null}
     */
    function extractShortsChannelId() {
        try {
            const candidates = [
                'ytd-reel-video-renderer[is-active] #metapanel a[href*="/channel/"]',
                'ytd-reel-player-overlay-renderer #metapanel a[href*="/channel/"]',
                '#shorts-player #metapanel a[href*="/channel/"]'
            ];
            for (const sel of candidates) {
                const href = document.querySelector(sel)?.href || '';
                if (href.includes('/channel/')) {
                    const id = href.split('/channel/')[1]?.split(/[/?#]/)[0];
                    if (id && id.length > 0) return id;
                }
            }
        } catch (_) { }
        return null;
    }

    // ------------------------------------------
    // MARK: 📺 Get Video Info
    // ------------------------------------------

    // Cache por video
    const viewCountCache = new Map();
    var watchMetaCache = new Map();
    var WATCH_META_TTL = 600000;
    function fetchWatchMeta(videoId) {
        return new Promise(function (resolve) {
            try {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: 'https://www.youtube.com/watch?v=' + encodeURIComponent(videoId),
                    onload: function (response) {
                        var text = response.responseText || '';
                        var data = {};
                        try {
                            var m = text.match(/ytInitialPlayerResponse\s*=\s*(\{.*?\});/s);
                            if (m && m[1]) {
                                var json = JSON.parse(m[1]);
                                var vd = json && json.videoDetails || null;
                                var mf = json && json.microformat && json.microformat.playerMicroformatRenderer || null;
                                if (vd && vd.title != null) data.title = vd.title;
                                if (vd && vd.author != null) data.author = vd.author;
                                if (vd && vd.channelId != null) data.authorId = vd.channelId;
                                if (vd && vd.viewCount != null) {
                                    try { data.viewsNumber = Number(vd.viewCount).toLocaleString(); } catch (_) { data.viewsNumber = '' + vd.viewCount; }
                                }
                                if (mf && mf.description && mf.description.simpleText != null) data.description = mf.description.simpleText.trim();
                            }
                        } catch (_) { }
                        resolve(data);
                    },
                    onerror: function () { resolve({}); },
                    ontimeout: function () { resolve({}); }
                });
            } catch (_) { resolve({}); }
        });
    }
    function getWatchMetaCached(videoId) {
        try {
            var c = watchMetaCache.get(videoId);
            if (c && (Date.now() - c.time) < WATCH_META_TTL) return Promise.resolve(c.data);
        } catch (_) { }
        return fetchWatchMeta(videoId).then(function (d) { try { watchMetaCache.set(videoId, { data: d, time: Date.now() }); } catch (_) { } return d; });
    }

    async function getVideoInfo(player, videoId) {
        const now = Date.now();

        // Título y autor
        let title = getVideoTittle(player);
        let author = getVideoAuthor(player);

        // Thumbnail
        let thumb = getVideoThumbnail(videoId)
        log('getVideoInfo', ' 📺 Thumbnail= ', thumb)

        // Datos adicionales para compatibilidad con FreeTube
        let published =
            YTHelper?.video?.publishDate?.getTime() ||
            (YTHelper?.video?.rawPublishDate ? new Date(YTHelper.video.rawPublishDate).getTime() : null);


        let description =
            YTHelper?.video?.rawDescription?.trim() ??
            document.querySelector('#inline-expander div#snippet.ytd-text-inline-expander yt-attributed-string')?.textContent.trim() ??
            '';

        let isLive = YTHelper?.video?.isCurrentlyLive || false;

        // Views (evitar contaminación en Shorts cuando no es miniplayer)
        let viewsNumber;
        try {
            const ptV = getYouTubePageType();
            let isMiniOnShortsV = false;
            if (ptV === 'shorts') {
                try {
                    const mp = document.querySelector('#movie_player');
                    const mpVid = mp?.getVideoData?.()?.video_id;
                    if (mpVid && mpVid === videoId) isMiniOnShortsV = true;
                } catch (_) { }
            }
            if (ptV === 'shorts' && !isMiniOnShortsV) {
                viewsNumber = null;
            } else if (YTHelper?.video?.viewCount) {
                viewsNumber = Number(YTHelper.video.viewCount).toLocaleString();
            } else if (window.ytInitialPlayerResponse?.videoDetails?.viewCount != null) {
                viewsNumber = Number(window.ytInitialPlayerResponse.videoDetails.viewCount).toLocaleString();
            } else {
                viewsNumber =
                    document.querySelector('.view-count')?.textContent?.match(/[\d.,\s]+/)?.[0].trim() ||
                    document.querySelector('view-count-factoid-renderer .ytwFactoidRendererFactoid[role="text"]')?.getAttribute('aria-label')?.match(/[\d.,\s]+/)?.[0].trim() ||
                    document.querySelector('ytd-watch-info-text div#tooltip.tp-yt-paper-tooltip')?.textContent?.match(/[\d.,\s]+/)?.[0].trim() ||
                    document.querySelector('yt-formatted-string.view-count')?.textContent?.match(/[\d.,\s]+/)?.[0].trim() ||
                    t('notAvailable');
            }
        } catch (_) {
            viewsNumber = t('notAvailable');
        }

        let videoEl = null;
        try {
            const ptX = getYouTubePageType();
            if (ptX === 'shorts') {
                videoEl =
                    document.querySelector('ytd-reel-video-renderer[is-active] #short-video-container ytd-player div.html5-video-container video') ||
                    document.querySelector('ytd-reel-video-renderer[is-active] video.reel-video-player-element') ||
                    document.querySelector('ytd-reel-player-overlay-renderer #shorts-player video') ||
                    document.querySelector('#shorts-player video') ||
                    document.querySelector('ytd-shorts video.html5-main-video');
            }
        } catch (_) { }
        if (!videoEl) {
            videoEl = await getActiveVideoElement();
        }
        let duration = normalizeSeconds(getVideoDuration(player, videoEl));
        log('getVideoInfo', ' 🕕 Duración tras normalizeSeconds = ', duration);

        const pt = getYouTubePageType();
        let authorId;
        let isMiniOnShorts = false;
        try {
            if (pt === 'shorts') {
                const mpChk = document.querySelector('#movie_player');
                const mpVid = mpChk?.getVideoData?.()?.video_id;
                if (mpVid && mpVid === videoId) isMiniOnShorts = true;
            }
        } catch (_) { }
        if (pt === 'shorts' && !isMiniOnShorts) {
            // Shorts normales: primero metapanel/overlay del Short ACTIVO
            authorId = extractShortsChannelId() ||
                document.querySelector('#shorts-player a[href*="/channel/"]')?.href?.split('/channel/')[1]?.split(/[/?#]/)[0] ||
                document.querySelector('ytd-reel-player-header-renderer a[href*="/channel/"]')?.href?.split('/channel/')[1]?.split(/[/?#]/)[0] ||
                document.querySelector('ytd-reel-video-renderer a[href*="/channel/"]')?.href?.split('/channel/')[1]?.split(/[/?#]/)[0] ||
                document.querySelector('link[rel="canonical"]')?.href?.split('/channel/')[1]?.split(/[/?#]/)[0] ||
                document.querySelector('meta[property="og:url"]')?.content?.split('/channel/')[1]?.split(/[/?#]/)[0] ||
                document.querySelector('link[itemprop="url"]')?.href?.split('/channel/')[1]?.split(/[/?#]/)[0] ||
                null;

            // Fallback seguro: usar videoDetails.channelId si pertenece al mismo Short
            if (!authorId) {
                try {
                    const vd = window.ytInitialPlayerResponse?.videoDetails;
                    if (vd && vd.videoId === videoId && typeof vd.channelId === 'string' && vd.channelId) {
                        authorId = vd.channelId;
                    }
                } catch (_) { }
            }

            // Fallback extra: intentar usar YTHelper.video.channelId sólo si pertenece al mismo video
            if (!authorId) {
                try {
                    const helperVideo = YTHelper?.video;
                    const helperVid =
                        helperVideo && (helperVideo.id || helperVideo.videoId || helperVideo.video_id);
                    const helperChannelId = helperVideo && helperVideo.channelId;
                    if (
                        helperChannelId &&
                        typeof helperChannelId === 'string' &&
                        helperChannelId.trim().length > 0 &&
                        helperVid &&
                        helperVid === videoId
                    ) {
                        authorId = helperChannelId.trim();
                    }
                } catch (_) { }
            }

            if (!authorId) authorId = t('unknown');

            // En Shorts, intentar forzar título y vistas desde su propia UI (evitar contaminación del miniplayer)
            try {
                const sTitle = extractShortsTitle();
                if (sTitle && typeof sTitle === 'string' && sTitle.trim().length > 1) title = sTitle.trim();
            } catch (_) { }
            try {
                if (!viewsNumber || viewsNumber === t('notAvailable')) {
                    const sViews = extractShortsViews();
                    if (sViews) viewsNumber = sViews;
                }
            } catch (_) { }
            // Fallback seguro: usar meta de la página watch del mismo ID SOLO para vistas (no título/autor)
            try {
                if (!viewsNumber || viewsNumber === t('notAvailable')) {
                    const metaForShort = await getWatchMetaCached(videoId);
                    if (metaForShort && metaForShort.viewsNumber) viewsNumber = metaForShort.viewsNumber;
                }
            } catch (_) { }
        } else if (pt === 'shorts' && isMiniOnShorts) {
            // Shorts + miniplayer: NO usar window.ytInitialPlayerResponse (pertenece al Short)
            // 1) Intentar enlaces dentro de #movie_player
            authorId =
                document.querySelector('#movie_player a.ytp-ce-channel-title.ytp-ce-link[href*="/channel/"]')?.href?.split('/channel/')[1]?.split(/[/?#]/)[0] ||
                document.querySelector('#movie_player a[href*="/channel/"]')?.href?.split('/channel/')[1]?.split(/[/?#]/)[0] ||
                '';
            // 2) Siempre fusionar con metadata de la página watch del miniplayer para asegurar author/authorId correctos
            try {
                const meta = await getWatchMetaCached(videoId);
                if (meta && meta.authorId) authorId = meta.authorId;
                if (meta && meta.author) author = meta.author;
                if (meta && meta.viewsNumber) viewsNumber = meta.viewsNumber;
                if (meta && meta.title) title = meta.title;
                if (meta && meta.description) description = meta.description;
            } catch (_) { }
            if (!authorId) authorId = t('unknown');
        } else {
            // Páginas normales (watch, embed, home, etc.)
            authorId =
                window.ytInitialPlayerResponse?.videoDetails?.channelId ||
                document.querySelector('#upload-info a.yt-simple-endpoint')?.href?.split('/channel/')[1] ||
                document.querySelector('a.ytp-ce-channel-title.ytp-ce-link')?.href?.split('/channel/')[1] ||
                document.querySelector('#items yt-button-shape a')?.href?.split('/channel/')[1]?.split('/')[0] ||
                document.querySelector('#infocard-channel-button yt-button-shape a')?.href?.split('/channel/')[1]?.split('/')[0] ||
                t('unknown');
        }

        try {
            const pt2 = getYouTubePageType();
            if (pt2 !== 'watch') {
                let isMiniContext = false;
                try {
                    const mpIdNow = document.querySelector('#movie_player')?.getVideoData?.()?.video_id;
                    if (pt2 === 'shorts' && mpIdNow && mpIdNow === videoId) isMiniContext = true;
                } catch (_) { }
                const meta = await getWatchMetaCached(videoId);
                const isGeneric = (s) => !s || ['YouTube', 'Loading...', 'Untitled'].includes(('' + s).trim());
                if (meta) {
                    if (isMiniContext) {
                        if (meta.title) title = meta.title;
                        if (meta.author) author = meta.author;
                        if (meta.authorId) authorId = meta.authorId;
                        if (meta.viewsNumber) viewsNumber = meta.viewsNumber;
                        if (meta.description) description = meta.description;
                    } else {
                        // En Shorts (no-miniplayer), no adoptar título/autor/authorId/descripcion desde meta para evitar contaminación
                        if (pt2 !== 'shorts') {
                            if (isGeneric(title) && meta.title) title = meta.title;
                            if ((!author || author === t('unknown')) && meta.author) author = meta.author;
                            if ((!authorId || authorId === t('unknown')) && meta.authorId) authorId = meta.authorId;
                            if (!viewsNumber && meta.viewsNumber) viewsNumber = meta.viewsNumber;
                            if (!description && meta.description) description = meta.description;
                        } else {
                            // En Shorts, solo permitir rellenar vistas si aún faltan
                            if (!viewsNumber && meta.viewsNumber) viewsNumber = meta.viewsNumber;
                        }
                    }
                }
            }
        } catch (_) { }

        try {
            const ptFinal = getYouTubePageType();
            if (ptFinal === 'shorts' && !isMiniOnShorts) {
                if (!title || title === t('unknown')) {
                    try {
                        const sTitle2 = extractShortsTitle();
                        if (sTitle2 && typeof sTitle2 === 'string' && sTitle2.trim().length > 1) {
                            title = sTitle2.trim();
                        }
                    } catch (_) { }
                }
                if (!author || author === t('unknown')) {
                    try {
                        const shortsAuthor2 = getVideoAuthor(null);
                        if (shortsAuthor2 && shortsAuthor2 !== t('unknown')) {
                            author = shortsAuthor2;
                        }
                    } catch (_) { }
                }
            }
        } catch (_) { }

        // Caché de views
        const cached = viewCountCache.get(videoId);
        if (!cached || (now - cached.time) > 5000) {
            viewCountCache.set(videoId, { views: viewsNumber, time: now });
        } else {
            viewsNumber = cached.views;
        }

        // Valor por defecto legible si no se pudo determinar
        if (!viewsNumber) viewsNumber = t('notAvailable');

        log('getVideoInfo', 'Info:', { title, author, viewsNumber, duration, videoId, authorId, published })
        return {
            title,
            author,
            thumb,
            viewsNumber,
            savedAt: now,
            duration,
            authorId,
            videoId,
            published,
            description,
            isLive
        };
    }

    // ------------------------------------------
    // MARK: 📺 get Video Duration
    // ------------------------------------------

    function getVideoDuration(player, videoEl) {
        const nowTs = Date.now();
        const canLogDur = nowTs - lastDurationLog > 2000;
        if (canLogDur) lastDurationLog = nowTs;
        let duration;

        // Player directo
        // 275.421
        duration = player?.getDuration?.();
        if (canLogDur) log('getVideoDuration', 'duración player.getDuration', duration)
        if (duration && duration > 0 && !isNaN(duration)) return duration;

        // movie_player global
        // 275.421
        // Evitar este fallback si estamos en Shorts o si el elemento pertenece a shorts-player
        let ptNow = null; let contId = null;
        try {
            ptNow = getYouTubePageType();
            contId = videoEl?.closest?.('#movie_player, #shorts-player')?.id || null;
            if (ptNow !== 'shorts' && contId !== 'shorts-player') {
                duration = document.querySelector('#movie_player')?.getDuration?.();
                if (canLogDur) log('getVideoDuration', 'duración movie_player.getDuration', duration)
                if (duration && duration > 0 && !isNaN(duration)) return duration;
            } else if (canLogDur) {
                log('getVideoDuration', '⛔ Omitiendo movie_player.getDuration por contexto Shorts');
            }
        } catch (_) { }

        // PlayerObject
        // 275.421
        duration = player?.playerObject?.getDuration?.();
        if (canLogDur) log('getVideoDuration', 'duración playerObject.getDuration', duration)
        if (duration && duration > 0 && !isNaN(duration)) return duration;

        // Elemento video
        // 275.421
        duration = videoEl?.duration;
        if (canLogDur) log('getVideoDuration', 'duración videoEl.duration', duration)
        if (duration && duration > 0 && !isNaN(duration)) return duration;

        // YouTube Helper API (evitar en Shorts para no contaminar con miniplayer)
        if (ptNow !== 'shorts' && contId !== 'shorts-player') {
            // YouTube Helper API API Proxy
            // 275.421
            duration = YTHelper.apiProxy.getDuration?.()
            if (canLogDur) log('getVideoDuration', 'duración YTHelper.apiProxy.getDuration', duration)
            if (duration && duration > 0 && !isNaN(duration)) return duration;

            // YouTube Helper API (no da decimales)
            // 275 no da decimales!
            duration = YTHelper?.video?.lengthSeconds;
            if (canLogDur) log('getVideoDuration', 'duración YTHelper.video.lengthSeconds', duration)
            if (duration && duration > 0 && !isNaN(duration)) return duration;
        } else if (canLogDur) {
            log('getVideoDuration', '⛔ Omitiendo YTHelper.getDuration por contexto Shorts');
        }

        // ytp-time-duration (no usar en Shorts y no da decimales)
        // "4:35" -> parseTimeToSeconds -> 275 no da decimales!
        if (ptNow !== 'shorts' && contId !== 'shorts-player') {
            duration = document.querySelector('.ytp-time-duration')?.textContent;
            if (canLogDur) log('getVideoDuration', 'duración document.querySelector(.ytp-time-duration).textContent', duration)
            if (duration) {
                const parsedDuration = parseTimeToSeconds(duration.trim());
                if (canLogDur) log('getVideoDuration', 'duración document.querySelector(.ytp-time-duration).textContent', parsedDuration)
                if (parsedDuration && parsedDuration > 0 && !isNaN(parsedDuration)) return parsedDuration;
            }
        }

        // meta[itemprop="duration"] (evitar en Shorts y no da decimales)
        // "PT4M35S" -> parseISODuration -> 275  no da decimales!
        if (ptNow !== 'shorts' && contId !== 'shorts-player') {
            duration = document.querySelector('meta[itemprop="duration"]')?.content;
            if (canLogDur) log('getVideoDuration', 'duración document.querySelector(meta[itemprop="duration"]).content', duration)
            if (duration) {
                const parsedDuration = parseISODuration(duration);
                if (parsedDuration && parsedDuration > 0 && !isNaN(parsedDuration)) return parsedDuration;
            }
        }

        // Si todavía no se obtuvo, probar con el <video> real
        // 275.421
        if (!duration || isNaN(duration) || duration === 0) {
            if (videoEl) {
                // Esperar a que el <video> cargue los metadatos si aún no lo ha hecho
                if (isNaN(videoEl.duration) || videoEl.duration === 0) {
                    // Retornar una promesa que esperará los metadatos
                    return new Promise(resolve => {
                        const checkDuration = () => {
                            if (!isNaN(videoEl.duration) && videoEl.duration > 0) {
                                if (canLogDur) log('getVideoDuration', 'duración videoEl.duration (después de esperar metadatos)', videoEl.duration);
                                resolve(videoEl.duration);
                            } else {
                                videoEl.addEventListener('loadedmetadata', () => {
                                    if (canLogDur) log('getVideoDuration', 'duración videoEl.duration (después de loadedmetadata)', videoEl.duration);
                                    resolve(videoEl.duration);
                                }, { once: true });
                                // Timeout por si nunca se cargan los metadatos
                                setTimeout(() => {
                                    if (canLogDur) log('getVideoDuration', 'timeout esperando metadatos, usando 0');
                                    resolve(0);
                                }, 3000);
                            }
                        };
                        checkDuration();
                    });
                } else {
                    duration = videoEl.duration;
                    if (canLogDur) log('getVideoDuration', 'duración videoEl.duration (directo)', duration);
                    return duration;
                }
            }
        }

        return 0;
    }

    // ------------------------------------------
    // MARK: 📺 Extraer o Normalizar Video ID
    // ------------------------------------------

    /**
    * Extrae o normaliza un video ID de YouTube desde URL, embed o ID directo.
    * Soporta:
    *  - URLs normales: watch?v=ID
    *  - Shorts: /shorts/ID
    *  - Short URLs: youtu.be/ID
    *  - Embeds: /embed/ID
    *  - IDs directos
    * @param {string} input - URL completa o ID de video.
    * @returns {string|null} - Video ID o null si no se pudo determinar.
    */
    function extractOrNormalizeVideoId(input) {
        if (!input || typeof input !== 'string') return null;
        const trimmed = input.trim();

        // Si es solo un ID directo (no URL)
        if (/^[A-Za-z0-9_-]{6,}$/.test(trimmed)) {
            return { type: "video", id: trimmed };
        }

        try {
            const url = new URL(trimmed);

            // --- LISTAS ---
            const listParam = url.searchParams.get("list");
            if (url.pathname.includes("/playlist") && listParam) {
                return { type: "playlist", id: listParam };
            }

            // --- VIDEOS ---
            // watch?v=ID
            const vParam = url.searchParams.get("v");
            if (vParam) {
                const result = { type: "video", id: vParam };
                if (listParam) result.list = listParam; // video dentro de lista
                return result;
            }

            // shorts/ID
            const shortsMatch = url.pathname.match(/\/shorts\/([A-Za-z0-9_-]{6,})/);
            if (shortsMatch) return { type: "video", id: shortsMatch[1] };

            // embed/ID
            const embedMatch = url.pathname.match(/\/embed\/([A-Za-z0-9_-]{6,})/);
            if (embedMatch) return { type: "video", id: embedMatch[1] };

            // short URL (youtu.be/ID)
            if (url.hostname.includes("youtu.be")) {
                const shortId = url.pathname.slice(1);
                if (/^[A-Za-z0-9_-]{6,}$/.test(shortId)) {
                    const result = { type: "video", id: shortId };
                    if (listParam) result.list = listParam;
                    return result;
                }
            }
        } catch {
            // Si no es URL válida, continuar
        }

        // Si todo falla, decidir severidad según contexto de URL
        try {
            const url = new URL(trimmed);
            const host = url.hostname || '';
            const path = url.pathname || '';
            const isYouTube = /(^|\.)youtube\.com$/i.test(host) || /youtu\.be$/i.test(host);
            const isNonVideoPath = (
                path === '/' ||
                path.startsWith('/@') ||
                path.startsWith('/channel/') ||
                path.startsWith('/user/') ||
                path.startsWith('/c/') ||
                path.startsWith('/feed') ||
                path.startsWith('/results') ||
                path.startsWith('/account') ||
                path.startsWith('/playlist') && !url.searchParams.get('list')
            );
            if (isYouTube && isNonVideoPath) {
                // Páginas de canal/home/feed/búsqueda: no es un error, solo debug
                log('extractOrNormalizeVideoId', 'No es página de video, omitiendo:', input);
                return null;
            }
        } catch { }

        // Caso general: advertencia
        warn('extractOrNormalizeVideoId: no se pudo determinar video_id para', input);
        return null;
    }

    // MARK: 📺 Is Live Video
    function isLiveVideo() {
        try {
            log('isLiveVideo', '🔍 Iniciando detección de video live...');

            // Si YTHelper indica que NO es live y tiene duración >0, es un video VOD
            if (YTHelper?.video?.isCurrentlyLive === false && YTHelper?.video?.lengthSeconds > 0) {
                log('isLiveVideo', '❌ YTHelper indica no live y duración >0 - NO es live');
                return false;
            }

            // 1. Usar YouTube Helper API
            if (YTHelper?.video) {
                const isCurrentlyLive = YTHelper.video.isCurrentlyLive;
                const isLiveOrVodContent = YTHelper.video.isLiveOrVodContent;
                const wasStreamedOrPremiered = YTHelper.video.wasStreamedOrPremiered;

                log('isLiveVideo', `YouTube Helper API detección:`);
                log('isLiveVideo', `   - isCurrentlyLive: ${isCurrentlyLive}`);
                log('isLiveVideo', `   - isLiveOrVodContent: ${isLiveOrVodContent}`);
                log('isLiveVideo', `   - wasStreamedOrPremiered: ${wasStreamedOrPremiered}`);

                // Si está actualmente en vivo
                if (isCurrentlyLive /* || isLiveOrVodContent || wasStreamedOrPremiered */) {
                    log('isLiveVideo', '✅ Detectado como LIVE por YouTube Helper API');
                    return true;
                }
            }

            // 2. Fallback: Verificar badge de live en el player (solo si es visible)
            const liveBadge = document.querySelector('.ytp-live-badge');
            const isBadgeVisible = liveBadge && liveBadge.offsetParent !== null &&
                window.getComputedStyle(liveBadge).display !== 'none';
            const liveBadgeText = liveBadge?.getAttribute('aria-label') || liveBadge?.textContent || '';
            log('isLiveVideo', `Live badge encontrado: ${!!liveBadge}, visible: ${isBadgeVisible}, tiene aria-label: ${liveBadge?.hasAttribute('aria-label')}`);
            log('isLiveVideo', `Contenido del badge: "${liveBadgeText}"`);

            if (liveBadge && isBadgeVisible && liveBadge.hasAttribute('aria-label')) {
                // Verificar que el contenido del badge realmente indique "live" actual
                // Excluir mensajes de navegación como "Sigue adelante hasta alcanzar..."
                const lowerText = liveBadgeText.toLowerCase();
                const isNavigationMessage = lowerText.includes('sigue') ||
                    lowerText.includes('adelante') ||
                    lowerText.includes('alcanzar') ||
                    lowerText.includes('hasta') ||
                    lowerText.includes('go forward') ||
                    lowerText.includes('seek to');

                const badgeIndicatesLive = !isNavigationMessage && (
                    lowerText === 'live' ||
                    lowerText === 'en vivo' ||
                    lowerText === 'directo' ||
                    lowerText.startsWith('live ') ||
                    lowerText.startsWith('en vivo ') ||
                    lowerText.startsWith('directo ')
                );

                log('isLiveVideo', `Badge indica live: ${badgeIndicatesLive} (navegación: ${isNavigationMessage})`);

                if (badgeIndicatesLive) {
                    log('isLiveVideo', '✅ Detectado como LIVE por badge del player');
                    return true;
                } else {
                    log('isLiveVideo', '⚠️ Badge encontrado pero no indica live actual - continuando verificaciones...');
                }
            }

            // 3. Fallback: Verificar meta tag específico de live broadcast (con validación adicional)
            const isLiveBroadcastMeta = document.querySelector('meta[itemprop="isLiveBroadcast"]');
            const isLiveBroadcast = isLiveBroadcastMeta?.content === 'True';
            log('isLiveVideo', `Meta isLiveBroadcast: ${isLiveBroadcastMeta?.content}, es True: ${isLiveBroadcast}`);

            if (isLiveBroadcast) {
                // SIEMPRE validar meta tag con otros indicadores para evitar falsos positivos
                const currentTitle = YTHelper?.video?.title || document.title.replace(/ - YouTube$/, '').trim();
                const hasLiveInTitle = currentTitle.toLowerCase().includes('live');
                const hasLiveInUrl = window.location.href.includes('/live/');

                log('isLiveVideo', `Validación de meta tag isLiveBroadcast:`);
                log('isLiveVideo', `   - Título: "${currentTitle}" YTHelper?.video?.title: ${YTHelper?.video?.title} | document.title.replace(/ - YouTube$/, '').trim(): ${document.title.replace(/ - YouTube$/, '').trim()}`);
                log('isLiveVideo', `   - Título contiene "live": ${hasLiveInTitle}`);
                log('isLiveVideo', `   - URL contiene "/live/": ${hasLiveInUrl}`);

                // Solo confiar en el meta tag si hay otros indicadores de live
                if (hasLiveInTitle || hasLiveInUrl) {
                    log('isLiveVideo', '✅ Detectado como LIVE por meta tag isLiveBroadcast + validación');
                    return true;
                } else {
                    log('isLiveVideo', '⚠️ Meta isLiveBroadcast=True pero sin otros indicadores - posible falso positivo, continuando verificaciones...');
                }
            }

            // Verificar indicadores adicionales de live/DVR
            log('isLiveVideo', `Verificando indicadores adicionales de live/DVR`);

            // Verificar si YouTube considera este contenido como live (SOLO en URL)
            const hasLiveDvr = window.location.href.includes('live=dvr') ||
                window.location.href.includes('pltype=contentlive');

            // Verificar en el contexto del player (elementos DOM específicos, NO innerHTML general)
            const hasPlayerLiveContext = document.querySelector('[data-live="true"]') ||
                document.querySelector('.ytp-live:not(.ytp-live-badge)') ||
                document.querySelector('.live-stream-text') ||
                document.querySelector('.ytp-chrome-top .ytp-live');

            log('isLiveVideo', `YouTube live/DVR indicators:`);
            log('isLiveVideo', `   - URL contiene live=dvr/pltype=contentlive: ${hasLiveDvr}`);
            log('isLiveVideo', `   - Contexto de player live detectado (selectores DOM): ${hasPlayerLiveContext}`);

            if (hasLiveDvr || hasPlayerLiveContext) {
                log('isLiveVideo', '✅ Detectado como LIVE por indicadores de YouTube (live=dvr, pltype=contentlive o contexto de player)');
                return true;
            }

            // Verificar duración PT0M0S solo si no estamos navegando
            if (!isNavigating) {
                const durationMeta = document.querySelector('meta[itemprop="duration"]')?.content;
                log('isLiveVideo', `Meta duration: "${durationMeta}"`);

                if (durationMeta === 'PT0M0S') {
                    // Verificación adicional: buscar otros indicadores de live
                    const liveAriaLabel = document.querySelector('[aria-label*="live"]');
                    const liveTitle = document.querySelector('[title*="live"]');
                    const liveBadgeClass = document.querySelector('.live-badge');
                    const liveUrl = window.location.href.includes('/live/');

                    log('isLiveVideo', `Indicadores adicionales de live:`);
                    log('isLiveVideo', `   - aria-label con "live": ${!!liveAriaLabel}`);
                    log('isLiveVideo', `   - title con "live": ${!!liveTitle}`);
                    log('isLiveVideo', `   - clase .live-badge: ${!!liveBadgeClass}`);
                    log('isLiveVideo', `   - URL contiene /live/: ${liveUrl}`);

                    const hasLiveIndicators = liveAriaLabel || liveTitle || liveBadgeClass || liveUrl;

                    if (YTHelper?.video?.lengthSeconds > 0) {
                        log('isLiveVideo', '⚠️ PT0M0S pero YTHelper indica duración >0 - no es live');
                    } else if (hasLiveIndicators) {
                        log('isLiveVideo', '✅ Detectado como LIVE por PT0M0S + indicadores adicionales');
                        return true;
                    } else {
                        log('isLiveVideo', '⚠️ PT0M0S encontrado pero SIN indicadores adicionales de live - probablemente tiempo fijo');
                    }
                }
            } else {
                log('isLiveVideo', '⏭️ Omitiendo verificación PT0M0S durante navegación');
            }

            // Verificar si hay tiempo fijo desde el URL
            const hasFixedTime = window.location.href.includes('&t=') || window.location.href.includes('#t=');
            log('isLiveVideo', `Tiempo fijo en URL: ${hasFixedTime}`);
            if (hasFixedTime) {
                log('isLiveVideo', '❌ Video tiene tiempo fijo configurado - NO es live');
                return false;
            }

            log('isLiveVideo', '❌ NO detectado como live');
            return false;
        } catch (e) {
            warn('isLiveVideo', 'Error al detectar video live:', e);
            return false;
        }
    }

    // MARK: 📺 Get Playlist Name
    const playlistNameCache = new Map();
    /**
     * Obtiene el nombre de la playlist desde el DOM o la URL.
     * @param {string} playlistId - ID de la playlist.
     * @returns {string|null} Nombre de la playlist o null si no se encuentra.
     *
     * Ejemplo de URL:
     * https://www.youtube.com/watch?v=VIDEO_ID&list=PLAYLIST_ID
     */
    async function getPlaylistName(playlistId) {
        if (playlistNameCache.has(playlistId)) {
            return playlistNameCache.get(playlistId);
        }

        const url = new URL(location.href);
        const currentPlaylistId = url.searchParams.get('list');

        if (currentPlaylistId === playlistId) {
            // Intentar múltiples selectores para el panel de playlist
            const playlistName =
                // Playlist panel en el reproductor
                document.querySelector('ytd-playlist-panel-renderer #title span#text')?.textContent?.trim() ||
                // Header de la página de playlist
                document.querySelector('#header .ytd-playlist-header-renderer h1 yt-formatted-string')?.textContent?.trim() ||
                document.querySelector('ytd-browse[page-subtype="playlist"] ytd-playlist-header-renderer #title')?.textContent?.trim() ||
                document.querySelector('ytd-playlist-header-renderer h1.title')?.textContent?.trim() ||
                // Alternativas adicionales
                document.querySelector('#container #header-description yt-formatted-string')?.textContent?.trim() ||
                document.querySelector('yt-formatted-string.title:nth-child(1)')?.textContent?.trim() ||
                // Overlay del reproductor (menos confiable)
                document.querySelector('.ytp-title-playlist-button')?.getAttribute('aria-label')?.replace(/^Playlist:\s*/, '')?.trim() ||
                document.querySelector('.byline-title')?.textContent?.trim();

            log('getPlaylistName', 'Playlist name from DOM:', playlistName);
            if (playlistName) {
                playlistNameCache.set(playlistId, playlistName);
                return playlistName;
            }
        }

        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://www.youtube.com/playlist?list=${playlistId}`,
                onload: function (response) {
                    try {
                        const htmlText = response.responseText;

                        // Intentar extraer JSON principal
                        const ytInitialDataMatch = htmlText.match(/var ytInitialData = ({.+?});/);
                        let title = null;

                        if (ytInitialDataMatch) {
                            const data = JSON.parse(ytInitialDataMatch[1]);

                            // Intentar extraer título de playlist normal
                            title = data?.metadata?.playlistMetadataRenderer?.title ||
                                data?.sidebar?.playlistSidebarRenderer?.items?.[0]?.playlistSidebarPrimaryInfoRenderer?.title?.runs?.[0]?.text ||
                                data?.header?.playlistHeaderRenderer?.title?.simpleText;

                            // Fallback: intentar rutas típicas de Mixes automáticos
                            if (!title) {
                                title = data?.contents?.twoColumnWatchNextResults?.playlist?.playlist?.title ||
                                    data?.playerOverlays?.playerOverlayRenderer?.autoplay?.autoplay?.title?.simpleText ||
                                    data?.contents?.twoColumnWatchNextResults?.playlist?.title;
                            }
                        }

                        // Si aún no hay título, buscar en meta tags
                        if (!title) {
                            const metaTitleMatch = htmlText.match(/<meta property="og:title" content="([^"]+)">/);
                            if (metaTitleMatch && metaTitleMatch[1]) {
                                title = metaTitleMatch[1];
                            }
                        }

                        // Si no se pudo extraer, usar el ID como fallback
                        if (!title) {
                            warn('getPlaylistName', 'No se pudo extraer el título de la playlist, usando ID');
                            title = playlistId;
                        }

                        playlistNameCache.set(playlistId, title);
                        log('getPlaylistName', `Título obtenido: ${title}`);
                        resolve(title);
                    } catch (e) {
                        conError('getPlaylistName', 'Error parsing playlist page:', e);
                        playlistNameCache.set(playlistId, playlistId);
                        resolve(playlistId);
                    }
                },
                onerror: function (error) {
                    conError('getPlaylistName', 'Error fetching playlist page:', error);
                    playlistNameCache.set(playlistId, playlistId);
                    resolve(playlistId);
                }
            });
        });
    }

    // ------------------------------------------
    // MARK: 💾 Video Status Management
    // ------------------------------------------

    // Cache para info de video (evita llamadas repetidas a YouTube API)
    const videoInfoCache = new Map();
    const CACHE_DURATION = 5 * 60 * 1000; // 5 minutos

    /**
     * Limpia el caché de información de video para un ID específico o todo el caché
     * @param {string} [video_id] - ID del video a limpiar (opcional, si no se especifica limpia todo)
     */
    function clearVideoInfoCache(video_id = null) {
        if (video_id) {
            if (videoInfoCache.has(video_id)) {
                videoInfoCache.delete(video_id);
                log('clearVideoInfoCache', `🧹 Cache limpiado para video ${video_id}`);
            }
        } else {
            const size = videoInfoCache.size;
            videoInfoCache.clear();
            log('clearVideoInfoCache', `🧹 Cache completo limpiado (${size} entradas)`);
        }
    }

    /**
     * Obtiene la información del video con cache
     * @param {object} player - Player de YouTube
     * @param {string} video_id - ID del video
     * @returns {Promise<object>} - Información del video
     */
    async function getCachedVideoInfo(player, video_id) {
        const cacheKey = video_id;
        const cached = videoInfoCache.get(cacheKey);

        // Durante navegación, ser más estricto con el caché para evitar datos obsoletos
        const cacheTimeout = isNavigating ? 30 * 1000 : CACHE_DURATION; // 30 segundos durante navegación, 5 minutos normal

        // Verificar si el caché es válido
        const isCacheValid = cached && (Date.now() - cached.timestamp) < cacheTimeout;
        // En Shorts, forzar refresco para evitar contaminación (título/vistas)
        let effectiveCacheValid = isCacheValid;
        try {
            const ptNow = getYouTubePageType();
            if (ptNow === 'shorts') {
                effectiveCacheValid = false;
                log('getCachedVideoInfo', `⛔ Bypass cache en Shorts para ${video_id}`);
            }
        } catch (_) { }

        // Durante navegación, también verificar que el título en caché coincida con document.title actual
        let isCacheAccurate = true;
        if (isNavigating && cached?.data?.title) {
            const currentDocTitle = document.title.replace(/ - YouTube$/, '').trim();
            if (currentDocTitle) {
                const normalize = (str) => str.toLowerCase().replace(/\s+/g, ' ').trim();
                const normalizedCacheTitle = normalize(cached.data.title);
                const normalizedDocTitle = normalize(currentDocTitle);

                // Si los títulos no coinciden, el caché probablemente tiene datos del video anterior
                isCacheAccurate = normalizedDocTitle === normalizedCacheTitle ||
                    normalizedDocTitle.includes(normalizedCacheTitle) ||
                    normalizedCacheTitle.includes(normalizedDocTitle);

                if (!isCacheAccurate) {
                    log('getCachedVideoInfo', `⚠️ Cache inválido para ${video_id}: título "${cached.data.title}" no coincide con document.title "${currentDocTitle}"`);
                }
            }
        }

        if (cached && effectiveCacheValid) {
            if (cached?.data?.videoId && cached.data.videoId !== video_id) {
                isCacheAccurate = false;
                log('getCachedVideoInfo', `⚠️ Cache inválido para ${video_id}: videoId en cache (${cached.data.videoId}) no coincide`);
            }
            if (cached?.data?.thumb && !thumbnailHasVideoId(cached.data.thumb, video_id)) {
                isCacheAccurate = false;
                log('getCachedVideoInfo', `⚠️ Cache inválido para ${video_id}: thumbnail cacheado no corresponde al ID actual`);
            }
        }

        // Retornar cache solo si es válido Y preciso
        if (effectiveCacheValid && isCacheAccurate) {
            log('getCachedVideoInfo', `✅ Usando cache para ${video_id}`);
            return cached.data;
        }

        // Obtener info fresca
        log('getCachedVideoInfo', `🔄 Obteniendo info fresca para ${video_id}${isNavigating ? ' (navegando)' : ''}${!isCacheAccurate ? ' (cache impreciso)' : ''}`);
        const info = await getVideoInfo(player, video_id);

        // Solo guardar en cache si no estamos navegando O si los datos parecen válidos
        const shouldCache = !isNavigating || (info.title && info.title.length > 3);

        if (shouldCache) {
            videoInfoCache.set(cacheKey, {
                data: info,
                timestamp: Date.now()
            });
            log('getCachedVideoInfo', `💾 Info cacheada para ${video_id}: "${info.title}"`);
        } else {
            log('getCachedVideoInfo', `⏭️ No cacheando durante navegación para ${video_id}`);
        }

        return info;
    }

    /**
     * Guarda el progreso del video actual
     * @param {object} player - Player de YouTube
     * @param {HTMLVideoElement} videoEl - Elemento de video
     * @param {string} type - Tipo de video (watch, embed, shorts)
     * @param {string} plId - ID del playlist (opcional)
     * @returns {Promise<object>} - Resultado del guardado
     */
    const updateStatus = async (player, videoEl, type, plId, expectedVideoId) => {
        // Obtener el ID desde URL
        const homeVideoCheck = await getActiveVideoElement();
        let url;
        if (getYouTubePageType() === 'home' && homeVideoCheck) {
            let containerId = null;
            try { containerId = homeVideoCheck.closest('#movie_player, #shorts-player')?.id || null; } catch (_) { }
            // Usar lastVideoUrl solo si el activo está en el miniplayer
            url = (containerId === 'movie_player') ? lastVideoUrl : location.href;
        } else {
            url = location.href;
        }

        const pageTypeNow = getYouTubePageType();
        const urlDataNow = extractOrNormalizeVideoId(url);
        const urlIdNow = urlDataNow?.id || null;
        // Fuente del playlist en este ciclo (solo se usa para Shorts)
        let playlistSource = null; // 'url' | 'anchor' | 'fallback' | null
        try {
            if (pageTypeNow === 'shorts') {
                // Shorts: distinguir entre contenedor de Shorts y Miniplayer
                let p = urlDataNow?.list || null;
                let pSource = p ? 'url' : null; // url | anchor | fallback | null
                let contId = null;
                try { contId = videoEl?.closest?.('#movie_player, #shorts-player')?.id || null; } catch (_) { }

                if (contId === 'movie_player') {
                    // Miniplayer visible estando en página Shorts: permitir anchors del miniplayer y fallback a lastPlaylistId
                    if (!p) {
                        try {
                            const a = document.querySelector('#movie_player a[href*="list="]') ||
                                document.querySelector('.ytp-miniplayer-ui a[href*="list="]');
                            if (a) {
                                const ua = new URL(a.href, location.origin);
                                const la = ua.searchParams.get('list');
                                if (la) { p = la; pSource = 'mp_anchor'; }
                            }
                        } catch (_) { }
                    }
                    if (!p) {
                        try { if (lastPlaylistId) { p = lastPlaylistId; pSource = 'fallback'; } } catch (_) { }
                    }
                } else {
                    // Contenido Shorts en su propio contenedor: solo evidencia explícita (URL o UI de Shorts)
                    if (!p) {
                        try {
                            // Buscar anchors de playlist, pero solo aceptar los que refieran explícitamente al mismo video actual
                            const anchors = Array.from(document.querySelectorAll('#shorts-player a[href*="list="], ytd-reel-player-overlay-renderer a[href*="list="], ytd-reel-video-renderer a[href*="list="], #metapanel a[href*="list="]'));
                            const currentShortId = urlIdNow;
                            const pick = anchors.find(a => {
                                try {
                                    const ua = new URL(a.href, location.origin);
                                    const v = ua.searchParams.get('v') || '';
                                    const path = ua.pathname || '';
                                    // Aceptar solo si el anchor hace referencia al mismo video (watch?v=ID o /shorts/ID)
                                    return (v === currentShortId) || path.includes(`/shorts/${currentShortId}`);
                                } catch (_) { return false; }
                            });
                            if (pick) {
                                const ua = new URL(pick.href, location.origin);
                                const la = ua.searchParams.get('list');
                                if (la) { p = la; pSource = 'anchor'; }
                            }
                        } catch (_) { }
                    }
                }

                plId = p || null;
                // Actualizar lastPlaylistId solo si la fuente es explícita (url o anchor), nunca en fallback
                try { if ((pSource === 'url' || pSource === 'anchor') && plId) { lastPlaylistId = plId; } } catch (_) { }
                playlistSource = pSource;
            } else {
                if (!plId) {
                    let p = urlDataNow?.list || null;
                    if (!p) {
                        try {
                            const a = document.querySelector('#anchored-panel a[href*="list="]') ||
                                document.querySelector('#movie_player a[href*="list="]') ||
                                document.querySelector('.ytp-miniplayer-ui a[href*="list="]');
                            if (a) {
                                const ua = new URL(a.href, location.origin);
                                const la = ua.searchParams.get('list');
                                if (la) p = la;
                            }
                        } catch (_) { }
                    }
                    if (!p) {
                        try { if (lastPlaylistId) p = lastPlaylistId; } catch (_) { }
                    }
                    if (p) plId = p;
                    try { if (plId) lastPlaylistId = plId; } catch (_) { }
                }
                const isHomeishNow = pageTypeNow === 'home' || pageTypeNow === 'search' || pageTypeNow === 'channel';
                if (isHomeishNow && lastPlaylistId && /^RD/.test(lastPlaylistId) && plId && !/^RD/.test(plId)) {
                    plId = lastPlaylistId;
                }
            }
        } catch (_) { }
        let video_id;
        try {
            if (pageTypeNow === 'watch' || pageTypeNow === 'embed' || pageTypeNow === 'playlist') {
                video_id = urlIdNow || expectedVideoId || YTHelper?.video?.id || player?.getVideoData?.()?.video_id || null;
            } else if (pageTypeNow === 'shorts') {
                const cont = videoEl?.closest?.('#movie_player, #shorts-player');
                if (cont?.id === 'movie_player') {
                    const mp = document.querySelector('#movie_player');
                    const mpId = mp?.getVideoData?.()?.video_id;
                    // En Shorts pero con miniplayer visible: permitir expected/mpId primero
                    video_id = expectedVideoId || mpId || player?.getVideoData?.()?.video_id || urlIdNow || YTHelper?.video?.id || null;
                } else {
                    // En Shorts dentro de #shorts-player: priorizar SIEMPRE el ID de la URL; no usar expectedVideoId salvo como último recurso
                    video_id = urlIdNow || player?.getVideoData?.()?.video_id || YTHelper?.video?.id || expectedVideoId || null;
                }
            } else if (pageTypeNow === 'home') {
                video_id = expectedVideoId || urlIdNow || YTHelper?.video?.id || player?.getVideoData?.()?.video_id || null;
            } else {
                video_id = expectedVideoId || urlIdNow || YTHelper?.video?.id || player?.getVideoData?.()?.video_id || null;
            }
        } catch (_) {
            video_id = urlIdNow || YTHelper?.video?.id || player?.getVideoData?.()?.video_id || expectedVideoId || null;
        }
        log('updateStatus', `URL del reproductor: ${url} | Video ID Extraido: ${video_id}`)

        if (!video_id) {
            conError('updateStatus', 'No se pudo determinar video_id. Abortando guardado.');
            return { success: false, reason: 'no_video_id' };
        }

        // No forzar player desde YTHelper aquí para evitar mezclar contenedores; haremos rebind según tipo/ID
        if (!videoEl || !document.contains(videoEl)) {
            try { const hv = YTHelper?.player?.videoElement; if (hv) videoEl = hv; } catch (_) { }
        }
        // Re-enlazar al contenedor correcto para evitar contaminación entre Shorts y Miniplayer
        try {
            const mp = document.querySelector('#movie_player');
            const sp = document.querySelector('#shorts-player');
            const mpIdNow = mp?.getVideoData?.()?.video_id || null;
            // Determinar el contenedor objetivo según el propio videoEl (no forzar movie_player en Home)
            let targetContainerId = null;
            try { targetContainerId = videoEl?.closest?.('#movie_player, #shorts-player')?.id || null; } catch (_) { }
            // Si estamos en Shorts y el tipo solicitado es shorts pero el elemento proviene del miniplayer,
            // forzar rebind al contenedor de Shorts o abortar si no está disponible.
            // Importante: no rebindear contextos "watch" (miniplayer) solo porque la página sea Shorts;
            // esto provocaría que el miniplayer dejara de guardar al entrar a Shorts.
            if (type === 'shorts' && targetContainerId === 'movie_player') {
                try {
                    const spElTry = document.querySelector('#shorts-player video') || document.querySelector('ytd-shorts video.html5-main-video');
                    if (spElTry) {
                        targetContainerId = 'shorts-player';
                        videoEl = spElTry;
                        // En Shorts, el ID confiable es el de la URL
                        if (urlIdNow) video_id = urlIdNow;
                        log('updateStatus', '⤴️ Re-binding a shorts-player para evitar contaminación del miniplayer en Shorts');
                    } else if (type === 'shorts') {
                        log('updateStatus', '⛔ No se encontró contenedor de Shorts en contexto Shorts; no se guardará para evitar cruce');
                        return { success: false, reason: 'shorts_container_mismatch' };
                    }
                } catch (_) { }
            }
            if (!targetContainerId) {
                if (type === 'shorts' || pageTypeNow === 'shorts') targetContainerId = 'shorts-player';
            }
            // No cambiar a movie_player en páginas Shorts según coincidencia de IDs; se maneja por pollers separados
            if (targetContainerId === 'movie_player' && mp) {
                player = mp;
                const veMp = document.querySelector('#movie_player video.html5-main-video');
                if (veMp) videoEl = veMp;
            } else if (targetContainerId === 'shorts-player') {
                const veSp = document.querySelector('#shorts-player video') || document.querySelector('ytd-shorts video.html5-main-video');
                if (veSp) videoEl = veSp;
                // Siempre usar un player stub para Shorts para evitar tomar metadatos del miniplayer
                player = {
                    getVideoData: () => ({ video_id: urlIdNow, title: getVideoTittle(null) || null, author: getVideoAuthor(null) || null }),
                    getCurrentTime: () => { try { return videoEl?.currentTime || 0; } catch (_) { return 0; } },
                    getDuration: () => { try { return videoEl?.duration || 0; } catch (_) { return 0; } }
                };
            }
        } catch (_) { }
        // Obtener currentTime evitando contaminación entre Shorts y Miniplayer
        let currentTime = 0;
        let contId = null;
        try { contId = videoEl?.closest?.('#movie_player, #shorts-player')?.id || null; } catch (_) { }
        // Bloqueo de contexto cruzado: en página Shorts, si el tipo pasado es 'watch' pero el contenedor es Shorts, no guardar
        if (pageTypeNow === 'shorts' && type === 'watch' && contId === 'shorts-player') {
            log('updateStatus', '⛔ Contexto Shorts con tipo watch y contenedor shorts-player; evitando guardado cruzado');
            return { success: false, reason: 'context_mismatch_watch_in_shorts' };
        }
        // Evitar guardar si es una previsualización inline y el usuario no lo habilitó
        try {
            const isInline = !!(videoEl?.closest?.('#inline-preview-player') ||
                videoEl?.closest?.('.ytp-inline-preview-ui') ||
                videoEl?.closest?.('ytd-thumbnail-overlay-inline-playback-renderer'));
            const allowInline = cachedSettings?.saveInlinePreviews === true;
            if (isInline && !allowInline) {
                log('updateStatus', '🔕 Inline preview detectada y deshabilitada por configuración. No guardar.');
                return { success: false, reason: 'inline_preview_disabled' };
            }
        } catch (_) { }
        // 1) Priorizar el <video> específico
        try {
            if (videoEl) {
                const t3 = videoEl.currentTime;
                if (typeof t3 === 'number' && isFinite(t3) && t3 > 0) currentTime = t3;
            }
        } catch (_) { }
        // 2) Luego, player.getCurrentTime del player asociado
        if (!currentTime || currentTime <= 0) {
            try {
                const t = player?.getCurrentTime?.();
                if (typeof t === 'number' && isFinite(t) && t > 0) currentTime = t;
            } catch (_) { }
        }
        // 3) Solo como último recurso en videos NO-Shorts, usar #movie_player
        if ((!currentTime || currentTime <= 0) && type !== 'shorts' && contId !== 'shorts-player') {
            try {
                const mp = document.querySelector('#movie_player');
                const t2 = mp?.getCurrentTime?.();
                if (typeof t2 === 'number' && isFinite(t2) && t2 > 0) currentTime = t2;
            } catch (_) { }
        }
        const now = Date.now();

        return getCachedVideoInfo(player, video_id).then(async ({ duration, ...videoInfo }) => {
            log('updateStatus', `then duration: ${duration} = ${formatTime(duration)} | current time: ${currentTime} | video_id: ${video_id}`);
            const infoIsLive = videoInfo?.isLive === true;
            const isLiveType = type === 'live' || infoIsLive;

            // Respeto estricto de configuración para lives también en updateStatus
            try {
                if (isLiveType && cachedSettings?.saveLiveStreams === false) {
                    log('updateStatus', '🛑 Video detectado como LIVE en updateStatus y saveLiveStreams está deshabilitado, omitiendo guardado.');
                    return { success: false, reason: 'live_disabled_by_settings' };
                }
            } catch (_) { }

            // Evitar que un currentTime contaminado supere la duración
            if (!isLiveType && duration && isFinite(duration) && currentTime > duration) {
                try {
                    warn('updateStatus', `⚠️ currentTime (${currentTime}) > duration (${duration}) para ${video_id}. Ajustando a duración.`);
                } catch (_) { }
                currentTime = Math.max(0, Math.min(currentTime, Math.max(0, duration - 0.05)));
            }
            if (isLiveType) {
                // Para LIVE solo requerimos un currentTime válido >= 1s
                if (isNaN(currentTime) || currentTime < 1) return { success: false, reason: 'invalid_data' };
            } else {
                // Para no-LIVE requerimos duración finita y datos válidos
                if (!duration || !isFinite(duration) || isNaN(currentTime) || currentTime < 1) return { success: false, reason: 'invalid_data' };
            }

            // Evitar guardar progreso durante anuncios (tipo-aware)
            // Normalizar tipo para chequeo de anuncios: tratar preview_* como su base
            const typeForAdsCheck = (type === 'shorts' || type === 'preview_shorts') ? 'shorts' : 'watch';
            // Verificar si hay anuncios activos usando el estado por tipo
            if (isAdBlockedFor(typeForAdsCheck)) {
                log('updateStatus', `⏸ Anuncio activo (${type}) detectado, no guardando progreso`);
                return { success: false, reason: 'ad_playing' };
            }
            // Chequeo local de anuncios por contenedor (refuerzo) – solo para movie_player
            try {
                const container = videoEl?.closest?.('#movie_player, #shorts-player');
                let hasAdSignals = false;
                if (container?.id === 'movie_player') {
                    if (container.classList?.contains('ad-showing') || container.classList?.contains('ad-interrupting')) {
                        hasAdSignals = true;
                    } else {
                        const adNode = container.querySelector?.('#player-ads, .ytp-ad-module, .ytp-ad-player-overlay, .video-ads');
                        if (adNode) {
                            let isVisible = false;
                            try {
                                const rect = adNode.getBoundingClientRect?.();
                                if (rect && rect.width > 0 && rect.height > 0) {
                                    const style = window.getComputedStyle?.(adNode);
                                    if (style && style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity || '1') > 0.01) {
                                        isVisible = true;
                                    }
                                }
                            } catch (_) { }
                            if (isVisible) hasAdSignals = true;
                        }
                    }
                }
                if (hasAdSignals) {
                    log('updateStatus', '⏸ Señales locales de anuncio detectadas, no guardando progreso');
                    return { success: false, reason: 'ad_playing_local' };
                }
            } catch (_) { }
            if (isResuming) {
                log('updateStatus', '⏸ isResuming está en true, no guardando progreso');
                return { success: false, reason: 'is_resuming' };
            }

            // Buscar progreso previo siempre
            const sourceData = getSavedVideoData(video_id, plId);

            // Verificar si el video está completado (staticFinishPercent porcentaje del video)
            const isFinished = !isLiveType && duration > 0 && (currentTime / duration) * 100 >= (cachedSettings?.staticFinishPercent || CONFIG.defaultSettings.staticFinishPercent);

            if (sourceData && sourceData.forceResumeTime > 0) {
                if (isFinished) {
                    log('updateStatus', `Video con tiempo fijo ${video_id} completado. Manteniendo tiempo fijo.`);
                    const base = {
                        ...sourceData,
                        isCompleted: true,
                        lastUpdated: now,
                        timestamp: 0,
                    };
                    if (!thumbnailHasVideoId(base.thumb, video_id)) {
                        base.thumb = `https://i.ytimg.com/vi/${video_id}/maxresdefault.jpg`;
                    }
                    if (plId) {
                        const playlist = Storage.get(plId);
                        if (playlist?.videos?.[video_id]) {
                            playlist.videos[video_id] = base;
                            Storage.set(plId, playlist);
                        }
                    } else {
                        Storage.set(video_id, base);
                        log('updateStatus', `Datos guardados para ${video_id}:`, base);
                        return { success: true, video_id, timestamp: base.timestamp };
                    }
                }
                // No sobreescribir progreso en videos con tiempo fijo
                return { success: false, reason: 'fixed_time_no_overwrite' };
            }

            // Ajustar metadata con fuentes confiables en watch/home
            const pageType = getYouTubePageType();
            let baseVideoInfo = { ...videoInfo };
            if (pageType !== 'shorts') {
                try {
                    const mp = document.querySelector('#movie_player');
                    const mpData = mp?.getVideoData?.();
                    const mpIdNow2 = mpData?.video_id;
                    const mpAuthor = mpData?.author;
                    if (mpIdNow2 && mpIdNow2 === video_id && mpAuthor) baseVideoInfo.author = mpAuthor;
                } catch (_) { }
                try {
                    if (pageType === 'watch' || pageType === 'embed') {
                        const vdAuthor = window.ytInitialPlayerResponse?.videoDetails?.author;
                        if (vdAuthor) baseVideoInfo.author = vdAuthor;
                        const chId = window.ytInitialPlayerResponse?.videoDetails?.channelId;
                        if (chId) baseVideoInfo.authorId = chId;
                    }
                } catch (_) { }
            }

            // En páginas que no son watch (home, etc.), evitar sobreescribir metadata sensible
            const safeVideoInfo = { ...baseVideoInfo };
            if (pageType !== 'watch' && sourceData) {
                if (!safeVideoInfo.title && sourceData.title) safeVideoInfo.title = sourceData.title;
                if ((!safeVideoInfo.author || safeVideoInfo.author === t('unknown')) && sourceData.author) safeVideoInfo.author = sourceData.author;
                if ((!safeVideoInfo.authorId || safeVideoInfo.authorId === t('unknown')) && sourceData.authorId) safeVideoInfo.authorId = sourceData.authorId;
                if (!safeVideoInfo.thumb && sourceData.thumb) safeVideoInfo.thumb = sourceData.thumb;
            }

            // Determinar tipo efectivo a guardar: preservar previews inline si están habilitadas y no son anuncios
            let finalType = type;
            try {
                if (allowInline) {
                    const isInlineCtx = !!(videoEl?.closest?.('#inline-preview-player') ||
                        videoEl?.closest?.('.ytp-inline-preview-ui') ||
                        videoEl?.closest?.('ytd-thumbnail-overlay-inline-playback-renderer'));
                    const isAdInline = !!(videoEl?.closest?.('.ytp-ad-module, .ytp-ad-player-overlay, .video-ads, ytd-in-feed-ad-layout-renderer, ytd-ad-slot-renderer, ytd-display-ad-renderer, ytd-promoted-sparkles-web-renderer, #player-ads'));
                    if (isInlineCtx) {
                        if (isAdInline) {
                            log('updateStatus', '⏸ Inline detectada pero es un contexto de anuncio. No guardar.');
                            return { success: false, reason: 'ad_context_inline' };
                        }
                        const isShortishInline = !!(videoEl?.closest?.('ytd-reel-video-renderer, ytd-shorts, #shorts-player'));
                        finalType = isShortishInline ? 'preview_shorts' : 'preview_watch';
                    }
                }
            } catch (_) { }

            // Forzar tipo "watch" si el contenedor es miniplayer/desktop player, pero nunca sobreescribir "live"
            try {
                if (!isLiveType && contId === 'movie_player') {
                    finalType = 'watch';
                }
            } catch (_) { }

            // Forzar tipo Shorts solo cuando el contenedor real es Shorts
            try {
                if (pageType === 'shorts' && contId === 'shorts-player') {
                    finalType = 'shorts';
                }
            } catch (_) { }

            if (isLiveType && finalType !== 'shorts' && !(typeof finalType === 'string' && finalType.startsWith('preview_'))) {
                finalType = 'live';
            }

            // Protección adicional: en páginas home-like, si el enlace al video_id vive dentro de contenedores de anuncio
            // o ni siquiera existe un enlace directo al watch para ese ID, no guardar (previene anuncios autoplay en Home).
            try {
                const ptNow2 = getYouTubePageType();
                const isHomeLike2 = (ptNow2 === 'home' || ptNow2 === 'search' || ptNow2 === 'channel' || ptNow2 === 'unknown');
                const isPreviewType2 = typeof type === 'string' && type.startsWith('preview');
                if (isHomeLike2 && isPreviewType2 && video_id) {
                    const anchors = Array.from(document.querySelectorAll(`a[href*="/watch?v=${video_id}"]`));
                    const hasAnchors = anchors.length > 0;
                    if (!hasAnchors) {
                        log('updateStatus', '⏸ Inline preview en Home sin enlaces watch para este video; tratada como anuncio. No guardar.');
                        return { success: false, reason: 'inline_no_watch_link' };
                    }
                    let adLinked = false;
                    for (const a of anchors) {
                        if (!a) continue;
                        const inAdContainer = !!a.closest('ytd-in-feed-ad-layout-renderer, ytd-ad-slot-renderer, ytd-display-ad-renderer, ytd-promoted-sparkles-web-renderer');
                        const hasAdBadgeNearby = !!(a.closest('ytd-rich-item-renderer, ytd-ad-slot-renderer')?.querySelector?.('.yt-badge-shape--ad, .ytwAdBadgeViewModelHostIsClickableAdComponent, [aria-label="Ad"], [aria-label="Sponsored"], [data-ad-impressions]'));
                        if (inAdContainer || hasAdBadgeNearby) { adLinked = true; break; }
                    }
                    if (adLinked) {
                        log('updateStatus', '⏸ Enlace asociado al video dentro de un contexto de anuncio (ID-based). No guardar.');
                        return { success: false, reason: 'ad_link_context_inline' };
                    }
                }
            } catch (_) { }

            // Guardar progreso en formato FreeTube (video independiente con metadata de playlist)
            const normalizedDuration = isLiveType ? ((isFinite(duration) && duration > 0) ? duration : 0) : duration;

            // Preservar asociación de playlist previa cuando corresponda
            let prevSaved = null;
            try { prevSaved = Storage.get(video_id) || null; } catch (_) { prevSaved = null; }

            let playlistToPersist = null;
            if (finalType === 'shorts') {
                // Para Shorts, solo persistir playlist si proviene explícitamente de la URL o de un anchor del propio Short
                playlistToPersist = (plId && (playlistSource === 'url' || playlistSource === 'anchor')) ? plId : null;
            } else if (plId) {
                // Si hay un plId en la URL, usarlo
                playlistToPersist = plId;
            } else if (prevSaved?.lastViewedPlaylistId) {
                // Mantener la asociación con cualquier tipo de lista de reproducción previa
                playlistToPersist = prevSaved.lastViewedPlaylistId;
                log('updateStatus', `Manteniendo asociación con lista de reproducción: ${playlistToPersist}`);
            }

            const videoData = {
                videoId: video_id,
                timestamp: currentTime,
                lastUpdated: now,
                videoType: finalType,
                isCompleted: isFinished,
                duration: normalizedDuration,
                ...safeVideoInfo,
                // Metadatos de playlist (estilo FreeTube)
                lastViewedPlaylistId: playlistToPersist,
                lastViewedPlaylistType: playlistToPersist ? ((type === 'shorts') ? '' : (prevSaved?.lastViewedPlaylistType || 'channel')) : '', // 'channel', 'user', o vacío
                lastViewedPlaylistItemId: null // YouTube no proporciona itemId, FreeTube lo usa internamente
            };
            videoData.thumb = `https://i.ytimg.com/vi/${video_id}/maxresdefault.jpg`;

            try {
                const channelUrl = videoData.authorId ? `https://www.youtube.com/channel/${videoData.authorId}` : '';
                log('updateStatus', 'Canal final para guardar', {
                    author: videoData.author,
                    authorId: videoData.authorId,
                    channelUrl: channelUrl || '(none)'
                });
            } catch (_) { }

            // Siempre guardar con video_id como clave
            Storage.set(video_id, videoData);
            log('updateStatus', `Datos guardados para video ${video_id}${plId ? ` (playlist: ${plId})` : ''}:`, videoData);

            // Si hay playlist, obtener su nombre de manera asíncrona y guardarlo en metadata de playlist
            if (playlistToPersist) {
                // Guardar metadata de la playlist por separado (para referencia)
                const playlistMetaKey = `playlist_meta_${playlistToPersist}`;
                let playlistMeta = Storage.get(playlistMetaKey) || {
                    playlistId: playlistToPersist,
                    title: '',
                    lastWatchedVideoId: video_id,
                    lastUpdated: now
                };

                playlistMeta.lastWatchedVideoId = video_id;
                playlistMeta.lastUpdated = now;
                Storage.set(playlistMetaKey, playlistMeta);

                if (!playlistMeta.title) {
                    log('updateStatus', `Playlist ${playlistToPersist} sin título, buscando...`);
                    getPlaylistName(playlistToPersist).then(name => {
                        const updated = Storage.get(playlistMetaKey);
                        if (updated && !updated.title) {
                            updated.title = name;
                            Storage.set(playlistMetaKey, updated);
                        }
                    });
                }

                return { success: true, video_id, timestamp: videoData.timestamp, playlistId: playlistToPersist };
            } else {
                return { success: true, video_id, timestamp: videoData.timestamp };
            }

        }).catch(error => {
            conError('updateStatus', 'Error en getVideoInfo:', error);
            return { success: false, reason: 'get_video_info_error', error };
        });
    };

    const resumePlayback = async (player, vid, videoEl, savedData, type) => {
        log('resumePlayback', 'Llamado con:', { player, vid, videoEl, savedData, type });
        if (!savedData || !vid) {
            log('resumePlayback', '⚠️ No se encontró información para reanudar o video_id inválido.');
            return;
        }

        const lastTime = savedData.timestamp;
        const forceTime = savedData.forceResumeTime;
        let timeToSeek = forceTime > 0 ? forceTime : lastTime;

        log('resumePlayback', `🎬 Reanudando video ${vid} en ${timeToSeek}s = ${(timeToSeek / 60).toFixed(2)} minutos`);
        try {
            // Evitar adoptar shorts-player fuera de Shorts
            const co = YTHelper?.player?.playerObject;
            const ve = YTHelper?.player?.videoElement;
            const pt0 = getYouTubePageType();
            const isCoShorts = (co?.id || co?.getAttribute?.('id')) === 'shorts-player';
            const isVeShorts = (() => { try { return ve?.closest?.('#shorts-player') != null; } catch (_) { return false; } })();
            if (co && (!isCoShorts || pt0 === 'shorts')) player = co;
            if (ve && (!isVeShorts || pt0 === 'shorts')) videoEl = ve;
        } catch (_) { }
        if (isNavigating) {
            log('resumePlayback', '⏸ Navegación en curso, posponiendo reanudación...');
            const cb = async () => {
                try {
                    const newEl = await getActiveVideoElement();
                    const newPlayer = YTHelper?.player?.playerObject || player;
                    setTimeout(() => { try { resumePlayback(newPlayer, vid, newEl || videoEl, savedData, type); } catch (_) { } }, 0);
                } catch (_) { }
            };
            window.addEventListener('yt-navigate-finish', cb, { once: true });
            return;
        }

        if (!timeToSeek || timeToSeek <= 1) {
            log('resumePlayback', '⏩ No hay tiempo válido para reanudar');
            return;
        }

        const container = YTHelper?.player?.playerObject || player?.playerContainer_?.children?.[0] || null;
        const hasAdClasses = () => !!(container?.classList?.contains('ad-showing') || container?.classList?.contains('ad-interrupting'));
        const isReadyNoAds = () => !isAdPlaying && !isScriptPaused && !hasAdClasses();

        const waitForNoAds = () => new Promise((resolve) => {
            try {
                if (isReadyNoAds()) return resolve();
                let done = false;
                const cleanup = () => {
                    if (done) return;
                    done = true;
                    observer?.disconnect();
                    YTHelper?.eventTarget?.removeEventListener?.('yt-helper-api-ready', onApiReady);
                };
                const onApiReady = (evt) => {
                    try {
                        const playingAds = evt?.detail?.player?.isPlayingAds === true;
                        if (!playingAds && isReadyNoAds()) {
                            cleanup();
                            resolve();
                        }
                    } catch (_) { }
                };
                const observer = container ? new MutationObserver(() => {
                    if (isReadyNoAds()) {
                        cleanup();
                        resolve();
                    }
                }) : null;
                if (observer && container) observer.observe(container, { attributes: true, attributeFilter: ['class'] });
                YTHelper?.eventTarget?.addEventListener?.('yt-helper-api-ready', onApiReady);
            } catch (_) {
                resolve();
            }
        });

        const waitForDuration = () => new Promise((resolve) => {
            const hasDuration = () => {
                try { return getVideoDuration(player, videoEl) > 0; } catch (_) { return false; }
            };
            if (hasDuration()) return resolve();
            const cleanup = () => {
                videoEl?.removeEventListener('loadedmetadata', onReady);
                videoEl?.removeEventListener('durationchange', onReady);
                videoEl?.removeEventListener('canplay', onReady);
                videoEl?.removeEventListener('loadeddata', onReady);
            };
            const onReady = () => {
                if (hasDuration()) { cleanup(); resolve(); }
            };
            videoEl?.addEventListener('loadedmetadata', onReady);
            videoEl?.addEventListener('durationchange', onReady);
            videoEl?.addEventListener('canplay', onReady);
            videoEl?.addEventListener('loadeddata', onReady);
        });

        await Promise.all([waitForNoAds(), waitForDuration()]);
        try { player = YTHelper?.player?.playerObject || player; videoEl = YTHelper?.player?.videoElement || videoEl; } catch (_) { }
        try {
            const pt = getYouTubePageType();
            if (pt === 'watch') {
                const mp = document.querySelector('#movie_player');
                if (mp?.getDuration) player = mp;
                const ve = document.querySelector('#movie_player video.html5-main-video');
                if (ve) videoEl = ve;
            } else if (pt === 'home') {
                // En Home (miniplayer), preferir movie_player y su <video>
                const mpH = document.querySelector('#movie_player');
                if (mpH?.getDuration) player = mpH;
                const veH = document.querySelector('#movie_player video.html5-main-video') || videoEl;
                if (veH) videoEl = veH;
            }
        } catch (_) { }

        const duration = getVideoDuration(player, videoEl);
        if (duration > 0 && timeToSeek >= duration) {
            const maxSeek = Math.max(0, duration - 1);
            const originalTime = timeToSeek;
            timeToSeek = maxSeek;
            warn('resumePlayback', `⚠️ Timestamp inválido: ${originalTime}s excede duración ${duration}s. Ajustando a ${timeToSeek}s.`);
        }

        log('resumePlayback', `✅ Player listo (event-driven), aplicando seek a ${timeToSeek}s`);
        await applySeek(player, videoEl, timeToSeek, {
            bypassMinDiff: forceTime > 0,
            isForced: forceTime > 0,
            type,
            targetVideoId: vid,
            retryOnFail: true,
            resumeCallback: () => {
                try {
                    const newPlayer = YTHelper?.player?.playerObject || player;
                    const newEl = YTHelper?.player?.videoElement || videoEl;
                    setTimeout(() => {
                        try { resumePlayback(newPlayer, vid, newEl, savedData, type); } catch (_) { }
                    }, 200);
                } catch (_) { }
            }
        });

        try {
            const d = getVideoDuration(player, videoEl);
            const videoType = type === 'short' ? 'shorts' : 'watch';
            updateProgressBarGradient(timeToSeek, d, videoType);
        } catch (_) { }

        try {
            const saveResult = await updateStatus(player, videoEl, type, savedData?.lastViewedPlaylistId || null, vid);
            if (saveResult?.success) {
                log('resumePlayback', `💾 Progreso guardado post-resume para ${vid}`);
                lastSaveTime = Date.now();
            } else {
                log('resumePlayback', `⚠️ No se guardó post-resume para ${vid}:`, saveResult?.reason);
            }
        } catch (e) {
            conError('resumePlayback', 'Error guardando post-resume:', e);
        }

        // Asegurar re-enlace del handler de timeupdate al <video> actual tras el seek
        try {
            setTimeout(async () => {
                try {
                    const freshPlayer = YTHelper?.player?.playerObject || player;
                    const freshEl = await getActiveVideoElement();
                    if (freshPlayer && (freshEl || videoEl)) {
                        try { processVideo(freshPlayer, freshEl || videoEl); } catch (_) { }
                    }
                } catch (_) { }
            }, 500);
        } catch (_) { }
    };

    let timeDisplay;
    let clearMessageTimeout = null;
    let shortsTimeDisplay;
    let clearShortsMessageTimeout = null;
    let lastShortsMessageHtml = '';
    let shortsPanelObserver = null;
    let lastShortsReattachTs = 0;
    let lastHandlerPerfLog = 0;
    let lastAdCheckLog = 0;
    let lastAdStateLog = 0;
    let lastDurationLog = 0;
    let timeupdateRebindIntervalId = null;
    let progressPollIntervalId = null;
    let secondaryProgressPollIntervalId = null;
    let lastTimeUpdateTick = 0;

    // Inicializa la visualización de tiempo en la barra de reproducción
    function initTimeDisplay() {
        const timeContainer = document.querySelector('.ytp-time-contents');
        log('initTimeDisplay', 'timeContainer encontrado:', timeContainer);

        // Verificar si timeDisplay existe pero fue removido del DOM (ej: después de un anuncio)
        if (timeDisplay && !document.contains(timeDisplay)) {
            log('initTimeDisplay', 'timeDisplay existe pero no está en el DOM, reinicializando...');
            timeDisplay = null;
        }

        if (!timeContainer || timeDisplay) return;
        timeDisplay = createElement('span', {
            className: 'ypp-time-display ypp-d-none',
            onClickEvent: showSavedVideosList,
            atribute: { title: `${t('savedVideos')}` }
        });
        timeContainer.appendChild(timeDisplay);
        log('initTimeDisplay', 'Creada visualización de tiempo en la barra de reproducción');
    }

    /**
     * Determina si un elemento está visible en el layout (no display:none/visibility:hidden, con tamaño > 0)
     * @param {HTMLElement} el
     * @returns {boolean}
     */
    function isVisiblyDisplayed(el) {
        if (!el || !el.isConnected) return false;
        try {
            const rect = el.getBoundingClientRect();
            const style = window.getComputedStyle(el);
            const visible = rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity || '1') > 0;
            return visible;
        } catch (_) {
            return !!el.offsetWidth && !!el.offsetHeight;
        }
    }

    /**
     * Obtiene el contenedor de controles del Short activo (#metapanel)
     * Intenta seleccionar el overlay del Short actualmente activo, con fallbacks seguros.
     * @returns {HTMLElement|null} Contenedor de controles del Short activo o null si no existe aún.
     */
    function getActiveShortsControlsContainer() {
        try {
            // Priorizar el overlay del reproductor de Shorts (UI visible)
            const overlayCandidates = [
                document.querySelector('ytd-reel-player-overlay-renderer #metapanel'),
                document.querySelector('#shorts-player #metapanel'),
                document.querySelector('ytd-shorts #metapanel'),
            ];
            for (const c of overlayCandidates) {
                if (isVisiblyDisplayed(c)) return c;
            }

            // Como último recurso, usar el metapanel del item activo en el feed
            const activeFeed = document.querySelector('ytd-reel-video-renderer[is-active] #metapanel');
            if (isVisiblyDisplayed(activeFeed)) return activeFeed;

            // Fallback genérico
            const any = document.querySelector('#metapanel');
            return any || null;
        } catch (_) {
            return document.querySelector('#metapanel');
        }
    }

    /**
     * Inicializa la visualización de tiempo para videos Shorts
     */
    function initShortsTimeDisplay() {
        // Buscar el contenedor de controles dentro del Short ACTIVO
        const shortsPlayerControls = getActiveShortsControlsContainer();
        const overlayRoot = document.querySelector('ytd-reel-player-overlay-renderer') ||
            document.querySelector('#shorts-player') ||
            document.querySelector('ytd-shorts');
        log('initShortsTimeDisplay', 'shortsPlayerControls encontrado:', shortsPlayerControls)

        // Si el metapanel aún no existe, preparar fallback flotante en overlay/body
        if (!shortsPlayerControls) {
            if (!shortsTimeDisplay) {
                shortsTimeDisplay = createElement('div', {
                    className: 'ypp-shorts-time-display ypp-d-none',
                    onClickEvent: showSavedVideosList,
                    atribute: { title: `${t('savedVideos')}` }
                });
            }
            if (!shortsTimeDisplay.isConnected) {
                try { (overlayRoot || document.body).appendChild(shortsTimeDisplay); } catch (_) { }
            }
            try { shortsTimeDisplay.classList.add('ypp-floating'); } catch (_) { }
            // Iniciar observador para re-ancorar cuando aparezca el metapanel
            startShortsPanelObserver();
            log('initShortsTimeDisplay', 'Metapanel no disponible; usando fallback flotante en overlay/body');
            return;
        }

        // Si ya existe el display, asegurar que esté en el contenedor activo
        if (shortsTimeDisplay && shortsTimeDisplay.parentElement !== shortsPlayerControls) {
            try { shortsPlayerControls.appendChild(shortsTimeDisplay); } catch (_) { }
        }

        // Crear contenedor específico para Shorts si no existe
        if (!shortsTimeDisplay) {
            shortsTimeDisplay = createElement('div', {
                className: 'ypp-shorts-time-display ypp-d-none',
                onClickEvent: showSavedVideosList,
                atribute: {
                    title: `${t('savedVideos')}`,
                }
            });
        }

        // Añadir al contenedor del player de Shorts si aún no está conectado
        if (!shortsTimeDisplay.isConnected) {
            shortsPlayerControls.appendChild(shortsTimeDisplay);
        }

        log('initShortsTimeDisplay', 'Creada visualización de tiempo para Shorts dentro del player');

        // Iniciar observador de cambios en la UI de Shorts para re-anclar si cambia el Short activo
        startShortsPanelObserver();
    }

    /**
     * Inicia un MutationObserver para detectar cambios en el overlay de Shorts y re-ancorar el display.
     */
    function startShortsPanelObserver() {
        try { if (shortsPanelObserver) return; } catch (_) { }
        const target = document.querySelector('ytd-reel-player-overlay-renderer') ||
            document.querySelector('ytd-shorts') ||
            document.querySelector('#shorts-player') ||
            document.body;
        if (!target) return;

        shortsPanelObserver = new MutationObserver((mutations) => {
            let shouldReattach = false;
            for (const m of mutations) {
                if (m.type === 'childList') {
                    // Cambios de hijos suelen indicar cambio de Short activo
                    shouldReattach = true; break;
                }
                if (m.type === 'attributes') {
                    const tn = (m.target?.tagName || '').toLowerCase();
                    if (tn.includes('reel') || tn.includes('short')) { shouldReattach = true; break; }
                    if (m.attributeName === 'is-active' || m.attributeName === 'class' || m.attributeName === 'hidden') { shouldReattach = true; break; }
                }
            }
            if (!shouldReattach) return;

            const now = Date.now();
            if (now - lastShortsReattachTs < 100) return; // throttle
            lastShortsReattachTs = now;

            try {
                const panel = getActiveShortsControlsContainer();
                if (panel && shortsTimeDisplay && shortsTimeDisplay.parentElement !== panel) {
                    panel.appendChild(shortsTimeDisplay);
                }
                if (shortsTimeDisplay && lastShortsMessageHtml) {
                    setInnerHTML(shortsTimeDisplay, lastShortsMessageHtml);
                    shortsTimeDisplay.classList.remove('ypp-d-none');
                }
            } catch (_) { }
        });

        try {
            shortsPanelObserver.observe(target, {
                subtree: true,
                childList: true,
                attributes: true,
                attributeFilter: ['is-active', 'class', 'hidden']
            });
        } catch (_) { shortsPanelObserver = null; }
    }

    /**
     * Detiene el observer del panel de Shorts si está activo.
     */
    function stopShortsPanelObserver() {
        try { shortsPanelObserver?.disconnect?.(); } catch (_) { }
        shortsPanelObserver = null;
    }

    // ------------------------------------------
    // MARK: 📢 Playback Bar Messages
    // ------------------------------------------
    /**
    * Actualiza el mensaje en la barra de reproducción
    * @param {string} message - Mensaje a mostrar en la barra de reproducción
    */
    function updatePlaybackBarMessage(message) {
        // Verificar si timeDisplay existe y está en el DOM, si no, reinicializar
        if (!timeDisplay || !document.contains(timeDisplay)) {
            initTimeDisplay();
        }

        if (!timeDisplay) {
            warn('updatePlaybackBarMessage', '⚠️ No se pudo inicializar timeDisplay');
            return;
        }
        setInnerHTML(timeDisplay, message);
        timeDisplay.classList.remove('ypp-d-none');

        // Limpiar timeout anterior si existe
        if (clearMessageTimeout) {
            clearTimeout(clearMessageTimeout);
        }

        // No programar limpieza automática para mensajes seek si el video está pausado
        const isSeekMessage = !!message.includes('svgPlayOrPauseIcon')
        const activeVideoEl = currentVideoEl || getActiveVideoElement();
        const isVideoPaused = activeVideoEl?.paused || false;
        log('updatePlaybackBarMessage', `🔍 Estado: videoPaused=${isVideoPaused}, isSeekMessage=${isSeekMessage}, currentVideoEl=${!!currentVideoEl}`);

        if (isSeekMessage && isVideoPaused) return;

        // Programar limpieza automática del mensaje para evitar que quede pegado
        try {
            clearMessageTimeout = setTimeout(() => {
                try { clearPlaybackBarMessage(); } catch (_) { }
            }, 1600);
        } catch (_) { }
    }

    function clearPlaybackBarMessage() {
        if (timeDisplay) {
            setInnerHTML(timeDisplay, '');
            timeDisplay.classList.add('ypp-d-none');
        }

        // Limpiar timeout si existe
        if (clearMessageTimeout) {
            clearTimeout(clearMessageTimeout);
            clearMessageTimeout = null;
        }
    }

    // MARK: 📢 Shorts Messages
    /**
    * Actualiza el mensaje para videos Shorts
    * @param {string} message - Mensaje a mostrar en Shorts
    */
    function updateShortsMessage(message) {
        // Verificar si shortsTimeDisplay existe y está en el DOM, si no, reinicializar
        if (!shortsTimeDisplay || !document.contains(shortsTimeDisplay)) {
            initShortsTimeDisplay();
        }

        if (!shortsTimeDisplay) {
            warn('updateShortsMessage', '⚠️ No se pudo inicializar el display de Shorts');
            return;
        }

        // Asegurar que el observador esté activo aunque el display existiera previamente
        try { startShortsPanelObserver(); } catch (_) { }

        // Re-anclar al contenedor del Short activo si cambió por scroll
        const activePanel = getActiveShortsControlsContainer();
        const overlayRoot = document.querySelector('ytd-reel-player-overlay-renderer') || document.querySelector('#shorts-player') || document.querySelector('ytd-shorts');
        if (activePanel && shortsTimeDisplay.parentElement !== activePanel) {
            try { activePanel.appendChild(shortsTimeDisplay); } catch (_) { }
        }
        // Si aún no existe o no es visible (DOM en transición), reintentar en el próximo frame
        if (!activePanel || !isVisiblyDisplayed(activePanel)) {
            try {
                const reattach = () => {
                    const p = getActiveShortsControlsContainer();
                    if (p && isVisiblyDisplayed(p)) {
                        try { p.appendChild(shortsTimeDisplay); } catch (_) { }
                        shortsTimeDisplay.classList.remove('ypp-floating');
                    } else if (overlayRoot) {
                        try { overlayRoot.appendChild(shortsTimeDisplay); } catch (_) { }
                        shortsTimeDisplay.classList.add('ypp-floating');
                    } else {
                        return;
                    }
                    lastShortsMessageHtml = message;
                    setInnerHTML(shortsTimeDisplay, message);
                    shortsTimeDisplay.classList.remove('ypp-d-none');
                    // Post-check en el siguiente frame: si aún no es visible, forzar fallback al overlayRoot
                    const postCheck = () => {
                        try {
                            const stillHidden = !isVisiblyDisplayed(shortsTimeDisplay);
                            if (stillHidden && overlayRoot) {
                                try { overlayRoot.appendChild(shortsTimeDisplay); } catch (_) { }
                                shortsTimeDisplay.classList.add('ypp-floating');
                                shortsTimeDisplay.classList.remove('ypp-d-none');
                                setInnerHTML(shortsTimeDisplay, message);
                            }
                        } catch (_) { }
                    };
                    if (typeof requestAnimationFrame === 'function') {
                        requestAnimationFrame(postCheck);
                    } else {
                        setTimeout(postCheck, 50);
                    }
                };
                if (document.visibilityState === 'visible' && typeof requestAnimationFrame === 'function') {
                    requestAnimationFrame(reattach);
                } else {
                    setTimeout(reattach, 50);
                }
            } catch (_) { }
            return;
        }

        lastShortsMessageHtml = message;
        setInnerHTML(shortsTimeDisplay, message);
        shortsTimeDisplay.classList.remove('ypp-d-none');
        // Si está en overlayRoot (no metapanel visible), marcar flotante
        try {
            if (shortsTimeDisplay.parentElement && shortsTimeDisplay.parentElement !== activePanel) {
                shortsTimeDisplay.classList.add('ypp-floating');
            } else {
                shortsTimeDisplay.classList.remove('ypp-floating');
            }
        } catch (_) { }

        // Limpiar timeout anterior si existe
        if (clearShortsMessageTimeout) {
            clearTimeout(clearShortsMessageTimeout);
        }

        // No programar limpieza automática para mensajes seek si el video está pausado
        const isSeekMessage = !!message.includes('svgPlayOrPauseIcon')
        const activeVideoEl = currentVideoEl || getActiveVideoElement();
        const isVideoPaused = activeVideoEl?.paused || false;
        log('updateShortsMessage', `🔍 Estado: videoPaused=${isVideoPaused}, isSeekMessage=${isSeekMessage}, currentVideoEl=${!!currentVideoEl}`);

        if (isSeekMessage && isVideoPaused) return;

        // Programar limpieza automática del mensaje para evitar que quede pegado
        try {
            const baseMinSeconds = cachedSettings?.minSecondsBetweenSaves || CONFIG.defaultSettings.minSecondsBetweenSaves || 1;
            const ttlMs = Math.max((baseMinSeconds * 1000) + 1500, 1600);
            clearShortsMessageTimeout = setTimeout(() => {
                try { clearShortsMessage(); } catch (_) { }
            }, ttlMs);
        } catch (_) { }
    }

    function clearShortsMessage() {
        if (shortsTimeDisplay) {
            setInnerHTML(shortsTimeDisplay, '');
            shortsTimeDisplay.classList.add('ypp-d-none');
            shortsTimeDisplay.classList.remove('ypp-floating');
            // Al limpiar el mensaje, también vaciar el caché para evitar reusar mensajes de Shorts anteriores
            lastShortsMessageHtml = '';
        }

        // Limpiar timeout si existe
        if (clearShortsMessageTimeout) {
            clearTimeout(clearShortsMessageTimeout);
            clearShortsMessageTimeout = null;
        }
    }

    // ------------------------------------------
    // MARK: 🍞 Toasts
    // ------------------------------------------

    const toastTimeouts = new WeakMap();

    function createToastContainer() {
        let container = document.querySelector('.ypp-toast-container');
        if (!container) {
            container = createElement('div', { className: 'ypp-toast-container' });
            document.body.appendChild(container);
            log('createToastContainer', 'Contenedor de toasts creado');
        }

        return container;
    }

    /**
    * Desvanece y elimina un toast después de un tiempo.
    * @param {HTMLElement} toast - Elemento toast a eliminar.
    * @param {number} duration - Tiempo en ms antes de iniciar el fade out.
    */
    function fadeAndRemoveToast(toast, duration) {
        // Limpiar timeout previo si existe
        if (toastTimeouts.has(toast)) {
            clearTimeout(toastTimeouts.get(toast));
            toastTimeouts.delete(toast);
        }

        const timeoutId = setTimeout(() => {
            // Desactivar interacción y lanzar fade
            toast.style.pointerEvents = 'none';
            toast.style.opacity = '0';

            const container = toast.parentElement;
            let cleanupTimer = null;

            const onTransitionEnd = () => {
                toast.removeEventListener('transitionend', onTransitionEnd);
                if (cleanupTimer) {
                    clearTimeout(cleanupTimer);
                    cleanupTimer = null;
                }
                if (toast.isConnected) {
                    toast.remove();
                }
                // Si el contenedor queda vacío, eliminarlo
                if (container && container.children.length === 0) {
                    container.remove();
                }
            };

            toast.addEventListener('transitionend', onTransitionEnd);

            // Fallback por si transitionend no dispara (seguridad)
            cleanupTimer = setTimeout(onTransitionEnd, 600);

            toastTimeouts.delete(toast);
        }, duration);

        toastTimeouts.set(toast, timeoutId);
    }

    /**
    * Muestra un toast flotante.
    * @param {string} message - Texto del toast.
    * @param {number} [duration=2500] - Duración en ms del toast temporal.
    * @param {Object} [options={}] - Opciones:
    *   - persistent: boolean (reutiliza un toast único)
    *   - keep: boolean (no se auto elimina)
    *   - action: { label: string, callback: function }
    */
    function showFloatingToast(message, duration = 2500, options = {}) {
        const container = createToastContainer();
        let toast;

        if (options.persistent) {
            toast = container.querySelector('.ypp-toast.persistent');
            if (!toast) {
                toast = createElement('div', { className: 'ypp-toast persistent sombra' });
                container.appendChild(toast);
            }
            // Resetear contenido y estilo
            setInnerHTML(toast, '');
            toast.style.opacity = '1';
        } else {
            toast = createElement('div', { className: 'ypp-toast sombra' });
            if (options.action) toast.classList.add('has-action');
            container.appendChild(toast);
            // Inicializar opacity 0 antes de animar
            toast.style.opacity = '0';
            requestAnimationFrame(() => (toast.style.opacity = '1'));
        }

        // Contenido
        const messageSpan = createElement('span', { html: message });
        toast.appendChild(messageSpan);

        if (options.action) {
            const actionBtn = createElement('button', {
                className: 'ypp-toast-action',
                text: options.action.label,
                onClickEvent: () => {
                    if (typeof options.action.callback === 'function') {
                        options.action.callback();
                    }
                    fadeAndRemoveToast(toast, 0);
                },
                atribute: { 'aria-label': options.action.label, type: 'button' }
            });
            toast.appendChild(actionBtn);
        }

        // Agregar botón de cerrar para toasts persistentes
        if (options.persistent) {
            const closeBtn = createElement('button', {
                className: 'ypp-toast-close',
                html: SVG_ICONS.close,
                atribute: { 'aria-label': t('close'), title: t('close'), type: 'button' },
                onClickEvent: () => {
                    fadeAndRemoveToast(toast, 0);
                }
            });
            toast.appendChild(closeBtn);
        }

        if (!options.keep && !options.persistent) fadeAndRemoveToast(toast, duration);

        log('showFloatingToast', 'Toast mostrado', { message, options });
    }

    // ------------------------------------------
    // MARK: 🛠 showSettingsUI
    // ------------------------------------------

    function showSettingsUI() {
        // Ocultar modal de videos si existe, pero no eliminarlo
        let wasVideosModalOpen = false;
        if (videosOverlay && videosContainer) {
            videosOverlay.style.display = 'none';
            videosContainer.style.display = 'none';
            wasVideosModalOpen = true;
        }

        // Cerrar otros modales que no sean el de videos
        const existingModals = document.querySelectorAll('.ypp-modalOverlay');
        existingModals.forEach(modal => {
            if (modal !== videosOverlay) {
                modal.remove();
            }
        });

        const closeModal = () => {
            overlay.remove();
            document.body.style.overflow = '';

            // Restaurar modal de videos si estaba abierto
            if (wasVideosModalOpen && videosOverlay && videosContainer) {
                videosOverlay.style.display = '';
                videosContainer.style.display = '';
            }
        };

        // Crear overlay con tema dinámico
        const overlay = createElement('div', {
            className: 'ypp-modalOverlay',
            atribute: { 'aria-modal': 'true', role: 'dialog' },
            onClickEvent: (e) => {
                if (e.target === overlay) closeModal();
            }
        });

        // Aplicar tema dinámico

        // Crear modal
        const modal = createElement('div', { className: 'ypp-modalBox' });
        const header = createElement('div', { className: 'ypp-modalHeader' });
        const titleEl = createElement('h3', { className: 'ypp-modalTitle', text: `⚙️ ${t('settings')}` });
        const closeBtn = createElement('button', {
            className: 'ypp-btn ypp-btn-small ypp-btn-close',
            html: SVG_ICONS.close,
            atribute: { 'aria-label': t('close'), title: t('close'), type: 'button' },
            onClickEvent: closeModal
        });
        header.appendChild(titleEl);
        header.appendChild(closeBtn);

        // Crear body con scroll
        const body = createElement('div', { className: 'ypp-modalBody' });
        const content = createElement('div', { className: 'ypp-settingsContent' });

        // Contenido de configuración existente...
        const toggleNotif = createElement('input', { atribute: { type: 'checkbox', id: 'showNotifications' } });
        const toggleButtons = createElement('input', { atribute: { type: 'checkbox', id: 'showFloatingButtons' } });
        const toggleProgressBarGradient = createElement('input', { atribute: { type: 'checkbox', id: 'enableProgressBarGradient' } });
        const intervalInput = createElement('input', { className: 'ypp-input-small', atribute: { type: 'number', id: 'minSeconds', min: 1, max: 9999 } });
        const staticFinishPercentInput = createElement('input', { className: 'ypp-input-small', atribute: { type: 'number', id: 'staticFinishPercent', min: 1, max: 99 } });
        const languageSelect = createElement('select', { className: 'ypp-select', id: 'languageSelect' });
        const alertStyleSelect = createElement('select', { className: 'ypp-select', id: 'alertStyleSelect' });

        // Añadir opciones de idioma
        const sortedLanguages = Object.entries(LANGUAGE_FLAGS).sort((a, b) => a[1].name.localeCompare(b[1].name));
        sortedLanguages.forEach(([code, lang]) => {
            const option = createElement('option', { atribute: { value: code }, text: `${lang.emoji || '🌐'} ${lang.name}` });
            languageSelect.appendChild(option);
        });

        // Añadir estilos de alerta
        const alertStyles = [
            { value: 'iconText', text: '🔔 Icon + Text' },
            { value: 'iconOnly', text: '🔔 Icon Only' },
            { value: 'textOnly', text: '📝 Text Only' },
            { value: 'hidden', text: '🚫 Hidden' }
        ];
        alertStyles.forEach(style => {
            const option = createElement('option', { atribute: { value: style.value }, text: style.text });
            alertStyleSelect.appendChild(option);
        });

        // Crear checkboxes
        const languageLabel = createElement('label', { className: 'ypp-label ypp-label-language' });
        languageLabel.appendChild(createElement('span', { text: `${t('language')}: ` }));
        languageLabel.appendChild(languageSelect);

        const buttonsLabel = createElement('label', { className: 'ypp-label' });
        buttonsLabel.appendChild(toggleButtons);
        buttonsLabel.appendChild(createElement('span', { text: t('showFloatingButton') }));

        const gradientLabel = createElement('label', { className: 'ypp-label' });
        gradientLabel.appendChild(toggleProgressBarGradient);
        gradientLabel.appendChild(createElement('span', { text: t('enableProgressBarGradient') }));

        const notifLabel = createElement('label', { className: 'ypp-label' });
        notifLabel.appendChild(toggleNotif);
        notifLabel.appendChild(createElement('span', { text: t('showNotifications') }));

        const intervalLabel = createElement('label', { className: 'ypp-label' });
        intervalLabel.appendChild(createElement('span', { text: `${t('minSecondsBetweenSaves')}: ` }));
        intervalLabel.appendChild(intervalInput);

        const staticFinishPercentLabel = createElement('label', { className: 'ypp-label' });
        staticFinishPercentLabel.appendChild(createElement('span', { text: `${t('staticFinishPercent')}: ` }));
        staticFinishPercentLabel.appendChild(staticFinishPercentInput);
        staticFinishPercentLabel.appendChild(createElement('span', { className: 'ypp-percent-symbol', text: `%` }));

        const alertStyleLabel = createElement('label', { className: 'ypp-label' });
        alertStyleLabel.appendChild(createElement('span', { text: `${t('alertStyle')}: ` }));
        alertStyleLabel.appendChild(alertStyleSelect);

        // Checkboxes para tipo de videos
        const savingOptions = createElement('div', { className: 'ypp-saving-options' });

        const containerSavingOptions = createElement('div', { className: 'ypp-container-saving-options' });
        const savingOptionsTitle = createElement('h2', { text: `${t('enableSavingFor')}:` });
        const saveRegularVideosLabel = createElement('label', { className: 'ypp-label-save-type' });
        const saveRegularVideosCheckbox = createElement('input', { atribute: { type: 'checkbox', id: 'saveRegularVideos' } });
        saveRegularVideosLabel.appendChild(saveRegularVideosCheckbox);
        saveRegularVideosLabel.appendChild(createElement('span', { text: t('regularVideos') }));

        const saveShortsLabel = createElement('label', { className: 'ypp-label-save-type' });
        const saveShortsCheckbox = createElement('input', { atribute: { type: 'checkbox', id: 'saveShorts' } });
        saveShortsLabel.appendChild(saveShortsCheckbox);
        saveShortsLabel.appendChild(createElement('span', { text: t('shorts') }));

        const saveLiveStreamsLabel = createElement('label', { className: 'ypp-label-save-type' });
        const saveLiveStreamsCheckbox = createElement('input', { atribute: { type: 'checkbox', id: 'saveLiveStreams' } });
        saveLiveStreamsLabel.appendChild(saveLiveStreamsCheckbox);
        saveLiveStreamsLabel.appendChild(createElement('span', { text: t('liveStreams') }));

        const saveInlinePreviewsLabel = createElement('label', { className: 'ypp-label-save-type' });
        const saveInlinePreviewsCheckbox = createElement('input', { atribute: { type: 'checkbox', id: 'saveInlinePreviews' } });
        saveInlinePreviewsLabel.appendChild(saveInlinePreviewsCheckbox);
        saveInlinePreviewsLabel.appendChild(createElement('span', { text: t('inlinePreviews') }));

        // Añadir todo al contenido
        const buttonsGroup = createElement('div');
        buttonsGroup.appendChild(languageLabel); //
        buttonsGroup.appendChild(buttonsLabel);
        buttonsGroup.appendChild(gradientLabel);

        containerSavingOptions.appendChild(savingOptionsTitle)
        containerSavingOptions.appendChild(saveRegularVideosLabel);
        containerSavingOptions.appendChild(saveShortsLabel);
        containerSavingOptions.appendChild(saveLiveStreamsLabel);
        containerSavingOptions.appendChild(saveInlinePreviewsLabel);

        savingOptions.appendChild(containerSavingOptions)
        savingOptions.appendChild(notifLabel);
        savingOptions.appendChild(intervalLabel);
        savingOptions.appendChild(staticFinishPercentLabel);
        savingOptions.appendChild(alertStyleLabel);
        buttonsGroup.appendChild(savingOptions);

        content.appendChild(buttonsGroup);
        body.appendChild(content);

        // Crear footer fijo
        const footer = createElement('div', { className: 'ypp-btnGroup' });
        const saveBtn = createElement('button', {
            className: 'ypp-btn ypp-save-button ypp-sombra',
            id: 'saveBtn',
            text: t('save'),
            onClickEvent: async () => {
                const newSettings = {
                    showNotifications: toggleNotif.checked,
                    minSecondsBetweenSaves: Math.max(1, parseInt(intervalInput.value, 10)),
                    showFloatingButtons: toggleButtons.checked,
                    enableProgressBarGradient: toggleProgressBarGradient.checked,
                    staticFinishPercent: Math.max(1, Math.min(99, parseInt(staticFinishPercentInput.value, 10))),
                    saveRegularVideos: document.getElementById('saveRegularVideos').checked,
                    saveShorts: document.getElementById('saveShorts').checked,
                    saveLiveStreams: document.getElementById('saveLiveStreams').checked,
                    saveInlinePreviews: document.getElementById('saveInlinePreviews').checked,
                    language: languageSelect.value,
                    alertStyle: alertStyleSelect.value,
                };
                await Settings.set(newSettings);
                // Actualizar cache inmediatamente para evitar problemas de timing
                cachedSettings = newSettings;
                await setLanguage(languageSelect.value);
                showFloatingToast(`${SVG_ICONS.check} ${t('configurationSaved')}`);
                location.reload();
            }
        });
        const viewBtn = createElement('button', {
            className: 'ypp-btn ypp-btn-outlined ypp-sombra',
            id: 'viewSavedBtn',
            text: `${t('savedVideos')}`,
            onClickEvent: () => {
                overlay.remove();
                showSavedVideosList();
            }
        });
        footer.appendChild(viewBtn);
        footer.appendChild(saveBtn);

        // Ensamblar modal
        modal.appendChild(header);
        modal.appendChild(body);
        modal.appendChild(footer);
        overlay.appendChild(modal);

        // Cargar configuración actual
        Settings.get().then(settings => {
            toggleNotif.checked = settings.showNotifications;
            toggleButtons.checked = settings.showFloatingButtons;
            toggleProgressBarGradient.checked = settings.enableProgressBarGradient;
            intervalInput.value = settings.minSecondsBetweenSaves;
            staticFinishPercentInput.value = settings.staticFinishPercent;
            languageSelect.value = settings.language;
            alertStyleSelect.value = settings.alertStyle;
            saveRegularVideosCheckbox.checked = settings.saveRegularVideos;
            saveShortsCheckbox.checked = settings.saveShorts;
            saveLiveStreamsCheckbox.checked = settings.saveLiveStreams;
            const inlineEl = document.getElementById('saveInlinePreviews');
            if (inlineEl) inlineEl.checked = settings.saveInlinePreviews === true;
        });

        // Añadir al DOM
        document.body.appendChild(overlay);
        document.body.style.overflow = 'hidden';
    }

    // ------------------------------------------
    // MARK: 📢 Notify Seek or Progress
    // ------------------------------------------

    let cachedSettings = null;

    /**
    * Notifica al usuario sobre el progreso guardado o la posición de seek (reanudación)
    * @param {number} time - Tiempo en segundos
    * @param {string} context - 'seek' o 'progress'
    * @param {object} options - Opciones adicionales
    *      @param {boolean} options.isForced - Indica si el seek fue forzado
    *      @param {object} options.saveResult - Resultado del guardado (solo para context='progress')
    */
    async function notifySeekOrProgress(time, context = 'progress', options = {}) {
        log('notifySeekOrProgress', 'Llamado con:', { time, context, options });

        // Para progreso, verificar que realmente se guardó
        if (context === 'progress') {
            if (!options.saveResult || !options.saveResult.success) {
                log('notifySeekOrProgress', 'No se notifica progreso: guardado fallido o no confirmado', options.saveResult);
                return;
            }
            log('notifySeekOrProgress', '✅ Progreso guardado confirmado, procediendo con notificación');
        }

        if (!cachedSettings) {
            try {
                cachedSettings = await Settings.get();
            } catch (error) {
                conError('notifySeekOrProgress', 'Error al cargar configuración para notificaciones (usaran defaults):', error);
                cachedSettings = CONFIG.defaultSettings;
            }
        }

        if (cachedSettings.showNotifications === false || cachedSettings.alertStyle === 'hidden') {
            log('notifySeekOrProgress', 'Notificaciones deshabilitadas o estilo oculto, no se muestra mensaje');
            return;
        }

        // Bloquear notificación de progreso si hay tiempo fijo o si hay un mensaje seek activo y video está pausado
        if (context === 'progress') {
            // Opción A: si estamos en Shorts y el guardado proviene del miniplayer (watch), no mostrar notificación
            if (currentPageType === 'shorts' && options?.videoType === 'watch') {
                return;
            }
            const videoId = YTHelper?.video.id || extractOrNormalizeVideoId(location.href)?.id;
            log('notifySeekOrProgress', 'Video ID:', videoId, 'YTHelper?.video.id:', YTHelper?.video.id, 'extractOrNormalizeVideoId(location.href)?.id:', extractOrNormalizeVideoId(location.href)?.id)

            // Usar el playlistId del saveResult si está disponible
            const playlistId = options.saveResult?.playlistId ||
                new URLSearchParams(location.search).get('list') ||
                null;
            log('notifySeekOrProgress', 'Playlist ID desde saveResult:', options.saveResult?.playlistId, 'Playlist ID desde URL:', new URLSearchParams(location.search).get('list'));

            // Pequeño retraso para permitir que el mensaje seek se establezca
            await new Promise(resolve => setTimeout(resolve, 100));

            // Verificar si hay un mensaje seek activo y el video está pausado
            // Usar el display correcto según el contexto visible (Shorts vs Watch)
            const displayType = currentPageType === 'shorts' ? 'shorts' : 'watch';
            const activeVideoEl = displayType === 'shorts' ? (await getActiveVideoElement()) : (currentVideoEl || await getActiveVideoElement());
            const isVideoPaused = activeVideoEl?.paused || false;
            const currentMessage = displayType === 'shorts' ? (shortsTimeDisplay?.innerHTML || '') : (timeDisplay?.innerHTML || '');
            const hasSeekMessage = !!currentMessage.includes('svgPlayOrPauseIcon')

            log('notifySeekOrProgress', `🔍 Estado: videoPaused=${isVideoPaused}, hasSeekMessage=${hasSeekMessage}, currentVideoEl=${!!currentVideoEl}`);

            if (displayType !== 'shorts' && hasSeekMessage && isVideoPaused) {
                log('notifySeekOrProgress', '⏸ Video pausado con mensaje seek activo, omitiendo notificación de progreso');
                return;
            }

            if (videoId) {
                const videoData = getSavedVideoData(videoId, playlistId);
                if (videoData?.forceResumeTime > 0) {
                    log('notifySeekOrProgress', 'Video con tiempo fijo, omitiendo notificación de progreso.');
                    return;
                }
            }
        }

        const { isForced = false/* , videoType = 'normal' */ } = options;
        const timeStr = formatTime(normalizeSeconds((time)));

        let icon = '';
        let text = '';

        // Preparar los textos según el contexto
        if (context === 'seek') {
            icon = isForced ? `${SVG_ICONS.timer}${SVG_ICONS.pin}` : SVG_ICONS.playOrPause;
            text = `${t(isForced ? 'alwaysStartFrom' : 'resumedAt')}: ${timeStr}`;
        } else {
            icon = SVG_ICONS.save;
            text = `${t('progressSaved')}: ${timeStr}`;
        }

        // Aplicar estilo según alertStyle
        let message = '';
        switch (cachedSettings.alertStyle) {
            case 'iconOnly':
                message = `${icon} ${timeStr}`;
                break;
            case 'textOnly':
                message = text;
                break;
            case 'iconText':
            default:
                message = `${icon} ${text}`;
                break;
        }

        // Mostrar en UI según el tipo de video realmente guardado (boundType)
        log('notifySeekOrProgress', `Mensaje generado timeStr: "${timeStr}" | Contexto: ${context} | currentPageType: ${currentPageType}`);

        // Mostrar en el contexto visible: para progreso, priorizar el tipo de página actual (evita perder el mensaje en Shorts con miniplayer activo)
        const targetType = context === 'progress' ? currentPageType : (options.videoType || currentPageType);
        if (targetType === 'shorts') {
            updateShortsMessage(message);
        } else {
            // watch/embed/home con miniplayer -> usar barra de reproducción
            updatePlaybackBarMessage(message);
        }
    }


    // ------------------------------------------
    // MARK: 🎵 Selección de Videos
    // ------------------------------------------

    let selectedVideos = new Set(); // IDs de videos seleccionados
    let isSelectionMode = false; // Modo de selección activo

    /**
     * Activa/desactiva el modo de selección de videos
     */
    function toggleSelectionMode() {
        isSelectionMode = !isSelectionMode;
        selectedVideos.clear();

        // Actualizar la interfaz
        updateVideoList();
        updateSelectionUI();

        log('toggleSelectionMode', `Modo de selección: ${isSelectionMode ? 'ACTIVADO' : 'DESACTIVADO'}`);
    }

    /**
     * Actualiza la interfaz según el modo de selección
     */
    function updateSelectionUI() {
        // Buscar el botón por su contenido o clase específica
        const createPlaylistBtn = Array.from(document.querySelectorAll('.ypp-btn')).find(btn =>
            btn.innerHTML.includes(t('createPlaylist')) || btn.innerHTML.includes(t('selectVideos'))
        );

        if (createPlaylistBtn) {
            if (isSelectionMode) {
                setInnerHTML(createPlaylistBtn, `${SVG_ICONS.close} ${t('selectVideos')} (${selectedVideos.size})`);
                createPlaylistBtn.className = 'ypp-btn ypp-btn-danger ypp-sombra';
            } else {
                setInnerHTML(createPlaylistBtn, `${SVG_ICONS.playlist} ${t('createPlaylist')}`);
                createPlaylistBtn.className = 'ypp-btn ypp-btn-primary ypp-sombra';
            }
        }

        // Mostrar/ocultar área de playlist y botones del footer
        updatePlaylistArea();
    }

    /**
     * Actualiza el área de playlist integrada
     */
    function updatePlaylistArea() {
        const playlistArea = document.getElementById('ypp-playlist-area');
        const firstRow = document.querySelector('.ypp-footer-row:first-child');
        const secondRow = document.querySelector('.ypp-footer-row:last-child');

        if (!playlistArea) return;

        if (isSelectionMode) {
            // Mostrar área de playlist y ocultar botones normales
            playlistArea.classList.add('active');
            if (playlistArea) {
                playlistArea.classList.add('active');
            }
            if (firstRow) {
                firstRow.classList.add('hidden');
            }
            if (secondRow) {
                secondRow.classList.add('hidden');
            }

            // Actualizar información si hay videos seleccionados
            if (selectedVideos.size > 0) {
                updatePlaylistContent();
            }
        } else {
            // Ocultar área de playlist y mostrar botones normales
            playlistArea.classList.remove('active');
            if (playlistArea) {
                playlistArea.classList.remove('active');
            }
            if (firstRow) {
                firstRow.classList.remove('hidden');
            }
            if (secondRow) {
                secondRow.classList.remove('hidden');
            }

            // Limpiar contenido del área de playlist
            clearPlaylistContent();
        }
    }

    /**
     * Actualiza el contenido del área de playlist
     */
    function updatePlaylistContent() {
        const playlistInfo = document.getElementById('ypp-playlist-info');
        const playlistTextarea = document.getElementById('ypp-playlist-textarea');

        if (playlistInfo) {
            playlistInfo.textContent = `${t('selectedVideos')}: ${selectedVideos.size}`;
        }

        if (playlistTextarea) {
            if (selectedVideos.size > 0) {
                const videoIds = Array.from(selectedVideos);
                const playlistUrl = `https://www.youtube.com/watch_videos?video_ids=${videoIds.join(',')}`;
                playlistTextarea.value = playlistUrl;
            } else {
                playlistTextarea.value = '';
            }
        }
    }

    /**
     * Limpia el contenido del área de playlist
     */
    function clearPlaylistContent() {
        const playlistInfo = document.getElementById('ypp-playlist-info');
        const playlistTextarea = document.getElementById('ypp-playlist-textarea');

        if (playlistInfo) {
            playlistInfo.textContent = `${t('selectedVideos')}: 0`;
        }

        if (playlistTextarea) {
            playlistTextarea.value = '';
        }
    }

    /**
     * Copia el enlace de playlist al portapapeles
     */
    function copyPlaylistLink() {
        const textarea = document.getElementById('ypp-playlist-textarea');
        if (!textarea || !textarea.value) {
            alert(t('selectAtLeastOne'));
            return;
        }

        copyToClipboard(textarea.value, document.getElementById('ypp-copy-playlist-btn'));
    }

    /**
     * Abre el enlace de playlist en una nueva pestaña
     */
    function openPlaylistLink() {
        const textarea = document.getElementById('ypp-playlist-textarea');
        if (!textarea || !textarea.value) {
            alert(t('selectAtLeastOne'));
            return;
        }

        window.open(textarea.value, '_blank');
    }

    /**
     * Copia texto al portapapeles
     */
    async function copyToClipboard(text, button) {
        try {
            await navigator.clipboard.writeText(text);
            const originalText = button.innerHTML;
            setInnerHTML(button, `${SVG_ICONS.check} ${t('linkCopied')}`);
            button.className = 'ypp-btn ypp-btn-success';

            setTimeout(() => {
                setInnerHTML(button, originalText);
                button.className = 'ypp-btn ypp-btn-primary';
            }, 2000);

            log('copyToClipboard', 'Enlace copiado al portapapeles');
        } catch (err) {
            conError('copyToClipboard', 'Error al copiar al portapapeles:', err);
            // Fallback para navegadores que no soportan clipboard API
            const textarea = document.createElement('textarea');
            textarea.value = text;
            document.body.appendChild(textarea);
            textarea.select();
            document.execCommand('copy');
            document.body.removeChild(textarea);

            setInnerHTML(button, `${SVG_ICONS.check} ${t('linkCopied')}`);
            button.className = 'ypp-btn ypp-btn-success';

            setTimeout(() => {
                setInnerHTML(button, `${SVG_ICONS.copy} ${t('copyLink')}`);
                button.className = 'ypp-btn ypp-btn-primary';
            }, 2000);
        }
    }

    /**
     * Alterna la selección de un video
     */
    function toggleVideoSelection(videoId) {
        if (selectedVideos.has(videoId)) {
            selectedVideos.delete(videoId);
            log('toggleVideoSelection', `Video ${videoId} deseleccionado`);
        } else {
            selectedVideos.add(videoId);
            log('toggleVideoSelection', `Video ${videoId} seleccionado`);
        }

        // Actualizar UI
        updateSelectionUI();

        // Actualizar el checkbox específico
        const checkbox = document.querySelector(`input[data-video-id="${videoId}"]`);
        if (checkbox) {
            checkbox.checked = selectedVideos.has(videoId);
        }

        // Actualizar contenido de playlist
        updatePlaylistContent();
    }

    // ------------------------------------------
    // MARK: 📺 Modal Videos
    // ------------------------------------------
    let currentVideoEl = null;
    let lastPlaylistId = null;
    let lastUrl = ''; // Rastrear la última URL procesada
    let lastVideoUrl = ''; // URL del último video (para miniplayer en home)
    let lastSaveTime = 0; // Para controlar la frecuencia de guardado
    let lastSaveTimesByVideoId = Object.create(null);
    let lastResumeId = null;
    let currentlyProcessingVideoId = null;
    let currentTimeUpdateHandler = null; // Referencia al manejador actual para limpieza correcta

    const processVideo = async (playerToProcess, videoEl) => {
        log('processVideo', `Llegando a processVideo con player: ${typeof playerToProcess} | ¿permite getDuration?: ${typeof playerToProcess?.getDuration === 'function'}`);
        log('processVideo', `Llegando a processVideo con videoEl: ${videoEl?.constructor?.name} | src: ${videoEl?.src || 'N/A'}`);
        log('processVideo', `isNavigating: ${isNavigating}`);
        log('processVideo', `isResuming: ${isResuming} | Video paused: ${videoEl?.paused}`);
        log('processVideo', `🔍 Estado inicial: isAdPlaying=${isAdPlaying}, isScriptPaused=${isScriptPaused}, cachedSettings=${!!cachedSettings}`);

        // CRÍTICO: Detener procesamiento solo por anuncios de watch; en Shorts continuar para permitir guardado del miniplayer
        if (isScriptPaused) {
            log('processVideo', '⏸️  ABORTANDO processVideo - Script pausado');
            return;
        }
        try {
            const pt0 = getYouTubePageType();
            if (pt0 !== 'shorts' && isAdBlockedFor('watch')) {
                log('processVideo', '⏸️  ABORTANDO processVideo - Anuncio (watch) activo');
                return;
            }
        } catch (_) { }

        // Asegurar que cachedSettings esté cargado
        if (!cachedSettings) {
            try {
                cachedSettings = await Settings.get();
            } catch (error) {
                conError('processVideo', 'Error al cargar configuración, usando defaults:', error);
                cachedSettings = CONFIG.defaultSettings;
            }
        }
        log('processVideo', `✅ cachedSettings cargado:`, cachedSettings);

        // Resetear isResuming si está stuck por mucho tiempo
        if (isResuming) {
            log('processVideo', '⚠️ isResuming estaba en true, reseteando por seguridad');
            isResuming = false;
        }

        const activeVideoCheck = await getActiveVideoElement();
        if (isNavigating && !activeVideoCheck) {
            log('processVideo', `isNavigating: ${isNavigating}`);
            log('processVideo', `getActiveVideoElement(): ${activeVideoCheck}`);
            log('processVideo', 'Navegación en curso y no se encontró elemento de video, omitiendo procesamiento de video.');
            return;
        }

        let player = playerToProcess;

        // Asegurar que los elementos existen
        if (!player || !videoEl) {
            // Intentar obtenerlos automáticamente si no se proporcionaron
            videoEl = await getActiveVideoElement();

            // Priorizar YouTube Helper API
            if (YTHelper?.apiProxy && typeof YTHelper.apiProxy.getDuration === 'function') {
                player = YTHelper.apiProxy;
                log('processVideo', '✅ Usando YTHelper.apiProxy como player fallback');
            } else {
                // Fallback a métodos antiguos
                player = window.ytplayer || window.yt || {};
                warn('processVideo', '⚠️ YouTube Helper API no disponible, usando fallback antiguo');
            }

            log('processVideo', `Player fallback tiene getDuration: ${typeof player?.getDuration} | videoEl: ${!!videoEl}`);

            if (!videoEl) {
                warn('processVideo', 'No se encontró videoEl. Abortando.');
                return;
            }
            // En home, forzar el uso de movie_player/miniplayer y su <video> asociado
            try {
                if (getYouTubePageType() === 'home') {
                    const mp = document.querySelector('#movie_player');
                    if (mp?.getDuration) player = mp;
                    const ve = document.querySelector('#movie_player video.html5-main-video') || videoEl;
                    if (ve) videoEl = ve;
                }
            } catch (_) { }
        }

        let urlForVideoId = window.location.href;
        const pageType = getYouTubePageType();
        let videoIdDetected = null;

        let urlData = extractOrNormalizeVideoId(urlForVideoId);

        if (pageType === 'watch' || pageType === 'embed' || pageType === 'playlist') {
            videoIdDetected = urlData?.id || YTHelper?.video?.id || player?.getVideoData?.()?.video_id || null;
        } else if (pageType === 'shorts') {
            try {
                const cont = activeVideoCheck?.closest?.('#movie_player, #shorts-player');
                if (cont?.id === 'movie_player') {
                    try {
                        const mp = document.querySelector('#movie_player');
                        const mpId = mp?.getVideoData?.()?.video_id;
                        if (mpId) videoIdDetected = mpId;
                    } catch (_) { }
                }
            } catch (_) { }
            // Si shorts está deshabilitado, intentar forzar el ID del miniplayer si existe
            if (!videoIdDetected) {
                try {
                    const shortsDisabled = cachedSettings?.saveShorts === false;
                    if (shortsDisabled) {
                        const mp2 = document.querySelector('#movie_player');
                        const mp2Id = mp2?.getVideoData?.()?.video_id || null;
                        if (mp2Id) videoIdDetected = mp2Id;
                    }
                } catch (_) { }
            }
            if (!videoIdDetected) {
                videoIdDetected = urlData?.id || YTHelper?.video?.id || player?.getVideoData?.()?.video_id || null;
            }
        } else if (pageType === 'home') {
            // Priorizar ID desde el movie_player (miniplayer en Home) para evitar tomar Shorts antiguos
            try {
                const mp = document.querySelector('#movie_player');
                const mpId = mp?.getVideoData?.()?.video_id;
                if (mpId) videoIdDetected = mpId;
            } catch (_) { }
            if (!videoIdDetected) {
                try { videoIdDetected = player?.getVideoData?.()?.video_id || YTHelper?.video?.id || null; } catch (_) { }
            }
            if (!videoIdDetected && lastVideoUrl && activeVideoCheck) {
                urlForVideoId = lastVideoUrl;
                urlData = extractOrNormalizeVideoId(urlForVideoId);
                log('processVideo', `📍 Usando lastVideoUrl para miniplayer: ${lastVideoUrl}`);
            }
            if (!videoIdDetected) videoIdDetected = urlData?.id;
        } else if (pageType === 'search' || pageType === 'channel') {
            // En búsqueda/canal, preferir movie_player (miniplayer) si existe
            try {
                const mp = document.querySelector('#movie_player');
                const mpId = mp?.getVideoData?.()?.video_id;
                if (mpId) videoIdDetected = mpId;
            } catch (_) { }
            if (!videoIdDetected) {
                try { videoIdDetected = player?.getVideoData?.()?.video_id || YTHelper?.video?.id || null; } catch (_) { }
            }
            if (!videoIdDetected) videoIdDetected = urlData?.id;
        } else {
            videoIdDetected = urlData?.id || YTHelper?.video?.id || player?.getVideoData?.()?.video_id || null;
        }

        if (!videoIdDetected) {
            log('processVideo', `🚨 No se pudo determinar el video_id desde ${urlForVideoId}. Abortando procesamiento temprano.`);
            return;
        }

        if (pageType !== 'home') {
            lastVideoUrl = window.location.href;
        } else {
            lastVideoUrl = `https://www.youtube.com/watch?v=${videoIdDetected}`;
        }
        log('processVideo', `URL del video guardada para miniplayer: ${lastVideoUrl}`);

        let plId = urlData?.list;
        // Intentar obtener playlistId desde el contenedor de la tarjeta actual (home/search/channel) para asociar correctamente Mix (RD...)
        if (!plId && (pageType === 'home' || pageType === 'search' || pageType === 'channel')) {
            try {
                const elCtx = (currentVideoEl || videoEl || activeVideoCheck);
                const card = elCtx?.closest?.('ytd-rich-item-renderer, ytd-video-renderer, ytd-compact-video-renderer, ytd-playlist-panel-video-renderer, ytd-grid-video-renderer');
                if (card) {
                    let aSel = null;
                    try { if (videoIdDetected) aSel = card.querySelector(`a[href*="/watch?v=${videoIdDetected}"][href*="list="]`); } catch (_) { }
                    if (!aSel) {
                        try { aSel = card.querySelector('a[href*="list="]'); } catch (_) { }
                    }
                    if (aSel) {
                        const uCard = new URL(aSel.href, location.origin);
                        const lCard = uCard.searchParams.get('list');
                        if (lCard) plId = lCard;
                        log('processVideo', `📌 plId obtenido desde card actual: ${plId || '(none)'}`);
                    }
                }
            } catch (_) { }
        }
        if (!plId) {
            try {
                if (pageType === 'home' || pageType === 'search' || pageType === 'channel') {
                    const a = document.querySelector('#anchored-panel a[href*="list="]') ||
                        document.querySelector('#movie_player a[href*="list="]') ||
                        document.querySelector('.ytp-miniplayer-ui a[href*="list="]');
                    if (a) {
                        const u = new URL(a.href, location.origin);
                        const l = u.searchParams.get('list');
                        if (l) plId = l;
                    }
                }
            } catch (_) { }
        }

        if (!plId && (pageType === 'home' || pageType === 'search' || pageType === 'channel')) {
            try { if (!plId && lastPlaylistId) plId = lastPlaylistId; } catch (_) { }
            if (!plId) {
                try {
                    if (lastVideoUrl) {
                        const u3 = new URL(lastVideoUrl, location.origin);
                        const l3 = u3.searchParams.get('list');
                        if (l3) plId = l3;
                    }
                } catch (_) { }
            }
        }
        try {
            const isHomeish = pageType === 'home' || pageType === 'search' || pageType === 'channel';
            if (isHomeish && lastPlaylistId && /^RD/.test(lastPlaylistId) && plId && !/^RD/.test(plId)) {
                plId = lastPlaylistId;
            }
            if (isHomeish && !plId && lastPlaylistId && /^RD/.test(lastPlaylistId)) {
                plId = lastPlaylistId;
            }
        } catch (_) { }
        try {
            if (plId) {
                const elForStability = (currentVideoEl || videoEl || activeVideoCheck);
                const contForStability = elForStability?.closest?.('#movie_player');
                const ptForStability = getYouTubePageType();
                const isStableContext = (ptForStability === 'watch' || ptForStability === 'embed') || !!contForStability;
                if (isStableContext) lastPlaylistId = plId;
            }
        } catch (_) { }

        try {
            if ((pageType === 'home' || pageType === 'search' || pageType === 'channel') && videoIdDetected) {
                lastVideoUrl = `https://www.youtube.com/watch?v=${videoIdDetected}${plId ? `&list=${plId}` : ''}`;
            }
        } catch (_) { }

        log('processVideo', `URL del reproductor: ${window.location.href} | Video ID del reproductor: ${videoIdDetected}`);

        // Conjunto para rastrear videos que están siendo procesados actualmente
        const currentlyProcessingVideos = new Set();

        // Evitar reprocesar el mismo video
        if (currentlyProcessingVideos.has(videoIdDetected)) {
            log('processVideo', `El video ${videoIdDetected} ya está siendo procesado. Ignorando.`);
            return;
        }
        currentlyProcessingVideos.add(videoIdDetected);

        try {
            // Detección del tipo de video
            let type = getYouTubePageType();
            const isLive = isLiveVideo();
            log('processVideo', `📋 Tipo de página inicial: ${type}, isLive: ${isLive}`);

            // Si el tipo es 'watch' o 'embed' y el video está en vivo, cambia el tipo a 'live'.
            if ((type === 'watch' || type === 'embed') && isLive) {
                type = 'live';
                log('processVideo', `🔄 Tipo cambiado a 'live' porque se detectó video en vivo`);
            }
            try {
                const cont2 = activeVideoCheck?.closest?.('#movie_player, #shorts-player');
                const shortsDisabled = (() => { try { return cachedSettings?.saveShorts === false; } catch (_) { return false; } })();
                const mpPresent = (() => { try { return !!(document.querySelector('#movie_player')); } catch (_) { return false; } })();
                if (cont2?.id === 'movie_player' || (type === 'shorts' && shortsDisabled && mpPresent)) {
                    // Si el video está en el miniplayer/movie_player, tratar shorts/home como watch para el guardado
                    if (type === 'shorts' || type === 'home') type = 'watch';
                }
            } catch (_) { }

            log('processVideo', `🎇 Tipo de video/página detectado: ${type}`);

            // Determinar tipo lógico para guardado (distinguir previews)
            let logicalType = type;
            try {
                const el = (currentVideoEl || videoEl || activeVideoCheck);
                const cont = el?.closest?.('#movie_player, #shorts-player');
                const isHomeish = (type !== 'watch' && type !== 'shorts' && type !== 'embed' && type !== 'live');
                if (isHomeish && (cont?.id !== 'movie_player')) {
                    const isShortish = !!(el?.closest?.('ytd-reel-video-renderer, ytd-shorts, #shorts-player'));
                    logicalType = isShortish ? 'preview_shorts' : 'preview_watch';
                } else if (type === 'home' && (cont?.id === 'movie_player')) {
                    logicalType = 'watch';
                } else if (type === 'shorts') {
                    logicalType = 'shorts';
                }
            } catch (_) { }
            log('processVideo', `🎯 Tipo lógico para guardado: ${logicalType}`);

            // Respeto estricto de configuración para lives: si saveLiveStreams es false, no procesar
            if (isLive && cachedSettings?.saveLiveStreams === false) {
                log('processVideo', '🛑 Video detectado como LIVE y la opción saveLiveStreams está deshabilitada, omitiendo.');
                isNavigating = false;
                return;
            }

            // Verificar si el tipo de video actual está deshabilitado en la configuración
            const typeToSetting = {
                watch: 'saveRegularVideos',
                shorts: 'saveShorts',
                live: 'saveLiveStreams'
            };

            // Si el tipo de video actual está deshabilitado en la configuración, sale de la función
            // Excepto si hay miniplayer activo (video en #movie_player) aunque estemos en search/channel/etc.
            let allowByMiniplayer = false;
            try {
                // Permitir procesamiento si hay un miniplayer presente aun estando en Shorts
                const cont = activeVideoCheck?.closest?.('#movie_player, #shorts-player');
                const mp = document.querySelector('#movie_player');
                allowByMiniplayer = (!!cont && (cont.id === 'movie_player')) || !!mp;
            } catch (_) { }
            if (!cachedSettings[typeToSetting[type]] && !(((getYouTubePageType() === 'home') || allowByMiniplayer) && activeVideoCheck)) {
                // Loguea un mensaje indicando que este tipo de video no se debe procesar
                log('processVideo', `🛑 Tipo "${type}" no está habilitado para guardado, omitiendo.`);
                log('processVideo', `Es home con un video activo? ${!(getYouTubePageType() === 'home' && activeVideoCheck)}`);

                // NO detenemos el monitor - debe seguir activo para detectar ads en próximos videos
                // El monitor se gestiona en handleNavigation y debe permanecer activo

                // Resetear isNavigating para permitir nueva navegación inmediatamente
                isNavigating = false;

                // Sale de la función, evitando que el video se procese
                return;
            }

            // Buscar progreso previo
            let savedData = getSavedVideoData(videoIdDetected, plId);
            log('processVideo', `Verificando reanudación: savedData=${!!savedData}, videoIdDetected=${videoIdDetected}, lastResumeId=${lastResumeId}, videoIdDetected === lastResumeId=${videoIdDetected === lastResumeId}`);

            // Verifica si hay datos guardados para el video y si el video detectado es diferente al último ID de video que se intentó reanudar.
            // Opción B: desactivar reanudación para previews inline en páginas no watch/shorts/embed (conservar miniplayer)
            let disableResumeInInlinePreview = false;
            try {
                const cont3 = (currentVideoEl || videoEl || activeVideoCheck)?.closest?.('#movie_player, #shorts-player');
                disableResumeInInlinePreview = (type !== 'watch' && type !== 'shorts' && type !== 'embed' && cont3?.id !== 'movie_player');
            } catch (_) { }

            if (savedData && videoIdDetected !== lastResumeId && !disableResumeInInlinePreview) {
                // Define si se debe reanudar la reproducción del video
                const shouldResume =
                    // La reanudación está forzada si 'forceResumeTime' es mayor que cero,
                    // lo que indica un tiempo específico programado para retomar el video.
                    savedData.forceResumeTime > 0 ||

                    // Alternativamente, se decide reanudar si el tiempo de visualización
                    // es mayor a minSeekDiff (1.5 segundos) y el video no se ha completado.
                    // Esto evita reanudar videos que apenas se comenzaron y aún no han capturado el interés del usuario.
                    (savedData.timestamp > CONFIG.minSeekDiff && !savedData.isCompleted);

                log('processVideo', `shouldResume=${shouldResume} (forceResumeTime=${savedData.forceResumeTime}, timestamp=${savedData.timestamp}, isCompleted=${savedData.isCompleted})`);

                // resumePlayback con 200 ms de espera para "shorts"
                const debouncedResumePlayback = debounce(resumePlayback, 200);

                if (shouldResume) {
                    isResuming = true;
                    log('processVideo', `✅ Reanudando ${videoIdDetected} (${type})...`);

                    if (type === 'short') {
                        // Espera 200 ms sin más llamadas antes de reanudar
                        debouncedResumePlayback(
                            player,
                            videoIdDetected,
                            videoEl,
                            savedData,
                            type
                        );
                    } else {
                        // Reanuda de inmediato para otros tipos
                        resumePlayback(
                            player,
                            videoIdDetected,
                            videoEl,
                            savedData,
                            type
                        );
                    }

                    lastResumeId = videoIdDetected;
                } else {
                    isResuming = false;
                    log('processVideo', '⏩ No se cumple condición de reanudación, reproducción normal');
                }
            } else {
                if (disableResumeInInlinePreview) {
                    log('processVideo', '⏭ Reanudación deshabilitada en previews inline (solo guardar).');
                }
                log('processVideo', '⏩ No hay datos guardados o video ya fue reanudado, reproducción normal');
            }

            // Capturar contexto para evitar guardar en navegaciones cruzadas
            const boundVideoId = videoIdDetected;
            const boundType = logicalType;
            const boundPlId = plId;

            // Handler para guardar progreso
            let lastAdBlockedHandlerLogTs = 0; // throttle para logs repetitivos del handler
            const handler = () => {
                try {
                    if (isNavigating) return;

                    // Verificar si hay anuncios activos usando el estado por tipo
                    if (isAdBlockedFor(boundType)) {
                        const now = Date.now();
                        if (now - lastAdBlockedHandlerLogTs > 2000) {
                            log('handler', `⏸ Anuncio activo (${boundType}) detectado, no procesando timeupdate`);
                            lastAdBlockedHandlerLogTs = now;
                        }
                        return;
                    }

                    // Verificar visibilidad del video para previews en páginas de inicio/búsqueda
                    const currentPageType = getYouTubePageType();
                    const contNow = (currentVideoEl || videoEl)?.closest?.('#movie_player, #shorts-player');
                    const isPreviewOnHomeLike = (currentPageType === 'home' || currentPageType === 'search' || currentPageType === 'channel') &&
                        contNow?.id !== 'movie_player';

                    if (isPreviewOnHomeLike) {
                        // Verificar si el elemento del video es visible y tiene un tamaño razonable
                        const isVisible = (el) => {
                            if (!el) return false;
                            const rect = el.getBoundingClientRect();
                            const isInViewport = (
                                rect.top >= 0 &&
                                rect.left >= 0 &&
                                rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
                                rect.right <= (window.innerWidth || document.documentElement.clientWidth)
                            );
                            return isInViewport && el.offsetWidth > 0 && el.offsetHeight > 0;
                        };

                        if (!isVisible(videoEl)) {
                            log('handler', 'Video preview no está visible, omitiendo guardado');
                            return;
                        }

                        // Para previews, asegurarse de que el tiempo de reproducción sea suficiente
                        const currentTime = videoEl?.currentTime || 0;
                        if (currentTime < 1.0) {
                            log('handler', `Tiempo de reproducción insuficiente (${currentTime.toFixed(2)}s) para guardar preview`);
                            return;
                        }
                    }

                    lastTimeUpdateTick = Date.now();

                    // Validar que aún exista un video válido
                    if (!videoIdDetected) {
                        conError('handler', 'No se encontró videoIdDetected al intentar guardar progreso.');
                        return;
                    }

                    // Diagnosticar si el video está pausado injustificadamente
                    if ((currentVideoEl || videoEl) && (currentVideoEl || videoEl).paused && !isAdPlaying && !isResuming) {
                        log('handler', '⚠️ Video está pausado. No por anuncios ni reanudación.');
                        // No intentar forzar reproducción - los navegadores modernos lo bloquean
                        return;
                    }

                    // Evitar convertir player a string porque puede contener Symbols (es un Proxy)
                    if (!lastHandlerPerfLog || Date.now() - lastHandlerPerfLog > 3000) {
                        log('handler de processVideo', `Player tiene getDuration: ${typeof player?.getDuration}, isProxy: ${!!YTHelper?.apiProxy}`);
                        lastHandlerPerfLog = Date.now();
                    }

                    // Usar currentPageType ya definido arriba
                    const urlIdNow = extractOrNormalizeVideoId(window.location.href)?.id || null;
                    let currentId = null;
                    try { currentId = YTHelper?.video?.id || player?.getVideoData?.()?.video_id || null; } catch (_) { }
                    let mpId = null;
                    try { const mp = document.querySelector('#movie_player'); mpId = mp?.getVideoData?.()?.video_id || null; } catch (_) { }
                    let effectiveIdNow = null;
                    try {
                        const cont = (currentVideoEl || videoEl)?.closest?.('#movie_player, #shorts-player');
                        if (cont?.id === 'movie_player') {
                            effectiveIdNow = mpId || currentId || urlIdNow || videoIdDetected;
                        } else if (cont?.id === 'shorts-player') {
                            effectiveIdNow = urlIdNow || currentId || videoIdDetected;
                        } else {
                            effectiveIdNow = currentId || mpId || urlIdNow || videoIdDetected;
                        }
                    } catch (_) {
                        effectiveIdNow = currentId || mpId || urlIdNow || videoIdDetected;
                    }

                    if (effectiveIdNow && effectiveIdNow !== boundVideoId) {
                        log('handler', `El ID del video cambió (${boundVideoId} -> ${effectiveIdNow}). Re-enlazando handler.`);
                        try {
                            videoEl.removeEventListener('timeupdate', currentTimeUpdateHandler || handler);
                        } finally {
                            // Limpiar el estado de procesamiento con un pequeño retraso
                            setTimeout(() => {
                                currentlyProcessingVideos.delete(videoIdDetected);
                            }, 1000);
                        } try { clearVideoInfoCache(boundVideoId); } catch (_) { }
                        try { clearVideoInfoCache(effectiveIdNow); } catch (_) { }
                        if (currentPageType === 'home' || currentPageType === 'search' || currentPageType === 'channel') {
                            lastVideoUrl = `https://www.youtube.com/watch?v=${effectiveIdNow}${boundPlId ? `&list=${boundPlId}` : ''}`;
                            try { if (boundPlId) lastPlaylistId = boundPlId; } catch (_) { }
                        } else if (currentPageType === 'watch' || currentPageType === 'embed') {
                            lastVideoUrl = window.location.href;
                        }
                        setTimeout(() => {
                            try { processVideo(player, videoEl); } catch (_) { }
                        }, 0);
                        return;
                    }

                    // Limpiar mensajes seek cuando el video está reproduciéndose,
                    // pero solo en el display que realmente contiene el mensaje seek.
                    const currentMessage = timeDisplay?.innerHTML || '';
                    const hasSeekMessage = !!currentMessage.includes('svgPlayOrPauseIcon');
                    const currentShortsMessage = shortsTimeDisplay?.innerHTML || '';
                    const hasShortsSeekMessage = !!currentShortsMessage.includes('svgPlayOrPauseIcon');
                    const isPlayingNow = !(currentVideoEl || videoEl)?.paused;
                    if (isPlayingNow) {
                        if (hasSeekMessage) {
                            log('handler', '🎬 Reproduciendo: limpiando mensaje seek en barra de reproducción');
                            clearPlaybackBarMessage();
                        }
                        if (hasShortsSeekMessage) {
                            log('handler', '🎬 Reproduciendo: limpiando mensaje seek en Shorts');
                            clearShortsMessage();
                        }
                    }

                    // Lógica de guardado de progreso
                    const now = Date.now();
                    const minInterval = (cachedSettings?.minSecondsBetweenSaves || CONFIG.defaultSettings.minSecondsBetweenSaves) * 1000;
                    const lastSavedForVideo = lastSaveTimesByVideoId[boundVideoId] || 0;

                    // Para previews, usar un intervalo más corto para mejor respuesta
                    const effectiveMinInterval = isPreviewOnHomeLike ?
                        Math.min(minInterval, 2000) : // Máximo 2 segundos para previews
                        minInterval;

                    // Verificar si ya se guardó recientemente
                    if (now - lastSavedForVideo < effectiveMinInterval) {
                        return;
                    }

                    if (now - lastSavedForVideo >= minInterval) {
                        // Llamar a updateStatus y esperar el resultado
                        updateStatus(player, (currentVideoEl || videoEl), boundType, ((boundType === 'shorts' || currentPageType === 'shorts') ? null : boundPlId), boundVideoId).then(saveResult => {
                            if (saveResult?.success) {
                                log('processVideo', `✅ Progreso guardado exitosamente para ${videoIdDetected}`);

                                // Notificar solo si realmente se guardó
                                const currentTime = player.getCurrentTime ? player.getCurrentTime() : ((currentVideoEl || videoEl)?.currentTime || 0);
                                notifySeekOrProgress(saveResult?.timestamp ?? currentTime, 'progress', { saveResult, videoType: boundType });
                                try { lastSaveTimesByVideoId[boundVideoId] = Date.now(); } catch (_) { }
                            } else {
                                log('processVideo', `⚠️ No se guardó progreso para ${videoIdDetected}:`, saveResult?.reason);
                                // Throttle de reintentos cuando es preview inline con datos inválidos
                                try {
                                    if (saveResult?.reason === 'invalid_data') {
                                        const contNow2 = (currentVideoEl || videoEl)?.closest?.('#movie_player, #shorts-player');
                                        let pageTypeNow2 = getYouTubePageType();
                                        const isPreviewOnHomeLike2 = (pageTypeNow2 === 'home' || pageTypeNow2 === 'search' || pageTypeNow2 === 'channel') && contNow2?.id !== 'movie_player';
                                        if (isPreviewOnHomeLike2) {
                                            lastSaveTimesByVideoId[boundVideoId] = Date.now();
                                        }
                                    }
                                } catch (_) { }
                            }

                            // Actualizar barra de progreso con colores (independientemente del guardado)
                            try {
                                const currentTime = player.getCurrentTime ? player.getCurrentTime() : ((currentVideoEl || videoEl)?.currentTime || 0);
                                const duration = getVideoDuration(player, (currentVideoEl || videoEl));
                                updateProgressBarGradient(currentTime, duration, boundType);
                            } catch (error) {
                                // Silenciar errores para no afectar el funcionamiento principal
                            }
                        }).catch(error => {
                            conError('processVideo', 'Error al guardar progreso:', error);
                        });

                        // No mover lastSaveTime aquí; se actualiza solo en éxito para permitir reintentos tempranos
                    }
                } catch (err) {
                    conError('handler', 'Error en el handler de timeupdate:', err);
                }
            };

            // Si es preview y el usuario no permite guardar previews, no enlazar handler ni pollers
            try {
                const isPreviewType = typeof boundType === 'string' && boundType.startsWith('preview');
                const allowInline = cachedSettings?.saveInlinePreviews === true;
                if (isPreviewType && !allowInline) {
                    log('processVideo', '🔕 Inline preview deshabilitada por configuración. No se enlaza handler.');
                    return;
                }
            } catch (_) { }

            // Antes de agregar el nuevo handler, elimina el anterior si existía
            if (currentVideoEl && currentTimeUpdateHandler) {
                try {
                    currentVideoEl.removeEventListener('timeupdate', currentTimeUpdateHandler);
                    log('processVideo', '🧹 Handler anterior removido correctamente.');
                } catch (err) {
                    warn('processVideo', `No se pudo remover el handler anterior: ${err.message}`);
                }
            }
            // Adjuntar el nuevo handler
            currentTimeUpdateHandler = handler;
            currentVideoEl = videoEl;
            videoEl.addEventListener('timeupdate', handler);

            if (timeupdateRebindIntervalId) {
                try { clearInterval(timeupdateRebindIntervalId); } catch (_) { }
            }
            const observedHandler = handler;
            const observedVideoId = boundVideoId;
            timeupdateRebindIntervalId = setInterval(async () => {
                try {
                    let activeEl = null;
                    const pt = getYouTubePageType();
                    if (pt === 'watch' || pt === 'embed') {
                        try { activeEl = document.querySelector('#movie_player video.html5-main-video') || YTHelper?.player?.videoElement || await getActiveVideoElement(); } catch (_) { }
                    } else if (pt === 'shorts') {
                        try {
                            const mpVid = (() => { try { return document.querySelector('#movie_player') || null } catch (_) { return null; } })();
                            const preferMp = (observedVideoId && mpVid && observedVideoId === mpVid) || (cachedSettings?.saveShorts === false);
                            const preferSp = (observedVideoId && mpVid && observedVideoId !== mpVid) || (cachedSettings?.saveShorts !== false);
                            if (preferMp) {
                                activeEl = document.querySelector('#movie_player video.html5-main-video');
                            } else if (preferSp) {
                                activeEl = document.querySelector('#shorts-player video') || document.querySelector('ytd-shorts video.html5-main-video');
                            }
                            if (!activeEl) {
                                // Fallbacks en orden: Shorts explícito -> helper -> cualquier activo
                                activeEl = document.querySelector('#shorts-player video') || document.querySelector('ytd-shorts video.html5-main-video') || YTHelper?.player?.videoElement || await getActiveVideoElement();
                            }
                        } catch (_) { }
                    } else {
                        try { activeEl = await getActiveVideoElement(); if (!activeEl) { activeEl = YTHelper?.player?.videoElement; } } catch (_) { }
                    }
                    if (!activeEl) return;
                    let idOk = true;
                    try {
                        const container = activeEl.closest('#movie_player, #shorts-player');
                        if (container?.id === 'shorts-player') {
                            const urlShortId = extractOrNormalizeVideoId(window.location.href)?.id || null;
                            if (urlShortId && observedVideoId) idOk = (urlShortId === observedVideoId);
                        } else {
                            const vid = container?.getVideoData?.()?.video_id;
                            if (vid && observedVideoId) idOk = (vid === observedVideoId);
                        }
                    } catch (_) { }
                    if ((activeEl !== currentVideoEl || !document.contains(currentVideoEl)) && idOk) {
                        try { currentVideoEl?.removeEventListener('timeupdate', currentTimeUpdateHandler || observedHandler); } catch (_) { }
                        currentVideoEl = activeEl;
                        currentTimeUpdateHandler = observedHandler;
                        try { activeEl.addEventListener('timeupdate', observedHandler); } catch (_) { }
                    }
                } catch (_) { }
            }, 1000);

            if (progressPollIntervalId) {
                try { clearInterval(progressPollIntervalId); } catch (_) { }
            }
            progressPollIntervalId = setInterval(async () => {
                try {
                    const nowTs = Date.now();
                    if (nowTs - lastTimeUpdateTick <= 2500) return; // timeupdate activo, no hace falta fallback
                    if (isNavigating || isAdBlockedFor(boundType)) return;

                    let activeEl = null;
                    try { activeEl = YTHelper?.player?.videoElement || await getActiveVideoElement(); } catch (_) { }
                    if (!activeEl || !document.contains(activeEl)) return;
                    if (activeEl.paused) return; // Evitar polling cuando está pausado

                    let effPlayer = player;
                    try { const hp = YTHelper?.player?.playerObject; if (hp) effPlayer = hp; } catch (_) { }
                    let pageTypeNow = getYouTubePageType();
                    const urlIdNow = extractOrNormalizeVideoId(window.location.href)?.id || null;
                    let currentId = null;
                    try { currentId = YTHelper?.video?.id || effPlayer?.getVideoData?.()?.video_id || null; } catch (_) { }
                    let mpId2 = null;
                    try { if (pageTypeNow !== 'shorts') { const mp2 = document.querySelector('#movie_player'); mpId2 = mp2?.getVideoData?.()?.video_id || null; } else { const mp2s = document.querySelector('#movie_player'); mpId2 = mp2s?.getVideoData?.()?.video_id || null; } } catch (_) { }
                    const effectiveIdNow = (pageTypeNow === 'watch' || pageTypeNow === 'embed')
                        ? (urlIdNow || currentId)
                        : (pageTypeNow === 'shorts')
                            ? ((boundType === 'watch' || (cachedSettings?.saveShorts === false)) ? (mpId2 || currentId || urlIdNow || boundVideoId) : (urlIdNow || currentId))
                            : (mpId2 || currentId || urlIdNow || boundVideoId);
                    if (effectiveIdNow && effectiveIdNow !== boundVideoId) return;

                    const minInterval = (cachedSettings?.minSecondsBetweenSaves || CONFIG.defaultSettings.minSecondsBetweenSaves) * 1000;
                    if (nowTs - lastSaveTime < minInterval) return;

                    updateStatus(effPlayer, activeEl, boundType, ((boundType === 'shorts' || pageTypeNow === 'shorts') ? null : boundPlId), boundVideoId).then(saveResult => {
                        if (saveResult?.success) {
                            log('processVideo', `✅ Progreso guardado exitosamente para ${boundVideoId}`);
                            const currentTime = effPlayer.getCurrentTime ? effPlayer.getCurrentTime() : (activeEl?.currentTime || 0);
                            notifySeekOrProgress(saveResult?.timestamp ?? currentTime, 'progress', { saveResult, videoType: boundType });
                        } else {
                            log('processVideo', `⚠️ No se guardó progreso para ${boundVideoId}:`, saveResult?.reason);
                        }
                        try {
                            const currentTime = effPlayer.getCurrentTime ? effPlayer.getCurrentTime() : (activeEl?.currentTime || 0);
                            const duration = getVideoDuration(effPlayer, activeEl);
                            updateProgressBarGradient(currentTime, duration, boundType);
                        } catch (_) { }
                    }).catch(err => {
                        conError('processVideo', 'Error al guardar progreso (poller):', err);
                    });

                    lastSaveTime = nowTs;
                } catch (_) { }
            }, 1000);

            if (secondaryProgressPollIntervalId) {
                try { clearInterval(secondaryProgressPollIntervalId); } catch (_) { }
            }
            secondaryProgressPollIntervalId = setInterval(async () => {
                try {
                    if (isNavigating) return;
                    const pageTypeNow2 = getYouTubePageType();
                    if (pageTypeNow2 !== 'shorts') return;
                    const mpC = document.querySelector('#movie_player');
                    const spC = document.querySelector('#shorts-player');
                    if (!mpC && !spC) return;
                    const mpEl2 = document.querySelector('#movie_player video.html5-main-video');
                    const spEl2 = document.querySelector('#shorts-player video');
                    const nowTs2 = Date.now();
                    const minInterval2 = (cachedSettings?.minSecondsBetweenSaves || CONFIG.defaultSettings.minSecondsBetweenSaves) * 1000;
                    let mpId3 = null;
                    try { mpId3 = mpC?.getVideoData?.()?.video_id || null; } catch (_) { }
                    const urlIdNow2 = extractOrNormalizeVideoId(window.location.href)?.id || null;
                    if (mpEl2 && !mpEl2.paused && mpId3 && mpId3 !== boundVideoId && !isAdBlockedFor('watch')) {
                        const lastSavedMp = lastSaveTimesByVideoId[mpId3] || 0;
                        if (nowTs2 - lastSavedMp >= minInterval2) {
                            const mpPlayer2 = mpC && typeof mpC.getDuration === 'function' ? mpC : player;
                            try { clearVideoInfoCache(mpId3); } catch (_) { }
                            // Derivar playlistId del miniplayer desde lastVideoUrl o anclado
                            let mpPlId2 = boundPlId;
                            if (!mpPlId2) {
                                try {
                                    if (lastVideoUrl) {
                                        const u = new URL(lastVideoUrl, location.origin);
                                        const l = u.searchParams.get('list');
                                        if (l) mpPlId2 = l;
                                    }
                                } catch (_) { }
                                if (!mpPlId2) {
                                    try {
                                        const a = document.querySelector('#anchored-panel a[href*="list="]') || document.querySelector('#movie_player a[href*="list="]');
                                        if (a) {
                                            const u2 = new URL(a.href, location.origin);
                                            const l2 = u2.searchParams.get('list');
                                            if (l2) mpPlId2 = l2;
                                        }
                                    } catch (_) { }
                                }
                            }
                            updateStatus(mpPlayer2, mpEl2, 'watch', mpPlId2, mpId3).then(r => {
                                if (r && r.success) { try { lastSaveTimesByVideoId[mpId3] = Date.now(); } catch (_) { } }
                            }).catch(() => { });
                        }
                    }
                    if (spEl2 && !spEl2.paused && urlIdNow2 && urlIdNow2 !== boundVideoId && !isAdBlockedFor('shorts')) {
                        // Respetar configuración: no guardar shorts si está deshabilitado
                        try { if (cachedSettings?.saveShorts === false) return; } catch (_) { }
                        const lastSavedSp = lastSaveTimesByVideoId[urlIdNow2] || 0;
                        if (nowTs2 - lastSavedSp >= minInterval2) {
                            const spPlayer2 = {
                                getVideoData: () => ({ video_id: urlIdNow2, title: getVideoTittle(null) || null, author: getVideoAuthor(null) || null }),
                                getCurrentTime: () => { try { return spEl2.currentTime || 0; } catch (_) { return 0; } },
                                getDuration: () => { try { return spEl2.duration || 0; } catch (_) { return 0; } }
                            };
                            try { clearVideoInfoCache(urlIdNow2); } catch (_) { }
                            updateStatus(spPlayer2, spEl2, 'shorts', null, urlIdNow2).then(r => {
                                if (r && r.success) {
                                    try { lastSaveTimesByVideoId[urlIdNow2] = Date.now(); } catch (_) { }
                                    try {
                                        const currentTime2 = spPlayer2.getCurrentTime ? spPlayer2.getCurrentTime() : (spEl2?.currentTime || 0);
                                        notifySeekOrProgress(r.timestamp ?? currentTime2, 'progress', { saveResult: r, videoType: 'shorts' });
                                    } catch (_) { }
                                }
                            }).catch(() => { });
                        }
                    }
                } catch (_) { }
            }, 1000);

            try {
                const ptEnd = getYouTubePageType();
                const elEnd = currentVideoEl || videoEl || activeVideoCheck;
                const contEnd = elEnd?.closest?.('#movie_player');
                const isStableEnd = (ptEnd === 'watch' || ptEnd === 'embed') || !!contEnd;
                if (plId && isStableEnd && ptEnd !== 'shorts') lastPlaylistId = plId;
            } catch (_) { }
        } catch (error) {
            conError('processVideo', `Error al procesar el video ${videoIdDetected}:`, error);
        } finally {
            setTimeout(() => (currentlyProcessingVideoId = null), 100);
        }
    };

    // ------------------------------------------
    // MARK: ⏯ Seek
    // ------------------------------------------

    const applySeek = async (player, videoEl, time, options = {}) => {
        const { bypassMinDiff = false, isForced = false, type = 'normal', retryOnFail = false, resumeCallback = null, targetVideoId = null } = options;
        // CRÍTICO: No hacer seek si hay anuncio activo del tipo correspondiente
        if (isAdBlockedFor(type) || isScriptPaused) {
            log('applySeek', `⏸️  ABORTANDO applySeek - Anuncio (${type}) activo detectado`);
            return;
        }
        const startedPageType = getYouTubePageType();
        const getContainerForEl = (el) => { try { return el?.closest?.('#movie_player, #shorts-player'); } catch (_) { return null; } };
        const matchesTargetPlayer = () => {
            try {
                const id = player?.getVideoData?.()?.video_id;
                return !targetVideoId || id === targetVideoId;
            } catch (_) { return !targetVideoId; }
        };
        const matchesTargetVideoEl = () => {
            try {
                const c = getContainerForEl(videoEl);
                const id = c?.getVideoData?.()?.video_id;
                return !targetVideoId || id === targetVideoId;
            } catch (_) { return !targetVideoId; }
        };
        const refreshRefs = async () => {
            try {
                const pt = getYouTubePageType();
                if (pt === 'watch') {
                    const mp = document.querySelector('#movie_player');
                    if (mp?.getDuration) {
                        try {
                            const idOk = typeof mp.getVideoData === 'function' ? (mp.getVideoData()?.video_id === targetVideoId || !targetVideoId) : true;
                            if (idOk) player = mp;
                        } catch (_) { player = mp; }
                    }
                    const ve = document.querySelector('#movie_player video.html5-main-video');
                    if (ve && document.contains(ve)) {
                        const idOkVe = (() => { try { const id = mp?.getVideoData?.()?.video_id; return !targetVideoId || !id || id === targetVideoId; } catch (_) { return !targetVideoId; } })();
                        if (idOkVe) videoEl = ve;
                    }
                } else if (pt === 'shorts') {
                    const sp = document.querySelector('#shorts-player');
                    if (sp?.getDuration) {
                        try {
                            const idOk = typeof sp.getVideoData === 'function' ? (sp.getVideoData()?.video_id === targetVideoId || !targetVideoId) : true;
                            if (idOk) player = sp;
                        } catch (_) { player = sp; }
                    }
                    const ve2 = document.querySelector('#shorts-player video');
                    if (ve2 && document.contains(ve2)) {
                        const idOkVe2 = (() => { try { const id = sp?.getVideoData?.()?.video_id; return !targetVideoId || !id || id === targetVideoId; } catch (_) { return !targetVideoId; } })();
                        if (idOkVe2) videoEl = ve2;
                    }
                }

                const hp = YTHelper?.player?.playerObject;
                const hv = YTHelper?.player?.videoElement;
                if (!player && hp) {
                    try {
                        const id = hp?.getVideoData?.()?.video_id;
                        if (!targetVideoId || !id || id === targetVideoId) player = hp;
                    } catch (_) { player = hp; }
                }
                if (!(videoEl && document.contains(videoEl))) {
                    if (hv && document.contains(hv)) {
                        try {
                            const c = getContainerForEl(hv);
                            const id = c?.getVideoData?.()?.video_id;
                            if (!targetVideoId || !id || id === targetVideoId) videoEl = hv;
                        } catch (_) { }
                    } else {
                        try {
                            const newEl = await getActiveVideoElement();
                            if (newEl) {
                                const c2 = getContainerForEl(newEl);
                                const id2 = c2?.getVideoData?.()?.video_id;
                                if (!targetVideoId || !id2 || id2 === targetVideoId) videoEl = newEl;
                            }
                        } catch (_) { }
                    }
                }
            } catch (_) { }
        };
        try {
            const hp = YTHelper?.player?.playerObject;
            const hv = YTHelper?.player?.videoElement;
            if (hp) player = hp;
            if (hv) videoEl = hv;
        } catch (_) { }
        if (videoEl && !document.contains(videoEl)) {
            try {
                const newEl = await getActiveVideoElement();
                if (newEl) videoEl = newEl;
            } catch (_) { }
        }
        // Si es un tiempo forzado (manual), automáticamente bypass minSeekDiff
        const effectiveBypassMinDiff = bypassMinDiff || isForced;
        const TIMEOUT_MS = 5000; // Timeout máximo para el seek
        const NEAR_THRESHOLD = 0.5; // Consideramos seek completado si estamos a menos de 0.5s

        if (!player && !videoEl) {
            warn('applySeek', 'No se proporcionó player ni videoEl. Abortando.');
            return;
        }

        // Normalizar tiempo
        if (typeof time !== 'number') {
            if (typeof time === 'string') time = parseTimeToSeconds(time.trim());
            else {
                warn('applySeek', 'Tiempo inválido:', time);
                return;
            }
        }

        log('applySeek', `Seek hacia ${time}s | Forzado: ${isForced} | BypassMinDiff: ${effectiveBypassMinDiff} | Tipo: ${type}`);

        // Verificar si el seek es necesario
        try {
            const current = videoEl?.currentTime ?? player?.getCurrentTime?.() ?? 0;
            const diff = Math.abs(current - time);

            if (!effectiveBypassMinDiff && diff <= CONFIG.minSeekDiff) {
                log('applySeek', `Diferencia mínima (${diff}s). Omitiendo seek.`);
                return;
            }
        } catch (e) {
            warn('applySeek', 'Error al obtener tiempo actual:', e);
        }

        // Validar estado del player antes del seek
        const isPlayerReady = () => {
            try {
                if (player?.seekTo) {
                    // Verificar que el player tenga métodos esenciales y duración válida
                    const duration = player.getDuration?.();
                    const currentTime = player.getCurrentTime?.();
                    return duration > 0 && typeof currentTime === 'number';
                }
                return videoEl && videoEl.readyState >= 2 && !isNaN(videoEl.duration) && videoEl.duration > 0;
            } catch (e) {
                return false;
            }
        };

        // Esperar a que el player esté listo si no lo está
        if (!isPlayerReady()) {
            log('applySeek', 'Player no está listo, esperando...');
            await new Promise(resolve => {
                let attempts = 0;
                const maxAttempts = 10;
                const checkReady = () => {
                    attempts++;
                    try { const hp2 = YTHelper?.player?.playerObject; const hv2 = YTHelper?.player?.videoElement; if (hp2) player = hp2; if (hv2) videoEl = hv2; } catch (_) { }
                    if (isPlayerReady() || attempts >= maxAttempts) {
                        log('applySeek', `Player listo después de ${attempts} intentos`);
                        resolve();
                    } else {
                        setTimeout(checkReady, 200);
                    }
                };
                checkReady();
            });
            await refreshRefs();
        }

        // Ejecutar el seek con retry logic
        let seekSuccess = false;
        let retryCount = 0;
        const maxRetries = 3;

        while (!seekSuccess && retryCount < maxRetries) {
            await refreshRefs();
            if (!isPlayerReady()) {
                // Espera breve adicional para casos de swap tardío del <video>/player
                let inner = 0;
                while (!isPlayerReady() && inner < 5) {
                    await new Promise(r => setTimeout(r, 150));
                    await refreshRefs();
                    inner++;
                }
            }
            const currentPageType = getYouTubePageType();
            if (currentPageType !== startedPageType) {
                warn('applySeek', `Página cambió de ${startedPageType} a ${currentPageType} durante seek. Abortando para evitar cruce.`);
                try { if (retryOnFail && typeof resumeCallback === 'function') resumeCallback(); } catch (_) { }
                return;
            }
            if (!matchesTargetPlayer() && !matchesTargetVideoEl()) {
                warn('applySeek', 'Player/Video no corresponde al targetVideoId, abortando para evitar cruce');
                try { if (retryOnFail && typeof resumeCallback === 'function') resumeCallback(); } catch (_) { }
                return;
            }
            try {
                if (player?.seekTo && isPlayerReady() && matchesTargetPlayer()) {
                    log('applySeek', `Usando player.seekTo(${time}, true) - Intento ${retryCount + 1}`);
                    player.seekTo(time, true);
                    seekSuccess = true;
                } else if (videoEl && !isNaN(videoEl.duration) && matchesTargetVideoEl()) {
                    log('applySeek', `Asignando videoEl.currentTime = ${time} - Intento ${retryCount + 1}`);
                    videoEl.currentTime = time;
                    seekSuccess = true;
                } else {
                    throw new Error('Player o videoEl no disponible');
                }
            } catch (e) {
                retryCount++;
                if (retryCount < maxRetries) {
                    warn('applySeek', `Error en intento ${retryCount}, reintentando en 300ms:`, e);
                    await refreshRefs();
                    await new Promise(resolve => setTimeout(resolve, 300));
                } else {
                    conError('applySeek', 'Error al ejecutar seek después de todos los reintentos:', e);
                    try {
                        if (retryOnFail && typeof resumeCallback === 'function') {
                            log('applySeek', '🔁 Reintentando resumePlayback tras fallo de seek');
                            resumeCallback();
                        }
                    } catch (_) { }
                    return;
                }
            }
        }

        // Esperar confirmación del seek con respaldo múltiple y timeout
        await new Promise(resolve => {
            let seekCompleted = false;

            const completeSeek = () => {
                if (!seekCompleted) {
                    seekCompleted = true;
                    log('applySeek', 'Seek completado.');
                    cleanup();
                    resolve();
                }
            };

            const cleanup = () => {
                clearInterval(checkInterval);
                clearTimeout(timeoutId);
                videoEl?.removeEventListener('seeked', onSeeked);
                videoEl?.removeEventListener('timeupdate', onTimeUpdate);
            };

            const onSeeked = () => {
                log('applySeek', 'Evento "seeked" detectado.');
                completeSeek();
            };

            const onTimeUpdate = () => {
                try {
                    try { const hvNow = YTHelper?.player?.videoElement; if (hvNow) videoEl = hvNow; } catch (_) { }
                    const currentTime = videoEl?.currentTime ?? player?.getCurrentTime?.();
                    if (Math.abs(currentTime - time) <= NEAR_THRESHOLD) {
                        log('applySeek', `Seek detectado por timeupdate. Diferencia: ${Math.abs(currentTime - time)}s`);
                        completeSeek();
                    }
                } catch (e) {
                    // ignorar errores de lectura
                }
            };

            // Intervalo de verificación como respaldo
            const checkInterval = setInterval(() => {
                try {
                    try { const hvNow = YTHelper?.player?.videoElement; if (hvNow) videoEl = hvNow; } catch (_) { }
                    const currentTime = videoEl?.currentTime ?? player?.getCurrentTime?.();
                    if (Math.abs(currentTime - time) <= NEAR_THRESHOLD) {
                        log('applySeek', `Seek detectado por intervalo. Diferencia: ${Math.abs(currentTime - time)}s`);
                        completeSeek();
                    }
                } catch (e) {
                    // ignorar
                }
            }, 200);

            // Timeout final con lógica mejorada
            const timeoutId = setTimeout(() => {
                warn('applySeek', `Timeout de ${TIMEOUT_MS}ms alcanzado. Verificando estado final...`);
                try {
                    const currentTime = videoEl?.currentTime ?? player?.getCurrentTime?.() ?? 0;
                    const diff = Math.abs(currentTime - time);

                    // Ser más tolerante durante navegación rápida
                    const tolerance = isNavigating ? 2.0 : 1.0;

                    if (diff <= tolerance) {
                        log('applySeek', `Seek completado con tolerancia. Diferencia final: ${diff}s`);
                    } else {
                        warn('applySeek', `Seek incompleto. Diferencia final: ${diff}s`);

                        // Último intento de seek directo si el player está disponible
                        if (player?.seekTo && isPlayerReady()) {
                            log('applySeek', 'Último intento de seek en timeout...');
                            try {
                                player.seekTo(time, true);
                            } catch (e) {
                                warn('applySeek', 'Falló último intento de seek:', e);
                            }
                        }
                    }
                } catch (e) {
                    warn('applySeek', 'Error al verificar estado final:', e);
                }
                completeSeek();
            }, TIMEOUT_MS);

            // Listeners de eventos
            videoEl?.addEventListener('seeked', onSeeked, { once: true });
            videoEl?.addEventListener('timeupdate', onTimeUpdate);
        });

        // Notificación final
        notifySeekOrProgress(time, 'seek', { isForced, videoType: type });
        try {
            const ptAfter = getYouTubePageType();
            const shouldAutoPlay = (ptAfter === 'watch' || ptAfter === 'embed' || type === 'watch' || type === 'embed');
            if (shouldAutoPlay) {
                if (typeof player?.playVideo === 'function') {
                    try { player.playVideo(); } catch (_) { }
                } else if (videoEl?.paused && typeof videoEl.play === 'function') {
                    try { videoEl.play(); } catch (_) { }
                }
                try {
                    let tries = 0;
                    while (tries < 6) {
                        try { const hv = YTHelper?.player?.videoElement; if (hv) videoEl = hv; } catch (_) { }
                        const state = (() => { try { return player?.getPlayerState?.(); } catch (_) { return null; } })();
                        const paused = !!videoEl?.paused || state === 2;
                        if (!paused) break;
                        try { player?.playVideo?.(); } catch (_) { }
                        if (videoEl?.paused) {
                            try { videoEl.play?.(); } catch (_) { }
                        }
                        if (videoEl?.paused) {
                            try { document.querySelector('#movie_player .ytp-play-button')?.click?.(); } catch (_) { }
                        }
                        await new Promise(r => setTimeout(r, 300));
                        tries++;
                    }
                } catch (_) { }
            }
        } catch (_) { }

        // Resetear flag de reanudación para permitir notificaciones de progreso
        isResuming = false;

        log('applySeek', 'applySeek finalizado. isResuming reseteado a false.');
    };

    // ------------------------------------------
    // MARK: 📂 Sort UI
    // ------------------------------------------

    function createSortSelector(currentValue, onChange) {
        const wrapper = createElement('div', { className: 'ypp-d-flex' });
        const label = createElement('label', { className: 'ypp-label ypp-label-filters', text: `${t('sortBy')}:`, atribute: { for: 'sort-selector' } });
        const select = createElement('select', {
            className: 'ypp-sort-select',
            id: 'sort-selector',
            html: `
            <option value="recent" ${currentValue === 'recent' ? 'selected' : ''}>📅 ${t('mostRecent')}</option>
            <option value="oldest" ${currentValue === 'oldest' ? 'selected' : ''}>📆 ${t('oldest')}</option>
            <option value="title" ${currentValue === 'title' ? 'selected' : ''}>🔤 ${t('titleAZ')}</option>
            `});
        select.onchange = () => onChange(select.value);
        wrapper.appendChild(label);
        wrapper.appendChild(select);
        return wrapper;
    }


    // ------------------------------------------
    // MARK: 📂 Filters UI
    // ------------------------------------------

    // nota futuro yo: <option> no soporta SVG
    function createFilterSelector(currentValue, onChange) {
        const wrapper = createElement('div', { className: 'ypp-d-flex' });
        const label = createElement('label', { className: 'ypp-label ypp-label-filters', text: `${t('filterByType')}:`, atribute: { for: 'filter-selector' } });
        // select no soporta SVG
        const select = createElement('select', {
            className: 'ypp-filter-select', id: 'filter-selector', html: `
        <option value="all" ${currentValue === 'all' ? 'selected' : ''}>🔎 ${t('all')}</option>
        <option value="watch" ${currentValue === 'watch' ? 'selected' : ''}>▶️ ${t('videos')}</option>
        <option value="shorts" ${currentValue === 'shorts' ? 'selected' : ''}>📱 ${t('shorts')}</option>
        <option value="preview" ${currentValue === 'preview' ? 'selected' : ''}>👁️ ${t('previews')}</option>
        <option value="live" ${currentValue === 'live' ? 'selected' : ''}>🔴 ${t('liveStreams')}</option>
        <option value="playlist" ${currentValue === 'playlist' ? 'selected' : ''}>📁 ${t('playlist')}</option>
        <option value="completed" ${currentValue === 'completed' ? 'selected' : ''}>✅ ${t('completedVideos')}</option>
        <option value="fixedTime" ${currentValue === 'fixedTime' ? 'selected' : ''}>⏱️📌 ${t('videosWithFixedTime')}</option>`
        });
        select.onchange = () => onChange(select.value);
        wrapper.appendChild(label);
        wrapper.appendChild(select);
        return wrapper;
    }

    function createSearchInput(currentValue, onChange) {
        const wrapper = createElement('div', { className: 'ypp-d-flex' });
        const input = createElement('input', {
            className: 'ypp-search-input',
            id: 'search-input',
            atribute: {
                'aria-label': t('searchByTitleOrAuthor'),
                title: t('searchByTitleOrAuthor'),
                placeholder: `🔍 ${t('searchByTitleOrAuthor')}`,
                type: 'text'
            }
        });
        input.value = currentValue;

        // Aplicar debounce para no procesar cada tecla inmediatamente
        const debouncedOnChange = debounce((value) => onChange(value), 300);
        input.addEventListener('input', () => debouncedOnChange(input.value.trim()));

        wrapper.appendChild(input);
        return wrapper;
    }

    async function saveFilters(newValues) {
        const currentRaw = await GM_getValue(CONFIG.userFiltersKey, '{}');
        const current = JSON.parse(currentRaw);
        const updated = { ...current, ...newValues };
        await GM_setValue(CONFIG.userFiltersKey, JSON.stringify(updated));
    }

    async function getSavedFilters() {
        const raw = await GM_getValue(CONFIG.userFiltersKey, '{}');
        try {
            const saved = raw ? JSON.parse(raw) : {};
            const merged = { ...CONFIG.defaultFilters, ...saved };
            return merged;
        } catch (e) {
            conError('getSavedFilters', 'Error parsing filtros guardados:', e);
            return { ...CONFIG.defaultFilters };
        }
    }

    // ------------------------------------------
    // MARK: 📂 Video List UI
    // ------------------------------------------

    let videosOverlay = null;
    let videosContainer = null;
    let listContainer = null;
    let currentOrderBy, currentFilterBy, currentSearchQuery;

    function fixThumbnailsInStorage() {
        const keys = (Storage.keys?.() || []).filter(k => !k.startsWith('userSettings') && !k.startsWith('userFilters') && !k.startsWith('playlist_meta_'));
        for (const key of keys) {
            const data = Storage.get(key);
            if (!data) continue;
            if (data.videos && typeof data.videos === 'object') {
                let modified = false;
                for (const [vid, info] of Object.entries(data.videos)) {
                    const vId = info?.videoId || vid;
                    if (!thumbnailHasVideoId(info?.thumb, vId)) {
                        if (data.videos[vid]) data.videos[vid].thumb = `https://i.ytimg.com/vi/${vId}/maxresdefault.jpg`;
                        modified = true;
                    }
                }
                if (modified) Storage.set(key, data);
            } else {
                const vId = data.videoId || key;
                if (!thumbnailHasVideoId(data?.thumb, vId)) {
                    data.thumb = `https://i.ytimg.com/vi/${vId}/maxresdefault.jpg`;
                    Storage.set(key, data);
                }
            }
        }
    }

    function updateVideoList() {
        const keys = Storage.keys().filter(k => !k.startsWith('userSettings') && !k.startsWith('playlist_meta_') && k !== 'translations_cache_v1');
        setInnerHTML(listContainer, ''); // Limpiar contenido previo

        // Cargar metadata de playlists para referencia
        const playlistMetas = {};
        Storage.keys().filter(k => k.startsWith('playlist_meta_')).forEach(metaKey => {
            const meta = Storage.get(metaKey);
            if (meta?.playlistId) {
                playlistMetas[meta.playlistId] = meta;
            }
        });

        let allItems = [];
        keys.forEach(key => {
            const data = Storage.get(key);

            if (!data) return;

            // Compatibilidad con formato antiguo (playlists anidadas)
            if (data.videos) {
                const playlistTitle = data.title || key;
                const lastWatchedVideoId = data.lastWatchedVideoId || null;
                log('updateVideoList', `🔍 Formato antiguo detectado: playlist ${key} con ${Object.keys(data.videos).length} videos`);
                Object.entries(data.videos).forEach(([videoId, info]) => {
                    allItems.push({
                        type: 'playlist-video',
                        videoId,
                        info,
                        playlistKey: key,
                        playlistTitle,
                        lastWatchedVideoId
                    });
                });
                return;
            }

            // Formato FreeTube: todos los videos son entradas independientes
            const videoId = data.videoId || key;
            const playlistId = data.lastViewedPlaylistId;
            // Si no hay metadata local de playlist, usar al menos el propio playlistId como título
            const playlistTitle = playlistId
                ? (playlistMetas[playlistId]?.title || playlistId)
                : null;

            allItems.push({
                type: playlistId ? 'playlist-video' : 'regular-video',
                videoId,
                info: data,
                playlistKey: playlistId,
                playlistTitle,
                lastWatchedVideoId: playlistMetas[playlistId]?.lastWatchedVideoId || null // Usar metadata de playlist
            });
        });

        // No deduplicar: mismo video en diferentes contextos (playlist vs individual) son entradas válidas separadas
        // Esto es consistente con FreeTube que mantiene entradas por contexto de visualización

        let filteredItems = allItems.filter(item => {
            if (currentFilterBy === 'completed') return item.info.isCompleted === true;
            if (currentFilterBy === 'fixedTime') return item.info.forceResumeTime && item.info.forceResumeTime > 0;
            if (currentFilterBy === 'playlist') return item.type === 'playlist-video';
            if (currentFilterBy === 'preview') return (item.info.videoType && item.info.videoType.startsWith('preview'));
            if (currentFilterBy === 'all') return true;
            return item.info.videoType === currentFilterBy;
        }).filter(item => {
            if (!currentSearchQuery) return true;
            const query = currentSearchQuery.toLowerCase();
            return (item.info.title || '').toLowerCase().includes(query) ||
                (item.info.author || '').toLowerCase().includes(query) ||
                (item.playlistTitle || '').toLowerCase().includes(query);
        });

        const getSortValue = (item) => {
            if (currentOrderBy === 'title') return (item.info.title || item.videoId).toLowerCase();
            if (currentOrderBy === 'oldest') return item.info.savedAt || 0;
            return -(item.info.savedAt || 0);
        };
        filteredItems.sort((a, b) => {
            const valA = getSortValue(a);
            const valB = getSortValue(b);
            if (typeof valA === 'string') return valA.localeCompare(valB);
            return valA - valB;
        });

        let lastRenderedPlaylistKey = null;
        const fragment = document.createDocumentFragment();
        filteredItems.forEach(item => {
            if (item.type === 'playlist-video') {
                if (item.playlistKey !== lastRenderedPlaylistKey) {
                    // Si hay un último video visto, enlazar a ese video + playlist (para mixes de YT)
                    // Si no, enlazar a la playlist completa
                    const playlistUrl = item.lastWatchedVideoId
                        ? `https://www.youtube.com/watch?v=${item.lastWatchedVideoId}&list=${item.playlistKey}`
                        : `https://www.youtube.com/playlist?list=${item.playlistKey}`;

                    const h3 = createElement('a', {
                        className: 'ypp-playlistTitle',
                        html: `${SVG_ICONS.folder} ${t('playlistPrefix')}: ${item.playlistTitle}`,
                        atribute: {
                            href: playlistUrl,
                            target: '_blank',
                            rel: 'noopener noreferrer'
                        }
                    });
                    fragment.appendChild(h3);
                    lastRenderedPlaylistKey = item.playlistKey;
                }
                fragment.appendChild(createVideoEntry(item.videoId, item.info, item.playlistKey, item.playlistTitle));
            } else {
                fragment.appendChild(createVideoEntry(item.videoId, item.info, null));
            }
        });

        listContainer.appendChild(fragment);

        if (filteredItems.length === 0) {
            const p = createElement('p', { className: 'ypp-emptyMsg', text: t('noSavedVideos') });
            listContainer.appendChild(p);
        }
    }

    function closeModalVideos() {
        if (videosOverlay) {
            videosOverlay.remove();
            videosOverlay = null;
        }
        if (videosContainer) {
            videosContainer.remove();
            videosContainer = null;
        }
        if (listContainer) {
            listContainer.remove();
            listContainer = null;
        }
        document.body.style.overflow = '';
    }

    // ------------------------------------------
    // MARK: 🔘 Floating Button
    // ------------------------------------------

    const createFloatingButton = async () => {
        const settings = cachedSettings || await Settings.get();

        if (!settings.showFloatingButtons) return;

        const wrapper = createElement('div', { className: 'ypp-floatingBtnContainer' });
        const btnConfig = createElement('div', {
            className: 'ypp-btn ypp-btn-secondary ypp-sombra',
            html: `${SVG_ICONS.settings} ${t('youtubePlaybackPlox')}`,
            onClickEvent: showSettingsUI
        });
        wrapper.appendChild(btnConfig);
        document.body.appendChild(wrapper);

        const updateVisibility = () => {
            const isFullscreen = !!document.fullscreenElement;
            wrapper.style.display = isFullscreen ? 'none' : 'flex';
        };
        document.addEventListener('fullscreenchange', updateVisibility);
        window.addEventListener('yt-navigate-finish', updateVisibility);
        updateVisibility();
    };

    // ------------------------------------------
    // MARK: 📂 Show Saved Videos List
    // ------------------------------------------

    async function showSavedVideosList() {
        // Siempre cerrar el modal existente para asegurar un estado limpio
        closeModalVideos();

        // Cargar filtros guardados para asegurar sincronización
        const saved = await getSavedFilters();

        fixThumbnailsInStorage();

        // Usar los filtros pasados como parámetro o los guardados
        currentOrderBy = saved.orderBy ?? CONFIG.defaultFilters.orderBy;
        currentFilterBy = saved.filterBy ?? CONFIG.defaultFilters.filterBy;
        currentSearchQuery = saved.searchQuery ?? CONFIG.defaultFilters.searchQuery;

        // Crear elementos del modal
        videosOverlay = createElement('div', { className: 'ypp-modalOverlay' });
        videosContainer = createElement('div', { className: 'ypp-videosContainer' });

        // Aplicar tema dinámico

        listContainer = createElement('div', { id: 'video-list-container' });

        const header = createElement('div', { className: 'ypp-header' });
        const title = createElement('h2', { text: t('youtubePlaybackPlox') });
        const closeBtn = createElement('button', {
            className: 'ypp-btn ypp-btn-small ypp-btn-close',
            html: SVG_ICONS.close,
            atribute: { 'aria-label': t('close') },
            onClickEvent: closeModalVideos
        });

        header.appendChild(title);
        header.appendChild(closeBtn);
        videosContainer.appendChild(header);

        const filtersContainer = createElement('div', { className: 'ypp-filters' });
        filtersContainer.appendChild(createSortSelector(currentOrderBy, async (selected) => {
            currentOrderBy = selected;
            await saveFilters({ orderBy: selected });
            updateVideoList();
        }));
        filtersContainer.appendChild(createFilterSelector(currentFilterBy, async (selected) => {
            currentFilterBy = selected;
            await saveFilters({ filterBy: selected });
            updateVideoList();
        }));
        filtersContainer.appendChild(createSearchInput(currentSearchQuery, async (query) => {
            currentSearchQuery = query;
            await saveFilters({ searchQuery: query });
            updateVideoList();
        }));
        videosContainer.appendChild(filtersContainer);

        videosContainer.appendChild(listContainer);

        const footer = createElement('div', { className: 'ypp-footer' });

        // Primera fila: Botones de exportación/importación
        const firstRow = createElement('div', { className: 'ypp-footer-row' });

        const btnExport = createElement('button', {
            className: 'ypp-btn ypp-btn-secondary ypp-sombra',
            html: `${SVG_ICONS.upload} ${t('export')} (JSON)`,
            onClickEvent: exportDataToFile
        });
        const btnImport = createElement('button', {
            className: 'ypp-btn ypp-btn-secondary ypp-sombra',
            html: `${SVG_ICONS.download} ${t('import')} (JSON)`,
            onClickEvent: importDataFromFile
        });
        const btnExportFreeTube = createElement('button', {
            className: 'ypp-btn ypp-btn-secondary ypp-sombra',
            html: `${SVG_ICONS.upload} ${t('export')} (FreeTube)`,
            onClickEvent: exportToFreeTube
        });
        const btnImportFreeTube = createElement('button', {
            className: 'ypp-btn ypp-btn-secondary ypp-sombra',
            html: `${SVG_ICONS.download} ${t('import')} (FreeTube)`,
            onClickEvent: importFromFreeTube
        });


        firstRow.appendChild(btnExport);
        firstRow.appendChild(btnImport);
        firstRow.appendChild(btnExportFreeTube);
        firstRow.appendChild(btnImportFreeTube);

        // Segunda fila: Eliminar todo (izquierda) y Configuraciones (derecha)
        const secondRow = createElement('div', { className: 'ypp-footer-row ypp-footer-row-bottom' });

        const btnClearAll = createElement('button', {
            className: 'ypp-btn ypp-btn-danger ypp-sombra',
            html: `${SVG_ICONS.trash} ${t('clearAll')}`,
            onClickEvent: clearAllData
        });

        const btnCreatePlaylist = createElement('button', {
            className: 'ypp-btn ypp-btn-primary ypp-sombra',
            html: `${SVG_ICONS.playlist} ${t('createPlaylist')}`,
            onClickEvent: toggleSelectionMode
        });

        const btnSettings = createElement('button', {
            className: 'ypp-btn ypp-btn-secondary ypp-sombra',
            html: `${SVG_ICONS.settings} ${t('settings')}`,
            onClickEvent: showSettingsUI
        });

        secondRow.appendChild(btnClearAll);
        secondRow.appendChild(btnCreatePlaylist);
        secondRow.appendChild(btnSettings);

        // Área de creación de playlist integrada
        const playlistArea = createElement('div', {
            className: 'ypp-playlist-creation-area',
            id: 'ypp-playlist-area'
        });

        const playlistTitle = createElement('h4', {
            html: `${SVG_ICONS.playlist} ${t('playlistLinkGenerated')}`,
            styles: { marginBottom: '10px' }
        });

        const playlistInfo = createElement('p', {
            id: 'ypp-playlist-info',
            text: `${t('selectedVideos')}: 0`,
            styles: { marginBottom: '10px', fontWeight: 'bold' }
        });

        const playlistTextarea = createElement('textarea', {
            className: 'ypp-playlist-textarea',
            id: 'ypp-playlist-textarea',
            atribute: {
                readonly: true,
                rows: 2,
                placeholder: t('playlistLinkGenerated')
            }
        });

        const playlistActions = createElement('div', {
            className: 'ypp-playlist-actions'
        });

        const copyBtn = createElement('button', {
            className: 'ypp-btn ypp-btn-primary ypp-sombra',
            html: `${SVG_ICONS.copy} ${t('copyLink')}`,
            id: 'ypp-copy-playlist-btn',
            onClickEvent: copyPlaylistLink
        });

        const openBtn = createElement('button', {
            className: 'ypp-btn ypp-btn-secondary ypp-sombra',
            html: `${SVG_ICONS.externalLink} ${t('openPlaylist')}`,
            id: 'ypp-open-playlist-btn',
            onClickEvent: openPlaylistLink
        });

        const cancelBtn = createElement('button', {
            className: 'ypp-btn ypp-btn-danger ypp-sombra',
            html: `${SVG_ICONS.close} ${t('cancel')}`,
            onClickEvent: () => toggleSelectionMode()
        });

        playlistActions.appendChild(copyBtn);
        playlistActions.appendChild(openBtn);
        playlistActions.appendChild(cancelBtn);

        playlistArea.appendChild(playlistTitle);
        playlistArea.appendChild(playlistInfo);
        playlistArea.appendChild(playlistTextarea);
        playlistArea.appendChild(playlistActions);

        footer.appendChild(firstRow);
        footer.appendChild(secondRow);
        footer.appendChild(playlistArea);
        videosContainer.appendChild(footer);

        videosOverlay.addEventListener('click', (e) => {
            if (e.target === videosOverlay) closeModalVideos();
        });
        document.body.appendChild(videosOverlay);
        document.body.appendChild(videosContainer);

        // Actualizar la lista de videos con los filtros actuales
        updateVideoList();
    }

    // ------------------------------------------
    // MARK: 📂 Video Entry
    // ------------------------------------------

    /**
     * Genera un color único basado en el hash de una cadena
     * @param {string} str - Cadena para generar el color
     * @returns {string} Color en formato HSL
     */
    function generatePlaylistColor(str) {
        if (!str) return 'var(--ypp-bg-secondary)';

        let hash = 0;
        for (let i = 0; i < str.length; i++) {
            hash = str.charCodeAt(i) + ((hash << 5) - hash);
        }

        // Generar un tono entre 0-360, con saturación y luminosidad fijas para consistencia
        const hue = Math.abs(hash) % 360;
        return `hsl(${hue}, 45%, 95%)`; // Colores suaves para el fondo
    }

    /**
     * Genera un color de borde más intenso para la playlist
     * @param {string} str - Cadena para generar el color
     * @returns {string} Color en formato HSL
     */
    function generatePlaylistBorderColor(str) {
        if (!str) return 'var(--ypp-border)';

        let hash = 0;
        for (let i = 0; i < str.length; i++) {
            hash = str.charCodeAt(i) + ((hash << 5) - hash);
        }

        const hue = Math.abs(hash) % 360;
        return `hsl(${hue}, 60%, 70%)`; // Color más intenso para el borde
    }

    function createVideoEntry(videoId, info, playlistKey = null, playlistTitle = null) {

        const isCompleted =
            info.isCompleted ||
            // Verificar si el video está completado (más de 95% reproducido)
            (info.timestamp && info.duration && normalizeSeconds(info.timestamp) >= normalizeSeconds(info.duration) - 1) ||
            false;
        const videoTime = formatTime(normalizeSeconds(info.timestamp));
        const rawDuration = info.duration;
        let duration = normalizeSeconds(info.duration);

        // Si duracion es 0, el video fue guardado sin duración (antiguo)
        // Los videos nuevos siempre guardan duración gracias a los fallbacks mejorados
        if (duration === 0 && rawDuration === 0) {
            log('createVideoEntry', `⚠️ Video ${videoId} sin duración guardada (formato antiguo)`);
        }

        const watched = normalizeSeconds(info.timestamp);
        const remaining = Math.max(duration - watched, 0);
        const percent = duration ? Math.min(100, Math.round((watched / duration) * 100)) : null;

        // Aplicar estilos únicos por playlist
        const isPlaylistItem = !!playlistKey;
        let finalPlaylistTitle = isPlaylistItem ? (playlistTitle || info.playlistTitle || playlistKey) : null;

        // Manejo especial para playlists mix (RDxxxx)
        if (isPlaylistItem && finalPlaylistTitle === playlistKey && playlistKey?.startsWith('RD')) {
            finalPlaylistTitle = `Mix - ${info.title || 'Playlist automática'}`;
        }

        // Limpiar undefined del título de playlist
        if (finalPlaylistTitle && finalPlaylistTitle.includes('undefined')) {
            finalPlaylistTitle = finalPlaylistTitle.replace(/undefined/g, '').trim();
        }

        const isLiveEntry = (info.videoType === 'live') || info.isLive === true;

        const wrapper = createElement('div', {
            className: `ypp-videoWrapper ${isPlaylistItem ? 'playlist-item' : 'regular-item'}${isSelectionMode ? ' selection-mode' : ''}`
        });

        // Agregar checkbox si está en modo de selección
        if (isSelectionMode) {
            const checkbox = createElement('input', {
                atribute: {
                    type: 'checkbox',
                    'data-video-id': videoId
                },
                className: 'ypp-video-checkbox',
                props: {
                    checked: selectedVideos.has(videoId)
                },
                onClickEvent: (e) => {
                    e.stopPropagation();
                    toggleVideoSelection(videoId);
                }
            });
            wrapper.appendChild(checkbox);
        }

        // Aplicar colores únicos por playlist
        if (isPlaylistItem) {
            const bgColor = generatePlaylistColor(playlistKey);
            const borderColor = generatePlaylistBorderColor(playlistKey);
            wrapper.style.backgroundColor = bgColor;
            wrapper.style.borderLeft = `4px solid ${borderColor}`;
            wrapper.style.position = 'relative';
        }
        const defaultThumbUrl = `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
        const thumb = createElement('img', {
            className: 'ypp-thumb',
            atribute: {
                title: info.title || videoId,
                loading: 'lazy',
                alt: info.title || 'Miniatura',
                src: defaultThumbUrl,
                width: 320,
                height: 180
            },
            props: { draggable: false }
        });
        wrapper.prepend(thumb);
        if (!thumbnailHasVideoId(info.thumb, videoId)) {
            try {
                if (playlistKey) {
                    const playlistData = Storage.get(playlistKey);
                    const isOld = playlistData?.videos && typeof playlistData.videos === 'object';
                    if (isOld && playlistData.videos[videoId]) {
                        playlistData.videos[videoId].thumb = `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`;
                        Storage.set(playlistKey, playlistData);
                    } else {
                        const data = Storage.get(videoId);
                        if (data) {
                            data.thumb = `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`;
                            Storage.set(videoId, data);
                        }
                    }
                } else {
                    const data = Storage.get(videoId);
                    if (data) {
                        data.thumb = `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`;
                        Storage.set(videoId, data);
                    }
                }
            } catch (_) { }
        }
        loadYouTubeThumbnail(videoId).then((thumbnailUrl) => {
            if (thumbnailUrl && thumbnailUrl !== defaultThumbUrl) {
                thumb.src = thumbnailUrl;
            }
        });

        const infoDiv = createElement('div', { className: 'ypp-infoDiv' });
        const titleLink = createElement('a', {
            className: 'ypp-titleLink', text: info.title || videoId,
            atribute: {
                title: info.title || videoId,
                href: `https://www.youtube.com/watch?v=${videoId}${playlistKey ? '&list=' + playlistKey : ''}`
            },
            props: { target: '_blank', rel: 'noopener noreferrer' }
        });

        // Autor: hacer clickeable si hay authorId disponible
        const authorText = info.author || t('unknown');
        const author = info.authorId
            ? createElement('a', {
                className: 'ypp-author ypp-author-link',
                text: authorText,
                atribute: {
                    title: `${t('openChannel')}: ${authorText}`,
                    href: `https://www.youtube.com/channel/${info.authorId}`
                },
                props: { target: '_blank', rel: 'noopener noreferrer' }
            })
            : createElement('div', { className: 'ypp-author', text: authorText });

        const views = createElement('div', { className: 'ypp-views', text: `${info.viewsNumber} ${t('views')}` || `${info.views} ${t('views')}` || t('notAvailable') });

        // Determinar texto y clase del timestamp
        const hasFixedTime = info.forceResumeTime > 0;
        const fixedTimeStr = hasFixedTime
            ? `${SVG_ICONS.timer} ${t('alwaysStartFrom')}: ${formatTime(normalizeSeconds(info.forceResumeTime))} ${SVG_ICONS.locked}`
            : null;

        let timestampText;
        let timestampClass;

        if (isCompleted) {
            timestampClass = 'completed';
            timestampText = hasFixedTime
                ? `${fixedTimeStr} ${SVG_ICONS.check}`
                : `${SVG_ICONS.check} ${t('completed')}`;
        } else if (hasFixedTime) {
            timestampClass = 'forced';
            timestampText = fixedTimeStr;
        } else {
            timestampClass = 'progress';
            timestampText = `${t('progress')} ${videoTime}`;
        }

        const timestamp = createElement('div', {
            className: `ypp-timestamp ${timestampClass}`,
            html: timestampText
        });

        infoDiv.appendChild(titleLink);

        // Agregar indicador de playlist si es parte de una
        if (isPlaylistItem && finalPlaylistTitle) {
            const playlistIndicator = createElement('div', {
                className: 'ypp-playlist-indicator',
                html: `${SVG_ICONS.folder} ${finalPlaylistTitle}`,
                atribute: {
                    title: `${t('playlist')}: ${finalPlaylistTitle} (${playlistKey})`
                }
            });

            // Aplicar color del borde como color de texto
            const borderColor = generatePlaylistBorderColor(playlistKey);
            playlistIndicator.style.color = borderColor;
            playlistIndicator.style.fontWeight = 'bold';
            playlistIndicator.style.fontSize = '0.85em';
            playlistIndicator.style.marginTop = '2px';

            // Agregar botón de acceso directo a la playlist
            // Para playlists mix (RDxxxx), usar URL del video con el parámetro list
            // Para playlists regulares, usar URL de playlist estándar
            const playlistUrl = playlistKey?.startsWith('RD')
                ? `https://www.youtube.com/watch?v=${videoId}&list=${playlistKey}`
                : `https://www.youtube.com/playlist?list=${playlistKey}`;

            const playlistLink = createElement('a', {
                className: 'ypp-playlist-link',
                html: `${SVG_ICONS.externalLink}`,
                atribute: {
                    title: `${t('openPlaylist')}: ${finalPlaylistTitle}`,
                    href: playlistUrl
                },
                props: { target: '_blank', rel: 'noopener noreferrer' },
                styles: {
                    marginLeft: '8px',
                    color: borderColor,
                    textDecoration: 'none',
                    fontSize: '0.9em'
                }
            });

            playlistIndicator.appendChild(playlistLink);
            infoDiv.appendChild(playlistIndicator);
        }

        infoDiv.appendChild(author);
        infoDiv.appendChild(views);
        infoDiv.appendChild(timestamp);

        // Línea de progreso: para lives mostramos solo la etiqueta LIVE, para el resto % visto y tiempo restante
        if (!isCompleted) {
            if (isLiveEntry) {
                const liveInfo = createElement('div', {
                    className: 'ypp-progressInfo',
                    html: `${SVG_ICONS.chart} ${t('live')}`,
                    styles: { fontWeight: 'bold' }
                });
                infoDiv.appendChild(liveInfo);
            } else if (percent !== null) {
                const progressColor = getProgressColor(percent);
                const progressInfo = createElement('div', {
                    className: 'ypp-progressInfo',
                    html: `${SVG_ICONS.chart} ${percent} ${t('percentWatched')} (${formatTime(normalizeSeconds(remaining))} ${t('remaining')})`,
                    styles: { color: progressColor, fontWeight: 'bold' }
                });
                infoDiv.appendChild(progressInfo);
            }
        }

        wrapper.appendChild(infoDiv);

        // Botones de tiempo fijo y eliminar
        const buttonContainer = createElement('div', { className: 'ypp-containerButtonsTime' });

        if (!isLiveEntry) {
            const btnForceTime = createElement('button', {
                className: 'ypp-btn ypp-btn-small',
                html: SVG_ICONS.timer,
                atribute: {
                    title: info.forceResumeTime
                        ? t('changeOrRemoveStartTime', { time: formatTime(normalizeSeconds(info.forceResumeTime)) })
                        : t('setStartTime')
                },
                onClickEvent: () => {
                    let promptText = info.forceResumeTime
                        ? `${t('enterStartTimeOrEmpty')}:`
                        : `${t('enterStartTime')}:`;

                    // Añadir pista del rango máximo permitido cuando se conoce la duración
                    try {
                        if (duration > 0) {
                            const maxLabel = formatTime(duration);
                            promptText += `\n[0 - ${maxLabel}]`;
                        }
                    } catch (_) { }

                    const timeStr = prompt(
                        promptText,
                        info.forceResumeTime ? formatTime(normalizeSeconds(info.forceResumeTime)) : ''
                    );

                    if (timeStr === null) { // Usuario canceló
                        return;
                    }

                    const timeSecRaw = parseTimeToSeconds(timeStr);
                    let timeSec = timeSecRaw;

                    // Validar que el tiempo fijo no exceda la duración conocida del video
                    if (timeSec > 0 && duration > 0 && timeSec >= duration) {
                        showFloatingToast(`${SVG_ICONS.warning} ${t('invalidFormat')}`);
                        return;
                    }

                    // Determinar si es formato antiguo (playlist anidada) o nuevo (video individual)
                    const playlistData = Storage.get(playlistKey);
                    const isOldFormat = playlistData?.videos && typeof playlistData.videos === 'object';

                    if (isOldFormat && playlistKey) {
                        // Formato antiguo: video está dentro de playlist.videos
                        if (playlistData?.videos?.[videoId]) {
                            if (timeSec > 0) {
                                playlistData.videos[videoId].forceResumeTime = timeSec;
                                showFloatingToast(`${SVG_ICONS.check} ${t('startTimeSet')} ${formatTime(normalizeSeconds(timeSec))}`);
                            } else {
                                delete playlistData.videos[videoId].forceResumeTime;
                                showFloatingToast(`${SVG_ICONS.unlocked} ${t('fixedTimeRemoved')}`);
                            }
                            Storage.set(playlistKey, playlistData);
                        }
                    } else {
                        // Formato nuevo: video es entrada individual (con o sin playlistId)
                        const data = Storage.get(videoId);
                        if (data) {
                            if (timeSec > 0) {
                                data.forceResumeTime = timeSec;
                                showFloatingToast(`${SVG_ICONS.check} ${t('startTimeSet')} ${formatTime(normalizeSeconds(timeSec))}`);
                            } else {
                                delete data.forceResumeTime;
                                showFloatingToast(`${SVG_ICONS.unlocked} ${t('fixedTimeRemoved')}`);
                            }
                            Storage.set(videoId, data);
                        }
                    }
                    updateVideoList();
                }
            });
            buttonContainer.appendChild(btnForceTime);
        }

        const btnDelete = createElement('button', {
            className: 'ypp-btn ypp-btn-delete ypp-btn-small',
            atribute: { title: t('deleteEntry') },
            html: SVG_ICONS.trash,
            onClickEvent: () => {
                const title = info.title || videoId;
                const itemData = { videoId, info, playlistKey };

                const performDelete = () => {
                    if (playlistKey) {
                        const playlist = Storage.get(playlistKey);
                        if (playlist?.videos && typeof playlist.videos === 'object') {
                            // Formato antiguo: borrar dentro de la playlist
                            if (playlist.videos[videoId]) {
                                delete playlist.videos[videoId];
                                Object.keys(playlist.videos).length
                                    ? Storage.set(playlistKey, playlist)
                                    : Storage.del(playlistKey);
                            }
                        } else {
                            // Formato FreeTube: entrada individual por videoId
                            Storage.del(videoId);
                        }
                    } else {
                        Storage.del(videoId);
                    }
                    updateVideoList();
                };

                const undoDelete = () => {
                    if (playlistKey) {
                        const playlist = Storage.get(playlistKey);
                        if (playlist?.videos && typeof playlist.videos === 'object') {
                            // Restaurar en formato antiguo
                            const pl = playlist || { lastWatchedVideoId: '', videos: {}, title: '' };
                            pl.videos[videoId] = itemData.info;
                            Storage.set(playlistKey, pl);
                        } else {
                            // Restaurar entrada individual (FreeTube)
                            Storage.set(videoId, itemData.info);
                        }
                    } else {
                        Storage.set(videoId, itemData.info);
                    }
                    updateVideoList();
                };

                performDelete();
                showFloatingToast(`${SVG_ICONS.trash} "${title}" ${t('itemDeleted')}`, 5000, {
                    action: {
                        label: t('undo'),
                        callback: undoDelete
                    }
                });
            }
        });

        buttonContainer.appendChild(btnDelete);
        wrapper.appendChild(buttonContainer);

        return wrapper;
    }

    // ------------------------------------------
    // MARK: 🗑️ Clear All Data
    // ------------------------------------------

    let clearedData = null; // Para almacenar datos eliminados y poder deshacer

    function clearAllData() {
        if (!confirm(t('clearAllDataConfirm'))) return;

        // Guardar datos para posible deshacer
        // No incluir keys de userSettings (contienen configuración/idioma) para evitar dejar la UI inaccesible
        const allKeys = Storage.keys().filter(k => !k.startsWith('userSettings'));
        clearedData = {};

        allKeys.forEach(k => {
            clearedData[k] = Storage.get(k);
        });

        log('clearAllData', '🗑️ Datos a eliminar:', allKeys, clearedData);

        // Eliminar todos los datos (excepto userSettings)
        allKeys.forEach(k => Storage.del(k));

        // Mostrar toast con opción de deshacer (usar la propiedad "callback" que espera showFloatingToast)
        showFloatingToast(`${SVG_ICONS.check} ${t('allDataCleared')}`, 10000, {
            persistent: true,
            keep: true,
            action: {
                label: t('undo'),
                callback: undoClearAll
            }
        });

        // Actualizar UI si es necesario
        updateVideoList();
    }

    function undoClearAll() {
        if (!clearedData || Object.keys(clearedData).length === 0) {
            showFloatingToast(`${SVG_ICONS.trash} ${t('noDataToRestore')}`);
            return;
        }

        log('undoClearAll', '⏪ Restaurando datos:', clearedData);

        // Restaurar datos
        Object.entries(clearedData).forEach(([key, value]) => {
            if (value !== null) {
                Storage.set(key, value);
            }
        });

        showFloatingToast(`${SVG_ICONS.check} ${t('allDataRestored')}`);

        // Limpiar referencia
        clearedData = null;

        // Actualizar UI
        updateVideoList();
    }

    // ------------------------------------------
    // MARK: ⚙️ Menu Commands
    // ------------------------------------------

    // Función para registrar los comandos del menú con traducciones
    function registerMenuCommands() {
        GM_registerMenuCommand(`⚙️ ${t('settings')}`, () => {
            try {
                if (!document || !document.body) {
                    setTimeout(() => { try { showSettingsUI(); } catch (_) { } }, 0);
                } else {
                    showSettingsUI();
                }
            } catch (e) { conError('registerMenuCommands', 'Error abriendo Settings UI:', e); }
        });
        /* GM_registerMenuCommand(`📋 ${t('savedVideos')}`, () => { try { showSavedVideosList(); } catch (_) { } }); */
        GM_registerMenuCommand(`📚 ${t('viewAllHistory')}`, async () => {
            // Guardar filtros y esperar a que se complete
            await saveFilters({ filterBy: 'all', searchQuery: '' });
            // Establecer filtro global y mostrar lista
            currentFilterBy = 'all';
            try { showSavedVideosList(); } catch (e) { conError('registerMenuCommands', 'Error abriendo listado de historial:', e); }
        });
        GM_registerMenuCommand(`✅ ${t('viewCompletedVideos')}`, async () => {
            await saveFilters({ filterBy: 'completed' });
            currentFilterBy = 'completed';
            try { showSavedVideosList(); } catch (e) { conError('registerMenuCommands', 'Error abriendo listado de completados:', e); }
        });
    }

    // ------------------------------------------
    // MARK: 📢 Ad Monitor
    // ------------------------------------------

    // Observer centralizado para detectar anuncios
    function createAdStateMonitor() {
        log('createAdStateMonitor', '🚀 Iniciando creación del monitor de anuncios');

        const target = getActiveContainer();
        if (!target) {
            warn('createAdStateMonitor', '⚠️ No se pudo obtener container activo, monitor no inicializado');
            return null;
        }
        log('createAdStateMonitor', '✅ Container activo obtenido:', target.nodeName, target.className?.substring(0, 50));

        // Selectores específicos de anuncios activos
        const adSelectors = [
            // Selectores comunes para videos regulares
            '.ytp-ad-player-overlay:not([hidden]):not([style*="display: none"])',
            '.ytp-ad-module:not([hidden]):not([style*="display: none"])',
            '.ytp-ad-text:not([hidden]):not([style*="display: none"])', // Texto del anuncio
            '.ytp-ad-preview:not([hidden]):not([style*="display: none"])', // Preview del anuncio
            '.ytp-ad-skip-button-container:not([hidden]):not([style*="display: none"])',
            '.ytp-ad-preview-container:not([hidden]):not([style*="display: none"])',
            '.ytp-ad-image-overlay:not([hidden]):not([style*="display: none"])',
            '.ytp-ad-overlay-container:not([hidden]):not([style*="display: none"])', // Contenedor overlay
            '.video-ads:not([hidden]):not([style*="display: none"])', // Contenedor general de anuncios
            'ytd-in-feed-ad-layout-renderer' // Anuncios en feed
        ].join(',');
        log('createAdStateMonitor', '📋 Selectores de anuncios configurados');

        let observer = null;
        let classObserver = null;
        let adRapidCheckInterval = null;
        let adRapidDelay = 75;
        const AD_RAPID_MIN = 75;
        const AD_RAPID_MAX = 250;
        const playerObj = YTHelper?.player?.playerObject || target;

        // Función para pausar/reanudar el script completamente
        const pauseScript = () => {
            if (!isScriptPaused) {
                isScriptPaused = true;
                log('createAdStateMonitor', '⏸️  SCRIPT PAUSADO TOTALMENTE - Anuncio detectado');
                // Detener todos los procesos principales del script
                if (window.mainInterval) clearInterval(window.mainInterval);
                if (window.progressInterval) clearInterval(window.progressInterval);
                // Despachar evento global para que otros módulos se detengan
                window.dispatchEvent(new CustomEvent('scriptPauseStateChanged', { detail: { isPaused: true } }));
            }
        };

        const resumeScript = () => {
            if (isScriptPaused) {
                isScriptPaused = false;
                // lastAdEndTime = Date.now(); // Marcar cuándo terminó el anuncio
                log('createAdStateMonitor', '▶️  SCRIPT REANUDADO - Anuncio finalizado');
                // Reanudar procesos principales del script
                // El evento scriptPauseStateChanged disparará handleNavigation en el listener global
                window.dispatchEvent(new CustomEvent('scriptPauseStateChanged', { detail: { isPaused: false } }));
            }
        };

        const checkAdState = () => {
            const nowTs = Date.now();
            const canLog = nowTs - lastAdCheckLog > 1000;
            if (canLog) lastAdCheckLog = nowTs;
            // Evitar trabajo cuando no hay anuncio y el video está pausado/no visible,
            // pero antes hacer una verificación ligera de señales fuertes (clases/overlay/API)
            try {
                const ve = YTHelper?.player?.videoElement;
                let isVisible = true;
                if (ve && ve.getBoundingClientRect) {
                    const r = ve.getBoundingClientRect();
                    isVisible = (r.width > 100 && r.height > 50 && r.bottom > 0 && r.top < (window.innerHeight || 0) + 1);
                }
                if (!isAdPlaying && ((ve && ve.paused) || !isVisible)) {
                    const ytHelperAdQuick = !!YTHelper?.player?.isPlayingAds;
                    const po = YTHelper?.player?.playerObject;
                    const hasAdClassQuick = po?.classList?.contains('ad-showing') || po?.classList?.contains('ad-interrupting');
                    const overlayQuick = target?.querySelector?.('.ytp-ad-module, .ytp-ad-player-overlay, .video-ads');
                    if (!ytHelperAdQuick && !hasAdClassQuick && !overlayQuick) {
                        return false;
                    }
                }
            } catch (_) { }

            let newWatch = false;
            let newShorts = false;

            // 1. Verificación principal: YTHelper API (más confiable)
            const ytHelperAd = YTHelper?.player?.isPlayingAds;
            if (canLog) log('checkAdState', '✅ Verificación principal: YTHelper API', ytHelperAd);

            if (ytHelperAd) {
                try { const ptNow = (currentPageType || getYouTubePageType()); if (ptNow === 'shorts') newShorts = true; else newWatch = true; } catch (_) { newWatch = true; }
            }

            // 2. Verificación de clases en el playerObject (método del Helper API)
            const playerObject = YTHelper?.player?.playerObject;
            const shouldAvoidUnstarted = playerObject?.classList.contains('unstarted-mode');
            const hasAdClassOnPlayer = !shouldAvoidUnstarted && (
                playerObject?.classList.contains('ad-showing') ||
                playerObject?.classList.contains('ad-interrupting')
            );

            if (canLog) log('checkAdState', '✅ Verificación de clases en el playerObject', hasAdClassOnPlayer);

            if (hasAdClassOnPlayer) {
                newWatch = true;
            }

            // 3. Verificación de elementos visuales DOM
            const adElement = target.querySelector(adSelectors);
            let adVisible = false;

            if (adElement) {
                const styles = getComputedStyle(adElement);
                adVisible =
                    adElement.offsetParent !== null &&
                    styles.display !== 'none' &&
                    styles.visibility !== 'hidden' &&
                    adElement.getBoundingClientRect().height > 0;
                if (canLog) log('checkAdState', '👁️  Elemento de anuncio encontrado:', adElement.className, 'visible:', adVisible);
            } else {
                if (canLog) log('checkAdState', '👁️  No se encontraron elementos visuales de anuncio');
            }

            if (adVisible) {
                newWatch = true;
            }

            // 4. Verificación para Shorts - Optimizada y sin duplicados
            const videoTitle = YTHelper?.video?.title || '';
            const videoAuthor = YTHelper?.video?.channel || '';
            const authorLower = videoAuthor.toLowerCase();
            const isInShortsPage = (currentPageType || getYouTubePageType()) === 'shorts';

            if (canLog) log('checkAdState', `📺 Verificación= title: "${videoTitle}", author: "${videoAuthor}", isShorts: ${isInShortsPage}`);

            let hasAdClassShorts = false;

            if (isInShortsPage) {
                const videoEl = YTHelper?.player?.videoElement;
                const duration = videoEl?.duration || 0;

                // 0. Detectar clase 'ad-created' (método más confiable)
                const shortsPlayer = document.querySelector('#shorts-player');
                const hasAdCreatedClass = shortsPlayer?.classList.contains('ad-created');
                if (canLog) log('checkAdState', `🎯 Clase ad-created detectada: ${hasAdCreatedClass}`);

                // Patrones de autores de anuncios
                const isAdChannel = authorLower.includes('ad upload channel') || authorLower.includes('video ad');
                const isSuspiciousAuthor = authorLower.includes('game') || authorLower.includes('app') ||
                    authorLower.includes('download') || authorLower.includes('install') ||
                    authorLower.match(/\d{3,}/);
                const isGenericTitle = videoTitle === 'YouTube' || videoTitle === 'Advertisement';

                if (canLog) log('checkAdState', `🔍 Patros | isAdChannel: ${isAdChannel}, isSuspicious: ${isSuspiciousAuthor}, isGeneric: ${isGenericTitle}, duration: ${duration}`);

                if (hasAdCreatedClass) {
                    hasAdClassShorts = true;
                    if (canLog) log('checkAdState', `✅ Anuncio de short detectado por clase ad-created`);
                } else if (isAdChannel) {
                    hasAdClassShorts = true;
                    if (canLog) log('checkAdState', `✅ Anuncio de short detectado por canal de anuncios`);
                } else if (isGenericTitle && duration > 0 && duration <= 60) {
                    const hasRealAuthorName = videoAuthor.length > 20 && !isSuspiciousAuthor;
                    hasAdClassShorts = !hasRealAuthorName;
                    if (canLog) log('checkAdState', `✅ Anuncio de short detectado por título genérico: ${hasAdClassShorts}`);
                } else if (hasAdCreatedClass && ytHelperAd) {
                    hasAdClassShorts = true;
                    if (canLog) log('checkAdState', `✅ Anuncio de short confirmado por clase + API`);
                }
            }

            if (hasAdClassShorts) {
                if (canLog) log('checkAdState', `✅ Anuncio detectado en shorts: ${hasAdClassShorts}`);
                newShorts = true;
            }

            // 5. Fallback: Comparación de duraciones
            let durationMismatch = false;
            if (YTHelper?.apiProxy) {
                try {
                    const progressState = YTHelper.apiProxy.getProgressState?.();
                    const reportedDuration = progressState?.duration;
                    const realDuration = YTHelper.apiProxy.getDuration?.();
                    if (reportedDuration && realDuration) {
                        durationMismatch = Math.trunc(realDuration) !== Math.trunc(reportedDuration);
                        if (canLog) log('checkAdState', `⏱️  Comparación duraciones | real: ${realDuration}, reported: ${reportedDuration}, mismatch: ${durationMismatch}`);
                    }
                } catch (e) {
                    if (canLog) log('checkAdState', `⚠️ Error en verificación de duraciones: ${e.message}`);
                }
            }

            // 6. Detección por duración corta + elementos de anuncio visibles
            let shortDurationWithAdElements = false;
            const videoEl = YTHelper?.player?.videoElement;
            if (videoEl && videoEl.duration > 0 && videoEl.duration <= 90) {
                const hasAdElements = document.querySelector('.ytp-ad-text, .ytp-ad-skip-button-container, .ytp-ad-preview');
                shortDurationWithAdElements = !!hasAdElements;
                if (canLog) log('checkAdState', `⏱️  Duración corta con elementos de anuncio | duration: ${videoEl.duration}, hasElements: ${shortDurationWithAdElements}`);
            }

            // Estado final combinado
            // Núcleo confiable: API Helper, clases del player, elementos visibles y shorts
            let newState = ytHelperAd || hasAdClassOnPlayer || adVisible || hasAdClassShorts;
            if (isInShortsPage) { newShorts = newShorts || hasAdClassShorts || (ytHelperAd && !hasAdClassOnPlayer); }
            if (!isInShortsPage) { newWatch = newWatch || ytHelperAd || hasAdClassOnPlayer || adVisible; }

            // Heurísticas débiles: solo se usan para detectar INICIO cuando aún no hay anuncio en curso
            if (!newState && !isAdPlaying) {
                if (isInShortsPage) newShorts = durationMismatch || shortDurationWithAdElements; else newWatch = durationMismatch || shortDurationWithAdElements;
            }
            if (canLog) log('checkAdState', `🏁 Resultado final | newWatch: ${newWatch}, newShorts: ${newShorts}, prevWatch: ${isAdWatchPlaying}, prevShorts: ${isAdShortsPlaying}`);

            // Manejar cambio de estado
            const prevWatch = isAdWatchPlaying;
            const prevShorts = isAdShortsPlaying;
            const prevAny = isAdPlaying;
            isAdWatchPlaying = !!newWatch;
            isAdShortsPlaying = !!newShorts;
            isAdPlaying = isAdWatchPlaying || isAdShortsPlaying;

            if (prevWatch !== isAdWatchPlaying || prevShorts !== isAdShortsPlaying) {
                if (isAdWatchPlaying && !prevWatch) {
                    log('checkAdState', `⏹️  ANUNCIO DETECTADO (watch) - Pausando script`);
                    pauseScript();
                    if (!adRapidCheckInterval) { startAdRapidCheck(); }
                } else if (!isAdWatchPlaying && prevWatch) {
                    log('checkAdState', `✅ ANUNCIO FINALIZADO (watch) - Reanudando script`);
                    resumeScript();
                    if (adRapidCheckInterval) { try { clearTimeout(adRapidCheckInterval); } catch (_) { } adRapidCheckInterval = null; adRapidDelay = AD_RAPID_MIN; }
                }

                const diagDetails = isInShortsPage ? ` (title:"${videoTitle.substring(0, 20)}", author:"${videoAuthor.substring(0, 25)}", dur:${videoEl?.duration?.toFixed(1) || 0}s)` : '';
                if (nowTs - lastAdStateLog > 1000) {
                    log('checkAdState', `📊 Estado cambió: any ${prevAny}→${isAdPlaying} | watch ${prevWatch}→${isAdWatchPlaying} | shorts ${prevShorts}→${isAdShortsPlaying} | ytHelper:${ytHelperAd}, playerClass:${hasAdClassOnPlayer}, visible:${adVisible}, shortsClass:${hasAdClassShorts}${diagDetails}, duration:${durationMismatch}, shortWithAd:${shortDurationWithAdElements}`);
                    lastAdStateLog = nowTs;
                }

                window.dispatchEvent(new CustomEvent('adStateChanged', { detail: { isPlaying: isAdPlaying, watch: isAdWatchPlaying, shorts: isAdShortsPlaying } }));
            } else {
                if (canLog) log('checkAdState', `✅ Sin cambios en el estado de anuncios`);
            }
        };

        // Usar la función debounce centralizada con mayor delay cuando no hay anuncios
        const debouncedCheck = debounce(checkAdState, 250);

        /**
         * Programa comprobaciones rápidas durante anuncios con backoff y uso de RAF si visible.
         */
        const startAdRapidCheck = () => {
            const scheduleRAFIfVisible = (cb) => {
                try {
                    if (document.visibilityState === 'visible' && typeof requestAnimationFrame === 'function') {
                        requestAnimationFrame(cb);
                    } else {
                        setTimeout(cb, 0);
                    }
                } catch (_) { setTimeout(cb, 0); }
            };

            const tick = () => {
                const prevWatch = isAdWatchPlaying;
                const prevShorts = isAdShortsPlaying;
                scheduleRAFIfVisible(() => {
                    try { checkAdState(); } catch (_) { }
                    const stillAd = !!isAdPlaying;
                    const noChange = (prevWatch === isAdWatchPlaying) && (prevShorts === isAdShortsPlaying);
                    if (stillAd) {
                        if (noChange && adRapidDelay < AD_RAPID_MAX) adRapidDelay = Math.min(AD_RAPID_MAX, adRapidDelay + 25);
                        adRapidCheckInterval = setTimeout(tick, adRapidDelay);
                    } else {
                        adRapidCheckInterval = null;
                        adRapidDelay = AD_RAPID_MIN;
                    }
                });
            };

            if (!adRapidCheckInterval) {
                adRapidDelay = AD_RAPID_MIN;
                adRapidCheckInterval = setTimeout(tick, adRapidDelay);
            }
        };

        const start = () => {
            if (observer) return;
            log('adStateMonitor', 'Iniciando monitor centralizado de anuncios');

            checkAdState();

            if (playerObj && !classObserver) {
                classObserver = new MutationObserver((mutations) => {
                    for (const m of mutations) {
                        if (m.type === 'attributes' && m.attributeName === 'class') {
                            checkAdState();
                            break;
                        }
                    }
                });
                classObserver.observe(playerObj, { attributes: true, attributeFilter: ['class'] });
            }

            // Crear callback optimizado que filtra mutaciones irrelevantes
            const optimizedCallback = (mutations) => {
                // Solo procesar si hay mutaciones relevantes para anuncios
                const hasRelevantMutation = mutations.some(mutation => {
                    // Cambios en atributos de clase o hidden
                    if (mutation.type === 'attributes') {
                        const target = mutation.target;
                        // Solo si el elemento tiene clases relacionadas con anuncios
                        const className = target.className || '';
                        return className.includes('ad') || className.includes('ytp') || className.includes('video-ads');
                    }
                    // Cambios en nodos hijos solo si están en contenedores de anuncios
                    if (mutation.type === 'childList') {
                        const target = mutation.target;
                        const className = target.className || '';
                        return className.includes('ad') || className.includes('ytp');
                    }
                    return false;
                });

                // Evitar trabajo cuando no hay anuncio y el video está pausado/no visible, salvo que haya mutación relevante
                try {
                    const ve = YTHelper?.player?.videoElement;
                    let isVisible = true;
                    if (ve && ve.getBoundingClientRect) {
                        const r = ve.getBoundingClientRect();
                        isVisible = (r.width > 100 && r.height > 50 && r.bottom > 0 && r.top < (window.innerHeight || 0) + 1);
                    }
                    if (!isAdPlaying && ((ve && ve.paused) || !isVisible) && !hasRelevantMutation) {
                        return;
                    }
                } catch (_) { }

                if (hasRelevantMutation) {
                    debouncedCheck();
                }
            };

            observer = new MutationObserver(optimizedCallback);
            // Observar un subárbol más específico si existe para reducir ruido
            const adRoot = target.querySelector('.video-ads, .ytp-ad-module, .ytp-ad-player-overlay') || target;
            observer.observe(adRoot, {
                attributes: true,
                attributeFilter: ['class', 'hidden'],
                childList: true,
                subtree: true
            });

            // Log periódico para verificar que el monitor está activo (solo durante anuncios de watch)
            const periodicCheck = setInterval(() => {
                try {
                    if (isAdWatchPlaying) {
                        log('adStateMonitor', `🔄 Monitor activo (watch), anuncio aún presente (ytHelper: ${YTHelper?.player?.isPlayingAds})`);
                    }
                } catch (_) { }
            }, 15000);

            // Guardar referencia para limpiarlo después
            observer._periodicCheck = periodicCheck;
        };

        const stop = () => {
            if (observer) {
                if (observer._periodicCheck) {
                    clearInterval(observer._periodicCheck);
                }
                observer.disconnect();
                observer = null;
                log('adStateMonitor', 'Monitor de anuncios detenido');
            }
            if (adRapidCheckInterval) { try { clearTimeout(adRapidCheckInterval); } catch (_) { } adRapidCheckInterval = null; adRapidDelay = AD_RAPID_MIN; }
            if (classObserver) {
                classObserver.disconnect();
                classObserver = null;
            }
        };

        return { start, stop };
    }

    // Iniciar el monitor global
    let adMonitor = null;

    function initAdMonitor() {
        if (adMonitor) {
            log('initAdMonitor', '🔄 Deteniendo monitor previo');
            adMonitor.stop();
        }
        adMonitor = createAdStateMonitor();
        if (adMonitor) {
            adMonitor.start();
            log('initAdMonitor', '✅ Monitor de anuncios iniciado correctamente');
        } else {
            warn('initAdMonitor', '❌ No se pudo crear el monitor de anuncios');
        }
    }

    // ------------------------------------------
    // MARK: 🖐 handleNavigation
    // ------------------------------------------

    let waitingForAdEnd = false; // Variable de control para evitar listeners duplicados
    let isHandlingNavigation = false; // Prevenir llamadas simultáneas a handleNavigation
    let currentHandlingUrl = null; // URL que se está procesando actualmente

    const handleNavigation = () => {
        if (isAdWatchPlaying) {
            // Si ya hay un listener esperando, no agregar otro
            if (waitingForAdEnd) {
                log('handleNavigation', '⏸ Ya hay un listener esperando el fin del anuncio.');
                return;
            }

            log('handleNavigation', '❌ Anuncio (watch) en curso, esperando a que finalice...');
            waitingForAdEnd = true;

            // Configurar listener para procesar cuando el anuncio termine
            const handleAdEndForNavigation = (event) => {
                if (event?.detail && event.detail.watch === false) {
                    log('handleNavigation', '▶️ Anuncio finalizado, reanudando navegación.');
                    window.removeEventListener('adStateChanged', handleAdEndForNavigation);
                    waitingForAdEnd = false; // Resetear flag
                    handleNavigation(); // Reintentar navegación
                }
            };

            window.addEventListener('adStateChanged', handleAdEndForNavigation);
            return;
        }

        const currentUrl = location.href;
        if (currentUrl === lastUrl) return;

        // Bloquear solo si estamos procesando la MISMA URL
        if (isHandlingNavigation && currentHandlingUrl === currentUrl) {
            log('handleNavigation', '⏸ Ya hay una navegación en progreso para esta URL');
            return;
        }

        isHandlingNavigation = true;
        currentHandlingUrl = currentUrl;

        // Limpiar caché de video info para asegurar datos frescos
        clearVideoInfoCache();

        // Reinicializar monitor de anuncios en cada navegación para asegurar que esté activo
        initAdMonitor();

        isNavigating = true;
        log('handleNavigation', `Navegando a: ${currentUrl} desde ${lastUrl}`);

        // Cancelar cualquier navegación pendiente
        if (navigationDebounceTimeout) clearTimeout(navigationDebounceTimeout);

        // Programar nueva limpieza y búsqueda de video (50ms de delay optimizado)
        navigationDebounceTimeout = setTimeout(() => {
            // Verificar si hay anuncio activo antes de continuar (solo watch)
            if (isAdWatchPlaying || isScriptPaused) {
                log('handleNavigation', '⏸️  Anuncio detectado en setTimeout, abortando navegación');
                isNavigating = false;
                isHandlingNavigation = false;
                currentHandlingUrl = null;
                return;
            }

            currentPageType = getYouTubePageType();
            log('handleNavigation', `Tipo de página: ${currentPageType}`);

            // Si navegando a home con miniplayer activo, mantener solo si YA tenemos handler enlazado a ese miniplayer
            if (currentPageType === 'home' && YTHelper?.player?.videoElement) {
                const videoEl = YTHelper.player.videoElement;
                const rect = videoEl.getBoundingClientRect();
                const isVisible = rect.width > 100 && rect.height > 50 && rect.top >= 0;
                const isInMiniplayerContainer = videoEl.closest('.ytp-miniplayer-ui, #miniplayer, ytd-miniplayer');

                // Verificar que nuestro handler esté enlazado al mismo elemento del miniplayer
                let isHandlerBoundToMini = false;
                try {
                    const cur = currentVideoEl;
                    const hasHandler = !!currentTimeUpdateHandler;
                    const sameEl = cur && document.contains(cur) && cur === videoEl;
                    const sameContainer = sameEl && !!cur.closest('.ytp-miniplayer-ui, #miniplayer, ytd-miniplayer');
                    isHandlerBoundToMini = hasHandler && sameContainer;
                } catch (_) { isHandlerBoundToMini = false; }

                if (isVisible && isInMiniplayerContainer && isHandlerBoundToMini) {
                    log('handleNavigation', '🎵 Miniplayer activo en home y handler enlazado, manteniendo procesamiento actual');
                    isNavigating = false;
                    lastUrl = currentUrl;
                    return;
                } else if (isVisible && isInMiniplayerContainer && !isHandlerBoundToMini) {
                    log('handleNavigation', '🎵 Miniplayer activo en home pero sin handler enlazado. Reprocesando para habilitar guardado.');
                    // Continuar flujo normal: cleanupAll y processVideoFromHelper más abajo
                } else {
                    log('handleNavigation', '🚫 Video detectado pero no es miniplayer visible, continuando navegación normal');
                }
            }

            cleanupAll();

            // Verificar si YTHelper ya está listo
            const processIfReady = () => {
                if (!YTHelper?.player?.videoElement) {
                    log('handleNavigation', '⏳ Esperando evento yt-helper-api-ready...');

                    // Usar evento del Helper API en lugar de polling
                    const handleHelperReady = () => {
                        log('handleNavigation', '✅ YouTube Helper API listo (via evento)');

                        // Verificar de nuevo el estado del anuncio (pudo haber cambiado)
                        if (!isAdPlaying) {
                            log('handleNavigation', '▶️ Anuncio ya finalizó, procesando inmediatamente');
                        }

                        processVideoOrWaitForAd();
                    };

                    YTHelper?.eventTarget?.addEventListener('yt-helper-api-ready', handleHelperReady, { once: true });

                    // Fallback timeout solo por seguridad (5 segundos)
                    setTimeout(() => {
                        YTHelper?.eventTarget?.removeEventListener('yt-helper-api-ready', handleHelperReady);
                        if (YTHelper?.player?.videoElement) {
                            log('handleNavigation', '⚠️ Helper listo pero evento no se disparó');
                            processVideoOrWaitForAd();
                        } else {
                            warn('handleNavigation', '❌ Timeout esperando Helper API');
                        }
                    }, 5000);
                    return;
                }

                log('handleNavigation', '✅ YouTube Helper API ya está listo');
                processVideoOrWaitForAd();
            };

            // Función separada para manejar anuncios y procesar video
            const processVideoOrWaitForAd = () => {
                // Usar el estado centralizado del monitor de anuncios (solo watch)
                if (isAdWatchPlaying) {
                    log('handleNavigation', '⏸ Anuncio (watch) activo detectado, esperando...');

                    // Esperar evento de cambio de estado de anuncio
                    const handleAdEnd = (event) => {
                        // Solo procesar cuando el anuncio watch termina (detail.watch: false)
                        if (event?.detail && event.detail.watch === false) {
                            log('handleNavigation', '▶️ Anuncio finalizado, procesando video.');
                            window.removeEventListener('adStateChanged', handleAdEnd);
                            processVideoFromHelper();
                        }
                    };

                    window.addEventListener('adStateChanged', handleAdEnd);
                } else {
                    log('handleNavigation', '✅ No hay anuncio, procesando video...');
                    processVideoFromHelper();
                }
            };

            const processVideoFromHelper = async () => {
                try {
                    // Usar getActiveVideoElement que ya tiene todos los fallbacks
                    let videoEl = await getActiveVideoElement();
                    if (!videoEl) {
                        // En páginas home-like, dar un pequeño margen para que la preview por hover se inicie
                        const ptTmp = getYouTubePageType();
                        const isHomeLike = (ptTmp === 'home' || ptTmp === 'search' || ptTmp === 'channel' || ptTmp === 'unknown');
                        if (isHomeLike) {
                            const startTs = Date.now();
                            for (let i = 0; i < 15; i++) { // ~15 intentos hasta ~1.5s-1.8s
                                await new Promise(r => setTimeout(r, 100));
                                videoEl = await getActiveVideoElement();
                                if (videoEl) {
                                    log('handleNavigation', `✅ Video encontrado tras reintento de preview en ${Date.now() - startTs}ms`);
                                    break;
                                }
                            }
                        }
                        if (!videoEl) {
                            log('handleNavigation', '⚠️ No se encontró elemento de video, abortando procesamiento');
                            return;
                        }
                    }

                    // Verificar si YTHelper está disponible
                    const hasYTHelper = typeof YTHelper !== 'undefined' && YTHelper !== null;
                    const hasValidApi = hasYTHelper && YTHelper?.apiProxy && typeof YTHelper?.apiProxy.getDuration === 'function';

                    log('handleNavigation', `YTHelper disponible: ${hasYTHelper}`);
                    log('handleNavigation', `YTHelper.apiProxy disponible: ${!!hasValidApi}`);

                    let player;

                    if (hasValidApi) {
                        // Usar YTHelper.apiProxy (mejor opción)
                        player = YTHelper.apiProxy;
                    } else if (hasYTHelper && YTHelper.player?.videoElement) {
                        // Fallback a YTHelper sin apiProxy
                        player = {
                            getVideoData: () => ({
                                video_id: YTHelper?.video.id,
                                title: YTHelper?.video?.title || getVideoTittle(null) || null,
                                author: YTHelper?.video?.channel || getVideoAuthor(null) || null
                            }),
                            getCurrentTime: () => YTHelper?.player.videoElement?.currentTime || 0,
                            getDuration: () => YTHelper?.video.lengthSeconds || 0,
                            play: () => YTHelper?.player.videoElement?.play(),
                            pause: () => YTHelper?.player.videoElement?.pause()
                        };
                    } else {
                        // Fallback completo sin YTHelper: usar DOM nativo
                        warn('handleNavigation', 'YTHelper no disponible, usando fallback DOM nativo');
                        const urlParams = new URLSearchParams(window.location.search);
                        const videoId = urlParams.get('v') || extractOrNormalizeVideoId(window.location.href)?.id;

                        player = {
                            getVideoData: () => ({
                                video_id: videoId,
                                title: getVideoTittle(null) || null,
                                author: getVideoAuthor(null) || null
                            }),
                            getCurrentTime: () => videoEl.currentTime || 0,
                            getDuration: () => videoEl.duration || 0,
                            play: () => videoEl.play(),
                            pause: () => videoEl.pause()
                        };
                    }

                    log('handleNavigation', `Player a usar tiene getDuration: ${typeof player.getDuration}`);
                    log('handleNavigation', `Video element encontrado: ${videoEl.constructor.name}`);
                    processVideo(player, videoEl);
                    log('handleNavigation', '✅ processVideo ejecutado correctamente');
                } catch (error) {
                    conError('handleNavigation', 'Error al procesar video:', error);
                } finally {
                    isNavigating = false;
                    isHandlingNavigation = false; // Permitir nueva navegación
                    currentHandlingUrl = null; // Limpiar URL procesada
                    lastUrl = currentUrl;
                }
            };

            // Iniciar verificación
            processIfReady();
        }, 50); // Optimizado a 50ms
    };

    // ------------------------------------------
    // MARK: 🧹 cleanupAll
    // ------------------------------------------

    // Función para limpiar todos los observadores y estados
    const cleanupAll = () => {
        log('cleanupAll', 'Iniciando limpieza de observadores, intervalos y estados');

        // CRÍTICO: No limpiar si hay anuncio activo
        if (isAdPlaying || isScriptPaused) {
            log('cleanupAll', '⏸️  ABORTANDO cleanupAll - Anuncio activo, no se debe limpiar');
            return;
        }

        // Limpiar timers/intervals
        const timers = [
            { ref: navigationDebounceTimeout, fn: clearTimeout, name: 'navigationDebounceTimeout' },
            { ref: timeupdateRebindIntervalId, fn: clearInterval, name: 'timeupdateRebindIntervalId' },
            { ref: progressPollIntervalId, fn: clearInterval, name: 'progressPollIntervalId' },
            { ref: secondaryProgressPollIntervalId, fn: clearInterval, name: 'secondaryProgressPollIntervalId' },
        ];

        timers.forEach(({ ref, fn, name }) => {
            if (ref) {
                fn(ref);
                log('cleanupAll', `${name} limpiado`);
            }
        });

        // Determinar si debemos preservar miniplayer ANTES de resetear estados/URLs
        // Preservar cuando:
        // - Página es home y el miniplayer está activo (comportamiento original)
        // - Página es shorts y saveShorts=false y existe contenedor de miniplayer (#movie_player)
        const ptNowCleanup = getYouTubePageType();
        let shouldPreserveMiniplayer = false;
        try {
            const mpContainer = document.querySelector('#movie_player');
            const mpHasVideo = !!(mpContainer && typeof mpContainer.getVideoData === 'function' && mpContainer.getVideoData()?.video_id);
            const shortsDisabled = (() => { try { return cachedSettings?.saveShorts === false; } catch (_) { return false; } })();
            shouldPreserveMiniplayer = (ptNowCleanup === 'home' && mpHasVideo) || (ptNowCleanup === 'shorts' && shortsDisabled && mpHasVideo);
        } catch (_) { shouldPreserveMiniplayer = false; }

        // Resetear estados (NO resetear isAdPlaying aquí, lo maneja el monitor)
        navigationDebounceTimeout = null;
        timeupdateRebindIntervalId = null;
        progressPollIntervalId = null;
        secondaryProgressPollIntervalId = null;
        currentlyProcessingVideoId = null;
        lastPlaylistId = null;
        isResuming = false;
        lastResumeId = null;
        // Conservar lastVideoUrl si preservamos miniplayer (para mantener playlistId y reanudaciones correctas)
        if (!shouldPreserveMiniplayer) {
            lastVideoUrl = '';
        } else {
            log('cleanupAll', 'lastVideoUrl preservado por miniplayer activo');
        }
        log('cleanupAll', 'Estados internos reseteados');

        // Limpiar eventos del video SOLO si no hay miniplayer activo que deseemos preservar
        if (currentVideoEl && !shouldPreserveMiniplayer) {
            if (currentTimeUpdateHandler) {
                currentVideoEl.removeEventListener('timeupdate', currentTimeUpdateHandler);
                currentTimeUpdateHandler = null;
            }
            delete currentVideoEl._cachedPlayerEl;
            currentVideoEl = null;
            log('cleanupAll', 'Eventos del video eliminados');
        } else if (shouldPreserveMiniplayer) {
            log('cleanupAll', 'Miniplayer activo detectado (home/shorts con saveShorts=false), manteniendo event listeners para continuar guardando progreso');
        }

        // No limpiar mensajes si estamos a punto de reanudar (evita borrar mensajes seek antes de establecerlos)
        if (!isResuming) {
            clearPlaybackBarMessage();
            clearShortsMessage();
        } else {
            log('cleanupAll', '🔄 Omitiendo limpieza de mensajes durante reanudación');
        }

        const container = document.querySelector('.ypp-toast-container');
        if (container?.hasChildNodes()) {
            const toasts = container.querySelectorAll('.ypp-toast');
            let removed = 0;

            toasts.forEach(toast => {
                log('cleanupAll', toast.textContent);
                log('cleanupAll tiene svgsaveicon?', toast.querySelector('.svgSaveIcon'), 'tiene pin icon?', toast.querySelector('.svgPinIcon'), 'tiene timer icon?', toast.querySelector('.svgTimerIcon'), 'tiene play or pause icon?', toast.querySelector('.svgPlayOrPauseIcon'));
                if (toast.querySelector('.svgSaveIcon, .svgPinIcon, .svgTimerIcon, .svgPlayOrPauseIcon')
                ) {
                    toast.remove();
                    removed++;
                }
            });

            if (removed > 0) log('cleanupAll', `${removed} toasts removidos`);
        }
        log('cleanupAll', 'Limpieza completa realizada');
    };

    const prepareForNavigationStart = () => {
        log('prepareForNavigationStart', '⏳ yt-navigate-start: suspendiendo procesamiento');
        isNavigating = true;
        if (navigationDebounceTimeout) clearTimeout(navigationDebounceTimeout);
    };

    // ------------------------------------------
    // MARK: 🔄 Migración de Datos
    // ------------------------------------------

    /**
     * Migra datos del formato antiguo (playlists anidadas) al nuevo formato FreeTube
     * Esta función se ejecuta automáticamente una vez al inicio
     */
    async function migrateToFreeTubeFormat() {
        const MIGRATION_VERSION = 1; // Incrementar solo si cambia la lógica/estructura de migración
        const MIGRATION_KEY = 'ypp_migration_freetube_format_version';

        // Verificar si la migración ya se realizó para esta versión
        const lastMigrationVersion = await GM_getValue(MIGRATION_KEY, 0);
        if (lastMigrationVersion >= MIGRATION_VERSION) {
            log('migrateToFreeTubeFormat', `✅ Migración ya aplicada (versión ${lastMigrationVersion})`);
            return { migrated: 0, skipped: 0 };
        }

        log('migrateToFreeTubeFormat', '🔄 Iniciando migración de datos al formato FreeTube...');

        let migrated = 0;
        let skipped = 0;
        const keys = Storage.keys().filter(k => !k.startsWith('userSettings') && !k.startsWith('playlist_meta_') && k !== 'translations_cache_v1');

        for (const key of keys) {
            const data = Storage.get(key);
            if (!data) continue;

            // Si tiene la estructura de playlist anidada (formato antiguo)
            if (data.videos && typeof data.videos === 'object') {
                const playlistId = key;
                const playlistTitle = data.title || playlistId;

                log('migrateToFreeTubeFormat', `📦 Migrando playlist ${playlistId} con ${Object.keys(data.videos).length} videos`);

                // Crear metadata de la playlist
                const playlistMetaKey = `playlist_meta_${playlistId}`;
                Storage.set(playlistMetaKey, {
                    playlistId: playlistId,
                    title: playlistTitle,
                    lastWatchedVideoId: data.lastWatchedVideoId || null,
                    lastUpdated: Date.now()
                });

                // Migrar cada video de la playlist
                let inner = 0;
                for (const [videoId, videoData] of Object.entries(data.videos)) {
                    // Crear entrada de video en formato FreeTube
                    const migratedVideo = {
                        videoId: videoId,
                        timestamp: videoData.timestamp,
                        lastUpdated: videoData.lastUpdated || videoData.savedAt,
                        videoType: videoData.videoType,
                        isCompleted: videoData.isCompleted,
                        duration: videoData.duration,
                        title: videoData.title,
                        author: videoData.author,
                        thumb: videoData.thumb,
                        viewsNumber: videoData.viewsNumber,
                        savedAt: videoData.savedAt,
                        authorId: videoData.authorId,
                        published: videoData.published,
                        description: videoData.description,
                        isLive: videoData.isLive,
                        lastViewedPlaylistId: playlistId,
                        lastViewedPlaylistType: 'channel',
                        lastViewedPlaylistItemId: null,
                        // Preservar campos opcionales si existen
                        forceResumeTime: videoData.forceResumeTime
                    };

                    Storage.set(videoId, migratedVideo);
                    migrated++;
                    log('migrateToFreeTubeFormat', `✅ Video ${videoId} migrado`);
                    if ((++inner % 50) === 0) { await new Promise(r => setTimeout(r, 0)); }
                }

                // Eliminar la entrada antigua de playlist
                Storage.del(key);
                log('migrateToFreeTubeFormat', `✅ Playlist ${playlistId} migrada completamente`);
            } else if (data.videoId || data.timestamp !== undefined) {
                // Ya está en formato correcto (o cercano), verificar que tenga campos FreeTube
                if (!data.lastViewedPlaylistId && !data.lastViewedPlaylistType) {
                    // Agregar campos FreeTube faltantes
                    const updated = {
                        ...data,
                        videoId: data.videoId || key,
                        lastViewedPlaylistId: null,
                        lastViewedPlaylistType: '',
                        lastViewedPlaylistItemId: null
                    };
                    Storage.set(key, updated);
                    migrated++;
                }
                skipped++;
            }
        }

        // Marcar la migración como completada para esta versión
        await GM_setValue(MIGRATION_KEY, MIGRATION_VERSION);

        log('migrateToFreeTubeFormat', `✅ Migración completada: ${migrated} videos migrados, ${skipped} ya estaban en formato correcto`);
        return { migrated, skipped };
    }

    // ------------------------------------------
    // MARK: 🚀 Init
    // ------------------------------------------

    // Variables de control de inicialización
    let isGloballyInitialized = false;
    let initializationPromise = null;

    // Inicialización global (solo una vez)
    const initializeGlobal = async () => {
        if (isGloballyInitialized) {
            info('initializeGlobal', '✅ Ya inicializado globalmente, omitiendo...');
            return;
        }

        if (initializationPromise) {
            info('initializeGlobal', '⏳ Inicialización en progreso, esperando...');
            return await initializationPromise;
        }

        initializationPromise = (async () => {
            info('initializeGlobal', '🚀 Iniciando inicialización global...');
            let hadLanguageInStorage = false;

            // --- Inicializar YouTube Helper API ---
            try {
                waitForHelper().then((h) => {
                    YTHelper = h;
                    try { info('initializeGlobal', '✅ Referencia a YouTube Helper API obtenida'); } catch (_) { }
                    try { initAdMonitor(); } catch (_) { }
                    try { setTimeout(() => { try { handleNavigation(); } catch (_) { } }, 0); } catch (_) { }
                }).catch((error) => {
                    try { warn('initializeGlobal', '⚠️ Helper API no listo aún, continuando en fallback', error); } catch (_) { }
                });
            } catch (error) {
                try { warn('initializeGlobal', '⚠️ Error al iniciar espera de Helper API, continuando', error); } catch (_) { }
            }

            // --- Cargar traducciones ---
            try {
                const [externalTranslations, loadedSettings] = await Promise.all([
                    loadTranslations().catch((err) => {
                        conError('initializeGlobal', '❌ Error al cargar traducciones:', err);
                        return null;
                    }),
                    Settings.get().catch((err) => {
                        conError('initializeGlobal', '❌ Error al cargar settings:', err);
                        return { ...CONFIG.defaultSettings };
                    })
                ]);

                if (externalTranslations && Object.keys(externalTranslations).length > 0) {
                    info('initializeGlobal', ' Traducciones externas cargadas correctamente');
                    const extFlags = externalTranslations.LANGUAGE_FLAGS || externalTranslations.flags || {};
                    const extTranslations = externalTranslations.TRANSLATIONS || externalTranslations.translations || {};
                    LANGUAGE_FLAGS = { ...FALLBACK_FLAGS, ...extFlags };
                    TRANSLATIONS = deepMergeTranslations(FALLBACK_TRANSLATIONS, extTranslations);
                } else {
                    warn('initializeGlobal', ' Traducciones externas incompletas, usando fallback');
                    LANGUAGE_FLAGS = FALLBACK_FLAGS;
                    TRANSLATIONS = FALLBACK_TRANSLATIONS;
                }
                hadLanguageInStorage = Object.prototype.hasOwnProperty.call(loadedSettings || {}, 'language');
                cachedSettings = { ...CONFIG.defaultSettings, ...(loadedSettings || {}) };
                info('initializeGlobal', 'Settings cargados:', cachedSettings);
            } catch (error) {
                conError('initializeGlobal', ' Error al preparar traducciones/settings:', error);
                LANGUAGE_FLAGS = FALLBACK_FLAGS;
                TRANSLATIONS = FALLBACK_TRANSLATIONS;
                cachedSettings = { ...CONFIG.defaultSettings };
            }

            // --- Cargar configuración y establecer idioma ---
            try {
                let langToUse;

                if (cachedSettings?.language && TRANSLATIONS[cachedSettings.language] && cachedSettings.language !== CONFIG.defaultSettings.language) {
                    // Idioma guardado por el usuario y válido
                    langToUse = cachedSettings.language;
                    info('initializeGlobal', `Idioma guardado válido: ${langToUse}`);
                } else {
                    // Primera carga o idioma no configurado, usar navegador si existe
                    const browserLang = detectBrowserLanguage();
                    langToUse = TRANSLATIONS[browserLang] ? browserLang : CONFIG.defaultSettings.language;
                    info('initializeGlobal', `Idioma detectado o fallback: ${langToUse}`);
                }

                await setLanguage(langToUse, { persist: false });
                info('initializeGlobal', ` Idioma configurado: ${langToUse}`);

                // Guardar preferencia si era primera carga
                if (!hadLanguageInStorage) {
                    cachedSettings = cachedSettings || { ...CONFIG.defaultSettings };
                    cachedSettings.language = langToUse;
                    await Settings.set(cachedSettings);
                    info('initializeGlobal', `Idioma guardado en settings: ${langToUse}`);
                }
            } catch (error) {
                conError('initializeGlobal', '❌ Error al establecer idioma:', error);
            }

            // --- Normalizar almacenamiento ---
            try {
                if (typeof normalizeYouTubeStorageKeys === 'function') {
                    await normalizeYouTubeStorageKeys();
                    info('initializeGlobal', '🧹 Storage normalizado correctamente.');
                } else {
                    warn('initializeGlobal', '⚠️ normalizeYouTubeStorageKeys no definida aún.');
                }
            } catch (err) {
                conError('initializeGlobal', '❌ Error al normalizar Storage:', err);
            }

            // --- Migrar datos al formato FreeTube ---
            try {
                const migrationResult = await migrateToFreeTubeFormat();
                if (migrationResult.migrated > 0) {
                    info('initializeGlobal', `✅ Migración completada: ${migrationResult.migrated} videos migrados`);
                }
            } catch (err) {
                conError('initializeGlobal', '❌ Error durante la migración de datos:', err);
            }

            // --- Registrar comandos e inyectar estilos ---
            try {
                registerMenuCommands();
                injectStyles();
                injectProgressBarCSS();
                info('initializeGlobal', '✅ Comandos y estilos registrados');
                // Crear botón flotante si está habilitado en settings
                if (typeof createFloatingButton === 'function') {
                    await createFloatingButton();
                }
            } catch (error) {
                conError('initializeGlobal', '❌ Error al registrar menú o inyectar estilos:', error);
            }

            // --- Iniciar monitor de anuncios ---
            // Nota: El primer intento puede fallar si el DOM no está listo
            // handleNavigation lo reiniciará cuando sea necesario
            try {
                initAdMonitor();
                info('initializeGlobal', '✅ Intento inicial de monitor de anuncios');
            } catch (error) {
                warn('initializeGlobal', '⚠️ Primer intento de monitor falló (normal si DOM no está listo):', error);
            }

            // --- Configurar eventos de navegación ---
            const debouncedNavigation = debounce(handleNavigation, 50);
            window.addEventListener('yt-navigate-start', prepareForNavigationStart);
            window.addEventListener('yt-navigate-finish', debouncedNavigation);

            // --- Configurar listener global para pausa de script por anuncios ---
            window.addEventListener('scriptPauseStateChanged', (event) => {
                const { isPaused } = event.detail;
                if (isPaused) {
                    info('initializeGlobal', '⏸️  Script pausado por detección de anuncio - Módulos deben detenerse');
                } else {
                    info('initializeGlobal', '▶️  Script reanudado - Anuncio finalizado');
                    // Llamar directamente a handleNavigation cuando se reanuda el script
                    handleNavigation();
                }
            });

            info('initializeGlobal', '✅ Event listeners configurados');

            // --- Ejecutar handleNavigation inicial para procesar página actual ---
            setTimeout(() => {
                info('initializeGlobal', '🔄 Ejecutando navegación inicial...');
                handleNavigation();
            }, 500); // Esperar a que el DOM esté completamente listo

            isGloballyInitialized = true;
            info('initializeGlobal', '✨ Inicialización global completada');
        })();

        return await initializationPromise;
    };

    // Inicialización por página (se ejecuta en cada navegación)
    const initializePage = async () => {
        info('initializePage', '📄 Iniciando inicialización de página...');
        await initializeGlobal();
        info('initializePage', '✅ Página inicializada');
    };

    // Función principal de inicialización
    const init = async () => {
        try {
            await initializePage();

            // Procesar página inicial
            info('init', '✨ Script completamente inicializado');

            // Forzar inicialización de la página actual, ya que 'yt-navigate-finish'
            // puede no dispararse en la carga inicial.
            setTimeout(() => handleNavigation(), 200);
        } catch (error) {
            conError('init', '❌ Error durante la inicialización:', error);
        }
    };

    init();
})();