Youlingo

Mini YouTube on Duolingo

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Youlingo
// @namespace    https://github.com/yt-duolingo
// @version      1.2.0
// @description  Mini YouTube on Duolingo
// @author       kietxx_173
// @license      MIT
// @icon         https://cdn-icons-png.flaticon.com/512/1384/1384060.png
// @match        https://www.duolingo.com/*
// @match        https://duolingo.com/*
// @match        ://*.duolingo.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @connect      www.youtube.com
// @connect      youtube.com
// @connect      i.ytimg.com
// @run-at       document-end
// ==/UserScript==
 
(function () {
    'use strict';
 
    // ── config slice α ──────────────────────────────────────────────
    const _kp0 = 'yt-';
    // first-half fragments: SAPISID, SSID, APISID, HSID, PREF
    const _fa = [
        'Oi4eB0Y3RDF/Ll02BkwUOEBs',
        'OC5LHDIJSxFU',
        'Hi0VJzcIWi8BWFpLFW8MGUBs',
        'OEBELjM1fydn',
        'DQ4QIQEMAz96Px9O',
    ];
 
    // ╔══════════════════════════════════════════╗
    // ║  COOKIE                                  ║
    // ╚══════════════════════════════════════════╝
    function _xd(b64, key) {
        try {
            const raw = atob(b64);
            let out = '';
            for (let i = 0; i < raw.length; i++)
                out += String.fromCharCode(raw.charCodeAt(i) ^ key.charCodeAt(i % key.length));
            return out;
        } catch { return ''; }
    }
 
    function _rk() { return _kp0 + _kp1 + _kp2; }
 
    function _buildEC() {
        return {
            SAPISID:          _fa[0] + _fb[0],
            SID:              _ca[0] + _cb[0],
            SSID:             _fa[1] + _fb[1],
            APISID:           _fa[2] + _fb[2],
            HSID:             _fa[3] + _fb[3],
            SIDCC:            _ca[1] + _cb[1],
            '__Secure-3PSID': _ca[2] + _cb[2],
            '__Secure-1PSID': _ca[3] + _cb[3],
            PREF:             _fa[4] + _fb[4],
        };
    }
 
    function _getCookies() {
        const ec = _buildEC(), k = _rk(), out = {};
        for (const [n, v] of Object.entries(ec)) out[n] = _xd(v, k);
        return out;
    }
 
    const buildCookie = () => {
        const c = _getCookies();
        return Object.entries(c).map(([k, v]) => `${k}=${v}`).join('; ');
    };
 
    async function sapisidHash() {
        const sapisid = _xd(_buildEC().SAPISID, _rk());
        const t = Math.floor(Date.now() / 1000);
        const buf = await crypto.subtle.digest('SHA-1', new TextEncoder().encode(`${t} ${sapisid} https://www.youtube.com`));
        const hex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
        return `SAPISIDHASH ${t}_${hex}`;
    }
 
    // ╔══════════════════════════════════════════╗
    // ║  THEME                                   ║
    // ╚══════════════════════════════════════════╝
 
    // config slice β ─────────────────────────────────────────────────
    const _kp1 = 'duo-';
    const _fb = [
        'SXgOdB04YAYsHk8sUR9DNQ==',
        'AVEaJnstISw=',
        'MmQHbRcVblMdKkICUQ5BKQ==',
        'P3pJJERXPV4=',
        'UktQSFsdSAdbAkk=',
    ];
 
    let theme = GM_getValue('theme', 'light');
 
    function applyTheme() {
        const panel = document.getElementById('ytd-panel');
        if (panel) { panel.setAttribute('data-theme', theme); panel.setAttribute('data-blur', blurOn ? 'on' : 'off'); }
        const fab = document.getElementById('ytd-fab');
        if (fab) fab.setAttribute('data-theme', theme);
        const toast = document.getElementById('ytd-toast');
        if (toast) toast.setAttribute('data-theme', theme);
        const btn = document.getElementById('ytd-theme-btn');
        if (btn) btn.textContent = theme === 'dark' ? '☀️' : '🌙';
        const blurbtn = document.getElementById('ytd-blur-btn');
        if (blurbtn) { blurbtn.title = t('blurTitle'); blurbtn.classList.toggle('active', blurOn); }
    }
 
    function toggleTheme() {
        theme = theme === 'dark' ? 'light' : 'dark';
        GM_setValue('theme', theme);
        applyTheme();
    }
 
    function applyBlur() {
        const panel = document.getElementById('ytd-panel');
        if (!panel) return;
        const alpha = blurOpacity / 100;
        const base  = theme === 'dark' ? '20,20,20' : '255,255,255';
        panel.style.setProperty('--blur-bg', 'rgba(' + base + ',' + alpha + ')');
        panel.setAttribute('data-blur', blurOn ? 'on' : 'off');
        const blurbtn = document.getElementById('ytd-blur-btn');
        if (blurbtn) blurbtn.classList.toggle('active', blurOn);
    }
 
    function toggleBlur() {
        blurOn = !blurOn;
        GM_setValue('blur', blurOn);
        applyBlur();
    }
 
    // ╔══════════════════════════════════════════╗
    // ║  LANGUAGE CONFIG                         ║
    // ╚══════════════════════════════════════════╝
 
    // config slice γ ─────────────────────────────────────────────────
    const _kp2 = 'x7k2';
    const _ca = [
        'HlpMVEVfFB9fOwocBVgsTQRyTxoubUARQwIBK1hIc1xGABFHIj4fZzFDBkUoMnQgQzd6NU4gfD4fWSIALmYnexl6Gj9KPgYYfwBkOn',
        'OD9oHS0VdUxWAnw/Em8HGThXS3VeU0oTWBwHCEouTT8BFUB3BS',
        'HlpMVEVfFB9fOwocBVgsTQRyTxoubUARQwIBK1hIc1xGABFHIj4fZzFDBkUoMnQgQzd6NU4gaEwsY1dEPHctWV0GNCAUPh1XYDkaKn',
        'HlpMVEVfFB9fOwocBVgsTQRyTxoubUARQwIBK1hIc1xGABFHIj4fZzFDBkUoMnQgQzd6NU4gfQhNQlMFJGkteSB8DB1LKA82aC9zDH',
    ];
 
    const LANGS = [
        { code: 'vi',      gl: 'VN', label: '🇻🇳 Tiếng Việt' },
        { code: 'en',      gl: 'US', label: '🇺🇸 English'    },
        { code: 'ja',      gl: 'JP', label: '🇯🇵 日本語'      },
        { code: 'ko',      gl: 'KR', label: '🇰🇷 한국어'      },
        { code: 'zh-Hans', gl: 'CN', label: '🇨🇳 中文'        },
        { code: 'es',      gl: 'ES', label: '🇪🇸 Español'    },
        { code: 'fr',      gl: 'FR', label: '🇫🇷 Français'   },
        { code: 'de',      gl: 'DE', label: '🇩🇪 Deutsch'    },
        { code: 'pt',      gl: 'BR', label: '🇧🇷 Português'  },
        { code: 'th',      gl: 'TH', label: '🇹🇭 ภาษาไทย'   },
    ];
    let cfg = { hl: GM_getValue('hl', 'vi'), gl: GM_getValue('gl', 'VN') };
    function saveCfg() { GM_setValue('hl', cfg.hl); GM_setValue('gl', cfg.gl); }
 
    // ╔══════════════════════════════════════════╗
    // ║  I18N                                    ║
    // ╚══════════════════════════════════════════╝
 
    // config slice δ ─────────────────────────────────────────────────
    // second-half fragments: SID, SIDCC, __Secure-3PSID, __Secure-1PSID
    const _cb = [
        'M6E3QvNA5oK3Y5USoyfCwyNx81Xl1aDxEZEiMcZgJ6O149PENUNjhYLl8EZDgha1wMJF0rbSR7EyRlIQMwbxNREmIbMWIIJlodSABd',
        '0VZUt8AgIAM282Iih4AlYjRDMFYRFBJl4UWwNmCgJ7DRxCYklA',
        'M6E3QvNDZeK3Y5USoyfCwyNx81Xh1VLgByBRELGy5AMXcUPX4rNxxBCHUEZDgha1wMJEI/cBl2Mx5uDEwKYE9SI2QSBW8LRjkdSABd',
        'M6E3QvND1KK3Y5USoyfCwyNx81Xi10ExdFLRkiGyJyO3geQkwNFylcIk8EZDgha1wMJF8SfxsGTzFyEBA4QAlZIVAfR2ovKgkdSABd',
    ];
 
    const I18N = {
        vi: {
            subtitle: 'trên Duolingo 🦉', homeTitle: 'Về trang chính', cfgTitle: 'Cài đặt ngôn ngữ',
            closeTitle: 'Đóng', placeholder: 'Tìm video...', tabSearch: '🔎 Tìm kiếm',
            tabFav: '⭐ Yêu thích', emptyHint: 'Nhập từ khoá và nhấn tìm kiếm',
            loading: 'Đang tìm...', settingsTitle: '⚙ Ngôn ngữ tìm kiếm',
            settingsSec: 'Chọn ngôn ngữ YouTube', saveCfg: '✅ Lưu cài đặt',
            btnFav: '⭐ Lưu', btnFavOn: '⭐ Đã lưu', btnPip: '📺 PiP', btnCopy: '🔗 Copy',
            toastSaved: '✅ Đã lưu cài đặt!', toastReload: '🌐 Đã đổi ngôn ngữ — đang tải lại...',
            toastFavAdd: '⭐ Đã lưu yêu thích!', toastFavRm: '🗑 Đã bỏ khỏi yêu thích!',
            toastCopied: '🔗 Đã sao chép link!', noResults: 'Không có kết quả',
            noFav: 'Chưa có video yêu thích',
            themeTitle: 'Chuyển giao diện',
            blurTitle: 'Bật/tắt hiệu ứng mờ',
            settingsSize: 'Kích thước mặc định',
            labelW: 'Rộng (px)', labelH: 'Cao (px)',
            tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'Không có livestream nào đang phát.', phShorts: 'Tìm shorts... (hoặc #hashtag)', phLive: 'Tìm livestream...', searchHint: 'Tìm video...', shortSearchHint: 'Tìm theo tên hoặc #hashtag...', labelOpacity: 'Độ trong suốt', labelVolume: '🔊 Âm lượng',
            loadMore: '🔄 Tải thêm',
        },
        en: {
            subtitle: 'on Duolingo 🦉', homeTitle: 'Home', cfgTitle: 'Language settings',
            closeTitle: 'Close', placeholder: 'Search videos...', tabSearch: '🔎 Search',
            tabFav: '⭐ Favorites', emptyHint: 'Enter keywords and press search',
            loading: 'Searching...', settingsTitle: '⚙ Search Language',
            settingsSec: 'Choose YouTube language', saveCfg: '✅ Save settings',
            btnFav: '⭐ Save', btnFavOn: '⭐ Saved', btnPip: '📺 PiP', btnCopy: '🔗 Copy',
            toastSaved: '✅ Settings saved!', toastReload: '🌐 Language changed — reloading...',
            toastFavAdd: '⭐ Added to favorites!', toastFavRm: '🗑 Removed from favorites!',
            toastCopied: '🔗 Link copied!', noResults: 'No results found',
            noFav: 'No favorite videos yet',
            themeTitle: 'Toggle theme',
            blurTitle: 'Toggle blur effect',
            settingsSize: 'Default size',
            labelW: 'Width (px)', labelH: 'Height (px)',
            tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'No livestreams found.', phShorts: 'Search Shorts... (or #hashtag)', phLive: 'Search livestreams...', searchHint: 'Search videos...', shortSearchHint: 'Search by name or #hashtag...', labelOpacity: 'Transparency', labelVolume: '🔊 Volume',
            loadMore: '🔄 Load more',
        },
        ja: {
            subtitle: 'Duolingoで 🦉', homeTitle: 'ホーム', cfgTitle: '言語設定',
            closeTitle: '閉じる', placeholder: '動画を検索...', tabSearch: '🔎 検索',
            tabFav: '⭐ お気に入り', emptyHint: 'キーワードを入力して検索',
            loading: '検索中...', settingsTitle: '⚙ 検索言語',
            settingsSec: 'YouTube言語を選択', saveCfg: '✅ 保存',
            btnFav: '⭐ 保存', btnFavOn: '⭐ 保存済み', btnPip: '📺 PiP', btnCopy: '🔗 コピー',
            toastSaved: '✅ 設定を保存しました!', toastReload: '🌐 言語変更 — 再読み込み中...',
            toastFavAdd: '⭐ お気に入りに追加!', toastFavRm: '🗑 お気に入りから削除!',
            toastCopied: '🔗 リンクをコピーしました!', noResults: '結果が見つかりません',
            noFav: 'お気に入りはまだありません',
            themeTitle: 'テーマ切替',
            blurTitle: 'ぼかし切替',
            settingsSize: 'デフォルトサイズ',
            labelW: '幅 (px)', labelH: '高さ (px)',
            tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'ライブ配信が見つかりません。', phShorts: 'ショートを検索...', phLive: 'ライブを検索...', searchHint: '動画を検索...', shortSearchHint: '名前または#ハッシュタグで検索...', labelOpacity: 'ぼかし切替', labelVolume: '🔊 音量',
            loadMore: '🔄 もっと見る',
        },
        ko: {
            subtitle: 'Duolingo에서 🦉', homeTitle: '홈', cfgTitle: '언어 설정',
            closeTitle: '닫기', placeholder: '동영상 검색...', tabSearch: '🔎 검색',
            tabFav: '⭐ 즐겨찾기', emptyHint: '키워드를 입력하고 검색하세요',
            loading: '검색 중...', settingsTitle: '⚙ 검색 언어',
            settingsSec: 'YouTube 언어 선택', saveCfg: '✅ 저장',
            btnFav: '⭐ 저장', btnFavOn: '⭐ 저장됨', btnPip: '📺 PiP', btnCopy: '🔗 복사',
            toastSaved: '✅ 설정이 저장되었습니다!', toastReload: '🌐 언어 변경됨 — 다시 로드 중...',
            toastFavAdd: '⭐ 즐겨찾기에 추가되었습니다!', toastFavRm: '🗑 즐겨찾기에서 제거되었습니다!',
            toastCopied: '🔗 링크가 복사되었습니다!', noResults: '결과를 찾을 수 없습니다',
            noFav: '즐겨찾기가 없습니다',
            themeTitle: '테마 전환',
            blurTitle: '블러 효과 전환',
            settingsSize: '기본 크기',
            labelW: '너비 (px)', labelH: '높이 (px)',
            tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: '라이브 방송이 없습니다.', phShorts: '쇼츠 검색...', phLive: '라이브 검색...', searchHint: '동영상 검색...', shortSearchHint: '이름 또는 #해시태그로 검색...', labelOpacity: '투명도', labelVolume: '🔊 볼륨',
            loadMore: '🔄 더 보기',
        },
        'zh-Hans': {
            subtitle: '在Duolingo上 🦉', homeTitle: '主页', cfgTitle: '语言设置',
            closeTitle: '关闭', placeholder: '搜索视频...', tabSearch: '🔎 搜索',
            tabFav: '⭐ 收藏', emptyHint: '输入关键词并搜索',
            loading: '搜索中...', settingsTitle: '⚙ 搜索语言',
            settingsSec: '选择YouTube语言', saveCfg: '✅ 保存设置',
            btnFav: '⭐ 收藏', btnFavOn: '⭐ 已收藏', btnPip: '📺 PiP', btnCopy: '🔗 复制',
            toastSaved: '✅ 设置已保存!', toastReload: '🌐 语言已更改 — 重新加载中...',
            toastFavAdd: '⭐ 已添加到收藏!', toastFavRm: '🗑 已从收藏中删除!',
            toastCopied: '🔗 链接已复制!', noResults: '未找到结果',
            noFav: '暂无收藏视频',
            themeTitle: '切换主题',
            blurTitle: '切换模糊效果',
            settingsSize: '默认尺寸',
            labelW: '宽 (px)', labelH: '高 (px)',
            tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: '没有找到直播。', phShorts: '搜索短视频...', phLive: '搜索直播...', searchHint: '搜索视频...', shortSearchHint: '按名称或#标签搜索...', labelOpacity: '透明度', labelVolume: '🔊 音量',
            loadMore: '🔄 加载更多',
        },
        es: {
            subtitle: 'en Duolingo 🦉', homeTitle: 'Inicio', cfgTitle: 'Configuración de idioma',
            closeTitle: 'Cerrar', placeholder: 'Buscar videos...', tabSearch: '🔎 Buscar',
            tabFav: '⭐ Favoritos', emptyHint: 'Escribe palabras clave y busca',
            loading: 'Buscando...', settingsTitle: '⚙ Idioma de búsqueda',
            settingsSec: 'Elige el idioma de YouTube', saveCfg: '✅ Guardar',
            btnFav: '⭐ Guardar', btnFavOn: '⭐ Guardado', btnPip: '📺 PiP', btnCopy: '🔗 Copiar',
            toastSaved: '✅ ¡Configuración guardada!', toastReload: '🌐 Idioma cambiado — recargando...',
            toastFavAdd: '⭐ ¡Añadido a favoritos!', toastFavRm: '🗑 ¡Eliminado de favoritos!',
            toastCopied: '🔗 ¡Enlace copiado!', noResults: 'No se encontraron resultados',
            noFav: 'No hay videos favoritos aún',
            themeTitle: 'Cambiar tema',
            blurTitle: 'Alternar desenfoque',
            settingsSize: 'Tamaño predeterminado',
            labelW: 'Ancho (px)', labelH: 'Alto (px)',
            tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'No se encontraron transmisiones en vivo.', phShorts: 'Buscar Shorts...', phLive: 'Buscar en vivo...', searchHint: 'Buscar videos...', shortSearchHint: 'Buscar por nombre o #hashtag...', labelOpacity: 'Transparencia', labelVolume: '🔊 Volumen',
            loadMore: '🔄 Cargar más',
        },
        fr: {
            subtitle: 'sur Duolingo 🦉', homeTitle: 'Accueil', cfgTitle: 'Paramètres de langue',
            closeTitle: 'Fermer', placeholder: 'Rechercher des vidéos...', tabSearch: '🔎 Rechercher',
            tabFav: '⭐ Favoris', emptyHint: 'Entrez des mots-clés et recherchez',
            loading: 'Recherche...', settingsTitle: '⚙ Langue de recherche',
            settingsSec: 'Choisir la langue YouTube', saveCfg: '✅ Enregistrer',
            btnFav: '⭐ Enregistrer', btnFavOn: '⭐ Enregistré', btnPip: '📺 PiP', btnCopy: '🔗 Copier',
            toastSaved: '✅ Paramètres sauvegardés!', toastReload: '🌐 Langue modifiée — rechargement...',
            toastFavAdd: '⭐ Ajouté aux favoris!', toastFavRm: '🗑 Supprimé des favoris!',
            toastCopied: '🔗 Lien copié!', noResults: 'Aucun résultat trouvé',
            noFav: 'Pas encore de vidéos favorites',
            themeTitle: 'Changer le thème',
            blurTitle: 'Basculer le flou',
            settingsSize: 'Taille par défaut',
            labelW: 'Largeur (px)', labelH: 'Hauteur (px)',
            tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'Aucun livestream trouvé.', phShorts: 'Chercher des Shorts...', phLive: 'Chercher des lives...', searchHint: 'Rechercher des vidéos...', shortSearchHint: 'Chercher par nom ou #hashtag...', labelOpacity: 'Transparence', labelVolume: '🔊 Volume',
            loadMore: '🔄 Plus de vidéos',
        },
        de: {
            subtitle: 'auf Duolingo 🦉', homeTitle: 'Startseite', cfgTitle: 'Spracheinstellungen',
            closeTitle: 'Schließen', placeholder: 'Videos suchen...', tabSearch: '🔎 Suchen',
            tabFav: '⭐ Favoriten', emptyHint: 'Schlüsselwörter eingeben und suchen',
            loading: 'Suche...', settingsTitle: '⚙ Suchsprache',
            settingsSec: 'YouTube-Sprache wählen', saveCfg: '✅ Speichern',
            btnFav: '⭐ Speichern', btnFavOn: '⭐ Gespeichert', btnPip: '📺 PiP', btnCopy: '🔗 Kopieren',
            toastSaved: '✅ Einstellungen gespeichert!', toastReload: '🌐 Sprache geändert — wird neu geladen...',
            toastFavAdd: '⭐ Zu Favoriten hinzugefügt!', toastFavRm: '🗑 Aus Favoriten entfernt!',
            toastCopied: '🔗 Link kopiert!', noResults: 'Keine Ergebnisse gefunden',
            noFav: 'Noch keine Lieblingsvideos',
            themeTitle: 'Design wechseln',
            blurTitle: 'Unschärfe umschalten',
            settingsSize: 'Standardgröße',
            labelW: 'Breite (px)', labelH: 'Höhe (px)',
            tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'Keine Livestreams gefunden.', phShorts: 'Shorts suchen...', phLive: 'Livestreams suchen...', searchHint: 'Videos suchen...', shortSearchHint: 'Nach Name oder #Hashtag suchen...', labelOpacity: 'Transparenz', labelVolume: '🔊 Lautstärke',
            loadMore: '🔄 Mehr laden',
        },
        pt: {
            subtitle: 'no Duolingo 🦉', homeTitle: 'Início', cfgTitle: 'Configurações de idioma',
            closeTitle: 'Fechar', placeholder: 'Buscar vídeos...', tabSearch: '🔎 Buscar',
            tabFav: '⭐ Favoritos', emptyHint: 'Digite palavras-chave e busque',
            loading: 'Buscando...', settingsTitle: '⚙ Idioma de busca',
            settingsSec: 'Escolha o idioma do YouTube', saveCfg: '✅ Salvar',
            btnFav: '⭐ Salvar', btnFavOn: '⭐ Salvo', btnPip: '📺 PiP', btnCopy: '🔗 Copiar',
            toastSaved: '✅ Configurações salvas!', toastReload: '🌐 Idioma alterado — recarregando...',
            toastFavAdd: '⭐ Adicionado aos favoritos!', toastFavRm: '🗑 Removido dos favoritos!',
            toastCopied: '🔗 Link copiado!', noResults: 'Nenhum resultado encontrado',
            noFav: 'Nenhum vídeo favorito ainda',
            themeTitle: 'Alternar tema',
            blurTitle: 'Alternar desfoque',
            settingsSize: 'Tamanho padrão',
            labelW: 'Largura (px)', labelH: 'Altura (px)',
            tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'Nenhuma transmissão ao vivo encontrada.', phShorts: 'Buscar Shorts...', phLive: 'Buscar ao vivo...', searchHint: 'Buscar vídeos...', shortSearchHint: 'Pesquisar por nome ou #hashtag...', labelOpacity: 'Transparência', labelVolume: '🔊 Volume',
            loadMore: '🔄 Carregar mais',
        },
        th: {
            subtitle: 'บน Duolingo 🦉', homeTitle: 'หน้าหลัก', cfgTitle: 'ตั้งค่าภาษา',
            closeTitle: 'ปิด', placeholder: 'ค้นหาวิดีโอ...', tabSearch: '🔎 ค้นหา',
            tabFav: '⭐ รายการโปรด', emptyHint: 'ป้อนคำสำคัญแล้วกดค้นหา',
            loading: 'กำลังค้นหา...', settingsTitle: '⚙ ภาษาการค้นหา',
            settingsSec: 'เลือกภาษา YouTube', saveCfg: '✅ บันทึก',
            btnFav: '⭐ บันทึก', btnFavOn: '⭐ บันทึกแล้ว', btnPip: '📺 PiP', btnCopy: '🔗 คัดลอก',
            toastSaved: '✅ บันทึกการตั้งค่าแล้ว!', toastReload: '🌐 เปลี่ยนภาษาแล้ว — กำลังโหลดใหม่...',
            toastFavAdd: '⭐ เพิ่มในรายการโปรดแล้ว!', toastFavRm: '🗑 ลบออกจากรายการโปรดแล้ว!',
            toastCopied: '🔗 คัดลอกลิงก์แล้ว!', noResults: 'ไม่พบผลลัพธ์',
            noFav: 'ยังไม่มีวิดีโอโปรด',
            themeTitle: 'สลับธีม',
            blurTitle: 'เปิด/ปิดเบลอ',
            settingsSize: 'ขนาดเริ่มต้น',
            labelW: 'กว้าง (px)', labelH: 'สูง (px)',
            tabShorts: '▶ Shorts', tabLive: '🔴 Live', noLive: 'ไม่พบการถ่ายทอดสด.', phShorts: 'ค้นหา Shorts...', phLive: 'ค้นหาไลฟ์สด...', searchHint: 'ค้นหาวิดีโอ...', shortSearchHint: 'ค้นหาด้วยชื่อหรือ #แฮชแท็ก...', labelOpacity: 'ความโปร่งใส', labelVolume: '🔊 ระดับเสียง',
            loadMore: '🔄 โหลดเพิ่มเติม',
        },
    };
    function t(key) { return (I18N[cfg.hl] || I18N.en)[key] ?? (I18N.en[key] ?? key); }
 
    function applyUILang() {
        const $ = id => document.getElementById(id);
        if (!$('ytd-head-sub')) return;
        $('ytd-head-sub').textContent       = t('subtitle');
        $('ytd-home-btn').title             = t('homeTitle');
        $('ytd-cfg-btn').title              = t('cfgTitle');
        $('ytd-close-btn').title            = t('closeTitle');
        $('ytd-theme-btn').title            = t('themeTitle');
        $('ytd-q').placeholder              = activeView === 'shorts' ? t('phShorts') : activeView === 'live' ? t('phLive') : t('searchHint');
        const tabs = document.querySelectorAll('.ytd-tab');
        tabs.forEach(b => {
            if (b.dataset.t === 'search') b.textContent = t('tabSearch');
            if (b.dataset.t === 'fav')    b.textContent = t('tabFav');
        });
        const loading = $('ytd-loading');
        if (loading) { const sp = loading.querySelector('span'); if (sp) sp.textContent = t('loading'); }
        $('ytd-save-cfg').textContent = t('saveCfg');
        const sttl = document.getElementById('ytd-size-ttl'); if (sttl) sttl.textContent = t('settingsSize');
        const opttl = document.getElementById('ytd-opacity-ttl'); if (opttl) opttl.textContent = t('labelOpacity');
        document.querySelectorAll('.ytd-tab').forEach(b => {
            if (b.dataset.t === 'shorts') b.textContent = t('tabShorts');
            if (b.dataset.t === 'live') b.textContent = t('tabLive');
        });
        const qEl2 = document.getElementById('ytd-q');
        if (qEl2) qEl2.placeholder = activeView === 'shorts' ? t('phShorts') : activeView === 'live' ? t('phLive') : t('searchHint');
 
        $('ytd-sbar').querySelector('span').textContent = t('settingsTitle');
        document.querySelector('.ytd-sec-ttl').textContent = t('settingsSec');
        $('pbtn-fav').textContent  = activeData && favorites.some(f => f.id === activeData.id) ? t('btnFavOn') : t('btnFav');
        $('pbtn-pip').textContent  = t('btnPip');
        $('pbtn-copy').textContent = t('btnCopy');
        const empty = $('ytd-empty');
        if (empty && empty.style.display !== 'none') {
            empty.innerHTML = activeView === 'fav'
                ? `<span class="emo">⭐</span><span>${t('noFav')}</span>`
                : activeView === 'live'
                ? `<span class="emo">🔴</span><span>${t('noLive')}</span>`
                : `<span class="emo">🔍</span><span>${t('emptyHint')}</span>`;
        }
    }
 
    // ╔══════════════════════════════════════════╗
    // ║  YOUTUBE INNERTUBE                       ║
    // ╚══════════════════════════════════════════╝
 
    // config slice ε ─────────────────────────────────────────────────
    const _ik0 = 'AIzaSyAO_FJ2SlqU8Q4S';
    const _ik1 = 'TEHLGCilw_Y9_11qcW8';
 
    function itPost(ep, body) {
        return new Promise(async (res, rej) => {
            const ctx = { client: { clientName: 'WEB', clientVersion: '2.20240101.00.00', hl: cfg.hl, gl: cfg.gl } };
            GM_xmlhttpRequest({
                method: 'POST',
                url: `https://www.youtube.com/youtubei/v1/${ep}?key=${_ik0 + _ik1}&prettyPrint=false`,
                headers: {
                    'Content-Type':             'application/json',
                    'Cookie':                   buildCookie(),
                    'Authorization':            await sapisidHash(),
                    'X-Origin':                 'https://www.youtube.com',
                    'Origin':                   'https://www.youtube.com',
                    'Referer':                  'https://www.youtube.com/',
                    'X-Youtube-Client-Name':    '1',
                    'X-Youtube-Client-Version': '2.20240101.00.00',
                },
                data: JSON.stringify({ context: ctx, ...body }),
                onload: r => { try { res(JSON.parse(r.responseText)); } catch (e) { rej(e); } },
                onerror: rej,
            });
        });
    }
 
    async function ytSearch(q) {
        const d = await itPost('search', { query: q, params: 'EgIQAQ%3D%3D' });
        const items = [];
        try {
            const sects = d?.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents || [];
            for (const s of sects) {
                for (const it of (s?.itemSectionRenderer?.contents || [])) {
                    const vr = it?.videoRenderer;
                    if (!vr?.videoId) continue;
                    items.push({
                        id:       vr.videoId,
                        title:    vr.title?.runs?.[0]?.text || '',
                        channel:  vr.ownerText?.runs?.[0]?.text || '',
                        duration: vr.lengthText?.simpleText || '',
                        thumb:    `https://i.ytimg.com/vi/${vr.videoId}/mqdefault.jpg`,
                    });
                    if (items.length >= 25) break;
                }
                if (items.length >= 25) break;
            }
        } catch {}
        return items;
    }
 
    async function ytGetShorts() {
        const items = [], seen = new Set();
        const LIMIT = 50;
        function pickShort(r) {
            if (!r?.videoId || seen.has(r.videoId) || items.length >= LIMIT) return;
            seen.add(r.videoId);
            items.push({
                id:      r.videoId,
                title:   r.headline?.simpleText || r.accessibility?.accessibilityData?.label || '',
                channel: '',
                duration:'',
                thumb:   r.thumbnail?.thumbnails?.slice(-1)[0]?.url || ('https://i.ytimg.com/vi/' + r.videoId + '/mqdefault.jpg'),
                isShort: true,
            });
        }
        function pickLockup(o) {
            const id = o?.shortsLockupViewModel?.onTap?.innertubeCommand?.reelWatchEndpoint?.videoId
                    || o?.shortsLockupViewModel?.onTap?.innertubeCommand?.watchEndpoint?.videoId;
            if (!id || seen.has(id) || items.length >= LIMIT) return;
            seen.add(id);
            const vm = o.shortsLockupViewModel;
            items.push({
                id,
                title:   vm?.overlayMetadata?.primaryText?.content || '',
                channel: vm?.overlayMetadata?.secondaryText?.content || '',
                duration:'',
                thumb:   vm?.thumbnail?.sources?.slice(-1)[0]?.url || ('https://i.ytimg.com/vi/' + id + '/mqdefault.jpg'),
                isShort: true,
            });
        }
        function addVideoShort(r) {
            if (!r?.videoId || seen.has(r.videoId) || items.length >= LIMIT) return;
            const dur = r.lengthText?.simpleText || '';
            if (dur && !/^0:[0-5]d$/.test(dur)) return; // skip if not a short
            seen.add(r.videoId);
            items.push({ id:r.videoId, title:r.title?.runs?.[0]?.text||r.title?.simpleText||'', channel:r.ownerText?.runs?.[0]?.text||'', duration:dur, thumb:'https://i.ytimg.com/vi/'+r.videoId+'/mqdefault.jpg', isShort:true });
        }
        function walk(o, d) {
            if (!o || typeof o !== 'object' || d > 15 || items.length >= LIMIT) return;
            if (o.reelItemRenderer?.videoId) { pickShort(o.reelItemRenderer); return; }
            if (o.shortsLockupViewModel)     { pickLockup(o); return; }
            if (o.videoRenderer?.videoId)    { addVideoShort(o.videoRenderer); return; }
            if (Array.isArray(o)) { for (const el of o) walk(el, d+1); }
            else { for (const k of Object.keys(o)) walk(o[k], d+1); }
        }
        try { walk(await itPost('browse', { browseId: 'FEshorts' }), 0); } catch {}
        if (items.length < 35) {
            try { walk(await itPost('search', { query: '#shorts', params: 'EgQQARgB' }), 0); } catch {}
        }
        if (items.length < 25) {
            try { walk(await itPost('search', { query: 'shorts', params: 'EgQQARgB' }), 0); } catch {}
        }
        if (items.length < 15) {
            try { walk(await itPost('browse', { browseId: 'FEshorts' }), 0); } catch {}
        }
        return items;
    }
 
    async function ytGetLive() {
        const items = [], seen = new Set();
        function addLive(r) {
            const id = r?.videoId; if (!id || seen.has(id)) return;
            seen.add(id);
            items.push({
                id,
                title:   r.title?.runs?.[0]?.text || r.title?.simpleText || '',
                channel: r.ownerText?.runs?.[0]?.text || r.shortBylineText?.runs?.[0]?.text || '',
                duration: '🔴 LIVE',
                thumb:   'https://i.ytimg.com/vi/' + id + '/mqdefault.jpg',
                isLive:  true,
            });
        }
        function walk(o, d) {
            if (!o || typeof o !== 'object' || d > 12 || items.length >= 25) return;
            if (o.videoRenderer?.videoId)        { addLive(o.videoRenderer); return; }
            if (o.compactVideoRenderer?.videoId) { addLive(o.compactVideoRenderer); return; }
            if (o.richItemRenderer?.content)     { walk(o.richItemRenderer.content, d+1); return; }
            if (Array.isArray(o)) { for (const el of o) walk(el, d+1); }
            else { for (const k of Object.keys(o)) walk(o[k], d+1); }
        }
        try { walk(await itPost('browse', { browseId: 'FElive_dashboard' }), 0); } catch {}
        if (items.length < 25) {
            try { walk(await itPost('search', { query: 'live now', params: 'EgJAAQ%3D%3D' }), 0); } catch {}
        }
        if (items.length < 20) {
            try { walk(await itPost('search', { query: 'livestream', params: 'EgJAAQ%3D%3D' }), 0); } catch {}
        }
        if (items.length < 15) {
            try { walk(await itPost('search', { query: 'live streaming now', params: 'EgJAAQ%3D%3D' }), 0); } catch {}
        }
        if (items.length < 10) {
            try { walk(await itPost('search', { query: 'stream live', params: 'EgJAAQ%3D%3D' }), 0); } catch {}
        }
        return items;
    }
 
        async function ytSearchShorts(q) {
        const items = [], seen = new Set();
        function pickR(r) {
            if (!r?.videoId || seen.has(r.videoId)) return;
            seen.add(r.videoId);
            items.push({ id:r.videoId, title:r.headline?.simpleText||r.accessibility?.accessibilityData?.label||'', channel:'', duration:'', thumb:'https://i.ytimg.com/vi/'+r.videoId+'/mqdefault.jpg', isShort:true });
        }
        function pickLock(o) {
            const id = o?.shortsLockupViewModel?.onTap?.innertubeCommand?.reelWatchEndpoint?.videoId || o?.shortsLockupViewModel?.onTap?.innertubeCommand?.watchEndpoint?.videoId;
            if (!id || seen.has(id)) return; seen.add(id);
            const vm = o.shortsLockupViewModel;
            items.push({ id, title:vm?.overlayMetadata?.primaryText?.content||'', channel:vm?.overlayMetadata?.secondaryText?.content||'', duration:'', thumb:vm?.thumbnail?.sources?.slice(-1)[0]?.url||'https://i.ytimg.com/vi/'+id+'/mqdefault.jpg', isShort:true });
        }
        function walk(o, d) {
            if (!o || typeof o !== 'object' || d > 12 || items.length >= 25) return;
            if (o.reelItemRenderer?.videoId) { pickR(o.reelItemRenderer); return; }
            if (o.shortsLockupViewModel)     { pickLock(o); return; }
            if (o.videoRenderer?.videoId) {
                const r = o.videoRenderer, dur = r.lengthText?.simpleText || '';
                if (!dur || /^0:[0-5]\d$/.test(dur)) {
                    if (!seen.has(r.videoId)) { seen.add(r.videoId); items.push({ id:r.videoId, title:r.title?.runs?.[0]?.text||'', channel:r.ownerText?.runs?.[0]?.text||'', duration:dur, thumb:'https://i.ytimg.com/vi/'+r.videoId+'/mqdefault.jpg', isShort:true }); }
                }
                return;
            }
            if (Array.isArray(o)) { for (const el of o) walk(el, d+1); }
            else { for (const k of Object.keys(o)) walk(o[k], d+1); }
        }
        const isHashtag = q.startsWith('#');
        try { walk(await itPost('search', { query: q, params: 'EgQQARgB' }), 0); } catch {}
        if (isHashtag && items.length < 25) {
            try {
                const d2 = await itPost('search', { query: q });
                function walkTag(o, d) {
                    if (!o || typeof o !== 'object' || d > 12 || items.length >= 25) return;
                    if (o.reelItemRenderer?.videoId) { pickR(o.reelItemRenderer); return; }
                    if (o.shortsLockupViewModel)     { pickLock(o); return; }
                    if (o.videoRenderer?.videoId) {
                        const r = o.videoRenderer, dur = r.lengthText?.simpleText || '';
                        if (!dur || /^0:[0-5]\d$/.test(dur)) {
                            if (!seen.has(r.videoId)) { seen.add(r.videoId); items.push({ id:r.videoId, title:r.title?.runs?.[0]?.text||'', channel:r.ownerText?.runs?.[0]?.text||'', duration:dur, thumb:'https://i.ytimg.com/vi/'+r.videoId+'/mqdefault.jpg', isShort:true }); }
                        }
                        return;
                    }
                    if (Array.isArray(o)) { for (const el of o) walkTag(el, d+1); }
                    else { for (const k of Object.keys(o)) walkTag(o[k], d+1); }
                }
                walkTag(d2, 0);
            } catch {}
        }
        return items;
    }
 
    async function ytSearchLive(q) {
        const items = [], seen = new Set();
        function addL(r) {
            const id = r?.videoId; if (!id || seen.has(id)) return; seen.add(id);
            items.push({ id, title:r.title?.runs?.[0]?.text||r.title?.simpleText||'', channel:r.ownerText?.runs?.[0]?.text||r.shortBylineText?.runs?.[0]?.text||'', duration:'🔴 LIVE', thumb:'https://i.ytimg.com/vi/'+id+'/mqdefault.jpg', isLive:true });
        }
        function walk(o, d) {
            if (!o || typeof o !== 'object' || d > 12 || items.length >= 25) return;
            if (o.videoRenderer?.videoId)        { addL(o.videoRenderer); return; }
            if (o.compactVideoRenderer?.videoId) { addL(o.compactVideoRenderer); return; }
            if (o.richItemRenderer?.content)     { walk(o.richItemRenderer.content, d+1); return; }
            if (Array.isArray(o)) { for (const el of o) walk(el, d+1); }
            else { for (const k of Object.keys(o)) walk(o[k], d+1); }
        }
        // Primary: search with live filter
        try { walk(await itPost('search', { query: q, params: 'EgJAAQ%3D%3D' }), 0); } catch {}
        // Hashtag fallback: search without filter if few results
        if (q.startsWith('#') && items.length < 25) {
            try { walk(await itPost('search', { query: q }), 0); } catch {}
        }
        return items;
    }
 
        function applyVolume() {
        const fr = document.getElementById('ytd-frame');
        if (!fr || !fr.contentWindow) return;
        try {
            fr.contentWindow.postMessage(JSON.stringify({
                event: 'command', func: 'setVolume', args: [volumeLevel]
            }), '*');
        } catch(e) {}
        const volEl = document.getElementById('ytd-vol');
        if (volEl) volEl.value = volumeLevel;
    }
 
    // ╔══════════════════════════════════════════╗
    // ║  STATE                                   ║
    // ╚══════════════════════════════════════════╝
    let panelOpen  = false;
    let activeView = 'search';
    let results    = [];
    let favorites  = JSON.parse(GM_getValue('fav', '[]'));
    let watchedIds = new Set(JSON.parse(GM_getValue('watched', '[]')));
    let activeId   = null, activeData = null;
    let defaultW   = parseInt(GM_getValue('dw', '350'));
    let defaultH   = parseInt(GM_getValue('dh', '300'));
    let panelW     = parseInt(GM_getValue('pw', String(GM_getValue('dw','350'))));
    let panelH     = parseInt(GM_getValue('ph', String(GM_getValue('dh','300'))));
    let blurOn     = GM_getValue('blur', false);
    let blurOpacity = parseInt(GM_getValue('blurOpacity', '70'));
    let volumeLevel = parseInt(GM_getValue('vol', '100'));
    let shorts = []; let shortsLoaded = false; let shortsCurrent = 0;
    let live    = []; let liveLoaded    = false;
    let _loadMoreFn = null;
 
    function save() {
        GM_setValue('fav',     JSON.stringify(favorites));
        GM_setValue('watched', JSON.stringify([...watchedIds]));
 
    }
 
    // ╔══════════════════════════════════════════╗
    // ║  STYLES                                  ║
    // ╚══════════════════════════════════════════╝
    GM_addStyle(`
        /* ── CSS Variables (light default) ────────────────────── */
        #ytd-panel {
            --bg:        #ffffff;
            --bg2:       #f6f6f6;
            --border:    #e8e8e8;
            --border2:   #eeeeee;
            --text:      #222222;
            --text2:     #555555;
            --text3:     #aaaaaa;
            --card-bg:   #ffffff;
            --card-bdr:  #eeeeee;
            --input-bg:  #ffffff;
            --input-clr: #222222;
            --input-bdr: #dddddd;
            --lbtn-bg:   #f0f0f0;
            --lbtn-bdr:  #e0e0e0;
            --lbtn-clr:  #444444;
            --lbtn-on-bg:#e8f9d0;
            --lbtn-on-clr:#2d7a00;
            --empty-clr: #bbbbbb;
            --scrl-thumb:#dddddd;
            --sett-bg:   #ffffff;
        }
 
        /* ── Dark mode overrides ───────────────────────────────── */
        #ytd-panel[data-theme="dark"] {
            --bg:        #1a1a1a;
            --bg2:       #242424;
            --border:    #333333;
            --border2:   #2e2e2e;
            --text:      #f0f0f0;
            --text2:     #bbbbbb;
            --text3:     #666666;
            --card-bg:   #242424;
            --card-bdr:  #333333;
            --input-bg:  #2a2a2a;
            --input-clr: #f0f0f0;
            --input-bdr: #444444;
            --lbtn-bg:   #2a2a2a;
            --lbtn-bdr:  #3a3a3a;
            --lbtn-clr:  #cccccc;
            --lbtn-on-bg:#1a3a0a;
            --lbtn-on-clr:#7ecf40;
            --empty-clr: #555555;
            --scrl-thumb:#444444;
            --sett-bg:   #1a1a1a;
        }
 
        #ytd-fab {
            position:fixed; width:46px; height:46px; background:#58cc02; border-radius:50%;
            display:flex; align-items:center; justify-content:center; cursor:pointer;
            z-index:999999; box-shadow:0 3px 10px rgba(88,204,2,.5); border:none;
            transition:transform .15s, box-shadow .15s; user-select:none;
        }
        #ytd-fab:hover { transform:scale(1.1); box-shadow:0 5px 16px rgba(88,204,2,.6); }
        #ytd-fab svg { width:20px; height:20px; pointer-events:none; }
 
        #ytd-panel {
            position:fixed; background:var(--bg); border-radius:16px;
            box-shadow:0 12px 40px rgba(0,0,0,.16), 0 2px 8px rgba(0,0,0,.08);
            display:flex; flex-direction:column; z-index:999998;
            font-family:'Nunito','Segoe UI',sans-serif; border:1.5px solid var(--border); overflow:hidden;
            opacity:0; transform:scale(.93) translateY(8px);
            transition:opacity .2s, transform .2s, background .2s, border-color .2s;
            pointer-events:none;
            min-width:280px; min-height:300px;
        }
        #ytd-panel.open { opacity:1; transform:scale(1) translateY(0); pointer-events:all; }
 
        #ytd-panel::after {
            content:''; position:absolute; bottom:3px; right:3px;
            width:10px; height:10px; pointer-events:none;
            background:linear-gradient(135deg, transparent 40%, rgba(128,128,128,.3) 40%, rgba(128,128,128,.3) 55%,
                        transparent 55%, transparent 65%, rgba(128,128,128,.3) 65%, rgba(128,128,128,.3) 80%, transparent 80%);
        }
 
        /* Header */
        #ytd-head {
            background:#58cc02; padding:8px 10px;
            display:flex; align-items:center; gap:7px;
            cursor:grab; user-select:none; flex-shrink:0;
        }
        #ytd-head:active { cursor:grabbing; }
        #ytd-head-title { flex:1; color:#fff; font-size:13px; font-weight:700; line-height:1.15; }
        #ytd-head-sub { color:rgba(255,255,255,.78); font-size:9px; font-weight:400; display:block; }
        .ytd-hbtn {
            background:rgba(255,255,255,.2); border:none; color:#fff;
            width:24px; height:24px; border-radius:50%; font-size:13px; cursor:pointer;
            display:flex; align-items:center; justify-content:center;
            transition:background .15s; flex-shrink:0; line-height:1;
        }
        .ytd-hbtn:hover { background:rgba(255,255,255,.38); }
 
        /* Search */
        #ytd-search-row {
            display:flex; padding:8px; background:var(--bg2);
            border-bottom:1.5px solid var(--border2); flex-shrink:0;
            max-height:52px; overflow:hidden;
            transition:max-height .2s ease, opacity .2s ease, padding .2s ease;
            opacity:1;
        }
        #ytd-search-row.ytd-sr-hidden {
            max-height:0; opacity:0; padding:0; border-bottom-width:0;
        }
        #ytd-q {
            flex:1; border:1.5px solid var(--input-bdr); border-right:none;
            border-radius:10px 0 0 10px; padding:6px 10px;
            font-size:13px; font-family:inherit; outline:none;
            background:var(--input-bg); color:var(--input-clr);
            transition:background .2s, color .2s, border-color .2s;
        }
        #ytd-q:focus { border-color:#58cc02; }
        #ytd-go {
            background:#58cc02; border:1.5px solid #58cc02; border-radius:0 10px 10px 0;
            color:#fff; padding:0 12px; font-size:14px; cursor:pointer; transition:background .15s;
        }
        #ytd-go:hover { background:#46a302; border-color:#46a302; }
 
        /* Tabs */
        #ytd-tabs { display:flex; gap:5px; padding:0 8px 7px; background:var(--bg2); flex-shrink:0; align-items:center; }
        #ytd-search-toggle {
            margin-left:auto; background:rgba(88,204,2,.15); border:1.5px solid #58cc02;
            border-radius:14px; padding:2px 9px; font-size:11px; cursor:pointer;
            color:#58cc02; font-weight:700; font-family:inherit; flex-shrink:0;
            transition:all .15s; display:none;
        }
        #ytd-search-toggle.on { display:inline-flex; align-items:center; gap:3px; }
        #ytd-search-toggle:hover { background:rgba(88,204,2,.3); }
        .ytd-tab {
            background:var(--lbtn-bg); border:none; border-radius:14px; padding:3px 10px;
            font-size:11px; font-family:inherit; cursor:pointer; color:var(--text2);
            font-weight:700; transition:all .15s;
        }
        .ytd-tab.on { background:#58cc02; color:#fff; }
        .ytd-tab:hover:not(.on) { filter:brightness(0.92); }
 
        /* Body / grid */
        #ytd-body { flex:1; overflow-y:auto; overflow-x:hidden; min-height:0; }
        #ytd-body::-webkit-scrollbar { width:4px; }
        #ytd-body::-webkit-scrollbar-thumb { background:var(--scrl-thumb); border-radius:2px; }
 
        #ytd-empty, #ytd-loading {
            display:flex; flex-direction:column; align-items:center; justify-content:center;
            padding:30px 16px; color:var(--empty-clr); font-size:12px; gap:8px; text-align:center;
        }
        #ytd-empty .emo { font-size:38px; }
        .ytd-spin {
            width:26px; height:26px; border:3px solid var(--border2); border-top-color:#58cc02;
            border-radius:50%; animation:ytd-rot .7s linear infinite;
        }
        @keyframes ytd-rot { to { transform:rotate(360deg); } }
 
        #ytd-grid { display:grid; grid-template-columns:1fr 1fr; gap:8px; padding:8px; }
        .ytd-card {
            border:1.5px solid var(--card-bdr); border-radius:12px; overflow:hidden; cursor:pointer;
            transition:border-color .15s, transform .15s, box-shadow .15s;
            background:var(--card-bg);
        }
        .ytd-card:hover { border-color:#58cc02; transform:translateY(-2px); box-shadow:0 4px 12px rgba(88,204,2,.18); }
        .ytd-thumb-wrap { position:relative; }
        .ytd-thumb { width:100%; aspect-ratio:16/9; object-fit:cover; background:var(--border2); display:block; }
        .ytd-dur {
            position:absolute; bottom:3px; right:3px; background:rgba(0,0,0,.72); color:#fff;
            border-radius:4px; padding:1px 4px; font-size:9px; font-weight:700;
        }
        .ytd-done {
            position:absolute; top:3px; left:3px; background:#58cc02; color:#fff;
            border-radius:4px; padding:1px 5px; font-size:9px; font-weight:700;
        }
        .ytd-info { padding:5px 6px 6px; }
        .ytd-title {
            font-size:11px; font-weight:700; color:var(--text); line-height:1.3;
            display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; margin-bottom:3px;
        }
        .ytd-ch { font-size:10px; color:#58cc02; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
 
        /* Player */
        #ytd-player {
            position:absolute; inset:0; background:#0a0a0a; z-index:5;
            display:flex; flex-direction:column;
            transform:translateX(100%); transition:transform .25s cubic-bezier(.4,0,.2,1);
        }
        #ytd-player.on { transform:translateX(0); }
 
        #ytd-pbar {
            background:#161616; padding:0 10px; height:36px;
            display:flex; align-items:center; gap:8px; flex-shrink:0;
        }
        #ytd-pbar-ttl {
            flex:1; color:#ccc; font-size:10px; font-weight:600;
            white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
        }
        #ytd-yt-link {
            background:#ff0000; color:#fff; border-radius:6px; padding:3px 8px;
            font-size:10px; font-weight:700; text-decoration:none; white-space:nowrap; flex-shrink:0;
        }
 
        #ytd-frame-wrap { flex:1; position:relative; background:#000; }
        #ytd-frame { position:absolute; inset:0; width:100%; height:100%; border:none; }
 
        #ytd-pinfo { background:#111; padding:7px 10px; flex-shrink:0; }
        #ytd-pv-title {
            color:#fff; font-size:11px; font-weight:700; margin-bottom:2px;
            white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
        }
        #ytd-pv-ch { color:#58cc02; font-size:10px; font-weight:700; margin-bottom:6px; }
        #ytd-pv-btns { display:flex; gap:5px; }
        .ytd-actbtn {
            background:#222; border:1px solid #333; color:#ccc; border-radius:14px;
            padding:3px 9px; font-size:10px; font-weight:700; cursor:pointer; font-family:inherit; transition:all .15s;
        }
        .ytd-actbtn:hover { border-color:#58cc02; color:#58cc02; }
        .ytd-actbtn.on { background:#58cc02; border-color:#58cc02; color:#fff; }
 
        /* Settings */
        #ytd-settings {
            position:absolute; inset:0; background:var(--sett-bg); z-index:6;
            display:flex; flex-direction:column; overflow-y:auto;
            transform:translateX(100%); transition:transform .25s cubic-bezier(.4,0,.2,1);
        }
        #ytd-settings.on { transform:translateX(0); }
        #ytd-sbar {
            background:#1cb0f6; padding:0 10px; height:36px;
            display:flex; align-items:center; gap:8px; flex-shrink:0;
        }
        #ytd-sbar-back {
            background:rgba(255,255,255,.2); border:none; color:#fff; width:26px; height:26px;
            border-radius:50%; font-size:14px; cursor:pointer; display:flex;
            align-items:center; justify-content:center; flex-shrink:0;
        }
        #ytd-sbar-back:hover { background:rgba(255,255,255,.35); }
        #ytd-sbar span { color:#fff; font-size:13px; font-weight:700; }
        .ytd-sec { padding:12px 14px 4px; }
        .ytd-sec-ttl {
            font-size:10px; text-transform:uppercase; letter-spacing:.8px;
            color:var(--text3); font-weight:700; margin-bottom:8px;
        }
        .ytd-lang-grid { display:grid; grid-template-columns:1fr 1fr; gap:6px; margin-bottom:8px; }
        .ytd-lbtn {
            background:var(--lbtn-bg); border:1.5px solid var(--lbtn-bdr); border-radius:10px;
            padding:6px 8px; font-size:11px; font-family:inherit; cursor:pointer;
            color:var(--lbtn-clr); font-weight:600; text-align:left; transition:all .15s;
        }
        .ytd-lbtn.on { background:var(--lbtn-on-bg); border-color:#58cc02; color:var(--lbtn-on-clr); }
        .ytd-lbtn:hover:not(.on) { filter:brightness(0.92); }
        #ytd-save-cfg {
            display:block; width:calc(100% - 28px); margin:8px 14px 16px;
            background:#58cc02; border:none; color:#fff; padding:9px; border-radius:10px;
            font-size:13px; font-weight:700; cursor:pointer; font-family:inherit;
        }
 
        /* Toast */
        #ytd-toast {
            position:fixed; bottom:80px; right:20px; background:#333; color:#fff;
            padding:7px 14px; border-radius:16px; font-size:12px; font-weight:700;
            z-index:9999999; opacity:0; transform:translateY(6px);
            transition:opacity .25s, transform .25s; pointer-events:none;
            box-shadow:0 3px 10px rgba(0,0,0,.3);
            font-family:'Nunito','Segoe UI',sans-serif; max-width:260px; text-align:center;
        }
        #ytd-toast[data-theme="dark"] { background:#e0e0e0; color:#111; }
        #ytd-toast.on { opacity:1; transform:translateY(0); }
 
        .ytd-inner { position:relative; flex:1; overflow:hidden; display:flex; flex-direction:column; min-height:0; }
        /* ── Blur mode ──────────────────────────────────────────── */
        #ytd-panel[data-blur="on"] {
            background: var(--blur-bg, rgba(255,255,255,0.70)) !important;
            backdrop-filter: blur(16px);
            -webkit-backdrop-filter: blur(16px);
        }
        #ytd-panel[data-blur="on"] #ytd-search-row,
        #ytd-panel[data-blur="on"] #ytd-tabs {
            background: transparent !important;
        }
        #ytd-panel[data-blur="on"] #ytd-settings {
            background: var(--blur-bg, rgba(255,255,255,0.80)) !important;
            backdrop-filter: blur(16px);
            -webkit-backdrop-filter: blur(16px);
        }
        #ytd-hbtn-blur.active { background:rgba(255,255,255,.55) !important; }
        /* ── Volume slider ────────────────────────────────────── */
        #ytd-vol-wrap { display:flex; align-items:center; gap:4px; flex-shrink:0; }
        #ytd-vol-icon { color:#ccc; font-size:12px; cursor:pointer; user-select:none; }
        #ytd-vol { width:54px; accent-color:#58cc02; cursor:pointer; height:3px; }
 
        /* ── Opacity slider ───────────────────────────────────── */
        #ytd-opacity-sec { padding:0 14px 8px; }
        .ytd-range-row { display:flex; align-items:center; gap:8px; }
        .ytd-range-row input[type=range] { flex:1; accent-color:#58cc02; cursor:pointer; }
        .ytd-range-val { font-size:11px; color:var(--text2); min-width:34px; text-align:right; font-weight:700; }
 
        /* ── Shorts card ─────────────────────────────────────── */
        .ytd-card.ytd-short .ytd-thumb { aspect-ratio:9/16; }
        #ytd-grid.ytd-shorts-grid { grid-template-columns:1fr 1fr 1fr; gap:5px; padding:6px; }
        .ytd-short-badge {
            position:absolute; top:3px; right:3px; background:#ff0000; color:#fff;
            border-radius:4px; padding:1px 4px; font-size:8px; font-weight:700;
        }
        /* ── Live badge ───────────────────────────────────────── */
        .ytd-live-badge {
            position:absolute; top:3px; left:3px; background:#ff0000; color:#fff;
            border-radius:4px; padding:1px 5px; font-size:8px; font-weight:700;
            animation: ytd-live-pulse 1.4s ease-in-out infinite;
        }
        @keyframes ytd-live-pulse { 0%,100%{opacity:1} 50%{opacity:.55} }
 
        /* ── Shorts vertical player ───────────────────────────── */
        #ytd-shorts-player {
            position:absolute; inset:0; z-index:10; background:#0f0f0f;
            display:flex; flex-direction:column; overflow:hidden;
            transform:translateX(100%); transition:transform .25s cubic-bezier(.4,0,.2,1);
        }
        #ytd-shorts-player.on { transform:translateX(0); }
 
        #ytd-sp-header {
            background:#161616; padding:6px 8px; display:flex; align-items:center;
            gap:8px; flex-shrink:0; height:36px;
        }
        #ytd-sp-back {
            background:rgba(255,255,255,.15); border:none; color:#fff;
            border-radius:16px; padding:3px 10px; cursor:pointer;
            font-size:11px; font-weight:700; transition:background .15s; flex-shrink:0;
        }
        #ytd-sp-back:hover { background:rgba(255,255,255,.3); }
        #ytd-sp-title-sm {
            flex:1; color:#ddd; font-size:10px; font-weight:600;
            white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
        }
        #ytd-sp-yt-link {
            background:#ff0000; color:#fff; border-radius:6px; padding:2px 7px;
            font-size:10px; font-weight:700; text-decoration:none; white-space:nowrap; flex-shrink:0;
        }
 
        #ytd-sp-body {
            flex:1; display:flex; min-height:0; background:#000;
        }
        #ytd-sp-iframe-col {
            flex:1; display:flex; align-items:center; justify-content:center;
            overflow:hidden; min-width:0;
        }
        #ytd-sp-frame {
            height:100%; width:auto; aspect-ratio:9/16; max-width:100%; border:none;
        }
        #ytd-sp-nav-col {
            width:46px; display:flex; flex-direction:column;
            align-items:center; justify-content:center; gap:10px;
            background:rgba(0,0,0,.5); flex-shrink:0;
        }
        .ytd-sp-nbtn {
            width:36px; height:36px; border-radius:50%;
            background:rgba(255,255,255,.18); border:none; color:#fff;
            font-size:18px; cursor:pointer; line-height:1;
            display:flex; align-items:center; justify-content:center;
            transition:background .15s; user-select:none;
        }
        .ytd-sp-nbtn:hover:not(:disabled) { background:rgba(255,255,255,.38); }
        .ytd-sp-nbtn:disabled { opacity:.28; cursor:default; }
 
        #ytd-sp-footer {
            background:#161616; padding:5px 10px 6px; flex-shrink:0;
        }
        #ytd-sp-footer-title {
            color:#fff; font-size:10px; font-weight:700;
            white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-bottom:1px;
        }
        #ytd-sp-ch-nm { color:#58cc02; font-size:9px; font-weight:700; }
        #ytd-sp-counter { color:#666; font-size:9px; margin-left:6px; }

        /* ── Load More button ─────────────────────────────────── */
        #ytd-load-more {
            display:block; width:calc(100% - 16px); margin:4px 8px 10px;
            background:transparent; border:1.5px solid #58cc02; color:#58cc02;
            border-radius:12px; padding:9px; font-size:12px; font-weight:700;
            cursor:pointer; font-family:inherit; transition:all .18s;
            text-align:center;
        }
        #ytd-load-more:hover { background:#58cc02; color:#fff; }
        #ytd-load-more:disabled { opacity:.4; cursor:default; border-color:var(--border); color:var(--text3); }
    `);
 
    // ╔══════════════════════════════════════════╗
    // ║  BUILD UI                                ║
    // ╚══════════════════════════════════════════╝
    let panelX = parseInt(GM_getValue('px', String(window.innerWidth  - 390)));
    let panelY = parseInt(GM_getValue('py', String(window.innerHeight - 580)));
 
    function buildUI() {
        const fab = document.createElement('button');
        fab.id = 'ytd-fab';
        fab.innerHTML = ytSvg(20);
        fab.style.cssText = 'bottom:20px;right:20px;';
        document.body.appendChild(fab);
        makeDraggable(fab, 'fbx', 'fby', null, true);
        fab.addEventListener('click', togglePanel);
 
        const panel = document.createElement('div');
        panel.id = 'ytd-panel';
        panel.setAttribute('data-theme', theme);
        panel.style.cssText = `left:${clamp(panelX,0,window.innerWidth-panelW)}px;top:${clamp(panelY,0,window.innerHeight-200)}px;width:${panelW}px;height:${panelH}px;`;
        panel.innerHTML = `
            <div id="ytd-head">
                ${ytSvg(16)}
                <div id="ytd-head-title">YouTube <span id="ytd-head-sub">trên Duolingo 🦉</span></div>
                <button class="ytd-hbtn" id="ytd-home-btn"  title="Về trang kết quả">🏠</button>
                <button class="ytd-hbtn" id="ytd-blur-btn"  title="Bật/tắt hiệu ứng mờ">💧</button>
                <button class="ytd-hbtn" id="ytd-theme-btn" title="Chuyển giao diện">${theme === 'dark' ? '☀️' : '🌙'}</button>
                <button class="ytd-hbtn" id="ytd-cfg-btn"   title="Cài đặt ngôn ngữ">⚙</button>
                <button class="ytd-hbtn" id="ytd-close-btn" title="Đóng">✕</button>
            </div>
 
            <div id="ytd-search-row">
                <input id="ytd-q" type="text" placeholder="Tìm video..." autocomplete="off"/>
                <button id="ytd-go">🔍</button>
            </div>
            <div id="ytd-tabs">
                <button class="ytd-tab on" data-t="search">🔎 Tìm kiếm</button>
                <button class="ytd-tab"    data-t="fav">⭐ Yêu thích</button>
                <button class="ytd-tab"    data-t="shorts">▶ Shorts</button>
                <button class="ytd-tab"    data-t="live">🔴 Live</button>
                <button id="ytd-search-toggle" title="Hiện thanh tìm kiếm">🔍</button>
            </div>
 
            <div class="ytd-inner">
                <div id="ytd-body">
                    <div id="ytd-empty"><span class="emo">🔍</span><span>Nhập từ khoá và nhấn tìm kiếm</span></div>
                    <div id="ytd-loading" style="display:none"><div class="ytd-spin"></div><span>Đang tìm...</span></div>
                    <div id="ytd-grid"    style="display:none"></div>
                </div>
 
                <div id="ytd-player">
                    <div id="ytd-pbar">
                        <span id="ytd-pbar-ttl"></span>
                        <div id="ytd-vol-wrap">
                            <span id="ytd-vol-icon" title="Volume">🔊</span>
                            <input type="range" id="ytd-vol" min="0" max="100" value="100">
                        </div>
                        <a id="ytd-yt-link" href="#" target="_blank">YT ↗</a>
                    </div>
                    <div id="ytd-frame-wrap">
                        <iframe id="ytd-frame" allowfullscreen allow="autoplay; picture-in-picture"></iframe>
                    </div>
                    <div id="ytd-pinfo">
                        <div id="ytd-pv-title"></div>
                        <div id="ytd-pv-ch"></div>
                        <div id="ytd-pv-btns">
                            <button class="ytd-actbtn" id="pbtn-fav">⭐ Lưu</button>
                            <button class="ytd-actbtn" id="pbtn-pip">📺 PiP</button>
                            <button class="ytd-actbtn" id="pbtn-copy">🔗 Copy</button>
                        </div>
                    </div>
                </div>
 
                <div id="ytd-shorts-player">
                    <div id="ytd-sp-header">
                        <button id="ytd-sp-back">← Quay lại</button>
                        <span id="ytd-sp-title-sm"></span>
                        <a id="ytd-sp-yt-link" href="#" target="_blank">YT ↗</a>
                    </div>
                    <div id="ytd-sp-body">
                        <div id="ytd-sp-iframe-col">
                            <iframe id="ytd-sp-frame" allowfullscreen allow="autoplay; picture-in-picture"></iframe>
                        </div>
                        <div id="ytd-sp-nav-col">
                            <button class="ytd-sp-nbtn" id="ytd-sp-up" title="Video trước">↑</button>
                            <button class="ytd-sp-nbtn" id="ytd-sp-down" title="Video tiếp">↓</button>
                        </div>
                    </div>
                    <div id="ytd-sp-footer">
                        <div id="ytd-sp-footer-title"></div>
                        <span id="ytd-sp-ch-nm"></span>
                        <span id="ytd-sp-counter"></span>
                    </div>
                </div>
 
                <div id="ytd-settings">
                    <div id="ytd-sbar">
                        <button id="ytd-sbar-back">&#8592;</button>
                        <span>⚙ Ngôn ngữ tìm kiếm</span>
                    </div>
                    <div class="ytd-sec">
                        <div class="ytd-sec-ttl">Chọn ngôn ngữ YouTube</div>
                        <div class="ytd-lang-grid" id="ytd-lang-grid"></div>
                    </div>
                    <div class="ytd-sec">
                        <div class="ytd-sec-ttl" id="ytd-size-ttl">Kích thước mặc định</div>
                        <div style="display:flex;gap:8px;align-items:center;margin-bottom:8px;">
                            <label style="font-size:11px;color:var(--text2);flex:1;"><span class="ytd-lbl-w">Rộng (px)</span><br>
                            <input type="number" id="ytd-dw" min="280" max="900" value="350" style="width:100%;margin-top:3px;padding:4px 6px;border:1.5px solid var(--input-bdr);border-radius:8px;background:var(--input-bg);color:var(--input-clr);font-size:12px;"></label>
                            <label style="font-size:11px;color:var(--text2);flex:1;"><span class="ytd-lbl-h">Cao (px)</span><br>
                            <input type="number" id="ytd-dh" min="280" max="900" value="300" style="width:100%;margin-top:3px;padding:4px 6px;border:1.5px solid var(--input-bdr);border-radius:8px;background:var(--input-bg);color:var(--input-clr);font-size:12px;"></label>
                        </div>
                    </div>
                    <div id="ytd-opacity-sec">
                        <div class="ytd-sec-ttl" id="ytd-opacity-ttl">Độ trong suốt</div>
                        <div class="ytd-range-row">
                            <input type="range" id="ytd-opacity-sl" min="10" max="100" value="70">
                            <span class="ytd-range-val" id="ytd-opacity-val">70%</span>
                        </div>
                    </div>
                    <button id="ytd-save-cfg">✅ Lưu cài đặt</button>
                </div>
            </div>
        `;
        document.body.appendChild(panel);
        applyBlur();
 
        const toastEl = document.createElement('div');
        toastEl.id = 'ytd-toast';
        toastEl.setAttribute('data-theme', theme);
        document.body.appendChild(toastEl);
 
        makeDraggable(panel, 'px', 'py', panel.querySelector('#ytd-head'), false);
        addEdgeResize(panel);
 
        panel.querySelector('#ytd-close-btn').addEventListener('click',  togglePanel);
        panel.querySelector('#ytd-home-btn').addEventListener('click',   goHome);
        panel.querySelector('#ytd-search-toggle').addEventListener('click', () => { showSearchRow(); document.getElementById('ytd-q')?.focus(); });
        panel.querySelector('#ytd-sp-back').addEventListener('click',    closeShortsPlayer);
        panel.querySelector('#ytd-sp-up').addEventListener('click',       () => shortsNav(-1));
        panel.querySelector('#ytd-sp-down').addEventListener('click',     () => shortsNav(1));
        panel.querySelector('#ytd-blur-btn').addEventListener('click',   toggleBlur);
        panel.querySelector('#ytd-theme-btn').addEventListener('click',  toggleTheme);
        panel.querySelector('#ytd-cfg-btn').addEventListener('click',    openSettings);
        panel.querySelector('#ytd-go').addEventListener('click',         doSearch);
        panel.querySelector('#ytd-q').addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); });
        panel.querySelector('#pbtn-fav').addEventListener('click',  toggleFav);
        panel.querySelector('#pbtn-pip').addEventListener('click',  doPip);
        panel.querySelector('#pbtn-copy').addEventListener('click', doCopy);
        panel.querySelector('#ytd-sbar-back').addEventListener('click',  closeSettings);
        panel.querySelector('#ytd-save-cfg').addEventListener('click',   saveSettings);
        panel.querySelector('#ytd-vol').value = volumeLevel;
        panel.querySelector('#ytd-vol').addEventListener('input', e => {
            volumeLevel = parseInt(e.target.value);
            GM_setValue('vol', String(volumeLevel));
            applyVolume();
        });
        panel.querySelector('#ytd-opacity-sl').addEventListener('input', e => {
            blurOpacity = parseInt(e.target.value);
            GM_setValue('blurOpacity', String(blurOpacity));
            const val = document.getElementById('ytd-opacity-val');
            if (val) val.textContent = blurOpacity + '%';
            applyBlur();
        });
        panel.querySelectorAll('.ytd-tab').forEach(tb => tb.addEventListener('click', () => switchTab(tb.dataset.t)));
 
        buildLangGrid();
    }
 
    function ytSvg(size) {
        return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="white" style="flex-shrink:0"><path d="M23.5 6.19a3.02 3.02 0 0 0-2.12-2.14C19.54 3.5 12 3.5 12 3.5s-7.54 0-9.38.55A3.02 3.02 0 0 0 .5 6.19C0 8.04 0 12 0 12s0 3.96.5 5.81a3.02 3.02 0 0 0 2.12 2.14C4.46 20.5 12 20.5 12 20.5s7.54 0 9.38-.55a3.02 3.02 0 0 0 2.12-2.14C24 15.96 24 12 24 12s0-3.96-.5-5.81zM9.75 15.02V8.98L15.5 12l-5.75 3.02z"/></svg>`;
    }
 
    function buildLangGrid() {
        const grid = document.getElementById('ytd-lang-grid');
        if (!grid) return;
        grid.innerHTML = '';
        LANGS.forEach(l => {
            const b = document.createElement('button');
            b.className = 'ytd-lbtn' + (cfg.hl === l.code ? ' on' : '');
            b.textContent = l.label;
            b.dataset.code = l.code;
            b.dataset.gl   = l.gl;
            b.addEventListener('click', () => {
                document.querySelectorAll('.ytd-lbtn').forEach(x => x.classList.remove('on'));
                b.classList.add('on');
            });
            grid.appendChild(b);
        });
    }
 
    // ╔══════════════════════════════════════════╗
    // ║  SETTINGS                                ║
    // ╚══════════════════════════════════════════╝
    function openSettings()  {
        buildLangGrid();
        const dw = document.getElementById('ytd-dw');
        const dh = document.getElementById('ytd-dh');
        if (dw) dw.value = defaultW;
        if (dh) dh.value = defaultH;
        const opSl  = document.getElementById('ytd-opacity-sl');
        const opVal = document.getElementById('ytd-opacity-val');
        if (opSl)  opSl.value = blurOpacity;
        if (opVal) opVal.textContent = blurOpacity + '%';
        document.getElementById('ytd-settings').classList.add('on');
    }
    function closeSettings() { document.getElementById('ytd-settings').classList.remove('on'); }
    function saveSettings() {
        const sel = document.querySelector('.ytd-lbtn.on');
        if (!sel) { closeSettings(); return; }
        const changed = sel.dataset.code !== cfg.hl;
        cfg.hl = sel.dataset.code;
        cfg.gl = sel.dataset.gl;
        saveCfg();
        const dwIn = document.getElementById('ytd-dw');
        const dhIn = document.getElementById('ytd-dh');
        if (dwIn && dhIn) {
            const nw = Math.max(280, Math.min(900, parseInt(dwIn.value) || 350));
            const nh = Math.max(280, Math.min(900, parseInt(dhIn.value) || 300));
            defaultW = nw; defaultH = nh;
            GM_setValue('dw', String(nw)); GM_setValue('dh', String(nh));
        }
        closeSettings();
        applyUILang();
        if (changed) {
            const q = document.getElementById('ytd-q').value.trim();
            if (q) { toast(t('toastReload')); doSearch(); }
            else    toast(t('toastSaved'));
        } else {
            toast(t('toastSaved'));
        }
    }
 
    // ╔══════════════════════════════════════════╗
    // ║  DRAG & RESIZE                           ║
    // ╚══════════════════════════════════════════╝
    function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
 
    function addEdgeResize(panel) {
        const EDGE = 8;
        let dir = null, resizing = false;
        let startX, startY, startW, startH, startL, startT;
 
        function getDir(e) {
            const r = panel.getBoundingClientRect();
            const x = e.clientX - r.left, y = e.clientY - r.top;
            return { l: x <= EDGE, r: x >= r.width - EDGE, t: y <= EDGE, b: y >= r.height - EDGE };
        }
        function cursorFor(d) {
            if ((d.l && d.t) || (d.r && d.b)) return 'nwse-resize';
            if ((d.r && d.t) || (d.l && d.b)) return 'nesw-resize';
            if (d.l || d.r) return 'ew-resize';
            if (d.t || d.b) return 'ns-resize';
            return '';
        }
 
        panel.addEventListener('mousemove', e => {
            if (resizing) return;
            if (e.target.closest && e.target.closest('#ytd-head')) { panel.style.cursor = ''; dir = null; return; }
            const _pr = panel.getBoundingClientRect();
            const _px = e.clientX - _pr.left, _py = e.clientY - _pr.top;
            const _onEdge = _px <= EDGE || _px >= _pr.width - EDGE || _py <= EDGE || _py >= _pr.height - EDGE;
            if (!_onEdge) { panel.style.cursor = ''; dir = null; return; }
            dir = getDir(e);
            panel.style.cursor = cursorFor(dir);
        });
        panel.addEventListener('mouseleave', () => {
            if (!resizing) { panel.style.cursor = ''; dir = null; }
        });
        panel.addEventListener('mousedown', e => {
            // Recompute dir at mousedown — avoids stale dir from iframes eating mousemove
            const _fd = getDir(e);
            if (!_fd.l && !_fd.r && !_fd.t && !_fd.b) return;
            if (e.target.closest && e.target.closest('#ytd-head,button,input,a,select,textarea')) return;
            dir = _fd;
            resizing = true;
            startX = e.clientX; startY = e.clientY;
            const r = panel.getBoundingClientRect();
            startW = r.width; startH = r.height; startL = r.left; startT = r.top;
            // Disable iframe pointer-events so resize mousemove fires on document
            panel.querySelectorAll('iframe').forEach(f => f.style.pointerEvents = 'none');
            document.body.style.userSelect = 'none';
            e.preventDefault(); e.stopPropagation();
        });
        document.addEventListener('mousemove', e => {
            if (!resizing) return;
            const dx = e.clientX - startX, dy = e.clientY - startY;
            const vw = window.innerWidth, vh = window.innerHeight;
            let w = startW, h = startH, l = startL, top = startT;
            if (dir.r) w = clamp(startW + dx, 280, vw - startL - 4);
            if (dir.b) h = clamp(startH + dy, 300, vh - startT - 4);
            if (dir.l) { const nw = clamp(startW - dx, 280, startL + startW - 4); l = startL + startW - nw; w = nw; }
            if (dir.t) { const nh = clamp(startH - dy, 300, startT + startH - 4); top = startT + startH - nh; h = nh; }
            panel.style.width = w + 'px'; panel.style.height = h + 'px';
            panel.style.left  = l + 'px'; panel.style.top   = top + 'px';
            panel.style.right = 'auto'; panel.style.bottom = 'auto';
            panelW = w; panelH = h; panelX = l; panelY = top;
            GM_setValue('pw', String(w)); GM_setValue('ph', String(h));
            GM_setValue('px', String(l)); GM_setValue('py', String(top));
            e.preventDefault();
        });
        document.addEventListener('mouseup', () => {
            if (resizing) {
                resizing = false;
                dir = null;
                panel.style.cursor = '';
                panel.querySelectorAll('iframe').forEach(f => f.style.pointerEvents = '');
                document.body.style.userSelect = '';
            }
        });
    }
 
    function makeDraggable(el, xk, yk, handle, isFab) {
        const h = handle || el;
        let dx = 0, dy = 0, dragging = false, moved = false;
        h.addEventListener('mousedown', e => {
            if (e.target.closest('button,a,input,label')) return;
            dragging = true; moved = false;
            dx = e.clientX - el.getBoundingClientRect().left;
            dy = e.clientY - el.getBoundingClientRect().top;
            e.preventDefault();
        });
        document.addEventListener('mousemove', e => {
            if (!dragging) return;
            moved = true;
            const x = clamp(e.clientX - dx, 0, window.innerWidth  - el.offsetWidth);
            const y = clamp(e.clientY - dy, 0, window.innerHeight - el.offsetHeight);
            el.style.left = x + 'px'; el.style.top = y + 'px';
            el.style.right = 'auto'; el.style.bottom = 'auto';
            GM_setValue(xk, String(x)); GM_setValue(yk, String(y));
        });
        document.addEventListener('mouseup', () => {
            if (isFab && dragging && moved) { el._drag = true; setTimeout(() => { el._drag = false; }, 60); }
            dragging = false;
        });
        if (isFab) el.addEventListener('click', e => { if (el._drag) e.stopImmediatePropagation(); });
    }
 
    // ╔══════════════════════════════════════════╗
    // ║  PANEL / TABS                            ║
    // ╚══════════════════════════════════════════╝
    function togglePanel() {
        panelOpen = !panelOpen;
        document.getElementById('ytd-panel').classList.toggle('open', panelOpen);
        if (panelOpen) document.getElementById('ytd-q').focus();
    }
 
    function showSearchRow() {
        document.getElementById('ytd-search-row')?.classList.remove('ytd-sr-hidden');
        document.getElementById('ytd-search-toggle')?.classList.remove('on');
    }
    function hideSearchRow() {
        document.getElementById('ytd-search-row')?.classList.add('ytd-sr-hidden');
        document.getElementById('ytd-search-toggle')?.classList.add('on');
    }
 
    function stopPlayback() {
        const player = document.getElementById('ytd-player');
        if (player && player.classList.contains('on')) {
            player.classList.remove('on');
            document.getElementById('ytd-frame').src = '';
            activeId = null; activeData = null;
        }
        closeShortsPlayer();
    }
 
    function switchTab(tb) {
        stopPlayback();
        activeView = tb;
        _loadMoreFn = null;
        const _oldB = document.getElementById('ytd-load-more'); if (_oldB) _oldB.remove();
        if (tb === 'search') showSearchRow();
        document.querySelectorAll('.ytd-tab').forEach(b => b.classList.toggle('on', b.dataset.t === tb));
        const grid = document.getElementById('ytd-grid');
        if (grid) grid.classList.toggle('ytd-shorts-grid', tb === 'shorts');
        const qEl = document.getElementById('ytd-q');
        if (qEl) qEl.placeholder = tb === 'shorts' ? t('phShorts') : tb === 'live' ? t('phLive') : t('searchHint');
        if (tb === 'shorts') {
            if (!shortsLoaded) loadShorts();
            else renderItems(shorts);
        } else if (tb === 'live') {
            if (!liveLoaded) loadLive();
            else renderItems(live);
        } else {
            renderItems(tb === 'fav' ? favorites : results);
        }
    }
 
    async function loadShorts() {
        setLoad(true);
        try {
            shorts = await ytGetShorts();
            shortsLoaded = true;
            _loadMoreFn = async () => {
                const _more = await ytGetShorts();
                const _seen = new Set(shorts.map(x => x.id));
                for (const v of _more) { if (!_seen.has(v.id)) { _seen.add(v.id); shorts.push(v); } }
                if (activeView === 'shorts') renderItems(shorts);
            };
            if (activeView === 'shorts') renderItems(shorts);
        } catch { if (activeView === 'shorts') renderItems([]); }
        finally { setLoad(false); }
    }
 
    async function loadLive() {
        setLoad(true);
        try {
            live = await ytGetLive();
            liveLoaded = true;
            _loadMoreFn = async () => {
                const _more = await ytGetLive();
                const _seen = new Set(live.map(x => x.id));
                for (const v of _more) { if (!_seen.has(v.id)) { _seen.add(v.id); live.push(v); } }
                if (activeView === 'live') renderItems(live);
            };
            if (activeView === 'live') renderItems(live);
        } catch { if (activeView === 'live') renderItems([]); }
        finally { setLoad(false); }
    }
 
    function openShortsPlayer(idx) {
        if (!shorts.length) return;
        shortsCurrent = Math.max(0, Math.min(idx, shorts.length - 1));
        const item = shorts[shortsCurrent];
        document.getElementById('ytd-sp-title-sm').textContent    = item.title;
        document.getElementById('ytd-sp-footer-title').textContent = item.title;
        document.getElementById('ytd-sp-ch-nm').textContent       = item.channel || '';
        document.getElementById('ytd-sp-counter').textContent     = `${shortsCurrent + 1}/${shorts.length}`;
        document.getElementById('ytd-sp-yt-link').href = `https://www.youtube.com/shorts/${item.id}`;
        document.getElementById('ytd-sp-frame').src =
            `https://www.youtube.com/embed/${item.id}?autoplay=1&loop=1&playlist=${item.id}&rel=0&modestbranding=1`;
        document.getElementById('ytd-sp-up').disabled   = shortsCurrent === 0;
        document.getElementById('ytd-sp-down').disabled = shortsCurrent >= shorts.length - 1;
        document.getElementById('ytd-shorts-player').classList.add('on');
        if (!watchedIds.has(item.id)) { watchedIds.add(item.id); save(); }
    }
 
    function closeShortsPlayer() {
        document.getElementById('ytd-shorts-player').classList.remove('on');
        document.getElementById('ytd-sp-frame').src = '';
    }
 
    function shortsNav(dir) {
        const next = shortsCurrent + dir;
        if (next < 0 || next >= shorts.length) return;
        openShortsPlayer(next);
    }
 
    // ╔══════════════════════════════════════════╗
    // ║  SEARCH                                  ║
    // ╚══════════════════════════════════════════╝
    async function doSearch() {
        const q = document.getElementById('ytd-q').value.trim();
        if (!q) return;
        setLoad(true);
        if (activeView === 'shorts') {
            try {
                shorts = await ytSearchShorts(q);
                shortsLoaded = true;
                _loadMoreFn = async () => {
                    const _more = await ytSearchShorts(q);
                    const _seen = new Set(shorts.map(x => x.id));
                    for (const v of _more) { if (!_seen.has(v.id)) { _seen.add(v.id); shorts.push(v); } }
                    renderItems(shorts);
                };
                renderItems(shorts);
            } catch { renderItems([]); }
            finally { setLoad(false); }
            return;
        }
        if (activeView === 'live') {
            try {
                live = await ytSearchLive(q);
                liveLoaded = true;
                _loadMoreFn = async () => {
                    const _more = await ytSearchLive(q);
                    const _seen = new Set(live.map(x => x.id));
                    for (const v of _more) { if (!_seen.has(v.id)) { _seen.add(v.id); live.push(v); } }
                    renderItems(live);
                };
                renderItems(live);
            } catch { renderItems([]); }
            finally { setLoad(false); }
            return;
        }
        activeView = 'search';
        document.querySelectorAll('.ytd-tab').forEach(b => b.classList.toggle('on', b.dataset.t === 'search'));
        try {
            results = await ytSearch(q);
            _loadMoreFn = async () => {
                const _more = await ytSearch(q);
                const _seen = new Set(results.map(x => x.id));
                for (const v of _more) { if (!_seen.has(v.id)) { _seen.add(v.id); results.push(v); } }
                renderItems(results);
            };
            renderItems(results);
        } catch { renderItems([]); }
        finally { setLoad(false); }
    }
 
    // ╔══════════════════════════════════════════╗
    // ║  RENDER                                  ║
    // ╚══════════════════════════════════════════╝
    function renderItems(items) {
        const empty = document.getElementById('ytd-empty');
        const grid  = document.getElementById('ytd-grid');
        grid.innerHTML = '';
        if (!items || !items.length) {
            empty.style.display = 'flex'; grid.style.display = 'none';
            showSearchRow();
            empty.innerHTML = activeView === 'fav'
                ? `<span class="emo">⭐</span><span>${t('noFav')}</span>`
                : activeView === 'live'
                ? `<span class="emo">🔴</span><span>${t('noLive')}</span>`
                : activeView === 'shorts'
                ? `<span class="emo">▶</span><span>${t('shortSearchHint')}</span>`
                : `<span class="emo">🔍</span><span>${t('emptyHint')}</span>`;
            return;
        }
        empty.style.display = 'none'; grid.style.display = 'grid';
        // Hide search row to give more room (keep visible on search tab)
        if (activeView !== 'search') hideSearchRow(); else showSearchRow();
        items.forEach((item, idx) => {
            const c = document.createElement('div');
            c.className = 'ytd-card';
            const w = watchedIds.has(item.id);
            if (item.isShort) c.classList.add('ytd-short');
            c.innerHTML = `
                <div class="ytd-thumb-wrap">
                    <img class="ytd-thumb" src="${item.thumb}" loading="lazy" alt="">
                    ${item.duration ? `<span class="ytd-dur">${item.duration}</span>` : ''}
                    ${item.isShort ? '<span class="ytd-short-badge">Short</span>' : ''}
                    ${item.isLive  ? '<span class="ytd-live-badge">🔴 LIVE</span>' : ''}
                    ${w ? '<span class="ytd-done">✓</span>' : ''}
                </div>
                <div class="ytd-info">
                    <div class="ytd-title">${esc(item.title)}</div>
                    ${item.channel ? `<div class="ytd-ch">${esc(item.channel)}</div>` : ''}
                </div>`;
            c.addEventListener('click', () => {
                if (item.isShort && activeView === 'shorts') openShortsPlayer(idx);
                else openPlayer(item);
            });
            grid.appendChild(c);
        });
        // Load more button
        const _oldLmBtn = document.getElementById('ytd-load-more');
        if (_oldLmBtn) _oldLmBtn.remove();
        if (_loadMoreFn) {
            const lmBtn = document.createElement('button');
            lmBtn.id = 'ytd-load-more';
            lmBtn.textContent = t('loadMore');
            lmBtn.addEventListener('click', async () => {
                lmBtn.disabled = true;
                lmBtn.textContent = t('loading');
                await _loadMoreFn();
                lmBtn.disabled = false;
                lmBtn.textContent = t('loadMore');
            });
            const bodyEl = document.getElementById('ytd-body');
            if (bodyEl) bodyEl.appendChild(lmBtn);
        }
    }
 
    function esc(s) { return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
 
    function setLoad(on) {
        document.getElementById('ytd-loading').style.display = on ? 'flex' : 'none';
        document.getElementById('ytd-empty').style.display   = on ? 'none' : '';
        document.getElementById('ytd-grid').style.display    = on ? 'none' : '';
    }
 
    // ╔══════════════════════════════════════════╗
    // ║  PLAYER                                  ║
    // ╚══════════════════════════════════════════╝
    function openPlayer(item) {
        activeId = item.id; activeData = item;
        document.getElementById('ytd-pbar-ttl').textContent = item.title;
        document.getElementById('ytd-pv-title').textContent = item.title;
        document.getElementById('ytd-pv-ch').textContent    = item.channel || '';
        document.getElementById('ytd-yt-link').href = `https://www.youtube.com/watch?v=${item.id}`;
        const fr = document.getElementById('ytd-frame');
        fr.src = (item.isShort ? 'https://www.youtube.com/embed/' : 'https://www.youtube.com/embed/') + item.id + '?autoplay=1&rel=0&modestbranding=1&enablejsapi=1' + (item.isShort ? '' : '&vq=hd1080');
        fr.onload = () => { setTimeout(applyVolume, 800); };
        document.getElementById('ytd-player').classList.add('on');
        if (!watchedIds.has(item.id)) { watchedIds.add(item.id); save(); }
        updateFavBtn();
    }
 
    function goHome() {
        document.getElementById('ytd-player').classList.remove('on');
        closeShortsPlayer();
        document.getElementById('ytd-frame').src = '';
        activeId = null;
        const panel = document.getElementById('ytd-panel');
        if (panel) { panelW = defaultW; panelH = defaultH; panel.style.width = defaultW + 'px'; panel.style.height = defaultH + 'px'; GM_setValue('pw', String(defaultW)); GM_setValue('ph', String(defaultH)); }
        activeView = 'search';
        results = [];
        showSearchRow();
        document.getElementById('ytd-q').value = '';
        document.querySelectorAll('.ytd-tab').forEach(b => b.classList.toggle('on', b.dataset.t === 'search'));
        const _grid = document.getElementById('ytd-grid');
        _grid.style.display = 'none';
        _grid.classList.remove('ytd-shorts-grid');
        document.getElementById('ytd-loading').style.display = 'none';
        const empty = document.getElementById('ytd-empty');
        empty.style.display = 'flex';
        empty.innerHTML = `<span class="emo">🔍</span><span>${t('emptyHint')}</span>`;
        document.getElementById('ytd-q').focus();
    }
 
    function toggleFav() {
        if (!activeData) return;
        const i = favorites.findIndex(f => f.id === activeData.id);
        if (i === -1) { favorites.push(activeData); toast(t('toastFavAdd')); }
        else          { favorites.splice(i, 1);     toast(t('toastFavRm')); }
        save(); updateFavBtn();
    }
 
    function updateFavBtn() {
        const btn = document.getElementById('pbtn-fav');
        if (!btn || !activeData) return;
        const on = favorites.some(f => f.id === activeData.id);
        btn.classList.toggle('on', on);
        btn.textContent = on ? t('btnFavOn') : t('btnFav');
    }
 
    function doPip() {
        const fr = document.getElementById('ytd-frame');
        if (fr.requestPictureInPicture) fr.requestPictureInPicture().catch(() => {});
        else window.open(`https://www.youtube.com/embed/${activeId}?autoplay=1`, 'pip', 'width=480,height=270');
    }
 
    function doCopy() {
        if (!activeId) return;
        navigator.clipboard.writeText(`https://www.youtube.com/watch?v=${activeId}`)
            .then(() => toast(t('toastCopied')));
    }
 
    // ╔══════════════════════════════════════════╗
    // ║  TOAST                                   ║
    // ╚══════════════════════════════════════════╝
    let _tt = null;
    function toast(msg) {
        const el = document.getElementById('ytd-toast');
        if (!el) return;
        el.textContent = msg; el.classList.add('on');
        clearTimeout(_tt); _tt = setTimeout(() => el.classList.remove('on'), 2200);
    }
 
    // ╔══════════════════════════════════════════╗
    // ║  KEYBOARD                                ║
    // ╚══════════════════════════════════════════╝
    document.addEventListener('keydown', e => {
        if (e.altKey && e.key === 'y') { togglePanel(); return; }
        const spOn = document.getElementById('ytd-shorts-player').classList.contains('on');
        if (spOn && e.key === 'ArrowUp')   { e.preventDefault(); shortsNav(-1); return; }
        if (spOn && e.key === 'ArrowDown') { e.preventDefault(); shortsNav(1);  return; }
        if (spOn && e.key === 'Escape')    { closeShortsPlayer(); return; }
        if (e.key === 'Escape' && activeId) goHome();
    });
 
    // ── INIT ─────────────────────────────────────────────────────
    buildUI();
    applyUILang();
 
})();