BongaCams Unchained

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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      1.7.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        *://*.webcamsluts.ru/*
// @match        *://*.ukr.bongacams.com/*
// @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
// @connect      bestcam.tv
// @connect      camshowrecordings.com
// @connect      camshowrecord.net
// @connect      livecamsrip.com
// @connect      camdownload.net
// @connect      camshow.cc
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';
    if (window !== window.top) return;

    let streamUrl = null;
    window.addEventListener('bc_hls', e => streamUrl = e.detail);

    const blockLimits = () => {
        const script = document.createElement('script');
        script.textContent = `
        try {
            const origSessionSet = sessionStorage.setItem;
            sessionStorage.setItem = function(k, v) {
                if (!/guestTime|timeLimit|limit/i.test(k)) origSessionSet.apply(this, arguments);
            };
            window.redirectOnLimit = function() { console.log("Unchained: Блокировка редиректа по лимиту."); };

            const origOpen = XMLHttpRequest.prototype.open;
            XMLHttpRequest.prototype.open = function(method, url) {
                if (typeof url === 'string' && url.includes('.m3u8') && !url.includes('chunk')) {
                    window.dispatchEvent(new CustomEvent('bc_hls', {detail: url}));
                }
                origOpen.apply(this, arguments);
            };

            const origSet = Storage.prototype.setItem;
            const reL = /guestTime|ls\\.tft|limit|freeTime|timeLimit/i;
            Storage.prototype.setItem = function (k, v) {
                if (!reL.test(k)) origSet.apply(this, arguments);
            };
            const origC = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie') || Object.getOwnPropertyDescriptor(HTMLDocument.prototype, 'cookie');
            if (origC && origC.set) {
                const reC = /^(limit|time|guest|free)/i;
                Object.defineProperty(document, 'cookie', {
                    get: () => origC.get.call(document),
                    set: v => { if (!reC.test(v.trim())) origC.set.call(document, v); },
                    configurable: true
                });
            }
        } catch {}
        `;
        (document.head || document.documentElement).appendChild(script);
        script.remove();
    };
    blockLimits();

    const Store = {
        get: (key, defaultValue) => {
            try {
                const val = GM_getValue(key);
                if (val === undefined || val === null) return defaultValue;
                const parsed = JSON.parse(val);
                if (Array.isArray(defaultValue) && !Array.isArray(parsed)) return defaultValue;
                return parsed !== null ? parsed : defaultValue;
            } catch {
                return defaultValue;
            }
        },
        set: (key, value) => {
            try {
                GM_setValue(key, JSON.stringify(value));
            } catch {}
        }
    };

    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'),
        userPaused: false,
        bookmarks: Store.get('bc_bookmarks', [])
    };

    const icons = {
        color: '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" fill="none" stroke-width="2"><path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"/></svg>',
        menu: '<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" fill="none" stroke-width="2"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>',
        eye: '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" fill="none" stroke-width="2"><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"/></svg>',
        close: '<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
        home: '<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none" stroke-width="2"><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"/></svg>',
        rel: '<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>',
        min: '<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/></svg>',
        bgOn: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><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"/></svg>',
        bgOff: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>',
        vol: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><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"/></svg>',
        mute: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><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"/></svg>',
        volLow: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/></svg>',
        volMed: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><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"/></svg>',
        snap: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><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"/></svg>',
        rec: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3" fill="currentColor"/></svg>',
        qual: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><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"/></svg>',
        pip: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><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"/></svg>',
        fs: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><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"/></svg>',
        thtr: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><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"/></svg>',
        search: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
        copy: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><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"/></svg>',
        fav: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>',
        play: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>',
        pause: '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>',
        del: '<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" fill="none" stroke-width="2"><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"/></svg>'
    };

    const i18n = {
        RU: { drag: '⋮⋮ ПУЛЬТ', bg: 'ФОН', sound: 'ЗВУК', snap: 'СКРИН', rec: 'ЗАПИСЬ', start: 'СТАРТ...', stop: 'СТОП', err: 'ОШИБКА', no_vid: 'НЕТ ВИДЕО', qual: 'КАЧ-ВО', pip: 'PIP', fs: 'ЭКРАН', thtr: 'ТЕАТР', search: 'ПОИСК', copy: 'ССЫЛКА', pause: 'ПАУЗА', play: 'ПЛЕЙ', timer: 'ТАЙМЕР', home: 'Домой', rel: 'Рестарт', min: 'Свернуть', hide: 'Скрыть (H)', hints: 'M=ЗВУК F=ЭКРАН T=ТЕАТР H=СКРЫТЬ', favs: 'ЗАКЛАДКИ', hidden: 'Панель скрыта. Нажмите Меню или "H".', prompt: 'Остановить через (минут)?', no_model: 'Модель не найдена', copied: 'Ссылка скопирована!', no_audio: '! Без звука', bm_title: 'Избранное', bm_add: 'В избранное', bm_empty: 'Список пуст.', bm_exp: 'Экспорт', bm_imp: 'Импорт', bm_dup: 'Уже в списке!', bm_ok: 'Сохранено!', note_ph: 'Заметка...' },
        EN: { drag: '⋮⋮ REMOTE', bg: 'BG', sound: 'VOL', snap: 'SNAP', rec: 'REC', start: 'START...', stop: 'STOP', err: 'ERROR', no_vid: 'NO VIDEO', qual: 'QUAL', pip: 'PIP', fs: 'FULL', thtr: 'THTR', search: 'SEARCH', copy: 'LINK', pause: 'PAUSE', play: 'PLAY', timer: 'TIMER', home: 'Home', rel: 'Reload', min: 'Minimize', hide: 'Hide (H)', hints: 'M=MUTE F=FULL T=THTR H=HIDE', favs: 'FAVS', hidden: 'Panel hidden. Tap Menu or press "H".', prompt: 'Stop in how many minutes?', no_model: 'Model not found', copied: 'Link copied!', no_audio: '! No audio', bm_title: 'Favorites', bm_add: 'Add current', bm_empty: 'List is empty.', bm_exp: 'Export', bm_imp: 'Import', bm_dup: 'Already added!', bm_ok: 'Saved!', note_ph: 'Note...' }
    };
    const L = () => i18n[AppState.lang];

    const currentSite = (() => {
        const hostname = window.location.hostname;
        const configs = {
            bongacams: {
                match: /bongacams|webcamsluts/,
                videoSelectors: ['.performer-video-container video', '.video-panel video', '.player-container video', '.chat-video-container video', '#video-panel-wrap video', 'video'],
                containerSelectors: ['.player-container', '.performer-video-container', '.chat-video-container', '.video-panel', '#video-panel-wrap'],
                fullscreenSelectors: ['.performer-video-container', '.video-panel', '#video-panel-wrap', '.player-container', '.chat-video-container'],
                getName: () => {
                    const el = document.querySelector('#bChatRoomTitle, .chat-header-inner h1 a');
                    if (el && el.textContent) return el.textContent.trim();
                    const m = location.pathname.match(/^\/(?:profile\/)?([^/]+)/);
                    return m ? decodeURIComponent(m[1]) : document.title.replace(/Публічний онлайн секс-чат|Public online sex chat|Free online sex chat|Бесплатный онлайн секс-чат/ig, '').split(/[|\-–—]/)[0].trim();
                },
                css: `.bc-header,.bc-header-main,.h_container,#mainbar_container,.join_button_container,.js-chat_join_button,.join_btn,.login-btn,.bcm_socials,.bc-footer,#footer_container,.bcm_footer,#id-chat-header,#bChatInputContainer,#games_button,.mls_title,.live_tabs,#login,[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"]{display:none!important;opacity:0!important;pointer-events:none!important;height:0!important;margin:0!important;padding:0!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}.bc_alternate_header_live_tabs,.mls_hash_tag,.__fixed_top{position:static!important}#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}`
            },
            chaturbate: {
                match: /chaturbate/,
                videoSelectors: ['video#html5_video', '.video-js video', '.vjs-tech', 'video'],
                containerSelectors: ['.video-js', '.player-wrap', '.player-container'],
                fullscreenSelectors: ['.video-js', '.player-wrap', '.player-container'],
                getName: () => {
                    const path = window.location.pathname.split('/').filter(Boolean);
                    if (path.length > 0 && !['tag', 'tags', 'exhibitionist', 'female', 'male', 'trans', 'couples'].includes(path[0])) return path[0];
                    return '';
                },
                css: `.room-header-info, .ad-banner { display:none!important; }`
            },
            stripchat: {
                match: /stripchat/,
                videoSelectors: ['video.player-video', 'video'],
                containerSelectors: ['.player-video-container', '.player-container'],
                fullscreenSelectors: ['.player-video-container', '.player-container'],
                getName: () => document.title.split('-')[0].trim(),
                css: `[class*="banner"] { display:none!important; }`
            }
        };
        return Object.values(configs).find(c => c.match.test(hostname)) || configs.bongacams;
    })();

    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; }
        .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; color: rgba(255, 255, 255, .6); font-size: 11px; font-weight: 600; letter-spacing: 1px; text-transform: uppercase; cursor: grab; display: flex; align-items: center; padding-left: 8px; user-select: none; touch-action: none !important; }
        .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: 8px; box-sizing: border-box; line-height: 1; }
        .hdr-btns { display: flex; gap: 5px; align-items: center; flex-shrink: 0; }
        .h-btn { width: 24px; height: 24px; background: rgba(255, 255, 255, .05); border: none; border-radius: 6px; color: #fff; cursor: pointer; display: flex; justify-content: center; align-items: center; 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; display: flex; flex-direction: column; align-items: center; justify-content: center; 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; align-items: center; justify-content: center; 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; align-items: center; justify-content: center; 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 { display: flex; align-items: center; justify-content: center; 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; display: flex; justify-content: center; align-items: center; 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: 340px; max-height: 85vh; 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: 20px; cursor: pointer; }
        .bm-close:hover { color: #fff; }
        .bm-list { flex: 1; min-height: 0; overflow-y: auto; 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: 32px; height: 32px; border-radius: 8px; background: rgba(231, 76, 60, .2); color: #e74c3c; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 14px; transition: .2s; flex-shrink: 0; }
        .bm-del:hover { background: #e74c3c; color: #fff; }
        .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; }
        .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; }
        .toast-container { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; gap: 10px; z-index: 2147483647; pointer-events: none; }
        
        .resize-handle { position: absolute; right: 2px; bottom: 2px; width: 16px; height: 16px; cursor: nwse-resize; touch-action: none !important; display: flex; align-items: center; justify-content: center; opacity: 0.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; }
    
        .toast { background: rgba(10, 10, 10, .85); color: #fff; padding: 12px 24px; border-radius: 30px; font-family: system-ui, sans-serif; font-size: 14px; border: 1px solid rgba(255, 255, 255, .2); backdrop-filter: blur(10px); box-shadow: 0 4px 15px rgba(0, 0, 0, .5); opacity: 0; transition: all .4s ease; transform: translateY(20px); text-align: center; }
    `;

    class ToastManager {
        constructor(shadowRoot) {
            this.container = document.createElement('div');
            this.container.className = 'toast-container';
            shadowRoot.appendChild(this.container);
        }
        show(message) {
            const t = document.createElement('div');
            t.className = 'toast';
            t.textContent = message;
            this.container.appendChild(t);
            requestAnimationFrame(() => {
                t.style.opacity = '1';
                t.style.transform = 'translateY(0)';
            });
            setTimeout(() => {
                t.style.opacity = '0';
                t.style.transform = 'translateY(-20px)';
                setTimeout(() => t.remove(), 400);
            }, 3000);
        }
    }

    class Helpers {
        static getModelSecretData() {
            try {
                for (let s of document.querySelectorAll('script[type="application/json"]')) {
                    if (s.textContent.includes('chatLocalData')) return JSON.parse(s.textContent);
                }
            } 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}`);
            const ic = this.createElement('span');
            ic.innerHTML = iconHTML;
            const sub = this.createElement('sub');
            sub.textContent = label;
            b.append(ic, sub);
            return b;
        }
        static formatTime(sec) {
            const m = Math.floor(sec / 60).toString().padStart(2, '0');
            const s = (sec % 60).toString().padStart(2, '0');
            return `${m}:${s}`;
        }
    }

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

        bindEvents() {
            window.addEventListener('keydown', e => {
                try { const ae = e.composedPath()[0]; if (ae?.tagName?.match(/INPUT|TEXTAREA/) || ae?.isContentEditable) return; } catch {}
                if (e.ctrlKey || e.altKey || e.metaKey) return;
                const v = this.cachedVideo;
                switch (e.code) {
                    case 'KeyM': e.preventDefault(); this.toggleMute(); break;
                    case 'KeyF': e.preventDefault(); this.toggleFullscreen(); break;
                    case 'KeyP': e.preventDefault(); if (v) this.togglePiP(); break;
                    case 'KeyT': e.preventDefault(); this.toggleTheater(); break;
                    case 'KeyS': e.preventDefault(); this.takeScreenshot(); break;
                    case 'KeyH': e.preventDefault(); this.ui.toggleHide(!AppState.isHidden); break;
                    case 'ArrowUp': case 'ArrowDown':
                        if (e.shiftKey && v) {
                            e.preventDefault();
                            this.setVolume(Math.max(0, Math.min(1, v.volume + (e.code === 'ArrowUp' ? 0.1 : -0.1))));
                        }
                        break;
                }
            });
        }

        findVideo() {
            if (this.cachedVideo && this.cachedVideo.isConnected) return;
            if (this.cachedVideo && !this.cachedVideo.isConnected && this.isTheater) {
                this.toggleTheater();
            }
            let v = null;
            for (const sel of currentSite.videoSelectors) {
                try { v = document.querySelector(sel); if (v && v.tagName === 'VIDEO') break; } catch {}
            }
            if (!v) {
                const all = Array.from(document.querySelectorAll('video'));
                if (all.length) v = all.reduce((best, cur) => (cur.videoWidth * cur.videoHeight >= best.videoWidth * best.videoHeight ? cur : best));
            }
            if (v && v !== this.cachedVideo) {
                this.cachedVideo = v;
                this.attachInteractions(v);
            } else if (!v) {
                this.cachedVideo = null;
            }
        }

        attachInteractions(v) {
            if (v._bcBound) return;
            v._bcBound = true;
            
            // "Выдавливаем" видео и его контейнер на самый передний план (поверх вотермарки и DMCA)
            v.style.setProperty('position', 'relative', 'important');
            v.style.setProperty('z-index', '9999', 'important');
            if (v.parentElement) {
                v.parentElement.style.setProperty('position', 'relative', 'important');
                v.parentElement.style.setProperty('z-index', '9999', 'important');
            }

            ['volumechange', 'pause', 'play'].forEach(ev => v.addEventListener(ev, () => this.syncUI(), { passive: true }));
            
            let container = null;
            for (const sel of currentSite.containerSelectors) {
                try { container = v.closest(sel); if (container) break; } catch {}
            }
            const target = container || 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(() => {
                            this.setVolume(Math.max(0, Math.min(1, v.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;
            v.volume = val;
            if (val > 0) {
                v.muted = false;
                this.lastVolume = val;
            }
            this.syncUI();
        }

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

        togglePlayPause() {
            const v = this.cachedVideo;
            if (!v) return;
            if (v.paused) {
                AppState.userPaused = false;
                v.play().catch(() => {});
            } else {
                AppState.userPaused = true;
                v.pause();
            }
            this.syncUI();
        }

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

        toggleFullscreen() {
            let t = null;
            for (const sel of currentSite.fullscreenSelectors) {
                try { t = document.querySelector(sel); if (t) break; } catch {}
            }
            if (!t) t = this.cachedVideo;
            if (!t) return;

            if (!document.fullscreenElement && !document.webkitFullscreenElement) {
                try { (t.requestFullscreen ?? t.webkitRequestFullscreen ?? t.mozRequestFullScreen ?? (() => {})).call(t); } catch {}
            } else {
                try { (document.exitFullscreen ?? document.webkitExitFullscreen ?? document.mozCancelFullScreen ?? (() => {})).call(document); } catch {}
            }
        }

        toggleTheater() {
            this.isTheater = !this.isTheater;
            const styleId = 'bc-theater-style';
            if (this.isTheater) {
                const v = this.cachedVideo;
                if (!v) { this.isTheater = false; return; }
                let wrap = null;
                for (const sel of currentSite.containerSelectors) {
                    try { wrap = v.closest(sel); if (wrap) break; } catch {}
                }
                wrap = wrap || v.parentElement;
                this.theaterWrap = wrap;
                
                const s = document.createElement('style');
                s.id = styleId;
                s.textContent = `
                    body.bc-theater-mode { overflow:hidden!important; background:#000!important; margin:0!important; padding:0!important; }
                    body.bc-theater-mode .chat_panel_wrap, body.bc-theater-mode .player_footer, body.bc-theater-mode [class*="chat-panel"], body.bc-theater-mode [class*="chat_panel"], body.bc-theater-mode [class*="player-footer"], body.bc-theater-mode [class*="footer"]:not(#bc-unchained-host *), body.bc-theater-mode #chat_bar_v2_container, body.bc-theater-mode #chat_bar_v2, body.bc-theater-mode #switch_between_chat, body.bc-theater-mode .chat-tabs, body.bc-theater-mode [class*="chat_nav"], body.bc-theater-mode [class*="chat_bar"], body.bc-theater-mode #games_button, body.bc-theater-mode .mls_title, body.bc-theater-mode .live_tabs { display:none!important; }
                    body.bc-theater-mode .bc-thtr-wrap { position:fixed!important; inset:0!important; width:100vw!important; height:100vh!important; max-width:100vw!important; max-height:100vh!important; z-index:2147483600!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; max-width:100vw!important; max-height:100vh!important; z-index:2147483601!important; object-fit:contain!important; background:#000!important; margin:0!important; padding:0!important; transform:none!important; }
                `;
                document.head.appendChild(s);
                this.theaterWrap.classList.add('bc-thtr-wrap');
                v.classList.add('bc-thtr-video');
                document.body.classList.add('bc-theater-mode');
                v.style.removeProperty('position');
                v.style.setProperty('z-index', '2147483646', 'important');
                if (v.parentElement) {
                    v.parentElement.style.removeProperty('position');
                    v.parentElement.style.setProperty('z-index', '2147483645', 'important');
                }
                if (this.theaterWrap) {
                    this.theaterWrap.style.removeProperty('position');
                    this.theaterWrap.style.setProperty('z-index', '2147483644', 'important');
                }
                window.scrollTo(0, 0);
            } else {
                const s = document.getElementById(styleId);
                if (s) s.remove();
                document.body.classList.remove('bc-theater-mode');
                if (this.theaterWrap && this.cachedVideo) {
                    this.theaterWrap.classList.remove('bc-thtr-wrap');
                    this.cachedVideo.classList.remove('bc-thtr-video');
                    this.cachedVideo.style.setProperty('position', 'relative', 'important');
                    this.cachedVideo.style.setProperty('z-index', '9999', 'important');
                    if (this.cachedVideo.parentElement) {
                        this.cachedVideo.parentElement.style.setProperty('position', 'relative', 'important');
                        this.cachedVideo.parentElement.style.setProperty('z-index', '9999', 'important');
                    }
                }
                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;
                    const url = URL.createObjectURL(b), a = document.createElement('a');
                    a.href = url; a.download = `BC_${currentSite.getName()}_${new Date().toLocaleTimeString().replace(/:/g, '-')}.jpg`;
                    a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
                    this.ui.flash(this.ui.elements.bShot, 'OK');
                }, 'image/jpeg', 0.95);
            } catch {}
        }

        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 paused = AppState.userPaused || v.paused;

            if (this.ui._lastVol !== vol || this.ui._lastMuted !== muted) {
                const subEl = this.ui.elements.bMute.querySelector('sub');
                if (subEl) subEl.textContent = L().sound;
                this.ui.elements.bMute.firstChild.innerHTML = muted ? icons.mute : icons.vol;
                this.ui.elements.bMute.classList.toggle('on', muted);
                
                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;
            }

            if (this.ui._lastPaused !== paused) {
                this.ui._lastPaused = paused;
            }
        }
    }

    class MediaRecorderManager {
        constructor(videoController, uiController, toastManager) {
            this.vc = videoController;
            this.ui = uiController;
            this.toast = toastManager;
            this.mRec = null;
            this.chunks = [];
            this.interval = null;
            this.seconds = 0;
            this.partNum = 1;
            this.recStartTime = '';
            this.splitRequested = false;
            this.wakeLock = null;
        }

        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 === 'recording') {
                this.splitRequested = false;
                this.mRec.stop();
                if (this.wakeLock) { this.wakeLock.release().catch(()=>{}); this.wakeLock = null; }
                return;
            }

            try {
                const s = v.captureStream?.(30) ?? v.mozCaptureStream?.(30);
                if (!s) throw new Error();
                if (s.getAudioTracks().length === 0) this.toast.show(L().no_audio);
                
                const mt = ['video/mp4', 'video/webm;codecs=vp8,opus', 'video/webm'].find(t => MediaRecorder.isTypeSupported(t)) ?? 'video/webm';
                const ext = mt.includes('mp4') ? 'mp4' : 'webm';
                
                this.recStartTime = new Date().toLocaleTimeString().replace(/:/g, '-');
                this.partNum = 1;
                
                this.startNewRecord(s, mt, ext, btn, sub);

                btn.classList.add('on', 'rec-active');
                if (sub) sub.textContent = Helpers.formatTime(0);
                
                this.seconds = 0;
                this.interval = setInterval(() => {
                    this.seconds++;
                    if (sub) sub.textContent = Helpers.formatTime(this.seconds);
                    if (this.mRec && this.mRec.state === 'recording' && this.seconds % 5 === 0) {
                        this.mRec.requestData();
                    }
                }, 1000);

            } catch (err) {
                this.ui.flash(btn, `! ${L().err}`);
            }
        }

        startNewRecord(stream, mimeType, ext, btn, sub) {
            this.chunks = [];
            this.mRec = new MediaRecorder(stream, { mimeType: mimeType });
            
            this.mRec.ondataavailable = e => {
                if (e.data?.size > 0) {
                    this.chunks.push(e.data);
                    let currentSize = this.chunks.reduce((acc, c) => acc + c.size, 0);
                    if (currentSize > 150000000 && !this.splitRequested) {
                        this.splitRequested = true;
                        this.mRec.stop();
                    }
                }
            };

            this.mRec.onstop = () => {
                if (this.chunks.length > 0) {
                    const blob = new Blob(this.chunks, { type: mimeType });
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.style.display = 'none';
                    a.href = url;
                    const suffix = (this.partNum > 1 || this.splitRequested) ? `_PT${this.partNum}` : '';
                    a.download = `BC_${currentSite.getName() || 'REC'}_${this.recStartTime}${suffix}.${ext}`;
                    document.body.appendChild(a);
                    a.click();
                    setTimeout(() => { a.remove(); URL.revokeObjectURL(url); }, 100);
                }

                if (this.splitRequested) {
                    this.partNum++;
                    this.splitRequested = false;
                    this.startNewRecord(stream, mimeType, ext, btn, sub);
                } else {
                    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; }
                    stream.getTracks().forEach(track => track.stop());
                }
            };

            setTimeout(async () => {
                if (this.mRec && this.mRec.state !== 'recording') {
                    this.mRec.start();
                    try { if ('wakeLock' in navigator && !this.wakeLock) this.wakeLock = await navigator.wakeLock.request('screen'); } catch {}
                }
            }, 500);
        }
    }

    class BookmarkManager {
        constructor(uiController, toastManager) {
            this.ui = uiController;
            this.toast = toastManager;
            this.overlay = null;
            this.list = null;
            this.build();
        }

        build() {
            this.overlay = Helpers.createElement('div', 'bm-overlay');
            this.overlay.onclick = e => { if (e.target === this.overlay) this.overlay.classList.remove('open'); };
            
            const modal = Helpers.createElement('div', 'bm-modal');
            const hdr = Helpers.createElement('div', 'bm-hdr');
            this.title = Helpers.createElement('span', 'bm-title');
            
            const closeBtn = Helpers.createElement('button', 'bm-close');
            closeBtn.innerHTML = '×';
            closeBtn.onclick = () => this.overlay.classList.remove('open');
            
            const svgCam = '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" fill="none" stroke-width="2" style="vertical-align:-1px; margin-right:4px;"><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"/></svg>';
            const btnTogglePrev = Helpers.createElement('button');
            btnTogglePrev.style.cssText = 'font-size:11px; margin-left:auto; margin-right:8px; cursor:pointer; background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.15); color:#fff; padding:3px 8px; border-radius:6px; display:flex; align-items:center;';
            btnTogglePrev.innerHTML = svgCam + (Store.get('bc_previews', false) ? ' On' : ' Off');
            btnTogglePrev.onclick = () => {
                const cur = !Store.get('bc_previews', false);
                Store.set('bc_previews', cur);
                btnTogglePrev.innerHTML = svgCam + (cur ? ' On' : ' Off');
                this.render();
            };

            const svgClock = '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" fill="none" stroke-width="2" style="vertical-align:-1px; margin-right:4px;"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
            const svgName = '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" fill="none" stroke-width="2" style="vertical-align:-1px; margin-right:4px;"><path d="M4 20l5-12 5 12"/><path d="M6 15h6"/></svg>';
            const svgDot = '<svg viewBox="0 0 24 24" width="12" height="12" stroke="#2ecc71" fill="#2ecc71" stroke-width="2" style="vertical-align:-1px; margin-right:3px;"><circle cx="12" cy="12" r="7"/></svg>';
            const svgArrowDown = '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" fill="none" stroke-width="2" style="vertical-align:-1px;"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>';
            const svgArrowUp = '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" fill="none" stroke-width="2" style="vertical-align:-1px;"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>';

            const sortWrap = Helpers.createElement('div');
            sortWrap.style.cssText = 'display:flex; align-items:center; gap:4px; margin-left:auto; margin-right:6px;';

            this.btnSortType = Helpers.createElement('button');
            this.btnSortType.style.cssText = 'font-size:11px; cursor:pointer; background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.15); color:#fff; padding:3px 7px; border-radius:6px; display:flex; align-items:center;';

            this.btnSortDir = Helpers.createElement('button');
            this.btnSortDir.style.cssText = 'font-size:11px; cursor:pointer; background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.15); color:#fff; padding:3px 6px; border-radius:6px; display:flex; align-items:center;';

            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';
                
                let icon = svgClock;
                let label = ru ? 'Дата' : 'Date';
                if (type === 'name') {
                    icon = svgName;
                    label = ru ? 'Имя' : 'Name';
                } else if (type === 'online') {
                    icon = svgDot;
                    label = ru ? 'Лайв' : 'Live';
                }
                
                this.btnSortType.innerHTML = icon + label;
                this.btnSortDir.innerHTML = isRev ? svgArrowUp : svgArrowDown;
            };

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

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

            this.updateSortUI();
            sortWrap.append(this.btnSortType, this.btnSortDir);

            hdr.append(this.title, sortWrap, btnTogglePrev, closeBtn);
            
            this.list = Helpers.createElement('div', 'bm-list');
            
            const foot = Helpers.createElement('div', 'bm-foot');
            this.btnAdd = Helpers.createElement('button', 'bm-add');
            this.btnAdd.onclick = () => this.add();
            this.btnExp = Helpers.createElement('button', 'bm-btn');
            this.btnExp.onclick = () => this.export();
            this.btnImp = Helpers.createElement('button', 'bm-btn');
            this.btnImp.onclick = () => this.import();
            
            foot.append(this.btnAdd, this.btnExp, this.btnImp);
            modal.append(hdr, 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;
            if (this.updateSortUI) this.updateSortUI();
        }

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

        render() {
            this.list.innerHTML = '';
            const currentName = currentSite.getName();
            
            this.btnAdd.disabled = !currentName;
            this.btnAdd.textContent = currentName ? `${L().bm_add} (${currentName})` : L().no_model;
            
            if (!AppState.bookmarks.length) {
                this.list.innerHTML = `<div class="bm-empty">${L().bm_empty}</div>`;
                return;
            }

            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) => {
                let res = 0;
                if (sortType === 'name') {
                    res = x.b.name.localeCompare(y.b.name);
                } else if (sortType === 'online') {
                    const stX = (x.b._statusCache && x.b._statusCache > 0) ? 1 : 0;
                    const stY = (y.b._statusCache && y.b._statusCache > 0) ? 1 : 0;
                    res = stY - stX;
                    if (res === 0) res = x.i - y.i;
                } else {
                    res = x.i - y.i;
                }
                return isRev ? -res : res;
            });

            sortedList.forEach(({ b }) => {
                const item = Helpers.createElement('div', 'bm-item');
                const top = Helpers.createElement('div', 'bm-item-top');
                const link = Helpers.createElement('a', 'bm-link');
                link.href = b.url;
                
                const img = Helpers.createElement('img', 'bm-avatar');
                img.src = b.avatar || `https://ui-avatars.com/api/?name=${b.name}&background=random`;
                img.onerror = function() { this.src = `https://ui-avatars.com/api/?name=${b.name}&background=random`; };
                
                const stat = Helpers.createElement('span');
                stat.style.cssText = 'width:8px;height:8px;border-radius:50%;background:#888;margin-right:8px;flex-shrink:0;box-shadow:0 0 4px rgba(0,0,0,0.5);transition:0.3s;';
                
                const nameWrap = Helpers.createElement('div');
                nameWrap.style.cssText = 'display:flex;align-items:center;min-width:0;flex:1;';
                
                const nameEl = Helpers.createElement('span', 'bm-name');
                nameEl.textContent = b.name;
                
                nameWrap.append(stat, nameEl);
                link.append(img, nameWrap);
                
                const delBtn = Helpers.createElement('button', 'bm-del');
                delBtn.innerHTML = icons.del;
                delBtn.onclick = (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    AppState.bookmarks = AppState.bookmarks.filter(x => x.name !== b.name);
                    Store.set('bc_bookmarks', AppState.bookmarks);
                    this.render();
                };
                
                top.append(link, delBtn);
                
                const noteInput = Helpers.createElement('textarea', 'bm-note');
                    noteInput.rows = '1';
                    noteInput.style.cssText = 'width:100%; background:rgba(0,0,0,0.4); border:1px solid rgba(255,255,255,0.1); color:#fff; border-radius:8px; padding:8px 10px; font-size:13px; font-family:inherit; outline:none; 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 = (el) => {
                        el.style.height = 'auto';
                        el.style.height = Math.max(36, el.scrollHeight) + 'px';
                    };

                    setTimeout(() => autoResize(noteInput), 0);

                    noteInput.oninput = e => {
                        autoResize(e.target);
                        b.note = e.target.value.trim();
                        Store.set('bc_bookmarks', AppState.bookmarks);
                    };
                    noteInput.onchange = () => {
                        this.toast.show(L().bm_ok);
                    };
                    noteInput.onkeydown = e => e.stopPropagation();

                    const topicEl = Helpers.createElement('div', 'bm-topic');
                    topicEl.style.cssText = 'font-size:11px; color:#ddd; background:rgba(0,0,0,0.35); padding:6px 10px; border-radius:6px; margin-top:6px; margin-bottom:6px; word-break:break-word; white-space:pre-wrap; display:none; border:1px solid rgba(255,255,255,0.08);';

                    const previewImg = Helpers.createElement('img');
                    previewImg.style.cssText = 'width:100%; max-height:180px; object-fit:cover; border-radius:8px; margin-top:8px; display:none; border:1px solid rgba(255,255,255,0.1);';

                    item.append(top, topicEl, noteInput, previewImg);
                    this.list.append(item);

                    const applyStatus = () => {
                        if (b._statusCache === 1) {
                            stat.style.background = '#2ecc71'; stat.style.boxShadow = '0 0 8px #2ecc71';
                        } else if (b._statusCache === 2) {
                            stat.style.background = '#f39c12'; stat.style.boxShadow = '0 0 8px #f39c12';
                        } else if (b._statusCache === 3) {
                            stat.style.background = '#9b59b6'; stat.style.boxShadow = '0 0 8px #9b59b6';
                        } else if (b._statusCache === 0) {
                            stat.style.background = '#e74c3c'; stat.style.boxShadow = '0 0 4px rgba(0,0,0,0.5)';
                        } else {
                            stat.style.background = '#888'; stat.style.boxShadow = '0 0 4px rgba(0,0,0,0.5)';
                        }
                        nameEl.textContent = b.name + (b._labelCache || '');

                        const showPrev = Store.get('bc_previews', false);
                        if (showPrev && b._frameCache && b._statusCache !== 0) {
                            previewImg.setAttribute('data-frame', b._frameCache);
                            previewImg.src = b._frameCache.split('?')[0] + '?t=' + Date.now();
                            previewImg.style.display = 'block';
                        } else {
                            previewImg.removeAttribute('data-frame');
                            previewImg.style.display = 'none';
                        }
                    };

                    applyStatus();

                    if (!this._previewTimer) {
                        this._previewTimer = setInterval(() => {
                            if (!this.overlay.classList.contains('open') || !Store.get('bc_previews', false)) return;
                            const listRect = this.list.getBoundingClientRect();
                            this.list.querySelectorAll('img[data-frame]').forEach(imgEl => {
                                const rect = imgEl.getBoundingClientRect();
                                if (rect.top < listRect.bottom && rect.bottom > listRect.top) {
                                    const url = imgEl.getAttribute('data-frame');
                                    if (url) imgEl.src = url.split('?')[0] + '?t=' + Date.now();
                                }
                            });
                        }, 500);
                    }

                    fetch(b.url, { cache: "no-store" }).then(r => r.text()).then(h => {
                        let status = 1;
                        let label = '';
                        
                        const isProfile = /__page_profile/i.test(h);
                        const isOff = /"isOnline"\s*:\s*false/i.test(h) || /"isOffline"\s*:\s*true/i.test(h) || /"room_status"\s*:\s*["']offline["']/i.test(h) || /"status"\s*:\s*["']off(?:line)?["']/i.test(h);
                        const isPaused = /"isChatShowPaused"\s*:\s*true/i.test(h);
                        const isAway = /"isAway"\s*:\s*true/i.test(h);
                        
                        const isPriv = /"isPrivateChat"\s*:\s*true/i.test(h) || /"isFullPrivateChat"\s*:\s*true/i.test(h);
                        const isGroup = /"isGroupChat"\s*:\s*true/i.test(h);
                        const isVip = /"isVipShow"\s*:\s*true/i.test(h);

                        if (isProfile || isOff || isPaused) {
                            status = 0;
                        } else if (isPriv) {
                            status = 3; label = ' (Приват)';
                        } else if (isGroup) {
                            status = 3; label = ' (Группа)';
                        } else if (isVip) {
                            status = 3; label = ' (VIP)';
                        } else if (isAway) {
                            status = 2; label = ' (Отошла)';
                        }

                        if (AppState.lang === 'EN') {
                            if (label === ' (Приват)') label = ' (Private)';
                            if (label === ' (Группа)') label = ' (Group)';
                            if (label === ' (Отошла)') label = ' (Away)';
                        }

                        const topicMatch = h.match(/"currentTopic"\s*:\s*"([^"]+)"/i);
                        if (topicMatch && topicMatch[1] && status !== 0) {
                            let cleanTopic = topicMatch[1].replace(/\\u([a-fA-F0-9]{4})/g, (m, cc) => String.fromCharCode(parseInt(cc, 16))).trim();
                            if (cleanTopic) {
                                topicEl.style.display = 'block';
                                const txtMore = AppState.lang === 'EN' ? '[More]' : '[Еще]';
                                const txtLess = AppState.lang === 'EN' ? '[Less]' : '[Свернуть]';
                                const renderTopic = (exp) => {
                                    if (cleanTopic.length > 55) {
                                        if (exp) {
                                            topicEl.innerHTML = `<span>${cleanTopic} </span><a class="tp-btn" style="color:var(--th); cursor:pointer; font-weight:bold; text-decoration:underline;">${txtLess}</a>`;
                                        } else {
                                            topicEl.innerHTML = `<span>${cleanTopic.slice(0, 55)}... </span><a class="tp-btn" style="color:var(--th); cursor:pointer; font-weight:bold; text-decoration:underline;">${txtMore}</a>`;
                                        }
                                        topicEl.querySelector('.tp-btn').onclick = (ev) => {
                                            ev.preventDefault();
                                            b._topicExp = !b._topicExp;
                                            renderTopic(b._topicExp);
                                        };
                                    } else {
                                        topicEl.textContent = cleanTopic;
                                    }
                                };
                                renderTopic(b._topicExp || false);
                            }
                        } else {
                            topicEl.style.display = 'none';
                        }

                        const frameMatch = h.match(/"frame"\s*:\s*"([^"]+)"/i);
                        if (frameMatch && frameMatch[1]) {
                            let frameUrl = frameMatch[1].replace(/\\/g, '');
                            if (frameUrl.startsWith('//')) frameUrl = 'https:' + frameUrl;
                            b._frameCache = frameUrl;
                        }

                        b._statusCache = status;
                        b._labelCache = label;
                        applyStatus();
                    }).catch(()=>{});
            });
        }

        add() {
            const name = currentSite.getName();
            if (!name) return;
            if (AppState.bookmarks.some(b => b.name === name)) return this.toast.show(L().bm_dup);
            
            const bUrl = location.href.split(/[?#]/)[0];
            let avatar = '';
            const secretData = Helpers.getModelSecretData();
            if (secretData && secretData.chatHeaderOptions && secretData.chatHeaderOptions.profileImage) {
                avatar = secretData.chatHeaderOptions.profileImage;
                if (avatar.startsWith('//')) avatar = 'https:' + avatar;
            } else {
                avatar = document.querySelector('.chat-info-avatar img, .chat-header-avatar img, .profile-photo img')?.src || '';
            }
            
            const bm = { name, url: bUrl, avatar, note: '' };
            AppState.bookmarks.push(bm);
            Store.set('bc_bookmarks', AppState.bookmarks);
            this.render();
            this.toast.show(L().bm_ok);
            
            if (!avatar) {
                fetch(bUrl).then(r => r.text()).then(html => {
                    const m = html.match(/<meta\s+(?:[^>]*?\s+)?(?:property=["']og:image["']|name=["']og:image["'])[^>]*?\s+content=["']([^"']+)["']/i)
                        || html.match(/content=["']([^"']+)["'][^>]*?property=["']og:image["']/i)
                        || html.match(/"profileImage"\s*:\s*"([^"]+)"/i)
                        || html.match(/"avatarUrl"\s*:\s*"([^"]+)"/i);
                    if (m?.[1]) {
                        bm.avatar = m[1].replace(/\\/g, '');
                        if (bm.avatar.startsWith('//')) bm.avatar = 'https:' + bm.avatar;
                        Store.set('bc_bookmarks', AppState.bookmarks);
                        if (this.overlay && this.overlay.classList.contains('open')) this.render();
                    }
                }).catch(()=>{});
            }
        }

        export() {
            if (!AppState.bookmarks.length) return;
            const blob = new Blob([JSON.stringify(AppState.bookmarks, null, 2)], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `BC_Favs_${new Date().toISOString().slice(0, 10)}.json`;
            a.click();
            URL.revokeObjectURL(url);
        }

        import() {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.json';
            input.onchange = e => {
                const file = e.target.files[0];
                if (!file) return;
                const reader = new FileReader();
                reader.onload = ev => {
                    try {
                        const arr = JSON.parse(ev.target.result);
                        if (Array.isArray(arr)) {
                            const existing = new Set(AppState.bookmarks.map(b => b.name));
                            const newItems = arr.filter(b => b.name && !existing.has(b.name)).map(b => ({ ...b, note: b.note || '' }));
                            AppState.bookmarks.push(...newItems);
                            Store.set('bc_bookmarks', AppState.bookmarks);
                            this.render();
                            this.toast.show(L().bm_ok);
                        }
                    } catch {
                        this.toast.show(L().err);
                    }
                };
                reader.readAsText(file);
            };
            input.click();
        }
    }

    class SearchManager {
        constructor(uiController, toastManager) {
            this.ui = uiController;
            this.toast = toastManager;
            this.overlay = null;
            this.input = null;
            this.btns = [];
            this.build();
        }

        build() {
            this.overlay = Helpers.createElement('div', 'bm-overlay');
            this.overlay.onclick = e => { if (e.target === this.overlay) this.overlay.classList.remove('open'); };
            
            const modal = Helpers.createElement('div', 'bm-modal');
            const hdr = Helpers.createElement('div', 'bm-hdr');
            this.title = Helpers.createElement('span', 'bm-title');
            
            const closeBtn = Helpers.createElement('button', 'bm-close');
            closeBtn.innerHTML = icons.close;
            closeBtn.onclick = () => this.overlay.classList.remove('open');
            
            const svgCam = '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" fill="none" stroke-width="2" style="vertical-align:-1px; margin-right:4px;"><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"/></svg>';
            const btnTogglePrev = Helpers.createElement('button');
            btnTogglePrev.style.cssText = 'font-size:11px; margin-left:auto; margin-right:8px; cursor:pointer; background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.15); color:#fff; padding:3px 8px; border-radius:6px; display:flex; align-items:center;';
            btnTogglePrev.innerHTML = svgCam + (Store.get('bc_previews', false) ? ' On' : ' Off');
            btnTogglePrev.onclick = () => {
                const cur = !Store.get('bc_previews', false);
                Store.set('bc_previews', cur);
                btnTogglePrev.innerHTML = svgCam + (cur ? ' On' : ' Off');
                this.render();
            };
            hdr.append(this.title, btnTogglePrev, closeBtn);
            
            const list = Helpers.createElement('div', 'bm-list');
            this.input = Helpers.createElement('input', 'bm-note');
            this.input.type = 'text';
            this.input.style.cssText = 'margin-bottom:12px; font-size:18px; padding:12px; text-align:center; font-weight:bold; color:#fff;';
            this.input.onkeydown = e => e.stopPropagation();

            const createBtn = (txt, urlFn) => {
                const btn = Helpers.createElement('button', 'bm-btn');
                btn.textContent = txt;
                btn.style.cssText = 'margin-bottom:8px; font-size:15px; padding:12px; transition: all 0.3s ease; border-left: 4px solid transparent;';
                btn.onclick = () => {
                    const v = this.input.value.trim();
                    if(v) window.open(urlFn(v), '_blank');
                    else this.toast.show(L().no_model);
                };
                this.btns.push({ btn, urlFn });
                return btn;
            };

            list.append(
                this.input,
                createBtn('BestCam.tv', v => `https://bestcam.tv/model/${encodeURIComponent(v)}`),
                createBtn('CamShowRecordings', v => `https://www.camshowrecordings.com/?s=${encodeURIComponent(v)}`),
                createBtn('LiveCamsRip', v => `https://www.livecamsrip.com/${encodeURIComponent(v)}/profile`),
                createBtn('CamDownload', v => `https://camdownload.net/performer/${encodeURIComponent(v)}`),
                createBtn('CamShow.cc', v => `https://camshow.cc/streamer/${encodeURIComponent(v)}`)
            );

            let debounceTimer;
            this.input.oninput = () => {
                clearTimeout(debounceTimer);
                debounceTimer = setTimeout(() => {
                    const v = this.input.value.trim();
                    if (v) {
                        this.btns.forEach(item => this.checkPage(item.urlFn(v), item.btn));
                    } else {
                        this.btns.forEach(item => { item.btn.style.borderLeft = '4px solid transparent'; item.btn.style.opacity = '1'; });
                    }
                }, 600);
            };

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

        checkPage(url, btn) {
            btn.style.borderLeft = '4px solid orange';
            btn.style.opacity = '0.8';
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                timeout: 8000,
                onload: (res) => {
                    try {
                        if (res.status === 404 || res.status >= 500) throw new Error();
                        const text = res.responseText.toLowerCase();
                        const titleMatch = text.match(/<title[^>]*>(.*?)<\/title>/i);
                        const title = titleMatch ? titleMatch[1] : '';
                        if (['not found', '404', 'error'].some(t => title.includes(t))) throw new Error();
                        if (/no\s*videos?\s*found/i.test(text) || /no\s*results?\s*found/i.test(text) || /does\s*not\s*exist/i.test(text) || /\b0\s*results?\b/i.test(text)) throw new Error();
                        if (url.includes('camshowrecordings') && !res.responseText.includes('class="h1modelpage"')) throw new Error();
                        btn.style.borderLeft = '4px solid #10b981';
                        btn.style.opacity = '1';
                    } catch {
                        btn.style.borderLeft = '4px solid #ef4444';
                        btn.style.opacity = '0.5';
                    }
                },
                onerror: () => { btn.style.borderLeft = '4px solid #ef4444'; btn.style.opacity = '0.5'; },
                ontimeout: () => { btn.style.borderLeft = '4px solid #ef4444'; btn.style.opacity = '0.5'; }
            });
        }

        open() {
            this.title.textContent = '🔍 ' + L().search;
            const name = currentSite.getName() || '';
            this.input.value = name;
            this.overlay.classList.add('open');
            if (name) {
                this.btns.forEach(item => {
                    item.btn.style.borderLeft = '4px solid transparent';
                    item.btn.style.opacity = '1';
                    this.checkPage(item.urlFn(name), item.btn);
                });
            }
        }
    }

    class UIController {
        constructor() {
            this.elements = {};
            this.isDragging = false;
            this.hasMoved = false;
            this.startX = 0;
            this.startY = 0;
            this.initialLeft = 0;
            this.initialTop = 0;
            this.rafId = null;

            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 = Helpers.createElement('div', 'panel');
            this.panel.style.transform = `scale(${AppState.scale})`;
            this.shadow.appendChild(this.panel);

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

            this.restoreBtn = Helpers.createElement('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 = Helpers.createElement('div', 'col');
            this.panel.appendChild(this.fullContainer);

            this.toastManager = new ToastManager(this.shadow);
            
            this.buildHeader();
            this.buildMediaRow();
            this.buildVolume();
            this.buildControls();
            this.buildMisc();
            this.buildSettings();

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

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

        buildHeader() {
            const hdr = Helpers.createElement('div', 'hdr');
            
            this.elements.dragZone = Helpers.createElement('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 = Helpers.createElement('div', 'stats');
            this.elements.stats.innerHTML = `${icons.eye}<span>0</span>`;
            
            this.elements.bLang = Helpers.createElement('button', 'h-btn lang');
            this.elements.bLang.onclick = () => {
                AppState.lang = AppState.lang === 'RU' ? 'EN' : 'RU';
                Store.set('bc_lang', AppState.lang);
                this.updateLabels();
            };

            this.elements.bHome = Helpers.createElement('button', 'h-btn');
            this.elements.bHome.innerHTML = icons.home;
            this.elements.bHome.onclick = () => location.href = '/';

            this.elements.bRel = Helpers.createElement('button', 'h-btn');
            this.elements.bRel.innerHTML = icons.rel;
            this.elements.bRel.onclick = () => location.reload();

            this.elements.bMin = Helpers.createElement('button', 'h-btn');
            this.elements.bMin.innerHTML = icons.min;
            this.elements.bMin.onclick = () => this.toggleMinimize(true);

            this.elements.bHide = Helpers.createElement('button', 'h-btn close');
            this.elements.bHide.innerHTML = icons.close;
            this.elements.bHide.onclick = () => this.toggleHide(true);

            const btns = Helpers.createElement('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 = Helpers.createElement('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 = Helpers.createElement('div', 'v-cap');
            
            this.elements.vIco = Helpers.createElement('span', 'vico');
            this.elements.vIco.innerHTML = icons.vol;
            
            this.elements.vSlider = Helpers.createElement('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 = Helpers.createElement('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(Helpers.createElement('div', 'sep'));
            const row = Helpers.createElement('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');
                const uWin = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

                if (qBlock) {
                    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 isVisible = qBlock.style.display === 'block' || (!qBlock.classList.contains('__hidden') && qBlock.offsetWidth > 0);

                    if (isVisible) {
                        qBlock.classList.add('__hidden');
                        qBlock.style.display = 'none';
                    } else {
                        qBlock.classList.remove('__hidden');
                        qBlock.style.display = 'block';
                        qBlock.style.zIndex = '2147483645';

                        try {
                            if (uWin.$ || uWin.jQuery) {
                                (uWin.$ || uWin.jQuery)('#quality-btn').trigger('click');
                            }
                        } catch {}
                    }
                } else {
                    const q = document.querySelector('#quality-btn, .vjs-resolution-button, .vjs-cog-menu-button, .vjs-icon-cog, [class*="quality"], [class*="resolution"]');
                    if (q) {
                        try { q.click(); } catch {}
                    } else {
                        this.flash(this.elements.bQual, `! ${L().err}`);
                    }
                }
            };

            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 = Helpers.createElement('div', 'grid-4');
            
            this.elements.bSearch = Helpers.createButton(icons.search, '');
            
            this.elements.bHls = Helpers.createButton('<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" fill="none" stroke-width="2"><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><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>', 'HLS');
            
            let hlsDownTime = 0;
            this.elements.bHls.oncontextmenu = e => e.preventDefault();
            this.elements.bHls.onpointerdown = e => {
                if (e.button !== 0 && e.pointerType === 'mouse') return;
                hlsDownTime = Date.now();
            };
            this.elements.bHls.onpointerup = e => {
                if (!hlsDownTime) return;
                const duration = Date.now() - hlsDownTime;
                hlsDownTime = 0;
                
                if (duration > 500) {
                    if (streamUrl && navigator.share) {
                        navigator.share({ title: currentSite.getName() + ' Stream', url: streamUrl }).catch(()=>{});
                    } else if (streamUrl) {
                        this.toastManager.show('Sharing не поддерживается');
                    } else {
                        this.toastManager.show('Поток не найден.');
                    }
                } else {
                    if (streamUrl) {
                        navigator.clipboard.writeText(streamUrl);
                        this.toastManager.show('HLS скопирован!');
                    } else {
                        this.toastManager.show('Поток не найден.');
                    }
                }
            };
            this.elements.bHls.onpointercancel = () => { hlsDownTime = 0; };

            this.elements.bCopy = Helpers.createButton(icons.copy, '');
            this.elements.bCopy.onclick = () => {
                const name = currentSite.getName();
                if (!name) return this.toastManager.show(L().no_model);
                navigator.clipboard.writeText(`${location.origin}/profile/${name}`)
                    .then(() => { this.flash(this.elements.bCopy, 'OK'); this.toastManager.show(L().copied); })
                    .catch(() => 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(Helpers.createElement('div', 'sep'));
            const row = Helpers.createElement('div', 'row-4');
            
            const btnCol = Helpers.createElement('button', 'z-btn');
            btnCol.innerHTML = icons.color;
            btnCol.style.cssText = 'width:32px; height:22px; border-radius:6px;';
            btnCol.onclick = () => {
                const c = `hsla(${Math.floor(Math.random() * 360)}, ${Math.floor(35 + Math.random() * 20)}%, ${Math.floor(35 + Math.random() * 15)}%, 0.85)`;
                AppState.theme = c;
                Store.set('bc_theme', c);
                this.panel.style.setProperty('--th', c);
                this.restoreBtn.style.background = c;
                this.ghostBtn.style.background = c;
            };

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

            const setScale = s => {
                s = Math.max(0.6, Math.min(s, 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 = Helpers.createElement('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 updateSub = (btn, text) => { const s = btn.querySelector('sub'); if (s) s.textContent = text; };
            updateSub(this.elements.bBg, l.bg);
            updateSub(this.elements.bMute, l.sound);
            updateSub(this.elements.bShot, l.snap);
            updateSub(this.elements.bRec, l.rec);
            updateSub(this.elements.bQual, l.qual);
            updateSub(this.elements.bPip, l.pip);
            updateSub(this.elements.bFs, l.fs);
            updateSub(this.elements.bThtr, l.thtr);
            updateSub(this.elements.bSearch, l.search);
            updateSub(this.elements.bCopy, l.copy);
            updateSub(this.elements.bFav, l.favs);
            
            this.elements.hints.innerHTML = l.hints;
            if (this.bookmarkManager) this.bookmarkManager.updateLabels();
        }

        flash(btn, text) {
            const sub = btn.querySelector('sub');
            if (!sub) return;
            const original = sub.textContent;
            sub.textContent = text;
            setTimeout(() => sub.textContent = original, 1500);
        }

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

        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 * 168 * AppState.scale;
                const newTop = (parseFloat(this.panel.style.top) || rect.top) + sign * 5 * AppState.scale;
                
                this.panel.style.cssText += `;left:${newLeft}px;top:${newTop}px;right:auto;bottom:auto`;
                AppState.posX = `${newLeft}px`;
                AppState.posY = `${newTop}px`;
                AppState.hasPos = true;
                Store.set('bc_x', AppState.posX);
                Store.set('bc_y', AppState.posY);
                Store.set('bc_pos', true);
            }
            
            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();
            this.isDragging = true;
            this.hasMoved = false;
            
            const rect = this.panel.getBoundingClientRect();
            this.startX = e.clientX;
            this.startY = e.clientY;
            this.initialLeft = rect.left;
            this.initialTop = rect.top;

            this.panel.style.left = `${rect.left}px`;
            this.panel.style.top = `${rect.top}px`;
            this.panel.style.right = 'auto';
            this.panel.style.bottom = 'auto';

            const target = e.target;
            try { target.setPointerCapture(e.pointerId); } catch {}

            const onMove = (ev) => {
                if (!this.isDragging) return;
                const dx = ev.clientX - this.startX;
                const dy = ev.clientY - this.startY;
                if (!this.hasMoved && Math.hypot(dx, dy) > 2) this.hasMoved = true;
                
                if (this.hasMoved) {
                    this.panel.style.left = `${this.initialLeft + dx}px`;
                    this.panel.style.top = `${this.initialTop + dy}px`;
                }
            };

            const onEnd = (ev) => {
                if (!this.isDragging) return;
                this.isDragging = false;
                try { target.releasePointerCapture(ev.pointerId); } catch {}
                window.removeEventListener('pointermove', onMove);
                window.removeEventListener('pointerup', onEnd);
                window.removeEventListener('pointercancel', onEnd);
                
                if (this.hasMoved) {
                    this.clampPosition();
                    AppState.posX = this.panel.style.left;
                    AppState.posY = this.panel.style.top;
                    AppState.hasPos = true;
                    Store.set('bc_x', AppState.posX);
                    Store.set('bc_y', AppState.posY);
                    Store.set('bc_pos', true);
                }
            };

            window.addEventListener('pointermove', onMove, { passive: true });
            window.addEventListener('pointerup', onEnd);
            window.addEventListener('pointercancel', onEnd);
        }

        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 = Helpers.createElement('div', 'resize-handle');
            this.panel.appendChild(this.resizeHandle);

            let isResizing = false, startX = 0, startY = 0, startScale = 1;

            this.resizeHandle.addEventListener('pointerdown', (e) => {
                if (e.button !== 0 && e.pointerType === 'mouse') return;
                e.preventDefault();
                e.stopPropagation();
                isResizing = true;
                startX = e.clientX;
                startY = e.clientY;
                startScale = AppState.scale;

                const onMove = (ev) => {
                    if (!isResizing) return;
                    const dx = ev.clientX - startX;
                    const dy = ev.clientY - startY;
                    const newScale = Math.max(0.6, Math.min(1.8, startScale + (dx + dy) / 250));
                    
                    AppState.scale = newScale;
                    Store.set('bc_scale', newScale);
                    this.panel.style.transform = `scale(${newScale})`;
                    
                    const scaleVal = this.shadow.querySelector('.s-val');
                    if (scaleVal) scaleVal.textContent = `${Math.round(newScale * 100)}%`;
                    
                    if (AppState.hasPos) this.clampPosition();
                };

                const onEnd = () => {
                    isResizing = false;
                    window.removeEventListener('pointermove', onMove);
                    window.removeEventListener('pointerup', onEnd);
                    window.removeEventListener('pointercancel', onEnd);
                };

                window.addEventListener('pointermove', onMove, { passive: true });
                window.addEventListener('pointerup', onEnd);
                window.addEventListener('pointercancel', onEnd);
            });
        }
    
        clampPosition(gap = 10) {
            const rect = this.panel.getBoundingClientRect();
            let l = parseFloat(this.panel.style.left);
            let t = parseFloat(this.panel.style.top);
            if (isNaN(l) || isNaN(t)) return;
            l = Math.max(gap, Math.min(l, window.innerWidth - gap - rect.width));
            t = Math.max(gap, Math.min(t, window.innerHeight - gap - rect.height));
            this.panel.style.left = `${l}px`;
            this.panel.style.top = `${t}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() {
            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);
            });
            try {
                Object.defineProperties(document, {
                    'hidden': { get: () => AppState.bgPlay ? false : undefined, configurable: true },
                    'visibilityState': { get: () => AppState.bgPlay ? 'visible' : 'hidden', configurable: true }
                });
            } catch {}
        }

        injectSiteCSS() {
            if (document.getElementById('bc-unchained-site-css')) return;
            const style = document.createElement('style');
            style.id = 'bc-unchained-site-css';
            style.textContent = currentSite.css;
            (document.head || document.documentElement).appendChild(style);
        }
    }

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

            this.ui.bookmarkManager = this.bookmarks;
            
            this.ui.elements.bMute.onclick = () => this.videoController.toggleMute();
            this.ui.elements.bPip.onclick = () => this.videoController.togglePiP();
            this.ui.elements.bFs.onclick = () => this.videoController.toggleFullscreen();
            this.ui.elements.bThtr.onclick = () => this.videoController.toggleTheater();
            this.ui.elements.bShot.onclick = () => this.videoController.takeScreenshot();
            this.ui.elements.vIco.onclick = () => this.videoController.toggleMute();
            
            this.ui.elements.vSlider.oninput = e => this.videoController.setVolume(parseFloat(e.target.value));
            this.ui.elements.vSlider.onwheel = e => {
                e.preventDefault();
                const v = this.videoController.cachedVideo;
                if (v) this.videoController.setVolume(Math.max(0, Math.min(1, v.volume + (e.deltaY < 0 ? 0.05 : -0.05))));
            };

            this.ui.elements.bRec.onclick = () => this.recorder.toggle();
            this.ui.elements.bFav.onclick = () => this.bookmarks.open();
            this.ui.elements.bSearch.onclick = () => this.search.open();

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

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

            setInterval(() => this.ui.injectSiteCSS(), 3000);

            setInterval(() => this.updateViewersCount(), 5000);
        }

        updateViewersCount() {
            let countEl = document.querySelector('.js-oc_count, .chat-info-viewers, #online-chat .oc_count, [class*="viewers"], [class*="online-count"]');
            if (countEl) {
                const text = countEl.textContent.trim();
                if (text && this.ui.elements.stats._last !== text) {
                    this.ui.elements.stats.innerHTML = `${icons.eye}<span>${text}</span>`;
                    this.ui.elements.stats._last = text;
                }
            }
        }
    }

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

})();