Youlingo

Mini YouTube on Duolingo

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==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();
 
})();