☰

BongaCams Unchained

Removes annoying UI elements, adds PiP, fullscreen mode, a bookmarks base.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name:ru      BongaCams Unchained
// @name         BongaCams Unchained
// @namespace    DiadiaBonga
// @version      2.4.0
// @description:ru Убирает мусор, добавляет PiP, полноэкранный режим, запись, базу закладок.
// @description  Removes annoying UI elements, adds PiP, fullscreen mode, a bookmarks base.
// @author       DiaDiaBogDan
// @license      MIT
// @match        *://*.bongacams.com/*
// @match        *://*.bongacams2.com/*
// @match        *://*.bongocams.com/*
// @match        *://*.webcamsluts.ru/*
// @match        *://*.stripchat.com/*
// @match        *://stripchat.com/*
// @match        *://*.chaturbate.com/*
// @match        *://chaturbate.com/*
// @icon         https://i.bgicdn.com/favicon/bc/favicon.svg?20240227
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addValueChangeListener
// @connect      bongacams.com
// @connect      bongacams2.com
// @connect      bongocams.com
// @connect      webcamsluts.ru
// @connect      stripchat.com
// @connect      chaturbate.com
// @connect      bestcam.tv
// @connect      camshowrecordings.com
// @connect      camshowrecord.net
// @connect      livecamsrip.com
// @connect      camdownload.net
// @connect      camshow.cc
// @connect      cdncontent.top
// @connect      pornotorrent.top
// @connect      pornolab.net
// @connect      simpcity.cr
// @connect      camsclips.com
// @noframes
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    let streamUrl = null, streamPath = '';
    window.addEventListener('bc_hls', e => { streamUrl = e.detail; streamPath = location.pathname; });
    const getStreamUrl = () => (streamUrl && streamPath === location.pathname) ? streamUrl : null;

    const blockLimits = () => {
        const s = document.createElement('script');
        s.textContent = `try{if('serviceWorker' in navigator)navigator.serviceWorker.register=()=>new Promise(()=>{});window.redirectOnLimit=()=>{};const emit=u=>{try{u=String(u);if(u.includes('.m3u8')&&!u.includes('chunk'))window.dispatchEvent(new CustomEvent('bc_hls',{detail:new URL(u,location.href).href}))}catch{}};const o=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(m,u){emit(u);return o.apply(this,arguments)};const f=window.fetch;if(f)window.fetch=function(i){try{emit(i&&i.url?i.url:i)}catch{}return f.apply(this,arguments)};const s=Storage.prototype.setItem,r=/guestTime|ls\\.tft|limit|freeTime|timeLimit/i;Storage.prototype.setItem=function(k,v){if(!r.test(k))s.apply(this,arguments)};const c=Object.getOwnPropertyDescriptor(Document.prototype,'cookie')||Object.getOwnPropertyDescriptor(HTMLDocument.prototype,'cookie');if(c?.set){const re=/^(limit|time|guest|free)/i;Object.defineProperty(document,'cookie',{get:()=>c.get.call(document),set:v=>{if(!re.test(v.trim()))c.set.call(document,v)},configurable:true})}}catch{}`;
        (document.head || document.documentElement).appendChild(s);
        s.remove();
    };
    blockLimits();

    const Store = {
        get: (k, def) => {
            try {
                const v = GM_getValue(k);
                if (v == null) return def;
                const p = JSON.parse(v);
                return (Array.isArray(def) && !Array.isArray(p)) || p === null ? def : p;
            } catch { return def; }
        },
        set: (k, v) => { try { GM_setValue(k, JSON.stringify(v)); } catch {} }
    };

    const clamp = (v, min, max) => Math.max(min, Math.min(v, max));
    const escHTML = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
    const safeDecode = s => { try { return decodeURIComponent(s); } catch { return s; } };

    const MINI_SHIFT_X = 168, MINI_SHIFT_Y = 5;
    const BM_HOST = /(^|\.)(bongacams2?\.com|bongocams\.com|webcamsluts\.ru|stripchat\.com|chaturbate\.com)$/;
    const isSafeUrl = u => { try { const x = new URL(u); return x.protocol === 'https:' && BM_HOST.test(x.hostname); } catch { return false; } };
    const BC_RE = /bongacams|bongocams|webcamsluts/;
    const siteKey = h => BC_RE.test(h) ? 'bc' : /stripchat/.test(h) ? 'sc' : /chaturbate/.test(h) ? 'cb' : h;
    const urlSite = u => { try { return siteKey(new URL(u).hostname); } catch { return ''; } };
    const cleanBm = b => {
        try {
            if (!b || typeof b.name !== 'string' || typeof b.url !== 'string' || !b.name.trim()) return null;
            if (!isSafeUrl(b.url)) return null;
            const u = new URL(b.url);
            return {
                name: b.name.slice(0, 100),
                url: u.href,
                avatar: typeof b.avatar === 'string' && b.avatar.startsWith('https://') ? b.avatar : '',
                note: typeof b.note === 'string' ? b.note.slice(0, 500) : ''
            };
        } catch { return null; }
    };
    const stripBm = ({ _updateItemDOM, _topicExp, _lastImg, _chk, _isUpdating, ...rest }) => rest;

    const BC_CONTAINERS = ['#stream-container', '#ResizablePane', '#EmbedSWFHolder', '.player-container', '.performer-video-container', '.chat-video-container', '.video-panel', '#video-panel-wrap'];

    const bcTokC = n => `[class^="${n}"],[class*=" ${n}"]`;
    const bcTokI = n => `[id^="${n}"]`;
    const BC_HIDE = [
        '.bc-header', '.bc-header-main', '.h_container', '#mainbar_container', '.join_button_container', '.js-chat_join_button', '.bcm_socials', '.bc-footer', '#footer_container', '.bcm_footer', '#id-chat-header', '#games_button', '.mls_title', '.live_tabs', '[data-role="header"]', '#popupChatSelection', '#popupGroup', '#popupVoyeur', '#gamecontrol_popup_container', '.get_app_banner', '.js-get_app_banner', '#chat_actions_bar', '.bcm_chat_footer', '.ui-popup-container', '.chat_alert_container', '#chat_prompt_container', '.fancybox-overlay',
        '[class*="promo-banner"]', '[class*="age-gate"]', '[class*="age_gate"]', '[class*="cookie-banner"]', '[class*="notification-bar"]',
        '#firstChatTooltipContainer', '.gab_wrp', '.m_socials', '.js-bottom_alerts_area', '.chat_bar_alert_container', '#mls_filter_result_container', '.cam2cam_modal', '#top_banner_above_chat', '.banner_top_chat', '#alerts_above_chat', '.banner_wrp', '.js-index_banner', '.sb_topmodel', '.free_tokens_panel', '#jsx-ls_seo_custom_text', '#jsx-ls_footer_container', '.footer_seo', 'footer.footer', '.footer_copy', '.footer_mobile_guide',
        '.js-chat_dialog_window', '.js-dialog_window', '.guide_popup', '.guide-popup', '.guide_overlay', '.tutorial_overlay',
        ...['ims_', 'ims-', 'spoof_im', 'chat_dialog', 'dialog_window', 'dialog_wrp', 'im_dialog', 'fan_boost_message', 'fake_chat', 'direct_message', 'quick_chat', 'message_popup', 'chat_popup', 'mobile_guide', 'guide_', 'swipe_guide', 'tutorial', 'onboarding'].map(bcTokC),
        ...['chat_dialog', 'ims_', 'mobile_guide', 'guide_', 'swipe_guide'].map(bcTokI)
    ].join(',');

    const BC_CSS = `${BC_HIDE}{display:none!important;opacity:0!important;pointer-events:none!important;height:0!important;margin:0!important;padding:0!important}#header_login,#header_login .join_btn,#header_login .jbt_inner{display:inline-flex!important;opacity:1!important;pointer-events:auto!important;height:auto!important;visibility:visible!important}.performer-frame,.blurred-performer-frame{pointer-events:none!important}#video-panel-wrap,#stream-container,[class*="video-container"],[class*="player-wrap"]{pointer-events:auto!important;touch-action:manipulation}.chat_panel_wrap{height:100%!important;flex:1}body:not(.page_index),body:not(.page_index) .ui-page,body:not(.page_index) .js-page,body:not(.page_index) .bc-content{padding-top:0!important;margin-top:0!important}.bc_alternate_header_live_tabs,.mls_hash_tag,.__fixed_top{position:static!important;margin:0!important}.bc_sticky_bar{position:fixed!important;top:var(--bc-sticky-y,80px)!important;left:var(--bc-sticky-x,50%)!important;transform:var(--bc-sticky-tf,translateX(-50%))!important;margin:0!important;bottom:auto!important;z-index:9990!important;pointer-events:none!important}.js-filter_button{pointer-events:auto!important;display:flex;justify-content:center}#chat_bar_v2_container,#chat_bar_v2,#chatProfileButton,#bChatProfilePhotosLink,#chatBarChatButton,#switch_between_chat{display:flex!important;opacity:1!important;pointer-events:auto!important;z-index:2147483605!important}@media (min-width:1024px){*:fullscreen video,*:-webkit-full-screen video,video:fullscreen,video:-webkit-full-screen{object-fit:contain!important;width:100%!important;height:100%!important;max-width:100vw!important;max-height:100vh!important;margin:auto!important}}`;

    const firstSegment = () => location.pathname.split('/').filter(Boolean)[0] || '';

    const currentSite = (() => {
        const hostname = window.location.hostname;
        const configs = {
            bongacams: {
                match: BC_RE,
                videoSelectors: BC_CONTAINERS.map(s => `${s} video`).concat('video'),
                containerSelectors: BC_CONTAINERS,
                fullscreenSelectors: BC_CONTAINERS,
                getName: () => {
                    const [first = '', second = ''] = location.pathname.split('/').filter(Boolean);
                    const isSys = /^(?:ru|uk|en|es|de|fr|pt|it|pl|tr|tags?|categor(?:y|ies)|free-cams|all|search|terms|privacy|faq|contacts|login|signup|register|apps|mobile|profile)$/i.test(first);
                    const fromUrl = first ? safeDecode(isSys ? (first.toLowerCase() === 'profile' ? second : '') : first) : '';
                    if (fromUrl) return fromUrl;
                    const d = Helpers.getModelSecretData();
                    return d?.chatHost?.username || d?.chatHost?.displayName
                        || document.querySelector('#bChatRoomTitle, .chat-header-inner h1 a')?.textContent?.trim() || '';
                },
                css: BC_CSS
            },
            stripchat: {
                match: /stripchat/,
                videoSelectors: ['video.player-video', 'video'],
                containerSelectors: ['.player-video-container', '.player-container'],
                fullscreenSelectors: ['.player-video-container', '.player-container'],
                getName: () => {
                    const first = firstSegment();
                    if (!first || /^(?:girls|men|couples|trans|tags?|favorites|login|signup|search|blog|cams|explore|top|api|help|support|terms|privacy|models|profile|settings|videos|photos|pricing|studios|category|categories|new|vr|contest|announcement|user|guests|latest)$/i.test(first)) return '';
                    return /^[\w-]+$/.test(first) ? first : '';
                },
                css: `[class*="banner"] { display:none!important; }`
            },
            chaturbate: {
                match: /chaturbate/,
                videoSelectors: ['#VideoPanel video', '.videoPlayerDiv video', 'video'],
                containerSelectors: ['#VideoPanel', '.videoPlayerDiv', '#video_container'],
                fullscreenSelectors: ['#VideoPanel', '.videoPlayerDiv', '#video_container'],
                getName: () => {
                    const first = firstSegment();
                    if (!first || /^(?:b|tags?|.+-cams|accounts|auth|tipping|supporter|affiliates|api|contest|apps|security|terms|privacy|dmca|support|billing|photo_videos|store|login|signup|register|search|explore|hosting)$/i.test(first)) return '';
                    return /^\w+$/.test(first) ? first : '';
                },
                css: ''
            }
        };
        return Object.values(configs).find(c => c.match.test(hostname)) || configs.bongacams;
    })();

    const AppState = {
        bgPlay: Store.get('bc_bg', true),
        isMinimized: Store.get('bc_min', false),
        isHidden: Store.get('bc_hid', false),
        scale: parseFloat(Store.get('bc_scale', 1)),
        posX: Store.get('bc_x', null),
        posY: Store.get('bc_y', null),
        hasPos: Store.get('bc_pos', false),
        theme: Store.get('bc_theme', 'rgba(220, 20, 60, 0.9)'),
        lang: Store.get('bc_lang', 'RU'),
        gmute: Store.get('bc_gmute', false) === true,
        bookmarks: Store.get('bc_bookmarks', [])
    };

    const saveBookmarks = () => Store.set('bc_bookmarks', AppState.bookmarks.map(stripBm));
    const setGMute = v => { AppState.gmute = !!v; Store.set('bc_gmute', !!v); };
    const sameBm = (b, name) => b.name === name && urlSite(b.url) === siteKey(location.hostname);
    const isFavName = name => !!name && AppState.bookmarks.some(b => sameBm(b, name));

    const injectSiteCSS = () => {
        if (document.getElementById('bc-unchained-site-css')) return;
        const fx = Store.get('bc_fx', null), fy = Store.get('bc_fy', null);
        let css = `${currentSite.css || ''} .js-filter_button, .bc_sticky_bar { touch-action: none !important; user-select: none !important; -webkit-user-drag: none !important; }
body.bc-theater-mode{overflow:hidden!important;background:#000!important;margin:0!important;padding:0!important}
body.bc-theater-mode :is(.chat_panel_wrap,.player_footer,[class*="chat-panel"],[class*="chat_panel"],[class*="player-footer"],#chat_bar_v2_container,#chat_bar_v2,#switch_between_chat,.chat-tabs,[class*="chat_nav"],[class*="chat_bar"],#games_button,.mls_title,.live_tabs,[class*="footer"]:not(#bc-unchained-host *)){display:none!important}
body.bc-theater-mode .bc-thtr-wrap{position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;z-index:2147483644!important;background:#000!important;margin:0!important;padding:0!important;transform:none!important;display:block!important}
body.bc-theater-mode video.bc-thtr-video{position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;z-index:2147483646!important;object-fit:contain!important;background:#000!important;margin:0!important;padding:0!important;transform:none!important}`;
        if (fx && fy) {
            css += `.bc_sticky_bar{left:${fx}!important;top:${fy}!important;transform:none!important;margin:0!important;bottom:auto!important;right:auto!important;width:max-content!important;transition:none!important;animation:none!important}`;
        }
        const s = document.createElement('style');
        s.id = 'bc-unchained-site-css';
        s.textContent = css;
        (document.head || document.documentElement).appendChild(s);
    };
    injectSiteCSS();

    const pSvg = (d, s = 20, fill = 'none', stroke = 'currentColor') => `<svg viewBox="0 0 24 24" width="${s}" height="${s}" stroke="${stroke}" fill="${fill}" stroke-width="2">${d}</svg>`;
    const icons = {
        color: pSvg('<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"/>', 14),
        menu: pSvg('<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/>', 24),
        eye: pSvg('<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>', 14),
        close: pSvg('<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>', 16),
        home: pSvg('<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>', 16),
        rel: pSvg('<polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>', 16),
        min: pSvg('<line x1="5" y1="12" x2="19" y2="12"/>', 16),
        bgOn: pSvg('<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>'),
        bgOff: pSvg('<circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/>'),
        vol: pSvg('<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/>'),
        mute: pSvg('<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><line x1="23" y1="9" x2="17" y2="15"/><line x1="17" y1="9" x2="23" y2="15"/>'),
        volLow: pSvg('<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>'),
        volMed: pSvg('<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>'),
        snap: pSvg('<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/>'),
        rec: pSvg('<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3" fill="currentColor"/>'),
        qual: pSvg('<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>'),
        pip: pSvg('<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><rect x="11" y="11" width="8" height="8" rx="1" ry="1"/>'),
        fs: pSvg('<polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/>'),
        thtr: pSvg('<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>'),
        search: pSvg('<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>'),
        copy: pSvg('<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>'),
        fav: pSvg('<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>'),
        del: pSvg('<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/>', 16),
        hls: pSvg('<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>'),
        ok: pSvg('<polyline points="20 6 9 17 4 12"/>', 16, 'none', '#2ecc71'),
        arrowUp: pSvg('<line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/>', 12),
        arrowDown: pSvg('<line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/>', 12),
        back: pSvg('<polyline points="15 18 9 12 15 6"/>', 18),
        cam: pSvg('<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/>', 12),
        clock: pSvg('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>', 12),
        name: pSvg('<path d="M4 20l5-12 5 12"/><path d="M6 15h6"/>', 12),
        dot: pSvg('<circle cx="12" cy="12" r="7"/>', 12, '#2ecc71', '#2ecc71'),
        list: pSvg('<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>', 12),
        grid: pSvg('<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>', 12)
    };

    const i18n = {
        RU: { drag: '⁞⁞ ПУЛЬТ', bg: 'ФОН', sound: 'ЗВУК', snap: 'СКРИН', rec: 'ЗАПИСЬ', err: 'ОШИБКА', no_vid: 'НЕТ ВИДЕО', qual: 'КАЧ-ВО', pip: 'PIP', fs: 'ЭКРАН', thtr: 'ТЕАТР', search: 'ПОИСК', copy: 'ССЫЛКА', home: 'Домой', rel: 'Рестарт', min: 'Свернуть', hide: 'Скрыть (H)', hints: 'M=ЗВУК F=ЭКРАН T=ТЕАТР H=СКРЫТЬ', favs: 'ИЗБРАННОЕ', no_model: 'Модель не найдена', bm_title: 'Избранное', bm_add: 'В избранное', bm_empty: 'Список пуст.', bm_exp: 'Экспорт', bm_imp: 'Импорт', bm_dup: 'Уже добавлено!', note_ph: 'Заметка...' },
        EN: { drag: '⁞⁞ REMOTE', bg: 'BG', sound: 'VOL', snap: 'SNAP', rec: 'REC', err: 'ERROR', no_vid: 'NO VIDEO', qual: 'QUAL', pip: 'PIP', fs: 'FULL', thtr: 'THTR', search: 'SEARCH', copy: 'LINK', home: 'Home', rel: 'Reload', min: 'Minimize', hide: 'Hide (H)', hints: 'M=MUTE F=FULL T=THTR H=HIDE', favs: 'FAVS', no_model: 'Model not found', bm_title: 'Favorites', bm_add: 'Add current', bm_empty: 'List is empty.', bm_exp: 'Export', bm_imp: 'Import', bm_dup: 'Already added!', note_ph: 'Note...' }
    };
    const L = () => i18n[AppState.lang];

    const CSS_UI = `:host{--th:${AppState.theme};pointer-events:none;position:fixed;inset:0;z-index:2147483647;font-family:system-ui,-apple-system,sans-serif}*{box-sizing:border-box;margin:0;padding:0}.h-btn,.btn,.restore,.ghost,.vico,.z-btn,.bm-close,.bm-del,.bm-act-btn,.resize-handle{display:flex;align-items:center;justify-content:center}.panel{user-select:none;touch-action:none;position:absolute;width:285px;padding:12px;border-radius:16px;pointer-events:auto;background:rgba(15,15,18,.85);border:1px solid rgba(255,255,255,.1);box-shadow:0 10px 30px rgba(0,0,0,.7),inset 0 1px 1px rgba(255,255,255,.1);backdrop-filter:blur(24px) saturate(150%);transform-origin:top left;will-change:transform,left,top}.panel.mini{width:48px;height:48px;padding:0;background:transparent;border:none;box-shadow:none;backdrop-filter:none}.panel.hidden{display:none!important}.col{display:flex;flex-direction:column;gap:8px}.grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}.sep{height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,.15),transparent);margin:2px 0}.hdr{display:flex;justify-content:space-between;align-items:center;background:rgba(0,0,0,.35);border-radius:10px;padding:6px 8px;margin-bottom:6px}.drag{flex:1 1 auto;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:rgba(255,255,255,.6);font-size:9.5px;font-weight:600;letter-spacing:1px;text-transform:uppercase;cursor:grab;display:flex;align-items:center;justify-content:flex-start;padding-left:4px;user-select:none;touch-action:none!important;margin-right:auto}.drag:active{cursor:grabbing}.stats{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;background:rgba(0,0,0,.5);color:#fff;font-size:9px;min-width:30px;height:26px;padding:2px 4px;border-radius:6px;font-weight:bold;margin-right:2px;box-sizing:border-box;line-height:1;flex-shrink:0}.hdr-btns{display:flex;gap:5px;align-items:center;flex-shrink:0;margin-left:auto;justify-content:flex-end}.h-btn{width:24px;height:24px;background:rgba(255,255,255,.05);border:none;border-radius:6px;color:#fff;cursor:pointer;font-size:12px;transition:.2s}.h-btn:hover{background:rgba(255,255,255,.2);transform:translateY(-1px)}.h-btn.close:hover{background:#e74c3c}.h-btn.lang{width:28px;font-weight:bold;background:rgba(255,255,255,.1);font-size:10px}.btn{color:#fff;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.05);border-radius:10px;padding:8px 2px 6px;font-size:18px;cursor:pointer;flex-direction:column;width:100%;transition:all .2s ease;outline:none}.btn:hover{background:rgba(255,255,255,.15);transform:translateY(-2px);border-color:rgba(255,255,255,.2)}.btn:active{transform:scale(.92)}.btn.on{background:var(--th);box-shadow:0 4px 10px rgba(0,0,0,.3);border-color:transparent}.btn sub{font-size:8px;font-weight:600;margin-top:4px;letter-spacing:.5px;text-transform:uppercase;opacity:.8;pointer-events:none;text-align:center;word-break:break-word;line-height:1}.restore{width:48px;height:48px;border-radius:24px;font-size:22px;background:var(--th);border:2px solid rgba(255,255,255,.3);display:none;cursor:pointer;pointer-events:auto;box-shadow:0 4px 15px rgba(0,0,0,.6);transition:.2s;touch-action:none!important}.restore:hover{transform:scale(1.05)}.ghost{position:fixed;bottom:80px;right:16px;width:44px;height:44px;border-radius:22px;font-size:20px;background:var(--th);border:2px solid rgba(255,255,255,.35);display:none;cursor:pointer;pointer-events:auto;box-shadow:0 4px 15px rgba(0,0,0,.6);transition:transform .2s,opacity .2s;touch-action:manipulation;z-index:2147483647}.ghost:hover,.ghost:active{transform:scale(1.1)}.v-cap{display:flex;align-items:center;gap:8px;background:rgba(0,0,0,.4);border-radius:12px;padding:8px 12px;border:1px solid rgba(255,255,255,.05)}.vico{color:#fff;font-size:16px;cursor:pointer;flex-shrink:0;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.vslider{-webkit-appearance:none;appearance:none;flex:1;height:3px;border-radius:2px;outline:none;cursor:pointer;background:linear-gradient(to right,var(--th) 0%,var(--th) var(--p,100%),rgba(255,255,255,.1) var(--p,100%),rgba(255,255,255,.1) 100%)}.vslider::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;background:#fff;box-shadow:0 0 5px rgba(0,0,0,.5);border:2px solid var(--th);cursor:pointer;transition:.2s}.vslider::-webkit-slider-thumb:hover{transform:scale(1.3)}.vval{font-size:10px;font-weight:600;color:#ccc;min-width:32px;text-align:right}.row-4{display:flex;align-items:center;justify-content:space-between;gap:8px}.scale-ctrl{display:flex;align-items:center;gap:4px;background:rgba(0,0,0,.3);border-radius:6px;padding:2px}.z-btn{width:20px;height:20px;background:rgba(255,255,255,.05);border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:14px;transition:.2s;outline:none}.z-btn:hover{background:rgba(255,255,255,.2)}.s-val{font-size:9px;color:#ccc;min-width:24px;text-align:center;font-weight:bold}.hotkeys{font-size:9px;color:rgba(255,255,255,.4);text-align:center;font-weight:500;letter-spacing:.5px;padding-top:2px}.bm-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:.3s;backdrop-filter:blur(5px);z-index:2147483647}.bm-overlay.open{opacity:1;pointer-events:auto}.bm-modal{width:370px;max-width:96vw;height:90vh;max-height:90vh;min-height:450px;background:rgba(20,20,25,.95);border:1px solid rgba(255,255,255,.1);border-radius:16px;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 10px 40px rgba(0,0,0,.8)}.bm-hdr{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:rgba(0,0,0,.4);border-bottom:1px solid rgba(255,255,255,.05);flex-shrink:0}.bm-title{color:#fff;font-weight:bold;font-size:13px;white-space:nowrap;flex-shrink:0}.bm-close{background:none;border:none;color:#aaa;font-size:26px;cursor:pointer;padding:2px 8px;line-height:1;min-width:32px;min-height:32px}.bm-close:hover{color:#fff}.bm-list{flex:1;min-height:0;overflow-y:scroll;scrollbar-gutter:stable;padding:12px;display:flex;flex-direction:column;gap:10px}.bm-item{display:flex;flex-direction:column;gap:8px;padding:10px;background:rgba(255,255,255,.05);border-radius:12px;transition:.2s}.bm-item:hover{background:rgba(255,255,255,.08)}.bm-item-top{display:flex;align-items:center;justify-content:space-between;gap:10px}.bm-link{display:flex;align-items:center;gap:10px;text-decoration:none;color:#fff;flex:1;overflow:hidden}.bm-link:hover .bm-name{color:var(--th)}.bm-avatar{width:42px;height:42px;border-radius:50%;object-fit:cover;aspect-ratio:1/1;background:#333;border:1px solid rgba(255,255,255,.1);flex-shrink:0}.bm-name{font-size:15px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transition:.2s}.bm-del{width:30px;height:30px;border-radius:6px;background:rgba(231,76,60,.2);color:#e74c3c;border:none;cursor:pointer;transition:.2s;flex-shrink:0}.bm-del:hover{background:#e74c3c;color:#fff}.bm-del svg,.bm-modal .bm-del svg{width:14px;height:14px;margin:0!important;display:block;vertical-align:baseline}.bm-act-btn{height:28px;padding:0 8px;border-radius:6px;background:rgba(255,255,255,.06);color:#ccc;border:1px solid rgba(255,255,255,.08);gap:4px;cursor:pointer;font-size:11px;font-weight:600;transition:.2s}.bm-act-btn:hover{background:rgba(255,255,255,.15);color:#fff;border-color:rgba(255,255,255,.2)}.bm-act-btn svg{width:13px;height:13px;flex-shrink:0}.bm-note{width:100%;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.1);color:#fff;border-radius:8px;padding:8px 10px;font-size:13px;font-family:inherit;outline:none;transition:.2s}.bm-note:focus{border-color:var(--th);background:rgba(0,0,0,.6);box-shadow:0 0 5px rgba(255,255,255,.1)}.bm-foot{padding:12px;display:grid;grid-template-columns:1fr 1fr;gap:8px;background:rgba(0,0,0,.4);border-top:1px solid rgba(255,255,255,.05);flex-shrink:0}.bm-add{grid-column:span 2;padding:12px;border-radius:8px;border:none;background:var(--th);color:#fff;font-weight:bold;cursor:pointer;font-size:14px;box-shadow:0 2px 10px rgba(0,0,0,.3);transition:.2s}.bm-add:hover{filter:brightness(1.2)}.bm-add:disabled{background:#555;cursor:not-allowed;opacity:.5}.bm-btn{padding:10px;border-radius:8px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:#ddd;font-size:13px;font-weight:600;cursor:pointer;transition:.2s}.bm-btn:hover{background:rgba(255,255,255,.15);color:#fff}.bm-empty{text-align:center;padding:30px 10px;color:#777;font-size:14px;grid-column:1 / -1}.bm-gallery-mode{display:grid!important;grid-template-columns:1fr 1fr;align-content:start;gap:10px;padding:10px}.bm-gallery-mode .bm-item{padding:10px;cursor:pointer;display:flex;flex-direction:column;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:12px;position:relative;gap:6px}.bm-gallery-mode .bm-item:hover{transform:translateY(-2px);z-index:10;border-color:var(--th);background:rgba(255,255,255,.08);box-shadow:0 4px 12px rgba(0,0,0,.5)}.bm-gallery-mode .bm-topic,.bm-gallery-mode .bm-note{display:none!important}.bm-gallery-mode .bm-item-top{display:contents}.bm-gallery-mode .bm-link{display:contents}.bm-gallery-mode .bm-link>div{order:1;display:flex;align-items:center;width:calc(100% - 32px);max-width:calc(100% - 32px);pointer-events:none}.bm-gallery-mode .bm-del{order:2;position:absolute;top:6px;right:6px;width:24px;height:24px;z-index:2;margin:0;border-radius:6px}.bm-gallery-mode .bm-avatar{order:3;width:100%!important;height:auto!important;aspect-ratio:16/9!important;border-radius:8px;object-fit:cover;object-position:center;border:1px solid rgba(0,0,0,.3);pointer-events:none;margin:0;display:block!important;filter:brightness(.75)}.bm-gallery-mode .bm-item:has(img[data-frame]) .bm-avatar{display:none!important}.bm-gallery-mode img[data-frame]{order:3;width:100%!important;height:auto!important;aspect-ratio:16/9!important;border-radius:8px;object-fit:cover;border:1px solid rgba(0,0,0,.3)!important;margin:0!important;pointer-events:none;display:block!important}.bm-gallery-mode .bm-actions-row{order:4;display:grid!important;width:100%;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:auto;padding-top:2px}.bm-gallery-mode .bm-act-btn{height:28px;padding:0;background:rgba(0,0,0,.4);border-radius:6px;border:1px solid rgba(255,255,255,.05)}.bm-gallery-mode .bm-act-btn span{display:none}.bm-gallery-mode .bm-act-btn svg{margin:0;width:14px;height:14px}.bm-gallery-mode .bm-name{font-size:12px;color:#eee;font-weight:bold;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media (min-width:768px){.bm-modal{width:780px!important;max-width:92vw!important;height:85vh!important;max-height:85vh!important}.bm-list{display:grid!important;grid-template-columns:repeat(2,1fr)!important;align-content:start!important;gap:12px!important}.bm-gallery-mode{grid-template-columns:repeat(3,1fr)!important;gap:12px!important}}@media (min-width:1200px){.bm-modal{width:920px!important}.bm-gallery-mode{grid-template-columns:repeat(4,1fr)!important}}.bm-list::-webkit-scrollbar{width:6px}.bm-list::-webkit-scrollbar-thumb{background:rgba(255,255,255,.2);border-radius:3px}@keyframes pulseRec{0%,100%{background:rgba(255,255,255,.06);box-shadow:none}50%{background:#c0392b;box-shadow:0 2px 8px rgba(0,0,0,.5)}}.rec-active{animation:pulseRec 1.5s infinite!important;border-color:transparent!important}.resize-handle{position:absolute;right:2px;bottom:2px;width:16px;height:16px;cursor:nwse-resize;touch-action:none!important;opacity:.4;transition:opacity .2s;z-index:10}.resize-handle:hover{opacity:1}.resize-handle::after{content:'';width:6px;height:6px;border-right:2px solid #fff;border-bottom:2px solid #fff}.panel.mini .resize-handle{display:none!important}.bm-stat{width:8px;height:8px;border-radius:50%;background:#888;margin-right:8px;flex-shrink:0;box-shadow:0 0 4px rgba(0,0,0,.5);transition:.3s}.bm-nwrap{display:flex;align-items:center;min-width:0;flex:1}.bm-acts{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:4px}.bm-topic{font-size:11px;color:#ddd;background:rgba(0,0,0,.35);padding:6px 10px;border-radius:6px;margin:6px 0;word-break:break-word;white-space:pre-wrap;display:none;border:1px solid rgba(255,255,255,.08)}.bm-prev{width:100%;max-height:180px;object-fit:cover;border-radius:8px;margin-top:8px;display:none;border:1px solid rgba(255,255,255,.1);cursor:pointer}.bm-tbtn{display:flex;align-items:center;font-size:11px;font-weight:bold;padding:4px 10px;border-radius:12px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.05);color:#fff;cursor:pointer;transition:.2s;margin-right:4px;flex-shrink:0;box-sizing:border-box}.bm-tbtn.on{background:var(--th)}.bm-hbtn{font-size:11px;cursor:pointer;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.15);color:#fff;border-radius:6px;display:flex;align-items:center;padding:3px 7px}.bm-sort-wrap{display:flex;align-items:center;gap:4px;margin-left:auto;margin-right:6px}.bm-tags-wrap{display:none;flex-wrap:wrap;gap:6px;padding:4px 12px 10px;background:rgba(0,0,0,.4);flex-shrink:0;border-bottom:1px solid rgba(255,255,255,.05)}.bm-modal button svg,.bm-title svg{margin-right:4px;vertical-align:-2px}.bm-twrap{display:flex;align-items:center;gap:2px;flex:1}.bm-srch-in{margin-bottom:12px;font-size:18px;padding:12px;text-align:center;font-weight:bold}.bm-srch-btn{margin-bottom:8px;font-size:15px;padding:12px;transition:all .3s ease;border-left:4px solid transparent}`;

    class Helpers {
        static getModelSecretData() {
            try {
                for (const el of document.querySelectorAll('script[type="application/json"]')) {
                    const t = el.textContent;
                    if (!/chatHost|chatHeaderOptions|chatLocalData/.test(t)) continue;
                    if (this._secretSrc === t) return this._secretData;
                    this._secretSrc = t;
                    this._secretData = null;
                    return (this._secretData = JSON.parse(t));
                }
            } catch {}
            return null;
        }
        static createElement(tag, className = '') {
            const el = document.createElement(tag);
            if (className) el.className = className;
            return el;
        }
        static createButton(iconHTML, label, className = '') {
            const b = this.createElement('button', `btn ${className}`.trim());
            b.innerHTML = `<span>${iconHTML}</span><sub>${label}</sub>`;
            return b;
        }
        static formatTime(s) {
            const p = n => String(n | 0).padStart(2, '0');
            return `${p(s / 60)}:${p(s % 60)}`;
        }
        static getTimestamp() {
            const d = new Date(), p = n => String(n).padStart(2, '0');
            return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}-${p(d.getMinutes())}-${p(d.getSeconds())}`;
        }
        static download(blob, filename) {
            const url = URL.createObjectURL(blob), a = document.createElement('a');
            a.style.display = 'none';
            a.href = url;
            a.download = String(filename).replace(/[\\/:*?"<>|]+/g, '_');
            document.body.appendChild(a);
            a.click();
            setTimeout(() => { a.remove(); URL.revokeObjectURL(url); }, 60000);
        }
        static bindLongPress(el, onClick, onHold, ms = 400) {
            let timer = 0, fired = false, active = false;
            const clear = () => { clearTimeout(timer); timer = 0; };
            el.oncontextmenu = e => e.preventDefault();
            el.onpointerdown = e => {
                if (e.button !== 0 && e.pointerType === 'mouse') return;
                active = true;
                fired = false;
                clear();
                timer = setTimeout(() => { fired = true; timer = 0; onHold(e); }, ms);
            };
            el.onpointerup = e => {
                if (!active) return;
                active = false;
                clear();
                const wasHold = fired;
                fired = false;
                if (!wasHold) onClick(e);
            };
            el.onpointercancel = el.onpointerleave = () => { active = false; fired = false; clear(); };
        }
        static trackDrag(e, { onMove, onEnd, minDist = 2 }) {
            const el = e.target, sX = e.clientX, sY = e.clientY;
            try { el.setPointerCapture(e.pointerId); } catch {}
            let moved = false;
            const move = ev => {
                const dx = ev.clientX - sX, dy = ev.clientY - sY;
                if (!moved && Math.hypot(dx, dy) > minDist) moved = true;
                if (moved) onMove(dx, dy, ev);
            };
            const end = ev => {
                try { el.releasePointerCapture(ev.pointerId); } catch {}
                window.removeEventListener('pointermove', move);
                window.removeEventListener('pointerup', end);
                window.removeEventListener('pointercancel', end);
                onEnd?.(moved, ev);
            };
            window.addEventListener('pointermove', move, { passive: false });
            window.addEventListener('pointerup', end);
            window.addEventListener('pointercancel', end);
        }
        static copy(text, ok, err) {
            navigator.clipboard.writeText(text).then(ok).catch(err);
        }
        static flashBtn(btn, ok) {
            const orig = btn.innerHTML;
            btn.innerHTML = ok ? icons.ok : '<span style="color:#e74c3c">ERR</span>';
            setTimeout(() => { if (btn.isConnected) btn.innerHTML = orig; }, 1000);
        }
    }

    const mk = (tag, className) => Helpers.createElement(tag, className);

    class VideoController {
        constructor(uiController) {
            this.ui = uiController;
            this.cachedVideo = null;
            this.lastVolume = 1;
            this.isTheater = false;
            this.theaterWrap = null;
            this.bindEvents();
            this._findTimer = setInterval(() => this.findVideo(), 2000);
        }

        bindEvents() {
            const actions = {
                KeyM: () => this.toggleMute(),
                KeyF: () => this.toggleFullscreen(),
                KeyP: () => this.cachedVideo && this.togglePiP(),
                KeyT: () => this.toggleTheater(),
                KeyS: () => this.takeScreenshot(),
                KeyH: () => this.ui.toggleHide(!AppState.isHidden)
            };
            window.addEventListener('keydown', e => {
                try { const ae = e.composedPath()[0]; if (ae?.matches?.('input, textarea') || ae?.isContentEditable) return; } catch {}
                if (e.ctrlKey || e.altKey || e.metaKey) return;
                const act = actions[e.code];
                if (act) { e.preventDefault(); act(); return; }
                if (e.shiftKey && this.cachedVideo && (e.code === 'ArrowUp' || e.code === 'ArrowDown')) {
                    e.preventDefault();
                    this.setVolume(this.cachedVideo.volume + (e.code === 'ArrowUp' ? 0.1 : -0.1));
                }
            });
        }

        findVideo() {
            if (this.cachedVideo?.isConnected) return;
            if (this.cachedVideo) {
                if (this.isTheater) this.toggleTheater();
                this.cachedVideo = this.theaterWrap = null;
            }
            let v = null, max = 0;
            for (const cur of document.querySelectorAll('video')) {
                const area = (cur.videoWidth * cur.videoHeight) || (cur.offsetWidth * cur.offsetHeight) || 0;
                if (area > max) { max = area; v = cur; }
            }
            v = v || currentSite.videoSelectors.map(s => document.querySelector(s)).find(el => el?.tagName === 'VIDEO');
            if (v !== this.cachedVideo) {
                this.cachedVideo = v;
                if (v) this.attachInteractions(v);
            }
        }

        attachInteractions(v) {
            if (v._bcBound) return;
            v._bcBound = true;

            this._vcEvts = this._vcEvts || (e => {
                const cv = this.cachedVideo;
                if (e?.type === 'volumechange' && AppState.gmute && cv && (!cv.muted || cv.volume > 0)) {
                    cv.muted = true; cv.volume = 0;
                }
                this.syncUI();
            });
            ['volumechange', 'pause', 'play'].forEach(ev => v.addEventListener(ev, this._vcEvts, { passive: true }));

            if (AppState.gmute) {
                v.muted = true;
                v.volume = 0;
            }

            v.style.setProperty('z-index', '9999', 'important');
            v.parentElement?.style.setProperty('z-index', '9999', 'important');
            const target = currentSite.containerSelectors.map(s => { try { return v.closest(s); } catch {} }).find(Boolean) || v.parentElement;

            if (target && !target._bcTouchBound) {
                target._bcTouchBound = true;
                let wheelTick = false;
                target.addEventListener('wheel', e => {
                    try { if (e.target.closest('.vjs-control-bar, .vjs-menu, [role="slider"], [role="menu"]')) return; } catch {}
                    if (!this.isTheater && !document.fullscreenElement && !document.webkitFullscreenElement && !e.shiftKey) return;
                    e.preventDefault();
                    if (!wheelTick) {
                        requestAnimationFrame(() => {
                            const cv = this.cachedVideo;
                            if (cv) this.setVolume(Math.max(0, Math.min(1, cv.volume + (e.deltaY < 0 ? 0.05 : -0.05))));
                            wheelTick = false;
                        });
                        wheelTick = true;
                    }
                }, { passive: false });

                let clicks = 0, clickTimer = null, sX = 0, sY = 0;
                target.addEventListener('pointerdown', e => { sX = e.clientX; sY = e.clientY; }, { passive: true, capture: true });
                target.addEventListener('click', e => {
                    try {
                        const ignore = e.target.closest('.vjs-control-bar,.vjs-control,.vjs-button,.vjs-menu,.vjs-menu-item,.vjs-menu-content,[role="menu"],[role="menuitem"],[role="button"],[role="slider"],button,a');
                        if (ignore) return;
                    } catch {}
                    if (Math.abs(e.clientX - sX) > 10 || Math.abs(e.clientY - sY) > 10) return;
                    clicks++;
                    if (clicks === 1) {
                        clickTimer = setTimeout(() => { clicks = 0; }, 250);
                    } else if (clicks === 2) {
                        e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                        clearTimeout(clickTimer); this.toggleFullscreen(); clicks = 0;
                    }
                }, true);
            }
            this.syncUI();
        }

        setVolume(val) {
            const v = this.cachedVideo;
            if (!v) return;
            val = clamp(val, 0, 1);
            if (val > 0 && AppState.gmute) setGMute(false);
            v.volume = val;
            if (val > 0) {
                v.muted = false;
                this.lastVolume = val;
            }
            this.syncUI();
        }

        toggleMute() {
            const v = this.cachedVideo;
            if (!v) return;
            const isMuted = v.muted || v.volume === 0;
            v.muted = !isMuted;
            if (isMuted) v.volume = this.lastVolume || 1;
            else this.lastVolume = v.volume || 1;
            this.syncUI();
        }

        async togglePiP() {
            const v = this.cachedVideo;
            if (!v) return;
            try {
                if (document.pictureInPictureElement) await document.exitPictureInPicture();
                else if (document.pictureInPictureEnabled && v.requestPictureInPicture) {
                    v.removeAttribute('disablePictureInPicture');
                    await v.requestPictureInPicture();
                } else this.ui.flash(this.ui.elements.bPip, 'ERR');
            } catch { this.ui.flash(this.ui.elements.bPip, 'ERR'); }
        }

        toggleFullscreen() {
            const t = currentSite.fullscreenSelectors.map(s => { try { return document.querySelector(s); } catch { return null; } }).find(Boolean) || this.cachedVideo;
            if (!t) return;
            const isFS = document.fullscreenElement || document.webkitFullscreenElement;
            const fn = isFS ? (document.exitFullscreen || document.webkitExitFullscreen) : (t.requestFullscreen || t.webkitRequestFullscreen);
            try { fn?.call(isFS ? document : t); } catch {}
        }

        toggleTheater() {
            this.isTheater = !this.isTheater;
            const v = this.cachedVideo;
            if (this.isTheater && !v) { this.isTheater = false; return; }
            document.body.classList.toggle('bc-theater-mode', this.isTheater);
            if (this.isTheater) {
                this.theaterWrap = currentSite.containerSelectors.map(s => { try { return v.closest(s); } catch {} }).find(Boolean) || v.parentElement;
                this.theaterWrap?.classList.add('bc-thtr-wrap');
                v.classList.add('bc-thtr-video');
                window.scrollTo(0, 0);
            } else {
                this.theaterWrap?.classList.remove('bc-thtr-wrap');
                v?.classList.remove('bc-thtr-video');
                this.theaterWrap = null;
            }
            window.dispatchEvent(new Event('resize'));
        }

        takeScreenshot() {
            const v = this.cachedVideo;
            if (!v) return;
            try {
                const c = document.createElement('canvas');
                c.width = v.videoWidth || 1280; c.height = v.videoHeight || 720;
                c.getContext('2d').drawImage(v, 0, 0);
                c.toBlob(b => {
                    if (!b) return this.ui.flash(this.ui.elements.bShot, 'ERR');
                    Helpers.download(b, `${currentSite.getName() || 'Cam'}_${Helpers.getTimestamp()}.jpg`);
                    this.ui.flash(this.ui.elements.bShot, 'OK');
                }, 'image/jpeg', 0.95);
            } catch { this.ui.flash(this.ui.elements.bShot, 'ERR'); }
        }

        syncUI() {
            const v = this.cachedVideo;
            if (!v || !this.ui.elements.bMute) return;
            const vol = v.muted ? 0 : v.volume;
            const muted = v.muted || vol === 0;
            const gMute = AppState.gmute;
            if (this.ui._lastVol !== vol || this.ui._lastMuted !== muted || this.ui._lastGMute !== gMute) {
                const subEl = this.ui.elements.bMute.querySelector('sub');
                if (subEl) subEl.textContent = gMute ? 'LOCK' : L().sound;
                this.ui.elements.bMute.firstChild.innerHTML = muted ? icons.mute : icons.vol;
                this.ui.elements.bMute.classList.toggle('on', muted);
                this.ui.elements.bMute.style.boxShadow = gMute ? 'inset 0 0 0 2px #e74c3c' : '';

                this.ui.elements.vSlider.value = String(vol);
                this.ui.elements.vSlider.style.setProperty('--p', `${Math.round(vol * 100)}%`);
                this.ui.elements.vIco.innerHTML = muted ? icons.mute : (vol < 0.4 ? icons.volLow : vol < 0.75 ? icons.volMed : icons.vol);
                this.ui.elements.vVal.textContent = `${Math.round(vol * 100)}%`;

                this.ui._lastVol = vol;
                this.ui._lastMuted = muted;
                this.ui._lastGMute = gMute;
            }
        }
    }

    const fixWebmDuration = async (blob, durationMs) => {
        const HEAD = 8192;
        const head = new Uint8Array(await blob.slice(0, HEAD).arrayBuffer());
        const rd = (p, raw) => {
            const f = head[p];
            if (!f) return null;
            const len = Math.clz32(f) - 23;
            if (len > 8 || p + len > head.length) return null;
            const mask = 0xFF >> len;
            let v = raw ? f : (f & mask), ones = (f & mask) === mask;
            for (let i = 1; i < len; i++) { v = v * 256 + head[p + i]; if (head[p + i] !== 0xFF) ones = false; }
            return { v, len, unknown: ones };
        };
        const ebml = rd(0, true);
        if (!ebml || ebml.v !== 0x1A45DFA3) return blob;
        const es = rd(ebml.len, false);
        if (!es) return blob;
        let p = ebml.len + es.len + es.v;
        const seg = rd(p, true);
        if (!seg || seg.v !== 0x18538067) return blob;
        const ss = rd(p + seg.len, false);
        if (!ss || !ss.unknown) return blob;
        p += seg.len + ss.len;

        while (p < head.length) {
            const id = rd(p, true);
            if (!id) break;
            const sz = rd(p + id.len, false);
            if (!sz || sz.unknown) break;
            const ds = p + id.len + sz.len, de = ds + sz.v;
            if (id.v === 0x1549A966) {
                if (de > head.length) return blob;
                let q = ds, scale = 1e6;
                while (q < de) {
                    const cid = rd(q, true); if (!cid) return blob;
                    const csz = rd(q + cid.len, false); if (!csz) return blob;
                    const cs = q + cid.len + csz.len;
                    if (cid.v === 0x4489) return blob;
                    if (cid.v === 0x2AD7B1) { scale = 0; for (let i = 0; i < csz.v; i++) scale = scale * 256 + head[cs + i]; }
                    q = cs + csz.v;
                }
                const dur = new Uint8Array(11);
                dur.set([0x44, 0x89, 0x88]);
                new DataView(dur.buffer).setFloat64(3, durationMs * 1e6 / scale);
                const size8 = new Uint8Array(8);
                size8[0] = 0x01;
                new DataView(size8.buffer).setUint32(4, (de - ds) + dur.length);
                return new Blob([
                    head.subarray(0, p), head.subarray(p, p + id.len), size8,
                    head.subarray(ds, de), dur, head.subarray(de),
                    blob.slice(HEAD)
                ], { type: blob.type });
            }
            p = de;
        }
        return blob;
    };

    class MediaRecorderManager {
        constructor(videoController, uiController) {
            this.vc = videoController;
            this.ui = uiController;

            this.mRec = null;
            this.chunks = [];
            this.interval = null;
            this.seconds = 0;
            this.totalSize = 0;
            this.partNum = 1;
            this.partStart = 0;
            this.recStartTime = '';
            this.splitRequested = false;
            this.wakeLock = null;
            this.currentStream = null;
            this._onTrackEnded = () => this.stopActive();

            window.addEventListener('beforeunload', e => {
                if (this.mRec && this.mRec.state === 'recording') {
                    e.preventDefault();
                    e.returnValue = '';
                }
            });
        }

        getStream(v) {
            return v.captureStream?.(30) ?? v.mozCaptureStream?.(30) ?? null;
        }

        stopActive() {
            if (this.mRec && this.mRec.state !== 'inactive') {
                this.splitRequested = false;
                try { this.mRec.stop(); } catch {}
            }
        }

        cleanupSession(btn, sub) {
            clearInterval(this.interval);
            btn.classList.remove('on', 'rec-active');
            if (sub) sub.textContent = L().rec;
            if (this.wakeLock) { this.wakeLock.release().catch(() => {}); this.wakeLock = null; }
            try { this.currentStream?.getTracks().forEach(t => t.stop()); } catch {}
        }

        async saveBlob(blob, ext, durationMs, name) {
            try { if (ext === 'webm') blob = await fixWebmDuration(blob, durationMs); } catch {}
            Helpers.download(blob, name);
        }

        toggle() {
            const v = this.vc.cachedVideo;
            const btn = this.ui.elements.bRec;
            const sub = btn.querySelector('sub');
            if (!v) return this.ui.flash(btn, `! ${L().no_vid}`);

            if (this.mRec && this.mRec.state !== 'inactive') {
                this.splitRequested = false;
                try { this.mRec.stop(); } catch { this.currentStream?.getTracks().forEach(t => t.stop()); }
                this.cleanupSession(btn, sub);
                return;
            }

            try {
                const s = this.getStream(v);
                if (!s) throw new Error();
                this.currentStream = s;
                if (!s.getAudioTracks().length) this.ui.flash(btn, '! AUD');

                const mt = ['video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/webm', 'video/mp4;codecs=avc1,mp4a.40.2', 'video/mp4'].find(t => MediaRecorder.isTypeSupported(t)) || 'video/webm';
                const ext = mt.includes('mp4') ? 'mp4' : 'webm';

                this.recStartTime = Helpers.getTimestamp();
                this.partNum = 1;
                this.seconds = 0;
                this.startNewRecord(s, mt, ext, btn, sub);
                btn.classList.add('on', 'rec-active');
                if (sub) sub.textContent = `${Helpers.formatTime(0)} | 0.0M`;
                clearInterval(this.interval);
                this.interval = setInterval(() => {
                    this.seconds++;
                    if (sub) sub.textContent = `${Helpers.formatTime(this.seconds)} | ${(this.totalSize / 1048576).toFixed(1)}M`;
                }, 1000);
            } catch {
                this.ui.flash(btn, `! ${L().err}`);
            }
        }

        startNewRecord(stream, mimeType, ext, btn, sub) {
            this.chunks = [];
            this.totalSize = 0;
            try { this.mRec = new MediaRecorder(stream, { mimeType, videoBitsPerSecond: 6000000, audioBitsPerSecond: 128000 }); } catch {
                try { this.mRec = new MediaRecorder(stream, { mimeType }); } catch {
                    try { this.mRec = new MediaRecorder(stream); } catch { this.mRec = null; }
                }
            }
            if (!this.mRec) throw new Error('MediaRecorder init failed');

            stream.getTracks().forEach(t => t.addEventListener('ended', this._onTrackEnded));

            this.mRec.ondataavailable = e => {
                if (e.data?.size > 0) {
                    this.chunks.push(e.data);
                    this.totalSize += e.data.size;
                    if (this.totalSize >= 150000000 && !this.splitRequested) {
                        this.splitRequested = true;
                        try { this.mRec.stop(); } catch {}
                    }
                }
            };
            this.mRec.onerror = () => this.stopActive();
            this.mRec.onstop = () => {
                const chunks = this.chunks;
                this.chunks = [];
                const dur = performance.now() - this.partStart;
                if (chunks.length > 0) {
                    const suffix = (this.partNum > 1 || this.splitRequested) ? `_PT${this.partNum}` : '';
                    const name = `${currentSite.getName() || 'REC'}_${this.recStartTime}${suffix}.${ext}`;
                    this.saveBlob(new Blob(chunks, { type: mimeType.split(';')[0] }), ext, dur, name);
                }
                if (this.splitRequested) {
                    this.partNum++;
                    this.splitRequested = false;
                    const v = this.vc.cachedVideo;
                    const nextStream = (!stream.active && v ? this.getStream(v) : null) || stream;
                    this.currentStream = nextStream;
                    try {
                        this.startNewRecord(nextStream, mimeType, ext, btn, sub);
                        return;
                    } catch {}
                }
                this.cleanupSession(btn, sub);
            };

            try {
                this.partStart = performance.now();
                this.mRec.start(1000);
                if ('wakeLock' in navigator && !this.wakeLock) {
                    navigator.wakeLock.request('screen').then(wl => this.wakeLock = wl).catch(() => {});
                }
            } catch {
                this.ui.flash(btn, `! ${L().err}`);
            }
        }
    }

    const parseStatus = (h, en) => {
        if (!h) return [0, ''];
        if (/"is(?:Full)?PrivateChat"\s*:\s*true/i.test(h)) return [3, en ? ' (Private)' : ' (Приват)'];
        if (/"isGroupChat"\s*:\s*true/i.test(h)) return [3, en ? ' (Group)' : ' (Группа)'];
        if (/"isVipShow"\s*:\s*true/i.test(h)) return [3, ' (VIP)'];
        if (/"isAway"\s*:\s*true/i.test(h)) return [2, en ? ' (Away)' : ' (Отошла)'];
        if (/"showType"\s*:\s*"(?:private|full_?private|voyeur)"/i.test(h)) return [3, en ? ' (Private)' : ' (Приват)'];
        if (/"showType"\s*:\s*"group"/i.test(h)) return [3, en ? ' (Group)' : ' (Группа)'];
        if (/"showType"\s*:\s*"vip"/i.test(h)) return [3, ' (VIP)'];
        if (/__page_profile|"isOnline"\s*:\s*false|"online"\s*:\s*false|"isOffline"\s*:\s*true|"room_status"\s*:\s*["']offline["']|"isChatShowPaused"\s*:\s*true/i.test(h)) return [0, ''];
        if (/"isOnline"\s*:\s*true|"online"\s*:\s*true|"showType"\s*:\s*"public"|"isOffline"\s*:\s*false|"room_status"\s*:\s*["'](?:public|free|chat|group|private)["']/i.test(h)) return [1, ''];
        return [0, ''];
    };

    const decodeJson = s => { try { return JSON.parse(`"${s}"`); } catch { return s; } };

    const requestHtml = url => new Promise(resolve => {
        if (typeof GM_xmlhttpRequest === 'function') {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                timeout: 6000,
                onload: res => resolve(res.responseText || ''),
                onerror: () => resolve(''),
                ontimeout: () => resolve('')
            });
        } else {
            const ctrl = new AbortController();
            const to = setTimeout(() => ctrl.abort(), 6000);
            fetch(url, { cache: 'no-store', signal: ctrl.signal })
                .then(r => r.text())
                .then(resolve)
                .catch(() => resolve(''))
                .finally(() => clearTimeout(to));
        }
    });

    class BookmarkManager {
        constructor(uiController) {
            this.ui = uiController;
            this.overlay = null;
            this.list = null;
            this.build();
            this._statusTimer = setInterval(() => {
                if (this.overlay?.classList.contains('open')) this.checkStatuses();
            }, 60000);
        }

        applyRemote(json) {
            try {
                const arr = JSON.parse(json);
                if (!Array.isArray(arr)) return;
                AppState.bookmarks = arr;
                const editing = this.ui.shadow.activeElement?.matches?.('textarea');
                if (this.overlay?.classList.contains('open') && !editing) this.render();
            } catch {}
        }

        async checkStatuses() {
            if (!AppState.bookmarks?.length) return;
            this._abortCtrl?.abort();
            this._abortCtrl = new AbortController();
            const { signal } = this._abortCtrl;
            const sortType = Store.get('bc_bm_sort_type', 'date');
            const now = Date.now();
            const queue = AppState.bookmarks.filter(b => isSafeUrl(b.url) && (!b._chk || now - b._chk > 55000));
            let changedAny = false;

            const worker = async () => {
                while (queue.length > 0) {
                    if (signal.aborted) return;
                    const b = queue.shift();
                    try {
                        const h = await requestHtml(b.url);
                        if (signal.aborted || !h) continue;
                        b._chk = Date.now();

                        const [status, label] = parseStatus(h, AppState.lang === 'EN');
                        const tM = h.match(/"currentTopic"\s*:\s*"((?:[^"\\]|\\.)*)"/i);
                        const topic = (tM?.[1] && status !== 0) ? decodeJson(tM[1]).trim() : '';
                        const fM = h.match(/"frame"\s*:\s*"((?:[^"\\]|\\.)*)"/i);
                        let frame = fM?.[1] ? decodeJson(fM[1]).replace(/^\/\//, 'https://') : '';
                        if (!frame.startsWith('https://')) frame = '';

                        const changed = b.status !== status || b.label !== label || b.topic !== topic || (frame && b.frame !== frame);
                        b.status = status;
                        b.label = label;
                        b.topic = topic;
                        if (frame) b.frame = frame;

                        if (changed) {
                            changedAny = true;
                            if (this.overlay?.classList.contains('open')) {
                                if (sortType === 'online' || this.hidePrivate || this.hideOffline) {
                                    clearTimeout(this._resortTimer);
                                    this._resortTimer = setTimeout(() => this.render(), 200);
                                } else {
                                    b._updateItemDOM?.();
                                }
                            }
                        }
                    } catch {}
                }
            };
            await Promise.all(Array.from({ length: Math.min(6, queue.length) }, worker));
            if (changedAny) saveBookmarks();
        }

        build() {
            this.overlay = mk('div', 'bm-overlay');
            this.overlay.onclick = e => { if (e.target === this.overlay) this.overlay.classList.remove('open'); };

            const modal = mk('div', 'bm-modal');
            const hdr = mk('div', 'bm-hdr');
            this.title = mk('span', 'bm-title');

            const closeBtn = mk('button', 'bm-close');
            closeBtn.innerHTML = '×';
            closeBtn.onclick = () => this.overlay.classList.remove('open');

            const btnTogglePrev = mk('button', 'bm-hbtn');
            btnTogglePrev.style.marginLeft = 'auto';
            const btnSpeed = mk('button', 'bm-hbtn');
            btnSpeed.style.fontWeight = 'bold';
            this.prevEn = Store.get('bc_previews', false);
            this.updatePrevUI = () => {
                btnTogglePrev.innerHTML = icons.cam + (this.prevEn ? ' On' : ' Off');
                btnSpeed.style.display = this.prevEn ? 'flex' : 'none';
                btnSpeed.textContent = Store.get('bc_prev_spd', 1000) === 500 ? '0.5s' : '1.0s';
            };
            this.updatePrevUI();
            btnTogglePrev.onclick = () => {
                this.prevEn = !this.prevEn;
                Store.set('bc_previews', this.prevEn);
                this.updatePrevUI();
                this.render();
            };
            btnSpeed.onclick = () => {
                Store.set('bc_prev_spd', Store.get('bc_prev_spd', 1000) === 500 ? 1000 : 500);
                this.updatePrevUI();
                clearInterval(this._previewTimer);
                this._previewTimer = null;
                this.render();
            };
            const sortWrap = mk('div', 'bm-sort-wrap');
            this.btnSortType = mk('button', 'bm-hbtn');
            this.btnSortDir = mk('button', 'bm-hbtn');

            this.updateSortUI = () => {
                const type = Store.get('bc_bm_sort_type', 'date');
                const isRev = Store.get('bc_bm_sort_rev', false);
                const ru = AppState.lang === 'RU';
                const map = { date: [icons.clock, ru ? 'Дата' : 'Date'], name: [icons.name, ru ? 'Имя' : 'Name'], online: [icons.dot, ru ? 'Лайв' : 'Live'] };
                const [icon, label] = map[type] || map.date;
                this.btnSortType.innerHTML = icon + label;
                this.btnSortDir.innerHTML = isRev ? icons.arrowUp : icons.arrowDown;
            };

            this.btnSortType.onclick = () => {
                const types = ['date', 'name', 'online'];
                const cur = Store.get('bc_bm_sort_type', 'date');
                Store.set('bc_bm_sort_type', types[(types.indexOf(cur) + 1) % types.length]);
                this.updateSortUI();
                this.render();
            };

            this.btnSortDir.onclick = () => {
                Store.set('bc_bm_sort_rev', !Store.get('bc_bm_sort_rev', false));
                this.updateSortUI();
                this.render();
            };

            this.updateSortUI();
            sortWrap.append(this.btnSortType, this.btnSortDir);
            hdr.append(this.title, sortWrap, btnTogglePrev, btnSpeed, closeBtn);

            this.tagsWrap = mk('div', 'bm-tags-wrap');
            this.list = mk('div', 'bm-list');
            this._observer = new IntersectionObserver(entries => entries.forEach(e => e.target._inView = e.isIntersecting), { root: this.list, threshold: 0.05 });

            const foot = mk('div', 'bm-foot');
            this.btnAdd = mk('button', 'bm-add');
            this.btnAdd.onclick = () => this.add();
            this.btnExp = mk('button', 'bm-btn');
            this.btnExp.onclick = () => this.export();
            this.btnImp = mk('button', 'bm-btn');
            this.btnImp.onclick = () => this.import();

            foot.append(this.btnAdd, this.btnExp, this.btnImp);
            modal.append(hdr, this.tagsWrap, this.list, foot);
            this.overlay.appendChild(modal);
            this.ui.shadow.appendChild(this.overlay);
        }

        updateLabels() {
            this.title.textContent = L().bm_title;
            this.btnExp.textContent = L().bm_exp;
            this.btnImp.textContent = L().bm_imp;
            this.updateSortUI?.();
        }

        open() {
            this.updateLabels();
            this.render();
            this.checkStatuses();
            this.overlay.classList.add('open');
        }

        renderTags() {
            if (!this.tagsWrap) return;
            const ru = AppState.lang === 'RU';
            const makeBtn = (html, active, onClick, title = '') => {
                const btn = mk('button', `bm-tbtn${active ? ' on' : ''}`);
                btn.innerHTML = html;
                if (title) btn.title = title;
                btn.onclick = onClick;
                return btn;
            };

            this.tagsWrap.innerHTML = '';
            this.tagsWrap.style.display = 'flex';
            if (this.isGalleryMode === undefined) this.isGalleryMode = Store.get('bc_bm_gal', false);
            if (this.hidePrivate === undefined) this.hidePrivate = Store.get('bc_bm_hide_priv', false);
            if (this.hideOffline === undefined) this.hideOffline = Store.get('bc_bm_hide_off', false);

            this.tagsWrap.append(
                makeBtn(ru ? 'Онлайн' : 'Online', this.hideOffline, () => {
                    this.hideOffline = !this.hideOffline;
                    Store.set('bc_bm_hide_off', this.hideOffline);
                    this.render();
                }, ru ? 'Показывать только Онлайн' : 'Show only Online'),
                makeBtn(ru ? 'Приват' : 'Priv', this.hidePrivate, () => {
                    this.hidePrivate = !this.hidePrivate;
                    Store.set('bc_bm_hide_priv', this.hidePrivate);
                    this.render();
                }, ru ? 'Скрыть Приват/Группу/VIP' : 'Hide Private/Group/VIP'),
                makeBtn(this.isGalleryMode ? (icons.list + (ru ? 'Список' : 'List')) : (icons.grid + (ru ? 'Галерея' : 'Gallery')), this.isGalleryMode, () => {
                    this.isGalleryMode = !this.isGalleryMode;
                    Store.set('bc_bm_gal', this.isGalleryMode);
                    if (this.isGalleryMode) { this.prevEn = true; Store.set('bc_previews', true); this.updatePrevUI(); }
                    this.render();
                })
            );

            const allTags = new Set();
            AppState.bookmarks.forEach(b => (b.note?.match(/#[\wа-яА-ЯёЁ_]+/gi) || []).forEach(t => allTags.add(t.toLowerCase())));
            if (allTags.size) {
                this.tagsWrap.append(makeBtn(ru ? 'Все' : 'All', !this.currentTagFilter, () => { this.currentTagFilter = null; this.render(); }));
                [...allTags].sort().forEach(tag => {
                    this.tagsWrap.append(makeBtn(tag, this.currentTagFilter === tag, () => {
                        this.currentTagFilter = this.currentTagFilter === tag ? null : tag;
                        this.render();
                    }));
                });
            } else this.currentTagFilter = null;
        }

        renderItem(b, frag) {
            const item = mk('div', 'bm-item');
            const top = mk('div', 'bm-item-top');
            const link = mk('a', 'bm-link');
            link.href = isSafeUrl(b.url) ? b.url : '#';

            const img = mk('img', 'bm-avatar');
            const fbAva = `https://ui-avatars.com/api/?name=${encodeURIComponent(b.name)}&background=random`;
            img.src = (typeof b.avatar === 'string' && b.avatar.startsWith('https://')) ? b.avatar : fbAva;
            img.onerror = function () { this.onerror = null; this.src = fbAva; };

            const stat = mk('span', 'bm-stat');
            const nameWrap = mk('div', 'bm-nwrap');
            const nameEl = mk('span', 'bm-name');
            nameEl.textContent = b.name;
            nameWrap.append(stat, nameEl);
            link.append(img, nameWrap);

            const delBtn = mk('button', 'bm-del');
            delBtn.innerHTML = icons.del;
            delBtn.title = 'Удалить';
            delBtn.onclick = e => {
                e.preventDefault(); e.stopPropagation();
                AppState.bookmarks = AppState.bookmarks.filter(x => x !== b);
                saveBookmarks();
                this.render();
            };
            top.append(link, delBtn);

            const makeAct = (html, fn, onHold) => {
                const btn = mk('button', 'bm-act-btn');
                btn.innerHTML = html;
                if (onHold) Helpers.bindLongPress(btn, () => fn(btn), onHold);
                else btn.onclick = e => { e.preventDefault(); e.stopPropagation(); fn(btn); };
                return btn;
            };
            const getHls = () => (b.name === currentSite.getName() && urlSite(b.url) === siteKey(location.hostname)) ? getStreamUrl() : null;
            const actionsRow = mk('div', 'bm-actions-row bm-acts');
            actionsRow.append(
                makeAct(`${icons.search} <span>${L().search}</span>`, () => { this.overlay.classList.remove('open'); this.ui.searchManager?.open(b.name, true); }),
                makeAct(`${icons.hls} <span>HLS</span>`,
                    btn => {
                        const u = getHls();
                        if (!u) return Helpers.flashBtn(btn, false);
                        Helpers.copy(u, () => Helpers.flashBtn(btn, true), () => Helpers.flashBtn(btn, false));
                    },
                    () => {
                        const u = getHls();
                        if (u && navigator.share) navigator.share({ title: `${b.name} Stream`, url: u }).catch(() => {});
                        else Helpers.flashBtn(actionsRow.children[1], false);
                    }
                ),
                makeAct(`${icons.copy} <span>${L().copy}</span>`, btn => {
                    Helpers.copy(b.url, () => Helpers.flashBtn(btn, true), () => Helpers.flashBtn(btn, false));
                })
            );

            const noteInput = mk('textarea', 'bm-note');
            noteInput.rows = '1';
            noteInput.style.cssText = 'resize:none;overflow:hidden;min-height:36px;white-space:pre-wrap;word-break:break-word;';
            noteInput.placeholder = L().note_ph;
            noteInput.value = b.note || '';
            const autoResize = () => { noteInput.style.height = 'auto'; noteInput.style.height = `${Math.max(36, noteInput.scrollHeight)}px`; };
            requestAnimationFrame(autoResize);

            noteInput.oninput = e => {
                requestAnimationFrame(autoResize);
                b.note = e.target.value.trim();
                saveBookmarks();
            };
            noteInput.onchange = () => { this.ui.flash(this.btnAdd, 'OK'); this.render(); };
            noteInput.onkeydown = e => e.stopPropagation();

            const topicEl = mk('div', 'bm-topic');

            const updateTopicDOM = txt => {
                if (!txt) { topicEl.style.display = 'none'; return; }
                topicEl.style.display = 'block';
                if (txt.length > 55) {
                    const txtMore = AppState.lang === 'EN' ? '[More]' : '[Еще]';
                    const txtLess = AppState.lang === 'EN' ? '[Less]' : '[Свернуть]';
                    topicEl.innerHTML = `<span>${escHTML(b._topicExp ? txt : txt.slice(0, 55) + '...')} </span><a class="tp-btn" style="color:var(--th);cursor:pointer;font-weight:bold;text-decoration:underline;">${escHTML(b._topicExp ? txtLess : txtMore)}</a>`;
                    topicEl.querySelector('.tp-btn')?.addEventListener('click', ev => {
                        ev.preventDefault();
                        b._topicExp = !b._topicExp;
                        updateTopicDOM(txt);
                    });
                } else topicEl.textContent = txt;
            };

            const previewImg = mk('img', 'bm-prev');
            previewImg.onerror = () => { previewImg.style.display = 'none'; };

            item.onclick = e => { if (this.isGalleryMode && isSafeUrl(b.url) && !e.target.closest('button, textarea, a.tp-btn')) location.href = b.url; };
            item.append(top, actionsRow, topicEl, noteInput, previewImg);
            frag.append(item);

            const applyStatus = () => {
                const colors = { 1: '#2ecc71', 2: '#f39c12', 3: '#9b59b6', 0: '#e74c3c' };
                const st = b.status ?? 0;
                const col = colors[st] || '#888';
                stat.style.background = col;
                stat.style.boxShadow = st ? `0 0 8px ${col}` : '0 0 4px rgba(0,0,0,0.5)';
                nameEl.textContent = b.name + (b.label || '');
                updateTopicDOM(b.topic || '');

                if (this.prevEn && b.frame && st !== 0) {
                    previewImg.setAttribute('data-frame', b.frame);
                    previewImg.style.display = 'block';
                    previewImg._bRef = b;
                    if (!b._lastImg) b._lastImg = `${b.frame.split('?')[0]}?t=${Date.now()}`;
                    if (previewImg.src !== b._lastImg) previewImg.src = b._lastImg;
                } else {
                    previewImg.removeAttribute('data-frame');
                    previewImg.style.display = 'none';
                }
            };
            applyStatus();
            b._updateItemDOM = applyStatus;
            this._observer?.observe(item);
        }

        render() {
            AppState.bookmarks.forEach(b => { if (b._updateItemDOM) b._updateItemDOM = null; });
            this.list.replaceChildren();
            const currentName = currentSite.getName();
            const isAlreadyAdded = isFavName(currentName);
            this.btnAdd.disabled = !currentName || isAlreadyAdded;
            this.btnAdd.textContent = !currentName ? L().no_model : (isAlreadyAdded ? L().bm_dup : `${L().bm_add} (${currentName})`);

            if (!AppState.bookmarks.length) {
                clearInterval(this._previewTimer);
                this._previewTimer = null;
                this.list.innerHTML = `<div class="bm-empty">${L().bm_empty}</div>`;
                return;
            }

            this.renderTags();
            this.list.classList.toggle('bm-gallery-mode', !!this.isGalleryMode);

            let sortedList = AppState.bookmarks.map((b, i) => ({ b, i }));
            const sortType = Store.get('bc_bm_sort_type', 'date');
            const isRev = Store.get('bc_bm_sort_rev', false);

            sortedList.sort((x, y) => {
                if (sortType === 'name') return (isRev ? -1 : 1) * x.b.name.localeCompare(y.b.name);
                if (sortType === 'online') {
                    const diff = (((y.b.status ?? 0) > 0 ? 1 : 0) - ((x.b.status ?? 0) > 0 ? 1 : 0)) || (x.i - y.i);
                    return isRev ? -diff : diff;
                }
                return isRev ? (y.i - x.i) : (x.i - y.i);
            });

            if (this.hidePrivate) sortedList = sortedList.filter(x => (x.b.status ?? 0) !== 3);
            if (this.hideOffline) sortedList = sortedList.filter(x => (x.b.status ?? 0) !== 0);
            if (this.currentTagFilter) sortedList = sortedList.filter(x => x.b.note?.toLowerCase().includes(this.currentTagFilter));

            if (!sortedList.length) {
                this.list.innerHTML = `<div class="bm-empty">${AppState.lang === 'RU' ? 'Нет моделей по выбранному фильтру' : 'No models match filter'}</div>`;
                return;
            }

            const frag = document.createDocumentFragment();
            sortedList.forEach(({ b }) => this.renderItem(b, frag));
            this.list.append(frag);

            if (!this._previewTimer && this.prevEn) {
                const spd = Store.get('bc_prev_spd', 1000);
                this._previewTimer = setInterval(() => {
                    if (!this.overlay.classList.contains('open') || !this.prevEn) {
                        clearInterval(this._previewTimer);
                        this._previewTimer = null;
                        return;
                    }
                    this.list.querySelectorAll('.bm-item').forEach(item => {
                        if (!item._inView) return;
                        const imgEl = item.querySelector('img[data-frame]');
                        if (!imgEl || imgEl._isUpdating) return;
                        const url = imgEl.getAttribute('data-frame');
                        if (!url) return;
                        imgEl._isUpdating = true;
                        const newUrl = `${url.split('?')[0]}?t=${Date.now()}`;
                        const tmp = new Image();
                        tmp.onload = () => {
                            if (imgEl.isConnected) { imgEl.src = newUrl; imgEl.style.display = 'block'; }
                            if (imgEl._bRef) imgEl._bRef._lastImg = newUrl;
                            imgEl._isUpdating = false;
                        };
                        tmp.onerror = () => { imgEl._isUpdating = false; };
                        tmp.src = newUrl;
                    });
                }, spd);
            }
        }

        add() {
            const name = currentSite.getName();
            if (!name) return;
            if (isFavName(name)) return this.ui.flash(this.btnAdd, L().bm_dup);

            const bUrl = location.href.split(/[?#]/)[0];
            const sd = Helpers.getModelSecretData();
            let avatar = ((sd?.chatHost?.username || '').toLowerCase() === name.toLowerCase()) ? (sd?.chatHeaderOptions?.profileImage || '') : '';
            if (avatar.startsWith('//')) avatar = `https:${avatar}`;
            if (!avatar) avatar = document.querySelector('.chat-info-avatar img, .chat-header-avatar img, .profile-photo img')?.src || '';
            if (!avatar.startsWith('https://')) avatar = '';

            const bm = { name, url: bUrl, avatar, note: '' };
            AppState.bookmarks.push(bm);
            saveBookmarks();
            this.render();
            this.ui.flash(this.btnAdd, 'OK');
            this.checkStatuses();

            if (!avatar) {
                const fOpt = (typeof AbortSignal !== 'undefined' && AbortSignal.timeout) ? { signal: AbortSignal.timeout(15000) } : {};
                fetch(bUrl, fOpt).then(r => r.text()).then(html => {
                    const m = html.match(/(?:property=["']og:image["']|name=["']og:image["'])[^>]*?content=["']([^"']+)["']/i)
                        || html.match(/content=["']([^"']+)["'][^>]*?property=["']og:image["']/i)
                        || html.match(/"(?:profileImage|avatarUrl)"\s*:\s*"([^"]+)"/i);
                    if (m?.[1]) {
                        let a = m[1].replace(/\\/g, '');
                        if (a.startsWith('//')) a = `https:${a}`;
                        if (a.startsWith('https://')) {
                            bm.avatar = a;
                            saveBookmarks();
                            if (this.overlay?.classList.contains('open')) this.render();
                        }
                    }
                }).catch(() => {});
            }
        }

        export() {
            if (!AppState.bookmarks.length) return;
            const blob = new Blob([JSON.stringify(AppState.bookmarks.map(stripBm), null, 2)], { type: 'application/json' });
            Helpers.download(blob, `BC_Favs_${new Date().toISOString().slice(0, 10)}.json`);
        }

        import() {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.json';
            input.onchange = async e => {
                try {
                    const file = e.target.files?.[0];
                    if (!file || file.size > 5e6) return;
                    const arr = JSON.parse(await file.text());
                    if (!Array.isArray(arr)) return;
                    const seen = new Set(AppState.bookmarks.map(b => `${urlSite(b.url)}|${b.name}`));
                    for (const raw of arr) {
                        const b = cleanBm(raw);
                        if (!b) continue;
                        const k = `${urlSite(b.url)}|${b.name}`;
                        if (seen.has(k)) continue;
                        seen.add(k);
                        AppState.bookmarks.push(b);
                    }
                    saveBookmarks();
                    this.render();
                    this.ui.flash(this.btnAdd, 'OK');
                    this.checkStatuses();
                } catch {
                    this.ui.flash(this.btnImp, 'ERR');
                }
            };
            input.click();
        }
    }

    class SearchManager {
        constructor(uiController) {
            this.ui = uiController;

            this.overlay = null;
            this.input = null;
            this.btns = [];
            this._live = new Set();
            this._sched = [];
            this._gen = 0;
            this.build();
        }

        build() {
            this.overlay = mk('div', 'bm-overlay');
            this.overlay.onclick = e => { if (e.target === this.overlay) this.overlay.classList.remove('open'); };

            const modal = mk('div', 'bm-modal');
            const hdr = mk('div', 'bm-hdr');
            this.title = mk('span', 'bm-title');

            this.btnBack = mk('button', 'bm-close');
            this.btnBack.innerHTML = icons.back;
            this.btnBack.style.cssText = 'display:none;margin-left:auto;';
            this.btnBack.onclick = () => { this.overlay.classList.remove('open'); if (this.ui.bookmarkManager) this.ui.bookmarkManager.open(); };

            const titleWrap = mk('div', 'bm-twrap');
            titleWrap.append(this.title);

            const closeBtn = mk('button', 'bm-close');
            closeBtn.innerHTML = icons.close;
            closeBtn.onclick = () => this.overlay.classList.remove('open');

            hdr.append(titleWrap, this.btnBack, closeBtn);

            const list = mk('div', 'bm-list');
            this.input = mk('input', 'bm-note bm-srch-in');
            this.input.type = 'text';
            this.input.onkeydown = e => e.stopPropagation();

            const ENGINES = [
                ['PornoLab', 'https://pornolab.net/forum/tracker.php?nm=%s'],
                ['BestCam.tv', 'https://bestcam.tv/model/%s'],
                ['CamShowRecordings', 'https://www.camshowrecordings.com/?s=%s'],
                ['LiveCamsRip', 'https://www.livecamsrip.com/%s/profile'],
                ['CamDownload', 'https://camdownload.net/performer/%s'],
                ['CamShow.cc', 'https://camshow.cc/streamer/%s'],
                ['SimpCity', 'https://simpcity.cr/search/?q=%s&o=date', true],
                ['CamsClips', 'https://www.camsclips.com/search/%s/', true]
            ];
            list.append(this.input, ...ENGINES.map(([txt, tmpl, skipCheck]) => {
                const urlFn = v => tmpl.replace('%s', encodeURIComponent(v));
                const btn = mk('button', 'bm-btn bm-srch-btn');
                btn.textContent = txt;
                btn.onclick = () => {
                    const v = this.input.value.trim();
                    if (v) window.open(urlFn(v), '_blank');
                    else this.ui.flash(btn, 'ERR');
                };
                this.btns.push({ btn, urlFn, skipCheck: !!skipCheck });
                return btn;
            }));

            let debounceTimer;
            this.input.oninput = () => {
                clearTimeout(debounceTimer);
                debounceTimer = setTimeout(() => {
                    this._resetRequests();
                    const v = this.input.value.trim();
                    this.btns.forEach((item, index) => {
                        if (!v) {
                            item.btn.style.borderLeft = '4px solid transparent';
                            item.btn.style.opacity = '1';
                        } else if (item.skipCheck) {
                            item.btn.style.borderLeft = '4px solid #888';
                            item.btn.style.opacity = '1';
                        } else {
                            this._sched.push(setTimeout(() => this.checkPage(item.urlFn(v), item.btn), index * 150));
                        }
                    });
                }, 600);
            };

            modal.append(hdr, list);
            this.overlay.appendChild(modal);
            this.ui.shadow.appendChild(this.overlay);
        }

        _resetRequests() {
            this._gen++;
            if (this._sched) { this._sched.forEach(clearTimeout); this._sched = []; }
            this._live.forEach(r => { try { r.abort(); } catch {} });
            this._live.clear();
        }

        checkPage(url, btn) {
            if (!btn.isConnected) return;
            btn.style.borderLeft = '4px solid orange';
            btn.style.opacity = '0.8';
            const gen = this._gen;
            const stale = () => gen !== this._gen || !btn.isConnected;
            const markErr = () => { if (!stale()) { btn.style.borderLeft = '4px solid #ef4444'; btn.style.opacity = '0.5'; } };
            const req = GM_xmlhttpRequest({
                method: 'GET',
                url,
                timeout: 8000,
                onload: res => {
                    this._live.delete(req);
                    if (stale()) return;
                    const txt = (res.responseText || '').toLowerCase();
                    const title = txt.match(/<title[^>]*>(.*?)<\/title>/i)?.[1] || '';
                    const hasErr = res.status === 404 || res.status >= 500
                        || ['not found', '404', 'error'].some(t => title.includes(t))
                        || /no\s*(?:videos?|results?)\s*found|does\s*not\s*exist|\b0\s*results?\b/i.test(txt)
                        || (url.includes('camshowrecordings') && !res.responseText.includes('class="h1modelpage"'))
                        || (url.includes('pornolab.net') && (txt.includes('не найдено') || txt.includes('не найдена') || !txt.includes('tlink')));
                    if (hasErr) return markErr();
                    btn.style.borderLeft = '4px solid #10b981';
                    btn.style.opacity = '1';
                },
                onerror: () => { this._live.delete(req); markErr(); },
                ontimeout: () => { this._live.delete(req); markErr(); }
            });
            this._live.add(req);
        }

        open(customName, fromFavs = false) {
            if (this._resetRequests) this._resetRequests();
            if (this.btnBack) this.btnBack.style.display = fromFavs ? 'flex' : 'none';
            this.title.innerHTML = `${icons.search} ${L().search}`;
            const name = (typeof customName === 'string') ? customName : (currentSite.getName() || '');
            this.input.value = name;
            this.overlay.classList.add('open');
            if (name) {
                this.btns.forEach((item, index) => {
                    item.btn.style.opacity = '1';
                    if (item.skipCheck) {
                        item.btn.style.borderLeft = '4px solid #888';
                    } else {
                        item.btn.style.borderLeft = '4px solid transparent';
                        const t = setTimeout(() => this.checkPage(item.urlFn(name), item.btn), 200 + (index * 150));
                        this._sched.push(t);
                    }
                });
            }
        }
    }

    class UIController {
        constructor() {
            this.elements = {};
            this.hasMoved = false;

            this.host = document.createElement('div');
            this.host.id = 'bc-unchained-host';
            this.shadow = this.host.attachShadow({ mode: 'open' });

            const style = document.createElement('style');
            style.textContent = CSS_UI;
            this.shadow.appendChild(style);

            this.panel = mk('div', 'panel');
            this.panel.style.transform = `scale(${AppState.scale})`;
            this.shadow.appendChild(this.panel);

            this.ghostBtn = mk('div', 'ghost');
            this.ghostBtn.innerHTML = icons.menu;
            this.ghostBtn.onclick = () => this.toggleHide(false);
            this.shadow.appendChild(this.ghostBtn);

            this.restoreBtn = mk('div', 'restore');
            this.restoreBtn.innerHTML = icons.menu;
            this.restoreBtn.onclick = () => { if (!this.hasMoved) this.toggleMinimize(false); };
            this.restoreBtn.onpointerdown = e => this.onDragStart(e);
            this.panel.appendChild(this.restoreBtn);

            this.fullContainer = mk('div', 'col');
            this.panel.appendChild(this.fullContainer);

            this.buildHeader();
            this.buildMediaRow();
            this.buildVolume();
            this.buildControls();
            this.buildMisc();
            this.buildSettings();

            this.setupDrag();
            this.setupResize();
            this.applyPosition();
            this.setupFilterDrag();

            document.documentElement.appendChild(this.host);
        }

        setupFilterDrag() {
            let hasMoved = false, dragTarget = null, iL = 0, iT = 0;
            let cFx = Store.get('bc_fx', null), cFy = Store.get('bc_fy', null);
            const clampBar = (bar, x, y) => {
                const r = bar.getBoundingClientRect();
                return {
                    x: `${clamp(parseFloat(x) || 0, 0, window.innerWidth - (r.width || 50))}px`,
                    y: `${clamp(parseFloat(y) || 0, 0, window.innerHeight - (r.height || 50))}px`
                };
            };
            const applyPos = () => {
                const b = document.querySelector('.bc_sticky_bar');
                if (!b || dragTarget) return;
                if (cFx && cFy) {
                    const c = clampBar(b, cFx, cFy);
                    if (c.x !== cFx || c.y !== cFy) { cFx = c.x; cFy = c.y; Store.set('bc_fx', cFx); Store.set('bc_fy', cFy); }
                    b.style.cssText += `;left:${cFx}!important;top:${cFy}!important;bottom:auto!important;right:auto!important;transform:none!important;margin:0!important;width:max-content!important;`;
                } else {
                    b.style.cssText += ';left:50%!important;transform:translateX(-50%)!important;';
                }
            };
            setInterval(() => { if (!document.hidden) applyPos(); }, 2000);
            window.addEventListener('resize', applyPos, { passive: true });
            applyPos();

            window.addEventListener('pointerdown', e => {
                if (e.button !== 0 && e.pointerType === 'mouse') return;
                const handle = e.target.closest('.js-filter_button, .bc_sticky_bar'), bar = document.querySelector('.bc_sticky_bar');
                if (!handle || !bar) return;
                if (e.pointerType !== 'mouse') e.preventDefault();
                dragTarget = bar;
                const r = bar.getBoundingClientRect();
                iL = r.left; iT = r.top;
                bar.style.cssText += `;transform:none!important;margin:0!important;bottom:auto!important;right:auto!important;width:max-content!important;left:${iL}px!important;top:${iT}px!important;`;
                handle.style.setProperty('cursor', 'grabbing', 'important');

                Helpers.trackDrag(e, {
                    minDist: 4,
                    onMove: (dx, dy) => {
                        if (!dragTarget.isConnected) return;
                        dragTarget.style.setProperty('left', `${iL + dx}px`, 'important');
                        dragTarget.style.setProperty('top', `${iT + dy}px`, 'important');
                    },
                    onEnd: (moved) => {
                        hasMoved = moved;
                        if (moved && dragTarget?.isConnected) {
                            const c = clampBar(dragTarget, dragTarget.style.left || iL, dragTarget.style.top || iT);
                            dragTarget.style.setProperty('left', c.x, 'important');
                            dragTarget.style.setProperty('top', c.y, 'important');
                            cFx = c.x; cFy = c.y;
                            Store.set('bc_fx', cFx); Store.set('bc_fy', cFy);
                        }
                        handle.style.setProperty('cursor', 'grab', 'important');
                        dragTarget = null;
                    }
                });
            }, { passive: false, capture: true });

            window.addEventListener('click', e => {
                if (hasMoved && e.target.closest('.js-filter_button, .bc_sticky_bar')) {
                    e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
                }
            }, { capture: true });
        }

        buildHeader() {
            const hdr = mk('div', 'hdr');
            this.elements.dragZone = mk('div', 'drag');
            let dClicks = 0, dTimer = null;
            this.elements.dragZone.addEventListener('pointerdown', e => {
                dClicks++;
                if (dClicks === 1) dTimer = setTimeout(() => dClicks = 0, 500);
                else if (dClicks === 3) {
                    clearTimeout(dTimer); dClicks = 0;
                    AppState.posX = '15px'; AppState.posY = '10%'; AppState.hasPos = false;
                    Store.set('bc_pos', false); this.applyPosition();
                }
                this.onDragStart(e);
            });
            this.elements.stats = mk('div', 'stats');
            this.elements.stats.innerHTML = `${icons.eye}<span>0</span>`;
            const makeHBtn = (html, cls, click) => {
                const b = mk('button', `h-btn ${cls}`.trim());
                b.innerHTML = html;
                b.onclick = click;
                return b;
            };
            this.elements.bLang = makeHBtn('', 'lang', () => {
                AppState.lang = AppState.lang === 'RU' ? 'EN' : 'RU';
                Store.set('bc_lang', AppState.lang);
                this.updateLabels();
            });
            this.elements.bHome = makeHBtn(icons.home, '', () => location.href = '/');
            this.elements.bRel = makeHBtn(icons.rel, '', () => location.reload());
            this.elements.bMin = makeHBtn(icons.min, '', () => this.toggleMinimize(true));
            this.elements.bHide = makeHBtn(icons.close, 'close', () => this.toggleHide(true));
            const btns = mk('div', 'hdr-btns');
            btns.append(this.elements.stats, this.elements.bLang, this.elements.bHome, this.elements.bRel, this.elements.bMin, this.elements.bHide);
            hdr.append(this.elements.dragZone, btns);
            this.fullContainer.appendChild(hdr);
        }

        buildMediaRow() {
            const row = mk('div', 'grid-4');

            this.elements.bBg = Helpers.createButton(AppState.bgPlay ? icons.bgOn : icons.bgOff, '');
            if (AppState.bgPlay) this.elements.bBg.classList.add('on');
            this.elements.bBg.onclick = () => {
                AppState.bgPlay = !AppState.bgPlay;
                Store.set('bc_bg', AppState.bgPlay);
                this.elements.bBg.firstChild.innerHTML = AppState.bgPlay ? icons.bgOn : icons.bgOff;
                this.elements.bBg.querySelector('sub').textContent = L().bg;
                this.elements.bBg.classList.toggle('on', AppState.bgPlay);
                this.enforceBackgroundPlay();
            };

            this.elements.bMute = Helpers.createButton(icons.vol, '');
            this.elements.bShot = Helpers.createButton(icons.snap, '');
            this.elements.bRec = Helpers.createButton(icons.rec, '');

            row.append(this.elements.bBg, this.elements.bMute, this.elements.bShot, this.elements.bRec);
            this.fullContainer.appendChild(row);
        }

        buildVolume() {
            const row = mk('div', 'v-cap');

            this.elements.vIco = mk('span', 'vico');
            this.elements.vIco.innerHTML = icons.vol;

            this.elements.vSlider = mk('input', 'vslider');
            this.elements.vSlider.type = 'range';
            this.elements.vSlider.min = '0';
            this.elements.vSlider.max = '1';
            this.elements.vSlider.step = '0.02';
            this.elements.vSlider.value = '1';

            this.elements.vVal = mk('span', 'vval');
            this.elements.vVal.textContent = '100%';

            row.append(this.elements.vIco, this.elements.vSlider, this.elements.vVal);
            this.fullContainer.appendChild(row);
        }

        buildControls() {
            this.fullContainer.appendChild(mk('div', 'sep'));
            const row = mk('div', 'grid-4');

            this.elements.bQual = Helpers.createButton(icons.qual, '');
            this.elements.bQual.onclick = () => {
                const qBlock = document.querySelector('#quality_settings_block, .js-quality_settings_block');
                if (!qBlock) {
                    const q = document.querySelector('#quality-btn, .vjs-resolution-button, .vjs-cog-menu-button, .vjs-icon-cog, [class*="quality"], [class*="resolution"]');
                    return q ? q.click() : this.flash(this.elements.bQual, `! ${L().err}`);
                }
                if (!qBlock._bcCloseBound) {
                    qBlock._bcCloseBound = true;
                    qBlock.addEventListener('click', e => {
                        if (e.target.closest('.js-qsb_close, .qsb_close, .qsb_list_wrp, [class*="qsb"]')) {
                            setTimeout(() => { qBlock.classList.add('__hidden'); qBlock.style.display = 'none'; }, 50);
                        }
                    });
                }
                const isVis = qBlock.style.display === 'block' || (!qBlock.classList.contains('__hidden') && qBlock.offsetWidth > 0);
                qBlock.classList.toggle('__hidden', isVis);
                qBlock.style.display = isVis ? 'none' : 'block';
                if (!isVis) {
                    qBlock.style.zIndex = '2147483645';
                    try { (window.unsafeWindow?.$ || window.$ || window.jQuery)?.('#quality-btn').trigger('click'); } catch {}
                }
            };

            this.elements.bPip = Helpers.createButton(icons.pip, '');
            this.elements.bFs = Helpers.createButton(icons.fs, '');
            this.elements.bThtr = Helpers.createButton(icons.thtr, '');

            row.append(this.elements.bQual, this.elements.bPip, this.elements.bFs, this.elements.bThtr);
            this.fullContainer.appendChild(row);
        }

        buildMisc() {
            const row = mk('div', 'grid-4');
            this.elements.bSearch = Helpers.createButton(icons.search, '');
            this.elements.bHls = Helpers.createButton(icons.hls, 'HLS');
            Helpers.bindLongPress(this.elements.bHls,
                () => {
                    const u = getStreamUrl();
                    if (!u) return this.flash(this.elements.bHls, 'ERR');
                    Helpers.copy(u, () => this.flash(this.elements.bHls, 'OK'), () => this.flash(this.elements.bHls, 'ERR'));
                },
                () => {
                    const u = getStreamUrl();
                    if (u && navigator.share) navigator.share({ title: `${currentSite.getName()} Stream`, url: u }).catch(() => {});
                    else this.flash(this.elements.bHls, 'ERR');
                }
            );
            this.elements.bCopy = Helpers.createButton(icons.copy, '');
            this.elements.bCopy.onclick = () => {
                const name = currentSite.getName();
                if (!name) return this.flash(this.elements.bCopy, 'ERR');
                const link = BC_RE.test(location.hostname)
                    ? `${location.origin}/profile/${encodeURIComponent(name)}`
                    : `${location.origin}${location.pathname}`;
                Helpers.copy(link, () => this.flash(this.elements.bCopy, 'OK'), () => this.flash(this.elements.bCopy, 'ERR'));
            };
            this.elements.bFav = Helpers.createButton(icons.fav, '');
            row.append(this.elements.bSearch, this.elements.bHls, this.elements.bCopy, this.elements.bFav);
            this.fullContainer.appendChild(row);
        }

        buildSettings() {
            this.fullContainer.appendChild(mk('div', 'sep'));
            const row = mk('div', 'row-4');
            const btnCol = mk('button', 'z-btn');
            btnCol.innerHTML = icons.color;
            btnCol.style.cssText = 'width:32px;height:22px;border-radius:6px;';
            const rnd = (min, max) => Math.floor(min + Math.random() * (max - min));
            btnCol.onclick = () => {
                const c = `hsla(${rnd(0, 360)},${rnd(35, 55)}%,${rnd(35, 50)}%,0.85)`;
                AppState.theme = c;
                Store.set('bc_theme', c);
                this.host.style.setProperty('--th', c);
            };

            const scaleCtrl = mk('div', 'scale-ctrl');
            const btnZoomOut = mk('button', 'z-btn');
            btnZoomOut.innerHTML = '−';
            const btnZoomIn = mk('button', 'z-btn');
            btnZoomIn.innerHTML = '+';
            const scaleVal = mk('span', 's-val');
            scaleVal.textContent = `${Math.round(AppState.scale * 100)}%`;

            const setScale = s => {
                s = clamp(s, 0.6, 1.5);
                AppState.scale = s;
                Store.set('bc_scale', s);
                scaleVal.textContent = `${Math.round(s * 100)}%`;
                this.panel.style.transform = `scale(${s})`;
                if (AppState.hasPos) this.clampPosition();
            };
            btnZoomOut.onclick = () => setScale(AppState.scale - 0.1);
            btnZoomIn.onclick = () => setScale(AppState.scale + 0.1);

            scaleCtrl.append(btnZoomOut, scaleVal, btnZoomIn);
            row.append(btnCol, scaleCtrl);
            this.fullContainer.appendChild(row);

            this.elements.hints = mk('div', 'hotkeys');
            this.fullContainer.appendChild(this.elements.hints);
        }

        updateLabels() {
            const l = L();
            this.elements.dragZone.textContent = l.drag;
            this.elements.bLang.textContent = AppState.lang;
            this.elements.bHome.title = l.home;
            this.elements.bRel.title = l.rel;
            this.elements.bMin.title = l.min;
            this.elements.bHide.title = l.hide;

            const subs = [
                ['bBg', 'bg'], ['bMute', 'sound'], ['bShot', 'snap'], ['bRec', 'rec'],
                ['bQual', 'qual'], ['bPip', 'pip'], ['bFs', 'fs'], ['bThtr', 'thtr'],
                ['bSearch', 'search'], ['bCopy', 'copy'], ['bFav', 'favs']
            ];
            subs.forEach(([k, lk]) => {
                const s = this.elements[k]?.querySelector('sub');
                if (s) s.textContent = l[lk];
            });

            this.elements.hints.innerHTML = l.hints;
            if (this.bookmarkManager) this.bookmarkManager.updateLabels();
        }

        flash(btn, text) {
            const target = btn.querySelector('sub') || btn;
            clearTimeout(target._bcFlashT);
            const orig = target.innerHTML, sz = target.tagName === 'SUB' ? 12 : 16;
            target.innerHTML = text === 'OK'
                ? `<svg viewBox="0 0 24 24" width="${sz}" height="${sz}" stroke="#2ecc71" fill="none" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" style="display:inline-block;vertical-align:middle;margin:0 auto"><polyline points="20 6 9 17 4 12"/></svg>`
                : text;
            target._bcFlashT = setTimeout(() => { target._bcFlashT = null; if (target.isConnected) target.innerHTML = orig; }, 1500);
        }

        toggleHide(hide) {
            AppState.isHidden = hide;
            Store.set('bc_hid', hide);
            this.panel.classList.toggle('hidden', hide);
            this.ghostBtn.style.display = hide ? 'flex' : 'none';
            if (!hide && AppState.hasPos) this.clampPosition();
        }

        savePos(x, y) {
            AppState.posX = x; AppState.posY = y; AppState.hasPos = true;
            Store.set('bc_x', x); Store.set('bc_y', y); Store.set('bc_pos', true);
        }

        toggleMinimize(minimized, initial = false) {
            AppState.isMinimized = minimized;
            Store.set('bc_min', minimized);

            if (!initial) {
                const rect = this.panel.getBoundingClientRect();
                const sign = minimized ? 1 : -1;
                const newLeft = `${(parseFloat(this.panel.style.left) || rect.left) + sign * MINI_SHIFT_X * AppState.scale}px`;
                const newTop = `${(parseFloat(this.panel.style.top) || rect.top) + sign * MINI_SHIFT_Y * AppState.scale}px`;
                this.panel.style.cssText += `;left:${newLeft};top:${newTop};right:auto;bottom:auto`;
                this.savePos(newLeft, newTop);
            }

            this.fullContainer.style.display = minimized ? 'none' : 'flex';
            this.restoreBtn.style.display = minimized ? 'flex' : 'none';
            this.panel.className = minimized ? 'panel mini' : 'panel';
            if (!initial) this.clampPosition();
        }

        onDragStart(e) {
            if (e.button !== 0 && e.pointerType === 'mouse') return;
            e.preventDefault();
            const rect = this.panel.getBoundingClientRect();
            const iL = rect.left, iT = rect.top;
            this.panel.style.cssText += `;left:${iL}px;top:${iT}px;right:auto;bottom:auto`;

            Helpers.trackDrag(e, {
                minDist: 2,
                onMove: (dx, dy) => {
                    this.hasMoved = true;
                    this.panel.style.left = `${iL + dx}px`;
                    this.panel.style.top = `${iT + dy}px`;
                },
                onEnd: (moved) => {
                    this.hasMoved = moved;
                    if (moved) {
                        this.clampPosition();
                        this.savePos(this.panel.style.left, this.panel.style.top);
                    }
                }
            });
        }

        setupDrag() {
            this.panel.addEventListener('pointerdown', e => {
                if (AppState.isMinimized) return;
                if (e.target.closest('button, input, textarea, a, .btn, .h-btn, .z-btn, .vslider, .resize-handle, .bm-overlay')) return;
                this.onDragStart(e);
            });
        }

        setupResize() {
            this.resizeHandle = mk('div', 'resize-handle');
            this.panel.appendChild(this.resizeHandle);

            this.resizeHandle.addEventListener('pointerdown', e => {
                if (e.button !== 0 && e.pointerType === 'mouse') return;
                e.preventDefault();
                e.stopPropagation();
                const startScale = AppState.scale;
                const scaleVal = this.shadow.querySelector('.s-val');

                Helpers.trackDrag(e, {
                    minDist: 1,
                    onMove: (dx, dy) => {
                        const newScale = clamp(startScale + (dx + dy) / 250, 0.6, 1.8);
                        AppState.scale = newScale;
                        Store.set('bc_scale', newScale);
                        this.panel.style.transform = `scale(${newScale})`;
                        if (scaleVal) scaleVal.textContent = `${Math.round(newScale * 100)}%`;
                        if (AppState.hasPos) this.clampPosition();
                    }
                });
            });
        }

        clampPosition(gap = 10) {
            if (this.panel.classList.contains('hidden')) return;
            const rect = this.panel.getBoundingClientRect();
            const l = parseFloat(this.panel.style.left), t = parseFloat(this.panel.style.top);
            if (isNaN(l) || isNaN(t)) return;
            this.panel.style.left = `${clamp(l, gap, Math.max(gap, window.innerWidth - gap - rect.width))}px`;
            this.panel.style.top = `${clamp(t, gap, Math.max(gap, window.innerHeight - gap - rect.height))}px`;
        }

        applyPosition() {
            if (AppState.hasPos && AppState.posX && AppState.posY) {
                this.panel.style.left = AppState.posX;
                this.panel.style.top = AppState.posY;
                this.panel.style.right = 'auto';
                this.panel.style.bottom = 'auto';
            } else {
                this.panel.style.right = '15px';
                this.panel.style.bottom = '10%';
                this.panel.style.left = 'auto';
                this.panel.style.top = 'auto';
            }
        }

        enforceBackgroundPlay() {
            if (this._pbBound) return;
            this._pbBound = true;
            const pb = e => { if (AppState.bgPlay && e.isTrusted) e.stopImmediatePropagation(); };
            ['blur', 'visibilitychange', 'webkitvisibilitychange', 'pagehide'].forEach(evt => {
                window.addEventListener(evt, pb, true);
                document.addEventListener(evt, pb, true);
            });
            const hid = Object.getOwnPropertyDescriptor(Document.prototype, 'hidden');
            const vis = Object.getOwnPropertyDescriptor(Document.prototype, 'visibilityState');
            try {
                Object.defineProperties(document, {
                    hidden: { get: () => AppState.bgPlay ? false : hid.get.call(document), configurable: true },
                    visibilityState: { get: () => AppState.bgPlay ? 'visible' : vis.get.call(document), configurable: true }
                });
            } catch {}
        }

        injectSiteCSS() {
            injectSiteCSS();
        }
    }

    class App {
        constructor() {
            this.ui = new UIController();
            this.videoController = new VideoController(this.ui);
            this.recorder = new MediaRecorderManager(this.videoController, this.ui);
            this.bookmarks = new BookmarkManager(this.ui);
            this.search = new SearchManager(this.ui);

            this.ui.bookmarkManager = this.bookmarks;
            this.ui.searchManager = this.search;

            const vc = this.videoController, el = this.ui.elements;
            Helpers.bindLongPress(el.bMute,
                () => {
                    if (AppState.gmute) setGMute(false);
                    vc.toggleMute();
                },
                () => {
                    setGMute(true);
                    const v = vc.cachedVideo;
                    if (v) { v.muted = true; v.volume = 0; }
                    this.ui.flash(el.bMute, 'LOCK');
                    vc.syncUI();
                }
            );
            el.bPip.onclick = () => vc.togglePiP();
            el.bFs.onclick = () => vc.toggleFullscreen();
            el.bThtr.onclick = () => vc.toggleTheater();
            el.bShot.onclick = () => vc.takeScreenshot();
            el.vIco.onclick = () => vc.toggleMute();
            el.bRec.onclick = () => this.recorder.toggle();
            el.bSearch.onclick = () => this.search.open();
            el.vSlider.oninput = e => vc.setVolume(parseFloat(e.target.value));
            el.vSlider.onwheel = e => {
                e.preventDefault();
                if (vc.cachedVideo) vc.setVolume(clamp(vc.cachedVideo.volume + (e.deltaY < 0 ? 0.05 : -0.05), 0, 1));
            };
            Helpers.bindLongPress(el.bFav,
                () => setTimeout(() => this.bookmarks.open(), 50),
                () => {
                    const name = currentSite.getName();
                    if (name && !isFavName(name)) {
                        this.bookmarks.add();
                        this.ui.flash(el.bFav, 'OK');
                        this.updateViewersCount();
                    } else this.ui.flash(el.bFav, 'ERR');
                }
            );
            el.bFav.onclick = e => { e.preventDefault(); e.stopPropagation(); };

            this.ui.updateLabels();
            this.ui.injectSiteCSS();
            if (AppState.bgPlay) this.ui.enforceBackgroundPlay();
            if (AppState.isMinimized) this.ui.toggleMinimize(true, true);
            if (AppState.hasPos) this.ui.clampPosition();
            if (AppState.isHidden) this.ui.toggleHide(true);

            if (typeof GM_addValueChangeListener === 'function') {
                try {
                    GM_addValueChangeListener('bc_bookmarks', (n, o, v, remote) => { if (remote) this.bookmarks.applyRemote(v); });
                    GM_addValueChangeListener('bc_gmute', (n, o, v, remote) => {
                        if (!remote) return;
                        try { AppState.gmute = JSON.parse(v) === true; } catch {}
                        const cv = vc.cachedVideo;
                        if (AppState.gmute && cv) { cv.muted = true; cv.volume = 0; }
                        vc.syncUI();
                    });
                } catch {}
            }

            window.addEventListener('resize', () => {
                if (AppState.hasPos) this.ui.clampPosition();
            }, { passive: true });

            let tick = 0;
            setInterval(() => {
                if (document.hidden) return;
                tick++;
                this.ui.injectSiteCSS();
                this.updateViewersCount();
            }, 3000);
            setTimeout(() => this.updateViewersCount(), 1000);
        }

        updateViewersCount() {
            const name = currentSite.getName();
            const { stats, bFav } = this.ui.elements;
            if (!name) return stats && (stats.style.display = 'none');

            if (bFav) {
                const isFav = isFavName(name);
                bFav.style.background = isFav ? 'rgba(255, 255, 255, 0.2)' : '';
                bFav.style.borderColor = isFav ? 'rgba(255, 255, 255, 0.3)' : '';
            }

            if (!stats) return;
            stats.style.display = 'flex';
            const countEl = document.querySelector('#bChatHeader .js-oc_count, .chat-header-inner .js-oc_count, #online-chat .js-oc_count, .chat-info-viewers, .v-viewers, .js-oc_count, .oc_count');
            const txt = countEl?.textContent.trim();
            if (txt && stats._last !== txt) {
                stats.innerHTML = `${icons.eye}<span>${escHTML(txt)}</span>`;
                stats._last = txt;
            }
        }
    }

    const init = () => new App();
    document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();

})();