☰

Cams Unchained

BongaCams, Chaturbate, Stripchat: Clean interface, PiP, fullscreen, recording, screenshots, bookmarks and more.

Avant de procéder à l'installation, Sleazy Forkattention ce script contient des contre-fonctionnalités, qui sont là pour le bénéfice de l'auteur du script, plutôt que pour le vôtre.

Ce script comporte une ou plusieurs commissions pour l'auteur, par exemple en réécrivant des liens ou en fournissant des codes promo pour inclure un code de parrainage ou d'affiliation.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Cams Unchained
// @name:ru       Cams Unchained
// @name:uk       Cams Unchained
// @name:es       Cams Unchained
// @name:de       Cams Unchained
// @name:fr       Cams Unchained
// @name:pt       Cams Unchained
// @name:it       Cams Unchained
// @name:pl       Cams Unchained
// @name:tr       Cams Unchained
// @name:nl       Cams Unchained
// @name:cs       Cams Unchained
// @name:ro       Cams Unchained
// @name:hu       Cams Unchained
// @name:bg       Cams Unchained
// @name:el       Cams Unchained
// @name:ja       Cams Unchained
// @name:zh-CN    Cams Unchained
// @name:ko       Cams Unchained
// @name:he       Cams Unchained
// @name:ar       Cams Unchained
// @name:hi       Cams Unchained
// @name:id       Cams Unchained
// @name:vi       Cams Unchained
// @name:th       Cams Unchained
// @namespace    DiadiaBonga
// @version      4.3.4
// @description:ru BongaCams, Chaturbate, Stripchat: Чистый интерфейс, PiP, полный экран, запись, скриншоты, закладки и многое другое.
// @description BongaCams, Chaturbate, Stripchat: Clean interface, PiP, fullscreen, recording, screenshots, bookmarks and more.
// @description:uk BongaCams, Chaturbate, Stripchat: Чистий інтерфейс, PiP, повний екран, запис, скріншоти, закладки та багато іншого.
// @description:es BongaCams, Chaturbate, Stripchat: Interfaz limpia, PiP, pantalla completa, grabación, capturas, favoritos y mucho más.
// @description:de BongaCams, Chaturbate, Stripchat: Saubere Oberfläche, PiP, Vollbild, Aufnahme, Screenshots, Favoriten und vieles mehr.
// @description:fr BongaCams, Chaturbate, Stripchat: Interface épurée, PiP, plein écran, enregistrement, captures d'écran, favoris et bien plus.
// @description:pt BongaCams, Chaturbate, Stripchat: Interface limpa, PiP, tela cheia, gravação, capturas de tela, favoritos e muito mais.
// @description:it BongaCams, Chaturbate, Stripchat: Interfaccia pulita, PiP, schermo intero, registrazione, screenshot, preferiti e molto altro.
// @description:pl BongaCams, Chaturbate, Stripchat: Czysty interfejs, PiP, pełny ekran, nagrywanie, zrzuty ekranu, zakładki i wiele więcej.
// @description:tr BongaCams, Chaturbate, Stripchat: Temiz arayüz, PiP, tam ekran, kayıt, ekran görüntüsü, yer imleri ve daha fazlası.
// @description:nl BongaCams, Chaturbate, Stripchat: Schone interface, PiP, volledig scherm, opnemen, screenshots, favorieten en meer.
// @description:cs BongaCams, Chaturbate, Stripchat: Čisté rozhraní, PiP, celá obrazovka, nahrávání, snímky obrazovky, záložky a mnoho dalšího.
// @description:ro BongaCams, Chaturbate, Stripchat: Interfață curată, PiP, ecran complet, înregistrare, capturi de ecran, favorite și multe altele.
// @description:hu BongaCams, Chaturbate, Stripchat: Tiszta felület, PiP, teljes képernyő, felvétel, képernyőképek, kedvencek és még sok más.
// @description:bg BongaCams, Chaturbate, Stripchat: Чист интерфейс, PiP, цял екран, запис, скриншоти, любими и още много.
// @description:el BongaCams, Chaturbate, Stripchat: Καθαρή διεπαφή, PiP, πλήρης οθόνη, εγγραφή, στιγμιότυπα, αγαπημένα και πολλά άλλα.
// @description:ja BongaCams, Chaturbate, Stripchat: クリーンなインターフェース、PiP、全画面、録画、スクリーンショット、ブックマークなど。
// @description:zh-CN BongaCams, Chaturbate, Stripchat: 简洁界面、PiP、全屏、录制、截图、收藏等多种功能。
// @description:ko BongaCams, Chaturbate, Stripchat: 깔끔한 인터페이스, PiP, 전체 화면, 녹화, 스크린샷, 즐겨찾기 등.
// @description:he BongaCams, Chaturbate, Stripchat: ממשק נקי, PiP, מסך מלא, הקלטה, צילומי מסך, מועדפים ועוד.
// @description:ar BongaCams, Chaturbate, Stripchat: واجهة نظيفة، PiP، ملء الشاشة، تسجيل، لقطات شاشة، مفضلة والمزيد.
// @description:hi BongaCams, Chaturbate, Stripchat: साफ़ इंटरफ़ेस, PiP, फुलस्क्रीन, रिकॉर्डिंग, स्क्रीनशॉट, पसंदीदा और बहुत कुछ।
// @description:id BongaCams, Chaturbate, Stripchat: Antarmuka bersih, PiP, layar penuh, perekaman, tangkapan layar, favorit, dan lainnya.
// @description:vi BongaCams, Chaturbate, Stripchat: Giao diện gọn gàng, PiP, toàn màn hình, ghi hình, chụp màn hình, yêu thích và nhiều hơn nữa.
// @description:th BongaCams, Chaturbate, Stripchat: อินเทอร์เฟซสะอาด, PiP, เต็มจอ, อัดวิดีโอ, แคปหน้าจอ, รายการโปรด และอื่นๆ
// @author       DiaDiaBogDan
// @license      MIT
// @antifeature   referral-link
// @match        *://*.bongacams.com/*
// @match        *://*.bongacams2.com/*
// @match        *://*.bongocams.com/*
// @match        *://*.webcamsluts.ru/*
// @match        *://*.stripchat.com/*
// @match        *://stripchat.com/*
// @match        *://*.chaturbate.com/*
// @match        *://chaturbate.com/*
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAiIHkxPSIwIiB4Mj0iMCIgeTI9IjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzJiMmIzMCIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzEyMTIxNSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJoYWxvIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAiIHkxPSItNCIgeDI9IjAiIHkyPSI1MDgiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzE4MTgxYiIvPjxzdG9wIG9mZnNldD0iLjQyIiBzdG9wLWNvbG9yPSIjMTgxODFiIi8+PHN0b3Agb2Zmc2V0PSIuNTgiIHN0b3AtY29sb3I9InJnYigyOC41LDI4LjUsMzIuMzQpIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSJyZ2IoMTgsMTgsMjEpIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImhlYXJ0IiB4MT0iMC4xIiB5MT0iMCIgeDI9IjAuOSIgeTI9IjEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmOWRiNSIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI2Q0MmQ2OCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJsaW5rIiB4MT0iMCIgeTE9IjAiIHgyPSIxIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZlM2VhIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmY5ZmI4Ii8+PC9saW5lYXJHcmFkaWVudD48Y2xpcFBhdGggaWQ9Im92ZXIiPjxyZWN0IHg9Ii0xMzAiIHk9Ii02NCIgd2lkdGg9IjI2MCIgaGVpZ2h0PSI2NCIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJjcm9zcyI+PHJlY3QgeD0iLTUyIiB5PSItNjQiIHdpZHRoPSIxMDQiIGhlaWdodD0iNjQiLz48L2NsaXBQYXRoPjxwYXRoIGlkPSJoIiBkPSJNMjU2IDM5OCBDMTUyIDMyMiA5MCAyNjIgOTAgMTk4IEM5MCAxNTAgMTI2IDExNiAxNzAgMTE2IEMyMTAgMTE2IDI0MiAxMzggMjU2IDE3MiBDMjcwIDEzOCAzMDIgMTE2IDM0MiAxMTYgQzM4NiAxMTYgNDIyIDE1MCA0MjIgMTk4IEM0MjIgMjYyIDM2MCAzMjIgMjU2IDM5OCBaIi8+PHJlY3QgaWQ9ImwiIHg9Ii04OCIgeT0iLTM4IiB3aWR0aD0iMTc2IiBoZWlnaHQ9Ijc2IiByeD0iMzgiLz48bWFzayBpZD0iY3V0IiBtYXNrVW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4PSIwIiB5PSItMTAiIHdpZHRoPSI1MTIiIGhlaWdodD0iNTIwIj48cmVjdCB4PSIwIiB5PSItMTAiIHdpZHRoPSI1MTIiIGhlaWdodD0iNTIwIiBmaWxsPSIjZmZmIi8+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzY4IDMyMikgcm90YXRlKDQ1KSI+PGcgY2xpcC1wYXRoPSJ1cmwoI2Nyb3NzKSI+PHVzZSB4bGluazpocmVmPSIjbCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjUwIi8+PC9nPjwvZz48L21hc2s+PC9kZWZzPjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiByeD0iMTEyIiBmaWxsPSJ1cmwoI2JnKSIvPjxyZWN0IHg9IjMiIHk9IjMiIHdpZHRoPSI1MDYiIGhlaWdodD0iNTA2IiByeD0iMTA5IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS1vcGFjaXR5PSIuMDciIHN0cm9rZS13aWR0aD0iMiIvPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgNCkiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDM2OCAzMjIpIHJvdGF0ZSg0NSkiPjx1c2UgeGxpbms6aHJlZj0iI2wiIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNsaW5rKSIgc3Ryb2tlLXdpZHRoPSIzMCIvPjwvZz48ZyBtYXNrPSJ1cmwoI2N1dCkiPjx1c2UgeGxpbms6aHJlZj0iI2giIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNoYWxvKSIgc3Ryb2tlLXdpZHRoPSI2NCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjx1c2UgeGxpbms6aHJlZj0iI2giIGZpbGw9Im5vbmUiIHN0cm9rZT0idXJsKCNoZWFydCkiIHN0cm9rZS13aWR0aD0iNDIiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzY4IDMyMikgcm90YXRlKDQ1KSI+PGcgY2xpcC1wYXRoPSJ1cmwoI292ZXIpIj48dXNlIHhsaW5rOmhyZWY9IiNsIiBmaWxsPSJub25lIiBzdHJva2U9InVybCgjbGluaykiIHN0cm9rZS13aWR0aD0iMzAiLz48L2c+PC9nPjwvZz48L3N2Zz4=
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addValueChangeListener
// @grant        unsafeWindow
// @grant        GM_registerMenuCommand
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js
// @connect      bongacams.com
// @connect      bongacams2.com
// @connect      bongocams.com
// @connect      webcamsluts.ru
// @connect      stripchat.com
// @connect      chaturbate.com
// @connect      mmcdn.com
// @connect      doppiocdn.com
// @connect      doppiocdn.media
// @connect      doppiocdn.net
// @connect      doppiocdn.org
// @connect      bgicdn.com
// @connect      bcvcdn.com
// @connect      bestcam.tv
// @connect      camshowrecordings.com
// @connect      camshowrecord.net
// @connect      livecamsrip.com
// @connect      camdownload.net
// @connect      camshow.cc
// @connect      cdncontent.top
// @connect      pornotorrent.top
// @connect      pornolab.net
// @connect      simpcity.cr
// @connect      camsclips.com
// @noframes
// @run-at       document-start
// ==/UserScript==

(function () {
'use strict';

const realHidden = () => {
    try { return Object.getOwnPropertyDescriptor(Document.prototype, 'hidden').get.call(document); } catch { return document.hidden; }
};
const BG_SUBS = { visibilitychange: [], pagehide: [] };
const onPageEvent = (type, fn, target = document) => {
    target.addEventListener(type, fn, true);
    BG_SUBS[type].push(fn);
};

let streamUrl = null, streamPath = '';
const previewDirs = new Set();
const hlsDir = u => { try { const x = new URL(u); return x.origin + x.pathname.slice(0, x.pathname.lastIndexOf('/') + 1); } catch { return ''; } };
const hlsIsVariantOf = (u, master) => { const d = hlsDir(u), m = hlsDir(master); return !!d && !!m && d.length > m.length && d.startsWith(m); };
window.addEventListener('bc_hls', e => {
    const u = e.detail;
    for (const d of previewDirs) if (String(u).startsWith(d)) return;
    if (streamUrl && streamPath === location.pathname && hlsIsVariantOf(u, streamUrl)) return;
    streamUrl = u; streamPath = location.pathname;
});
const getStreamUrl = () => (streamUrl && streamPath === location.pathname) ? streamUrl : null;

const blockLimits = () => {
    const s = document.createElement('script');
    s.textContent = `try{const bcHost=/bongacams|bongocams|webcamsluts/.test(location.hostname);if(bcHost){if('serviceWorker' in navigator)navigator.serviceWorker.register=()=>new Promise(()=>{});window.redirectOnLimit=()=>{}}const emit=u=>{try{u=String(u);if(u.includes('.m3u8')&&!u.includes('chunk'))window.dispatchEvent(new CustomEvent('bc_hls',{detail:new URL(u,location.href).href}))}catch{}};const o=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(m,u){emit(u);return o.apply(this,arguments)};const f=window.fetch;if(f)window.fetch=function(i){try{emit(i&&i.url?i.url:i)}catch{}return f.apply(this,arguments)};if(bcHost){const s=Storage.prototype.setItem,r=/guestTime|ls\\.tft|limit|freeTime|timeLimit/i;Storage.prototype.setItem=function(k,v){if(!r.test(k))s.apply(this,arguments)};const c=Object.getOwnPropertyDescriptor(Document.prototype,'cookie')||Object.getOwnPropertyDescriptor(HTMLDocument.prototype,'cookie');if(c?.set){const re=/^\\s*(limit\\w*|guest[_-]?time\\w*|free[_-]?time\\w*|time[_-]?limit\\w*)\\s*=/i;Object.defineProperty(document,'cookie',{get:()=>c.get.call(document),set:v=>{if(!re.test(v.trim()))c.set.call(document,v)},configurable:true})}}if(bcHost&&window.WebSocket&&!window.__bcWS){window.__bcWS=1;var pend={},myId=2e9,live=[],last=0,chatWs=null,os=WebSocket.prototype.send;var fire=function(r){try{var u=+r.totalUsers||0,g=+r.totalGuests||0;last=Date.now();window.dispatchEvent(new CustomEvent('bc_viewers',{detail:JSON.stringify({n:u+g,u:u,g:g,t:last})}))}catch(x){}};WebSocket.prototype.send=function(d){try{if(typeof d==='string'&&d.indexOf('ChatModule.')>0){var j=JSON.parse(d);if(j&&typeof j.name==='string'&&j.name.indexOf('ChatModule.')===0){chatWs=this;if(j.name==='ChatModule.syncUserList')pend[j.id]=1}}}catch(x){}return os.apply(this,arguments)};var hook=function(ws){live.push(new WeakRef(ws));ws.addEventListener('message',function(e){try{var d=e.data;if(typeof d!=='string')return;if(d.indexOf('TOPIC_CHANGE')>0){var q=JSON.parse(d);if(q&&q.type==='ServerMessageEvent:TOPIC_CHANGE'&&q.body&&typeof q.body.topic==='string')window.dispatchEvent(new CustomEvent('bc_topic',{detail:JSON.stringify({t:q.body.topic})}));return}if(d.indexOf('totalUsers')<0)return;var j=JSON.parse(d);if(!j||j.id==null||!j.result||j.result.totalUsers==null)return;if(pend[j.id]){delete pend[j.id];fire(j.result)}else if(j.id>=2e9){e.stopImmediatePropagation();fire(j.result)}}catch(x){}})};window.WebSocket=new Proxy(window.WebSocket,{construct:function(t,a,nt){var ws=Reflect.construct(t,a,nt);hook(ws);return ws}});setInterval(function(){try{if(Date.now()-last<45000)return;var w=chatWs;if(!w||w.readyState!==1)return;os.call(w,JSON.stringify({id:++myId,name:'ChatModule.syncUserList',args:['public-chat']}))}catch(x){}},15000)}}catch{}`;
    (document.head || document.documentElement).appendChild(s);
    s.remove();
};
blockLimits();

const installMediaHook = () => {
    const s = document.createElement('script');
    s.textContent = `(()=>{try{if(window.__bcMH)return;window.__bcMH=1;
var refs=[],ctxs=[],dead=false;
var reg=function(e){if(!e)return e;if(refs.length>=300)refs=refs.filter(function(r){return r.deref()});if(refs.length<300&&refs.every(function(r){return r.deref()!==e}))refs.push(new WeakRef(e));return e};
var P=HTMLMediaElement.prototype,op=P.play;
P.play=function(){reg(this);if(dead){try{this.pause()}catch(x){}return Promise.resolve()}return op.apply(this,arguments)};
var kk=false;
['src','srcObject'].forEach(function(n){var d=Object.getOwnPropertyDescriptor(P,n);if(d&&d.set){var st=d.set;Object.defineProperty(P,n,{configurable:true,enumerable:d.enumerable,get:d.get,set:function(v){if(dead&&!kk)return;return st.call(this,v)}})}});
var sa=Element.prototype.setAttribute;
Element.prototype.setAttribute=function(n){if(dead&&this instanceof HTMLMediaElement&&String(n).toLowerCase()==='src')return;return sa.apply(this,arguments)};
var ce=Document.prototype.createElement;
Document.prototype.createElement=function(t){var e=ce.apply(this,arguments);if(/^(video|audio)$/i.test(String(t)))reg(e);return e};
var OA=window.Audio;if(OA){var NA=function(){var e=arguments.length?new OA(arguments[0]):new OA();reg(e);return e};NA.prototype=OA.prototype;window.Audio=NA}
var A=window.AudioContext||window.webkitAudioContext;
if(A){var R=A.prototype.resume;A.prototype.resume=function(){if(ctxs.indexOf(this)<0)ctxs.push(this);if(dead)return Promise.resolve();return R.apply(this,arguments)};
['createMediaElementSource','createMediaStreamSource','createBufferSource'].forEach(function(n){var f=A.prototype[n];if(f)A.prototype[n]=function(){if(ctxs.indexOf(this)<0)ctxs.push(this);return f.apply(this,arguments)}})}
var kill=function(){dead=true;kk=true;refs.forEach(function(r){var e=r.deref();if(!e)return;try{e.pause();var m=e.srcObject;if(m&&m.getTracks)m.getTracks().forEach(function(t){t.stop()});e.srcObject=null;e.removeAttribute('src');e.load();e.remove()}catch(x){}});ctxs.forEach(function(c){try{c.suspend()}catch(x){}});kk=false};
window.addEventListener('bc_sleep_kill',kill)
}catch(e){}})();`;
    (document.head || document.documentElement).appendChild(s);
    s.remove();
};
installMediaHook();

const COUNT_DIGIT_BASES = [0x660, 0x6F0, 0x966, 0xE50, 0xFF10];
const COUNT_SUF = {
    k: 1e3, 'к': 1e3, 'тыс': 1e3, 'тис': 1e3, 'tys': 1e3, 'tis': 1e3, 'mil': 1e3, '千': 1e3, '천': 1e3,
    m: 1e6, 'м': 1e6, 'млн': 1e6, 'mln': 1e6, 'mio': 1e6,
    '万': 1e4, '萬': 1e4, '만': 1e4
};
function parseCount(v) {
    const t = String(v == null ? '' : v).toLowerCase().replace(/\p{Nd}/gu, ch => {
        const c = ch.codePointAt(0);
        for (const b of COUNT_DIGIT_BASES) if (c >= b && c < b + 10) return String(c - b);
        return ch;
    });
    const m = t.match(/(\d{1,3}(?:[\s\u00a0\u202f\u066c.,'’]\d{3})+(?!\d)|\d+(?:[.,]\d+)?)\s?(\p{L}+)?/u);
    if (!m) return NaN;
    const raw = m[1], word = (m[2] || '').replace(/\.$/, '');
    let mul = COUNT_SUF[word];
    if (!mul && word && '万萬만千천'.includes(word[0])) mul = COUNT_SUF[word[0]];
    const grouped = /^\d{1,3}([\s\u00a0\u202f\u066c.,'’]\d{3})+$/.test(raw);
    let n;
    if (mul) n = parseFloat(raw.replace(/[\s\u00a0\u202f'’]/g, '').replace(',', '.')) * mul;
    else if (grouped) n = parseInt(raw.replace(/\D/g, ''), 10);
    else n = parseFloat(raw.replace(',', '.'));
    return Number.isFinite(n) ? Math.round(n) : NaN;
}
function fmtCount(n) {
    n = Math.floor(+n);
    if (!Number.isFinite(n) || n < 0) return '';
    const lang = String(AppState.lang || 'EN').toUpperCase();
    let div = 1, suf = '';
    const cjk = { JA: ['万', '億'], ZH: ['万', '亿'], KO: ['만', '억'] }[lang];
    if (cjk) {
        if (n >= 1e8) { div = 1e8; suf = cjk[1]; } else if (n >= 1e4) { div = 1e4; suf = cjk[0]; }
    } else {
        const cyr = lang === 'RU' || lang === 'UK' || lang === 'BG';
        if (n >= 1e6) { div = 1e6; suf = cyr ? 'М' : 'M'; } else if (n >= 1e3) { div = 1e3; suf = cyr ? 'к' : 'k'; }
    }
    if (!suf) return String(n);
    const v = Math.floor(n / div * 10) / 10;
    if (v >= 100) return Math.floor(v) + suf;
    let s;
    try { s = v.toLocaleString(lang.toLowerCase() + '-u-nu-latn', { maximumFractionDigits: 1 }); } catch { s = String(v); }
    return s + suf;
}
const STORE_SITE = (() => {
    const h = location.hostname;
    return /bongacams|bongocams|webcamsluts/.test(h) ? 'bc' : /stripchat/.test(h) ? 'sc' : /chaturbate/.test(h) ? 'cb' : h;
})();
const SHARED_KEY = /^(bc_bookmarks|bc_hist|bc_bm_|bc_cal_|bc_plat$|bc_lang|bc_bk_parts$|bc_mig_|bc_previews$|bc_prev_spd$|bc_favnav$)/;
const siteKeyOf = k => (SHARED_KEY.test(k) ? k : `${STORE_SITE}:${k}`);
const Store = {
    key: siteKeyOf,
    get: (k, def) => {
        try {
            const sk = siteKeyOf(k);
            let v = GM_getValue(sk);
            if (v == null && sk !== k) v = GM_getValue(k);
            if (v == null) return def;
            const p = JSON.parse(v);
            return (Array.isArray(def) && !Array.isArray(p)) || p === null ? def : p;
        } catch { return def; }
    },
    set: (k, v) => {
        try {
            const sk = siteKeyOf(k);
            const str = JSON.stringify(v);
            GM_setValue(sk, str);
            try { const back = GM_getValue(sk); if (back != null && back !== str) return false; } catch {}
            return true;
        } catch { return false; }
    }
};

const clamp = (v, min, max) => Math.max(min, Math.min(v, max));
let bcPtrDown = false;
['pointerdown'].forEach(t => window.addEventListener(t, () => { bcPtrDown = true; }, { capture: true, passive: true }));
['pointerup', 'pointercancel'].forEach(t => window.addEventListener(t, () => { bcPtrDown = false; }, { capture: true, passive: true }));
const isObj = o => !!o && typeof o === 'object';
const axisK = (grow, shrink, size, d) => (grow ? (size + d) / size : shrink ? (size - d) / size : null);
const isPlaying = v => !!v && !v.paused && !v.ended && v.readyState > 2;
const escHTML = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
const safeDecode = s => { try { return decodeURIComponent(s); } catch { return s; } };

const gmReq = o => new Promise(resolve => {
    const { method = 'GET', url, data, headers, type = 'text', timeout = 6000, signal, credentials, viaFetch } = o;
    let tm = 0;
    const onAbort = () => { try { req?.abort?.(); } catch {} try { ctrl?.abort(); } catch {} done(0, null, 'abort'); };
    const done = (status, body, fail = '') => {
        clearTimeout(tm);
        signal?.removeEventListener('abort', onAbort);
        resolve({ status, body, fail });
    };
    let req, ctrl;
    if (signal?.aborted) return done(0, null, 'abort');
    if (!viaFetch && typeof GM_xmlhttpRequest === 'function') {
        try {
            req = GM_xmlhttpRequest({
                method, url, headers: headers || {}, timeout,
                ...(data !== undefined && { data }),
                ...(type !== 'text' && { responseType: type }),
                onload: r => done(r.status, type === 'text' ? (r.responseText || '') : r.response),
                onerror: () => done(0, null, 'error'),
                ontimeout: () => done(0, null, 'timeout')
            });
            signal?.addEventListener('abort', onAbort, { once: true });
        } catch { done(0, null, 'error'); }
        return;
    }
    ctrl = new AbortController();
    let timedOut = false;
    if (timeout > 0) tm = setTimeout(() => { timedOut = true; ctrl.abort(); }, timeout);
    signal?.addEventListener('abort', onAbort, { once: true });
    fetch(url, {
        method, cache: 'no-store', signal: ctrl.signal,
        ...(headers && { headers }), ...(data !== undefined && { body: data }), ...(credentials && { credentials })
    })
        .then(async r => done(r.status, type === 'arraybuffer' ? await r.arrayBuffer() : type === 'blob' ? await r.blob() : await r.text()))
        .catch(() => done(0, null, timedOut ? 'timeout' : 'error'));
});
const bytesOf = r => (r.status >= 200 && r.status < 300 && r.body ? new Uint8Array(r.body) : null);
const gmBytes = async (url, referer, gmFirst, range) => {
    const hasGm = typeof GM_xmlhttpRequest === 'function';
    const rh = range ? { Range: `bytes=${range.o}-${range.o + range.n - 1}` } : null;
    const viaFetch = () => gmReq({ url, type: 'arraybuffer', timeout: 0, viaFetch: true, ...(rh && { headers: rh }) });
    const viaGm = () => gmReq({ url, type: 'arraybuffer', timeout: 20000, headers: { Referer: referer || `${location.origin}/`, ...rh } });
    const gone = r => r.status === 404 || r.status === 410;
    if (gmFirst) {
        if (!hasGm) return bytesOf(await viaFetch());
        const g = await viaGm();
        return bytesOf(g) || (gone(g) || g.fail === 'timeout' ? null : bytesOf(await viaFetch()));
    }
    const f = await viaFetch();
    return bytesOf(f) || (gone(f) || !hasGm ? null : bytesOf(await viaGm()));
};

const MINI_SHIFT_X = 168, MINI_SHIFT_Y = 5;
const BM_HOST = /(^|\.)(bongacams2?\.com|bongocams\.com|webcamsluts\.ru|stripchat\.com|chaturbate\.com)$/;
const isSafeUrl = u => { try { const x = new URL(u); return x.protocol === 'https:' && BM_HOST.test(x.hostname); } catch { return false; } };
const BC_RE = /bongacams|bongocams|webcamsluts/;
const siteKey = h => BC_RE.test(h) ? 'bc' : /stripchat/.test(h) ? 'sc' : /chaturbate/.test(h) ? 'cb' : h;
const urlSite = u => { try { return siteKey(new URL(u).hostname); } catch { return ''; } };
const SITE = siteKey(location.hostname), IS_BC = SITE === 'bc';
const toHttps = u => {
    u = typeof u === 'string' ? u : '';
    if (u.startsWith('//')) u = `https:${u}`;
    return u.startsWith('https://') ? u : '';
};
const isStr = v => typeof v === 'string';
const clip = (v, n) => isStr(v) ? v.slice(0, n) : '';
const validBm = b => !!b && isStr(b.name) && isStr(b.url) && !!b.name.trim() && isSafeUrl(b.url);
const cleanBm = b => {
    if (!validBm(b)) return null;
    const out = {
        name: clip(b.name, 100),
        url: normUrl(b.url),
        avatar: toHttps(b.avatar),
        note: clip(b.note, 500),
        g: clip(isStr(b.g) && b.g.trim(), 40),
        seen: Number.isFinite(b.seen) && b.seen > 0 ? b.seen : 0
    };
    return Object.assign(out, cleanBmExtras(b));
};
const cleanBmExtras = b => {
    const x = {};
    if (b.pin === true) x.pin = true;
    if (Array.isArray(b.ptags)) {
        const keep = [];
        b.ptags.forEach((t, i) => { const c = clip(t, 40); if (c && keep.length < 30) keep.push([c, b.ptic?.[i]]); });
        x.ptags = keep.map(p => p[0]);
        if (keep.some(p => p[1])) x.ptic = keep.map(p => (isStr(p[1]) && /^[a-z0-9_\/-]{1,60}$/i.test(p[1]) ? p[1] : ''));
    }
    if (Array.isArray(b.sch) && b.sch.length === 7) x.sch = b.sch.map(s => clip(s, 60));
    return x;
};
const HASHTAG_RE = /#[\p{L}\p{M}\p{N}_]+/giu;
const noteHashTags = b => b.note?.match(HASHTAG_RE) || [];
const bmTagMatch = (b, tag) => !tag || (tag[0] === '#' ? noteHashTags(b).some(t => t.toLowerCase() === tag) : (b.ptags || []).some(t => t.toLowerCase() === tag));
const countInto = (m, x) => { const k = x.toLowerCase(); m.set(k, (m.get(k) || 0) + 1); };
const buildModalShell = (shadow, modalCls, bodyCls = 'bm-list') => {
    const overlay = mk('div', 'bm-overlay');
    const modal = mk('div', modalCls);
    const hdr = mk('div', 'bm-hdr');
    const title = mk('span', 'bm-title');
    const closeBtn = mk('button', 'bm-close');
    closeBtn.innerHTML = '×';
    hdr.append(title, closeBtn);
    const body = mk('div', bodyCls);
    modal.append(hdr, body);
    overlay.appendChild(modal);
    shadow.appendChild(overlay);
    return { overlay, modal, title, closeBtn, body };
};

const bmTbtn = (html, active, onClick, title = '') => {
    const btn = mk('button', `bm-tbtn${active ? ' on' : ''}`);
    btn.innerHTML = html;
    btn.title = title;
    btn.onclick = onClick;
    return btn;
};
const RESORT_SORTS = new Set(['online', 'last', 'viewers']);
const stripBm = ({ _updateItemDOM, _topicExp, _lastImg, _lastData, _chk, _isUpdating, _avFix, _avK, _live, _mob, _pf, _sv, label, ...rest }) => rest;

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

const HIDE_DECL = 'display:none!important;opacity:0!important;pointer-events:none!important;height:0!important;margin:0!important;padding:0!important';

const HIDE_CATS = [
    { id: 'hdr_m', plat: 'm', def: true, sel: ['.bc-header', '.bc-header-main', '.h_container', '#mainbar_container', '.join_button_container', '.js-chat_join_button', '[data-role="header"]'] },
    { id: 'hdr_d', plat: 'd', def: false, sel: ['.js-page_header', '.js-header_bar'] },
    { id: 'tabs', plat: 'm', def: true, sel: ['.live_tabs', '#mls_filter_result_container', '.bc_alternate_header_live_tabs'] },
    { id: 'seotitle', plat: 'b', def: true, sel: ['.mls_title'] },
    { id: 'chat_tips', plat: 'b', def: false, sel: ['.history .system_msg.tip_success', '.system_msg.tip_success'] },
    { id: 'chat_topic', plat: 'b', def: false, sel: ['.history .system_msg:has(.bc_notice.__topic)', '.bc_notice.__topic'] },
    { id: 'chat_boost', plat: 'b', def: false, sel: ['.history [class*="boost"]', '.history [class*="Boost"]'] },
    { id: 'ftr', plat: 'b', def: true, sel: ['.bc-footer', '#footer_container', '.bcm_footer', '.bcm_socials', '.m_socials', 'footer.footer', '.footer_copy', '.footer_seo', '.footer_mobile_guide', '#jsx-ls_seo_custom_text', '#jsx-ls_footer_container'] },
    { id: 'promo', plat: 'b', def: true, sel: ['.get_app_banner', '.js-get_app_banner', '.gab_wrp', '.banner_wrp', '.js-index_banner', '.sb_topmodel', '.free_tokens_panel', '#top_banner_above_chat', '.banner_top_chat', '.hb_item.__promo', '.js-free_tokens', '.hb_contest_item', '.holiday_discount_alert', '.js-top_banner', 'a[rel="buy_tokens"].holiday_discount_alert', '.stream_join_btn', '.join_button_container', '.bc_popalert.__give_away', '.bc_popalert.__member_bonus', '.stream_social_links', '.fancybox-overlay', '.fp_outer', '.fp_outer_overlay', '[class*="promo-banner"]', '[class*="age-gate"]', '[class*="age_gate"]', '[class*="cookie-banner"]', '[class*="notification-bar"]'] },
    { id: 'lastview', plat: 'b', def: false, sel: ['.lst_bage.__last_viewed'] },
    { id: 'joinpop', plat: 'b', def: false, sel: ['#member_join_popup', '.member_join_popup', '.fp_outer', '.fp_outer_overlay', '.fp_inner_wrap', '*:has(> .fp_inner_wrap)'] },
    { id: 'tips', plat: 'b', def: true, sel: ['#firstChatTooltipContainer', '.chat_bar_alert_container', '.chat_alert_container', '#chat_prompt_container', '#alerts_above_chat', '.js-bottom_alerts_area', '#bFontSizeTipHolder'] },
    { id: 'modelchat', plat: 'b', def: true, sel: ['[data-bc-mpop]'] },
    { id: 'games', plat: 'm', def: true, sel: ['#games_button'] },
    { id: 'popups', plat: 'm', def: false, sel: ['#popupChatSelection', '#popupGroup', '#popupVoyeur', '#gamecontrol_popup_container', '.ui-popup-container', '.cam2cam_modal'] },
    { id: 'actions', plat: 'b', def: false, sel: ['#chat_actions_bar', '.bcm_chat_footer', '#private_btn_cell', '.cf_cell.__private', 'a.cf_item.__tip', 'button.cf_item.__tip_menu', '.private_btn_wrp', '#bPaidButtonTopRow', '#bPaidButtonGroup', '.paid_button_group', '#bGotoGroup', '#bGotoPrivate', '#bGotoVoyeur', '#bTip'] },
    { id: 'balance', plat: 'd', def: false, sel: ['.memberBalance', '.memberNavigation'] },
    { id: 'chathdr', plat: 'b', def: false, sel: ['#id-chat-header', '.bc_chat_header'] },
    { id: 'tipking', plat: 'b', def: false, sel: ['.tip_king_wrp', '.bc_new_tip_modal'] },
    { id: 'below', plat: 'd', def: false, sel: ['.stream-controls'] },
    { id: 'wm', plat: 'd', def: false, sel: ['#layover-logo', '.layover-logo'] },
    { id: 'age18', plat: 'b', def: true, sel: ['#popup_18_plus', '#popup_18_plus-screen', '#popup_18_plus-popup'] }
];
HIDE_CATS.forEach(c => { c.site = 'bc'; });
const SC_CATS = [
    { id: 'sc_hdr', def: true, sel: ['.header-top-wrapper'] },
    { id: 'sc_sub', def: false, sel: ['.view-cam-header-sub'] },
    { id: 'sc_promo', def: true, sel: ['[class*="SidebarBanners"]', '.tokens-giveaway-banner', '.tokens-giveaway-banner-container', '.player-join-button', '.player-controls-user__join-btn'] },
    { id: 'sc_cookie', def: true, sel: ['[class*="CookieReminder"]', '.cookies-banner', '[data-testid="CookiesReminder"]'] },
    { id: 'sc_footer', def: true, sel: ['#MAIN_FOOTER_ID', '[class*="footer-container"]', '[class*="Footer__inner"]'] },
    { id: 'sc_widgets', def: true, sel: ['.plugin-panel', '.viewcam-widget-panel'] },
    { id: 'sc_goal', def: false, sel: ['.view-cam-info-goal', '.epic-goal-progress__wrap'] },
    { id: 'sc_topic', def: false, sel: ['.view-cam-info-topic'] },
    { id: 'sc_king', def: false, sel: ['[data-testid="king-wrapper-is-king"]', '[data-testid="view-cam-info-king-content"]'] },
    { id: 'sc_wm', def: false, sel: ['[class*="Watermark"]', '.watermark'] },
    { id: 'sc_about', def: false, sel: ['[data-testid="viewcam-model-sections"]'] }
];
const CB_CATS = [
    { id: 'cb_hdr', def: false, sel: ['.MobileHeader', '#header-links'] },
    { id: 'cb_alerts', def: true, sel: ['.LanguageMismatchAlert', '.dismissible_notice', '.MobileDismissibleMessages', '#ie-support-notice'] },
    { id: 'cb_footer', def: true, sel: ['#footer', '#mobile-social-media-icons'] },
    { id: 'cb_sort', def: false, sel: ['.MobileSort', '#mobile-sort-container', '#mobile-sort-form-container'] },
    { id: 'cb_tags', def: false, sel: ['.TopTagsBanner'] },
    { id: 'cb_tips', def: false, sel: ['.roomNotice.isTip'] },
    { id: 'cb_status', def: false, sel: ['.roomStatusNotifier'] },
    { id: 'cb_actions', def: false, sel: ['.sendTipButton', '.tipForm'] },
    { id: 'cb_info', def: false, sel: ['.HeaderRoomInfo'] }
];
SC_CATS.forEach(c => HIDE_CATS.push({ ...c, site: 'sc', plat: 'b' }));
CB_CATS.forEach(c => HIDE_CATS.push({ ...c, site: 'cb', plat: 'b' }));

const HIDE_TEXT = {
    hdr_m:   ['Верх сайта',              'Логотип, меню, кнопки входа',                 'Site top bar',          'Logo, menu, login buttons'],
    hdr_d:   ['Шапка сайта (ПК)',        'Верхняя панель на компьютере',                'Site header (PC)',      'Top bar on desktop'],
    seotitle:['Заголовок-реклама',       'Строка «Безкоштовний секс в онлайн порно-чаті» над списком', 'Ad headline', 'The "Free sex in online porn chat" line above the list'],
    chat_tips:['Чаевые в чате',          'Сообщения «подарил токены», «Меню чаевых», тип-меню', 'Tips in chat', '\"Tipped N tokens\" and tip-menu messages'],
    chat_topic:['Тема комнаты',          'Длинное описание «Тема: …» в начале чата',     'Room topic',            'Long \"Topic: …\" block at the top of chat'],
    chat_boost:['Бусты в чате',          'Плашки «поднял модель на N секунд» (FAN BOOSTED)', 'Boosts in chat',   '\"Boosted the model\" cards'],
    tabs:    ['Вкладки и сортировка',    'Девушки/пары, фильтры, значок сортировки',    'Tabs & sorting',        'Category tabs, filters, sort icon'],
    ftr:     ['Низ страницы',            'Подвал, соцсети, SEO-текст',                  'Page bottom',           'Footer, socials, SEO text'],
    promo:   ['Реклама и баннеры',       'Баннеры, промо, плашка «Присоединяйся» на плеере, приложение',          'Ads & banners',         'Banners, promos, "Join me" strip on the player, get-the-app'],
    lastview:['Плашка «Продолжить просмотр»', 'Кнопка на превью последней модели',     '"Continue watching" badge', 'Button on the last viewed thumbnail'],
    joinpop: ['Окно регистрации',        'Всплывающая форма «Создайте аккаунт»',        'Sign-up popup',         '"Create an account" popup'],
    tips:    ['Подсказки в чате',        'Всплывающие подсказки и уведомления',         'Chat hints',            'Tooltips and alerts'],
    modelchat:['Всплывающий чат модели', 'Розовое окошко «Привет! Любишь виртуальный секс?» внизу', 'Model popup chat', 'Pink pop-up with the model\'s auto-greeting at the bottom'],
    games:   ['Кнопка «Игры»',           'Иконка геймпада в красной панели',            '"Games" button',        'Gamepad icon in the red bar'],
    popups:  ['Всплывающие окна',        'Выбор чата, группа, подглядывание',           'Popups',                'Chat choice, group, voyeur'],
    actions: ['Кнопки «Приват / Чаевые»','Синие (на ПК зелёные) кнопки под красной панелью',            '"Private / Tips" buttons','Blue (green on PC) buttons under the red bar'],
    balance: ['Баланс и меню (ПК)',      'Баланс и навигация на компьютере',            'Balance & menu (PC)',   'Balance and navigation on desktop'],
    chathdr: ['Плашка «Чат с …»',        'Строка «Бесплатный чат с …» и король чаевых', 'Title bar "Chat with …"','"Free chat with …" strip and tip king'],
    tipking: ['Король чаевых',           'Строка короля и окна чаевых',                 'Tip king',              'Tip king strip and tip popups'],
    below:   ['Панель под плеером (ПК)', 'Блок кнопок под видео',                       'Under-player bar (PC)', 'Button row under the video'],
    wm:      ['Водяной знак (ПК)',       'Логотип поверх видео',                        'Watermark (PC)',        'Logo over the video'],
    age18:   ['Окно «Старше 18»',        'Заставка «Заходи ТОЛЬКО если ты уже старше 18»', '18+ notice',          'The "18+ only" entry screen']
};
Object.assign(HIDE_TEXT, {
    sc_hdr:    ['Верх сайта',            'Шапка с логотипом, поиском и кнопками входа',   'Site top bar',     'Header with logo, search and login buttons'],
    sc_sub:    ['Строка модели',         'Аватар, подписка и меню над плеером',           'Model bar',        'Avatar, follow and menu row above the player'],
    sc_promo:  ['Реклама и баннеры',     'Розыгрыши токенов, кнопка «Присоединиться» на плеере', 'Ads & banners', 'Token giveaways, Join button on the player'],
    sc_cookie: ['Плашка cookies',        'Уведомление о cookie внизу',                    'Cookie notice',    'Cookie reminder at the bottom'],
    sc_footer: ['Низ страницы',          'Подвал и юридический текст',                    'Page bottom',      'Footer and legal text'],
    sc_widgets:['Виджеты на плеере',     'Опросы и реакции поверх видео',                 'Player widgets',   'Polls and reactions over the video'],
    sc_goal:   ['Цель (гол)',            'Полоса прогресса цели',                         'Goal bar',         'Goal progress bar'],
    sc_topic:  ['Тема комнаты',          'Текст темы над и внутри плеера',                'Room topic',       'Topic text over and inside the player'],
    sc_king:   ['Король чаевых',         'Плашка короля чаевых',                          'Tip king',         'Tip king strip'],
    sc_wm:     ['Водяной знак',          'Логотип поверх видео',                          'Watermark',        'Logo over the video'],
    sc_about:  ['Блоки под плеером',     'Обо мне, расписание, похожие модели',           'Below the player', 'About, schedule, similar models'],
    cb_hdr:    ['Верх сайта',            'Шапка с логотипом и меню',                      'Site top bar',     'Header with logo and menu'],
    cb_alerts: ['Уведомления',           'Предупреждение о языке и закрываемые заметки',   'Alerts',           'Language alert and dismissible notes'],
    cb_footer: ['Низ страницы',          'Подвал и соцсети',                              'Page bottom',      'Footer and socials'],
    cb_sort:   ['Сортировка',            'Панель сортировки в мобильной версии',          'Sorting',          'Mobile sorting panel'],
    cb_tags:   ['Полоса тегов',          'Популярные теги над списком',                   'Tag strip',        'Top tags above the list'],
    cb_tips:   ['Чаевые в чате',         'Сообщения о токенах и чаевых',                  'Tips in chat',     'Tip and token messages'],
    cb_status: ['Статус комнаты',        'Плашка про оффлайн и приват с подсказками',     'Room status',      'Offline and private notifier'],
    cb_actions:['Кнопка чаевых',         'Кнопка и форма отправки токенов',               'Tip button',       'Send-tip button and form'],
    cb_info:   ['Название комнаты',      'Имя и счётчик зрителей в шапке',                'Room info',        'Name and viewers in the header']
});
HIDE_CATS.forEach(c => { const t = HIDE_TEXT[c.id]; if (t) { c.ru = t[0]; c.dru = t[1]; c.en = t[2]; c.den = t[3]; } });

const HIDE_PRESETS = {
    min: ['promo', 'joinpop', 'tips', 'modelchat', 'popups', 'seotitle', 'sc_promo', 'sc_cookie', 'sc_widgets', 'cb_alerts'],
    clean: HIDE_CATS.filter(c => c.site === 'bc' ? !/^chat_/.test(c.id) : c.def).map(c => c.id)
};

const catOn = (c, cfg) => cfg[c.id] === undefined ? c.def : !!cfg[c.id];
const detectPlatform = () => document.body?.classList.contains('ui-mobile-viewport') ? 'm' : 'd';
const customAll = () => { const o = Store.get('bc_hide_custom', {}); return (o && typeof o === 'object' && !Array.isArray(o)) ? o : {}; };
const getCustom = () => { const a = customAll()[siteKey(location.hostname)]; return Array.isArray(a) ? a.filter(x => typeof x === 'string') : []; };
const setCustom = list => { const o = customAll(); o[siteKey(location.hostname)] = list; Store.set('bc_hide_custom', o); };
const addCustom = sel => { const l = getCustom(); if (!l.includes(sel)) { l.push(sel); setCustom(l); } };
const removeCustom = sel => setCustom(getCustom().filter(x => x !== sel));

const buildHideCSS = skipId => {
    const out = [];
    {
        const cfg = Store.get('bc_hide_cfg', {});
        const hdr = HIDE_CATS.find(x => x.id === 'hdr_m');
        if (IS_BC && hdr && !catOn(hdr, cfg)) out.push('.bc-header,.bc_header.bc-header-main,.bc-header__fixed,.bc-header.hidden,.bc-header__fixed.hidden{display:flex!important;visibility:visible!important;opacity:1!important;transform:none!important;pointer-events:auto!important;position:fixed!important;top:0!important;z-index:1000!important}.bc-header.hidden:before,.bc-header.hidden .ui-title,.bc-header.hidden .lbtn,.bc-header.hidden .rbtn,.bc-header.hidden .ui-title.logo{transform:none!important}');
        const scope = { m: 'body.ui-mobile-viewport ', d: 'body:not(.ui-mobile-viewport) ' };
        HIDE_CATS.forEach(c => {
            if (c.site !== SITE || c.id === skipId || !catOn(c, cfg)) return;
            const pre = c.site === 'bc' ? (scope[c.plat] || '') : '';
            out.push(`${c.sel.map(x => pre + x).join(',')}{${HIDE_DECL}}`);
        });
    }
    getCustom().forEach(x => out.push(`${x}{${HIDE_DECL}}`));
    return out.join('\n');
};
const applyHideCSS = skipId => {
    let el = document.getElementById('bc-unchained-hide-css');
    if (!el) {
        el = document.createElement('style');
        el.id = 'bc-unchained-hide-css';
        (document.head || document.documentElement).appendChild(el);
    }
    el.textContent = buildHideCSS(typeof skipId === 'string' ? skipId : undefined);
};
const ensureHideCSS = () => { if (!document.getElementById('bc-unchained-hide-css')) applyHideCSS(); };

const cssEsc = x => (window.CSS && CSS.escape) ? CSS.escape(x) : String(x).replace(/[^\w-]/g, '\\$&');
const stableCls = c => c.length < 40 && !/^(ui-|__|bcu-|bc-(?:theater|thtr|unlock))/.test(c) && !/\d{3,}/.test(c) && !/^(active|open|opened|hidden|visible|selected|current|hover|focus|loaded|loading|playing|disabled|on|off|hd)$/i.test(c);
const selUniq = x => { try { return document.querySelectorAll(x).length === 1; } catch { return false; } };
const pathSelector = el => {
    const parts = [];
    for (let cur = el; cur && cur.nodeType === 1 && cur !== document.documentElement; cur = cur.parentElement) {
        if (cur.id && selUniq(`#${cssEsc(cur.id)}`)) { parts.unshift(`#${cssEsc(cur.id)}`); break; }
        const par = cur.parentElement;
        const idx = par ? [...par.children].indexOf(cur) + 1 : 1;
        parts.unshift(`${cur.tagName.toLowerCase()}:nth-child(${idx})`);
        if (cur === document.body) break;
    }
    return parts.join(' > ');
};
const isPickable = n => n?.nodeType === 1 && n !== document.body && n !== document.documentElement;
const idSelector = c => {
    if (!c.id || /\d{3,}/.test(c.id)) return '';
    const s = `#${cssEsc(c.id)}`;
    return selUniq(s) ? s : '';
};
const classSelector = c => c.tagName.toLowerCase() + [...c.classList].filter(stableCls).slice(0, 2).map(x => `.${cssEsc(x)}`).join('');
const buildSelector = el => {
    const parts = [];
    let cur = el;
    for (let i = 0; i < 6 && isPickable(cur); i++, cur = cur.parentElement) {
        const id = idSelector(cur);
        if (id) {
            parts.unshift(id);
            const full = parts.join(' > ');
            return selUniq(full) ? full : pathSelector(el);
        }
        parts.unshift(classSelector(cur));
        const sel = parts.join(' > ');
        if (selUniq(sel)) return sel;
    }
    return pathSelector(el);
};

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

const firstSegment = () => location.pathname.split('/').filter(Boolean)[0] || '';
const slugName = (reserved, valid) => () => {
    const f = firstSegment();
    return f && !reserved.test(f) && valid.test(f) ? f : '';
};
const BC_SYS = /^(?:ru|uk|ua|en|es|de|fr|pt|it|pl|tr|nl|cs|ro|hu|bg|el|ja|zh|ko|he|ar|hi|id|vi|th|tags?|categor(?:y|ies)|free-cams|all|search|girls|couples?|male|men|guys|female|trans|transsexual|new|top|models|favou?rites|live|cams|blog|help|support|settings|terms|privacy|faq|contacts|login|signup|register|apps|mobile|profile)$/i;
const bcDisplayName = () => {
    const d = Helpers.getModelSecretData();
    return d?.chatHost?.displayName
        || document.querySelector('#bChatRoomTitle, .chat-header-inner h1 a')?.textContent?.trim() || '';
};
const bcFallbackName = () => {
    const d = Helpers.getModelSecretData();
    return bcDisplayName() || d?.chatHost?.username || '';
};

const SC_SYS = /^(?:girls|men|guys|couples|trans|tags?|favorites|favourites|login|signup|search|blog|cams|explore|top|api|help|support|terms|privacy|models|profile|settings|videos|photos|pricing|studios|category|categories|new|vr|contest|announcement|user|guests|latest|about|contact|dmca|2257|tokens|messages|notifications|affiliates|shop|rewards|community)$/i;
const CB_SYS = /^(?:b|tags?|.+-cams|accounts|auth|tipping|supporter|affiliates|api|contest|apps|security|terms|privacy|dmca|support|billing|photo_videos|store|login|signup|register|search|explore|hosting|location|followed|follow|female|male|couple|trans|featured|2257|tokens|purchase|help|about|press|contact|sitemap|faq|studios|business|broadcast|messages|notifications|settings|cams|chatbot)$/i;
const FS_CSS = '@media (min-width:1024px){*:fullscreen video,*:-webkit-full-screen video,video:fullscreen,video:-webkit-full-screen{object-fit:contain!important;width:100%!important;height:100%!important;max-width:100vw!important;max-height:100vh!important;margin:auto!important}}';

const currentSite = (() => {
    const hostname = window.location.hostname;
    const configs = {
        bongacams: {
            match: BC_RE,
            videoSelectors: BC_CONTAINERS.map(s => `${s} video`).concat('video'),
            containerSelectors: BC_CONTAINERS,
            getName: () => {
                const [first = '', second = ''] = location.pathname.split('/').filter(Boolean);
                const raw = !BC_SYS.test(first) ? first : /^profile$/i.test(first) ? second : '';
                return bcDisplayName() || safeDecode(raw) || bcFallbackName();
            },
            css: BC_CSS
        },
        stripchat: {
            match: /stripchat|strpst/,
            videoSelectors: ['video.video-element', '.player video', '.web-rtc-player video', 'video'],
            containerSelectors: ['.player-wrapper', '[class*="ViewCamPlayer"]', '.web-rtc-player-wrapper', '.video-element-wrapper'],
            getName: slugName(SC_SYS, /^[\w.-]+$/),
            css: FS_CSS
        },
        chaturbate: {
            match: /chaturbate/,
            videoSelectors: ['#chat-player_html5_api', '#VideoPanel video', '.videoPlayerDiv video', '.video-js video', 'video'],
            containerSelectors: ['#VideoPanel', '.videoPlayerDiv', '#chat-player', '.video-js', '#video_container'],
            getName: slugName(CB_SYS, /^\w+$/),
            css: FS_CSS
        }
    };
    return Object.values(configs).find(c => c.match.test(hostname)) || configs.bongacams;
})();

const LANGS = ['RU', 'EN', 'UK', 'ES', 'DE', 'FR', 'PT', 'IT', 'PL', 'TR', 'NL', 'CS', 'RO', 'HU', 'BG', 'EL', 'JA', 'ZH', 'KO', 'HE', 'AR', 'HI', 'ID', 'VI', 'TH'];
const RTL_LANGS = ['HE', 'AR'];
const LANG_NAMES = { RU: 'Русский', EN: 'English', UK: 'Українська', ES: 'Español', DE: 'Deutsch', FR: 'Français', PT: 'Português', IT: 'Italiano', PL: 'Polski', TR: 'Türkçe', NL: 'Nederlands', CS: 'Čeština', RO: 'Română', HU: 'Magyar', BG: 'Български', EL: 'Ελληνικά', JA: '日本語', ZH: '中文', KO: '한국어', HE: 'עברית', AR: 'العربية', HI: 'हिन्दी', ID: 'Bahasa Indonesia', VI: 'Tiếng Việt', TH: 'ไทย' };
const detectLang = () => {
    let list = [];
    try { list = [...(navigator.languages?.length ? navigator.languages : [navigator.language]), document.documentElement?.lang]; } catch {}
    for (const c of list) {
        const k = String(c || '').slice(0, 2).toUpperCase().replace('IW', 'HE');
        if (LANGS.includes(k)) return k;
    }
    return 'EN';
};
const savedLang = Store.get('bc_lang', null);
const langAuto0 = Store.get('bc_lang_auto', savedLang == null) === true;

const SKIN_THEME = { modern: 'rgb(52, 107, 241)', classic: 'rgba(220, 20, 60, 0.9)' };
let skinModern = Store.get('bc_skin', 'classic') === 'modern';
const SK = (modern, classic) => (skinModern ? modern : classic);
let skinSheets = [];
const skinCss = (el, base, skin) => {
    skinSheets.push({ el, base, skin });
    el.textContent = base + (skinModern ? skin : '');
};
const Skin = {
    get modern() { return skinModern; },
    get name() { return skinModern ? 'modern' : 'classic'; },
    set(v) {
        const m = v === 'modern';
        if (m === skinModern) return false;
        skinModern = m;
        Store.set('bc_skin', m ? 'modern' : 'classic');
        skinSheets = skinSheets.filter(s => s.el.isConnected);
        skinSheets.forEach(s => { s.el.textContent = s.base + (m ? s.skin : ''); });
        try {
            const o = document.getElementById('bc-unchained-host')?.shadowRoot?.getElementById('bc-dark-opaque');
            if (o) o.textContent = darkOpaqueCss();
        } catch {}
        return true;
    }
};

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', SKIN_THEME[skinModern ? 'modern' : 'classic']),
    langAuto: langAuto0,
    lang: langAuto0 ? detectLang() : (LANGS.includes(savedLang) ? savedLang : 'RU'),
    gmute: Store.get('bc_gmute', false) === true,
    bookmarks: Store.get('bc_bookmarks', []).filter(isObj),
    dark: Store.get('bc_dark', false) === true
};
const FavNav = {
    get on() { return Store.get('bc_favnav', false) === true; },
    set(v) { Store.set('bc_favnav', !!v); this.apply(); },
    apply() { try { this.hook && this.hook(this.on); } catch {} }
};

const bmTomb = new Set();
const bmPersisted = new Set();
const BM_BAK = 'bc_bookmarks_bak';
let bmSaveFailed = false, bmDirty = false, bmNeedSave = false, bmRetryT = 0, bmRetries = 0, bmPersistedInit = false, bmBakAt = 0, bmBakLen = -1;
const bmRuntimeKey = k => k[0] === '_' || k === 'label';
const bmEnsurePersisted = () => {
    if (bmPersistedInit) return;
    bmPersistedInit = true;
    try {
        const a = Store.get('bc_bookmarks', []);
        if (Array.isArray(a)) for (const r of a) if (validBm(r)) bmPersisted.add(bmKey(r.url));
    } catch {}
};
const saveBookmarks = () => {
    try {
        const have = new Set(AppState.bookmarks.filter(b => isObj(b) && isStr(b.url)).map(b => bmKey(b.url)));
        const stored = Store.get('bc_bookmarks', []);
        if (Array.isArray(stored)) for (const r of stored) {
            if (!validBm(r)) continue;
            const k = bmKey(r.url);
            if (have.has(k) || bmTomb.has(k)) continue;
            have.add(k);
            AppState.bookmarks.push({ ...stripBm(r) });
        }
    } catch {}
    const out = AppState.bookmarks.filter(isObj).map(stripBm);
    bmSaveFailed = !Store.set('bc_bookmarks', out);
    if (!bmSaveFailed) {
        bmTomb.clear();
        bmDirty = false; bmRetries = 0;
        bmPersistedInit = true;
        bmPersisted.clear();
        for (const r of out) if (isStr(r.url)) bmPersisted.add(bmKey(r.url));
        if (out.length && (out.length !== bmBakLen || Date.now() - bmBakAt > 60000)) { Store.set(BM_BAK, out); bmBakLen = out.length; bmBakAt = Date.now(); }
    } else if (!bmRetryT && bmRetries < 3) {
        bmRetries++;
        bmRetryT = setTimeout(() => { bmRetryT = 0; saveBookmarks(); }, 2500);
    }
};
const incomingBookmarks = arr => {
    const m = new Map();
    for (const r of arr) if (validBm(r)) m.set(bmKey(r.url), r);
    return m;
};
const BM_LIVE = ['status', 'viewers', 'frame', 'topic'];
const mergeInto = (b, src, skip) => {
    for (const f of Object.keys(b)) if (!bmRuntimeKey(f) && !BM_LIVE.includes(f) && !(f in src) && !skip.includes(f)) delete b[f];
    for (const f of Object.keys(src)) if (!skip.includes(f)) b[f] = src[f];
};
const mergeLocalBookmark = (b, incoming, seen, skip) => {
    if (!isStr(b.url)) return true;
    const k = bmKey(b.url), r = incoming.get(k);
    if (seen.has(k)) return false;
    if (!r && !validBm(b)) return false;
    if (!r && bmPersisted.has(k) && !bmSaveFailed) return false;
    seen.add(k);
    if (r) mergeInto(b, stripBm(r), skip); else bmNeedSave = true;
    return true;
};
const mergeRemoteBookmarks = (arr, skip = []) => {
    if (!Array.isArray(arr)) return false;
    bmEnsurePersisted();
    bmNeedSave = false;
    const incoming = incomingBookmarks(arr), out = [], seen = new Set();
    for (const b of AppState.bookmarks) {
        if (isObj(b) && mergeLocalBookmark(b, incoming, seen, skip)) out.push(b);
    }
    incoming.forEach((r, k) => { bmPersisted.add(k); if (!seen.has(k)) out.push({ ...stripBm(r) }); });
    AppState.bookmarks = out;
    if (bmNeedSave) { bmNeedSave = false; bmDirty = true; setTimeout(saveBookmarks, 0); }
    return true;
};
const syncBookmarks = () => {
    try { const a = Store.get('bc_bookmarks', null); if (Array.isArray(a)) mergeRemoteBookmarks(a); } catch {}
};
const bmDropHooks = [];
const dropBookmark = b => {
    const k = isStr(b.url) ? bmKey(b.url) : null;
    if (k) bmTomb.add(k);
    AppState.bookmarks = AppState.bookmarks.filter(x => x !== b && !(k && isObj(x) && isStr(x.url) && bmKey(x.url) === k));
    saveBookmarks();
    if (!AppState.bookmarks.length) { Store.set(BM_BAK, []); bmBakLen = -1; }
    if (k) for (const h of bmDropHooks) { try { h(k); } catch {} }
};
try {
    if (!AppState.bookmarks.length) {
        const bak = Store.get(BM_BAK, []);
        const good = Array.isArray(bak) ? bak.filter(validBm).map(stripBm) : [];
        if (good.length) { AppState.bookmarks = good; bmDirty = true; setTimeout(() => saveBookmarks(), 1500); }
    }
} catch {}
const flushBookmarks = () => { if (bmDirty) { try { saveBookmarks(); } catch {} } };
window.addEventListener('pagehide', flushBookmarks, { capture: true });
document.addEventListener('visibilitychange', () => {
    let hid; try { hid = typeof realHidden === 'function' ? realHidden() : document.hidden; } catch { hid = document.hidden; }
    if (hid) flushBookmarks();
}, true);
const setGMute = v => { AppState.gmute = !!v; Store.set('bc_gmute', !!v); };
const sameBm = (b, name) => String(b.name).toLowerCase() === String(name).toLowerCase() && urlSite(b.url) === SITE;

const mergeBookmarks = arr => {
    const seen = new Set(AppState.bookmarks.map(b => bmKey(b.url)));
    let added = 0;
    for (const raw of arr) {
        const b = cleanBm(raw);
        if (!b) continue;
        const k = bmKey(b.url);
        if (seen.has(k)) continue;
        seen.add(k);
        bmTomb.delete(k);
        AppState.bookmarks.push(b);
        added++;
    }
    saveBookmarks();
    return added;
};

const DARK_PLAYER = '#TheaterModePlayer,.video-js:not(#TheaterModePlayer .video-js,#basePlayer .video-js)';
const DARK_PLAYER_MEDIA = 'video,img,picture,canvas,iframe,[style*="background-image"]';
const DARK_FB_CSS = `html.bc-dark-fb{filter:invert(1) hue-rotate(180deg)!important;background:#fff}
html.bc-dark-fb :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}){filter:invert(1) hue-rotate(180deg)!important}
html.bc-dark-fb :is(${DARK_PLAYER}) :is(${DARK_PLAYER_MEDIA}){filter:none!important}
html.bc-dark-fb.bc-theater-html,html.bc-dark-fb.bc-theater-html :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}){filter:none!important}
html.bc-dark-fb.bc-fs-on,html.bc-dark-fb.bc-fs-on *,html.bc-dark-fb.bc-fs-on :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}){filter:none!important}
html.bc-dark-fb :fullscreen,html.bc-dark-fb :fullscreen *,html.bc-dark-fb :fullscreen :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}),html.bc-dark-fb :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}):fullscreen{filter:none!important}`;
const DARK_FB_CSS_WK = `html.bc-dark-fb :-webkit-full-screen,html.bc-dark-fb :-webkit-full-screen *,html.bc-dark-fb :-webkit-full-screen :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}),html.bc-dark-fb :is(video,img,picture,canvas,iframe,[style*="background-image"],#bc-unchained-host,${DARK_PLAYER}):-webkit-full-screen{filter:none!important}`;
const syncFsDark = () => {
    try { document.documentElement.classList.toggle('bc-fs-on', !!(document.fullscreenElement || document.webkitFullscreenElement)); } catch {}
};
document.addEventListener('fullscreenchange', syncFsDark, true);
document.addEventListener('webkitfullscreenchange', syncFsDark, true);
const DARK_M_CSS = () => `html.bc-dark-m{color-scheme:dark;background:#121216}
html^body,html^.ui-page,html^.ui-body-c,html^.ui-content,html^.ui-panel-content-wrap,html^.bc-content{background:#121216!important;color:#dedede;text-shadow:none}
html^#chat_bar_v2_container,html^.inputContainer,html^.video-panel-wrap{background:#18181c!important}
html^.history,html^.history p.msg_wrp,html^.history .msg,html^.history dfn:after{color:#dedede}
html^.history p:not(.model) .author_name{color:#e8e8ec}
html^.np_title,html^.nt_message_title,html^.bc_notice.__topic{color:#ee5b78}
^.cp_purchase_popup{--bc-thm-purchasePopup-bg:#121213;--bc-thm-purchasePopup-accent-color:#e17085;--bc-thm-purchasePopup-avatar-bg:#212124;--bc-thm-purchasePopup-desc-divider-bg:#212124;--bc-thm-purchasePopup-title-color:#bcbcbc;--bc-thm-purchasePopup-desc-color:#d4d4d4}
^.bc_confirm_popup_wrp{--bc-thm-confirmPopup_error-color:#f65a61;--bc-thm-confirmPopup_action_column-confirmBtn_decline-bg:#252527;--bc-thm-confirmPopup_action_column-confirmBtn_decline-bg-active:#323235;--bc-thm-confirmPopup_fanBoostWhat-bg:#121213;--bc-thm-confirmPopup_fanBoostWhat-color:#d8d8d8;--bc-thm-confirmPopup_fanBoostWhat-accent-color:#d67b87;--bc-thm-confirmPopup_fanBoostWhat-benefits-bg:#151517;--bc-thm-confirmPopup_fanBoostWhat-statuses-item-borderColor:#3b3b3f}
^.bc_confirm_popup.__confirmed_light{--bc-thm-confirmPopup_confirmLight-bg:#151516;--bc-thm-confirmPopup_confirmLight-color:#d8d8d8;--confirmPopup_profile_media-confirm-title-color:#e17085;--confirmPopup_profile_media-confirmBtn_decline-color:#cecece}
^.bc_confirm_popup_wrp.__vertical_orientation{--confirmPopup_vertical_orientation-color:#d8d8d8}
^.bc_confirm_popup.__qoq_contest{--bc-thm-confirmPopup_qoqContest-color:#f5f5f5}
^.bc_confirm_popup.__gift_preview{--confirmPopup_giftPreview-body-bg:#121213}
^.bc_confirm_popup.__private_close{--bcm-privateClose-title-color:#e17085}
^.mbc_btn{--bcm-btn-color:#cecece;--bcm-btn-bg:#252527;--bcm-btn-bg-active:#1d1d1f;--bcm-btn-bg-hover:#1b1b1d;--bcm-btn_secondary-color:#cecece;--bcm-btn_secondary-bg:#1c1c1e;--bcm-btn_secondary-bg-active:#323235;--bcm-btn_secondary-bg-hover:#1b1b1d;--bcm-btn_outline-borderColor:#3e3e43}
^.bc_static_page{--bcm-static-page-color:#d8d8d8;--bcm-static-page-striped-content-div-even-text-shadow-color:rgba(0,0,0,0);--bcm-static-page-striped-content-div-even-bg-color:#1a1a1c;--bcm-static-page-header-text-shadow-color:rgba(0,0,0,0);--bcm-static-page-header-bg-color:#1a1a1c}
^#mainbar{--bcm-mainbar-btn-color:#d8d8d8;--bcm-mainbar-btn-bg-active:#19191b;--bcm-mainbar-menuItemList-subItem-color:#e17085}
^#mainbar .holiday_discount{--bcm-sidebar-holidayDiscount-accent-color:#e17085}
^.bc_alternate_header_live_tabs{--alternateHeaderLiveTabs-base-color:#d8d8d8;--alternateHeaderLiveTabs-bg:#18181a;--alternateHeaderLiveTabs-scrollShadow-bg:linear-gradient(270deg, #18181a 0%, #f2f2f200 100%)}
^.bc_skeleton_item{--bcm-skeleton-item-bg-color:#18181a}
^.bc_skeleton_item:before{--bcm-skeleton-item-pseudo-bg-color:#121213}
^.bcm_alphabet_filter{--bcm-alphabetFilter-base-accent-color:#e17085;--bcm-alphabetFilter-base-borderColor:#404044;--bcm-alphabetFilter-dropdown-bg:#121213;--bcm-alphabetFilter-dropdown-item-color:#d8d8d8;--bcm-alphabetFilter-select-bg:#121213;--bcm-alphabetFilter-select-caption-color:#d8d8d8}
^.bcm_pagination{--bcm-pagination-item-color:#f3f3f3;--bcm-pagination-item-bg-color:#18181a;--bcm-pagination-item-bg-color-active:#1b1b1d;--bcm-pagination-item-border-color:#3e3e42}
^.bcm_fp_features{--bcm-filter-results-features-item-color:#f5f5f5;--bcm-filter-results-features-item-bg-color:#161618}
^.bcm_checkbox{--bcm-checkbox-mark-bg-color:#252527;--bcm-checkbox-mark-border-color:#4c4c52;--bcm-checkbox-mark-icon-fill:#bcbcbc;--bcm-checkbox-text-color:#bcbcbc;--bcm-checkbox-disabled-mark-bg-color:#1b1b1d;--bcm-checkbox-disabled-mark-border-color:#3c3c40;--bcm-checkbox-checked-text-color:#f5f5f5}
^.bcm_radio{--bcm-radio-icon-color:#e17085}
^.bcm_range{--bcm-range-values-color:#bcbcbc;--bcm-range-track-bg-color:#1a1a1c;--bcm-range-handle-bg-color:#121213;--bcm-range-handle-border-color:#45454a}
^.bcm_form_alert{--bcm-form-alert-error-color:#ff5252;--bcm-form-alert-error-bg-color:#19191b}
^.bcm_accordion{--bcm-accordion-title-color:#c1c1c1;--bcm-accordion-arrow-fill:#f5f5f5}
^.bc_nav_tabs_wrp{--navigationTabs-item-color:#d8d8d8;--navigationTabs-inner-bg:#18181a;--navigationTabs-item-borderColor:#38383c;--navigationTabs_tags-after-bg:linear-gradient(270deg, #18181a, #f2f2f200)}
^.bc_fade_scroll{--fadeScroll-fade-bg:linear-gradient(90deg, #18181a, rgba(24,24,26,0)}
^.bc_fade_scroll.__listing_tags{--fadeScroll-fade-bg:linear-gradient(90deg, #121213, rgba(18,18,19,0)}
^.smileysContainer{--mobile-smiles-panel-container-background-color:#121213;--mobile-smiles-panel-container-border-color:#4c4c52;--mobile-smiles-panel-close-wrapper-color:#e36e84;--mobile-smiles-panel-title-wrapper-color:#e2e2e2;--mobile-smiles-panel-title-shortcut-color:#c0c0c0;--mobile-smiles-panel-tab-color:#d2d2d2;--mobile-smiles-panel-tab-background-color:#151516;--mobile-smiles-panel-tab-background-color-hover:#121213;--mobile-smiles-panel-tab-border-color:#353539;--mobile-smiles-panel-tab-active-background-color:#121213}
^.bcm_socials{--bcm-thm-socials-counter-color:#bcbcbc;--bcm-thm-socials-counter-bg:#121213;--bcm-thm-socials-counter-borderColor:#45454a}
^.bcm_switcher{--bcm-switcher-bg:#222225}
^.bc_notice{--bcm-notice-color:#e6e6e6;--bcm-notice-bg:linear-gradient(#121213, #121213);--bcm-notice-close-iconColor-hover:#dedede;--bcm-notice_secondary-paragraph-color:#d8d8d8;--bcm-notice_membershipError-close-iconColor-hover:#a960f0;--bcm-notice_sexToy-avatar-boxShadowColor:rgba(0,0,0,0);--bc-thm-notice_attention-bg:#181819;--bc-thm-notice_attention-color:#a866eb;--bc-thm-notice_topic-bg:#161618;--bc-thm-notice_topic-color:#e17085}
^.bc_picture_stub{--bcm-thm-pictureStub-iconColor:linear-gradient(180deg, #e07184 0%, #e36d84 100%)}
^.bc_carousel_bar{--carouselBar-item_active-inner-bg:#121213}
^.mbc_header{--mob-header-menuBtn-shadow-color:rgba(0,0,0,0);--mob-header-menuBtn-bg:#121213}
^.mbc_header_menu{--mob-header-menu-border-color:#4c4c52;--mob-header-menu-item-bg:#141416;--mob-header-menu-action-color:#f5f5f5}
^.mbc_header_menu.__sorting{--mob-header-menu-item-border-color:#49494e}
^.bcm_footer{--bcm-thm-footer-bg:#18181a;--bcm-thm-footer-borderColor:#2b2b2e;--bcm-thm-footer-details-color:#d8d8d8;--bcm-thm-footer-details-nav-link-color:#d8d8d8;--bcm-thm-footer-details-nav-link-bg-active:#1d1d1f;--bcm-thm-copyright-color:#d8d8d8;--bcm-thm-copyright-link-color:#df7187}
^.mls_item{--bcm-thm-listingThumb-stub-bg:#161617;--bcm-thm-listingThumb-stub-img-bg:#1d1d1f}
^.mls_tag_badge{--bcm-listing-tagBadge-bg:#18181a;--bcm-listing-tagBadge-borderColor:#3e3e42;--bcm-listing-tagBadge-color:#f3f3f3}
^.lscl_item{--listing-category-item-color:#f3f3f3;--listing-category-item-bg-color:#1c1c1e;--listing-category-item-firstRow-color:#ececec;--listing-category-item-firstRow-bg:#18181a;--listing-category-item-firstRow-bg-active:#212124}
^.listing_age_verification_banner{--ageVerificationBanner-icon-color:linear-gradient(#e26e84, #e17084)}
^.mls_hash_tag{--mlsHashTag-tag-color:#ececec;--mlsHashTag-tag-bg:#18181a;--mlsHashTag-tag-bg-active:#212124;--mlsHashTag-tag_record-icon-color:#bdbdbd;--mlsHashTag-removeFilterLink-close-iconColor:#d4d4d4}
^.instant_buy{--bcm-instant-buy-bar-border-color:#4c4c52;--bcm-instant-buy-success-wrp-circle-border-color:#4c4c52;--bcm-instant-buy-success-wrp-block-bg:#121213;--bcm-instant-buy-menu-bg:#161617;--bcm-instant-buy-menu-block-bg-active:#1e1e20;--bcm-instant-buy-menu-item-color:#eeeeee;--bcm-instant-buy-menu-item-desc-color:#c5c5c5}
^.bc_chat_topic{--bcm-chatTopic-bg:#121213;--bcm-chatTopic-inner-bg:rgba(22,22,24,0.9);--bcm-chatTopic-content-color:#e6e6e6;--bcm-chatTopic-title-color:#e17085}
^.chat_notice{--bcm-chatNotice-color:#e6e6e6;--bcm-chatNotice-inner-bg:#121213;--bcm-chatNotice-link-color:#52a1ff;--bcm-chatNotice-author-name-color:#e6e6e6}
^.chat-panel .history .service_list{--sysMsg-serviceList-bg:#121213;--sysMsg-serviceList-authorName-color:#e6e6e6;--sysMsg-serviceList-title-color:#f5f5f5;--sysMsg-serviceList-services-item-bg-hover:#181819;--sysMsg-serviceList-item-title-color:#e6e6e6;--sysMsg-serviceList-showBtn-color:#d8d8d8;--sysMsg-serviceList-showBtn-bg:#121213;--sysMsg-serviceList-showBtn-borderColor:#38383c;--sysMsg-serviceList-showBtn-color-active:#f5f5f5;--sysMsg-serviceList-showBtn-borderColor-active:#49494e;--sysMsg-serviceList-showBtn-bg-active:#121213}
^.bcm_chat_footer{--bcm-chat-footer-item-border-color:#323235;--bcm-chat-footer-item_tipMenu_disabled-bg:#38383c}
^.bcm_history_scroll_up{--bcm-historyScrollUp-bg:#252527;--bcm-historyScrollUp-link-color:#e17085}
^.bcm_chat_auto_message{--bcm-chat-member-notification-borderColor:#45454a;--bcm-chat-member-notification-title-color:#e2e2e2}
^.video-panel-wrap{--videoPanelWrap-bg:#121213}
^.mbc_tabs{--bcm-tabs-item-color:#d8d8d8;--bcm-tabs-item-bg:#18181a;--bcm-tabs-item_selected-color:#d8d8d8;--bcm-tabs-item_selected-bg:#18181a}
^.bcm_nav_panel{--bcm-navPanel-bg:#18181a;--bcm-navPanel-item-color:#d8d8d8;--bcm-navPanel-item_active-color:#e36e84}
^.f_rptfmm .buy_tokens,^.f_rptsmm .buy_tokens{--bcm-base-buyTokens-color:#d8d8d8;--bcm-base-buyTokens-borderColor:#36363a;--bcm-base-buyTokens-error-color:#ff5272;--bcm-buyTokens-bg:#121213;--bcm-buyTokens-text_faded-color:#bcbcbc;--bcm-buyTokens-decline-text-bg:#161617;--bcm-buyTokens-paymentMethodCards-card-bankCounter-bg:#121213;--bcm-buyTokens-packages-checkbox_off-borderColor:#4a4a50;--bcm-buyTokens-packages-error-bg:#19191b;--bcm-buyTokens-confirmCode-title-accent-color:#e17085;--bcm-buyTokens-amountField-bg:#121213;--bcm-buyTokens-amountField_onlyLocalCurrenct-localCurrency-color:#d8d8d8;--bcm-buyTokens-choice-bg:#121213;--bcm-buyTokens-choice-iconRadioOff-bg:#313134;--bcm-buyTokens-select-iconWrp-bg:#131315;--bcm-buyTokens-stickyFooter-bg:#121213}
^.f_rptsmm .buy_tokens{--bcm-buyTokens-paymentMethodCards-moreBtn-iconWrp-bg:#161617;--bcm-buyTokens-morePaymentMethodsPopup-bg:#121213;--bcm-buyTokens-morePaymentMethodsPopup-listWrp-shadow-bg:linear-gradient(180deg, #161617 35%, rgba(22,22,23,0.8) 50%, #ebfff000 100%);--bcm-buyTokens-morePaymentMethodsPopup-item-bg:#161617;--bcm-buyTokens-morePaymentMethodsPopup-item_active-bg:#121213;--bcm-buyTokens-morePaymentMethodsPopup-item-bankCounter-bg:#121213}
^.bcm_popup{--bcm-popup-body-bg-color:#121213}
^.categories_popup{--bcm-popup-categories-main-bg-color:#121213;--bcm-popup-categories-list-wrp-border-color:#45454a;--bcm-popup-categories-item-icon-color:#e17085;--bcm-popup-categories-item-color:#d8d8d8;--bcm-popup-categories-item-bg-color-active:#1b1b1d;--bcm-popup-categories-item-counter-bg-active:#121213}
^.filter_popup{--bcm-popup-filter-main-bg:#121213;--bcm-popup-filter-features-title-color:#c1c1c1;--bcm-popup-filter-footer-borderColor:#45454a;--bcm-popup-filter-notice-color:#c1c1c1;--bcm-popup-filter-button-outline-bg:#2b2b2e;--bcm-popup-filter-button-outline-borderColor:#3f3f44}
^.search_popup{--bcm-popup-search-header-bg-color:#18181a}
^.mbc_search_block{--bcm-search-block-input-bg-color:#121213;--bcm-search-block-input-borderColor:#404044;--bcm-search-block-input-placeholder-color:#d8d8d8;--bcm-search-block-header-close-icon-color:#d8d8d8;--bcm-search-block-reset-button-bg-color:#1a1a1c;--bcm-search-block-reset-button-border-color:#45454a;--bcm-search-block-result-empty-color:#e17085;--bcm-search-block-result-recent-title-color:#e17085;--bcm-search-block-result-name-color:#f5f5f5;--bcm-search-block-result-border-color:#333337;--bcm-search-block-result-tag-color:#ececec;--bcm-search-block-result-tag-name-bg-color:#18181a;--bcm-search-block-result-tag-symbol-color:#e17085}
^#pop-sorting-popup{--bcm-thm-popSortingPopup-sortingItem-color:#f5f5f5}
^.categories{--bcm-categories-skeleton-bg:#151516}
^.contact_support_page{--bcm-contact-support-page-title-color:#e17085;--bcm-contact-support-page-text-color:#d4d4d4;--bcm-contact-support-page-form-label-color:#f5f5f5;--bcm-contact-support-page-item-border-color:#353539}
^.join{--bcm-join-base-color:#d8d8d8;--join-bg:#121213;--join_login-flashAlert-bg:#19191b;--join_login-redirect-bg:#212124;--join_login-redirect-borderColor:#45454a;--join_login-redirect-text-color:#d8d8d8;--join_login-redirect-text-shadowColor:rgba(0,0,0,0)}
^.join_form{--bcm-joinForm-color:#d8d8d8;--bcm-joinForm-termsOfUse-bg:#151516;--bcm-joinForm-termsOfUse-color:#cecece;--bcm-joinForm-termsOfUse-checkbox-borderColor:#45454a;--bcm-joinForm-termsOfUse-checkbox-bg:#121213;--bcm-joinForm-input-color:#f5f5f5;--bcm-joinForm-input-bg:#151516;--bcm-joinForm-input-borderColor:#4c4c52;--bcm-joinForm-formLink-color:#b6b6b6}
^.invisible_mode_overview{--bc-thm-invisibleModeOverview-content-color:#9099c1;--bc-thm-invisibleModeOverview-promoBlock-feature-bg:#151517;--bc-thm-invisibleModeOverview-switchBlock-box-bg:linear-gradient(180deg, #121213 0%, #141416 100%);--bc-thm-invisibleModeOverview-switchBlock-box-borderColor:#38383c;--bc-thm-invisibleModeOverview-switchBlock-title-color:#9099c1;--bc-thm-invisibleModeOverview-switcher-wrp-bg:linear-gradient(180deg, #b6bfd1 0%, #121213 100%)}
^.invisible_mode_overview_plank{--bc-thm-invisibleModeOverviewPlank-color:#cacaca;--bc-thm-invisibleModeOverviewPlank-bg:#151516}
^.im_conversations .imc_reply{--bcm-im-reply-bg:#121213;--bcm-im-reply-borderColor:#323235;--bcm-im-reply-quickSmiles-bg:#121213;--bcm-im-reply-quickSmiles-borderColor:#3d3d41}
^.im_conversations .imc_rf_textarea_wrapper{--bcm-im-replyForm-textareaWrapper-control-color:#c4c4c4;--bcm-im-replyForm-textareaWrapper-control-textShadowColor:rgba(0,0,0,0);--bcm-im-replyForm-textareaWrapper-placeholder-color:#c4c4c4}
^.im_conversations .imc_rf_textarea_error{--bcm-im-replyForm-textareaError-color:#ff5252}
^.im_conversations .imc_r_button{--bcm-im-replyButton_smilepicker-trigger-icon-stroke:#c4c4c4;--bcm-im-replyButton_smilepicker-trigger_active-icon-stroke:#f5f5f5}
^.im_conversations .imc_balance_restriction_alert{--im-balanceRestrictionAlert-color:#d4d4d4;--im-balanceRestrictionAlert-bg:#151516;--im-balanceRestrictionAlert-title-color:#e17085}
^.im_conversations .imc_kicked_alert{--im-kickedAlert-color:#d4d4d4;--im-kickedAlert-acent-color:#e17085;--im-kickedAlert-bg:#121213;--im-kickedAlert-borderColor:#45454a}
^.im_conversations .imc_notification{--im-notification-color:#e6e6e6;--im-notification_pink-accent-color:#ea67ce}
^.im_conversations .imch_contact_card{--im-contactCard-color:#d4d4d4;--im-contactCard-accent-color:#e17085;--im-contactCard-checkbox-color:#d8d8d8;--im-contactCard-bg-color:#121213;--im-contactCard-borderColor:#45454a}
^.imch_reactions{--im-reactions-item-bg:#121213;--im-reactions-item_own-bg:#161617;--im-reactions-item-focus-bg:#161617;--im-reactions-counter-color:#d2d2d2}
^.bc_switch_lang_popup{--bcm-thm-switchLangPopup-bg:#121213;--bcm-thm-switchLangPopup-close-color:#e17085;--bcm-thm-switchLangPopup-title-color:#e17085;--bcm-thm-switchLangPopup-noteText-color:#bdbdbd;--bcm-thm-switchLangPopup-note-bg:#141415;--bcm-thm-switchLangPopup-note-borderColor:#45454a;--bcm-thm-switchLangPopup-flagsGroup-itemCaption-color:#cecece}
^.mbc_alert{color:#d6d6d6;background:#121213}
^.mbc_alert.__info{color:#c9c9c9;background:#151516;border:1px solid #3f3f43}
^.mbc_alert.__warning{color:#ff5253;background:#171718}
^.bc_btn{color:#cecece;background:#252527}
^.bc_btn:active{background:#303033}
^.bc_btn.bc_btn_white{background:#121213;border:1px solid #3f3f44;color:#cecece !important}
^.bc_btn.bc_btn_white:active:not(.__disabled),^.bc_btn.bc_btn_white:hover:not(.__disabled){background:linear-gradient(#131315,#18181a)}
^.bc_btn.bc_btn_white.__disabled,^.bc_btn.bc_btn_blue.__disabled{background:#18181a;border-color:#36363a}
^.bc_btn.bc_btn_light{background:linear-gradient(#151516,#1a1a1c);border:1px solid #3f3f44;color:#cecece !important}
^.bc_btn.bc_btn_light:active:not(.__disabled),^.bc_btn.bc_btn_light:hover:not(.__disabled),^.bc_btn.bc_btn_light.__disabled{background:linear-gradient(#1a1a1c,#151516)}
^.bc_confirm_popup_wrp.__hint,^.bc_confirm_popup_wrp .bc_confirm_popup.__dialog .dlg_subTitle,^.bc_confirm_popup_wrp .account_deleted_popup,^.bc_confirm_popup_wrp .questions_popup,^.bc_confirm_popup_wrp .bc_confirm_popup.__paid_chat_hint .bc_cp_body,^.bc_confirm_popup_wrp .cancel_account_confirm,^.bc_confirm_popup_wrp .bc_confirm_popup.__auto_message_chat_hint .bc_cp_body,^.bcm_radio.__background .bcm_r_input:checked~.bcm_r_text,^.bcm_input_row,^.forward_to_remote,^.im_conversations .pm_confirm_email .pmc_body,^.im_conversations .pm_confirm_email .pmc_body .pmc_item,^.im_conversations .pm_offering .pmob_item,^.im_conversations .pm_offering .pmol_item{color:#d4d4d4}
^.bc_confirm_popup_wrp.__ask_again .bcc_header .bcc_title,^.bc_confirm_popup_wrp .bc_confirm_popup.__error .bcc_container_info,^.bc_confirm_popup_wrp .bcc_no_header_popup .nh_title,^.bc_confirm_popup_wrp .bc_confirm_popup.__dialog.__d_alert .dlg_title,^.bc_confirm_popup_wrp .account_deleted_popup .adp_title,^.bc_confirm_popup_wrp .questions_popup .qp_title,^.bc_confirm_popup_wrp .questions_popup .qp_accent,^.bc_confirm_popup_wrp .delete_msg .dm_accent,^.bc_confirm_popup_wrp .cancel_account_confirm .cac_title,^.bcm_input_row .bcm_i_label_required,^.mls_title .mlst_icon.__category,^.vip-payment .inner-vip .vip-blocks,^.suspect_login_page .slp_desc,^.bc_cmn_confirm_wrp.__danger .bc_confirm_popup .bcc_title,^.head_confirm_box .hcb_close_wrp .hcb_close,^.aChat .message.admin strong,^#admin_chat_popup .admin_info .details .label,^.buy_tokens h4 strong,^.verify_info_box p b,^.confirm_box .line b,^.confirm_box .note span,^.bt_first_step span,^.buy_tokens .discount_gift_card .new_price,^.loyalty_box.leaving .lb_blue,^.account_preview .acp_alert,^.forward_to_remote .ftr_highlight,^.imch_reply .imch_r_author,^.imc_footer_reply .imc_fr_username,^.imc_footer_reply .imc_fr_close_icon,^.im_conversations .pm_valentine_day_themed_gift .pmvdtgd_accent,^.cg_vibe_with_me .vwm_s_danger{color:#e17085}
^.bc_confirm_popup_wrp.__ask_again .cgr_label,^.bc_confirm_popup_wrp.__ask_again .cg_label,^.bc_confirm_popup_wrp.__ask_again .tknt_item,^.bc_confirmer .bcc_alert .ca_text,^.bcm_filter_result .fr_title,^.chat-panel .msg_chat_game .cg_msg_s_time,^.bc_cmn_confirm_wrp .bc_confirm_popup .bcc_title,^.confirm_box .example_box,^.cg_overlay .cg_options_list .ol_item .cgo_label,^.cg_overlay .cg_accordion .cga_item .ai_title,^.cg_overlay .cg_adj_empty_playlist .ep_message,^.cg_overlay .cg_adj_list .it_title,^.cg_overlay .cg_adj_notice,^.cg_overlay .cg_adj_title,^.cg_overlay .cg_select_subtitle .ss_chosen{color:#c1c1c1}
^.bc_confirm_popup_wrp.__ask_again .cgr_label .bc_capitalize,^.bc_confirm_popup_wrp.__ask_again .cg_label .bc_capitalize,^.bc_confirm_popup_wrp.__ask_again .tknt_item .bc_capitalize,^.bc_confirm_popup_wrp .bc_confirm_popup.__display_name,^.bc_confirm_popup_wrp .bcc_header .bcc_title,^.bc_confirm_popup_wrp .bc_confirm_popup.__change_displayname .bcm_input_row .bcm_form_input,^.mbc_menu .mbcm_item.__default,^.translator_settings .bcf_check,^#mainbar .mm_icon,^#mainbar.__lang_list_opened .change-lang>.change-lang-btn,^.bcm_radio .bcm_r_input:checked~.bcm_r_text,^.mls_container.is_history .mlshiw_icon,^.vip-payment .head-vip i,^.vip-payment .head-vip,^.buy-descr,^#bChat .member,^.history p.model,^.history dfn:after,^.bcm_chat_selection .bcm_cs_title,^.bcm_chat_selection .bcm_cs_arrow,^.chat-panel .history .system_msg.msg_activity .mat_accent.__member,^.chat-panel .msg_chat_game.__vibe_with_me .cg_vwm_text,^.chat-panel .msg_chat_game .gc_msg_label,^.ic_menu_wrp .iw_translator,^.ic_menu_wrp .iw_font_picker,^.bc_cmn_confirm_wrp .bc_confirm_popup .bcc_title .bc_capitalize,^.head_confirm_box .hcb_text,^.buy_tokens .buy_tokens_form p.form_notice,^.buy_tokens h4,^.processors .ui-btn-inner,^.buy_tokens .choice.discount_box .discount_sum,^.buy_tokens label.choice.discount_box .discount_sum,^.buy_tokens .bt_payment_title,^.buy_tokens .sp_current_package .spcp_local_currency,^.buy_invisible_package .choice_title,^.buy_invisible_package .choice_footer,^.bcm_popup .bcm_p_content,^.mbc_search_block .sb_input,^.mbc_search_block .sb_result .sbr_title,^.account_preview .acp_name,^.im_conversations .pm_confirm_email,^.smilePanelTabs .sm_t_label,^.cg_overlay .cg_options_list .ol_item .cgo_label .ol_accent,^.cg_overlay .cg_result .cgr_preview,^.cg_overlay .cg_accordion .cga_item .ai_arrow,^.cg_overlay .cg_adj_form .djf_title,^.cg_overlay .cg_adj_list .pll_item.__playing .it_title,^.cg_overlay .cg_select_subtitle,^.cg_overlay .cg_select_subtitle .ss_chosen .ssc_amount,^.cg_vibe_with_me.__vwm_success,^.cg_vibe_with_me.__vwm_success .vwm_s_desc,^.cg_vibe_with_me .vwm_text{color:#f5f5f5}
^.bc_confirm_popup_wrp .bc_confirm_popup,^.bc_confirm_popup_wrp .bc_confirm_popup.__dialog .bc_cp_body,^.mbc_fullscreen_popup,^.what_view .what_view_close:before,^.bongacams_popup_v2 .popup_close:before,^.mls .mls_pager_header .mls_header_content,^.footer .support_group li.ui-btn-up-c,^.padded.white,^.padded.stripe:nth-child(2n),^.vip-payment .inner-vip,^#conversations .notfound,^#bChatFullScreenHistory p,^#chat_bar_v2_container,^.chat_prompt_wrp .cp_msg .cp_msg_decline_close:after,^.chat_prompt_wrp .cp_msg .cp_msg_decline_close:before,^.tap_model_popup_container .tap_container_inner,^.fancybox-tip-popup-wrap .fancybox-inner,^.bc_cmn_confirm_wrp .bc_confirm_popup,^.promo_in_banner_popup .pibp_body,^#admin_chat_popup .admin_chat_popup_content,^.buy_tokens .section .box,^.buy_tokens .bt_anim_icon_success:after,^.profile_avatar_popup,^.ac_change_photo .ac_cp_inner,^.imc_footer,^.imc_footer_alerts,^.im_conversations .imc_avatar .imc_avatar_img_wrap.__moderator,^.cg_overlay .cg_body,^.cg_keno_cube.__selected,^.bc_token_keno_field.__win .tkf_cube:hover .fc_body,^.bc_token_keno_field .tkf_cube.__checked:hover .fc_body,^.bc_token_keno_field .tkf_cube.__checked .fc_body,^.cg_overlay .cg_prize_list.__keno .pl_item.__open{background-color:#121213}
^.bc_confirm_popup_wrp .bc_confirm_popup.__tip .bc_cp_body .bcf_check,^.bcm_radio.__background,^.bcm_checkbox_row .cr_label,^.cg_overlay .cg_input_wrp .cgi_label{background:#1c1c1e}
^.bc_confirm_popup_wrp .bc_confirm_popup.__tip .bc_cp_body .bcf_text,^.cg_overlay .cg_input_wrp .cgi_text{color:#cecece;text-shadow:0 1px transparent}
^.bc_confirm_popup_wrp .bc_confirm_popup.__tip .bc_cp_body .bcf_mark,^.cg_overlay .cg_input_wrp .cgi_mark{background:#2a2a2d}
^.bc_confirm_popup_wrp .bc_confirm_popup.__tip .bc_cp_body .bcf_mark:before,^.bc_confirm_popup_wrp .bc_confirm_popup.__buy_fb .bc_cp_close:before,^.bc_confirm_popup_wrp .bc_confirm_popup.__buy_fb .bc_cp_close:after,^.bc_confirm_popup_wrp .bc_confirm_popup.__thank .bc_cp_close:before,^.bc_confirm_popup_wrp .bc_confirm_popup.__thank .bc_cp_close:after,^.bc_confirm_popup_wrp .bc_confirm_popup.__feedback .bc_cp_close:before,^.bc_confirm_popup_wrp .bc_confirm_popup.__feedback .bc_cp_close:after,^.bc_confirm_popup_wrp .account_deleted_popup .bcc_content,^#mainbar .mm_icon.__free,^.bc_confirmer .bcc_alert,^.holiday_discount_alert .hda_perc,^.bcm_skeleton:before,^.bcm_toggle_switcher_holder .ts_switcher:before,^.bcm_switcher.__on .bcm_s_slider:before,^.bcm_switcher .bcm_s_slider:before,^.info_part .icon_membership.icon_free,^.close_icon_wr span:before,^.close_icon_wr span:after,^.chat_prompt_wrp .cp_msg,^.chat_tooltip .close-btn:before,^.chat_tooltip .close-btn:after,^.bcm_oops_popup_layover .opl_body,^.popup_c2c_no_access .c2c_na_content,^.popup_c2c_no_access .c2c_na_close:before,^.popup_c2c_no_access .c2c_na_close:after,^.head_confirm_box .hcb_links .input_wrapper .ui-input-text,^.head_confirm_box .hcb_links .input_wrapper .hcb_input.ui-input-text,^.phone_confirm_box,^.sp_dropdown,html.bc-dark-m,^.buy_tokens .packages_badge .pb_icon,^#page_loader_overlay,^.loyalty_box.__personal .lb_holder,^.buy_tokens_header_board,^.bt_select_modal .slm_item,^.fancybox-wrap.__shemale_gold_slider_wrap .fancybox-close:before,^.fancybox-wrap.__shemale_gold_slider_wrap .fancybox-close:after,^.mbc_search_block .sb_result,^.popHint,^.vip_overview_stock_page .spp_list .pl_item,^.invisible_mode_overview .imsw_switcher:before,^.page_im.__settings .im_conversations,^.im_conversations .imc_actions .mbc_btn,^.im_conversations .imc_empty .imce_icon_wrp,^.im_conversations .imci_wrap,^.im_conversations .imc_followers .imcf_item,^.im_conversations .imc_search.__active .search_input,^.im_conversations .imc_search .search_input:focus,^.imch_reply,^.pm_activation_2fa .pmaf_alert,^.im_conversations .pm_upgraded_membership.__diamond .pmuh_logo_plank,^.im_conversations .pm_downgrade_membership .dmh_logo_plank,^.notice_need_tokens.__alert,^.smilePanel.smilePanelMobile .smilePanelContent,^.cg_overlay .gc_btn.__white,^.cg_overlay .cg_input_wrp .cgi_mark:before,^.cg_overlay .cg_game_field.__dice:before,^.bc_token_keno_field.__win .tkf_cube .fc_body,^.cg_vibe_with_me.__vwm_app .vwm_ai_title:after{background:#121213}
^.bc_confirm_popup_wrp .bc_confirm_popup.__tip .bc_cp_body .bcf_control{color:#d8d8d8;border:1px solid #4c4c52}
^.bc_confirm_popup_wrp .bc_confirm_popup.__tip .bc_cp_body .form_error,^.bc_confirm_popup_wrp .bc_confirm_popup.__social_buy .bc_cp_body .form_error,^.form .form_errors_row .form_error,^#bChat .msg_wrp .ch_m_tr_error,^.confirm_code_form strong.hl,^.im_conversations .imc_history_box .imch_item.__translate.__error .imch_msg_translate,^.cg_overlay .cg_result .cgr_prize .rp_text{color:#ff5252}
^.bc_confirm_popup_wrp .bc_confirm_popup.__social_buy.__profile_lg .bc_cp_body .bcf_input_block{background-color:#151516;border-color:#4c4c52}
^.bc_confirm_popup_wrp .bc_confirm_popup.__social_buy .bc_cp_body .bcf_label_wrp,^.ui-btn-hover-c:visited,^.ui-btn-hover-c:hover,^.ui-btn-hover-c a.ui-link-inherit,^.ui-btn-down-c:visited,^.ui-btn-down-c:hover,^.ui-btn-down-c a.ui-link-inherit,^.ui-btn-active:visited,^.ui-btn-active:hover,^.ui-btn-active a.ui-link-inherit,^.what_view.__pin .what_view_inner,^.bcm_checkbox_row .cr_text,^.phone_confirm_box .hcb_text,^.password_changed_box,^.loyalty_box.speaker .lb_first,^.loyalty_box.leaving,^.mbc_search_block .sb_result .sbr_delete_icon,^.profile_avatar_popup .account_actions .mbc_btn.ac_a_btn_cancel,^.account_actions .mbc_btn.ac_a_btn_cancel{color:#cecece}
^.bc_confirm_popup_wrp .bc_confirm_popup.__social_buy .bc_cp_body .bcf_input_block,^.live_tabs .tabs_list .tabsl_item.ui-btn-active{background-color:#1c1c1e}
^.bc_confirm_popup_wrp .bc_confirm_popup.__error .bcc_button{color:#cecece;background-color:#1a1a1c}
^.bc_confirm_popup_wrp .bcc_actions{border-top:1px solid #49494e}
^.bc_confirm_popup_wrp .bcc_no_header_popup{color:#d4d4d4;background:#151516}
^.bc_confirm_popup .cp_signup_wrap{color:#d8d8d8;background-color:#212124;border-top:1px solid #45454a}
^.bc_confirm_popup .cp_signup_title,^.ui-bar-e,^.ui-body-e,^.ui-overlay-e,^.video-item .date,^.model_status .author,^.im_conversations .gift_card_promo_wrapper .gift_card_promo .gift_card_promo_code_wrapper .gift_card_promo_code_valid,^.im_conversations .gift_card_promo_wrapper .gift_card_promo .gift_card_promo_bonuses .gift_card_promo_bonus{text-shadow:0 1px transparent}
^.bc_confirm_popup_wrp .bc_confirm_popup.__dialog .dlg_title,^.bc_confirm_popup_wrp .bc_confirm_popup.__change_displayname .bcm_i_label_txt,^.ui-body-b .ui-link-inherit,^.ui-body-c .ui-link-inherit,^.ui-bar-e .ui-link-inherit,^#mainbar .menu_item .mi_list .ui-btn,^#mainbar .menu_item .mi_list .mil_item.__collapsed .mil_sub_item,^#mainbar .combobox-lang,^#mainbar .combobox-lang .mm_arrow,^#mainbar .home .mm_live,^.auto_purchase_form,^.input_wrapper.__field_password .fp_icon_wrp,^.mls_title,^.notfound p,^.chat-panel .history .system_msg.msg_activity,^.bc_cmn_confirm_wrp .bc_confirm_popup .bcc_content,^.bt_first_step,^.buy_tokens .buy_tokens_auto_purchase,^.loyalty_box.leaving .lb_second,^.f_rptfmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description .action_block,^.f_rptsmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description .action_block,^.f_rptfmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description .sdp_phone_title,^.f_rptsmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description .sdp_phone_title,^.buy_tokens .bt_decline .btd_desc,^.ac_change_photo,^.ac_change_photo .ac_cp_sub_title,^.im_conversations .imc_history_box .imch_item.__translate.__error,^.im_conversations .imc_history_box .imch_item.__tip .imch_msg_text,^.im_conversations .imc_history_box .imch_msg_translate,^.im_conversations .pm_valentine_day_themed_gift .pmvdtg_name,^.im_conversations .pm_valentine_day_themed_gift .pmvdtgd_text,^.cg_vibe_with_me.__vwm_app .vwm_a_qr_icon,^.cg_vibe_with_me.__vwm_app .vwm_a_qr_icon_wrp{color:#d8d8d8}
^.bc_confirm_popup_wrp .questions_popup .qp_title.__warn{color:#f06171}
^.bc_confirm_popup_wrp .questions_popup .qp_accent_info,^.mls_container.is_history .mls_history_icon_wrp,^.chat_user_menu .cum_content,^.buy_tokens_sms_dimoco_package .input_phone.__extended_description .description_block,^.buy_tokens .bt_extened_description_block,^.im_conversations .imc_history_box .imch_badge_unread,^.notice_need_tokens .nnt_icon_wrp{background:#161617}
^.bc_confirm_popup_wrp .questions_popup .qp_accent_info .ai_title{color:#5ff281}
^.bc_confirm_popup_wrp .questions_popup.__support .qp_title,^.ui-body-e .ui-link-inherit,^.what_view .what_view_inner .title,^.msg_style_panel .msp_subtitle,^.bcm_chat_invite.__decline .bcm_ci_badge_icon{color:#e2e2e2}
^.bc_confirm_popup_wrp .delete_msg .dm_text,^.seo_box h2,^.seo_box h3,^.mls_title .mlst_icon.__recordable,^.tap_model_popup_container .tip_after_private .tap_title_block{color:#bdbdbd}
^.bc_confirm_popup_wrp .bc_confirm_popup.__up_status .bc_cp_body,^.popup_c2c_no_access .c2c_na_text,^.buy_tokens_redirect .btr_text.__action,^.im_conversations .imc_actions,^.im_conversations .imc_actions .mbc_btn.__btn_msg.__active .imca_text.__new_msg,^.im_conversations .imc_actions .mbc_btn.__btn_msg:active .imca_text.__new_msg,^.im_conversations .imc_actions .imca_text.__online,^.empty_conversation .ec_inner,^.im_conversations .imc_search.__active .imcs_icon,^.im_conversations .imc_search .search_input:focus+.imcs_icon,^.notice_need_tokens .nnt_text{color:#d6d6d6}
^.bc_confirm_popup_wrp .bc_confirm_popup.__up_status .ups_desc{background-color:#141416}
^.bc_confirm_popup_wrp .bc_confirm_popup.__help .bcc_button{color:#cecece;background:linear-gradient(#151516,#1a1a1c);border:1px solid #3f3f44}
^.bc_confirm_popup_wrp .bc_confirm_popup.__pin_list .bc_cp_body,^.bc_online_notification .ont_avatar_wrp,^.bc_online_notification .ont_info_wrp,^.bc_context_menu_list .cml_item:active,^.bcm_skeleton .bcms_inner,^.bc_body.tablet.android .chat_panel_area:after,^.bcm_chat_selection .bcm_cs_link:hover,^.bcm_chat_selection .bcm_cs_link:active,^.cg_overlay .cg_footer{background:#18181a}
^.ui_floating_tooltip .ft_arrow{fill:rgba(245,245,245,0.8)}
^.mbc_menu{background-color:#121213;border:1px solid #38383c}
^.mbc_menu .mbcm_item{border-bottom:1px solid #38383c}
^.mbc_menu .mbcm_item:active,^.im_conversations .imc_history_box .imch_item.__selected,^.im_conversations .pm_upgraded_membership.__diamond .pmu_holder.__filled,^.im_conversations .pm_upgraded_membership.__diamond .pmu_footer{background:#1b1b1d}
^.ui-body-b,^.ui-overlay-b{color:#e2e2e2;text-shadow:0 1px transparent;background:#18181a;background-image:-moz-linear-gradient(#232325,#2b2b2e);background-image:-ms-linear-gradient(#232325, #2b2b2e);background-image:linear-gradient(#232325,#2b2b2e);border:1px solid #4c4c52}
^.ui-bar-c{color:#d2d2d2;text-shadow:0 1px transparent;background:#1a1a1c;background-image:-moz-linear-gradient(#19191b,#232325);background-image:-ms-linear-gradient(#19191b, #232325);background-image:linear-gradient(#19191b,#232325);border:1px solid #4c4c52}
^.ui-bar-c .ui-link-inherit{color:#d2d2d2}
^.ui-body-c,^.ui-overlay-c{color:#d8d8d8;text-shadow:0 1px transparent;background:#151516;background-image:-moz-linear-gradient(#151516,#1a1a1c);background-image:-ms-linear-gradient(#151516, #1a1a1c);background-image:linear-gradient(#151516,#1a1a1c);border:1px solid #4c4c52}
^.ui-btn-up-c{color:#cecece;background:#141416}
^.ui-btn-hover-c,^#mainbar .li_shadow.ui-btn-active,^.ui-btn-active,^#mainbar .ui-btn-up-c.ui-btn-active,^#mainbar .ui-btn-active.ui-btn-hover-c,^#mainbar .ui-btn-active:focus,^#mainbar .ui-btn-active:active,^#mainbar .ui-btn-active:hover,^#mainbar .ui-btn-active,^#mainbar a.join-btn.ui-btn-active,^#mainbar .ui-btn-up-c.ui-btn-active .btn_ico.ui-link-inherit,^.buy_tokens .processors_btn_disabled{color:#cecece;background:#1c1c1e}
^.ui-btn-down-c{text-shadow:0 1px transparent;color:#cecece;background:#303033}
^.ui-btn-up-c,^.ui-btn-hover-c,^.ui-btn-down-c{background-color:#1c1c1e;color:#cecece !important}
^.ui-btn-active.ui-btn-up-c span.ui-btn-up-c,^.ui-btn-active.ui-btn-up-c a.ui-link-inherit,^.ui-btn-active.ui-btn-up-c,^.ui-btn-active.ui-btn-up-c:visited,^.ui-btn-active.ui-btn-hover-c:hover,^.ui-btn-active.ui-btn-down-c a.ui-link-inherit{background:#1c1c1e;color:#cecece !important}
^.ui-checkbox .ui-btn-up-c,^.choice.ui-btn-up-c,^.ui-listview .ui-btn-up-c,^.ac_settings .ac_delete_link_wrp{color:#cecece;background:#141416;border:1px solid #45454a}
^.ui-collapsible-heading-toggle.ui-btn-up-c{color:#cecece;border:1px solid #45454a;background:#141416 !important}
^.ui-checkbox .ui-btn-hover-c,^.choice.ui-btn-hover-c,^.ui-listview .ui-btn-hover-c,^.ui-collapsible-heading-toggle.ui-btn-hover-c{color:#cecece;background-color:#1c1c1e;border:1px solid #45454a}
^.ui-body-d,^.ui-overlay-d,^#mainbar .bc_social_networks .sn_count,^.aChat,^#admin_chat_popup .admin_info .admin_avatar_wrp,^.loyalty_box,^.buy_tokens_sms_dimoco_package .input_phone,^.buy_tokens_sms_dimoco_package .input_phone.__extended_description .sdp_phone_input,^.buy_tokens .sp_input,^.im_conversations .pm_confirm_email .pmc_form,^.cg_overlay .cg_adj_list .djpl_list{border:1px solid #45454a}
^.ui-btn-up-e:visited,^.ui-btn-up-e a.ui-link-inherit,^.ui-btn-up-e,^.ui-btn-hover-e,^.ui-btn-down-e,^.ui-btn-active.ui-btn-up-e:visited,^.ui-btn-active.ui-btn-hover-e:hover,^.ui-btn-active.ui-btn-down-e a.ui-link-inherit{color:#feb953}
^.ui-icon-alt .ui-icon,^.ui-icon-alt .ui-icon-searchfield:after{background-color:rgba(18,18,19,0.3)}
^.buy_tokens label.selected,^.live_tabs .ui-btn-active.ui-btn-up-c .ui-btn-up-c,^.categories .ui-btn-active.ui-btn-up-c .ui-btn-up-c,^.footer .support_group li.ui-btn-up-c.support_message,^.im_conversations .imc_actions .bcf_selectbox:active,^.im_conversations .imc_actions .mbc_btn.__active,^.im_conversations .imc_actions .mbc_btn:active{background:#141416}
^.form div.ui-input-text input.ui-input-text,^.input_wrapper.__field_password .ui-input-text,^.bcm_input_row .bcm_form_input,^.bcm_input_row .bcm_form_textarea,^.tap_model_popup_container .tip_after_private .radio_list li .custom_price input,^.head_confirm_box .hcb_links .input_wrapper{border:1px solid #4c4c52}
^.what_view{color:#d4d4d4;background-color:#121213;border:1px solid #45454a}
^.what_view .what_view_inner{background-color:#151516}
^.what_view .what_view_corner{background:#121213;border-color:#0000 #0000 #45454a #45454a}
^.translator_settings .mbc_select{border:1px solid #3c3c40}
^.msg_style_panel .preview_content,^.mbc_search_block.__filter .sb_input,^.mbc_search_block.__filter .sb_result{border:1px solid #3f3f43}
^.msg_style_panel .msp_p_item.__selected,^.f_rptfmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description .sdp_phone_input,^.f_rptsmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description .sdp_phone_input{border-color:#3d3d41}
^.bongacams_popup_v2{background-color:#121213;border:1px solid #4c4c52}
^#mainbar,^#mainbar .ui-panel-inner,^body,^.ui-page,^.ui-content,^.ui-panel-content-wrap{background:#121213 !important}
^#mainbar .ui-listview .mc_followers .btn_icon .mc_icon_follower{fill:#d8d8d8;stroke:#d8d8d8}
^#mainbar .ui-li-divider,^.ic_menu_wrp .icm_item,^.buy_tokens_sms_form h3,^.buy_tokens_sms_sale_form h3,^.buy_tokens_wire_form h3,^.cg_overlay .cg_accordion .cga_item,^.cg_overlay .cg_adj_list.__no_btn .pll_item:last-child{border-bottom:1px solid #45454a}
^#mainbar .ui-li{border-color:#3e3e43}
^#mainbar .ui-btn-active,^#mainbar .mainbar-control.__end_position,^#bChat .historyContainer,^.buy_tokens_wire_first h4{border-top:1px solid #45454a}
^#mainbar .ui-btn-active.ui-btn-up-c .ui-link-inherit .mm_icon.__free{border-color:#47474c}
^#mainbar .btn_icon{border-top:1px solid #3e3e43}
^#mainbar .menu_item,^.footer_content .copy a{color:#f5f5f5 !important}
^#mainbar .menu_item.__collapsed,^.bc_header_live_tabs .hlt_link:visited,^.bc_header_live_tabs .hlt_link,^.im_conversations .pm_valentine_day_themed_gift .pmvdtg_text,^.im_conversations .pm_valentine_day_themed_gift .pmvdtg_signature{color:#d8d8d8 !important}
^#mainbar .menu_item .mi_list .mil_sub_item .mm_arrow,^.head_confirm_box .hcb_close_wrp.__bubble .hcb_close_icon{fill:#e17085}
^#mainbar .language-dropdown{background:#121213;border-top:1px solid #45454a}
^#mainbar .language-dropdown .change-lang-btn.__selected,^#mainbar .language-dropdown .change-lang-btn:active,^.im_conversations .pm_upgraded_membership.__sapphire .pmu_holder.__filled,^.im_conversations .pm_upgraded_membership.__sapphire .pmu_footer,^.im_conversations .pm_upgraded_membership.__titan .pmu_holder.__filled,^.im_conversations .pm_upgraded_membership.__titan .pmu_footer,^.im_conversations .pm_upgraded_membership .pmu_holder.__filled,^.im_conversations .pm_upgraded_membership .pmu_footer{background:#19191b}
^.bc_online_notification .ont_close_wrp .ont_close,^.bc_online_notification .ont_info .ont_i_text,^.chat-content .bcm_paid_show_countdown .bcm_psc_text,^.bcm_chat_footer .cf_item,^.bcm_chat_footer .cf_item:hover .cfi_inner,^.buy_tokens .bt_cashback_nft_wrp,^.buy_tokens_sms_dimoco_package .input_phone.__extended_description,^.buy_tokens_sms_dimoco_package .input_phone.__extended_description .sdp_phone_title,^#buy_tokens_sms_confirm_code_form_tele2 .cct_form,^.buy_tokens .sp_label{color:#c4c4c4}
^.bc_confirmer .bcc_alert .caa_button,^.bc_cmn_confirm_wrp .bc_confirm_popup .bcc_actions .bcc_button.__light{color:#cecece;background:#1a1a1c}
^.bc_header_live_tabs .hlt_inner,^.smileButton.ui-btn-hover-e,^.tap_model_popup_container .tip_after_private .form_actions,^.forward_to_remote .ftr_header{background-color:#18181a}
^.holiday_discount_alert.__world_sexual_health_day_2026 .hda_buy{color:#596ef8}
^.bc_ms_icon.__diamond{fill:#f65d5b}
^.bc_ms_icon.__exclusive{fill:#efefef}
^.bc_ms_icon.__sapphire{fill:#9452ff}
^.bc_ms_icon.__white_bg.__diamond{color:#f65d5b}
^.bc_ms_icon.__white_bg.__exclusive{color:#efefef}
^.bc_ms_icon.__white_bg.__sapphire{color:#9452ff}
^.bc_context_menu_list,^.im_conversations .imc_actions .bcf_selectbox .bcsb_dropdown,^.bcsb_dropdown.__in_portal,^.im_conversations .imc_history_box .imch_item.__action_btns .imch_msg_actions{background:#121213;border:1px solid #38383c}
^.bc_context_menu_list .cml_item,^.im_conversations .imc_actions .bcf_selectbox .bcsb_item,^.bcsb_dropdown.__in_portal .bcsb_item{color:#f5f5f5;border-bottom:1px solid #38383c}
^.auto_purchase_form .bcf_control{background:#151516;border:1px solid #4c4c52}
^.bc_dropdown.__reactions .mbcm_item+.mbcm_item{border-left:1px solid #38383c}
^.bcm_radio .bcm_r_input:disabled~.bcm_r_mark{background:#1b1b1d;border-color:#3c3c40}
^.bcm_radio .bcm_r_input:disabled~.bcm_r_mark:before,^.bcm_toggle_switcher_holder .ts_switcher{background:#222225}
^.bcm_radio .bcm_r_mark{background:#252527;border:1px solid #4c4c52}
^.bcm_radio .bcm_r_text,^.mls_item_online.actions a.chat,^.bcm_filter_result .fr_value,^.purchase_vip label.choice.discount .eur-vip s,^.history .service.__group .msgs_desc,^.chat_prompt_wrp .cp_msg.__group_join .cp_msg_s_text,^.chat_prompt_wrp .cp_msg.__group_join .cp_msg_arrow .cp_msg_arrow_icon,^.bcm_chat_selection .bcm_cs_desc,^.bcm_chat_selection .bcm_cs_price,^.buy_tokens_wire_form p,^.buy_tokens_sms_dimoco_form p,^.buy_tokens_digiseller_form p,^#buy_tokens_payment_type .ui-collapsible .package.__simple .brbw_input:disabled~.ui-btn .ui-btn-text,^#buy_tokens_payment_type .bt_radio_btn_wrp.__simple .brbw_input:disabled~.ui-btn .ui-btn-text,^.buy_tokens .choice .ui-btn-inner,^.buy_tokens .choice .foreign_currency,^.buy_tokens .bt_price_old,^.buy_tokens .pkg_cybersale .bt_strike_price,^.buy_tokens .bt_payment_description,^.buy_tokens_sms_dimoco_package .input_phone.__sending .status,^.buy_tokens_sms_dimoco_package .input_phone .sdp_phone_title,^.buy_tokens_sms_dimoco_package .form_note_btn,^.f_rptfmm .buy_tokens_sms_dimoco_package .package+p,^.f_rptsmm .buy_tokens_sms_dimoco_package .package+p,^.f_rptfmm .buy_tokens_sms_dimoco_package .package+p+p,^.f_rptsmm .buy_tokens_sms_dimoco_package .package+p+p,^.buy_tokens .sp_current_package .spcp_approx,^.buy_tokens .bt_custom_amount_field,^.buy_tokens .bt_custom_amount_field .caf_input_custom_field,^.buy_invisible_package .choice_description,^.im_conversations .imc_history_box .imch_item.__translate,^.im_conversations .imc_history_box .imch_item.__action_btns .imch_action_btn,^.cg_overlay .cg_options_list .ol_item.__checked .cgo_radio{color:#bcbcbc}
^.bcm_radio.__background .bcm_r_text{color:#d4d4d4;text-shadow:0 1px transparent}
^.bc_phone_select_box .bcsb_input_wrp{color:#c4c4c4;border:1px solid #444449;background:#161617}
^.bc_phone_select_box .bcsb_input{color:#c4c4c4;background:#121213;border:1px solid #444449}
^.bc_phone_select_box .bcsb_input::placeholder,^.bcm_input_row .bcm_form_input::placeholder,^.bcm_input_row .bcm_form_textarea::placeholder,^.purchase_vip label.choice.discount_gift_card .eur-vip s,^.purchase_vip label.choice.discount .eur-vip s,^.mbc_search_block .sb_input::placeholder,^.im_conversations .imc_search .imcs_icon,^.im_conversations .imc_search .search_input::placeholder,^.imch_reply .imch_r_msg,^.cg_vibe_with_me.__vwm_app .vwm_a_qr_code{color:#b3b3b3}
^.bcm_checkbox_row .cr_mark{background:linear-gradient(#2f2f33,#252528)}
^.bc_social_networks .sn_count{color:#bcbcbc;background:#121213}
^.range_slider .rs_track,^.im_conversations .imc_history_box .imch_date_wrp:before,^.im_conversations .imc_history_box .imch_date_wrp:after,^.cg_vibe_with_me.__vwm_app .vwm_a_qr_icon_wrp.__vwm_active{background:#252527}
^.range_slider .rs_progress.__zero:after{background:#121213;border:1px solid #404045}
^.mls .mls_pager_header{color:#bcbcbc;border-top:1px solid #45454a}
^.bcm_filter_btn_wrp .bcm_fbw_btn{color:#cecece;background:#121213;border:1px solid #3f3f44}
^.bcm_filter_btn_wrp .bcm_fbwb_icon{color:#dadada}
^.mls_tabs_container{background:linear-gradient(#121213,#1a1a1c);border-bottom:1px solid #38383c}
^ul.mls_tabs li{text-shadow:1px -1px transparent;background-color:#19191b;border:1px solid #45454a}
^ul.mls_tabs li:hover,^.phone_confirm_box .confirm_phone_form .bc_phone_select_box.__focused .bcsb_input,^.phone_confirm_box .confirm_phone_form .bc_phone_select_box.__focused .bcsb_input_wrp{border-color:#4c4c52}
^ul.mls_tabs li a,^.im_conversations .pm_offering p{color:#d4d4d4 !important}
^.pager>div{background-image:linear-gradient(#29292c,#222224)}
^.forget_info,^.history p.undefined,^.history p.undefined dfn,^.aChat .message .col1,^.buy_tokens .gray_desc,^.verification_failed p,^.credit_box_success .gray_desc{color:#b2b2b2}
^.live_tabs .tabs_list .tabsl_item.__new .tabsl_count:before{background:linear-gradient(#141416,#141416) padding-box padding-box,linear-gradient(270deg,#ff8201,#fca710) border-box}
^.form .form_errors_row{background-color:#19191b}
^.footer_content{background-color:#222225}
^.footer_content .separator{background-color:#343438;border-top:1px solid #333336}
^.center-wrapper .descr_block{border:1px dashed #4c4c52}
^.icon_membership.icon_free{background:#121213;border:2px solid #47474c}
^.mc_status_icon.__icon_free{background-color:#121213;border:2px solid #47474c}
^.padded.grey,^.padded.stripe,^.buy_tokens_wire_form .odd,^.page_im .im_conversations .imc_action_bar{background-color:#1a1a1c}
^.vip-payment .ui-btn-up-c{border:2px solid #45454a !important}
^.vip-payment .head-vip{background-color:#141415;border-bottom:1px solid #45454a}
^.vip-payment.pkg_cybersale .eur-vip,^.vip-payment.pkg_cybersale .inner-vip .vip-blocks,^.maroon_strong,^.buy_tokens .pkg_cybersale .bt_highlight,^.buy_tokens .pkg_cybersale .foreign_currency,^.buy_tokens .bt_custom_amount_alert .caa_cell.__accent,^.buy_tokens.__holiday .bt_highlight{color:#dd7488}
^.vip-payment .eur-vip{color:#e17085;border-left:1px solid #444449}
^.vip-payment .eur-vip b,^.fancybox-error,^.buy_tokens .mobile_pay_form .rich,^.loyalty_box.speaker,^.loyalty_box.speaker .lb_white,^a{color:#cfcfcf}
^.vip-payment .head-vip b,^.notfound .nt_des,^.notfound p.nf_bottom a,^.notfound p.nf_bottom{color:#b4b4b4}
^.vip-payment .foot-vip{color:#b4b4b4;background-color:#141415;border-top:1px solid #45454a}
^.vip-exist,^.buy_tokens_warning,^.f_rptfmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description,^.f_rptsmm .buy_tokens_sms_dimoco_package .input_phone.__extended_description{border:1px solid #36363a}
^.votes_percent_line,^#bChat .msg_wrp .ch_m_translator_link.__source,^#bChat .msg_wrp .ch_m_translator_link.__translate,^.cg_vibe_with_me.__vwm_app .vwm_a_qr_code_wrp{background:#121213;border:1px solid #45454a}
^.bitcoin_paypal_popup .close_button{color:#f5f5f5;background:#121213}
^.suspect_login_page form,^.vip_overview_page .vo_badge,^.im_conversations .imc_item.__selected .imci_wrap{background:#151516}
^.suspect_login_page .form_error{color:#ff5252;background-color:#19191b}
^#bChat .history p.deleted .msg{color:#b6b6b6 !important}
^#bChatFullScreenHistory p.groupChatWelcome,^.history p.groupChatWelcome{color:#5252ff !important}
^.history .service.__group{background:#141415;border:1px solid #38383c}
^.history p.topicChange{color:#e17085 !important}
^.history p.model .icon_avatar,^.imc_footer_reply .imc_fr_inner{background-color:#161618}
^.smileButton{background-color:#18181a;border:1px solid #3d3d41}
^.smileButton .smile_btn_icon,^.chat-content #bChatInputContainer .smile_btn_icon{stroke:#c4c4c4}
^.smileButton.__active .smile_btn_icon{stroke:#f5f5f5}
^#scroll-chat-btn .inner:after,^.popHint.topLeft:after,^.popHint.topRight:after,^.popHint.topCenter:after{border-top-color:#2b2b2e}
^.chat_prompt_wrp .cp_msg_after{background:linear-gradient(#202023 0%,#fff0 25%)}
^.chat-content .chat_panel_wrap>.bcm_ps_countdown_container .bcm_paid_show_countdown{border-top:1px solid #36363a}
^.chat-content .bcm_paid_show_countdown{background:#18181a;border-bottom:1px solid #36363a}
^.bcm_chat_selection{background:#121213;border:1px solid #3d3d41}
^.bcm_chat_selection .bcm_cs_list_item{border-bottom:1px solid #3d3d41}
^.chat-panel .history .system_msg.tip_success.__tip_menu .msg_clear_btn{border:1px solid #2b2b2e}
^.chat-panel .history .system_msg.follow_plank .fp_container{color:#e6e6e6;background:rgba(27,27,29,0.7)}
^.chat-panel .history .system_msg.msg_activity .ma_avatar_wrp:before{border:1px solid rgba(43,43,46,0.8)}
^.chat-panel .msg_chat_game,^#buy_tokens_payment_type .ui-collapsible .package .ui-btn.selected .ui-btn-inner,^#buy_tokens_payment_type .bt_radio_btn_wrp .ui-btn.selected .ui-btn-inner,^.bt_select_modal .slm_i_input:checked~.slm_i_label{background:#141415}
^.chat-content .cp_navigation .cpnl_item,^.im_conversations .imc_history_box .imch_item_wrp.__highlight:before{background-color:#212124}
^.chat-content #bChatInputContainer{background-color:#121213;border-top:1px solid #323235;border-bottom:1px solid #323235}
^.chat-content #bChatInputContainer .sendInput{color:#c4c4c4;background-color:#18181a}
^.chat-content #bChatInputContainer .sendInput::placeholder,^.chat-content #bChatInputContainer .sendInput.__placeholder:before{color:#c4c4c4 !important}
^.ic_menu_wrp .ic_menu{background:#151516;border:1px solid #45454a}
^#bChat .msg_wrp.__tr_enabled.__tr_show_tooltip{background:#171718}
^#bChat .msg_wrp .ch_m_translator_link.__empty_translate{color:#cecece;background:#171718}
^.chat_user_menu .cum_header{color:#d8d8d8;border-bottom:1px solid #38383c}
^.chat_user_menu .cum_info .mi_content,^.chat_user_menu .cum_action .ma_text{color:#b5b5b5}
^.bc_new_message_separator:before,^.bc_new_message_separator:after{background-color:#262628}
^.bcm_chat_footer{background:#18181a;border-top:1px solid #323235;border-bottom:1px solid #36363a}
^.bcm_chat_footer .cf_balance{color:#c4c4c4;background:#18181a}
^.bcm_chat_invite .bcm_ci_badge_icon,^.bcm_chat_invite .bcm_ci_toggle_icon{fill:rgba(245,245,245,0.2)}
^.video-panel .layover .layoverText .layoverIns,^.ru_buy_tokens_mobile_pay p.provider_attention{color:#c5c5c5}
^.video-panel .layover .layoverLimitText{color:#b2b2b2 !important}
^.layover_before_private .lbp_content{background-color:rgba(18,18,19,0.4)}
^.layover_before_private .lbp_item,^.bc_confirm_popup.fan_boosted_confirm .bcc_container_info{color:#d5d5d5}
^.bcm_oops_popup_layover .opl_message{color:#d8d8d8;background:#161618}
^.quality_settings_block .qsb_item.__active{background-color:rgba(18,18,19,0.2)}
^.cam2cam_modal{border:2px solid #444449}
^.tap_model_popup_container .tip_after_private .tap_title .tap_model_name{color:#dc748a}
^.tap_model_popup_container .tip_after_private .radio_list li{background-color:#121213;border:1px solid #45454a}
^.tap_model_popup_container .tip_after_private .radio_list li.selected{background:#131315}
^.tap_model_popup_container .tip_after_private .radio_list li .custom_price input:disabled{background-color:#121213;border-color:#3c3c41}
^.tap_model_popup_container .tip_after_private .radio_list li .radio_help{color:#a1a3b0}
^.tap_model_popup_container .tip_after_private .form_actions .tap_note{color:#e3e3e3}
^.tap_model_popup_container .tip_after_private .form_actions .tap_note .tap_red{color:#dc758a}
^.tap_model_popup_container .tip_after_private .form_error_list{color:#dc758a;background-color:#18181a}
^.fan_boosted_animation.__stopping .fba_timer{stroke:#ff527b}
^.bc_confirm_popup.fan_boosted_confirm .bcc_header .bcc_title{color:#f4f4f4}
^.fancybox-skin{color:#cfcfcf;background:#151516}
^.head_confirm_box{background-color:#131315}
^.head_confirm_box .hcb_close_wrp:hover .hcb_close{color:#e46d89}
^.head_confirm_box .hcb_close_wrp.__bubble:hover .hcb_close_icon{fill:#e46d89}
^.phone_confirm_box .fcb_not_show .ns_mark{border:2px solid #4c4c52}
^.phone_confirm_box .confirm_phone_form{color:#c9c9c9}
^.phone_confirm_box .confirm_phone_form .cpf_code .bcf_control{color:#cfcfcf;background:#121213;border:1px solid #4c4c52}
^.promo_in_banner_popup .pibp_name{color:#e17084 !important}
^#admin_chat_popup .admin_info .details h4{border-bottom:1px solid #4c4c52}
^.buy_tokens .bt_email_wrp,^.buy_tokens .sp_form{color:#c4c4c4;border:1px solid #414146}
^.buy_tokens .btc_input_warning{color:#f65a61}
^.buy_tokens .limit_alert,^.buy_tokens .buy_tokens_mbway_form .bt_input_wrp .err,^.buy_tokens .buy_tokens_wasabigate_form .bt_input_wrp .err,^.buy_tokens .buy_tokens_hkpay_form .bt_input_wrp .err,^.buy_tokens .coinpayments_form .bt_input_wrp .err.err_show,^.buy_tokens .mobile_pay_form .bt_input_wrp .err.err_show,^.buy_tokens .bt_custom_amount_field .caf_input_warning{color:#ff5272}
^.buy_vip_sms_form .price span,^.buy_vip_sms_form .price em,^.ru_buy_tokens_sms_pay .heading h4,^.ru_buy_tokens_mobile_pay .heading h4,^.ru_buy_tokens_sms_pay ul a{color:#db7688}
^.buy_tokens .mobile_pay_form input,^.confirm_code_form input{border:1px solid #45454a;border-color:#4c4c52 #45454a #45454a #4c4c52}
^.buy_tokens .mobile_pay_form input.disabled{color:#bcbcbc;background-color:#1a1a1c}
^.confirm_code_form{color:#bcbcbc;border:1px solid #45454a}
^table.wire-tab td{color:#f5f5f5;border:1px solid #45454a}
^.buy_tokens .foreign_currency span{color:#5297ff}
^.verify_info_box p,^.confirm_box{color:#d9d9d9}
^.confirm_box .example_box .ex{background-color:#121213;border:1px solid #3e3e43}
^.confirm_box .example_box .ex .amount span,^.confirm_box .example_box .info{color:#e86969}
^#buy_tokens_payment_type .ui-collapsible .package .ui-btn:after,^#buy_tokens_payment_type .bt_radio_btn_wrp .ui-btn:after,^.buy_tokens .bt_select_change:after{border:1px solid #414146}
^#buy_tokens_payment_type .payment_icon,^.bt_select_modal .payment_icon{background:#121213;border:1px solid #3e3e42}
^.buy_tokens .choice.discount_box,^.buy_tokens label.choice.discount_box,^.buy_tokens .sp_current_package{color:#bcbcbc;background:#131315;border:1px solid #45454a}
^.buy_tokens .sms_amount_error,^.buy_tokens .bitcoin_form .bitcoin_error,^.buy_tokens .bitcoin_form .info_text{color:#f25e5e}
^.buy_tokens .discount_box .original_sum,^.buy_tokens label.discount_box .original_sum,^.show_hide_packages{color:#b9b9b9}
^.gift_card_section .gift_card_box{background:#141416;border:1px solid #45454a}
^.buy_tokens .ui-radio label{background-color:#141416 !important}
^.buy_tokens .pkg_cybersale .ui-radio label,^.buy_tokens .bt_select_change{color:#bcbcbc !important}
^.buy_tokens_redirect .btr_text.__wait,^.buy_tokens_header_board .hbt_paragraph,^.im_conversations .imc_actions .imca_text.__new_msg,^.im_conversations .imc_empty .imce_text,^.im_conversations .imci_message,^.im_typing{color:#bababa}
^.loyalty_box.speaker:before{border-top:5px solid #2b2b2e}
^.loyalty_box.speaker:after{border-top:6px solid #45454a}
^.loyalty_box.bit{color:#cbcbcb;background:#141416}
^.loyalty_box.bit .lb_title{color:#eaeaea}
^.loyalty_box.bit .lb_green{color:#92ff52}
^.loyalty_box.bit .lb_warn:before{border-top:1px solid #3a3a3e}
^.loyalty_box.leaving .lb_warn:before,^.loyalty_box.leaving .lb_warn:after{background:#2b2b2e}
^.buy_tokens .confirm_code_form_resend{background:#131315;border:1px solid #45454a}
^.buy_tokens_header_board .hbt_headline_block,^.cg_service_list .cg_sub_header{border-bottom:1px solid #333336}
^.buy_tokens_header_board .hbt_headline{color:#e8e8e8}
^.buy_tokens .bt_custom_amount_alert{color:#bcbcbc;background:#131315;border:1px solid #414146}
^.buy_tokens .bt_custom_amount_field .caf_input_currency{color:#bcbcbc;background:#141415}
^.show_hide_packages .sh_label .sh_icon{color:#c5c5c5;background:#141416;border:1px solid #45454a}
^.buy_tokens .bt_anim_icon_success .ais_item:after{border:6px solid #2b2b2e}
^.bt_select_modal{background:#121213;border:1px solid #404044}
^.bt_select_modal .slm_i_label{background:#121213;color:#bcbcbc !important}
^#discount_green_purple .dgp_clock_hand{color:#c58bb5}
^.gc_popup{color:#9d78d8;background:#121213}
^.gc_popup .gc_title .gc_red{color:#e07185}
^#gc_popup_vip .gc_title .gc_red,^.ac_change_photo .ac_cp_desc.__error{color:#e36e84}
^.gc_popup.gc_popup_hor .gc_title{color:#9d78d8}
^.fancybox-item.fancybox-close.pl_un_default_close,^.fancybox-item.fancybox-close.di_ex_close{color:#f5f5f5;background:#121213 !important}
^.mbc_search_block.__filter .sb_result .sbr_head.__recent{border-bottom:1px solid #3f3f43}
^.mbc_search_block .sb_result .sbr_count{color:#c2c2c2}
^body{color:#cfcfcf;background:#242426}
^.popHint .popHintFooter{background:#141416;border-top:1px solid #3c3c41}
^.popHint.bottomLeft:after,^.popHint.bottomRight:after,^.popHint.bottomCenter:after{border-bottom-color:#2b2b2e}
^.popHint.leftTop:after,^.popHint.leftBottom:after,^.popHint.leftCenter:after{border-left-color:#2b2b2e}
^.popHint.rightTop:after,^.popHint.rightBottom:after,^.popHint.rightCenter:after{border-right-color:#2b2b2e}
^#bChat.bChatModel .popHint:after{border-top-color:#2f2f33}
^.profile_avatar_popup .account_actions,^.account_actions{background-color:#141415;border-top:1px solid #49494e}
^.profile_avatar_popup .account_actions .mbc_btn.ac_a_btn_cancel:active,^.account_actions .mbc_btn.ac_a_btn_cancel:active{background-color:#323235}
^.account_preview .acp_img_wrp{border:2px solid #2b2b2e}
^.account_preview .acp_btn{color:#cecece;text-shadow:0 1px transparent;background:#252527}
^.ac_change_photo .ac_cp_line{background-color:#2b2b2e}
^.vip_overview_page,^.vip_overview_page .vo_ttl{color:#c3c3c3}
^.page_content .cancel_subscription.__dashed{border:1px dashed #45454a}
^.vip_overview_page_badge{color:#c3c3c3;background:#151516}
^.vip_overview_stock_page{color:#e0e0e0;border:1px solid #313134}
^.vip_overview_stock_page .sp_privilege{border-top:1px solid #37373a}
^.invisible_mode_overview_plank{color:#9099c1;background:#151516}
^.page_im .im_conversations,^.smilePanelTabs{background:#1a1a1c}
^.im_conversations .imc_actions .bcf_selectbox,^.im_conversations .gift_card_promo_wrapper{background:#121213;border:1px solid #3e3e43}
^.im_conversations .imc_actions .mbc_btn.__btn_msg.__disabled{background:#2e2e31}
^.im_conversations .imc_actions .imca_icon{color:#d6d6d6;fill:#d6d6d6}
^.im_conversations .imc_item.__new .imci_wrap .imci_message,^.im_conversations .imci_user_name,^.im_conversations .imc_followers .imcf_name{color:#d7d7d7}
^.im_conversations .mbc_alert.__tip,^.im_conversations .imc_history_box .imch_group.__own .imch_item.__tip,^.im_conversations .pm_upgraded_membership.__unlimited .pmu_footer{background:#171719}
^.im_conversations .imc_history_box .imch_msg_check.__unsent .imch_mc_icon,^.im_conversations .imc_search .imcs_reset .imcs_r_icon{fill:#e36d84}
^.im_conversations .imc_history_box .imch_group.__own .imch_item,^.pm_activation_2fa .pmaf_body,^.im_conversations .pm_upgraded_membership.__exclusive .pmu_holder.__filled{background:#161618}
^.im_conversations .imc_history_box .imch_group.__own .imch_item:first-of-type:after{border-left:6px solid #303033}
^.im_conversations .imc_history_box .imch_group.__own .imch_item.__tip:first-of-type:after{border-left-color:#313134}
^.im_conversations .imc_history_box .imch_item{background:#151517}
^.im_conversations .imc_history_box .imch_item:first-of-type:after{border-right:6px solid #2f2f32}
^.im_conversations .imc_history_box .imch_item.__selected:first-of-type:after{border-right-color:#353538}
^.im_conversations .imc_history_box .imch_item.__sq .imc_question_link,^.im_conversations .imch_item.__default a{color:#52b0ff}
^.im_conversations .imc_history_box .imch_item.__sq_error{color:#e17085;background:#171719}
^.im_conversations .imc_history_box .imch_item.__sq_error:first-of-type:after{border-right-color:#313134}
^.im_conversations .imc_history_box .imch_item.__action_btns .imch_action_btn.__active{background:#202022}
^.im_conversations .imc_history_box .imch_context_menu,^.cg_service_list .cg_sl_price{border:1px solid #38383c}
^.im_conversations .imc_history_box .imch_context_menu .imch_cm_btn{color:#f5f5f5;background:#121213;border-bottom:1px solid #38383c}
^.im_conversations .imc_history_box .mbc_btn.__fab{background:#121213;border-color:#38383c}
^.im_conversations .imc_search .search_input{color:#d6d6d6;background:#1b1b1d;border:1px solid #47474c}
^.imc_footer_reply{color:#d8d8d8;background:#121213;border-top:1px solid #3d3d41}
^.imc_tip_before_start .tbs_content{background:#121213;border:2px dashed rgba(74,74,79,0.5)}
^.im_conversations .imch_contact_card{border:2px dashed #4a4a4f}
^.im_typing .imt_dots .imtd_item{background:#3d3d41}
^.pm_activation_2fa .pmaf_title{color:#cacaca}
^.pm_activation_2fa .pmaf_info_wrp{border-top:1px solid #37373b}
^.pm_activation_2fa .pmaf_info{color:#e7e7e7}
^.pm_activation_2fa .pmaf_a_text{color:#c6c6c6}
^.pm_activation_2fa .pmaf_footer{background:#1e1e20}
^.im_conversations .pm_upgraded_membership{color:#f5f5f5;background:#121213;border:2px solid #333336}
^.im_conversations .pm_upgraded_membership.__platinum{border-color:#37373b}
^.im_conversations .pm_upgraded_membership.__platinum .pmu_holder.__filled,^.im_conversations .pm_upgraded_membership.__platinum .pmu_footer,^.im_conversations .pm_upgraded_membership.__unlimited .pmu_holder.__filled,^.cg_overlay .gc_btn.__white:active{background:#1d1d1f}
^.im_conversations .pm_upgraded_membership.__unlimited{border-color:#313134}
^.im_conversations .pm_upgraded_membership.__sapphire,^.im_conversations .pm_upgraded_membership.__titan{border-color:#323236}
^.im_conversations .pm_upgraded_membership.__sapphire .pmu_highlight{color:#8b52ff}
^.im_conversations .pm_upgraded_membership.__diamond{border-color:#343438}
^.im_conversations .pm_upgraded_membership.__diamond .pmu_highlight{color:#ff527b}
^.im_conversations .pm_upgraded_membership.__exclusive{border-color:#303033}
^.im_conversations .pm_upgraded_membership .pmu_updated_status,^.im_conversations .pm_upgraded_membership .pmu_current_status{color:#94a2bc}
^.im_conversations .gift_card_promo_wrapper .gift_card_promo .gift_card_promo_code_wrapper{color:#a079d8}
^.im_conversations .gift_card_promo_wrapper .gift_card_promo .gift_card_promo_code_wrapper p span{color:#e17087}
^.im_conversations .gift_card_promo_wrapper .gift_card_promo .gift_card_promo_code_wrapper .gift_card_promo_code_ribbon_left:before{border-left:10px solid #2b2b2e}
^.im_conversations .gift_card_promo_wrapper .gift_card_promo .gift_card_promo_code_wrapper .gift_card_promo_code_ribbon_right:before{border-right:10px solid #2b2b2e}
^.im_conversations .pm_offering .pmo_bonus{color:#e26f85}
^.im_conversations .pm_downgrade_membership{border:2px solid #303033}
^.im_conversations .pm_downgrade_membership .dmh_content .hc_status{color:#91afbf;background-color:#121213}
^.im_conversations .pm_downgrade_membership .dm_title .dmt_username,^.im_conversations .pm_downgrade_membership .dm_title .dmt_desc{color:#949dbc}
^.im_conversations .pm_downgrade_membership .dm_title .dmt_desc .td_accent{color:#e76978}
^.im_conversations .pm_valentine_day_themed_gift .pmvdtg_card{background:#121213;border:1px solid #3c3c41}
^.smilePanelTabs:before{background:linear-gradient(#29292c,#efefef00)}
^.cg_overlay .cg_options_list .ol_item.__checked{border-color:#3f3f44}
^.cg_overlay .gc_btn{color:#cecece;border:1px solid #3f3f44}
^.cg_overlay .gc_btn.__gray{background:linear-gradient(#151516,#1a1a1c)}
^.cg_overlay .gc_btn.__gray:active{background:#323235}
^.cg_overlay .gc_btn.__disabled{background:#121213;border:1px solid #3f3f44}
^.cg_overlay .cg_prize_list .li_text,^.cg_overlay .cg_prize_list.__wheel .li_text,^.bc_token_keno_field .tkf_cube .fc_body .fc_number{color:#bd9494}
^.cg_overlay .cg_input_wrp .cgi_input_amount{color:#d8d8d8;background:#151516;border:1px solid #4c4c52}
^.cg_overlay .cg_accordion .cga_item:active{background-color:#1d1d1f}
^.cg_overlay .cg_adj_empty_playlist{background-color:#131315;border:1px solid #45454a}
^.cg_overlay .cg_adj_form .djf_link{color:#f5f5f5;border:1px solid #3c3c40}
^.cg_overlay .cg_adj_form .djf_premium.__checked{background-color:#141415}
^.cg_auto_dj_box .adjb_icon,^.cg_overlay .cg_prize_list.__slot .pli_icon_wrp .iw_icon,^.cg_slm_slot .slms_item.__active .slms_icon{fill:#bd9494}
^.cg_overlay .cg_adj_icon_premium .ip_icon{fill:#cfcfcf}
^.cg_overlay .cg_adj_list .pll_item{color:#c1c1c1;background-color:#131315;border-bottom:1px solid #45454a}
^.cg_overlay .cg_game_field .sm_body{background-image:linear-gradient(#1b1b1c 0%,#232326 100%)}
^.cg_overlay .cg_game_field .si_item:after{background:#212123}
^.cg_overlay .cg_game_field .si_item .sii_icon{fill:#f5f5f5}
^.cg_vibe_with_me.__vwm_app .vwm_a_list{color:#f5f5f5;background:#171719}
^.cg_vibe_with_me.__vwm_app .vwm_ai_container:before{background:rgba(43,43,46,0.5)}
^.cg_service_list .cg_sl_item{color:#d8d8d8;border-bottom:1px solid #2e2e31}
@media (any-hover:hover) and (pointer:fine){^.mbc_menu .mbcm_item:hover,^.cg_overlay .gc_btn.__white:hover,^.cg_overlay .gc_btn.__gray:hover{background:#1b1b1d}
^.profile_avatar_popup .account_actions .mbc_btn.ac_a_btn_cancel:hover,^.account_actions .mbc_btn.ac_a_btn_cancel:hover{background-color:#1b1b1d}
^.im_conversations .imc_actions .bcf_selectbox:hover{background:#141416}
^.im_conversations .imc_actions .mbc_btn.__btn_msg:hover .imca_text.__new_msg{color:#d6d6d6}}
@media screen and (max-width:480px){^.vip-payment .inner-vip .vip-blocks .cont-vip{border-bottom:1px solid #45454a}}
@media (max-width:40em){^.notfound p{color:#d8d8d8}}
@media (min-width:46em){^.aChat .message .col1{color:#b2b2b2}}
@media (max-width:780px){^.im_conversations .pm_upgraded_membership.__gold .pmup_icon,^.im_conversations .pm_upgraded_membership.__sapphire .pmup_icon,^.im_conversations .pm_upgraded_membership.__titan .pmup_icon{background-color:#19191b}
^.im_conversations .pm_upgraded_membership.__platinum .pmup_icon{background-color:#1d1d1f}
^.im_conversations .pm_upgraded_membership.__unlimited .pmup_icon{background-color:#171719}
^.im_conversations .pm_upgraded_membership.__diamond .pmup_icon{background-color:#1b1b1d}
^.im_conversations .pm_upgraded_membership.__exclusive .pmup_icon{background-color:#161618}}`.replaceAll('^', '.bc-dark-m ');
const DARK_FIX_CSS = () => `^.bc_fade_scroll:before,^.bc_fade_scroll:after,^.bc_fade_scroll.__listing_tags:before,^.bc_fade_scroll.__listing_tags:after{background:linear-gradient(90deg, #121216, rgba(18,18,22,0))!important}
^.mls_hash_tag{--bc-thm-hashTag-shadow-bg:#121216;--bc-thm-hashTag-shadow-bg-alpha0:rgba(18,18,22,0);--bc-thm-hashTag-viewAllLink-wrp-bg:#121216}
^.bc_notice{--bc-thm-notice-color:#e6e6e6;--bc-thm-notice-bg:linear-gradient(#121213, #121213);--bc-thm-notice-borderColor:linear-gradient(180deg, #3a3a3f, #3a3a3f);--bc-thm-notice-close-iconColor-hover:#dedede;--bc-thm-notice-link-color:#46c2ff;--bc-thm-notice_secondary-paragraph-color:#d8d8d8;--bc-thm-notice_countdownGoal-bg:#18181c;--bc-thm-notice_countdownGoal-borderColor:#8a28eb80;--bc-thm-notice_follow-borderColor:linear-gradient(90deg, #31c4ee, #20b8e3);--bc-thm-notice_pin-borderColor:linear-gradient(90deg, #e75ec9, #d331b0);--bc-thm-notice_love-borderColor:linear-gradient(90deg, #fe4668, #fe254d)}
^.bc_notice.__countdown_goal,^.bc_notice .nt_message{background-color:#18181c;color:#e6e6e6}
^.bc_notice .nt_message,^.bc_notice .nt_message p,^.bc_notice .nt_message span:not(.nt_accent){color:#e6e6e6}
^.chat-tab,^.chat-panel,^.history,^#bChat{--bc-thm-sysMsg_game-resultWrp-bg:#18181c;--bc-thm-sysMsg_game-label-color:#e6e6e6;--bc-thm-sysMsg_game-content-color:#ee5b78;--bc-thm-sysMsg_game-time-color:#a0a0a8;--bc-thm-sysMsg_topicChange-color:#ee5b78;--bc-thm-sysMsg_modelNote-color:#5fd97f;--bc-thm-sysMsg_serviceGroup-bg:#18181c;--bc-thm-sysMsg_serviceGroup-borderColor:#2b2b2e;--bc-thm-sysMsg_serviceGroup-boxShadowColor:#0000;--bc-thm-sysMsg_serviceGroup-boxShadowColor-inset:#2b2b2e;--bc-thm-sysMsg_serviceGroup-desc-color:#bcbcbc;--bc-thm-sysMsg_serviceGroup-title-color:#5fd97f;--bc-thm-sysMsg_friend-bg:#18181c;--bc-thm-sysMsg_friend-borderColor:#3a3a3f;--bc-thm-sysMsg_friend-color:#e6e6e6;--bc-thm-sysMsg_activity-color:#d8d8d8;--bc-thm-sysMsg_activity-matAccent_member-color:#f5f5f5;--bc-thm-sysMsg_service-bg:#2b2b2e;--bc-thm-sysMsg-serviceList-bg:#18181c;--bc-thm-sysMsg-serviceList-authorName-color:#e6e6e6;--bc-thm-sysMsg-serviceList-title-color:#f5f5f5;--bc-thm-sysMsg-serviceList-services-item-title-color:#e6e6e6;--bc-thm-sysMsg-serviceList-services-item-borderColor:#ffffff1f;--bc-thm-sysMsg-serviceList-services-item-bg-hover:#24242a;--bc-thm-sysMsg-serviceList-showBtn-bg-hover:#24242a;--bc-thm-sysMsg-serviceList-showBtn-color-hover:#fff;--bc-thm-game-autoDJ-title-color:#f5f5f5}
^.history .msg_chat_game .cg_msg_wrp,^.history .system_msg.msg_chat_game{background:#18181c!important}
^.history .msg_chat_game .cg_content,^.history .msg_chat_game .gc_msg_label,^.history .msg_chat_game .cg_msg_s_time{background:transparent}
^.history .msg_chat_game .gc_msg_label,^.history .msg_chat_game .gc_msg_label *{color:#e6e6e6}
^.history .msg_chat_game .cg_msg_s_time{color:#a0a0a8}
^.history .msg_chat_game .mbc_btn,^.history .msg_chat_game .bc_btn,^.history .msg_chat_game button,^.history .msg_chat_game a[class*="btn"]{color:#e6e6e6!important;background:transparent!important;border-color:#4c4c52!important}
^.chat_control_panel,^.send-input-container_wrap,^.ccp_inner,^#chat_control_panel{background-color:#121213!important;border-color:#2b2b2e!important;box-shadow:none!important}
^.chat_control_panel:before,^.chat_control_panel:after,^.ccp_inner:before,^.ccp_inner:after,^#bChatInputContainer:before,^#bChatInputContainer:after{border-color:#2b2b2e!important}
^.bc_fade_scroll{--fadeScroll-fade-bg:linear-gradient(90deg, #121216, rgba(18,18,22,0))}
^.bcm_nav_panel,^.bcm_nav_panel.ui-bar-c,^.bcm_nav_panel.ui-footer,^.bcm_nav_panel .np_inner,^.bcm_nav_panel .np_block{background:#18181a!important;background-image:none!important;border-color:#2b2b2e!important}
^.bcm_nav_panel .np_item{color:#d8d8d8!important;background:transparent!important}
^.bcm_nav_panel .np_item.__active,^.bcm_nav_panel .np_item:active{color:#e36e84!important}
^.bc_fade_scroll.__listing_tags{--fadeScroll-fade-bg:linear-gradient(90deg, #121216, rgba(18,18,22,0))}
^.profile_info_lg{--profileInfo-color:#e6e6e6;--profileInfo-base-accentTitle-color:#ee5b78;--profileInfo-section-bg:#1b1b1f;--profileInfo-detailsWrp-bg:#27272c;--profileInfo-detailsMore-color:#e6e6e6;--profileInfo-flag-borderColor:#3a3a3f;--profileInfo-shelf-broderColor:#3a3a3f;--profileInfo-shelf-badge-bg:#27272c;--profileInfo-section_shedule-text_highlight-color:#ee5b78;--profileInfo-qoqContests-chat-bg:#1b1b1f;--profileInfo-qoqContests-title-color:#ee5b78;--profileInfo-qoqContests-text-color:#e6e6e6;--profileInfo-gifts-emptyHead-imgWrp-bg:#27272c;--profileInfo-gifts-emptyHead-imgWrp-borderColor:#3a3a3f;--profileInfo-comments-content-bg:#27272c;--profileInfo-comments-content-text-color:#e6e6e6;--profileInfo-comments-btn-color:#e6e6e6;--profileInfo-comments-btn-bg:#27272c;--profileInfo-section_tags-stlItem-color:#e6e6e6;--profileInfo-section_tags-stlItem-bg:#27272c;--profileInfo_section_wish-listItem-bg:#27272c;--profileInfo-listWrp-bg:#27272c;--profileInfo-listItem-bg:#1b1b1f;--profileInfo-listItemCaption-color:#9a9aa2;--profileInfo-listItemHighlight-color:#e6e6e6}`.replaceAll('^', '.bc-dark-m ');

const DARK_ALL_CSS = () => `^.bc_notice,^.bc_notice.__countdown_goal,^.bc_notice.__primary,^.bc_notice.__secondary{--bcm-notice-color:#e6e6e6;--bcm-notice-bg:linear-gradient(#121213, #121213);--bcm-notice-borderColor:linear-gradient(#2e2e33, #2e2e33);--bcm-notice-link-color:#46c2ff;--bcm-notice-close-iconColor:#a0a0a8;--bcm-notice_countdownGoal-bg:linear-gradient(#121213, #121213);--bcm-notice_countdownGoal-borderColor:linear-gradient(#2e2e33, #2e2e33);--bcm-notice_membershipError-bg:linear-gradient(#121213, #121213);--bcm-notice_membershipError-borderColor:linear-gradient(#2e2e33, #2e2e33);--bcm-notice_notEnoughtTokens-bg:linear-gradient(#121213, #121213);--bc-thm-notice_topic-bg:#18181c;--bc-thm-notice_topic-color:#ee5b78;--bc-thm-notice_attention-bg:#1d1828;--bc-thm-notice_attention-color:#c291f2}
^.bc_notice.__countdown_goal,^.bc_notice.__primary{background:linear-gradient(#121213, #121213) padding-box, linear-gradient(#2e2e33, #2e2e33) border-box!important;border-color:transparent!important;box-shadow:none!important}
^.bc_notice.__countdown_goal .nt_message,^.bc_notice.__countdown_goal .nt_message *:not(.nt_accent):not(.nt_tip_btn){color:#e6e6e6}
^.mls_hash_tag{--mlsHashTag-tag-color:#e6e6e6;--mlsHashTag-tag-bg:#26262b;--mlsHashTag-tag-bg-active:#34343a;--mlsHashTag-tag_record-icon-color:#a0a0a8;--mlsHashTag-viewAllLink-color:#46c2ff;--mlsHashTag-removeFilterLink-close-iconColor:#c4c4c4}
^.mls_hash_tag .ht_item,^.mls_hash_tag .ht_item.ui-link{background:#26262b!important;color:#e6e6e6!important}
^.mls_tag_badge{--bcm-listing-tagBadge-bg:#26262b;--bcm-listing-tagBadge-borderColor:#3a3a3f;--bcm-listing-tagBadge-color:#e6e6e6;--bcm-listing-tagBadge-button-bg:#4c4c52;--bcm-listing-tagBadge-button-iconColor:#fff}
^.bcm_popup,^.filter_popup,^.search_popup,^.mbc_search_block{--bcm-popup-body-bg-color:#121213;--bcm-popup-filter-main-bg:#121213;--bcm-popup-filter-features-title-color:#c1c1c1;--bcm-popup-filter-footer-borderColor:#2e2e33;--bcm-popup-filter-notice-color:#c1c1c1;--bcm-popup-filter-button-outline-color:#d8d8d8;--bcm-popup-filter-button-outline-color-active:#fff;--bcm-popup-filter-button-outline-bg:#1b1b1f;--bcm-popup-filter-button-outline-borderColor:#3e3e43;--bcm-popup-search-header-bg-color:#18181a;--bcm-search-block-input-bg-color:#1b1b1f;--bcm-search-block-input-borderColor:#3e3e43;--bcm-search-block-input-placeholder-color:#8e8e96;--bcm-search-block-header-close-icon-color:#d8d8d8;--bcm-search-block-reset-button-bg-color:#26262b;--bcm-search-block-reset-button-border-color:#3e3e43;--bcm-search-block-result-name-color:#e6e6e6;--bcm-search-block-result-border-color:#2b2b2e;--bcm-search-block-result-tag-color:#e6e6e6;--bcm-search-block-result-tag-name-bg-color:#26262b}
^.bcm_popup .bcm_popup_body,^.bcm_popup .bp_body,^.filter_popup .fp_main,^.filter_popup .fp_body,^.filter_popup .fp_footer,^.filter_popup .fp_content,^.search_popup .sp_body,^.mbc_search_block{background-color:#121213!important;color:#d8d8d8}
^.bcm_range{--bcm-range-values-color:#bcbcbc;--bcm-range-track-bg-color:#2b2b2e;--bcm-range-handle-bg-color:#d8d8d8;--bcm-range-handle-border-color:#4c4c52}
^.bcm_accordion{--bcm-accordion-box-shadow-color:#2b2b2e;--bcm-accordion-title-color:#bcbcbc;--bcm-accordion-arrow-fill:#d8d8d8}
^.bcm_form_alert{--bcm-form-alert-error-bg-color:#2a1a1d;--bcm-form-alert-error-color:#ff7a85;--bcm-form-alert-success-bg-color:#173320;--bcm-form-alert-success-color:#b6f0c4}
^.bcm_switcher{--bcm-switcher-bg:#3a3a3f}
^.bcm_socials{--bcm-thm-socials-counter-color:#bcbcbc;--bcm-thm-socials-counter-bg:#18181c;--bcm-thm-socials-counter-borderColor:#3a3a3f}
^.bcm_footer{--bcm-thm-footer-bg:#18181a;--bcm-thm-footer-borderColor:#2b2b2e;--bcm-thm-footer-details-color:#d8d8d8;--bcm-thm-footer-details-nav-link-color:#d8d8d8;--bcm-thm-footer-details-nav-link-bg-active:#26262b;--bcm-thm-copyright-bg:#121213;--bcm-thm-copyright-color:#bcbcbc}
^.bcm_chat_footer{--bcm-chat-footer-item-border-color:#2b2b2e}
^.bcm_history_scroll_up{--bcm-historyScrollUp-bg:#26262b}
^.bcm_chat_auto_message{--bcm-chat-member-notification-borderColor:#3a3a3f;--bcm-chat-member-notification-title-color:#e6e6e6}
^input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=button]):not([type=submit]):not([type=reset]):not([type=file]):not([type=color]):not([type=image]):not([type=hidden]),^textarea,^.mbc_search_block .sb_input{color:#f0f0f0!important;-webkit-text-fill-color:#f0f0f0!important;caret-color:#f0f0f0!important}`.replaceAll('^', '.bc-dark-m ');
const darkOpaqueCss = () => '.panel,.bm-modal{background:' + SK('rgb(17,17,17)', 'rgb(18,18,22)') + '!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important}';
const syncOpaque = on => {
    const sr = document.getElementById('bc-unchained-host')?.shadowRoot;
    const st = sr?.getElementById('bc-dark-opaque');
    if (!sr || on === !!st) return;
    if (st) return st.remove();
    const e = document.createElement('style');
    e.id = 'bc-dark-opaque';
    e.textContent = darkOpaqueCss();
    sr.appendChild(e);
};
const setCls = (el, c, on) => { if (el.classList.contains(c) !== on) el.classList.toggle(c, on); };
const addStyle = (id, css, root) => {
    if (document.getElementById(id)) return;
    const st = document.createElement('style');
    st.id = id;
    st.textContent = typeof css === 'function' ? css() : css;
    (document.head || root).appendChild(st);
};
const darkBcDesktop = (b, on) => {
    const cl = b.classList;
    if (on) {
        if (b.dataset.bcTheme === undefined && !cl.contains('theme_dark')) b.dataset.bcTheme = cl.contains('theme_light') ? '1' : '0';
        setCls(b, 'theme_light', false);
        setCls(b, 'theme_dark', true);
    } else if (b.dataset.bcTheme !== undefined) {
        const wasLight = b.dataset.bcTheme === '1';
        delete b.dataset.bcTheme;
        setCls(b, 'theme_dark', false);
        if (wasLight) setCls(b, 'theme_light', true);
    }
};
const isOwnNode = n => n.nodeType === 1 && (n.classList.contains('bcu-star') || n.classList.contains('bcu-ring'));
const onlyOwnNodes = recs => {
    for (const r of recs) {
        if (r.type !== 'childList') return false;
        for (const n of r.addedNodes) if (!isOwnNode(n)) return false;
        for (const n of r.removedNodes) if (!isOwnNode(n)) return false;
    }
    return true;
};
const DomBus = (() => {
    const subs = [];
    let mo = null;
    const start = () => {
        if (mo || !document.documentElement) return;
        mo = new MutationObserver(recs => {
            let own;
            const isOwn = () => own === undefined ? (own = onlyOwnNodes(recs)) : own;
            for (const fn of subs) { try { fn(recs, isOwn); } catch {} }
        });
        mo.observe(document.documentElement, { childList: true, subtree: true });
    };
    return { subscribe(fn) { subs.push(fn); start(); } };
})();
const DarkSweep = (() => {
    const SKIP = '.video-panel-wrap,video,canvas,svg,img,#bc-unchained-host';
    const INK = '#e6e6e6';
    const BORDERS = [['Top', 0.55], ['Bottom', 0.55], ['Left', 0.75], ['Right', 0.75]];
    let on = false, mo = null, tm = 0, seen = new WeakSet(), gen = 0, lastSweep = 0;
    const touched = [];
    let touchedCap = 4000;
    const pruneTouched = () => {
        const live = touched.filter(r => r.deref());
        touched.length = 0;
        for (const r of live) touched.push(r);
        touchedCap = Math.max(4000, live.length * 2);
    };
    const rgba = s => s.match(/[\d.]+/g)?.map(Number) ?? null;
    const solid = (c, a) => !!c && (c.length < 4 || c[3] > a);
    const lumOf = c => (0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]) / 255;
    const spread = c => Math.max(c[0], c[1], c[2]) - Math.min(c[0], c[1], c[2]);
    const darkInk = c => solid(c, 0.3) && lumOf(c) < 0.33;
    const styleOf = el => { try { return getComputedStyle(el); } catch { return null; } };
    const put = (el, prop, val) => {
        if (!el.__bcdm) {
            el.__bcdm = {};
            touched.push(new WeakRef(el));
            if (touched.length > touchedCap) pruneTouched();
        }
        if (!(prop in el.__bcdm)) el.__bcdm[prop] = el.style.getPropertyValue(prop);
        if (defer) defer.push(el, prop, val);
        else el.style.setProperty(prop, val, 'important');
    };
    let defer = null;
    const batched = fn => {
        if (defer) return fn();
        defer = [];
        try { fn(); } finally {
            const d = defer;
            defer = null;
            for (let i = 0; i < d.length; i += 3) d[i].style.setProperty(d[i + 1], d[i + 2], 'important');
        }
    };
    const REJECT_SEL = '.video-panel-wrap,video,canvas,img,#bc-unchained-host';
    const walkFilter = { acceptNode: n => (n.ownerSVGElement || n.matches(REJECT_SEL)) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT };
    const walk = (root, cb) => {
        const tw = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, walkFilter);
        for (let n = tw.nextNode(); n; n = tw.nextNode()) cb(n);
    };
    const fixSvg = el => {
        const cs = styleOf(el);
        if (!cs) return;
        if (darkInk(rgba(cs.color))) put(el, 'color', INK);
        for (const k of ['fill', 'stroke']) {
            const v = cs[k];
            if (v && v !== 'none' && !v.startsWith('url') && darkInk(rgba(v))) put(el, k, INK);
        }
    };
    const hasText = el => Array.from(el.childNodes).some(n => n.nodeType === 3 && n.nodeValue.trim());
    const nestedBg = el => {
        for (let a = el.parentElement; a; a = a.parentElement) {
            if (a.__bcdm && 'background-color' in a.__bcdm) return true;
            if (a.id === 'bChatContent') break;
        }
        return false;
    };
    const fixBg = (el, cs) => {
        const bg = rgba(cs.backgroundColor);
        if (solid(bg, 0.6) && lumOf(bg) > 0.88) put(el, 'background-color', nestedBg(el) ? '#27272c' : '#1b1b1f');
    };
    const fixBorders = (el, cs) => {
        for (const [side, lim] of BORDERS) {
            const c = rgba(cs[`border${side}Color`]);
            if (parseFloat(cs[`border${side}Width`]) > 0 && solid(c, 0.3) && lumOf(c) > lim && spread(c) < 40) put(el, `border-${side.toLowerCase()}-color`, '#3a3a3f');
        }
    };
    const isRed = c => c[0] > 100 && c[1] < 90 && c[2] < 100 && c[0] > c[1] * 1.8;
    const textTone = c => {
        const L = lumOf(c), sat = spread(c);
        if (isRed(c)) return L < 0.33 ? '#ee5b78' : null;
        return (sat < 40 && L < 0.45) || (L < 0.33 && sat < 90) ? INK : null;
    };
    const fixText = (el, cs) => {
        const c = hasText(el) && rgba(cs.color);
        const tone = c && textTone(c);
        if (tone) put(el, 'color', tone);
    };
    const fix = el => {
        const cs = styleOf(el);
        if (!cs) return;
        fixBg(el, cs);
        fixBorders(el, cs);
        fixText(el, cs);
    };
    const inBox = el => !!el.closest && !el.closest('#bc-unchained-host');
    const visit = (el, trusted) => {
        if (el.nodeType !== 1 || seen.has(el)) return;
        seen.add(el);
        if (el.localName === 'svg') { if (trusted || !el.parentElement?.closest(SKIP)) fixSvg(el); }
        else if (trusted || !el.closest(SKIP)) fix(el);
    };
    const visitTree = el => {
        if (el.nodeType !== 1 || !inBox(el)) return;
        visit(el);
        if (el.localName === 'svg' || el.ownerSVGElement || el.closest(SKIP)) return;
        walk(el, n => visit(n, true));
    };
    const inViewport = e => {
        const r = e.getBoundingClientRect(), vh = window.innerHeight + 80, vw = window.innerWidth;
        return r.width > 0 && r.height > 0 && r.bottom > -80 && r.top < vh && r.right > 0 && r.left < vw;
    };
    const sweep = () => {
        tm = 0;
        const box = document.body;
        if (!on || !box) return;
        lastSweep = Date.now();
        const my = ++gen;
        const first = [], list = [];
        walk(box, e => { if (!seen.has(e)) (inViewport(e) ? first : list).push(e); });
        batched(() => { for (const e of first) visit(e, true); });
        let i = 0;
        const step = () => {
            if (!on || my !== gen) return;
            const t0 = performance.now();
            if (i < list.length) batched(() => { do visit(list[i++], true); while (i < list.length && performance.now() - t0 < 8); });
            if (i < list.length) requestAnimationFrame(step);
        };
        step();
    };
    const pendAttr = new Set(), pendTree = new Set();
    let pendRaf = 0, pendTo = 0;
    const flushPend = () => {
        cancelAnimationFrame(pendRaf); clearTimeout(pendTo); pendRaf = pendTo = 0;
        const trees = [...pendTree], attrs = [...pendAttr];
        pendTree.clear(); pendAttr.clear();
        if (!on) return;
        batched(() => {
            for (const n of trees) if (n.isConnected) visitTree(n);
            for (const t of attrs) if (t.isConnected) fix(t);
        });
    };
    const schedPend = () => {
        if (pendRaf || pendTo) return;
        pendRaf = requestAnimationFrame(flushPend);
        pendTo = setTimeout(flushPend, 250);
    };
    const onAttr = ({ target: t, attributeName }) => {
        if (t.nodeType !== 1 || !inBox(t) || t.closest(SKIP)) return;
        if (attributeName === 'style' && (t.__bcdm || !t.closest('#bChatContent'))) return;
        pendAttr.add(t);
    };
    const onMut = recs => {
        if (!on) return;
        for (const r of recs) {
            if (r.type === 'childList') r.addedNodes.forEach(n => { if (!isOwnNode(n)) pendTree.add(n); });
            else if (r.type === 'attributes') onAttr(r);
        }
        if (pendTree.size || pendAttr.size) schedPend();
    };
    const queue = () => { if (!tm) tm = requestAnimationFrame(sweep); };
    const revert = () => {
        for (const ref of touched) { const el = ref.deref(); if (!el || !el.__bcdm) continue; for (const k in el.__bcdm) { const v = el.__bcdm[k]; v ? el.style.setProperty(k, v) : el.style.removeProperty(k); } delete el.__bcdm; }
        touched.length = 0; seen = new WeakSet();
    };
    const start = () => {
        if (!mo && document.body) { mo = new MutationObserver(onMut); mo.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'hidden', 'style'] }); }
        queue();
    };
    const stop = () => { gen++; mo?.disconnect(); mo = null; pendTree.clear(); pendAttr.clear(); revert(); };
    return {
        set(v) {
            v = !!v;
            if (v === on) { if (on && Date.now() - lastSweep > 10000) queue(); return; }
            on = v;
            on ? start() : stop();
        }
    };
})();
const markHostDfb = on => {
    try { document.getElementById('bc-unchained-host')?.toggleAttribute('data-dfb', !!on); } catch {}
};
const DARK_M_SHEETS = [['bc-unchained-dark-m', DARK_M_CSS], ['bc-unchained-dark-fix', DARK_FIX_CSS], ['bc-unchained-dark-all', DARK_ALL_CSS]];
const darkBcMobile = (root, on) => {
    DarkSweep.set(on);
    markHostDfb(false);
    if (on) DARK_M_SHEETS.forEach(([id, css]) => addStyle(id, css, root));
    setCls(root, 'bc-dark-m', on);
    setCls(root, 'bc-dark-fb', false);
    syncOpaque(false);
};
const darkGeneric = (root, on) => {
    if (on) addStyle('bc-unchained-dark-css', DARK_FB_CSS, root);
    if (on) addStyle('bc-unchained-dark-css-wk', DARK_FB_CSS_WK, root);
    setCls(root, 'bc-dark-fb', on);
    markHostDfb(on);
    syncOpaque(on);
};
const PLAT_UA = /Android|iPhone|iPad|iPod|Mobile|Opera Mini|IEMobile/i;
let platFinal = false;
const platGuess = () => {
    const h = Store.get('bc_plat', null);
    return h === 'm' || h === 'd' ? h : (PLAT_UA.test(navigator.userAgent) ? 'm' : 'd');
};
const platNow = () => (document.body?.classList.contains('ui-mobile-viewport') ? 'm' : platFinal ? 'd' : platGuess());
const rememberPlat = p => { if (Store.get('bc_plat', null) !== p) Store.set('bc_plat', p); };
const applyDark = () => {
    const b = document.body, root = document.documentElement;
    if (!b || !root) return;
    const on = AppState.dark;
    if (!BC_RE.test(location.hostname)) return darkGeneric(root, on);
    const p = platNow();
    if (platFinal || b.classList.contains('ui-mobile-viewport')) rememberPlat(p);
    if (p === 'd') {
        if (root.classList.contains('bc-dark-m')) darkBcMobile(root, false);
        darkBcDesktop(b, on);
    } else darkBcMobile(root, on);
};
(() => {
    const start = () => {
        try { applyDark(); } catch {}
        let q = 0;
        const mo = new MutationObserver(() => {
            if (!AppState.dark || q) return;
            q = requestAnimationFrame(() => { q = 0; if (AppState.dark) try { applyDark(); } catch {} });
        });
        mo.observe(document.body, { attributes: true, attributeFilter: ['class'] });
        mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
        const settle = () => { platFinal = true; try { applyDark(); } catch {} };
        if (document.readyState === 'complete') settle();
        else window.addEventListener('load', settle, { once: true });
    };
    if (document.body) return start();
    const mo = new MutationObserver(() => { if (document.body) { mo.disconnect(); start(); } });
    mo.observe(document.documentElement, { childList: true });
})();

const REC_RES = { hi: '1080p', mid: '720p', low: '480p', min: '360p' };
const REC_Q = { hi: [6000000, 128000], mid: [3500000, 128000], low: [2000000, 96000], min: [1000000, 64000] };
const REC_PART = [50, 100, 150, 300];
const REC_TOTAL = [0, 512, 1024, 2048, 4096];
const REC_H = { hi: Infinity, mid: 720, low: 480, min: 360 };
const recNorm = c => {
    c = (c && typeof c === 'object' && !Array.isArray(c)) ? c : {};
    return { q: REC_Q[c.q] ? c.q : 'hi', part: REC_PART.includes(c.part) ? c.part : 150, total: REC_TOTAL.includes(c.total) ? c.total : 0, fmt: c.fmt === 'webm' ? 'webm' : 'mp4', src: c.src === 'cap' ? 'cap' : 'hls' };
};
const recCfg = () => recNorm(Store.get('bc_rec', {}));
const fmtSize = (b, mb, gb) => { const m = b / 1048576; return m >= 1024 ? `${(m / 1024).toFixed(2)}${gb}` : `${m.toFixed(1)}${mb}`; };
const fmtShort = b => fmtSize(b, 'M', 'G');
const fmtBytes = b => { const l = L(); return fmtSize(b, ` ${l.u_mb}`, ` ${l.u_gb}`); };

const BK_CHECK = {
    cols: v => Number.isInteger(v) && v >= 1 && v <= 2,
    b: v => typeof v === 'boolean',
    scale: v => typeof v === 'number' && v >= 0.6 && v <= 1.5,
    css: v => typeof v === 'string' && /^-?\d+(\.\d+)?(px|%)$/.test(v),
    color: v => typeof v === 'string' && v.length < 60 && /^(rgba?|hsla?)\([\d\s.,%]+\)$/.test(v),
    lang: v => LANGS.includes(v),
    sort: v => typeof v === 'string' && /^[a-z]{1,12}$/.test(v),
    spd: v => v === 0 || v === 500 || v === 1000,
    wf: v => v === -1 || v === 0 || v === 1,
    lay: v => Number.isInteger(v) && v >= 0 && v <= 3,
    ctext: v => typeof v === 'string' && v.length <= 80,
    keys: v => Array.isArray(v) && v.length <= 2000 && v.every(x => typeof x === 'string' && x.length <= 160),
    hsort: v => v === 'last' || v === 'time' || v === 'visits',
    skin: v => v === 'modern' || v === 'classic',
    mvdvr: v => Array.isArray(MV_DVR_OPTS) && MV_DVR_OPTS.includes(v),
    vol: v => typeof v === 'number' && v >= 0 && v <= 1,
    cbmode: v => v === 'off' || v === 'anim' || v === 'video',
    cbh: v => v === 240 || v === 360 || v === 480 || v === 720
};
const BACKUP_SCHEMA = {
    bc_bg: 'b', bc_min: 'b', bc_hid: 'b', bc_pos: 'b', bc_gmute: 'b', bc_dark: 'b',
    bc_bm_gal: 'b', bc_bm_hide_topic: 'b', bc_bm_hide_grp: 'b', bc_bm_hide_ptags: 'b', bc_bm_hide_note: 'b', bc_bm_hide_priv: 'b', bc_bm_hide_group: 'b', bc_bm_hide_vip: 'b', bc_bm_hide_away: 'b', bc_bm_hide_off: 'b', bc_bm_sort_rev: 'b', bc_previews: 'b', bc_favnav: 'b',
    bc_scale: 'scale', bc_x: 'css', bc_y: 'css', bc_fx: 'css', bc_fy: 'css', bc_fb: 'css',
    bc_theme: 'color', bc_lang: 'lang', bc_bm_sort_type: 'sort', bc_prev_spd: 'spd',
    bc_hide_cfg: 'hcfg', bc_hide_custom: 'hcustom', bc_rec: 'rec', bc_lang_auto: 'b',
    bc_hist: 'hist', bc_hist_ch: 'b', bc_btnhide: 'btns', bc_star: 'b', bc_hist_on: 'b', bc_bm_cols_g: 'cols', bc_bm_cols_l: 'cols',
    bc_bm_wf: 'wf', bc_cal_wf: 'wf', bc_cal_lay: 'lay', bc_cal_grp: 'ctext', bc_cal_tag: 'ctext', bc_cal_models: 'keys', bc_hist_sort: 'hsort',
    bc_mv_auto: 'b', bc_mv_bg: 'b', bc_mv_hide_off: 'b', bc_mv_hide_priv: 'b', bc_mv_hide_group: 'b', bc_mv_hide_vip: 'b', bc_mv_hide_away: 'b', bc_rad: 'b', bc_rad_x: 'css', bc_rad_y: 'css', bc_skin: 'skin',
    bc_mv_abr: 'b', bc_mv_dvr: 'mvdvr', bc_mv_vol: 'vol', bc_mv: 'mvlist',
    cbhp_mode: 'cbmode', cbhp_sound: 'b', cbhp_maxh: 'cbh'
};
const safeSel = x => {
    if (typeof x !== 'string' || !x.trim() || x.length > 400 || /[{};@]|\/\*/.test(x)) return false;
    try { document.querySelector(x); return true; } catch { return false; }
};
const cleanHideCfg = o => {
    if (!o || typeof o !== 'object' || Array.isArray(o)) return null;
    const out = {};
    HIDE_CATS.forEach(c => { if (typeof o[c.id] === 'boolean') out[c.id] = o[c.id]; });
    return out;
};
const cleanHideCustom = o => {
    if (!o || typeof o !== 'object' || Array.isArray(o)) return null;
    const out = {};
    Object.keys(o).forEach(k => {
        if (!/^[a-z0-9.-]{1,40}$/i.test(k) || !Array.isArray(o[k])) return;
        out[k] = o[k].filter(safeSel).slice(0, 200);
    });
    return out;
};
const histNum = v => Number.isFinite(v) && v >= 0 ? v : 0;
const cleanHist = o => {
    if (!o || typeof o !== 'object' || Array.isArray(o)) return null;
    const m = {}, list = [];
    const src = o.m && typeof o.m === 'object' && !Array.isArray(o.m) ? o.m : {};
    Object.keys(src).forEach(k => {
        const e = src[k];
        if (!/^[a-z0-9.-]{1,40}\|.{1,120}$/i.test(k) || !e || typeof e !== 'object' || !isStr(e.u) || !isSafeUrl(e.u)) return;
        m[k] = { n: clip(e.n, 100), u: normUrl(e.u), a: toHttps(e.a), s: histNum(e.s), v: histNum(e.v), f: histNum(e.f), l: histNum(e.l) };
    });
    (Array.isArray(o.s) ? o.s : []).forEach(x => {
        if (x && typeof x === 'object' && isStr(x.k) && m[x.k]) list.push({ k: x.k, n: clip(x.n, 100), t: histNum(x.t), d: histNum(x.d) });
    });
    return { m, s: list };
};
const mergeHist = (a, b) => {
    const cur = cleanHist(a) || { m: {}, s: [] }, inc = cleanHist(b) || { m: {}, s: [] };
    Object.keys(inc.m).forEach(k => { if (!cur.m[k] || cur.m[k].s < inc.m[k].s) cur.m[k] = inc.m[k]; });
    const seen = new Set(cur.s.map(x => `${x.k}|${x.t}`));
    inc.s.forEach(x => { if (cur.m[x.k] && !seen.has(`${x.k}|${x.t}`)) cur.s.push(x); });
    cur.s.sort((x, y) => x.t - y.t);
    return cur;
};
const cleanBtns = o => {
    if (!o || typeof o !== 'object' || Array.isArray(o)) return null;
    const out = {};
    Object.keys(o).forEach(k => { if (/^[A-Za-z]{2,12}$/.test(k) && o[k] === true) out[k] = true; });
    return out;
};
const cleanMvList = a => Array.isArray(a) ? a.filter(e => isObj(e) && (e.t === 'bm' || e.t === 'url') && isStr(e.url) && e.url.length <= 600 && isSafeUrl(e.url)).slice(0, MV_MAX).map(e => ({ t: e.t, name: clip(e.name, 100), url: e.url })) : null;
const BK_CLEAN = { mvlist: cleanMvList, hist: cleanHist, btns: cleanBtns, hcfg: cleanHideCfg, hcustom: cleanHideCustom, rec: o => (o && typeof o === 'object' && !Array.isArray(o)) ? recNorm(o) : null };
const bkValue = (kind, v) => BK_CLEAN[kind] ? BK_CLEAN[kind](v) : (BK_CHECK[kind](v) ? v : null);
const restoreSettings = src => Object.entries(BACKUP_SCHEMA).reduce((n, [k, kind]) => {
    if (!Object.prototype.hasOwnProperty.call(src, k)) return n;
    const v = bkValue(kind, src[k]);
    if (v == null) return n;
    Store.set(k, k === 'bc_hist' ? mergeHist(Store.get('bc_hist', {}), v) : v);
    return n + 1;
}, 0);
const Backup = {
    parts() {
        const p = Store.get('bc_bk_parts', {});
        const o = { s: p.s !== false, f: p.f !== false, h: p.h !== false };
        return o.s || o.f || o.h ? o : { s: true, f: true, h: true };
    },
    setPart(k, on) {
        const p = this.parts();
        p[k] = on;
        if (!p.s && !p.f && !p.h) return false;
        Store.set('bc_bk_parts', p);
        return true;
    },
    build() {
        try { syncBookmarks(); } catch {}
        const p = this.parts();
        const out = { app: 'Cams Unchained', ver: 2, date: new Date().toISOString() };
        if (p.s) {
            const settings = {};
            Object.keys(BACKUP_SCHEMA).forEach(k => { if (k === 'bc_hist') return; const v = Store.get(k, null); if (v !== null) settings[k] = v; });
            out.settings = settings;
        }
        if (p.f) out.bookmarks = AppState.bookmarks.map(stripBm);
        if (p.h) out.history = histLoad();
        return out;
    },
    export() {
        const blob = new Blob([JSON.stringify(this.build(), null, 2)], { type: 'application/json' });
        Helpers.download(blob, `BC_Backup_${Helpers.getTimestamp().slice(0, 10)}.json`);
    },
    exportFavCsv() {
        const rows = [['name', 'url', 'group', 'note', 'pinned', 'last_seen'],
            ...AppState.bookmarks.map(b => [b.name, b.url, b.g || '', b.note || '', b.pin ? 1 : 0, b.seen ? fmtStamp(b.seen) : ''])];
        Helpers.download(new Blob([csvText(rows)], { type: 'text/csv;charset=utf-8' }), `BC_Favorites_${Helpers.getTimestamp().slice(0, 10)}.csv`);
    },
    apply(data) {
        const p = this.parts();
        const favs = list => (p.f && Array.isArray(list) ? mergeBookmarks(list) : 0);
        if (Array.isArray(data)) return { n: 0, added: favs(data) };
        if (data?.app !== 'Cams Unchained' && data?.app !== 'BongaCams Unchained') throw new Error('bad backup');
        const n = p.s ? restoreSettings(isObj(data.settings) ? data.settings : {}) : 0;
        const added = favs(data.bookmarks);
        if (p.h && isObj(data.history)) { const hc = cleanHist(data.history); if (hc) histMerge(hc); }
        return { n, added };
    },
    import(onOk, onErr) {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';
        input.style.display = 'none';
        (document.body || document.documentElement).appendChild(input);
        input.oncancel = () => input.remove();
        input.onchange = async e => {
            try {
                const file = e.target.files?.[0];
                if (!file) return;
                if (file.size > 3e7) throw new Error('too big');
                onOk(this.apply(JSON.parse(await file.text())));
            } catch { onErr(); } finally { input.remove(); }
        };
        input.click();
    }
};
const LANG_SEG = /^(?:ru|uk|ua|en|es|de|fr|pt|it|pl|tr|nl|cs|ro|hu|bg|el|ja|zh|ko|he|ar|hi|id|vi|th)$/i;
const bcSlug = seg => {
    while (seg.length > 1 && LANG_SEG.test(seg[0])) seg.shift();
    if (/^profile$/i.test(seg[0] || '')) seg.shift();
    const s = seg[0] || '';
    return BC_SYS.test(s) ? '' : s.toLowerCase();
};
const plainSlug = (sys, valid) => seg => {
    const s = seg[0] || '';
    return sys.test(s) || !valid.test(s) ? '' : s;
};
const SLUG_BY_SITE = new Map([['bc', bcSlug], ['sc', plainSlug(SC_SYS, /^[\w.-]+$/)], ['cb', plainSlug(CB_SYS, /^\w+$/)]]);
const firstSeg = seg => seg[0] || '';
const modelSlug = u => {
    try {
        const x = new URL(u);
        const seg = x.pathname.split('/').filter(Boolean).map(safeDecode);
        return (SLUG_BY_SITE.get(siteKey(x.hostname)) || firstSeg)(seg);
    } catch { return ''; }
};
const bmKeyRaw = u => { const s = modelSlug(u); return s ? `${urlSite(u)}|${s.toLowerCase()}` : String(u); };
const bmKeyCache = new Map();
const bmKey = u => {
    if (typeof u !== 'string') return bmKeyRaw(u);
    let k = bmKeyCache.get(u);
    if (k === undefined) {
        if (bmKeyCache.size > 3000) bmKeyCache.clear();
        k = bmKeyRaw(u);
        bmKeyCache.set(u, k);
    }
    return k;
};
const pageKey = () => { const s = modelSlug(location.href); return s ? `${SITE}|${s.toLowerCase()}` : ''; };
const normUrl = (u, host) => {
    try {
        if (!isSafeUrl(u)) return u;
        const x = new URL(u), s = modelSlug(u);
        return s ? `https://${host || x.host}/${encodeURIComponent(s)}` : x.href;
    } catch { return u; }
};
const linkFor = u => {
    try {
        const host = urlSite(u) === siteKey(location.hostname) ? location.host : new URL(u).host;
        return normUrl(u, host);
    } catch { return u; }
};
const isFavKey = k => !!k && AppState.bookmarks.some(b => bmKey(b.url) === k);
const isFavName = name => !!name && (AppState.bookmarks.some(b => sameBm(b, name)) || isFavKey(pageKey()));
const BM_FILL_KEYS = ['note', 'g', 'avatar'];
const absorbBm = (prev, b) => {
    BM_FILL_KEYS.forEach(k => { if (!prev[k] && b[k]) prev[k] = b[k]; });
    if (b.pin) prev.pin = true;
    prev.seen = Math.max(prev.seen || 0, b.seen || 0);
};
const migrateBookmarks = () => {
    const map = new Map();
    const before = AppState.bookmarks.length;
    let changed = false;
    AppState.bookmarks.forEach((b, i) => {
        if (!b || !isStr(b.url)) { map.set(`#${i}`, b); return; }
        const nu = normUrl(b.url);
        if (nu !== b.url) { b.url = nu; changed = true; }
        const k = bmKey(b.url), prev = map.get(k);
        if (!prev) { map.set(k, b); return; }
        changed = true;
        absorbBm(prev, b);
    });
    if (changed) {
        AppState.bookmarks = [...map.values()].filter(Boolean);
        saveBookmarks();
    }
    return before - AppState.bookmarks.length;
};
migrateBookmarks();

const readFb = () => {
    const fb = Store.get('bc_fb', null);
    if (fb) return fb;
    const fy = Store.get('bc_fy', null);
    if (!fy || window.innerHeight < 200) return null;
    const v = `${Math.max(0, Math.round(window.innerHeight - parseFloat(fy) - 50))}px`;
    Store.set('bc_fb', v);
    return v;
};
const injectSiteCSS = () => {
    if (document.getElementById('bc-unchained-site-css')) return;
    const fx = Store.get('bc_fx', null), fb = readFb();
    let css = `${currentSite.css || ''} .js-filter_button, .bc_sticky_bar { touch-action: none !important; user-select: none !important; -webkit-user-drag: none !important; }
.bc_sticky_bar .bc_filter_button,.bc_sticky_bar .js-filter_button{animation:none!important;transition:none!important;transform:none!important;translate:none!important;opacity:1!important;top:auto!important;bottom:auto!important;inset-block:auto!important}
html.bc-theater-html{background:#000!important;overflow:hidden!important}
body.bc-theater-mode{overflow:hidden!important;background:#000!important;margin:0!important;padding:0!important}
body.bc-theater-mode :is(.chat_panel_wrap,.player_footer,[class*="chat-panel"],[class*="chat_panel"],[class*="player-footer"],#chat_bar_v2_container,#chat_bar_v2,#switch_between_chat,.chat-tabs,[class*="chat_nav"],[class*="chat_bar"],#games_button,.mls_title,.live_tabs,.page_header,.header_bar,.chat-wrapper,.bc_chat_header,.memberBalance,.memberNavigation,.private_btn_wrp,#bPaidButtonTopRow,#profile-content,.stream-controls,#top_banner_above_chat,[class*="footer"]:not(#bc-unchained-host *)){display:none!important}
body.bc-theater-mode .bc-thtr-anc{transform:none!important;translate:none!important;scale:none!important;rotate:none!important;filter:none!important;backdrop-filter:none!important;perspective:none!important;contain:none!important;overflow:visible!important;clip-path:none!important;mask:none!important;will-change:auto!important}
body.bc-theater-mode .bc-thtr-wrap{position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;width:100dvw!important;height:100dvh!important;max-width:none!important;max-height:none!important;z-index:2147483644!important;background:#000!important;margin:0!important;padding:0!important;transform:none!important;display:block!important}
body.bc-theater-mode video.bc-thtr-video{position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;width:100dvw!important;height:100dvh!important;max-width:none!important;max-height:none!important;aspect-ratio:auto!important;z-index:2147483646!important;object-fit:contain!important;background:#000!important;margin:0!important;padding:0!important;transform:none!important;translate:none!important;rotate:none!important;scale:none!important}`;
    if (fx && fb) {
        css += `.bc_sticky_bar:not([data-bcu]){visibility:hidden!important}.bc_sticky_bar{left:${fx}!important;top:auto!important;bottom:${fb}!important;transform:none!important;margin:0!important;right:auto!important;width:max-content!important;transition:none!important;animation:none!important}`;
    }
    addStyle('bc-unchained-site-css', css, document.documentElement);
};
injectSiteCSS();
applyHideCSS();

const LOCK_SEL = '.fp_outer,.fp_outer_overlay,.fp_inner_wrap';
const unlockScroll = () => {
    if (!BC_RE.test(location.hostname) || !document.documentElement) return;
    const root = document.documentElement;
    const locked = !!document.querySelector(LOCK_SEL);
    if (!locked) { root.classList.remove('bc-unlock'); return; }
    addStyle('bc-unchained-unlock-css', 'html.bc-unlock,html.bc-unlock body{overflow-y:auto!important;overflow-x:hidden!important;position:relative!important;top:auto!important;height:auto!important;touch-action:pan-y pinch-zoom!important;overscroll-behavior:auto!important}', root);
    root.classList.add('bc-unlock');
    [root, document.body].forEach(el => {
        if (!el) return;
        [...el.classList].forEach(c => { if (/^(fp_|__fp|noscroll|no_scroll|no-scroll|scroll_lock|scroll-lock|modal_open|modal-open|popup_open|has_popup)/i.test(c)) el.classList.remove(c); });
    });
};
if (!window.__bcUnlockBound) {
    window.__bcUnlockBound = true;
    const guard = e => {
        if (!document.documentElement.classList.contains('bc-unlock')) return;
        const path = e.composedPath ? e.composedPath() : [];
        if (path.some(n => n && (n.id === 'bc-unchained-host' || n.id === 'bc-cb-qual'))) return;
        e.stopImmediatePropagation();
    };
    ['touchmove', 'wheel', 'scroll'].forEach(ev => window.addEventListener(ev, guard, { capture: true, passive: true }));
}
const MPOP_ATTR = 'data-bc-mpop';
const MPOP_INPUTS = 'input.sendInput,textarea.sendInput,input.js-chat_msg_input,input.ccp_input';
const MPOP_SKIP = '#bChat,#bChatInputContainer,.ccp_wrapper,.chat_panel_wrap,#chat_bar_v2_container,[' + MPOP_ATTR + ']';
const isRootEl = p => p === document.body || p === document.documentElement;
const findFixedAncestor = inp => {
    for (let p = inp.parentElement; p && !isRootEl(p); p = p.parentElement) {
        if (getComputedStyle(p).position === 'fixed') return p;
    }
    return null;
};
const findBodyChild = inp => {
    let p = inp;
    while (p.parentElement && p.parentElement !== document.body) p = p.parentElement;
    return p.parentElement === document.body ? p : null;
};
const popupTarget = inp => {
    if (inp.id === 'send-input-container' || inp.closest(MPOP_SKIP)) return null;
    return findFixedAncestor(inp) || findBodyChild(inp);
};
const isModelPopup = (target, vw, vh) => {
    if (!target || target === document.body) return false;
    if (target.querySelector('video,#bChat,.chat_panel_wrap,#stream-container')) return false;
    const r = target.getBoundingClientRect();
    return !(r.width > vw * 0.9 && r.height > vh * 0.9);
};
const tagModelPopups = () => {
    if (!BC_RE.test(location.hostname) || !document.body) return;
    const inputs = document.querySelectorAll(MPOP_INPUTS);
    if (!inputs.length) return;
    const vw = window.innerWidth, vh = window.innerHeight;
    const found = new Set();
    inputs.forEach(inp => { const t = popupTarget(inp); if (isModelPopup(t, vw, vh)) found.add(t); });
    if (found.size) requestAnimationFrame(() => found.forEach(t => t.setAttribute(MPOP_ATTR, '1')));
};
(() => {
    let t = 0;
    const run = () => {
        clearTimeout(t); t = 0;
        if (realHidden()) return;
        try { tagModelPopups(); } catch {}
        try { unlockScroll(); } catch {}
    };
    const sched = () => { if (!t) t = setTimeout(run, 150); };
    DomBus.subscribe((recs, isOwn) => { if (!isOwn()) sched(); });
    onPageEvent('visibilitychange', () => { if (!realHidden()) sched(); });
    setInterval(run, 5000);
    sched();
})();

const COLS_KEY = 'bc_bm_cols_g';
const colsGet = () => (Store.get(COLS_KEY, 1) === 2 ? 2 : 1);
const pSvg = (d, s = 20, fill = 'none', stroke = 'currentColor') => `<svg viewBox="0 0 24 24" width="${s}" height="${s}" stroke="${stroke}" fill="${fill}" stroke-width="2">${d}</svg>`;
const COLS_ICONS = [
    pSvg('<rect x="4" y="5" width="16" height="14" rx="2.5"/>', 18),
    pSvg('<rect x="3" y="5" width="7.5" height="14" rx="2"/><rect x="13.5" y="5" width="7.5" height="14" rx="2"/>', 18)
];
const icons = {
    coffee: pSvg('<g stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 10h13v5.5a4.5 4.5 0 0 1-4.5 4.5h-4A4.5 4.5 0 0 1 4 15.5z\"/><path d=\"M17 12h1.5a2.5 2.5 0 0 1 0 5H17\"/><path d=\"M7 8c-1-1.2-1-2.3 0-3.5s1-2.3 0-3.5\"/><path d=\"M11 8c-1-1.2-1-2.3 0-3.5s1-2.3 0-3.5\"/><path d=\"M15 8c-1-1.2-1-2.3 0-3.5s1-2.3 0-3.5\"/></g>', 16),
    color: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a9 9 0 0 0 0 18c1.2 0 2-.9 2-2 0-.6-.3-1-.6-1.4-.3-.4-.4-.8-.4-1.2 0-1.1.9-2 2-2H17a4 4 0 0 0 4-4c0-4.5-4-7.4-9-7.4z"/><circle cx="7.5" cy="11" r="1" fill="currentColor"/><circle cx="10" cy="7" r="1" fill="currentColor"/><circle cx="15" cy="7" r="1" fill="currentColor"/></g>', 14),
    menu: pSvg('<circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/>', 24),
    eye: pSvg('<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>', 14),
    close: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9 9l6 6M15 9l-6 6"/></g>', 16),
    home: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M4 10.5V19a1.5 1.5 0 0 0 1.5 1.5H9v-5.5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v5.5h3.5A1.5 1.5 0 0 0 20 19v-8.5"/><path d="M2.5 11.5L12 3.5l9.5 8"/></g>', 16),
    rel: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 4.5V9h-4.5"/></g>', 16),
    min: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M6 9.5l6 6 6-6"/></g>', 16),
    bgOn: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9.5"/><polygon points="10 8.5 10 15.5 16 12" fill="currentColor"/></g>'),
    bgOff: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9.5"/><polygon points="10 8.5 10 15.5 16 12" fill="currentColor"/><line x1="3" y1="3" x2="21" y2="21"/></g>'),
    vol: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M3 9.5h3.2L11 6v12l-4.8-3.5H3z"/><path d="M15 9.5v5M18.5 7v10M22 10.5v3"/></g>'),
    mute: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M3 9.5h3.2L11 6v12l-4.8-3.5H3z"/><path d="M16 9.5l5 5M21 9.5l-5 5"/></g>'),
    volLow: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M3 9.5h3.2L11 6v12l-4.8-3.5H3z"/><path d="M15 9.5v5"/></g>'),
    volMed: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M3 9.5h3.2L11 6v12l-4.8-3.5H3z"/><path d="M15 9.5v5M18.5 7v10"/></g>'),
    snap: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M3 8V5a2 2 0 0 1 2-2h3M16 3h3a2 2 0 0 1 2 2v3M21 16v3a2 2 0 0 1-2 2h-3M8 21H5a2 2 0 0 1-2-2v-3"/><circle cx="12" cy="12" r="3.5"/></g>'),
    rec: pSvg('<g><rect x="2" y="6" width="20" height="12" rx="6"/><circle cx="12" cy="12" r="3.5" fill="currentColor" stroke="none"/></g>'),
    qual: pSvg('<rect x="2" y="5" width="20" height="14" rx="3"/><path d="M6.5 9v6M11 9v6M6.5 12H11M14.5 9v6h1.8a2.7 2.7 0 0 0 2.7-2.7v-.6A2.7 2.7 0 0 0 16.3 9z"/>'),
    pip: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="4.5" width="19" height="15" rx="2.5"/><rect x="12" y="11.5" width="6" height="4.5" rx="1" fill="currentColor" stroke="none"/></g>'),
    fs: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3"/></g>'),
    thtr: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="6" width="20" height="12" rx="2"/><rect x="5" y="9" width="14" height="6" rx="1" fill="currentColor"/></g>'),
    search: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><circle cx="10.5" cy="10.5" r="7"/><path d="M16 16l5 5M10.5 7.5v6M7.5 10.5h6"/></g>'),
    copy: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></g>'),
    fav: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h12a1 1 0 0 1 1 1v17l-7-4.5L5 21V4a1 1 0 0 1 1-1z"/><polygon points="12.00 7.30 13.00 9.82 15.71 9.99 13.62 11.73 14.29 14.36 12.00 12.90 9.71 14.36 10.38 11.73 8.29 9.99 11.00 9.82" fill="currentColor" stroke="currentColor" stroke-width="0.8"/></g>'),
    favPrev: pSvg('<polygon points="19 20 9 12 19 4 19 20"/><line x1="5" y1="19" x2="5" y2="5"/>'),
    favNext: pSvg('<polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19"/>'),
    del: pSvg('<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/>', 16),
    hls: pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h4.5l2.5-6 4 12 3-8 2 2H22"/></g>'),
    get ok() { return pSvg('<polyline points="20 6 9 17 4 12"/>', 16, 'none', SK('#34d399', '#2ecc71')); },
    arrowUp: pSvg('<line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/>', 12),
    arrowDown: pSvg('<line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/>', 12),
    back: pSvg('<polyline points="15 18 9 12 15 6"/>', 18),
    cam: pSvg('<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/>', 12),
    clock: pSvg('<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>', 12),
    name: pSvg('<path d="M4 20l5-12 5 12"/><path d="M6 15h6"/>', 12),
    get dot() { const c = SK('#10b981', '#2ecc71'); return pSvg('<circle cx="12" cy="12" r="7"/>', 12, c, c); },
    list: pSvg('<line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/>', 12),
    grid: pSvg('<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>', 12)
};
icons.sleep = pSvg('<circle cx="12" cy="13" r="8"/><polyline points="12 9 12 13 15 15"/><line x1="9" y1="2" x2="15" y2="2"/>');
icons.cal = pSvg('<rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>', 15);
icons.circle = pSvg('<circle cx="12" cy="12" r="9"/>', 16);
icons.check = pSvg('<polyline points="20 6 9 17 4 12"/>', 16);
icons.eyeNo = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><path d="M9 9l6 6M15 9l-6 6"/></g>', 16);
icons.eyeOk = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><path d="M8.3 12.2l2.7 2.7 4.8-5.4"/></g>', 16);
icons.tag = pSvg('<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><circle cx="7" cy="7" r="1"/>', 12);
icons.folder = pSvg('<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>', 12);
icons.chevL = pSvg('<polyline points="15 18 9 12 15 6"/>', 18);
icons.chevR = pSvg('<polyline points="9 18 15 12 9 6"/>', 18);
icons.boxOn = pSvg('<rect x="3" y="3" width="18" height="18" rx="4"/><polyline points="8 12 11 15 16 9"/>', 16);
icons.boxOff = pSvg('<rect x="3" y="3" width="18" height="18" rx="4"/>', 16);
icons.undo = pSvg('<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>', 14);

icons.eyeOff = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/><line x1="4" y1="20" x2="20" y2="4"/></g>', 14);
icons.sun = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4.5"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></g>', 14);
icons.moon = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/><path d="M18 1.5v3M16.5 3h3"/></g>', 14);
icons.dl = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9.5"/><path d="M12 7.5v8M8.5 12.5l3.5 3.5 3.5-3.5"/></g>', 14);
icons.ul = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="2.5" width="19" height="19" rx="5"/><path d="M12 16.5v-8M8.5 12l3.5-3.5 3.5 3.5"/></g>', 14);
icons.target = pSvg('<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>', 14);
icons.globe = pSvg('<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>', 15);
icons.dice = pSvg('<rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1"/><circle cx="15.5" cy="8.5" r="1"/><circle cx="8.5" cy="15.5" r="1"/><circle cx="15.5" cy="15.5" r="1"/>', 15);
icons.gear = pSvg('<g stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M3 12h18M3 18h18"/><circle cx="15" cy="6" r="2.2" fill="currentColor"/><circle cx="8" cy="12" r="2.2" fill="currentColor"/><circle cx="17" cy="18" r="2.2" fill="currentColor"/></g>', 15);
icons.star = pSvg('<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>', 16);
icons.history = pSvg('<path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><polyline points="3 3 3 8 8 8"/><polyline points="12 7 12 12 15 14"/>', 15);
icons.bookmark = pSvg('<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>', 15);
icons.table = pSvg('<rect x="3" y="4" width="18" height="16" rx="2"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="3" y1="15" x2="21" y2="15"/><line x1="9" y1="4" x2="9" y2="20"/>', 15);

const i18n = {
    RU: { drag: '⁞⁞', bg: 'ФОН', sound: 'ЗВУК', snap: 'СКРИН', rec: 'ЗАПИСЬ', err: 'ОШИБКА', no_vid: 'НЕТ ВИДЕО', qual: 'КАЧ-ВО', pip: 'PIP', fs: 'ЭКРАН', thtr: 'ТЕАТР', search: 'ПОИСК', copy: 'ССЫЛКА', home: 'Домой', rel: 'Рестарт', min: 'Свернуть', hide: 'Скрыть (H)', hints: 'M=ЗВУК F=ЭКРАН T=ТЕАТР H=СКРЫТЬ', favs: 'ИЗБРАННОЕ', no_model: 'Модель не найдена', bm_title: 'Избранное', bm_add: 'В избранное', bm_empty: 'Список пуст.', bm_dup: 'Уже добавлено!', note_ph: 'Заметка...', hd_title: 'Скрытие', hd_cats: 'Категории', hd_rules: 'Мои правила', hd_none: 'Правил пока нет.', hd_pick: 'Выбрать элемент', hd_reset: 'Сбросить для этого сайта', hd_all: 'Скрыть всё', hd_peek: 'Удерживайте пункт, чтобы увидеть, что он скрывает', hd_na: 'пока не найдено', hd_off: 'Показать всё', hd_clear: 'Удалить все мои правила', pk_hint: 'Коснитесь элемента на странице', pk_up: 'Выше', pk_ok: 'Скрыть', pk_cancel: 'Отмена', pk_found: 'совп.', hd_p_min: 'Минимум', hd_p_clean: 'Чистый плеер', dark: 'Тёмная / светлая тема', bk_exp: 'Экспорт резервной копии', bk_imp: 'Импорт резервной копии' },
    EN: { drag: '⁞⁞', bg: 'BG', sound: 'VOL', snap: 'SNAP', rec: 'REC', err: 'ERROR', no_vid: 'NO VIDEO', qual: 'QUAL', pip: 'PIP', fs: 'FULL', thtr: 'THTR', search: 'SEARCH', copy: 'LINK', home: 'Home', rel: 'Reload', min: 'Minimize', hide: 'Hide (H)', hints: 'M=MUTE F=FULL T=THTR H=HIDE', favs: 'FAVS', no_model: 'Model not found', bm_title: 'Favorites', bm_add: 'Add current', bm_empty: 'List is empty.', bm_dup: 'Already added!', note_ph: 'Note...', hd_title: 'Hiding', hd_cats: 'Categories', hd_rules: 'My rules', hd_none: 'No rules yet.', hd_pick: 'Pick element', hd_reset: 'Reset for this site', hd_all: 'Hide all', hd_peek: 'Hold an item to see what it hides', hd_na: 'not found yet', hd_off: 'Show all', hd_clear: 'Delete all my rules', pk_hint: 'Tap an element on the page', pk_up: 'Up', pk_ok: 'Hide', pk_cancel: 'Cancel', pk_found: 'matches', hd_p_min: 'Minimal', hd_p_clean: 'Clean player', dark: 'Dark / light theme', bk_exp: 'Export backup', bk_imp: 'Import backup' }
};
const unpackI18N = ({ k, v }) => Object.fromEntries(Object.entries(v).map(([lang, a]) => [lang, Object.fromEntries(k.map((key, i) => [key, a[i]]))]));
const I18N_X = Object.fromEntries(Object.entries({"NEW":{"k":["note_ph","sort_date","sort_name","sort_live","sort_last","bm_online","bm_online_t","bm_priv","bm_all","bm_gal","bm_list","bm_nomatch","bm_del","tp_more","tp_less","don_tip","don_title","don_sub","don_copied","don_fail","don_tap","st_private","st_group","st_vip","st_away","grp_none","grp_new","grp_all","seen_tip","seen_now","ago","u_m","u_h","u_d","u_mb","u_gb","lang_title","lang_auto","rec_tip","rs_title","rs_quality","rs_q_hi","rs_q_mid","rs_q_low","rs_q_min","rs_part","rs_total","rs_off","rs_upto","rs_mbmin","rs_gbh","rs_now","rs_p","rs_t","rs_hint","rs_next","bm_find","bm_pin","bm_unpin"],"v":{"RU":["Заметка... (#теги)","Дата","Имя","Лайв","Был(а)","Онлайн","Показывать только Онлайн","Приват","Все","Галерея","Список","Нет моделей по выбранному фильтру","Удалить","[Ещё]","[Свернуть]","Поддержать автора","Угостить автора кофе","Спасибо, что пользуешься","Адрес скопирован","Не удалось скопировать","Нажмите на адрес, чтобы раскрыть его целиком. Кнопка справа копирует.","Приват","Группа","VIP","BRB","Без группы","+ Новая группа…","Все группы","Последний раз онлайн","только что","{n} назад","мин","ч","д","МБ","ГБ","Язык","Авто","Нажатие — запись, удержание — настройки","Настройки записи","Качество (битрейт)","Высокое","Среднее","Низкое","Экономия","Размер части (файл делится)","Остановить при общем размере","Выкл.","до","МБ/мин","ГБ/ч","Сейчас","часть","всего","Часть хранится в памяти телефона, пока не сохранится, поэтому меньшие части меньше грузят память.","Качество применяется со следующей части или записи.","Поиск по имени…","Закрепить наверху","Открепить"],"EN":["Note... (#tags)","Date","Name","Live","Seen","Online","Show only Online","Priv","All","Gallery","List","No models match filter","Delete","[More]","[Less]","Support the author","Buy the author a coffee","Thanks for using it","Address copied","Copy failed","Tap an address to expand it. The button on the right copies it.","Private","Group","VIP","BRB","No group","+ New group…","All groups","Last seen online","just now","{n} ago","min","h","d","MB","GB","Language","Auto","Tap: record · Hold: settings","Recording settings","Quality (bitrate)","High","Medium","Low","Saver","Part size (file is split)","Stop at total size","Off","up to","MB/min","GB/h","Now","part","total","A part stays in phone memory until it is saved, so smaller parts use less RAM.","Quality applies from the next part or recording.","Search by name…","Pin to top","Unpin"]}},"LANG":{"k":["drag","bg","sound","snap","rec","err","no_vid","qual","pip","fs","thtr","search","copy","home","rel","min","hide","hints","favs","no_model","bm_title","bm_add","bm_empty","bm_dup","note_ph","hd_title","hd_cats","hd_rules","hd_none","hd_pick","hd_reset","hd_all","hd_peek","hd_na","hd_off","hd_clear","pk_hint","pk_up","pk_ok","pk_cancel","pk_found","hd_p_min","hd_p_clean","dark","sort_date","sort_name","sort_live","sort_last","bm_online","bm_online_t","bm_priv","bm_all","bm_gal","bm_list","bm_nomatch","bm_del","tp_more","tp_less","don_tip","don_title","don_sub","don_copied","don_fail","don_tap","st_private","st_group","st_vip","st_away","grp_none","grp_new","grp_all","seen_tip","seen_now","ago","u_m","u_h","u_d","u_mb","u_gb","lang_title","lang_auto","rec_tip","rs_title","rs_quality","rs_q_hi","rs_q_mid","rs_q_low","rs_q_min","rs_part","rs_total","rs_off","rs_upto","rs_mbmin","rs_gbh","rs_now","rs_p","rs_t","rs_hint","rs_next","bm_find","bm_pin","bm_unpin"],"v":{"UK":["⁞⁞ ПУЛЬТ","ФОН","ЗВУК","СКРІН","ЗАПИС","ПОМИЛКА","НЕМАЄ ВІДЕО","ЯКІСТЬ","PIP","ЕКРАН","ТЕАТР","ПОШУК","ЛІНК","Додому","Перезапуск","Згорнути","Сховати (H)","M=ЗВУК F=ЕКРАН T=ТЕАТР H=СХОВАТИ","ОБРАНЕ","Модель не знайдено","Обране","В обране","Список порожній.","Вже додано!","Нотатка... (#теги)","Приховування","Категорії","Мої правила","Правил поки немає.","Обрати елемент","Скинути для цього сайту","Сховати все","Утримуйте пункт, щоб побачити, що він ховає","поки не знайдено","Показати все","Видалити всі мої правила","Торкніться елемента на сторінці","Вище","Сховати","Скасувати","збіг.","Мінімум","Чистий плеєр","Темна / світла тема","Дата","Імʼя","Лайв","Був(ла)","Онлайн","Показувати лише Онлайн","Приват","Усі","Галерея","Список","Немає моделей за вибраним фільтром","Видалити","[Ще]","[Згорнути]","Підтримати автора","Пригостити автора кавою","Дякую, що користуєшся","Адресу скопійовано","Не вдалося скопіювати","Натисни на адресу, щоб розгорнути її повністю. Кнопка праворуч копіює її.","Приват","Група","VIP","BRB","Без групи","+ Нова група…","Усі групи","Востаннє онлайн","щойно","{n} тому","хв","год","д","МБ","ГБ","Мова","Авто","Дотик — запис, утримання — налаштування","Налаштування запису","Якість (бітрейт)","Висока","Середня","Низька","Економія","Розмір частини (файл ділиться)","Зупинити при загальному розмірі","Вимк.","до","МБ/хв","ГБ/год","Зараз","частина","всього","Частина тримається в памʼяті телефона, доки не збережеться, тому менші частини менше навантажують пам'ять.","Якість діє з наступної частини або запису.","Пошук за іменем…","Закріпити нагорі","Відкріпити"],"ES":["⁞⁞ MANDO","BG","SONIDO","CAPT.","GRABAR","ERROR","SIN VÍDEO","CALID.","PIP","PANT.","CINE","BUSCAR","ENLACE","Inicio","Recargar","Minimizar","Ocultar (H)","M=SONIDO F=PANT T=CINE H=OCULTAR","FAVS","Modelo no encontrada","Favoritos","Añadir actual","La lista está vacía.","¡Ya añadida!","Nota... (#etiquetas)","Ocultar","Categorías","Mis reglas","Aún no hay reglas.","Elegir elemento","Restablecer para este sitio","Ocultar todo","Mantén pulsado un elemento para ver qué oculta","aún no encontrado","Mostrar todo","Borrar todas mis reglas","Toca un elemento de la página","Subir","Ocultar","Cancelar","coinc.","Mínimo","Reproductor limpio","Tema oscuro / claro","Fecha","Nombre","En vivo","Visto","Online","Mostrar solo online","Priv","Todos","Galería","Lista","Ninguna modelo coincide con el filtro","Eliminar","[Más]","[Menos]","Apoyar al autor","Invita al autor a un café","Gracias por usarlo","Dirección copiada","No se pudo copiar","Toca una dirección para verla completa. El botón de la derecha la copia.","Privado","Grupo","VIP","BRB","Sin grupo","+ Nuevo grupo…","Todos los grupos","Última vez online","ahora mismo","hace {n}","min","h","d","MB","GB","Idioma","Automático","Toque: grabar · Mantener: ajustes","Ajustes de grabación","Calidad (bitrate)","Alta","Media","Baja","Ahorro","Tamaño de parte (divide el archivo)","Detener al llegar a","No","hasta","MB/min","GB/h","Ahora","parte","total","Cada parte se guarda en la memoria del teléfono hasta que se descarga; partes más pequeñas usan menos RAM.","La calidad se aplica desde la próxima parte o grabación.","Buscar por nombre…","Fijar arriba","Desfijar"],"DE":["⁞⁞ STEUERUNG","BG","TON","FOTO","AUFN.","FEHLER","KEIN VIDEO","QUAL.","PIP","VOLL","KINO","SUCHE","LINK","Start","Neu laden","Minimieren","Ausblenden (H)","M=TON F=VOLL T=KINO H=AUS","FAVS","Model nicht gefunden","Favoriten","Aktuelles hinzufügen","Die Liste ist leer.","Schon hinzugefügt!","Notiz... (#Tags)","Ausblenden","Kategorien","Meine Regeln","Noch keine Regeln.","Element wählen","Für diese Seite zurücksetzen","Alles ausblenden","Eintrag gedrückt halten, um zu sehen, was er ausblendet","noch nicht gefunden","Alles zeigen","Alle meine Regeln löschen","Tippe auf ein Element der Seite","Höher","Ausblenden","Abbrechen","Treffer","Minimal","Sauberer Player","Dunkles / helles Design","Datum","Name","Live","Zuletzt","Online","Nur Online anzeigen","Priv","Alle","Galerie","Liste","Keine Models passen zum Filter","Löschen","[Mehr]","[Weniger]","Autor unterstützen","Spendiere dem Autor einen Kaffee","Danke fürs Benutzen","Adresse kopiert","Kopieren fehlgeschlagen","Tippe auf eine Adresse, um sie komplett zu sehen. Die Schaltfläche rechts kopiert sie.","Privat","Gruppe","VIP","BRB","Keine Gruppe","+ Neue Gruppe…","Alle Gruppen","Zuletzt online","gerade eben","vor {n}","Min","Std","T","MB","GB","Sprache","Automatisch","Tippen: aufnehmen · Halten: Einstellungen","Aufnahme-Einstellungen","Qualität (Bitrate)","Hoch","Mittel","Niedrig","Sparen","Teilgröße (Datei wird geteilt)","Stopp bei Gesamtgröße","Aus","bis zu","MB/Min","GB/Std","Jetzt","Teil","gesamt","Jeder Teil liegt im Speicher des Handys, bis er gespeichert wird; kleinere Teile brauchen weniger RAM.","Die Qualität gilt ab dem nächsten Teil oder der nächsten Aufnahme.","Nach Namen suchen…","Oben anheften","Anheften aufheben"],"FR":["⁞⁞ COMMANDE","BG","SON","PHOTO","REC","ERREUR","PAS DE VIDÉO","QUAL.","PIP","PLEIN","CINÉMA","RECH.","LIEN","Accueil","Recharger","Réduire","Masquer (H)","M=SON F=PLEIN T=CINÉ H=MASQUER","FAVORIS","Modèle introuvable","Favoris","Ajouter l’actuel","La liste est vide.","Déjà ajouté !","Note... (#tags)","Masquage","Catégories","Mes règles","Aucune règle pour l’instant.","Choisir un élément","Réinitialiser pour ce site","Tout masquer","Maintenez un élément pour voir ce qu’il masque","pas encore trouvé","Tout afficher","Supprimer toutes mes règles","Touchez un élément de la page","Plus haut","Masquer","Annuler","corresp.","Minimal","Lecteur épuré","Thème sombre / clair","Date","Nom","En direct","Vu","En ligne","Afficher seulement en ligne","Privé","Tous","Galerie","Liste","Aucun modèle ne correspond au filtre","Supprimer","[Plus]","[Moins]","Soutenir l’auteur","Offrir un café à l’auteur","Merci de l’utiliser","Adresse copiée","Échec de la copie","Touchez une adresse pour l’afficher en entier. Le bouton à droite la copie.","Privé","Groupe","VIP","BRB","Sans groupe","+ Nouveau groupe…","Tous les groupes","Dernière connexion","à l’instant","il y a {n}","min","h","j","Mo","Go","Langue","Auto","Toucher : enregistrer · Maintenir : réglages","Réglages d’enregistrement","Qualité (débit)","Haute","Moyenne","Basse","Éco","Taille d’une partie (fichier découpé)","Arrêter à la taille totale","Non","jusqu’à","Mo/min","Go/h","Maintenant","partie","total","Chaque partie reste dans la mémoire du téléphone jusqu’à son enregistrement ; des parties plus petites utilisent moins de RAM.","La qualité s’applique dès la prochaine partie ou le prochain enregistrement.","Rechercher par nom…","Épingler en haut","Désépingler"],"PT":["⁞⁞ CONTROLE","BG","SOM","FOTO","GRAVAR","ERRO","SEM VÍDEO","QUAL.","PIP","TELA","CINEMA","BUSCAR","LINK","Início","Recarregar","Minimizar","Ocultar (H)","M=SOM F=TELA T=CINEMA H=OCULTAR","FAVS","Modelo não encontrada","Favoritos","Adicionar atual","A lista está vazia.","Já adicionada!","Nota... (#tags)","Ocultar","Categorias","Minhas regras","Ainda sem regras.","Escolher elemento","Redefinir para este site","Ocultar tudo","Segure um item para ver o que ele oculta","ainda não encontrado","Mostrar tudo","Apagar todas as minhas regras","Toque em um elemento da página","Acima","Ocultar","Cancelar","result.","Mínimo","Player limpo","Tema escuro / claro","Data","Nome","Ao vivo","Visto","Online","Mostrar só online","Priv","Todos","Galeria","Lista","Nenhuma modelo corresponde ao filtro","Excluir","[Mais]","[Menos]","Apoiar o autor","Pague um café ao autor","Obrigado por usar","Endereço copiado","Falha ao copiar","Toque em um endereço para vê-lo completo. O botão à direita o copia.","Privado","Grupo","VIP","BRB","Sem grupo","+ Novo grupo…","Todos os grupos","Última vez online","agora mesmo","há {n}","min","h","d","MB","GB","Idioma","Automático","Toque: gravar · Segurar: ajustes","Ajustes de gravação","Qualidade (bitrate)","Alta","Média","Baixa","Economia","Tamanho da parte (divide o arquivo)","Parar ao atingir","Não","até","MB/min","GB/h","Agora","parte","total","Cada parte fica na memória do celular até ser salva; partes menores usam menos RAM.","A qualidade vale a partir da próxima parte ou gravação.","Buscar por nome…","Fixar no topo","Desafixar"],"IT":["⁞⁞ TELECOMANDO","BG","AUDIO","FOTO","REC","ERRORE","NO VIDEO","QUAL.","PIP","SCHERMO","CINEMA","CERCA","LINK","Home","Ricarica","Riduci","Nascondi (H)","M=AUDIO F=SCHERMO T=CINEMA H=NASCONDI","PREF.","Modella non trovata","Preferiti","Aggiungi attuale","L’elenco è vuoto.","Già aggiunta!","Nota... (#tag)","Nascondi","Categorie","Le mie regole","Ancora nessuna regola.","Scegli elemento","Ripristina per questo sito","Nascondi tutto","Tieni premuto un elemento per vedere cosa nasconde","non ancora trovato","Mostra tutto","Elimina tutte le mie regole","Tocca un elemento della pagina","Su","Nascondi","Annulla","corrisp.","Minimo","Player pulito","Tema scuro / chiaro","Data","Nome","Live","Visto","Online","Mostra solo online","Priv","Tutti","Galleria","Elenco","Nessuna modella corrisponde al filtro","Elimina","[Altro]","[Meno]","Sostieni l’autore","Offri un caffè all’autore","Grazie per l’uso","Indirizzo copiato","Copia non riuscita","Tocca un indirizzo per vederlo per intero. Il pulsante a destra lo copia.","Privato","Gruppo","VIP","BRB","Nessun gruppo","+ Nuovo gruppo…","Tutti i gruppi","Ultimo accesso","proprio ora","{n} fa","min","h","g","MB","GB","Lingua","Automatica","Tocco: registra · Tieni premuto: impostazioni","Impostazioni di registrazione","Qualità (bitrate)","Alta","Media","Bassa","Risparmio","Dimensione parte (file diviso)","Ferma alla dimensione totale","No","fino a","MB/min","GB/h","Ora","parte","totale","Ogni parte resta nella memoria del telefono finché non viene salvata; parti più piccole usano meno RAM.","La qualità vale dalla prossima parte o registrazione.","Cerca per nome…","Fissa in alto","Sblocca"],"PL":["⁞⁞ PILOT","TŁO","DŹWIĘK","FOTO","NAGR.","BŁĄD","BRAK WIDEO","JAKOŚĆ","PIP","PEŁNY","KINO","SZUKAJ","LINK","Strona główna","Odśwież","Minimalizuj","Ukryj (H)","M=DŹWIĘK F=PEŁNY T=KINO H=UKRYJ","ULUB.","Nie znaleziono modelki","Ulubione","Dodaj bieżącą","Lista jest pusta.","Już dodano!","Notatka... (#tagi)","Ukrywanie","Kategorie","Moje reguły","Na razie brak reguł.","Wybierz element","Resetuj dla tej strony","Ukryj wszystko","Przytrzymaj pozycję, aby zobaczyć, co ukrywa","jeszcze nie znaleziono","Pokaż wszystko","Usuń wszystkie moje reguły","Dotknij elementu na stronie","Wyżej","Ukryj","Anuluj","dopas.","Minimum","Czysty odtwarzacz","Motyw ciemny / jasny","Data","Nazwa","Live","Widz.","Online","Pokaż tylko online","Priv","Wszystkie","Galeria","Lista","Brak modelek pasujących do filtra","Usuń","[Więcej]","[Mniej]","Wesprzyj autora","Postaw autorowi kawę","Dzięki, że korzystasz","Skopiowano adres","Nie udało się skopiować","Dotknij adresu, aby zobaczyć go w całości. Przycisk po prawej go kopiuje.","Prywatny","Grupowy","VIP","BRB","Bez grupy","+ Nowa grupa…","Wszystkie grupy","Ostatnio online","przed chwilą","{n} temu","min","godz.","d","MB","GB","Język","Automatycznie","Dotknięcie: nagrywaj · Przytrzymanie: ustawienia","Ustawienia nagrywania","Jakość (bitrate)","Wysoka","Średnia","Niska","Oszczędna","Rozmiar części (plik jest dzielony)","Zatrzymaj przy rozmiarze","Wył.","do","MB/min","GB/godz.","Teraz","część","razem","Każda część jest w pamięci telefonu do czasu zapisu; mniejsze części zużywają mniej RAM.","Jakość obowiązuje od następnej części lub nagrania.","Szukaj po nazwie…","Przypnij na górze","Odepnij"],"TR":["⁞⁞ KUMANDA","ARKA","SES","FOTO","KAYIT","HATA","VİDEO YOK","KALİTE","PIP","TAM","SİNEMA","ARA","LİNK","Ana sayfa","Yenile","Küçült","Gizle (H)","M=SES F=TAM T=SİNEMA H=GİZLE","FAVORİ","Model bulunamadı","Favoriler","Mevcut olanı ekle","Liste boş.","Zaten eklendi!","Not... (#etiket)","Gizleme","Kategoriler","Kurallarım","Henüz kural yok.","Öğe seç","Bu site için sıfırla","Hepsini gizle","Neyi gizlediğini görmek için bir öğeye basılı tutun","henüz bulunamadı","Hepsini göster","Tüm kurallarımı sil","Sayfadaki bir öğeye dokunun","Yukarı","Gizle","İptal","eşleşme","Minimum","Temiz oynatıcı","Koyu / açık tema","Tarih","Ad","Canlı","Son","Çevrimiçi","Yalnızca çevrimiçi olanları göster","Özel","Tümü","Galeri","Liste","Filtreye uyan model yok","Sil","[Daha fazla]","[Daha az]","Yazarı destekle","Yazara bir kahve ısmarla","Kullandığın için teşekkürler","Adres kopyalandı","Kopyalanamadı","Tamamını görmek için bir adrese dokun. Sağdaki düğme adresi kopyalar.","Özel","Grup","VIP","BRB","Grupsuz","+ Yeni grup…","Tüm gruplar","Son çevrimiçi","az önce","{n} önce","dk","sa","g","MB","GB","Dil","Otomatik","Dokun: kaydet · Basılı tut: ayarlar","Kayıt ayarları","Kalite (bit hızı)","Yüksek","Orta","Düşük","Tasarruf","Parça boyutu (dosya bölünür)","Toplam boyutta durdur","Kapalı","en fazla","MB/dk","GB/sa","Şimdi","parça","toplam","Her parça kaydedilene kadar telefonun belleğinde tutulur; küçük parçalar daha az RAM kullanır.","Kalite bir sonraki parça veya kayıttan itibaren geçerli olur.","İsme göre ara…","En üste sabitle","Sabitlemeyi kaldır"],"NL":["⁞⁞ BEDIENING","ACHT.","GELUID","FOTO","OPN.","FOUT","GEEN VIDEO","KWAL.","PIP","VOL","THEATER","ZOEK","LINK","Home","Herladen","Minimaliseren","Verbergen (H)","M=GELUID F=VOL T=THEATER H=VERBERG","FAVO'S","Model niet gevonden","Favorieten","Huidige toevoegen","De lijst is leeg.","Al toegevoegd!","Notitie... (#tags)","Verbergen","Categorieën","Mijn regels","Nog geen regels.","Element kiezen","Resetten voor deze site","Alles verbergen","Houd een item ingedrukt om te zien wat het verbergt","nog niet gevonden","Alles tonen","Al mijn regels wissen","Tik op een element op de pagina","Hoger","Verbergen","Annuleren","treffers","Minimaal","Schone speler","Donker / licht thema","Datum","Naam","Live","Gezien","Online","Alleen online tonen","Priv","Alle","Galerij","Lijst","Geen modellen voldoen aan het filter","Verwijderen","[Meer]","[Minder]","Steun de auteur","Trakteer de auteur op een koffie","Bedankt voor het gebruik","Adres gekopieerd","Kopiëren mislukt","Tik op een adres om het volledig te zien. De knop rechts kopieert het.","Privé","Groep","VIP","BRB","Geen groep","+ Nieuwe groep…","Alle groepen","Laatst online gezien","zojuist","{n} geleden","min","u","d","MB","GB","Taal","Auto","Tik: opnemen · Vasthouden: instellingen","Opname-instellingen","Kwaliteit (bitrate)","Hoog","Gemiddeld","Laag","Besparen","Deelgrootte (bestand wordt gesplitst)","Stop bij totale grootte","Uit","tot","MB/min","GB/u","Nu","deel","totaal","Een deel blijft in het telefoongeheugen tot het is opgeslagen, dus kleinere delen gebruiken minder RAM.","De kwaliteit geldt vanaf het volgende deel of de volgende opname.","Zoeken op naam…","Bovenaan vastzetten","Losmaken"],"CS":["⁞⁞ OVLÁDÁNÍ","POZADÍ","ZVUK","FOTO","NAHR.","CHYBA","BEZ VIDEA","KVAL.","PIP","CELÁ","KINO","HLEDAT","ODKAZ","Domů","Obnovit","Minimalizovat","Skrýt (H)","M=ZVUK F=CELÁ T=KINO H=SKRÝT","OBLÍBENÉ","Modelka nenalezena","Oblíbené","Přidat aktuální","Seznam je prázdný.","Už přidáno!","Poznámka... (#tagy)","Skrývání","Kategorie","Moje pravidla","Zatím žádná pravidla.","Vybrat prvek","Obnovit pro tento web","Skrýt vše","Podržte položku, abyste viděli, co skrývá","zatím nenalezeno","Zobrazit vše","Smazat všechna moje pravidla","Klepněte na prvek na stránce","Výše","Skrýt","Zrušit","shod","Minimum","Čistý přehrávač","Tmavý / světlý motiv","Datum","Jméno","Live","Viděna","Online","Zobrazit jen online","Priv","Vše","Galerie","Seznam","Filtru neodpovídá žádná modelka","Smazat","[Více]","[Méně]","Podpořit autora","Kupte autorovi kávu","Děkuji, že to používáte","Adresa zkopírována","Kopírování se nezdařilo","Klepnutím na adresu ji zobrazíte celou. Tlačítko vpravo ji zkopíruje.","Privát","Skupina","VIP","BRB","Bez skupiny","+ Nová skupina…","Všechny skupiny","Naposledy online","právě teď","před {n}","min","h","d","MB","GB","Jazyk","Auto","Klepnutí: nahrávat · Podržení: nastavení","Nastavení nahrávání","Kvalita (bitrate)","Vysoká","Střední","Nízká","Úsporná","Velikost části (soubor se dělí)","Zastavit při celkové velikosti","Vyp.","až","MB/min","GB/h","Nyní","část","celkem","Část zůstává v paměti telefonu, dokud se neuloží, takže menší části šetří RAM.","Kvalita platí od další části nebo nahrávky.","Hledat podle jména…","Připnout nahoru","Odepnout"],"RO":["⁞⁞ COMENZI","FUNDAL","SUNET","FOTO","ÎNREG.","EROARE","FĂRĂ VIDEO","CALIT.","PIP","ECRAN","TEATRU","CAUTĂ","LINK","Acasă","Reîncarcă","Minimizează","Ascunde (H)","M=SUNET F=ECRAN T=TEATRU H=ASCUNDE","FAVORITE","Modelul nu a fost găsit","Favorite","Adaugă curentul","Lista este goală.","Deja adăugat!","Notă... (#taguri)","Ascundere","Categorii","Regulile mele","Încă nu există reguli.","Alege element","Resetează pentru acest site","Ascunde tot","Ține apăsat un element ca să vezi ce ascunde","încă negăsit","Arată tot","Șterge toate regulile mele","Atinge un element de pe pagină","Mai sus","Ascunde","Anulează","potriviri","Minim","Player curat","Temă închisă / deschisă","Data","Nume","Live","Văzută","Online","Arată doar online","Priv","Toate","Galerie","Listă","Niciun model nu se potrivește filtrului","Șterge","[Mai mult]","[Mai puțin]","Susține autorul","Cumpără-i autorului o cafea","Mulțumesc că folosești scriptul","Adresă copiată","Copierea a eșuat","Atinge o adresă ca să o vezi completă. Butonul din dreapta o copiază.","Privat","Grup","VIP","BRB","Fără grup","+ Grup nou…","Toate grupurile","Ultima oară online","chiar acum","acum {n}","min","h","z","MB","GB","Limbă","Auto","Atingere: înregistrează · Apăsare lungă: setări","Setări de înregistrare","Calitate (bitrate)","Înaltă","Medie","Scăzută","Economie","Mărimea părții (fișierul se împarte)","Oprește la mărimea totală","Oprit","până la","MB/min","GB/h","Acum","parte","total","O parte rămâne în memoria telefonului până se salvează, deci părțile mici folosesc mai puțină RAM.","Calitatea se aplică de la următoarea parte sau înregistrare.","Caută după nume…","Fixează sus","Anulează fixarea"],"HU":["⁞⁞ VEZÉRLŐ","HÁTTÉR","HANG","FOTÓ","FELV.","HIBA","NINCS VIDEÓ","MIN.","PIP","TELJES","MOZI","KERESÉS","LINK","Kezdőlap","Újratöltés","Kicsinyítés","Elrejtés (H)","M=HANG F=TELJES T=MOZI H=ELREJT","KEDVENCEK","A modell nem található","Kedvencek","Jelenlegi hozzáadása","A lista üres.","Már hozzá van adva!","Jegyzet... (#címkék)","Elrejtés","Kategóriák","Saját szabályok","Még nincsenek szabályok.","Elem kiválasztása","Visszaállítás ezen az oldalon","Mindent elrejt","Tartsd nyomva a tételt, hogy lásd, mit rejt el","még nem található","Mindent mutat","Összes saját szabály törlése","Érints meg egy elemet az oldalon","Feljebb","Elrejtés","Mégse","találat","Minimum","Tiszta lejátszó","Sötét / világos téma","Dátum","Név","Live","Látva","Online","Csak az online-ok","Priv","Mind","Galéria","Lista","Nincs a szűrőnek megfelelő modell","Törlés","[Több]","[Kevesebb]","A szerző támogatása","Fizess egy kávét a szerzőnek","Köszönöm, hogy használod","Cím másolva","A másolás nem sikerült","Érintsd meg a címet a teljes megjelenítéshez. A jobb oldali gomb másolja.","Privát","Csoport","VIP","BRB","Nincs csoport","+ Új csoport…","Minden csoport","Utoljára online","épp most","{n} ezelőtt","perc","ó","n","MB","GB","Nyelv","Auto","Érintés: felvétel · Nyomva tartás: beállítások","Felvétel beállításai","Minőség (bitráta)","Magas","Közepes","Alacsony","Takarékos","Részméret (a fájl darabolódik)","Leállítás összméretnél","Ki","legfeljebb","MB/perc","GB/ó","Most","rész","összes","Egy rész a telefon memóriájában marad a mentésig, ezért a kisebb részek kevesebb RAM-ot használnak.","A minőség a következő résztől vagy felvételtől érvényes.","Keresés név szerint…","Kitűzés felülre","Kitűzés megszüntetése"],"BG":["⁞⁞ ПУЛТ","ФОН","ЗВУК","СНИМКА","ЗАПИС","ГРЕШКА","НЯМА ВИДЕО","КАЧ-ВО","PIP","ЕКРАН","ТЕАТЪР","ТЪРСЕНЕ","ЛИНК","Начало","Рестарт","Свиване","Скриване (H)","M=ЗВУК F=ЕКРАН T=ТЕАТЪР H=СКРИЙ","ЛЮБИМИ","Моделът не е намерен","Любими","Добави текущия","Списъкът е празен.","Вече е добавен!","Бележка... (#тагове)","Скриване","Категории","Моите правила","Все още няма правила.","Избор на елемент","Нулиране за този сайт","Скрий всичко","Задръжте елемент, за да видите какво скрива","още не е намерено","Покажи всичко","Изтрий всички мои правила","Докоснете елемент на страницата","По-горе","Скрий","Отказ","съвп.","Минимум","Чист плеър","Тъмна / светла тема","Дата","Име","Лайв","Видяна","Онлайн","Само онлайн","Прив","Всички","Галерия","Списък","Няма модели по филтъра","Изтрий","[Още]","[По-малко]","Подкрепи автора","Почерпи автора с кафе","Благодаря, че го ползвате","Адресът е копиран","Неуспешно копиране","Докоснете адреса, за да го видите изцяло. Бутонът вдясно го копира.","Приват","Група","VIP","BRB","Без група","+ Нова група…","Всички групи","Последно онлайн","току-що","преди {n}","мин","ч","д","МБ","ГБ","Език","Авто","Докосване: запис · Задържане: настройки","Настройки на записа","Качество (битрейт)","Високо","Средно","Ниско","Икономия","Размер на част (файлът се дели)","Спри при общ размер","Изкл.","до","МБ/мин","ГБ/ч","Сега","част","общо","Частта стои в паметта на телефона, докато не бъде запазена, затова по-малките части ползват по-малко RAM.","Качеството важи от следващата част или запис.","Търсене по име…","Закрепи най-горе","Откачи"],"EL":["⁞⁞ ΧΕΙΡΙΣΤΗΡΙΟ","ΦΟΝΤΟ","ΗΧΟΣ","ΦΩΤΟ","ΕΓΓΡ.","ΣΦΑΛΜΑ","ΧΩΡΙΣ ΒΙΝΤΕΟ","ΠΟΙΟΤ.","PIP","ΠΛΗΡΗΣ","ΘΕΑΤΡΟ","ΑΝΑΖΗΤ.","LINK","Αρχική","Επανάληψη","Ελαχιστοποίηση","Απόκρυψη (H)","M=ΗΧΟΣ F=ΠΛΗΡΗΣ T=ΘΕΑΤΡΟ H=ΚΡΥΨΕ","ΑΓΑΠΗΜΕΝΑ","Το μοντέλο δεν βρέθηκε","Αγαπημένα","Προσθήκη τρέχοντος","Η λίστα είναι άδεια.","Έχει ήδη προστεθεί!","Σημείωση... (#ετικέτες)","Απόκρυψη","Κατηγορίες","Οι κανόνες μου","Δεν υπάρχουν κανόνες ακόμα.","Επιλογή στοιχείου","Επαναφορά για αυτόν τον ιστότοπο","Απόκρυψη όλων","Κρατήστε πατημένο ένα στοιχείο για να δείτε τι κρύβει","δεν βρέθηκε ακόμα","Εμφάνιση όλων","Διαγραφή όλων των κανόνων μου","Αγγίξτε ένα στοιχείο της σελίδας","Πιο πάνω","Απόκρυψη","Ακύρωση","αντιστ.","Ελάχιστο","Καθαρός player","Σκούρο / ανοιχτό θέμα","Ημ/νία","Όνομα","Live","Τελευταία","Online","Μόνο online","Priv","Όλα","Γκαλερί","Λίστα","Κανένα μοντέλο δεν ταιριάζει με το φίλτρο","Διαγραφή","[Περισσότερα]","[Λιγότερα]","Υποστήριξη του δημιουργού","Κεράστε τον δημιουργό έναν καφέ","Ευχαριστώ που το χρησιμοποιείτε","Η διεύθυνση αντιγράφηκε","Η αντιγραφή απέτυχε","Αγγίξτε μια διεύθυνση για να τη δείτε ολόκληρη. Το κουμπί δεξιά την αντιγράφει.","Private","Group","VIP","BRB","Χωρίς ομάδα","+ Νέα ομάδα…","Όλες οι ομάδες","Τελευταία φορά online","μόλις τώρα","πριν {n}","λεπ.","ώ","μ","MB","GB","Γλώσσα","Αυτόματο","Πάτημα: εγγραφή · Παρατεταμένο: ρυθμίσεις","Ρυθμίσεις εγγραφής","Ποιότητα (bitrate)","Υψηλή","Μεσαία","Χαμηλή","Οικονομία","Μέγεθος τμήματος (το αρχείο σπάει)","Διακοπή στο συνολικό μέγεθος","Off","έως","MB/λεπτό","GB/ώρα","Τώρα","τμήμα","σύνολο","Ένα τμήμα μένει στη μνήμη του τηλεφώνου μέχρι να αποθηκευτεί, οπότε τα μικρότερα τμήματα χρησιμοποιούν λιγότερη RAM.","Η ποιότητα ισχύει από το επόμενο τμήμα ή την επόμενη εγγραφή.","Αναζήτηση με όνομα…","Καρφίτσωμα στην κορυφή","Ξεκαρφίτσωμα"],"JA":["⁞⁞ 操作パネル","背景","音声","撮影","録画","エラー","動画なし","画質","PIP","全画面","シアター","検索","リンク","ホーム","再読込","最小化","非表示 (H)","M=音声 F=全画面 T=シアター H=非表示","お気に入り","モデルが見つかりません","お気に入り","現在のものを追加","リストは空です。","追加済みです!","メモ... (#タグ)","非表示設定","カテゴリ","マイルール","ルールはまだありません。","要素を選択","このサイトをリセット","すべて非表示","項目を長押しすると、何を隠すか確認できます","まだ見つかりません","すべて表示","マイルールをすべて削除","ページ上の要素をタップしてください","上の階層","非表示","キャンセル","件一致","最小","クリーンプレーヤー","ダーク / ライトテーマ","日付","名前","ライブ","最終","オンライン","オンラインのみ表示","プライベート","すべて","ギャラリー","リスト","条件に合うモデルがいません","削除","[もっと見る]","[閉じる]","作者を支援","作者にコーヒーをおごる","ご利用ありがとうございます","アドレスをコピーしました","コピーに失敗しました","アドレスをタップすると全体が表示されます。右のボタンでコピーできます。","プライベート","グループ","VIP","BRB","グループなし","+ 新しいグループ…","すべてのグループ","最終オンライン","たった今","{n}前","分","時間","日","MB","GB","言語","自動","タップ: 録画 · 長押し: 設定","録画設定","画質(ビットレート)","高","中","低","節約","分割サイズ(ファイルを分割)","合計サイズで停止","オフ","最大","MB/分","GB/時","現在","パート","合計","各パートは保存されるまでスマホのメモリに残るため、小さいパートほどRAMを節約できます。","画質は次のパートまたは次の録画から適用されます。","名前で検索…","先頭に固定","固定を解除"],"ZH":["⁞⁞ 控制面板","后台","声音","截图","录制","错误","无视频","画质","PIP","全屏","影院","搜索","链接","主页","重新加载","最小化","隐藏 (H)","M=声音 F=全屏 T=影院 H=隐藏","收藏","未找到主播","收藏夹","添加当前","列表为空。","已添加!","备注... (#标签)","隐藏设置","分类","我的规则","暂无规则。","选择元素","重置此网站","全部隐藏","长按项目可查看其隐藏的内容","暂未找到","全部显示","删除我的所有规则","点击页面上的元素","上一级","隐藏","取消","个匹配","最简","纯净播放器","深色 / 浅色主题","日期","名称","直播","最近","在线","仅显示在线","私密","全部","图库","列表","没有符合筛选条件的主播","删除","[展开]","[收起]","支持作者","请作者喝杯咖啡","感谢使用","地址已复制","复制失败","点击地址可查看完整内容,右侧按钮可复制。","私密","群组","VIP","BRB","无分组","+ 新建分组…","所有分组","最后在线","刚刚","{n}前","分钟","小时","天","MB","GB","语言","自动","点按:录制 · 长按:设置","录制设置","画质(码率)","高","中","低","省空间","分段大小(文件会被拆分)","达到总大小时停止","关闭","最多","MB/分钟","GB/小时","当前","段","总计","每一段在保存前都会留在手机内存中,因此分段越小占用的 RAM 越少。","画质将从下一段或下一次录制开始生效。","按名称搜索…","置顶","取消置顶"],"KO":["⁞⁞ 컨트롤","배경","소리","캡처","녹화","오류","영상 없음","화질","PIP","전체","극장","검색","링크","홈","새로고침","최소화","숨기기 (H)","M=소리 F=전체 T=극장 H=숨김","즐겨찾기","모델을 찾을 수 없습니다","즐겨찾기","현재 추가","목록이 비어 있습니다.","이미 추가됨!","메모... (#태그)","숨기기","카테고리","내 규칙","아직 규칙이 없습니다.","요소 선택","이 사이트 초기화","모두 숨기기","항목을 길게 누르면 무엇을 숨기는지 볼 수 있습니다","아직 찾지 못함","모두 표시","내 규칙 모두 삭제","페이지의 요소를 탭하세요","상위","숨기기","취소","개 일치","최소","깔끔한 플레이어","다크 / 라이트 테마","날짜","이름","라이브","최근","온라인","온라인만 표시","프라이빗","전체","갤러리","목록","필터에 맞는 모델이 없습니다","삭제","[더보기]","[접기]","제작자 후원","제작자에게 커피 한 잔 사주기","이용해 주셔서 감사합니다","주소가 복사되었습니다","복사 실패","주소를 탭하면 전체가 표시되고, 오른쪽 버튼으로 복사할 수 있습니다.","프라이빗","그룹","VIP","BRB","그룹 없음","+ 새 그룹…","모든 그룹","마지막 온라인","방금","{n} 전","분","시간","일","MB","GB","언어","자동","탭: 녹화 · 길게 누르기: 설정","녹화 설정","화질(비트레이트)","높음","보통","낮음","절약","파트 크기(파일 분할)","총 크기에서 중지","끔","최대","MB/분","GB/시간","현재","파트","전체","각 파트는 저장되기 전까지 휴대폰 메모리에 남아 있으므로, 파트가 작을수록 RAM을 적게 사용합니다.","화질은 다음 파트 또는 다음 녹화부터 적용됩니다.","이름으로 검색…","맨 위에 고정","고정 해제"],"HE":["⁞⁞ בקרה","רקע","קול","צילום","הקלטה","שגיאה","אין וידאו","איכות","PIP","מסך מלא","תיאטרון","חיפוש","קישור","בית","רענון","מזעור","הסתר (H)","M=קול F=מסך מלא T=תיאטרון H=הסתר","מועדפים","המודל לא נמצא","מועדפים","הוסף את הנוכחי","הרשימה ריקה.","כבר נוסף!","הערה... (#תגיות)","הסתרה","קטגוריות","הכללים שלי","אין כללים עדיין.","בחר אלמנט","איפוס עבור אתר זה","הסתר הכול","החזק פריט לחוץ כדי לראות מה הוא מסתיר","עדיין לא נמצא","הצג הכול","מחק את כל הכללים שלי","הקש על אלמנט בדף","למעלה","הסתר","ביטול","התאמות","מינימלי","נגן נקי","ערכת נושא כהה / בהירה","תאריך","שם","לייב","נראה","אונליין","הצג רק אונליין","פרטי","הכול","גלריה","רשימה","אין מודלים התואמים לסינון","מחק","[עוד]","[פחות]","תמיכה ביוצר","קנה ליוצר כוס קפה","תודה על השימוש","הכתובת הועתקה","ההעתקה נכשלה","הקש על כתובת כדי לראות אותה במלואה. הכפתור שלצידה מעתיק אותה.","פרטי","קבוצה","VIP","BRB","ללא קבוצה","+ קבוצה חדשה…","כל הקבוצות","נראה אונליין לאחרונה","הרגע","לפני {n}","דק׳","שע׳","י׳","MB","GB","שפה","אוטומטי","הקשה: הקלטה · לחיצה ארוכה: הגדרות","הגדרות הקלטה","איכות (ביטרייט)","גבוהה","בינונית","נמוכה","חיסכון","גודל חלק (הקובץ מתפצל)","עצור בגודל כולל","כבוי","עד","MB/דקה","GB/שעה","עכשיו","חלק","סה״כ","כל חלק נשאר בזיכרון הטלפון עד שנשמר, ולכן חלקים קטנים יותר צורכים פחות RAM.","האיכות תחול מהחלק הבא או מההקלטה הבאה.","חיפוש לפי שם…","הצמד למעלה","בטל הצמדה"],"AR":["⁞⁞ التحكم","الخلفية","الصوت","لقطة","تسجيل","خطأ","لا فيديو","الجودة","PIP","ملء الشاشة","المسرح","بحث","رابط","الرئيسية","تحديث","تصغير","إخفاء (H)","M=الصوت F=ملء الشاشة T=المسرح H=إخفاء","المفضلة","لم يتم العثور على الموديل","المفضلة","إضافة الحالي","القائمة فارغة.","تمت الإضافة مسبقًا!","ملاحظة... (#وسوم)","الإخفاء","الفئات","قواعدي","لا توجد قواعد بعد.","اختيار عنصر","إعادة الضبط لهذا الموقع","إخفاء الكل","اضغط مطولًا على عنصر لترى ما الذي يخفيه","لم يُعثر عليه بعد","إظهار الكل","حذف جميع قواعدي","اضغط على عنصر في الصفحة","أعلى","إخفاء","إلغاء","تطابق","الحد الأدنى","مشغّل نظيف","مظهر داكن / فاتح","التاريخ","الاسم","مباشر","آخر ظهور","متصل","إظهار المتصلين فقط","خاص","الكل","معرض","قائمة","لا يوجد موديل يطابق الفلتر","حذف","[المزيد]","[أقل]","ادعم المطوّر","اشترِ للمطوّر فنجان قهوة","شكرًا لاستخدامك الأداة","تم نسخ العنوان","فشل النسخ","اضغط على العنوان لعرضه كاملًا. الزر المجاور له ينسخه.","خاص","مجموعة","VIP","BRB","بدون مجموعة","+ مجموعة جديدة…","كل المجموعات","آخر ظهور على الإنترنت","الآن","منذ {n}","د","س","ي","MB","GB","اللغة","تلقائي","نقرة: تسجيل · ضغط مطوّل: الإعدادات","إعدادات التسجيل","الجودة (معدل البت)","عالية","متوسطة","منخفضة","توفير","حجم الجزء (يُقسَّم الملف)","إيقاف عند الحجم الإجمالي","إيقاف","حتى","MB/د","GB/س","الآن","جزء","الإجمالي","يبقى كل جزء في ذاكرة الهاتف حتى يُحفظ، لذا تستهلك الأجزاء الأصغر ذاكرة RAM أقل.","تُطبَّق الجودة من الجزء التالي أو التسجيل التالي.","البحث بالاسم…","تثبيت في الأعلى","إلغاء التثبيت"],"HI":["⁞⁞ नियंत्रण","बैकग्राउंड","आवाज़","स्क्रीनशॉट","रिकॉर्ड","त्रुटि","वीडियो नहीं","क्वालिटी","PIP","फुलस्क्रीन","थिएटर","खोज","लिंक","होम","रीलोड","छोटा करें","छिपाएँ (H)","M=आवाज़ F=फुलस्क्रीन T=थिएटर H=छिपाएँ","पसंदीदा","मॉडल नहीं मिली","पसंदीदा","मौजूदा जोड़ें","सूची खाली है।","पहले ही जोड़ा जा चुका है!","नोट... (#टैग)","छिपाना","श्रेणियाँ","मेरे नियम","अभी कोई नियम नहीं।","एलिमेंट चुनें","इस साइट के लिए रीसेट करें","सब छिपाएँ","कोई आइटम दबाए रखें, देखने के लिए कि वह क्या छिपाता है","अभी नहीं मिला","सब दिखाएँ","मेरे सभी नियम हटाएँ","पेज पर किसी एलिमेंट को टैप करें","ऊपर","छिपाएँ","रद्द करें","मिलान","न्यूनतम","साफ़ प्लेयर","डार्क / लाइट थीम","तारीख़","नाम","लाइव","आख़िरी बार","ऑनलाइन","सिर्फ़ ऑनलाइन दिखाएँ","प्राइवेट","सभी","गैलरी","सूची","फ़िल्टर से कोई मॉडल मेल नहीं खाती","हटाएँ","[और]","[कम]","लेखक का समर्थन करें","लेखक को एक कॉफ़ी पिलाएँ","इस्तेमाल करने के लिए धन्यवाद","पता कॉपी हो गया","कॉपी नहीं हो सका","पूरा पता देखने के लिए उस पर टैप करें। दाईं ओर का बटन उसे कॉपी करता है।","प्राइवेट","ग्रुप","VIP","BRB","कोई ग्रुप नहीं","+ नया ग्रुप…","सभी ग्रुप","आख़िरी बार ऑनलाइन","अभी-अभी","{n} पहले","मिनट","घं","दिन","MB","GB","भाषा","ऑटो","टैप: रिकॉर्ड · दबाए रखें: सेटिंग्स","रिकॉर्डिंग सेटिंग्स","क्वालिटी (बिटरेट)","उच्च","मध्यम","कम","बचत","पार्ट का आकार (फ़ाइल बँटती है)","कुल आकार पर रोकें","बंद","तक","MB/मिनट","GB/घंटा","अभी","पार्ट","कुल","हर पार्ट सेव होने तक फ़ोन की मेमोरी में रहता है, इसलिए छोटे पार्ट कम RAM इस्तेमाल करते हैं।","क्वालिटी अगले पार्ट या अगली रिकॉर्डिंग से लागू होगी।","नाम से खोजें…","ऊपर पिन करें","पिन हटाएँ"],"ID":["⁞⁞ KONTROL","LATAR","SUARA","FOTO","REKAM","ERROR","TANPA VIDEO","KUALITAS","PIP","PENUH","TEATER","CARI","TAUTAN","Beranda","Muat ulang","Perkecil","Sembunyikan (H)","M=SUARA F=PENUH T=TEATER H=SEMBUNYI","FAVORIT","Model tidak ditemukan","Favorit","Tambah yang sekarang","Daftar kosong.","Sudah ditambahkan!","Catatan... (#tag)","Penyembunyian","Kategori","Aturan saya","Belum ada aturan.","Pilih elemen","Setel ulang untuk situs ini","Sembunyikan semua","Tahan item untuk melihat apa yang disembunyikannya","belum ditemukan","Tampilkan semua","Hapus semua aturan saya","Ketuk elemen di halaman","Naik","Sembunyikan","Batal","cocok","Minimal","Pemutar bersih","Tema gelap / terang","Tanggal","Nama","Live","Terlihat","Online","Tampilkan hanya online","Priv","Semua","Galeri","Daftar","Tidak ada model yang cocok dengan filter","Hapus","[Selengkapnya]","[Lebih sedikit]","Dukung pembuat","Traktir pembuat secangkir kopi","Terima kasih telah menggunakannya","Alamat disalin","Gagal menyalin","Ketuk alamat untuk melihatnya utuh. Tombol di kanan menyalinnya.","Privat","Grup","VIP","BRB","Tanpa grup","+ Grup baru…","Semua grup","Terakhir online","baru saja","{n} lalu","mnt","j","h","MB","GB","Bahasa","Otomatis","Ketuk: rekam · Tahan: pengaturan","Pengaturan rekaman","Kualitas (bitrate)","Tinggi","Sedang","Rendah","Hemat","Ukuran bagian (file dipecah)","Berhenti pada ukuran total","Mati","hingga","MB/mnt","GB/j","Sekarang","bagian","total","Satu bagian tetap di memori ponsel sampai disimpan, jadi bagian yang lebih kecil memakai lebih sedikit RAM.","Kualitas berlaku mulai bagian atau rekaman berikutnya.","Cari berdasarkan nama…","Sematkan di atas","Lepas sematan"],"VI":["⁞⁞ ĐIỀU KHIỂN","NỀN","ÂM","CHỤP","GHI","LỖI","KHÔNG VIDEO","CHẤT LƯỢNG","PIP","TOÀN MH","RẠP","TÌM","LIÊN KẾT","Trang chủ","Tải lại","Thu nhỏ","Ẩn (H)","M=ÂM F=TOÀN MH T=RẠP H=ẨN","YÊU THÍCH","Không tìm thấy model","Yêu thích","Thêm mục hiện tại","Danh sách trống.","Đã thêm rồi!","Ghi chú... (#thẻ)","Ẩn","Danh mục","Quy tắc của tôi","Chưa có quy tắc.","Chọn phần tử","Đặt lại cho trang này","Ẩn tất cả","Giữ một mục để xem nó ẩn gì","chưa tìm thấy","Hiện tất cả","Xóa tất cả quy tắc của tôi","Chạm vào một phần tử trên trang","Lên","Ẩn","Hủy","khớp","Tối thiểu","Trình phát sạch","Giao diện tối / sáng","Ngày","Tên","Live","Lần cuối","Online","Chỉ hiện online","Riêng","Tất cả","Thư viện","Danh sách","Không có model khớp bộ lọc","Xóa","[Thêm]","[Ít hơn]","Ủng hộ tác giả","Mời tác giả một ly cà phê","Cảm ơn bạn đã sử dụng","Đã sao chép địa chỉ","Sao chép thất bại","Chạm vào địa chỉ để xem đầy đủ. Nút bên phải sẽ sao chép nó.","Riêng tư","Nhóm","VIP","BRB","Không nhóm","+ Nhóm mới…","Tất cả nhóm","Online lần cuối","vừa xong","{n} trước","phút","giờ","ngày","MB","GB","Ngôn ngữ","Tự động","Chạm: ghi · Giữ: cài đặt","Cài đặt ghi hình","Chất lượng (bitrate)","Cao","Vừa","Thấp","Tiết kiệm","Kích thước phần (tệp được chia)","Dừng ở tổng dung lượng","Tắt","tối đa","MB/phút","GB/giờ","Hiện tại","phần","tổng","Mỗi phần nằm trong bộ nhớ điện thoại cho đến khi được lưu, nên phần nhỏ hơn dùng ít RAM hơn.","Chất lượng áp dụng từ phần hoặc bản ghi tiếp theo.","Tìm theo tên…","Ghim lên đầu","Bỏ ghim"],"TH":["⁞⁞ ควบคุม","พื้นหลัง","เสียง","แคป","อัด","ผิดพลาด","ไม่มีวิดีโอ","คุณภาพ","PIP","เต็มจอ","โรงหนัง","ค้นหา","ลิงก์","หน้าแรก","โหลดใหม่","ย่อ","ซ่อน (H)","M=เสียง F=เต็มจอ T=โรงหนัง H=ซ่อน","รายการโปรด","ไม่พบโมเดล","รายการโปรด","เพิ่มรายการปัจจุบัน","รายการว่างเปล่า","เพิ่มไว้แล้ว!","บันทึก... (#แท็ก)","การซ่อน","หมวดหมู่","กฎของฉัน","ยังไม่มีกฎ","เลือกองค์ประกอบ","รีเซ็ตสำหรับเว็บนี้","ซ่อนทั้งหมด","กดค้างที่รายการเพื่อดูว่าซ่อนอะไร","ยังไม่พบ","แสดงทั้งหมด","ลบกฎทั้งหมดของฉัน","แตะองค์ประกอบบนหน้าเว็บ","ขึ้น","ซ่อน","ยกเลิก","ตรงกัน","น้อยที่สุด","เครื่องเล่นสะอาด","ธีมมืด / สว่าง","วันที่","ชื่อ","ไลฟ์","ล่าสุด","ออนไลน์","แสดงเฉพาะออนไลน์","ส่วนตัว","ทั้งหมด","แกลเลอรี","รายการ","ไม่มีโมเดลที่ตรงกับตัวกรอง","ลบ","[เพิ่มเติม]","[น้อยลง]","สนับสนุนผู้สร้าง","เลี้ยงกาแฟผู้สร้างสักแก้ว","ขอบคุณที่ใช้งาน","คัดลอกที่อยู่แล้ว","คัดลอกไม่สำเร็จ","แตะที่อยู่เพื่อดูแบบเต็ม ปุ่มทางขวาใช้คัดลอก","ส่วนตัว","กลุ่ม","VIP","BRB","ไม่มีกลุ่ม","+ กลุ่มใหม่…","ทุกกลุ่ม","ออนไลน์ล่าสุด","เมื่อสักครู่","{n}ที่แล้ว","นาที","ชม.","วัน","MB","GB","ภาษา","อัตโนมัติ","แตะ: อัด · กดค้าง: ตั้งค่า","การตั้งค่าการอัด","คุณภาพ (บิตเรต)","สูง","กลาง","ต่ำ","ประหยัด","ขนาดแต่ละส่วน (ไฟล์จะถูกแบ่ง)","หยุดเมื่อถึงขนาดรวม","ปิด","สูงสุด","MB/นาที","GB/ชม.","ตอนนี้","ส่วน","รวม","แต่ละส่วนจะอยู่ในหน่วยความจำโทรศัพท์จนกว่าจะบันทึก ส่วนที่เล็กกว่าจึงใช้ RAM น้อยกว่า","คุณภาพจะมีผลตั้งแต่ส่วนถัดไปหรือการอัดครั้งถัดไป","ค้นหาตามชื่อ…","ปักหมุดไว้บนสุด","เลิกปักหมุด"]}},"HIDE":{"k":["hdr_m","hdr_d","seotitle","chat_tips","chat_topic","chat_boost","tabs","ftr","promo","lastview","joinpop","tips","modelchat","games","popups","actions","balance","chathdr","tipking","below","wm","age18"],"v":{"UK":[["Верх сайту","Логотип, меню, кнопки входу"],["Шапка сайту (ПК)","Верхня панель на комп’ютері"],["Заголовок-реклама","Рядок із SEO-заголовком над списком"],["Чайові в чаті","Повідомлення «подарував токени», меню чайових"],["Тема кімнати","Довгий опис «Тема: …» на початку чату"],["Бусти в чаті","Плашки «підняв модель на N секунд» (FAN BOOSTED)"],["Вкладки та сортування","Дівчата/пари, фільтри, значок сортування"],["Низ сторінки","Футер, соцмережі, SEO-текст"],["Реклама та банери","Банери, промо, плашка «Приєднуйся» на плеєрі, застосунок"],["Плашка «Продовжити перегляд»","Кнопка на превʼю останньої моделі"],["Вікно реєстрації","Спливаюча форма «Створіть акаунт»"],["Підказки в чаті","Спливаючі підказки та сповіщення"],["Спливаючий чат моделі","Рожеве віконце з автопривітанням моделі внизу"],["Кнопка «Ігри»","Іконка геймпада в червоній панелі"],["Спливаючі вікна","Вибір чату, група, підглядання"],["Кнопки «Приват / Чайові»","Сині (на ПК зелені) кнопки під червоною панеллю"],["Баланс і меню (ПК)","Баланс і навігація на комп’ютері"],["Плашка «Чат із …»","Рядок «Безкоштовний чат із …» і король чайових"],["Король чайових","Рядок короля та вікна чайових"],["Панель під плеєром (ПК)","Блок кнопок під відео"],["Водяний знак (ПК)","Логотип поверх відео"],["Вікно «Старше 18»","Заставка «Заходь, ТІЛЬКИ якщо тобі вже є 18»"]],"ES":[["Parte superior del sitio","Logo, menú, botones de acceso"],["Cabecera del sitio (PC)","Barra superior en ordenador"],["Título publicitario","Línea de título SEO sobre la lista"],["Propinas en el chat","Mensajes de «dio tokens» y menú de propinas"],["Tema de la sala","Bloque largo «Tema: …» al inicio del chat"],["Boosts en el chat","Tarjetas de «impulsó a la modelo N segundos»"],["Pestañas y orden","Chicas/parejas, filtros, icono de orden"],["Pie de página","Pie, redes sociales, texto SEO"],["Anuncios y banners","Banners, promos, franja «Únete» sobre el reproductor, app"],["Marca «Seguir viendo»","Botón sobre la miniatura de la última modelo"],["Ventana de registro","Formulario emergente «Crea una cuenta»"],["Consejos del chat","Avisos y notificaciones emergentes"],["Chat emergente de la modelo","Ventana rosa con el saludo automático abajo"],["Botón «Juegos»","Icono de mando en la barra roja"],["Ventanas emergentes","Elección de chat, grupo, voyeur"],["Botones «Privado / Propinas»","Botones azules (verdes en PC) bajo la barra roja"],["Saldo y menú (PC)","Saldo y navegación en ordenador"],["Barra «Chat con …»","Franja «Chat gratis con …» y rey de propinas"],["Rey de propinas","Franja del rey y ventanas de propinas"],["Barra bajo el reproductor (PC)","Fila de botones bajo el vídeo"],["Marca de agua (PC)","Logo sobre el vídeo"],["Aviso +18","Pantalla de entrada «Solo mayores de 18»"]],"DE":[["Seitenkopf","Logo, Menü, Anmelde-Buttons"],["Seitenkopf (PC)","Obere Leiste am Computer"],["Werbe-Überschrift","SEO-Überschrift über der Liste"],["Trinkgelder im Chat","Meldungen „hat Tokens gegeben“, Tip-Menü"],["Raumthema","Langer Block „Thema: …“ am Chatanfang"],["Boosts im Chat","Karten „hat das Model N Sekunden geboostet“"],["Tabs & Sortierung","Mädchen/Paare, Filter, Sortier-Symbol"],["Seitenende","Footer, Social Media, SEO-Text"],["Werbung & Banner","Banner, Promos, „Mach mit“-Leiste am Player, App"],["Hinweis „Weiter ansehen“","Button auf der Vorschau des zuletzt gesehenen Models"],["Anmelde-Fenster","Popup „Konto erstellen“"],["Chat-Hinweise","Tooltips und Benachrichtigungen"],["Pop-up-Chat des Models","Rosa Fenster mit der Auto-Begrüßung unten"],["Button „Spiele“","Gamepad-Symbol in der roten Leiste"],["Pop-up-Fenster","Chat-Auswahl, Gruppe, Voyeur"],["Buttons „Privat / Trinkgeld“","Blaue (am PC grüne) Buttons unter der roten Leiste"],["Guthaben & Menü (PC)","Guthaben und Navigation am Computer"],["Leiste „Chat mit …“","Streifen „Gratis-Chat mit …“ und Tip-König"],["Tip-König","Königs-Streifen und Trinkgeld-Fenster"],["Leiste unter dem Player (PC)","Button-Reihe unter dem Video"],["Wasserzeichen (PC)","Logo über dem Video"],["18+-Hinweis","Einstiegsbildschirm „Nur ab 18“"]],"FR":[["Haut du site","Logo, menu, boutons de connexion"],["En-tête du site (PC)","Barre supérieure sur ordinateur"],["Titre publicitaire","Ligne de titre SEO au-dessus de la liste"],["Pourboires dans le chat","Messages « a donné des tokens », menu de pourboires"],["Sujet du salon","Long bloc « Sujet : … » en haut du chat"],["Boosts dans le chat","Cartes « a boosté la modèle N secondes »"],["Onglets et tri","Filles/couples, filtres, icône de tri"],["Bas de page","Pied de page, réseaux sociaux, texte SEO"],["Pubs et bannières","Bannières, promos, bandeau « Rejoins-moi » sur le lecteur, appli"],["Pastille « Reprendre »","Bouton sur la miniature du dernier modèle vu"],["Fenêtre d’inscription","Formulaire « Créez un compte »"],["Astuces du chat","Infobulles et notifications"],["Chat flottant de la modèle","Fenêtre rose avec le message d’accueil automatique, en bas"],["Bouton « Jeux »","Icône de manette dans la barre rouge"],["Fenêtres surgissantes","Choix de chat, groupe, voyeur"],["Boutons « Privé / Pourboires »","Boutons bleus (verts sur PC) sous la barre rouge"],["Solde et menu (PC)","Solde et navigation sur ordinateur"],["Barre « Chat avec … »","Bandeau « Chat gratuit avec … » et roi des pourboires"],["Roi des pourboires","Bandeau du roi et fenêtres de pourboires"],["Barre sous le lecteur (PC)","Rangée de boutons sous la vidéo"],["Filigrane (PC)","Logo sur la vidéo"],["Avis 18+","Écran d’entrée « Réservé aux plus de 18 ans »"]],"PT":[["Topo do site","Logo, menu, botões de login"],["Cabeçalho do site (PC)","Barra superior no computador"],["Título publicitário","Linha de título SEO acima da lista"],["Gorjetas no chat","Mensagens de «deu tokens» e menu de gorjetas"],["Tema da sala","Bloco longo «Tema: …» no início do chat"],["Boosts no chat","Cartões de «impulsionou a modelo por N segundos»"],["Abas e ordenação","Garotas/casais, filtros, ícone de ordenação"],["Rodapé","Rodapé, redes sociais, texto SEO"],["Anúncios e banners","Banners, promoções, faixa «Junte-se» no player, app"],["Selo «Continuar assistindo»","Botão na miniatura da última modelo vista"],["Janela de cadastro","Pop-up «Crie uma conta»"],["Dicas do chat","Dicas e notificações pop-up"],["Chat pop-up da modelo","Janela rosa com a saudação automática, embaixo"],["Botão «Jogos»","Ícone de controle na barra vermelha"],["Janelas pop-up","Escolha de chat, grupo, voyeur"],["Botões «Privado / Gorjetas»","Botões azuis (verdes no PC) sob a barra vermelha"],["Saldo e menu (PC)","Saldo e navegação no computador"],["Barra «Chat com …»","Faixa «Chat grátis com …» e rei das gorjetas"],["Rei das gorjetas","Faixa do rei e janelas de gorjetas"],["Barra sob o player (PC)","Fileira de botões sob o vídeo"],["Marca d’água (PC)","Logo sobre o vídeo"],["Aviso 18+","Tela de entrada «Somente maiores de 18»"]],"IT":[["Parte alta del sito","Logo, menu, pulsanti di accesso"],["Intestazione del sito (PC)","Barra superiore su computer"],["Titolo pubblicitario","Riga di titolo SEO sopra l’elenco"],["Mance in chat","Messaggi «ha dato token» e menu mance"],["Argomento della stanza","Lungo blocco «Argomento: …» all’inizio della chat"],["Boost in chat","Schede «ha potenziato la modella per N secondi»"],["Schede e ordinamento","Ragazze/coppie, filtri, icona di ordinamento"],["Fondo pagina","Footer, social, testo SEO"],["Pubblicità e banner","Banner, promo, striscia «Unisciti» sul player, app"],["Badge «Continua a guardare»","Pulsante sull’anteprima dell’ultima modella vista"],["Finestra di registrazione","Popup «Crea un account»"],["Suggerimenti della chat","Tooltip e notifiche"],["Chat popup della modella","Finestra rosa con il saluto automatico, in basso"],["Pulsante «Giochi»","Icona del gamepad nella barra rossa"],["Finestre popup","Scelta chat, gruppo, voyeur"],["Pulsanti «Privato / Mance»","Pulsanti blu (verdi su PC) sotto la barra rossa"],["Saldo e menu (PC)","Saldo e navigazione su computer"],["Barra «Chat con …»","Striscia «Chat gratis con …» e re delle mance"],["Re delle mance","Striscia del re e finestre delle mance"],["Barra sotto il player (PC)","Fila di pulsanti sotto il video"],["Filigrana (PC)","Logo sopra il video"],["Avviso 18+","Schermata d’ingresso «Solo maggiorenni»"]],"PL":[["Góra strony","Logo, menu, przyciski logowania"],["Nagłówek strony (PC)","Górny pasek na komputerze"],["Reklamowy nagłówek","Linia z nagłówkiem SEO nad listą"],["Napiwki na czacie","Wiadomości „dał tokeny”, menu napiwków"],["Temat pokoju","Długi blok „Temat: …” na początku czatu"],["Boosty na czacie","Karty „podbił modelkę na N sekund”"],["Zakładki i sortowanie","Dziewczyny/pary, filtry, ikona sortowania"],["Dół strony","Stopka, social media, tekst SEO"],["Reklamy i banery","Banery, promocje, pasek „Dołącz” na odtwarzaczu, aplikacja"],["Plakietka „Kontynuuj oglądanie”","Przycisk na miniaturze ostatniej modelki"],["Okno rejestracji","Wyskakujący formularz „Załóż konto”"],["Podpowiedzi czatu","Dymki i powiadomienia"],["Wyskakujący czat modelki","Różowe okienko z automatycznym powitaniem na dole"],["Przycisk „Gry”","Ikona pada w czerwonym pasku"],["Okna wyskakujące","Wybór czatu, grupa, podglądanie"],["Przyciski „Prywatny / Napiwki”","Niebieskie (na PC zielone) przyciski pod czerwonym paskiem"],["Saldo i menu (PC)","Saldo i nawigacja na komputerze"],["Pasek „Czat z …”","Pasek „Darmowy czat z …” i król napiwków"],["Król napiwków","Pasek króla i okna napiwków"],["Pasek pod odtwarzaczem (PC)","Rząd przycisków pod wideo"],["Znak wodny (PC)","Logo na wideo"],["Ostrzeżenie 18+","Ekran wejściowy „Tylko dla pełnoletnich”"]],"TR":[["Sitenin üst kısmı","Logo, menü, giriş düğmeleri"],["Site başlığı (PC)","Bilgisayarda üst çubuk"],["Reklam başlığı","Listenin üstündeki SEO başlık satırı"],["Sohbette bahşişler","«Token verdi» mesajları ve bahşiş menüsü"],["Oda konusu","Sohbetin başındaki uzun «Konu: …» bloğu"],["Sohbette boostlar","«Modeli N saniye öne çıkardı» kartları"],["Sekmeler ve sıralama","Kızlar/çiftler, filtreler, sıralama simgesi"],["Sayfa altı","Alt bilgi, sosyal ağlar, SEO metni"],["Reklamlar ve bannerlar","Bannerlar, promolar, oynatıcıdaki «Katıl» şeridi, uygulama"],["«İzlemeye devam et» rozeti","Son bakılan modelin önizlemesindeki düğme"],["Kayıt penceresi","«Hesap oluştur» açılır formu"],["Sohbet ipuçları","Açılır ipuçları ve bildirimler"],["Modelin açılır sohbeti","Altta otomatik karşılama mesajlı pembe pencere"],["«Oyunlar» düğmesi","Kırmızı çubuktaki oyun kolu simgesi"],["Açılır pencereler","Sohbet seçimi, grup, gözetleme"],["«Özel / Bahşiş» düğmeleri","Kırmızı çubuğun altındaki mavi (PC’de yeşil) düğmeler"],["Bakiye ve menü (PC)","Bilgisayarda bakiye ve gezinme"],["«… ile sohbet» çubuğu","«… ile ücretsiz sohbet» şeridi ve bahşiş kralı"],["Bahşiş kralı","Kral şeridi ve bahşiş pencereleri"],["Oynatıcı altı çubuk (PC)","Videonun altındaki düğme sırası"],["Filigran (PC)","Videonun üzerindeki logo"],["18+ uyarısı","«Yalnızca 18 yaş üstü» giriş ekranı"]],"NL":[["Site-header","Logo, menu, inlogknoppen"],["Site-header (pc)","Bovenbalk op de computer"],["Reclamekop","SEO-kop boven de lijst"],["Tips in de chat","Meldingen „gaf tokens”, tipmenu"],["Kamerthema","Lang blok „Thema: …” aan het begin van de chat"],["Boosts in de chat","Kaarten „heeft het model N seconden geboost”"],["Tabs en sortering","Meisjes/koppels, filters, sorteericoon"],["Voettekst","Footer, socials, SEO-tekst"],["Reclame en banners","Banners, promo's, „Doe mee”-balk op de speler, app"],["Melding „Verder kijken”","Knop op de preview van het laatst bekeken model"],["Aanmeldvenster","Pop-up „Account maken”"],["Chat-hints","Tooltips en meldingen"],["Pop-upchat van model","Roze venster met automatische begroeting onderaan"],["Knop „Spelletjes”","Gamepad-icoon in de rode balk"],["Pop-upvensters","Chatkeuze, groep, voyeur"],["Knoppen „Privé / Tip”","Blauwe (op pc groene) knoppen onder de rode balk"],["Saldo en menu (pc)","Saldo en navigatie op de computer"],["Balk „Chat met …”","Strook „Gratis chat met …” en tipkoning"],["Tipkoning","Koningsstrook en tipvenster"],["Balk onder de speler (pc)","Knoppenrij onder de video"],["Watermerk (pc)","Logo over de video"],["18+-melding","Startscherm „Alleen 18+”"]],"CS":[["Hlavička webu","Logo, menu, tlačítka přihlášení"],["Hlavička webu (PC)","Horní panel na počítači"],["Reklamní nadpis","SEO nadpis nad seznamem"],["Tipy v chatu","Zprávy „dal tokeny“, menu tipů"],["Téma místnosti","Dlouhý blok „Téma: …“ na začátku chatu"],["Boosty v chatu","Karty „boostl modelku na N sekund“"],["Záložky a řazení","Dívky/páry, filtry, ikona řazení"],["Patička","Patička, sítě, SEO text"],["Reklama a bannery","Bannery, promo, lišta „Připoj se“ na přehrávači, aplikace"],["Lišta „Pokračovat ve sledování“","Tlačítko na náhledu poslední modelky"],["Registrační okno","Vyskakovací formulář „Vytvořit účet“"],["Nápovědy v chatu","Bubliny a oznámení"],["Vyskakovací chat modelky","Růžové okno s automatickým pozdravem dole"],["Tlačítko „Hry“","Ikona gamepadu v červené liště"],["Vyskakovací okna","Výběr chatu, skupina, voyeur"],["Tlačítka „Privát / Tip“","Modrá (na PC zelená) tlačítka pod červenou lištou"],["Zůstatek a menu (PC)","Zůstatek a navigace na počítači"],["Lišta „Chat s …“","Pruh „Zdarma chat s …“ a král tipů"],["Král tipů","Královský pruh a okno tipů"],["Lišta pod přehrávačem (PC)","Řada tlačítek pod videem"],["Vodoznak (PC)","Logo přes video"],["Upozornění 18+","Úvodní obrazovka „Pouze 18+“"]],"RO":[["Antetul site-ului","Logo, meniu, butoane de autentificare"],["Antetul site-ului (PC)","Bara de sus pe computer"],["Titlu publicitar","Titlu SEO deasupra listei"],["Tipuri în chat","Mesaje „a dat tokeni”, meniul de tip"],["Tema camerei","Bloc lung „Temă: …” la începutul chatului"],["Boost-uri în chat","Carduri „a boostat modelul N secunde”"],["Taburi și sortare","Fete/cupluri, filtre, pictogramă de sortare"],["Subsolul paginii","Footer, rețele sociale, text SEO"],["Reclame și bannere","Bannere, promo, bara „Alătură-te” de pe player, aplicația"],["Mesajul „Continuă vizionarea”","Buton pe previzualizarea ultimului model"],["Fereastra de înregistrare","Formularul pop-up „Creează cont”"],["Sfaturi în chat","Tooltip-uri și notificări"],["Chatul pop-up al modelului","Fereastră roz cu salut automat jos"],["Butonul „Jocuri”","Pictograma de gamepad din bara roșie"],["Ferestre pop-up","Alegerea chatului, grup, voyeur"],["Butoanele „Privat / Tip”","Butoane albastre (pe PC verzi) sub bara roșie"],["Sold și meniu (PC)","Soldul și navigarea pe computer"],["Bara „Chat cu …”","Banda „Chat gratuit cu …” și regele tipurilor"],["Regele tipurilor","Banda regelui și fereastra de tip"],["Bara de sub player (PC)","Rândul de butoane de sub video"],["Filigran (PC)","Logo peste video"],["Avertisment 18+","Ecranul de intrare „Doar 18+”"]],"HU":[["Oldal fejléce","Logó, menü, belépés gombok"],["Oldal fejléce (PC)","Felső sáv számítógépen"],["Reklámcím","SEO-cím a lista felett"],["Borravaló a csevegésben","„tokent adott” üzenetek, borravaló menü"],["Szoba témája","Hosszú „Téma: …” blokk a csevegés elején"],["Boostok a csevegésben","„N másodpercre boostolta a modellt” kártyák"],["Fülek és rendezés","Lányok/párok, szűrők, rendezés ikon"],["Oldal alja","Lábléc, közösségi média, SEO-szöveg"],["Reklámok és bannerek","Bannerek, promók, „Csatlakozz” sáv a lejátszón, app"],["„Folytatás” jelzés","Gomb az utoljára nézett modell előnézetén"],["Regisztrációs ablak","„Fiók létrehozása” felugró űrlap"],["Csevegési tippek","Buborékok és értesítések"],["A modell felugró csevegése","Rózsaszín ablak automatikus köszöntéssel alul"],["„Játékok” gomb","Gamepad ikon a piros sávban"],["Felugró ablakok","Csevegés választás, csoport, voyeur"],["„Privát / Borravaló” gombok","Kék (PC-n zöld) gombok a piros sáv alatt"],["Egyenleg és menü (PC)","Egyenleg és navigáció számítógépen"],["„Csevegés vele: …” sáv","„Ingyenes csevegés vele: …” csík és borravalókirály"],["Borravalókirály","Király csík és borravaló ablak"],["Sáv a lejátszó alatt (PC)","Gombsor a videó alatt"],["Vízjel (PC)","Logó a videó felett"],["18+ figyelmeztetés","„Csak 18+” belépő képernyő"]],"BG":[["Хедър на сайта","Лого, меню, бутони за вход"],["Хедър на сайта (PC)","Горна лента на компютър"],["Рекламно заглавие","SEO заглавие над списъка"],["Бакшиши в чата","Съобщения „даде токени“, меню за бакшиш"],["Тема на стаята","Дълъг блок „Тема: …“ в началото на чата"],["Бустове в чата","Карти „бустна модела за N секунди“"],["Раздели и сортиране","Момичета/двойки, филтри, иконка за сортиране"],["Долна част на страницата","Футър, социални мрежи, SEO текст"],["Реклами и банери","Банери, промо, лента „Присъедини се“ на плеъра, приложение"],["Плашка „Продължи гледането“","Бутон на превюто на последния модел"],["Прозорец за регистрация","Изскачаща форма „Създай акаунт“"],["Съвети в чата","Подсказки и известия"],["Изскачащ чат на модела","Розов прозорец с автоматичен поздрав долу"],["Бутон „Игри“","Иконка на геймпад в червената лента"],["Изскачащи прозорци","Избор на чат, група, воаяор"],["Бутони „Приват / Бакшиш“","Сини (на PC зелени) бутони под червената лента"],["Баланс и меню (PC)","Баланс и навигация на компютър"],["Лента „Чат с …“","Ивица „Безплатен чат с …“ и цар на бакшишите"],["Цар на бакшишите","Царска ивица и прозорец за бакшиш"],["Лента под плеъра (PC)","Ред с бутони под видеото"],["Воден знак (PC)","Лого върху видеото"],["Предупреждение 18+","Входен екран „Само 18+“"]],"EL":[["Κεφαλίδα ιστότοπου","Λογότυπο, μενού, κουμπιά σύνδεσης"],["Κεφαλίδα ιστότοπου (PC)","Επάνω γραμμή στον υπολογιστή"],["Διαφημιστικός τίτλος","Τίτλος SEO πάνω από τη λίστα"],["Φιλοδωρήματα στο chat","Μηνύματα «έδωσε tokens», μενού tip"],["Θέμα δωματίου","Μεγάλο μπλοκ «Θέμα: …» στην αρχή του chat"],["Boosts στο chat","Κάρτες «ενίσχυσε το μοντέλο για N δευτερόλεπτα»"],["Καρτέλες και ταξινόμηση","Κορίτσια/ζευγάρια, φίλτρα, εικονίδιο ταξινόμησης"],["Υποσέλιδο","Footer, social, κείμενο SEO"],["Διαφημίσεις και banners","Banners, promo, γραμμή «Γίνε μέλος» στον player, εφαρμογή"],["Ειδοποίηση «Συνέχεια παρακολούθησης»","Κουμπί στην προεπισκόπηση του τελευταίου μοντέλου"],["Παράθυρο εγγραφής","Αναδυόμενη φόρμα «Δημιουργία λογαριασμού»"],["Συμβουλές chat","Tooltips και ειδοποιήσεις"],["Αναδυόμενο chat μοντέλου","Ροζ παράθυρο με αυτόματο χαιρετισμό κάτω"],["Κουμπί «Παιχνίδια»","Εικονίδιο gamepad στην κόκκινη γραμμή"],["Αναδυόμενα παράθυρα","Επιλογή chat, ομάδα, voyeur"],["Κουμπιά «Ιδιωτικό / Φιλοδώρημα»","Μπλε (στον υπολογιστή πράσινα) κουμπιά κάτω από την κόκκινη γραμμή"],["Υπόλοιπο και μενού (PC)","Υπόλοιπο και πλοήγηση στον υπολογιστή"],["Γραμμή «Chat με …»","Λωρίδα «Δωρεάν chat με …» και βασιλιάς φιλοδωρημάτων"],["Βασιλιάς φιλοδωρημάτων","Λωρίδα βασιλιά και παράθυρο φιλοδωρημάτων"],["Γραμμή κάτω από τον player (PC)","Σειρά κουμπιών κάτω από το βίντεο"],["Υδατογράφημα (PC)","Λογότυπο πάνω από το βίντεο"],["Ειδοποίηση 18+","Αρχική οθόνη «Μόνο 18+»"]],"JA":[["サイトのヘッダー","ロゴ、メニュー、ログインボタン"],["サイトのヘッダー(PC)","パソコン表示の上部バー"],["宣伝見出し","リスト上のSEO見出し"],["チャットのチップ","「トークンを贈りました」メッセージ、チップメニュー"],["ルームのトピック","チャット冒頭の長い「トピック: …」ブロック"],["チャットのブースト","「モデルをN秒ブースト」カード"],["タブと並べ替え","女の子/カップル、フィルター、並べ替えアイコン"],["ページ下部","フッター、SNS、SEOテキスト"],["広告とバナー","バナー、プロモ、プレーヤー上の「参加」バー、アプリ"],["「続きを見る」表示","最後に見たモデルのプレビュー上のボタン"],["登録ウィンドウ","「アカウント作成」ポップアップ"],["チャットのヒント","ツールチップと通知"],["モデルのポップアップチャット","下部に自動あいさつが出るピンクのウィンドウ"],["「ゲーム」ボタン","赤いバーのゲームパッドアイコン"],["ポップアップウィンドウ","チャット選択、グループ、のぞき見"],["「プライベート / チップ」ボタン","赤いバー下の青い(PCでは緑の)ボタン"],["残高とメニュー(PC)","パソコン表示の残高とナビゲーション"],["「〜とチャット」バー","「〜と無料チャット」帯とチップキング"],["チップキング","キングの帯とチップウィンドウ"],["プレーヤー下のバー(PC)","動画下のボタン列"],["透かし(PC)","動画上のロゴ"],["18歳以上の確認","「18歳以上のみ」入口画面"]],"ZH":[["网站顶部","Logo、菜单、登录按钮"],["网站顶部(电脑)","电脑端的顶部栏"],["广告标题","列表上方的 SEO 标题"],["聊天打赏","“赠送了代币”消息、打赏菜单"],["房间话题","聊天开头很长的“话题:…”块"],["聊天加成","“为主播加成 N 秒”卡片"],["标签页和排序","女生/情侣、筛选、排序图标"],["页面底部","页脚、社交媒体、SEO 文字"],["广告和横幅","横幅、推广、播放器上的“加入”条、应用"],["“继续观看”提示","最近观看主播预览上的按钮"],["注册窗口","“创建账号”弹窗"],["聊天提示","提示气泡和通知"],["主播弹出聊天","底部带自动问候的粉色窗口"],["“游戏”按钮","红色栏中的手柄图标"],["弹出窗口","聊天选择、群组、偷看"],["“私密 / 打赏”按钮","红色栏下方的蓝色(电脑端为绿色)按钮"],["余额和菜单(电脑)","电脑端的余额和导航"],["“与…聊天”栏","“与…免费聊天”条和打赏之王"],["打赏之王","国王条和打赏窗口"],["播放器下方栏(电脑)","视频下方的按钮行"],["水印(电脑)","视频上的 Logo"],["18+ 提示","“仅限 18 岁以上”入口页"]],"KO":[["사이트 헤더","로고, 메뉴, 로그인 버튼"],["사이트 헤더(PC)","컴퓨터 화면의 상단 바"],["광고성 제목","목록 위의 SEO 제목"],["채팅 팁","“토큰을 선물했습니다” 메시지, 팁 메뉴"],["방 주제","채팅 시작 부분의 긴 “주제: …” 블록"],["채팅 부스트","“모델을 N초 부스트했습니다” 카드"],["탭과 정렬","여성/커플, 필터, 정렬 아이콘"],["페이지 하단","푸터, SNS, SEO 텍스트"],["광고 및 배너","배너, 프로모션, 플레이어의 “가입” 바, 앱"],["“이어보기” 표시","마지막으로 본 모델 미리보기의 버튼"],["가입 창","“계정 만들기” 팝업"],["채팅 안내","툴팁과 알림"],["모델 팝업 채팅","하단에 자동 인사가 있는 분홍색 창"],["“게임” 버튼","빨간 바의 게임패드 아이콘"],["팝업 창","채팅 선택, 그룹, 훔쳐보기"],["“프라이빗 / 팁” 버튼","빨간 바 아래의 파란색(PC에서는 녹색) 버튼"],["잔액 및 메뉴(PC)","컴퓨터 화면의 잔액과 내비게이션"],["“…와 채팅” 바","“…와 무료 채팅” 띠와 팁 킹"],["팁 킹","킹 띠와 팁 창"],["플레이어 아래 바(PC)","영상 아래의 버튼 줄"],["워터마크(PC)","영상 위의 로고"],["18+ 안내","“18세 이상만” 입장 화면"]],"HE":[["כותרת האתר","לוגו, תפריט, כפתורי כניסה"],["כותרת האתר (מחשב)","הסרגל העליון במחשב"],["כותרת פרסומית","כותרת SEO מעל הרשימה"],["טיפים בצ׳אט","הודעות “שלח טוקנים”, תפריט טיפים"],["נושא החדר","בלוק ארוך “נושא: …” בתחילת הצ׳אט"],["בוסטים בצ׳אט","כרטיסים “הגביר את המודל ל-N שניות”"],["לשוניות ומיון","בנות/זוגות, סינון, סמל מיון"],["תחתית העמוד","פוטר, רשתות חברתיות, טקסט SEO"],["פרסומות ובאנרים","באנרים, מבצעים, פס “הצטרף” בנגן, אפליקציה"],["הודעת “המשך צפייה”","כפתור על התצוגה המקדימה של המודל האחרון"],["חלון הרשמה","חלון קופץ “יצירת חשבון”"],["רמזים בצ׳אט","טיפים צפים והתראות"],["צ׳אט קופץ של המודל","חלון ורוד עם ברכה אוטומטית בתחתית"],["כפתור “משחקים”","סמל שלט משחק בפס האדום"],["חלונות קופצים","בחירת צ׳אט, קבוצה, מציצן"],["כפתורי “פרטי / טיפ”","כפתורים כחולים (במחשב ירוקים) מתחת לפס האדום"],["יתרה ותפריט (מחשב)","יתרה וניווט במחשב"],["פס “צ׳אט עם …”","רצועת “צ׳אט חינם עם …” ומלך הטיפים"],["מלך הטיפים","רצועת המלך וחלון הטיפים"],["פס מתחת לנגן (מחשב)","שורת כפתורים מתחת לוידאו"],["סימן מים (מחשב)","לוגו מעל הוידאו"],["הודעת 18+","מסך כניסה “18 ומעלה בלבד”"]],"AR":[["رأس الموقع","الشعار والقائمة وأزرار الدخول"],["رأس الموقع (الكمبيوتر)","الشريط العلوي على الكمبيوتر"],["عنوان إعلاني","عنوان SEO فوق القائمة"],["البقشيش في الدردشة","رسائل «أهدى رموزًا»، قائمة البقشيش"],["موضوع الغرفة","كتلة طويلة «الموضوع: …» في بداية الدردشة"],["التعزيزات في الدردشة","بطاقات «عزّز الموديل لمدة N ثانية»"],["التبويبات والفرز","فتيات/أزواج، فلاتر، أيقونة الفرز"],["أسفل الصفحة","التذييل، الشبكات الاجتماعية، نص SEO"],["الإعلانات واللافتات","لافتات، عروض، شريط «انضم» على المشغّل، التطبيق"],["تنبيه «متابعة المشاهدة»","زر على معاينة آخر موديل"],["نافذة التسجيل","نافذة منبثقة «إنشاء حساب»"],["تلميحات الدردشة","تلميحات وإشعارات"],["دردشة الموديل المنبثقة","نافذة وردية بتحية تلقائية في الأسفل"],["زر «الألعاب»","أيقونة يد التحكم في الشريط الأحمر"],["النوافذ المنبثقة","اختيار الدردشة، المجموعة، المتلصص"],["أزرار «خاص / بقشيش»","أزرار زرقاء (خضراء على الكمبيوتر) تحت الشريط الأحمر"],["الرصيد والقائمة (الكمبيوتر)","الرصيد والتنقل على الكمبيوتر"],["شريط «دردشة مع …»","شريط «دردشة مجانية مع …» وملك البقشيش"],["ملك البقشيش","شريط الملك ونافذة البقشيش"],["الشريط تحت المشغّل (الكمبيوتر)","صف الأزرار تحت الفيديو"],["العلامة المائية (الكمبيوتر)","الشعار فوق الفيديو"],["تنبيه +18","شاشة الدخول «للكبار فقط 18+»"]],"HI":[["साइट का हेडर","लोगो, मेन्यू, लॉगिन बटन"],["साइट का हेडर (PC)","कंप्यूटर पर ऊपर की पट्टी"],["विज्ञापन शीर्षक","सूची के ऊपर SEO शीर्षक"],["चैट में टिप","“टोकन दिए” संदेश, टिप मेन्यू"],["रूम का विषय","चैट की शुरुआत में लंबा “विषय: …” ब्लॉक"],["चैट में बूस्ट","“मॉडल को N सेकंड बूस्ट किया” कार्ड"],["टैब और सॉर्टिंग","लड़कियाँ/जोड़े, फ़िल्टर, सॉर्ट आइकन"],["पेज का निचला हिस्सा","फ़ुटर, सोशल मीडिया, SEO टेक्स्ट"],["विज्ञापन और बैनर","बैनर, प्रोमो, प्लेयर पर “जुड़ें” पट्टी, ऐप"],["“देखना जारी रखें” सूचना","आख़िरी देखी मॉडल के प्रीव्यू पर बटन"],["रजिस्ट्रेशन विंडो","“खाता बनाएँ” पॉप-अप"],["चैट सुझाव","टूलटिप और सूचनाएँ"],["मॉडल का पॉप-अप चैट","नीचे ऑटो-अभिवादन वाली गुलाबी विंडो"],["“गेम” बटन","लाल पट्टी में गेमपैड आइकन"],["पॉप-अप विंडो","चैट चयन, ग्रुप, वॉयर"],["“प्राइवेट / टिप” बटन","लाल पट्टी के नीचे नीले (PC पर हरे) बटन"],["बैलेंस और मेन्यू (PC)","कंप्यूटर पर बैलेंस और नेविगेशन"],["“… के साथ चैट” पट्टी","“… के साथ मुफ़्त चैट” पट्टी और टिप किंग"],["टिप किंग","किंग पट्टी और टिप विंडो"],["प्लेयर के नीचे की पट्टी (PC)","वीडियो के नीचे बटनों की पंक्ति"],["वॉटरमार्क (PC)","वीडियो के ऊपर लोगो"],["18+ चेतावनी","“केवल 18+” एंट्री स्क्रीन"]],"ID":[["Header situs","Logo, menu, tombol masuk"],["Header situs (PC)","Bilah atas di komputer"],["Judul iklan","Judul SEO di atas daftar"],["Tip di chat","Pesan “memberi token”, menu tip"],["Topik ruangan","Blok panjang “Topik: …” di awal chat"],["Boost di chat","Kartu “mem-boost model selama N detik”"],["Tab dan pengurutan","Cewek/pasangan, filter, ikon urutkan"],["Bagian bawah halaman","Footer, media sosial, teks SEO"],["Iklan dan banner","Banner, promo, bilah “Bergabung” di pemutar, aplikasi"],["Pemberitahuan “Lanjutkan menonton”","Tombol di pratinjau model terakhir"],["Jendela pendaftaran","Pop-up “Buat akun”"],["Petunjuk chat","Tooltip dan notifikasi"],["Chat pop-up model","Jendela pink dengan sapaan otomatis di bawah"],["Tombol “Game”","Ikon gamepad di bilah merah"],["Jendela pop-up","Pilihan chat, grup, voyeur"],["Tombol “Privat / Tip”","Tombol biru (hijau di PC) di bawah bilah merah"],["Saldo dan menu (PC)","Saldo dan navigasi di komputer"],["Bilah “Chat dengan …”","Pita “Chat gratis dengan …” dan raja tip"],["Raja tip","Pita raja dan jendela tip"],["Bilah di bawah pemutar (PC)","Baris tombol di bawah video"],["Watermark (PC)","Logo di atas video"],["Peringatan 18+","Layar masuk “Khusus 18+”"]],"VI":[["Phần đầu trang","Logo, menu, nút đăng nhập"],["Phần đầu trang (PC)","Thanh trên cùng trên máy tính"],["Tiêu đề quảng cáo","Tiêu đề SEO phía trên danh sách"],["Tip trong chat","Tin nhắn “đã tặng token”, menu tip"],["Chủ đề phòng","Khối dài “Chủ đề: …” ở đầu chat"],["Boost trong chat","Thẻ “đã boost model N giây”"],["Tab và sắp xếp","Các cô gái/cặp đôi, bộ lọc, biểu tượng sắp xếp"],["Chân trang","Footer, mạng xã hội, văn bản SEO"],["Quảng cáo và banner","Banner, khuyến mãi, thanh “Tham gia” trên trình phát, ứng dụng"],["Thông báo “Xem tiếp”","Nút trên bản xem trước của model xem gần nhất"],["Cửa sổ đăng ký","Cửa sổ bật lên “Tạo tài khoản”"],["Gợi ý trong chat","Tooltip và thông báo"],["Chat bật lên của model","Cửa sổ màu hồng có lời chào tự động ở dưới"],["Nút “Trò chơi”","Biểu tượng tay cầm ở thanh đỏ"],["Cửa sổ bật lên","Chọn chat, nhóm, voyeur"],["Nút “Riêng tư / Tip”","Nút xanh dương (xanh lá trên PC) dưới thanh đỏ"],["Số dư và menu (PC)","Số dư và điều hướng trên máy tính"],["Thanh “Chat với …”","Dải “Chat miễn phí với …” và vua tip"],["Vua tip","Dải vua và cửa sổ tip"],["Thanh dưới trình phát (PC)","Hàng nút bên dưới video"],["Hình mờ (PC)","Logo phía trên video"],["Cảnh báo 18+","Màn hình vào “Chỉ dành cho 18+”"]],"TH":[["ส่วนหัวเว็บไซต์","โลโก้ เมนู ปุ่มเข้าสู่ระบบ"],["ส่วนหัวเว็บไซต์ (PC)","แถบด้านบนบนคอมพิวเตอร์"],["หัวข้อโฆษณา","หัวข้อ SEO เหนือรายการ"],["ทิปในแชต","ข้อความ “ให้โทเคน” เมนูทิป"],["หัวข้อห้อง","บล็อกยาว “หัวข้อ: …” ตอนต้นแชต"],["บูสต์ในแชต","การ์ด “บูสต์โมเดล N วินาที”"],["แท็บและการเรียง","สาวๆ/คู่รัก ตัวกรอง ไอคอนเรียง"],["ท้ายหน้า","ส่วนท้าย โซเชียล ข้อความ SEO"],["โฆษณาและแบนเนอร์","แบนเนอร์ โปรโมชัน แถบ “เข้าร่วม” บนเครื่องเล่น แอป"],["แจ้งเตือน “ดูต่อ”","ปุ่มบนตัวอย่างของโมเดลที่ดูล่าสุด"],["หน้าต่างสมัครสมาชิก","ป๊อปอัป “สร้างบัญชี”"],["คำแนะนำในแชต","ทูลทิปและการแจ้งเตือน"],["แชตป๊อปอัปของโมเดล","หน้าต่างสีชมพูพร้อมคำทักทายอัตโนมัติด้านล่าง"],["ปุ่ม “เกม”","ไอคอนจอยเกมในแถบสีแดง"],["หน้าต่างป๊อปอัป","เลือกแชต กลุ่ม คนแอบดู"],["ปุ่ม “ส่วนตัว / ทิป”","ปุ่มสีน้ำเงิน (บน PC เป็นสีเขียว) ใต้แถบสีแดง"],["ยอดเงินและเมนู (PC)","ยอดเงินและการนำทางบนคอมพิวเตอร์"],["แถบ “แชตกับ …”","แถบ “แชตฟรีกับ …” และราชาทิป"],["ราชาทิป","แถบราชาและหน้าต่างทิป"],["แถบใต้เครื่องเล่น (PC)","แถวปุ่มใต้วิดีโอ"],["ลายน้ำ (PC)","โลโก้เหนือวิดีโอ"],["คำเตือน 18+","หน้าจอเข้าสู่ระบบ “เฉพาะ 18+”"]]}}}).map(([n, p]) => [n, unpackI18N(p)]));
Object.assign(i18n.RU, I18N_X.NEW.RU);
Object.assign(i18n.EN, I18N_X.NEW.EN);
Object.assign(i18n, I18N_X.LANG);
const I18N_Y = unpackI18N({"k":["st_favnav","st_prev","st_hide_topic","st_hide_grp","st_prev_spd","rs_cur","rs_resnote","st_av_refresh","fr_none","fr_hold","st_prev_off"],"v":{"RU":["Кнопки «назад / вперёд» на пульте","Превью моделей","Скрыть описание модели","Скрыть выбор группы","Обновление превью","Сейчас поток:","Разрешение записи равно разрешению потока и не меняется. Пункты выше ограничивают только битрейт (сжатие); 1080p/720p/480p/360p — примерное качество картинки при таком битрейте.","Обновить аватарки","Кадр недоступен: модель не в эфире","Удерживайте — показать текущий кадр","Не обновлять"],"EN":["Prev / next buttons on remote","Model previews","Hide model description","Hide group selector","Preview refresh","Current stream:","Recording resolution equals the stream resolution and is not changed. The options above only limit the bitrate (compression); 1080p/720p/480p/360p show the approximate picture quality at that bitrate.","Refresh avatars","No frame: model is offline","Hold to show the current frame","No refresh"],"UK":["Кнопки «назад / вперед» на пульті","Превʼю моделей","Сховати опис моделі","Сховати вибір групи","Оновлення превʼю","Зараз потік:","Роздільна здатність запису дорівнює роздільній здатності потоку і не змінюється. Пункти вище обмежують лише бітрейт (стиснення); 1080p/720p/480p/360p — приблизна якість картинки за такого бітрейту.","Оновити аватарки","Кадр недоступний: модель не в ефірі","Утримуйте — показати поточний кадр","Не оновлювати"],"ES":["Botones «anterior / siguiente» en el mando","Vistas previas de modelos","Ocultar la descripción de la modelo","Ocultar el selector de grupo","Actualización de la vista previa","Transmisión actual:","La resolución de la grabación es la de la transmisión y no cambia. Las opciones de arriba solo limitan el bitrate (compresión); 1080p/720p/480p/360p indican la calidad aproximada de la imagen con ese bitrate.","Actualizar avatares","Sin imagen: la modelo no está en directo","Mantén pulsado para ver el fotograma actual","Sin actualizar"],"DE":["Zurück-/Weiter-Tasten an der Steuerung","Modell-Vorschauen","Modellbeschreibung ausblenden","Gruppenauswahl ausblenden","Vorschau-Aktualisierung","Aktueller Stream:","Die Aufnahmeauflösung entspricht der Stream-Auflösung und wird nicht verändert. Die Optionen oben begrenzen nur die Bitrate (Kompression); 1080p/720p/480p/360p zeigen die ungefähre Bildqualität bei dieser Bitrate.","Avatare aktualisieren","Kein Bild: Model ist offline","Gedrückt halten für das aktuelle Bild","Keine Aktualisierung"],"FR":["Boutons « précédent / suivant » sur la télécommande","Aperçus des modèles","Masquer la description du modèle","Masquer le sélecteur de groupe","Actualisation des aperçus","Flux actuel :","La résolution de l’enregistrement est celle du flux et ne change pas. Les options ci-dessus limitent uniquement le débit (compression) ; 1080p/720p/480p/360p indiquent la qualité d’image approximative à ce débit.","Actualiser les avatars","Image indisponible : le modèle est hors ligne","Maintenez pour voir l’image actuelle","Pas d’actualisation"],"PT":["Botões «anterior / próximo» no controle","Pré-visualizações das modelos","Ocultar a descrição da modelo","Ocultar o seletor de grupo","Atualização da pré-visualização","Transmissão atual:","A resolução da gravação é a mesma da transmissão e não muda. As opções acima limitam apenas o bitrate (compressão); 1080p/720p/480p/360p indicam a qualidade aproximada da imagem nesse bitrate.","Atualizar avatares","Sem imagem: a modelo está offline","Segure para ver o quadro atual","Sem atualização"],"IT":["Pulsanti «precedente / successivo» sul telecomando","Anteprime delle modelle","Nascondi la descrizione della modella","Nascondi il selettore di gruppo","Aggiornamento anteprime","Stream attuale:","La risoluzione della registrazione è quella dello stream e non cambia. Le opzioni sopra limitano solo il bitrate (compressione); 1080p/720p/480p/360p indicano la qualità approssimativa dell’immagine con quel bitrate.","Aggiorna avatar","Nessun fotogramma: la modella è offline","Tieni premuto per vedere il fotogramma attuale","Nessun aggiornamento"],"PL":["Przyciski „wstecz / dalej” na pilocie","Podglądy modelek","Ukryj opis modelki","Ukryj wybór grupy","Odświeżanie podglądu","Obecny strumień:","Rozdzielczość nagrania jest taka jak rozdzielczość strumienia i się nie zmienia. Powyższe opcje ograniczają tylko bitrate (kompresję); 1080p/720p/480p/360p to przybliżona jakość obrazu przy takim bitrate.","Odśwież awatary","Brak klatki: modelka jest offline","Przytrzymaj, aby zobaczyć bieżącą klatkę","Bez odświeżania"],"TR":["Kumandada «geri / ileri» düğmeleri","Model önizlemeleri","Model açıklamasını gizle","Grup seçicisini gizle","Önizleme yenileme","Mevcut yayın:","Kayıt çözünürlüğü yayın çözünürlüğüne eşittir ve değişmez. Yukarıdaki seçenekler yalnızca bit hızını (sıkıştırmayı) sınırlar; 1080p/720p/480p/360p bu bit hızındaki yaklaşık görüntü kalitesini gösterir.","Avatarları yenile","Kare yok: model çevrimdışı","Geçerli kareyi görmek için basılı tut","Yenileme yok"],"NL":["«Vorige / volgende»-knoppen op de bediening","Voorbeelden van modellen","Modelbeschrijving verbergen","Groepskeuze verbergen","Voorbeelden vernieuwen","Huidige stream:","De opnameresolutie is gelijk aan de streamresolutie en verandert niet. De opties hierboven beperken alleen de bitrate (compressie); 1080p/720p/480p/360p geven de ongeveer verwachte beeldkwaliteit bij die bitrate.","Avatars vernieuwen","Geen beeld: model is offline","Houd ingedrukt voor het huidige beeld","Niet vernieuwen"],"CS":["Tlačítka „zpět / vpřed“ na ovladači","Náhledy modelek","Skrýt popis modelky","Skrýt výběr skupiny","Obnovování náhledů","Aktuální stream:","Rozlišení nahrávky se rovná rozlišení streamu a nemění se. Možnosti výše omezují jen bitrate (kompresi); 1080p/720p/480p/360p udávají přibližnou kvalitu obrazu při daném bitrate.","Obnovit avatary","Snímek není k dispozici: modelka je offline","Podržením zobrazíte aktuální snímek","Neobnovovat"],"RO":["Butoane „înapoi / înainte” pe telecomandă","Previzualizări ale modelelor","Ascunde descrierea modelului","Ascunde selectorul de grup","Reîmprospătarea previzualizărilor","Fluxul curent:","Rezoluția înregistrării este aceeași cu a fluxului și nu se schimbă. Opțiunile de mai sus limitează doar bitrate-ul (compresia); 1080p/720p/480p/360p arată calitatea aproximativă a imaginii la acel bitrate.","Actualizează avatarele","Fără cadru: modelul este offline","Ține apăsat pentru cadrul curent","Fără actualizare"],"HU":["„Vissza / előre” gombok a vezérlőn","Modellek előnézete","Modell leírásának elrejtése","Csoportválasztó elrejtése","Előnézet frissítése","Jelenlegi adás:","A felvétel felbontása megegyezik az adás felbontásával, és nem változik. A fenti opciók csak a bitrátát (tömörítést) korlátozzák; az 1080p/720p/480p/360p az adott bitrátánál kapott körülbelüli képminőséget jelzi.","Avatarok frissítése","Nincs kép: a modell offline","Tartsd nyomva az aktuális képkockához","Nincs frissítés"],"BG":["Бутони «назад / напред» на пулта","Превюта на моделите","Скриване на описанието на модела","Скриване на избора на група","Обновяване на превютата","Текущ поток:","Резолюцията на записа е равна на тази на потока и не се променя. Опциите по-горе ограничават само битрейта (компресията); 1080p/720p/480p/360p показват приблизителното качество на картината при този битрейт.","Обнови аватарите","Няма кадър: моделът е офлайн","Задръжте, за да видите текущия кадър","Без обновяване"],"EL":["Κουμπιά «προηγούμενο / επόμενο» στο χειριστήριο","Προεπισκοπήσεις μοντέλων","Απόκρυψη περιγραφής μοντέλου","Απόκρυψη επιλογής ομάδας","Ανανέωση προεπισκόπησης","Τρέχον stream:","Η ανάλυση της εγγραφής είναι ίδια με του stream και δεν αλλάζει. Οι επιλογές παραπάνω περιορίζουν μόνο το bitrate (συμπίεση)· τα 1080p/720p/480p/360p δείχνουν την προσεγγιστική ποιότητα εικόνας σε αυτό το bitrate.","Ανανέωση avatar","Δεν υπάρχει καρέ: το μοντέλο είναι εκτός σύνδεσης","Πατήστε παρατεταμένα για το τρέχον καρέ","Χωρίς ανανέωση"],"JA":["操作パネルの「前へ/次へ」ボタン","モデルのプレビュー","モデルの説明を非表示","グループ選択を非表示","プレビューの更新","現在のストリーム:","録画の解像度はストリームの解像度と同じで、変更されません。上の項目はビットレート(圧縮)のみを制限します。1080p/720p/480p/360pは、そのビットレートでのおおよその画質の目安です。","アバターを更新","フレームなし:モデルはオフラインです","長押しで現在のフレームを表示","更新しない"],"ZH":["控制面板上的“上一个 / 下一个”按钮","主播预览","隐藏主播简介","隐藏分组选择","预览刷新","当前直播流:","录制分辨率与直播流分辨率相同,不会改变。上面的选项只限制码率(压缩);1080p/720p/480p/360p 表示该码率下大致的画质。","刷新头像","无画面:主播已离线","长按查看当前画面","不刷新"],"KO":["컨트롤의 “이전 / 다음” 버튼","모델 미리보기","모델 소개 숨기기","그룹 선택 숨기기","미리보기 갱신","현재 스트림:","녹화 해상도는 스트림 해상도와 같으며 변경되지 않습니다. 위 옵션은 비트레이트(압축)만 제한하며, 1080p/720p/480p/360p는 해당 비트레이트에서의 대략적인 화질입니다.","아바타 새로고침","프레임 없음: 모델이 오프라인입니다","길게 눌러 현재 프레임 보기","갱신 안 함"],"HE":["כפתורי «הקודם / הבא» בבקרה","תצוגות מקדימות של מודלים","הסתר את תיאור המודל","הסתר את בחירת הקבוצה","רענון התצוגה המקדימה","הסטרים הנוכחי:","רזולוציית ההקלטה זהה לרזולוציית הסטרים ואינה משתנה. האפשרויות למעלה מגבילות רק את קצב הסיביות (דחיסה); 1080p/720p/480p/360p מציינות את איכות התמונה המשוערת בקצב סיביות זה.","רענון תמונות פרופיל","אין פריים: המודל לא מחובר","לחיצה ארוכה להצגת הפריים הנוכחי","ללא רענון"],"AR":["أزرار «السابق / التالي» في لوحة التحكم","معاينات الموديلات","إخفاء وصف الموديل","إخفاء اختيار المجموعة","تحديث المعاينة","البث الحالي:","دقة التسجيل تساوي دقة البث ولا تتغير. الخيارات أعلاه تحدّ من معدل البت (الضغط) فقط؛ 1080p/720p/480p/360p تشير إلى جودة الصورة التقريبية عند هذا المعدل.","تحديث الصور الرمزية","لا توجد لقطة: الموديل غير متصل","اضغط مطولًا لعرض اللقطة الحالية","بدون تحديث"],"HI":["कंट्रोल पर «पिछला / अगला» बटन","मॉडल प्रीव्यू","मॉडल का विवरण छिपाएँ","ग्रुप चयन छिपाएँ","प्रीव्यू रीफ़्रेश","मौजूदा स्ट्रीम:","रिकॉर्डिंग का रेज़ोल्यूशन स्ट्रीम के रेज़ोल्यूशन जितना ही रहता है और बदलता नहीं। ऊपर के विकल्प केवल बिटरेट (कंप्रेशन) सीमित करते हैं; 1080p/720p/480p/360p उस बिटरेट पर लगभग कैसी तस्वीर मिलेगी, यह बताते हैं।","अवतार रीफ़्रेश करें","फ़्रेम उपलब्ध नहीं: मॉडल ऑफ़लाइन है","मौजूदा फ़्रेम देखने के लिए दबाए रखें","रीफ़्रेश नहीं"],"ID":["Tombol «sebelumnya / berikutnya» di kontrol","Pratinjau model","Sembunyikan deskripsi model","Sembunyikan pilihan grup","Penyegaran pratinjau","Stream saat ini:","Resolusi rekaman sama dengan resolusi stream dan tidak berubah. Opsi di atas hanya membatasi bitrate (kompresi); 1080p/720p/480p/360p menunjukkan perkiraan kualitas gambar pada bitrate tersebut.","Segarkan avatar","Tidak ada bingkai: model sedang offline","Tahan untuk melihat bingkai saat ini","Tanpa pembaruan"],"VI":["Nút «trước / sau» trên bảng điều khiển","Xem trước model","Ẩn mô tả model","Ẩn lựa chọn nhóm","Làm mới xem trước","Luồng hiện tại:","Độ phân giải bản ghi bằng độ phân giải của luồng và không thay đổi. Các tùy chọn ở trên chỉ giới hạn bitrate (nén); 1080p/720p/480p/360p cho biết chất lượng hình ảnh xấp xỉ ở bitrate đó.","Làm mới ảnh đại diện","Không có khung hình: model đang offline","Giữ để xem khung hình hiện tại","Không làm mới"],"TH":["ปุ่ม «ก่อนหน้า / ถัดไป» บนแผงควบคุม","พรีวิวโมเดล","ซ่อนคำอธิบายโมเดล","ซ่อนตัวเลือกกลุ่ม","การรีเฟรชพรีวิว","สตรีมปัจจุบัน:","ความละเอียดของการอัดเท่ากับความละเอียดของสตรีมและไม่เปลี่ยนแปลง ตัวเลือกด้านบนจำกัดเฉพาะบิตเรต (การบีบอัด) เท่านั้น 1080p/720p/480p/360p คือคุณภาพภาพโดยประมาณที่บิตเรตนั้น","รีเฟรชอวาตาร์","ไม่มีเฟรม: โมเดลออฟไลน์","กดค้างเพื่อดูเฟรมปัจจุบัน","ไม่รีเฟรช"]}});
Object.keys(I18N_Y).forEach(k => { i18n[k] = Object.assign(i18n[k] || {}, I18N_Y[k]); });
const I18N_Z = {
    RU: {
        settings: 'Настройки', st_look: 'Оформление', st_lang: 'Язык', st_color: 'Цвет панели', st_color_rnd: 'Случайный', st_custom: 'Свой цвет', st_scale: 'Размер панели',
        st_hue: 'Оттенок', st_sat: 'Насыщенность', st_lig: 'Яркость',
        st_site_dark: 'Тёмная тема сайта', st_btns: 'Кнопки панели', st_btns_hint: 'Нажмите, чтобы скрыть кнопку или вернуть её на панель.',
        st_btns_reset: 'Показать все кнопки', st_open: 'Открыть', st_rec: 'Запись видео', st_fav: 'Избранное', st_star: 'Звёздочка на миниатюрах', st_list: 'Список',
        st_hist: 'История просмотров', st_hist_on: 'Вести историю', st_data: 'Данные', bk_parts: 'Состав копии', bk_p_set: 'Настройки', bk_p_fav: 'Избранное', bk_p_hist: 'История', bk_csv: 'Таблицы CSV', bk_csv_fav: 'Избранное', bk_csv_hist: 'История', bk_csv_ses: 'Сеансы',
        bt_stats: 'Зрители', bt_vol: 'Громкость', bt_hints: 'Подсказки', sort_viewers: 'Зрители', viewers_tip: 'Зрителей в комнате',
        hist_title: 'История', hist_empty: 'Пока пусто. Время считается, пока идёт видео.', hist_s_last: 'Недавние', hist_s_time: 'Время', hist_s_vis: 'Визиты',
        hist_total: 'Моделей: {n} · Всего: {t}', hist_visits: 'виз.', hist_clear: 'Очистить', hist_clear_q: 'Удалить всю историю просмотров?',
        hist_del: 'Убрать из истории',
        star_add: 'В избранное', star_del: 'Убрать из избранного', star_del_q: 'У этой модели есть заметка или группа. Убрать из избранного?',
        cf_ok: 'Да', cf_cancel: 'Отмена'
    },
    EN: {
        settings: 'Settings', st_look: 'Appearance', st_lang: 'Language', st_color: 'Panel color', st_color_rnd: 'Random', st_custom: 'Custom color', st_scale: 'Panel size',
        st_hue: 'Hue', st_sat: 'Saturation', st_lig: 'Lightness',
        st_site_dark: 'Site dark theme', st_btns: 'Panel buttons', st_btns_hint: 'Tap to hide a button or bring it back.',
        st_btns_reset: 'Show all buttons', st_open: 'Open', st_rec: 'Video recording', st_fav: 'Favorites', st_star: 'Star on thumbnails', st_list: 'List',
        st_hist: 'Watch history', st_hist_on: 'Keep history', st_data: 'Data', bk_parts: 'Backup contents', bk_p_set: 'Settings', bk_p_fav: 'Favorites', bk_p_hist: 'History', bk_csv: 'CSV tables', bk_csv_fav: 'Favorites', bk_csv_hist: 'History', bk_csv_ses: 'Sessions',
        bt_stats: 'Viewers', bt_vol: 'Volume', bt_hints: 'Hints', sort_viewers: 'Viewers', viewers_tip: 'Viewers in the room',
        hist_title: 'History', hist_empty: 'Empty for now. Time is counted while the video is playing.', hist_s_last: 'Recent', hist_s_time: 'Time', hist_s_vis: 'Visits',
        hist_total: 'Models: {n} · Total: {t}', hist_visits: 'visits', hist_clear: 'Clear', hist_clear_q: 'Delete the whole watch history?',
        hist_del: 'Remove from history',
        star_add: 'Add to favorites', star_del: 'Remove from favorites', star_del_q: 'This model has a note or a group. Remove from favorites?',
        cf_ok: 'Yes', cf_cancel: 'Cancel'
    },
    UK: {
        settings: 'Налаштування', st_look: 'Оформлення', st_lang: 'Мова', st_color: 'Колір панелі', st_color_rnd: 'Випадковий', st_custom: 'Свій колір', st_scale: 'Розмір панелі',
        st_hue: 'Відтінок', st_sat: 'Насиченість', st_lig: 'Яскравість',
        st_site_dark: 'Темна тема сайту', st_btns: 'Кнопки панелі', st_btns_hint: 'Натисніть, щоб сховати кнопку або повернути її на панель.',
        st_btns_reset: 'Показати всі кнопки', st_open: 'Відкрити', st_rec: 'Запис відео', st_fav: 'Обране', st_star: 'Зірочка на мініатюрах', st_list: 'Список',
        st_hist: 'Історія переглядів', st_hist_on: 'Зберігати історію', st_data: 'Дані', bk_parts: 'Склад копії', bk_p_set: 'Налаштування', bk_p_fav: 'Обране', bk_p_hist: 'Історія', bk_csv: 'Таблиці CSV', bk_csv_fav: 'Обране', bk_csv_hist: 'Історія', bk_csv_ses: 'Сеанси',
        bt_stats: 'Глядачі', bt_vol: 'Гучність', bt_hints: 'Підказки', sort_viewers: 'Глядачі', viewers_tip: 'Глядачів у кімнаті',
        hist_title: 'Історія', hist_empty: 'Поки порожньо. Час враховується, поки йде відео.', hist_s_last: 'Нещодавні', hist_s_time: 'Час', hist_s_vis: 'Візити',
        hist_total: 'Моделей: {n} · Усього: {t}', hist_visits: 'віз.', hist_clear: 'Очистити', hist_clear_q: 'Видалити всю історію переглядів?',
        hist_del: 'Прибрати з історії',
        star_add: 'В обране', star_del: 'Прибрати з обраного', star_del_q: 'У цієї моделі є нотатка або група. Прибрати з обраного?',
        cf_ok: 'Так', cf_cancel: 'Скасувати'
    }
};
Object.keys(I18N_Z).forEach(k => { i18n[k] = Object.assign(i18n[k] || {}, I18N_Z[k]); });
const I18N_W = unpackI18N({"k":["settings","st_look","st_lang","st_color","st_color_rnd","st_custom","st_scale","st_hue","st_sat","st_lig","st_site_dark","st_btns","st_btns_hint","st_btns_reset","st_open","st_rec","st_fav","st_star","st_list","st_hist","st_hist_on","st_data","bk_parts","bk_p_set","bk_p_fav","bk_p_hist","bk_csv","bk_csv_fav","bk_csv_hist","bk_csv_ses","bt_stats","bt_vol","bt_hints","sort_viewers","viewers_tip","hist_title","hist_empty","hist_s_last","hist_s_time","hist_s_vis","hist_total","hist_visits","hist_clear","hist_clear_q","hist_del","star_add","star_del","star_del_q","cf_ok","cf_cancel"],"v":{"ES":["Ajustes","Apariencia","Idioma","Color del panel","Aleatorio","Color personalizado","Tamaño del panel","Tono","Saturación","Luminosidad","Tema oscuro del sitio","Botones del panel","Toca para ocultar un botón o devolverlo.","Mostrar todos los botones","Abrir","Grabación de vídeo","Favoritos","Estrella en las miniaturas","Lista","Historial de visualización","Guardar historial","Datos","Contenido de la copia","Ajustes","Favoritos","Historial","Tablas CSV","Favoritos","Historial","Sesiones","Espectadores","Volumen","Consejos","Espectadores","Espectadores en la sala","Historial","Vacío por ahora. El tiempo se cuenta mientras se reproduce el vídeo.","Recientes","Tiempo","Visitas","Modelos: {n} · Total: {t}","visitas","Borrar","¿Borrar todo el historial de visualización?","Quitar del historial","Añadir a favoritos","Quitar de favoritos","Este modelo tiene una nota o un grupo. ¿Quitar de favoritos?","Sí","Cancelar"],"DE":["Einstellungen","Darstellung","Sprache","Panelfarbe","Zufällig","Eigene Farbe","Panelgröße","Farbton","Sättigung","Helligkeit","Dunkles Seitendesign","Panel-Schaltflächen","Tippen, um eine Schaltfläche aus- oder wieder einzublenden.","Alle Schaltflächen anzeigen","Öffnen","Videoaufnahme","Favoriten","Stern auf Vorschaubildern","Liste","Wiedergabeverlauf","Verlauf speichern","Daten","Inhalt der Sicherung","Einstellungen","Favoriten","Verlauf","CSV-Tabellen","Favoriten","Verlauf","Sitzungen","Zuschauer","Lautstärke","Hinweise","Zuschauer","Zuschauer im Raum","Verlauf","Noch leer. Die Zeit wird gezählt, solange das Video läuft.","Zuletzt","Zeit","Besuche","Models: {n} · Gesamt: {t}","Besuche","Leeren","Gesamten Wiedergabeverlauf löschen?","Aus dem Verlauf entfernen","Zu Favoriten hinzufügen","Aus Favoriten entfernen","Dieses Model hat eine Notiz oder eine Gruppe. Aus Favoriten entfernen?","Ja","Abbrechen"],"FR":["Paramètres","Apparence","Langue","Couleur du panneau","Aléatoire","Couleur personnalisée","Taille du panneau","Teinte","Saturation","Luminosité","Thème sombre du site","Boutons du panneau","Touchez pour masquer un bouton ou le remettre.","Afficher tous les boutons","Ouvrir","Enregistrement vidéo","Favoris","Étoile sur les miniatures","Liste","Historique de visionnage","Conserver l’historique","Données","Contenu de la sauvegarde","Paramètres","Favoris","Historique","Tableaux CSV","Favoris","Historique","Sessions","Spectateurs","Volume","Astuces","Spectateurs","Spectateurs dans le salon","Historique","Vide pour l’instant. Le temps est compté pendant la lecture de la vidéo.","Récents","Durée","Visites","Modèles : {n} · Total : {t}","visites","Effacer","Supprimer tout l’historique de visionnage ?","Retirer de l’historique","Ajouter aux favoris","Retirer des favoris","Ce modèle a une note ou un groupe. Le retirer des favoris ?","Oui","Annuler"],"PT":["Configurações","Aparência","Idioma","Cor do painel","Aleatória","Cor personalizada","Tamanho do painel","Matiz","Saturação","Luminosidade","Tema escuro do site","Botões do painel","Toque para ocultar um botão ou trazê-lo de volta.","Mostrar todos os botões","Abrir","Gravação de vídeo","Favoritos","Estrela nas miniaturas","Lista","Histórico de visualização","Manter histórico","Dados","Conteúdo do backup","Configurações","Favoritos","Histórico","Tabelas CSV","Favoritos","Histórico","Sessões","Espectadores","Volume","Dicas","Espectadores","Espectadores na sala","Histórico","Vazio por enquanto. O tempo é contado enquanto o vídeo está tocando.","Recentes","Tempo","Visitas","Modelos: {n} · Total: {t}","visitas","Limpar","Apagar todo o histórico de visualização?","Remover do histórico","Adicionar aos favoritos","Remover dos favoritos","Este modelo tem uma nota ou um grupo. Remover dos favoritos?","Sim","Cancelar"],"IT":["Impostazioni","Aspetto","Lingua","Colore del pannello","Casuale","Colore personalizzato","Dimensione del pannello","Tonalità","Saturazione","Luminosità","Tema scuro del sito","Pulsanti del pannello","Tocca per nascondere un pulsante o riportarlo.","Mostra tutti i pulsanti","Apri","Registrazione video","Preferiti","Stella sulle miniature","Elenco","Cronologia di visione","Salva cronologia","Dati","Contenuto del backup","Impostazioni","Preferiti","Cronologia","Tabelle CSV","Preferiti","Cronologia","Sessioni","Spettatori","Volume","Suggerimenti","Spettatori","Spettatori nella stanza","Cronologia","Ancora vuota. Il tempo viene conteggiato mentre il video è in riproduzione.","Recenti","Tempo","Visite","Modelli: {n} · Totale: {t}","visite","Svuota","Eliminare tutta la cronologia di visione?","Rimuovi dalla cronologia","Aggiungi ai preferiti","Rimuovi dai preferiti","Questo modello ha una nota o un gruppo. Rimuoverlo dai preferiti?","Sì","Annulla"],"PL":["Ustawienia","Wygląd","Język","Kolor panelu","Losowy","Własny kolor","Rozmiar panelu","Odcień","Nasycenie","Jasność","Ciemny motyw strony","Przyciski panelu","Dotknij, aby ukryć przycisk lub go przywrócić.","Pokaż wszystkie przyciski","Otwórz","Nagrywanie wideo","Ulubione","Gwiazdka na miniaturach","Lista","Historia oglądania","Zapisuj historię","Dane","Zawartość kopii","Ustawienia","Ulubione","Historia","Tabele CSV","Ulubione","Historia","Sesje","Widzowie","Głośność","Podpowiedzi","Widzowie","Widzowie w pokoju","Historia","Na razie pusto. Czas jest liczony, gdy wideo jest odtwarzane.","Ostatnie","Czas","Wizyty","Modele: {n} · Razem: {t}","wizyt","Wyczyść","Usunąć całą historię oglądania?","Usuń z historii","Dodaj do ulubionych","Usuń z ulubionych","Ten model ma notatkę lub grupę. Usunąć z ulubionych?","Tak","Anuluj"],"TR":["Ayarlar","Görünüm","Dil","Panel rengi","Rastgele","Özel renk","Panel boyutu","Ton","Doygunluk","Parlaklık","Sitenin koyu teması","Panel düğmeleri","Bir düğmeyi gizlemek veya geri getirmek için dokunun.","Tüm düğmeleri göster","Aç","Video kaydı","Favoriler","Küçük resimlerde yıldız","Liste","İzleme geçmişi","Geçmişi tut","Veri","Yedek içeriği","Ayarlar","Favoriler","Geçmiş","CSV tabloları","Favoriler","Geçmiş","Oturumlar","İzleyiciler","Ses","İpuçları","İzleyiciler","Odadaki izleyiciler","Geçmiş","Şimdilik boş. Süre, video oynarken sayılır.","Son","Süre","Ziyaret","Modeller: {n} · Toplam: {t}","ziyaret","Temizle","Tüm izleme geçmişi silinsin mi?","Geçmişten kaldır","Favorilere ekle","Favorilerden kaldır","Bu modelin notu veya grubu var. Favorilerden kaldırılsın mı?","Evet","İptal"],"NL":["Instellingen","Uiterlijk","Taal","Paneelkleur","Willekeurig","Eigen kleur","Paneelgrootte","Tint","Verzadiging","Helderheid","Donker thema van de site","Paneelknoppen","Tik om een knop te verbergen of terug te zetten.","Alle knoppen tonen","Openen","Video-opname","Favorieten","Ster op miniaturen","Lijst","Kijkgeschiedenis","Geschiedenis bewaren","Gegevens","Inhoud van de back-up","Instellingen","Favorieten","Geschiedenis","CSV-tabellen","Favorieten","Geschiedenis","Sessies","Kijkers","Volume","Tips","Kijkers","Kijkers in de kamer","Geschiedenis","Nog leeg. De tijd wordt geteld terwijl de video speelt.","Recent","Tijd","Bezoeken","Modellen: {n} · Totaal: {t}","bezoeken","Wissen","De hele kijkgeschiedenis verwijderen?","Verwijderen uit geschiedenis","Toevoegen aan favorieten","Verwijderen uit favorieten","Dit model heeft een notitie of groep. Verwijderen uit favorieten?","Ja","Annuleren"],"CS":["Nastavení","Vzhled","Jazyk","Barva panelu","Náhodná","Vlastní barva","Velikost panelu","Odstín","Sytost","Jas","Tmavý motiv webu","Tlačítka panelu","Klepnutím tlačítko skryjete nebo vrátíte.","Zobrazit všechna tlačítka","Otevřít","Nahrávání videa","Oblíbené","Hvězdička na náhledech","Seznam","Historie sledování","Ukládat historii","Data","Obsah zálohy","Nastavení","Oblíbené","Historie","Tabulky CSV","Oblíbené","Historie","Relace","Diváci","Hlasitost","Tipy","Diváci","Diváci v místnosti","Historie","Zatím prázdné. Čas se počítá, dokud video běží.","Nedávné","Čas","Návštěvy","Modelky: {n} · Celkem: {t}","návštěv","Vymazat","Smazat celou historii sledování?","Odebrat z historie","Přidat do oblíbených","Odebrat z oblíbených","Tato modelka má poznámku nebo skupinu. Odebrat z oblíbených?","Ano","Zrušit"],"RO":["Setări","Aspect","Limbă","Culoarea panoului","Aleatorie","Culoare personalizată","Dimensiunea panoului","Nuanță","Saturație","Luminozitate","Tema întunecată a site-ului","Butoanele panoului","Atinge pentru a ascunde un buton sau a-l readuce.","Arată toate butoanele","Deschide","Înregistrare video","Favorite","Stea pe miniaturi","Listă","Istoric vizionări","Păstrează istoricul","Date","Conținutul copiei","Setări","Favorite","Istoric","Tabele CSV","Favorite","Istoric","Sesiuni","Spectatori","Volum","Sfaturi","Spectatori","Spectatori în cameră","Istoric","Gol deocamdată. Timpul se numără cât timp rulează videoul.","Recente","Timp","Vizite","Modele: {n} · Total: {t}","vizite","Șterge","Ștergi tot istoricul de vizionări?","Elimină din istoric","Adaugă la favorite","Elimină din favorite","Acest model are o notă sau un grup. Îl elimini din favorite?","Da","Anulează"],"HU":["Beállítások","Megjelenés","Nyelv","Panel színe","Véletlen","Egyéni szín","Panel mérete","Árnyalat","Telítettség","Fényerő","Az oldal sötét témája","Panelgombok","Koppints egy gomb elrejtéséhez vagy visszaállításához.","Összes gomb megjelenítése","Megnyitás","Videófelvétel","Kedvencek","Csillag az előnézeteken","Lista","Megtekintési előzmények","Előzmények mentése","Adatok","A mentés tartalma","Beállítások","Kedvencek","Előzmények","CSV-táblázatok","Kedvencek","Előzmények","Munkamenetek","Nézők","Hangerő","Tippek","Nézők","Nézők a szobában","Előzmények","Egyelőre üres. Az idő a videó lejátszása közben telik.","Legutóbbi","Idő","Látogatások","Modellek: {n} · Összesen: {t}","látogatás","Törlés","Törlöd a teljes megtekintési előzményt?","Eltávolítás az előzményekből","Hozzáadás a kedvencekhez","Eltávolítás a kedvencekből","Ennek a modellnek van jegyzete vagy csoportja. Eltávolítod a kedvencekből?","Igen","Mégse"],"BG":["Настройки","Оформление","Език","Цвят на панела","Случаен","Собствен цвят","Размер на панела","Нюанс","Наситеност","Яркост","Тъмна тема на сайта","Бутони на панела","Докоснете, за да скриете бутон или да го върнете.","Покажи всички бутони","Отвори","Запис на видео","Любими","Звезда върху миниатюрите","Списък","История на гледането","Пази история","Данни","Съдържание на копието","Настройки","Любими","История","CSV таблици","Любими","История","Сесии","Зрители","Сила на звука","Подсказки","Зрители","Зрители в стаята","История","Засега е празно. Времето се брои, докато видеото върви.","Скорошни","Време","Посещения","Модели: {n} · Общо: {t}","посещ.","Изчисти","Да се изтрие ли цялата история на гледане?","Премахни от историята","Добави в любими","Премахни от любими","Този модел има бележка или група. Да се премахне ли от любими?","Да","Отказ"],"EL":["Ρυθμίσεις","Εμφάνιση","Γλώσσα","Χρώμα πάνελ","Τυχαίο","Προσαρμοσμένο χρώμα","Μέγεθος πάνελ","Απόχρωση","Κορεσμός","Φωτεινότητα","Σκούρο θέμα ιστοτόπου","Κουμπιά πάνελ","Πατήστε για να κρύψετε ένα κουμπί ή να το επαναφέρετε.","Εμφάνιση όλων των κουμπιών","Άνοιγμα","Εγγραφή βίντεο","Αγαπημένα","Αστέρι στις μικρογραφίες","Λίστα","Ιστορικό προβολών","Διατήρηση ιστορικού","Δεδομένα","Περιεχόμενο αντιγράφου","Ρυθμίσεις","Αγαπημένα","Ιστορικό","Πίνακες CSV","Αγαπημένα","Ιστορικό","Συνεδρίες","Θεατές","Ένταση","Συμβουλές","Θεατές","Θεατές στο δωμάτιο","Ιστορικό","Άδειο προς το παρόν. Ο χρόνος μετριέται όσο παίζει το βίντεο.","Πρόσφατα","Χρόνος","Επισκέψεις","Μοντέλα: {n} · Σύνολο: {t}","επισκέψεις","Εκκαθάριση","Διαγραφή ολόκληρου του ιστορικού προβολών;","Αφαίρεση από το ιστορικό","Προσθήκη στα αγαπημένα","Αφαίρεση από τα αγαπημένα","Αυτό το μοντέλο έχει σημείωση ή ομάδα. Αφαίρεση από τα αγαπημένα;","Ναι","Ακύρωση"],"JA":["設定","外観","言語","パネルの色","ランダム","カスタムカラー","パネルのサイズ","色相","彩度","明度","サイトのダークテーマ","パネルのボタン","タップしてボタンを非表示にしたり、元に戻したりできます。","すべてのボタンを表示","開く","動画の録画","お気に入り","サムネイルの星","リスト","視聴履歴","履歴を保存","データ","バックアップの内容","設定","お気に入り","履歴","CSV表","お気に入り","履歴","セッション","視聴者","音量","ヒント","視聴者","ルームの視聴者数","履歴","まだ空です。動画の再生中に時間がカウントされます。","最近","時間","訪問","モデル: {n} · 合計: {t}","回","消去","視聴履歴をすべて削除しますか?","履歴から削除","お気に入りに追加","お気に入りから削除","このモデルにはメモまたはグループがあります。お気に入りから削除しますか?","はい","キャンセル"],"ZH":["设置","外观","语言","面板颜色","随机","自定义颜色","面板大小","色相","饱和度","亮度","网站深色主题","面板按钮","点按可隐藏按钮或将其恢复。","显示所有按钮","打开","视频录制","收藏夹","缩略图上的星标","列表","观看历史","保存历史","数据","备份内容","设置","收藏夹","历史","CSV 表格","收藏夹","历史","会话","观众","音量","提示","观众","房间内观众","历史","暂无记录。视频播放时才会计时。","最近","时长","访问","模特:{n} · 总计:{t}","次访问","清空","删除全部观看历史?","从历史中移除","加入收藏夹","从收藏夹移除","该模特有备注或分组。要从收藏夹移除吗?","是","取消"],"KO":["설정","외관","언어","패널 색상","무작위","사용자 지정 색상","패널 크기","색조","채도","명도","사이트 다크 테마","패널 버튼","버튼을 숨기거나 다시 표시하려면 탭하세요.","모든 버튼 표시","열기","영상 녹화","즐겨찾기","썸네일 별표","목록","시청 기록","기록 저장","데이터","백업 내용","설정","즐겨찾기","기록","CSV 표","즐겨찾기","기록","세션","시청자","볼륨","도움말","시청자","방 시청자 수","기록","아직 비어 있습니다. 영상이 재생되는 동안 시간이 집계됩니다.","최근","시간","방문","모델: {n} · 합계: {t}","회","비우기","시청 기록을 모두 삭제할까요?","기록에서 제거","즐겨찾기에 추가","즐겨찾기에서 제거","이 모델에 메모나 그룹이 있습니다. 즐겨찾기에서 제거할까요?","예","취소"],"HE":["הגדרות","מראה","שפה","צבע הפאנל","אקראי","צבע מותאם","גודל הפאנל","גוון","רוויה","בהירות","ערכת נושא כהה לאתר","כפתורי הפאנל","הקש כדי להסתיר כפתור או להחזיר אותו.","הצג את כל הכפתורים","פתח","הקלטת וידאו","מועדפים","כוכב על התמונות הממוזערות","רשימה","היסטוריית צפייה","שמור היסטוריה","נתונים","תוכן הגיבוי","הגדרות","מועדפים","היסטוריה","טבלאות CSV","מועדפים","היסטוריה","סשנים","צופים","עוצמת קול","טיפים","צופים","צופים בחדר","היסטוריה","ריק לבינתיים. הזמן נספר בזמן שהווידאו מנגן.","אחרונים","זמן","ביקורים","מודלים: {n} · סה״כ: {t}","ביקורים","נקה","למחוק את כל היסטוריית הצפייה?","הסר מההיסטוריה","הוסף למועדפים","הסר מהמועדפים","למודל הזה יש הערה או קבוצה. להסיר מהמועדפים?","כן","ביטול"],"AR":["الإعدادات","المظهر","اللغة","لون اللوحة","عشوائي","لون مخصص","حجم اللوحة","التدرج اللوني","التشبع","السطوع","الوضع الداكن للموقع","أزرار اللوحة","المس لإخفاء زر أو إعادته.","إظهار كل الأزرار","فتح","تسجيل الفيديو","المفضلة","نجمة على الصور المصغرة","القائمة","سجل المشاهدة","حفظ السجل","البيانات","محتوى النسخة الاحتياطية","الإعدادات","المفضلة","السجل","جداول CSV","المفضلة","السجل","الجلسات","المشاهدون","الصوت","تلميحات","المشاهدون","المشاهدون في الغرفة","السجل","فارغ حتى الآن. يُحتسب الوقت أثناء تشغيل الفيديو.","الأحدث","الوقت","الزيارات","العارضات: {n} · الإجمالي: {t}","زيارات","مسح","حذف كل سجل المشاهدة؟","إزالة من السجل","إضافة إلى المفضلة","إزالة من المفضلة","لدى هذه العارضة ملاحظة أو مجموعة. هل تريد إزالتها من المفضلة؟","نعم","إلغاء"],"HI":["सेटिंग्स","दिखावट","भाषा","पैनल का रंग","रैंडम","अपना रंग","पैनल का आकार","रंगत","संतृप्ति","चमक","साइट की डार्क थीम","पैनल बटन","किसी बटन को छिपाने या वापस लाने के लिए टैप करें।","सभी बटन दिखाएँ","खोलें","वीडियो रिकॉर्डिंग","पसंदीदा","थंबनेल पर तारा","सूची","देखने का इतिहास","इतिहास सहेजें","डेटा","बैकअप की सामग्री","सेटिंग्स","पसंदीदा","इतिहास","CSV तालिकाएँ","पसंदीदा","इतिहास","सेशन","दर्शक","आवाज़","सुझाव","दर्शक","कमरे में दर्शक","इतिहास","अभी खाली है। वीडियो चलने पर ही समय गिना जाता है।","हाल के","समय","विज़िट","मॉडल: {n} · कुल: {t}","विज़िट","साफ़ करें","पूरा देखने का इतिहास हटाएँ?","इतिहास से हटाएँ","पसंदीदा में जोड़ें","पसंदीदा से हटाएँ","इस मॉडल का नोट या ग्रुप है। पसंदीदा से हटाएँ?","हाँ","रद्द करें"],"ID":["Pengaturan","Tampilan","Bahasa","Warna panel","Acak","Warna kustom","Ukuran panel","Rona","Saturasi","Kecerahan","Tema gelap situs","Tombol panel","Ketuk untuk menyembunyikan tombol atau memunculkannya kembali.","Tampilkan semua tombol","Buka","Perekaman video","Favorit","Bintang di thumbnail","Daftar","Riwayat tontonan","Simpan riwayat","Data","Isi cadangan","Pengaturan","Favorit","Riwayat","Tabel CSV","Favorit","Riwayat","Sesi","Penonton","Volume","Petunjuk","Penonton","Penonton di ruangan","Riwayat","Masih kosong. Waktu dihitung saat video diputar.","Terbaru","Waktu","Kunjungan","Model: {n} · Total: {t}","kunjungan","Hapus","Hapus seluruh riwayat tontonan?","Hapus dari riwayat","Tambah ke favorit","Hapus dari favorit","Model ini punya catatan atau grup. Hapus dari favorit?","Ya","Batal"],"VI":["Cài đặt","Giao diện","Ngôn ngữ","Màu bảng điều khiển","Ngẫu nhiên","Màu tùy chỉnh","Kích thước bảng","Sắc độ","Độ bão hòa","Độ sáng","Giao diện tối của trang","Nút trên bảng","Chạm để ẩn nút hoặc hiện lại.","Hiện tất cả nút","Mở","Ghi video","Yêu thích","Ngôi sao trên ảnh thu nhỏ","Danh sách","Lịch sử xem","Lưu lịch sử","Dữ liệu","Nội dung bản sao lưu","Cài đặt","Yêu thích","Lịch sử","Bảng CSV","Yêu thích","Lịch sử","Phiên","Người xem","Âm lượng","Gợi ý","Người xem","Người xem trong phòng","Lịch sử","Hiện còn trống. Thời gian được tính khi video đang phát.","Gần đây","Thời gian","Lượt xem","Người mẫu: {n} · Tổng: {t}","lượt","Xóa","Xóa toàn bộ lịch sử xem?","Xóa khỏi lịch sử","Thêm vào yêu thích","Xóa khỏi yêu thích","Người mẫu này có ghi chú hoặc nhóm. Xóa khỏi yêu thích?","Có","Hủy"],"TH":["การตั้งค่า","รูปลักษณ์","ภาษา","สีของแผง","สุ่ม","สีกำหนดเอง","ขนาดแผง","เฉดสี","ความอิ่มตัว","ความสว่าง","ธีมมืดของเว็บไซต์","ปุ่มบนแผง","แตะเพื่อซ่อนปุ่มหรือนำกลับมา","แสดงปุ่มทั้งหมด","เปิด","การบันทึกวิดีโอ","รายการโปรด","ดาวบนภาพขนาดย่อ","รายการ","ประวัติการรับชม","เก็บประวัติ","ข้อมูล","เนื้อหาของข้อมูลสำรอง","การตั้งค่า","รายการโปรด","ประวัติ","ตาราง CSV","รายการโปรด","ประวัติ","เซสชัน","ผู้ชม","ระดับเสียง","คำแนะนำ","ผู้ชม","ผู้ชมในห้อง","ประวัติ","ยังว่างอยู่ เวลาจะนับขณะที่วิดีโอเล่น","ล่าสุด","เวลา","การเข้าชม","โมเดล: {n} · รวม: {t}","ครั้ง","ล้าง","ลบประวัติการรับชมทั้งหมดหรือไม่?","นำออกจากประวัติ","เพิ่มในรายการโปรด","นำออกจากรายการโปรด","โมเดลนี้มีโน้ตหรือกลุ่ม นำออกจากรายการโปรดหรือไม่?","ใช่","ยกเลิก"]}});
Object.keys(I18N_W).forEach(k => { i18n[k] = Object.assign(i18n[k] || {}, I18N_W[k]); });
const I18N_FIX = {"ES": {"bg": "FONDO","bk_exp": "Exportar copia de seguridad","bk_imp": "Importar copia de seguridad","rs_off": "Desactivado","star_del_q": "Esta modelo tiene una nota o un grupo. ¿Quitar de favoritos?"},"DE": {"bg": "HINT.","bk_exp": "Sicherung exportieren","bk_imp": "Sicherung importieren"},"FR": {"bg": "FOND","bk_exp": "Exporter la sauvegarde","bk_imp": "Importer la sauvegarde","rs_off": "Désactivé"},"PT": {"bg": "FUNDO","bk_exp": "Exportar backup","bk_imp": "Importar backup","rs_off": "Desativado","star_del_q": "Esta modelo tem uma nota ou um grupo. Remover dos favoritos?"},"IT": {"bg": "SFONDO","bk_exp": "Esporta backup","bk_imp": "Importa backup","rs_off": "Disattivato","star_del_q": "Questa modella ha una nota o un gruppo. Rimuoverla dai preferiti?","hist_total": "Modelle: {n} · Totale: {t}"},"PL": {"bk_exp": "Eksport kopii zapasowej","bk_imp": "Import kopii zapasowej","star_del_q": "Ta modelka ma notatkę lub grupę. Usunąć z ulubionych?","hist_total": "Modelki: {n} · Razem: {t}"},"TR": {"bk_exp": "Yedeği dışa aktar","bk_imp": "Yedeği içe aktar"},"NL": {"bk_exp": "Back-up exporteren","bk_imp": "Back-up importeren"},"CS": {"bk_exp": "Export zálohy","bk_imp": "Import zálohy"},"RO": {"sort_last": "Văzut","bk_exp": "Exportă copia de rezervă","bk_imp": "Importă copia de rezervă"},"HU": {"bk_exp": "Biztonsági mentés exportálása","bk_imp": "Biztonsági mentés importálása"},"BG": {"bk_exp": "Експорт на резервно копие","bk_imp": "Импорт на резервно копие"},"EL": {"rel": "Ανανέωση","bk_exp": "Εξαγωγή αντιγράφου ασφαλείας","bk_imp": "Εισαγωγή αντιγράφου ασφαλείας","rs_off": "Ανενεργό","st_private": "Ιδιωτικό","st_group": "Ομάδα"},"JA": {"bk_exp": "バックアップをエクスポート","bk_imp": "バックアップをインポート"},"ZH": {"bg": "背景","bk_exp": "导出备份","bk_imp": "导入备份","star_del_q": "该主播有备注或分组。要从收藏夹移除吗?","hist_total": "主播:{n} · 总计:{t}"},"KO": {"bk_exp": "백업 내보내기","bk_imp": "백업 가져오기"},"HE": {"bk_exp": "ייצוא גיבוי","bk_imp": "ייבוא גיבוי"},"AR": {"bk_exp": "تصدير النسخة الاحتياطية","bk_imp": "استيراد النسخة الاحتياطية","star_del_q": "لدى هذا الموديل ملاحظة أو مجموعة. هل تريد إزالته من المفضلة؟","hist_total": "الموديلات: {n} · الإجمالي: {t}"},"HI": {"st_away": "अनुपलब्ध","bk_exp": "बैकअप निर्यात करें","bk_imp": "बैकअप आयात करें","no_model": "मॉडल नहीं मिला","bm_nomatch": "फ़िल्टर से कोई मॉडल मेल नहीं खाता"},"ID": {"st_away": "Tidak di tempat","sort_last": "Terakhir dilihat","bk_exp": "Ekspor cadangan","bk_imp": "Impor cadangan"},"VI": {"bk_exp": "Xuất bản sao lưu","bk_imp": "Nhập bản sao lưu","hist_total": "Model: {n} · Tổng: {t}"},"TH": {"bk_exp": "ส่งออกข้อมูลสำรอง","bk_imp": "นำเข้าข้อมูลสำรอง"},"UK": {"bk_exp": "Експорт резервної копії","bk_imp": "Імпорт резервної копії"}};
Object.keys(I18N_FIX).forEach(k => { i18n[k] = Object.assign(i18n[k] || {}, I18N_FIX[k]); });
const I18N_HIDE = {"ES":{"st_ptags":"Ocultar etiquetas del perfil (BongaCams)","st_hide_note":"Ocultar campo de nota (etiquetas)"},"DE":{"st_ptags":"Profil-Tags ausblenden (BongaCams)","st_hide_note":"Notizfeld (Tags) ausblenden"},"FR":{"st_ptags":"Masquer les tags du profil (BongaCams)","st_hide_note":"Masquer le champ de note (tags)"},"PT":{"st_ptags":"Ocultar tags do perfil (BongaCams)","st_hide_note":"Ocultar campo de nota (tags)"},"IT":{"st_ptags":"Nascondi i tag del profilo (BongaCams)","st_hide_note":"Nascondi il campo note (tag)"},"PL":{"st_ptags":"Ukryj tagi profilu (BongaCams)","st_hide_note":"Ukryj pole notatki (tagi)"},"TR":{"st_ptags":"Profil etiketlerini gizle (BongaCams)","st_hide_note":"Not alanını gizle (etiketler)"},"NL":{"st_ptags":"Profieltags verbergen (BongaCams)","st_hide_note":"Notitieveld (tags) verbergen"},"CS":{"st_ptags":"Skrýt štítky profilu (BongaCams)","st_hide_note":"Skrýt pole poznámky (štítky)"},"RO":{"st_ptags":"Ascunde etichetele profilului (BongaCams)","st_hide_note":"Ascunde câmpul de notițe (etichete)"},"HU":{"st_ptags":"Profilcímkék elrejtése (BongaCams)","st_hide_note":"Megjegyzés mező elrejtése (címkék)"},"BG":{"st_ptags":"Скрий таговете на профила (BongaCams)","st_hide_note":"Скрий полето за бележка (тагове)"},"EL":{"st_ptags":"Απόκρυψη ετικετών προφίλ (BongaCams)","st_hide_note":"Απόκρυψη πεδίου σημείωσης (ετικέτες)"},"JA":{"st_ptags":"プロフィールのタグを非表示 (BongaCams)","st_hide_note":"メモ欄(タグ)を非表示"},"ZH":{"st_ptags":"隐藏个人资料标签 (BongaCams)","st_hide_note":"隐藏备注栏(标签)"},"KO":{"st_ptags":"프로필 태그 숨기기 (BongaCams)","st_hide_note":"메모 입력란(태그) 숨기기"},"HE":{"st_ptags":"הסתר תגיות פרופיל (BongaCams)","st_hide_note":"הסתר שדה הערה (תגיות)"},"AR":{"st_ptags":"إخفاء وسوم الملف الشخصي (BongaCams)","st_hide_note":"إخفاء حقل الملاحظة (الوسوم)"},"HI":{"st_ptags":"प्रोफ़ाइल टैग छिपाएँ (BongaCams)","st_hide_note":"नोट फ़ील्ड (टैग) छिपाएँ"},"ID":{"st_ptags":"Sembunyikan tag profil (BongaCams)","st_hide_note":"Sembunyikan kolom catatan (tag)"},"VI":{"st_ptags":"Ẩn thẻ hồ sơ (BongaCams)","st_hide_note":"Ẩn ô ghi chú (thẻ)"},"TH":{"st_ptags":"ซ่อนแท็กโปรไฟล์ (BongaCams)","st_hide_note":"ซ่อนช่องบันทึก (แท็ก)"}};
Object.keys(I18N_HIDE).forEach(k => { Object.assign(i18n[k], I18N_HIDE[k]); });
Object.assign(i18n.RU, { sleep_title: 'Таймер сна', sleep_hint: 'Через выбранное время плеер будет выключен, а запись остановится и сохранится. Нажмите на чёрный экран, чтобы вернуть плеер.', sleep_left: 'Осталось', sleep_custom: 'Своё время', sleep_sec: 'сек', sleep_start: 'Запустить', sleep_wake: 'Таймер сна сработал.\nНажмите, чтобы продолжить' });
Object.assign(i18n.EN, { sleep_title: 'Sleep timer', sleep_hint: 'When time is up, the player is switched off and any recording stops and is saved. Tap the black screen to bring it back.', sleep_left: 'Left', sleep_custom: 'Custom time', sleep_sec: 'sec', sleep_start: 'Start', sleep_wake: 'Sleep timer finished.\nTap to continue' });
i18n.UK = Object.assign(i18n.UK || {}, { sleep_title: 'Таймер сну', sleep_hint: 'Через обраний час плеєр буде вимкнено, а запис зупиниться й збережеться. Натисніть на чорний екран, щоб повернути плеєр.', sleep_left: 'Залишилось', sleep_custom: 'Свій час', sleep_sec: 'сек', sleep_start: 'Запустити', sleep_wake: 'Таймер сну спрацював.\nНатисніть, щоб продовжити' });
Object.assign(i18n.RU, { ch_btn: 'Диаграммы', ch_days: 'Время по дням, 14 дней', ch_hours: 'Активность по часам', ch_top: 'Топ по времени', ch_peak: 'Пик', ch_avg: 'В среднем в день' });
Object.assign(i18n.EN, { mv_title: 'Player', mv_add: 'Add stream', mv_cols: 'Layout', mv_min: 'Minimize', mv_close: 'Close', mv_hint: 'Add streams to watch several at once. Tap a tile to move the sound to it.', mv_url: 'https://…/playlist.m3u8', mv_go: 'Add', mv_done: 'Done', mv_back: 'Back', mv_bm_h: 'Live bookmarks', mv_link_h: 'Or by link', mv_cur: 'Current stream', mv_search: 'Search live bookmarks', mv_none: 'No live bookmarks', mv_load: 'Loading…', mv_off: 'Stream unavailable', mv_retry: 'tap to retry', mv_full: 'Window is full', mv_live: 'LIVE', mv_auto: 'Auto', mv_qual: 'Quality', mv_dvr: 'Rewind buffer', mv_pip: 'Picture-in-picture', mv_fs: 'Fullscreen', mv_big: 'Enlarge', mv_play: 'Play / pause', mv_mute: 'Mute', mv_min_u: 'min', mv_dvr_off: 'Off', mv_clear_q: 'Close all streams?' });
Object.assign(i18n.RU, { mv_title: 'Плеер', mv_add: 'Добавить', mv_cols: 'Сетка', mv_min: 'Свернуть', mv_close: 'Закрыть', mv_hint: 'Добавьте трансляции, чтобы смотреть несколько сразу. Нажмите на плитку, чтобы включить на ней звук.', mv_url: 'https://…/playlist.m3u8', mv_go: 'Добавить', mv_done: 'Готово', mv_back: 'Назад', mv_bm_h: 'Закладки в эфире', mv_link_h: 'Или по ссылке', mv_cur: 'Текущая трансляция', mv_search: 'Поиск по онлайн-закладкам', mv_none: 'Нет закладок в эфире', mv_load: 'Загрузка…', mv_off: 'Трансляция недоступна', mv_retry: 'нажмите, чтобы повторить', mv_full: 'Окно заполнено', mv_live: 'LIVE', mv_auto: 'Авто', mv_qual: 'Качество', mv_dvr: 'Буфер перемотки', mv_pip: 'Картинка в картинке', mv_fs: 'Во весь экран', mv_big: 'Увеличить', mv_play: 'Пуск / пауза', mv_mute: 'Выключить звук', mv_min_u: 'мин', mv_dvr_off: 'Выкл', mv_clear_q: 'Закрыть все трансляции?' });
Object.assign(i18n.EN, { rs_src: 'Source', rs_src_cap: 'Capture (re-encode)', rs_src_hls: 'HLS (no re-encode)', rs_src_note: 'HLS downloads the stream segments as they are, without re-encoding, and saves MP4. It needs the stream address; if it is not available, Capture is used instead. Quality is original, as in the source stream. Recording is written to disk in small checkpoints.', rs_recover: 'Recover unsaved recordings' });
Object.assign(i18n.RU, { rs_src: 'Источник', rs_src_cap: 'Захват (перекодирование)', rs_src_hls: 'HLS (без перекодирования)', rs_src_note: 'HLS скачивает сегменты потока как есть, без перекодирования, и сохраняет MP4. Нужен адрес потока; если его нет, используется захват. Качество оригинальное, как у исходного потока. Запись пишется на диск небольшими порциями.', rs_recover: 'Восстановить несохранённые записи' });
Object.assign(i18n.EN, { ch_btn: 'Charts', ch_days: 'Time per day, 14 days', ch_hours: 'Activity by hour', ch_top: 'Top by time', ch_peak: 'Peak', ch_avg: 'Daily average' });
i18n.UK = Object.assign(i18n.UK || {}, { ch_btn: 'Діаграми', ch_days: 'Час по днях, 14 днів', ch_hours: 'Активність по годинах', ch_top: 'Топ за часом', ch_peak: 'Пік', ch_avg: 'У середньому за день' });
const I18N_S = {
    RU: { st_cols: "Колонки", st_back: "Назад", st_ptags: "Скрыть теги профиля (BongaCams)", st_hide_note: "Скрыть поле заметки (теги)" },
    EN: { st_cols: "Columns", st_back: "Back", st_ptags: "Hide profile tags (BongaCams)", st_hide_note: "Hide note field (tags)" },
    UK: { st_cols: "Колонки", st_back: "Назад", st_ptags: "Сховати теги профілю (BongaCams)", st_hide_note: "Сховати поле нотатки (теги)" },
    ES: { sleep_title: "Temporizador de apagado", sleep_hint: "Al terminar el tiempo, el reproductor se apaga y cualquier grabación se detiene y se guarda. Toca la pantalla negra para recuperarlo.", sleep_left: "Quedan", sleep_custom: "Tiempo personalizado", sleep_sec: "seg", sleep_start: "Iniciar", sleep_wake: "El temporizador ha terminado.\nToca para continuar", st_cols: "Columnas", st_back: "Atrás" },
    DE: { sleep_title: "Schlaf-Timer", sleep_hint: "Nach Ablauf der Zeit wird der Player ausgeschaltet, und eine laufende Aufnahme wird gestoppt und gespeichert. Tippe auf den schwarzen Bildschirm, um ihn zurückzuholen.", sleep_left: "Übrig", sleep_custom: "Eigene Zeit", sleep_sec: "Sek", sleep_start: "Starten", sleep_wake: "Schlaf-Timer abgelaufen.\nTippen zum Fortsetzen", st_cols: "Spalten", st_back: "Zurück" },
    FR: { sleep_title: "Minuteur de veille", sleep_hint: "À la fin du temps, le lecteur s’éteint et tout enregistrement s’arrête et est sauvegardé. Touchez l’écran noir pour le rallumer.", sleep_left: "Restant", sleep_custom: "Durée personnalisée", sleep_sec: "s", sleep_start: "Démarrer", sleep_wake: "Minuteur terminé.\nTouchez pour continuer", st_cols: "Colonnes", st_back: "Retour" },
    PT: { sleep_title: "Temporizador de sono", sleep_hint: "Ao fim do tempo, o player é desligado e qualquer gravação é interrompida e salva. Toque na tela preta para trazê-lo de volta.", sleep_left: "Restante", sleep_custom: "Tempo personalizado", sleep_sec: "seg", sleep_start: "Iniciar", sleep_wake: "O temporizador terminou.\nToque para continuar", st_cols: "Colunas", st_back: "Voltar" },
    IT: { sleep_title: "Timer di spegnimento", sleep_hint: "Allo scadere del tempo il player si spegne e ogni registrazione si interrompe e viene salvata. Tocca lo schermo nero per riattivarlo.", sleep_left: "Rimanente", sleep_custom: "Tempo personalizzato", sleep_sec: "sec", sleep_start: "Avvia", sleep_wake: "Timer terminato.\nTocca per continuare", st_cols: "Colonne", st_back: "Indietro" },
    PL: { sleep_title: "Wyłącznik czasowy", sleep_hint: "Po upływie czasu odtwarzacz zostanie wyłączony, a nagrywanie zatrzymane i zapisane. Dotknij czarnego ekranu, aby go przywrócić.", sleep_left: "Pozostało", sleep_custom: "Własny czas", sleep_sec: "sek", sleep_start: "Start", sleep_wake: "Czas minął.\nDotknij, aby kontynuować", st_cols: "Kolumny", st_back: "Wstecz" },
    TR: { sleep_title: "Uyku zamanlayıcısı", sleep_hint: "Süre dolunca oynatıcı kapanır, varsa kayıt durdurulur ve kaydedilir. Geri getirmek için siyah ekrana dokunun.", sleep_left: "Kalan", sleep_custom: "Özel süre", sleep_sec: "sn", sleep_start: "Başlat", sleep_wake: "Zamanlayıcı bitti.\nDevam etmek için dokunun", st_cols: "Sütunlar", st_back: "Geri" },
    NL: { sleep_title: "Slaaptimer", sleep_hint: "Als de tijd om is, wordt de speler uitgeschakeld en wordt een lopende opname gestopt en opgeslagen. Tik op het zwarte scherm om de speler terug te halen.", sleep_left: "Resterend", sleep_custom: "Eigen tijd", sleep_sec: "sec", sleep_start: "Starten", sleep_wake: "Slaaptimer afgelopen.\nTik om door te gaan", st_cols: "Kolommen", st_back: "Terug" },
    CS: { sleep_title: "Časovač vypnutí", sleep_hint: "Po uplynutí času se přehrávač vypne a případné nahrávání se zastaví a uloží. Klepnutím na černou obrazovku ho vrátíte.", sleep_left: "Zbývá", sleep_custom: "Vlastní čas", sleep_sec: "s", sleep_start: "Spustit", sleep_wake: "Časovač skončil.\nKlepnutím pokračujte", st_cols: "Sloupce", st_back: "Zpět" },
    RO: { sleep_title: "Temporizator de somn", sleep_hint: "La expirarea timpului, playerul se oprește, iar orice înregistrare se oprește și se salvează. Atinge ecranul negru pentru a-l readuce.", sleep_left: "Rămas", sleep_custom: "Timp personalizat", sleep_sec: "sec", sleep_start: "Pornește", sleep_wake: "Temporizatorul s-a încheiat.\nAtinge pentru a continua", st_cols: "Coloane", st_back: "Înapoi" },
    HU: { sleep_title: "Alvásidőzítő", sleep_hint: "Az idő leteltével a lejátszó kikapcsol, a felvétel pedig leáll és mentésre kerül. Koppints a fekete képernyőre a visszakapcsoláshoz.", sleep_left: "Hátra van", sleep_custom: "Egyéni idő", sleep_sec: "mp", sleep_start: "Indítás", sleep_wake: "Az időzítő lejárt.\nKoppints a folytatáshoz", st_cols: "Oszlopok", st_back: "Vissza" },
    BG: { sleep_title: "Таймер за заспиване", sleep_hint: "Когато времето изтече, плеърът се изключва, а всеки запис спира и се запазва. Докоснете черния екран, за да го върнете.", sleep_left: "Остава", sleep_custom: "Собствено време", sleep_sec: "сек", sleep_start: "Старт", sleep_wake: "Таймерът приключи.\nДокоснете, за да продължите", st_cols: "Колони", st_back: "Назад" },
    EL: { sleep_title: "Χρονοδιακόπτης ύπνου", sleep_hint: "Όταν λήξει ο χρόνος, ο player απενεργοποιείται και κάθε εγγραφή σταματά και αποθηκεύεται. Αγγίξτε τη μαύρη οθόνη για να τον επαναφέρετε.", sleep_left: "Απομένουν", sleep_custom: "Προσαρμοσμένος χρόνος", sleep_sec: "δευτ.", sleep_start: "Έναρξη", sleep_wake: "Ο χρονοδιακόπτης έληξε.\nΑγγίξτε για συνέχεια", st_cols: "Στήλες", st_back: "Πίσω" },
    JA: { sleep_title: "スリープタイマー", sleep_hint: "時間になるとプレーヤーが停止し、録画中の場合は録画が止まって保存されます。黒い画面をタップすると元に戻ります。", sleep_left: "残り", sleep_custom: "時間を指定", sleep_sec: "秒", sleep_start: "開始", sleep_wake: "スリープタイマーが終了しました。\nタップして続行", st_cols: "列数", st_back: "戻る" },
    ZH: { sleep_title: "睡眠定时器", sleep_hint: "时间到后,播放器会关闭,正在进行的录制会停止并保存。点按黑屏即可恢复播放器。", sleep_left: "剩余", sleep_custom: "自定义时间", sleep_sec: "秒", sleep_start: "开始", sleep_wake: "睡眠定时已结束。\n点按继续", st_cols: "列数", st_back: "返回" },
    KO: { sleep_title: "취침 타이머", sleep_hint: "시간이 되면 플레이어가 꺼지고 녹화 중이던 영상은 중지되어 저장됩니다. 검은 화면을 탭하면 플레이어가 돌아옵니다.", sleep_left: "남음", sleep_custom: "직접 설정", sleep_sec: "초", sleep_start: "시작", sleep_wake: "취침 타이머가 종료되었습니다.\n탭하여 계속", st_cols: "열 수", st_back: "뒤로" },
    HE: { sleep_title: "טיימר שינה", sleep_hint: "בתום הזמן הנגן יכבה, וכל הקלטה תיעצר ותישמר. הקש על המסך השחור כדי להחזיר את הנגן.", sleep_left: "נותר", sleep_custom: "זמן מותאם", sleep_sec: "שנ׳", sleep_start: "התחל", sleep_wake: "טיימר השינה הסתיים.\nהקש להמשך", st_cols: "עמודות", st_back: "חזרה" },
    AR: { sleep_title: "مؤقّت النوم", sleep_hint: "عند انتهاء الوقت يُطفأ المشغّل ويتوقف أي تسجيل ويُحفظ. المس الشاشة السوداء لإعادته.", sleep_left: "المتبقي", sleep_custom: "وقت مخصص", sleep_sec: "ث", sleep_start: "ابدأ", sleep_wake: "انتهى مؤقّت النوم.\nالمس للمتابعة", st_cols: "الأعمدة", st_back: "رجوع" },
    HI: { sleep_title: "स्लीप टाइमर", sleep_hint: "समय पूरा होने पर प्लेयर बंद हो जाएगा, और चल रही रिकॉर्डिंग रुककर सेव हो जाएगी। प्लेयर वापस लाने के लिए काली स्क्रीन पर टैप करें।", sleep_left: "शेष", sleep_custom: "अपना समय", sleep_sec: "सेक", sleep_start: "शुरू करें", sleep_wake: "स्लीप टाइमर पूरा हुआ।\nजारी रखने के लिए टैप करें", st_cols: "कॉलम", st_back: "वापस" },
    ID: { sleep_title: "Timer tidur", sleep_hint: "Saat waktu habis, pemutar dimatikan dan rekaman yang berjalan dihentikan lalu disimpan. Ketuk layar hitam untuk mengembalikannya.", sleep_left: "Sisa", sleep_custom: "Waktu kustom", sleep_sec: "dtk", sleep_start: "Mulai", sleep_wake: "Timer tidur selesai.\nKetuk untuk melanjutkan", st_cols: "Kolom", st_back: "Kembali" },
    VI: { sleep_title: "Hẹn giờ tắt", sleep_hint: "Khi hết giờ, trình phát sẽ tắt và mọi bản ghi đang chạy sẽ dừng và được lưu. Chạm vào màn hình đen để bật lại.", sleep_left: "Còn lại", sleep_custom: "Thời gian tùy chỉnh", sleep_sec: "giây", sleep_start: "Bắt đầu", sleep_wake: "Hẹn giờ đã kết thúc.\nChạm để tiếp tục", st_cols: "Số cột", st_back: "Quay lại" },
    TH: { sleep_title: "ตั้งเวลาปิด", sleep_hint: "เมื่อหมดเวลา เครื่องเล่นจะปิด และการอัดที่กำลังทำอยู่จะหยุดและบันทึกไว้ แตะหน้าจอสีดำเพื่อเปิดกลับมา", sleep_left: "เหลือ", sleep_custom: "กำหนดเวลาเอง", sleep_sec: "วินาที", sleep_start: "เริ่ม", sleep_wake: "ตั้งเวลาปิดสิ้นสุดแล้ว\nแตะเพื่อดำเนินการต่อ", st_cols: "จำนวนคอลัมน์", st_back: "ย้อนกลับ" }
};
Object.keys(I18N_S).forEach(k => { i18n[k] = Object.assign(i18n[k] || {}, I18N_S[k]); });
const I18N_RAD = {
    RU: { rad_title: 'Круговое меню', rad_reset: 'Сбросить позицию кружка', rad_media: 'Медиа', rad_view: 'Экран', rad_nav: 'Избранное', rad_page: 'Страница', rad_vup: 'Громче', rad_vdn: 'Тише', rad_prev: 'Пред.', rad_next: 'След.' },
    EN: { rad_title: 'Radial menu', rad_reset: 'Reset button position', rad_media: 'Media', rad_view: 'View', rad_nav: 'Favorites', rad_page: 'Page', rad_vup: 'Louder', rad_vdn: 'Quieter', rad_prev: 'Prev', rad_next: 'Next' },
    UK: { rad_title: 'Колове меню', rad_reset: 'Скинути позицію кружечка', rad_media: 'Медіа', rad_view: 'Екран', rad_nav: 'Обране', rad_page: 'Сторінка', rad_vup: 'Гучніше', rad_vdn: 'Тихіше', rad_prev: 'Попер.', rad_next: 'Наст.' },
    ES: { ch_btn: "Gráficos", ch_days: "Tiempo por día, 14 días", ch_hours: "Actividad por hora", ch_top: "Top por tiempo", ch_peak: "Pico", ch_avg: "Media diaria", rad_title: "Menú radial", rad_reset: "Restablecer posición del botón", rad_media: "Medios", rad_view: "Pantalla", rad_nav: "Favoritos", rad_page: "Página", rad_vup: "Más alto", rad_vdn: "Más bajo", rad_prev: "Ant.", rad_next: "Sig." },
    DE: { ch_btn: "Diagramme", ch_days: "Zeit pro Tag, 14 Tage", ch_hours: "Aktivität nach Stunde", ch_top: "Top nach Zeit", ch_peak: "Spitze", ch_avg: "Tagesdurchschnitt", rad_title: "Radialmenü", rad_reset: "Position des Buttons zurücksetzen", rad_media: "Medien", rad_view: "Ansicht", rad_nav: "Favoriten", rad_page: "Seite", rad_vup: "Lauter", rad_vdn: "Leiser", rad_prev: "Zurück", rad_next: "Weiter" },
    FR: { ch_btn: "Graphiques", ch_days: "Temps par jour, 14 jours", ch_hours: "Activité par heure", ch_top: "Top par durée", ch_peak: "Pic", ch_avg: "Moyenne quotidienne", rad_title: "Menu radial", rad_reset: "Réinitialiser la position du bouton", rad_media: "Médias", rad_view: "Affichage", rad_nav: "Favoris", rad_page: "Page", rad_vup: "Plus fort", rad_vdn: "Moins fort", rad_prev: "Préc.", rad_next: "Suiv." },
    PT: { ch_btn: "Gráficos", ch_days: "Tempo por dia, 14 dias", ch_hours: "Atividade por hora", ch_top: "Top por tempo", ch_peak: "Pico", ch_avg: "Média diária", rad_title: "Menu radial", rad_reset: "Redefinir posição do botão", rad_media: "Mídia", rad_view: "Tela", rad_nav: "Favoritos", rad_page: "Página", rad_vup: "Mais alto", rad_vdn: "Mais baixo", rad_prev: "Ant.", rad_next: "Próx." },
    IT: { ch_btn: "Grafici", ch_days: "Tempo per giorno, 14 giorni", ch_hours: "Attività per ora", ch_top: "Top per tempo", ch_peak: "Picco", ch_avg: "Media giornaliera", rad_title: "Menu radiale", rad_reset: "Reimposta posizione del pulsante", rad_media: "Media", rad_view: "Schermo", rad_nav: "Preferiti", rad_page: "Pagina", rad_vup: "Più forte", rad_vdn: "Più piano", rad_prev: "Prec.", rad_next: "Succ." },
    PL: { ch_btn: "Wykresy", ch_days: "Czas dziennie, 14 dni", ch_hours: "Aktywność według godzin", ch_top: "Top według czasu", ch_peak: "Szczyt", ch_avg: "Średnia dzienna", rad_title: "Menu kołowe", rad_reset: "Resetuj pozycję przycisku", rad_media: "Media", rad_view: "Widok", rad_nav: "Ulubione", rad_page: "Strona", rad_vup: "Głośniej", rad_vdn: "Ciszej", rad_prev: "Poprz.", rad_next: "Nast." },
    TR: { ch_btn: "Grafikler", ch_days: "Günlük süre, 14 gün", ch_hours: "Saate göre etkinlik", ch_top: "Süreye göre en çok", ch_peak: "Zirve", ch_avg: "Günlük ortalama", rad_title: "Dairesel menü", rad_reset: "Düğme konumunu sıfırla", rad_media: "Medya", rad_view: "Ekran", rad_nav: "Favoriler", rad_page: "Sayfa", rad_vup: "Daha yüksek", rad_vdn: "Daha kısık", rad_prev: "Önceki", rad_next: "Sonraki" },
    NL: { ch_btn: "Grafieken", ch_days: "Tijd per dag, 14 dagen", ch_hours: "Activiteit per uur", ch_top: "Top op tijd", ch_peak: "Piek", ch_avg: "Daggemiddelde", rad_title: "Radiaal menu", rad_reset: "Knoppositie resetten", rad_media: "Media", rad_view: "Weergave", rad_nav: "Favorieten", rad_page: "Pagina", rad_vup: "Harder", rad_vdn: "Zachter", rad_prev: "Vorige", rad_next: "Volgende" },
    CS: { ch_btn: "Grafy", ch_days: "Čas za den, 14 dní", ch_hours: "Aktivita podle hodin", ch_top: "Top podle času", ch_peak: "Špička", ch_avg: "Denní průměr", rad_title: "Kruhové menu", rad_reset: "Obnovit pozici tlačítka", rad_media: "Média", rad_view: "Zobrazení", rad_nav: "Oblíbené", rad_page: "Stránka", rad_vup: "Hlasitěji", rad_vdn: "Tišeji", rad_prev: "Předch.", rad_next: "Další" },
    RO: { ch_btn: "Grafice", ch_days: "Timp pe zi, 14 zile", ch_hours: "Activitate pe ore", ch_top: "Top după timp", ch_peak: "Vârf", ch_avg: "Media zilnică", rad_title: "Meniu radial", rad_reset: "Resetează poziția butonului", rad_media: "Media", rad_view: "Ecran", rad_nav: "Favorite", rad_page: "Pagină", rad_vup: "Mai tare", rad_vdn: "Mai încet", rad_prev: "Ant.", rad_next: "Urm." },
    HU: { ch_btn: "Diagramok", ch_days: "Idő naponta, 14 nap", ch_hours: "Aktivitás óránként", ch_top: "Toplista idő szerint", ch_peak: "Csúcs", ch_avg: "Napi átlag", rad_title: "Körmenü", rad_reset: "Gomb helyének visszaállítása", rad_media: "Média", rad_view: "Nézet", rad_nav: "Kedvencek", rad_page: "Oldal", rad_vup: "Hangosabb", rad_vdn: "Halkabb", rad_prev: "Előző", rad_next: "Következő" },
    BG: { ch_btn: "Диаграми", ch_days: "Време по дни, 14 дни", ch_hours: "Активност по часове", ch_top: "Топ по време", ch_peak: "Пик", ch_avg: "Средно на ден", rad_title: "Радиално меню", rad_reset: "Нулиране на позицията на бутона", rad_media: "Медия", rad_view: "Екран", rad_nav: "Любими", rad_page: "Страница", rad_vup: "По-силно", rad_vdn: "По-тихо", rad_prev: "Пред.", rad_next: "След." },
    EL: { ch_btn: "Διαγράμματα", ch_days: "Χρόνος ανά ημέρα, 14 ημέρες", ch_hours: "Δραστηριότητα ανά ώρα", ch_top: "Κορυφαία ανά χρόνο", ch_peak: "Κορύφωση", ch_avg: "Ημερήσιος μέσος όρος", rad_title: "Κυκλικό μενού", rad_reset: "Επαναφορά θέσης κουμπιού", rad_media: "Πολυμέσα", rad_view: "Προβολή", rad_nav: "Αγαπημένα", rad_page: "Σελίδα", rad_vup: "Πιο δυνατά", rad_vdn: "Πιο χαμηλά", rad_prev: "Προηγ.", rad_next: "Επόμ." },
    JA: { ch_btn: "グラフ", ch_days: "1日あたりの時間(14日間)", ch_hours: "時間帯別のアクティビティ", ch_top: "時間ランキング", ch_peak: "ピーク", ch_avg: "1日の平均", rad_title: "ラジアルメニュー", rad_reset: "ボタン位置をリセット", rad_media: "メディア", rad_view: "表示", rad_nav: "お気に入り", rad_page: "ページ", rad_vup: "音量アップ", rad_vdn: "音量ダウン", rad_prev: "前へ", rad_next: "次へ" },
    ZH: { ch_btn: "图表", ch_days: "每日时长,14天", ch_hours: "按小时的活跃度", ch_top: "时长排行", ch_peak: "峰值", ch_avg: "日均", rad_title: "环形菜单", rad_reset: "重置按钮位置", rad_media: "媒体", rad_view: "显示", rad_nav: "收藏夹", rad_page: "页面", rad_vup: "调大", rad_vdn: "调小", rad_prev: "上一个", rad_next: "下一个" },
    KO: { ch_btn: "차트", ch_days: "일별 시간, 14일", ch_hours: "시간대별 활동", ch_top: "시간 순위", ch_peak: "최고치", ch_avg: "일평균", rad_title: "원형 메뉴", rad_reset: "버튼 위치 초기화", rad_media: "미디어", rad_view: "화면", rad_nav: "즐겨찾기", rad_page: "페이지", rad_vup: "소리 크게", rad_vdn: "소리 작게", rad_prev: "이전", rad_next: "다음" },
    HE: { ch_btn: "תרשימים", ch_days: "זמן ליום, 14 ימים", ch_hours: "פעילות לפי שעה", ch_top: "מובילים לפי זמן", ch_peak: "שיא", ch_avg: "ממוצע יומי", rad_title: "תפריט מעגלי", rad_reset: "איפוס מיקום הכפתור", rad_media: "מדיה", rad_view: "תצוגה", rad_nav: "מועדפים", rad_page: "עמוד", rad_vup: "חזק יותר", rad_vdn: "שקט יותר", rad_prev: "הקודם", rad_next: "הבא" },
    AR: { ch_btn: "الرسوم البيانية", ch_days: "الوقت يوميًا، 14 يومًا", ch_hours: "النشاط حسب الساعة", ch_top: "الأعلى حسب الوقت", ch_peak: "الذروة", ch_avg: "المتوسط اليومي", rad_title: "القائمة الدائرية", rad_reset: "إعادة ضبط موضع الزر", rad_media: "الوسائط", rad_view: "العرض", rad_nav: "المفضلة", rad_page: "الصفحة", rad_vup: "أعلى", rad_vdn: "أخفض", rad_prev: "السابق", rad_next: "التالي" },
    HI: { ch_btn: "चार्ट", ch_days: "प्रति दिन समय, 14 दिन", ch_hours: "घंटे के अनुसार गतिविधि", ch_top: "समय के अनुसार शीर्ष", ch_peak: "शिखर", ch_avg: "दैनिक औसत", rad_title: "गोलाकार मेनू", rad_reset: "बटन की स्थिति रीसेट करें", rad_media: "मीडिया", rad_view: "दृश्य", rad_nav: "पसंदीदा", rad_page: "पेज", rad_vup: "आवाज़ बढ़ाएँ", rad_vdn: "आवाज़ घटाएँ", rad_prev: "पिछला", rad_next: "अगला" },
    ID: { ch_btn: "Grafik", ch_days: "Waktu per hari, 14 hari", ch_hours: "Aktivitas per jam", ch_top: "Teratas menurut waktu", ch_peak: "Puncak", ch_avg: "Rata-rata harian", rad_title: "Menu radial", rad_reset: "Atur ulang posisi tombol", rad_media: "Media", rad_view: "Tampilan", rad_nav: "Favorit", rad_page: "Halaman", rad_vup: "Lebih keras", rad_vdn: "Lebih pelan", rad_prev: "Sebelumnya", rad_next: "Berikutnya" },
    VI: { ch_btn: "Biểu đồ", ch_days: "Thời gian mỗi ngày, 14 ngày", ch_hours: "Hoạt động theo giờ", ch_top: "Xếp hạng theo thời gian", ch_peak: "Đỉnh", ch_avg: "Trung bình mỗi ngày", rad_title: "Menu vòng tròn", rad_reset: "Đặt lại vị trí nút", rad_media: "Phương tiện", rad_view: "Hiển thị", rad_nav: "Yêu thích", rad_page: "Trang", rad_vup: "Tăng âm", rad_vdn: "Giảm âm", rad_prev: "Trước", rad_next: "Sau" },
    TH: { ch_btn: "แผนภูมิ", ch_days: "เวลาต่อวัน 14 วัน", ch_hours: "กิจกรรมตามชั่วโมง", ch_top: "อันดับตามเวลา", ch_peak: "สูงสุด", ch_avg: "ค่าเฉลี่ยรายวัน", rad_title: "เมนูวงกลม", rad_reset: "รีเซ็ตตำแหน่งปุ่ม", rad_media: "สื่อ", rad_view: "มุมมอง", rad_nav: "รายการโปรด", rad_page: "หน้า", rad_vup: "เสียงดังขึ้น", rad_vdn: "เสียงเบาลง", rad_prev: "ก่อนหน้า", rad_next: "ถัดไป" }
};
Object.keys(I18N_RAD).forEach(k => { i18n[k] = Object.assign(i18n[k] || {}, I18N_RAD[k]); });
Object.assign(i18n.EN, { mv_abr: 'Auto', mv_abr_row: 'Auto quality', mv_abr_t: 'Auto quality: lowers the quality when the video stutters or the network gets weak, raises it when the connection is stable' });
Object.assign(i18n.RU, { mv_abr: 'Авто', mv_abr_row: 'Автокачество', mv_abr_t: 'Автокачество: снижает качество при зависаниях или слабой сети и повышает, когда соединение ровное' });
Object.assign(i18n.EN, { mv_auto_all: 'Add all live streams automatically' });
Object.assign(i18n.RU, { mv_auto_all: 'Добавлять все стримы сразу' });
i18n.UK = Object.assign(i18n.UK || {}, { mv_auto_all: 'Додавати всі стріми одразу' });
const I18N_MV = unpackI18N({"k":["mv_title","mv_add","mv_cols","mv_min","mv_close","mv_hint","mv_go","mv_done","mv_back","mv_bm_h","mv_link_h","mv_cur","mv_search","mv_none","mv_load","mv_off","mv_retry","mv_full","mv_auto","mv_qual","mv_dvr","mv_pip","mv_fs","mv_big","mv_play","mv_mute","mv_min_u","mv_dvr_off","mv_clear_q","rs_src","rs_src_cap","rs_src_hls","rs_src_note","rs_recover"],"v":{"UK":["Плеєр","Додати трансляцію","Сітка","Згорнути","Закрити","Додайте трансляції, щоб дивитися кілька одразу. Торкніться плитки, щоб увімкнути на ній звук.","Додати","Готово","Назад","Закладки в ефірі","Або за посиланням","Поточна трансляція","Пошук за закладками в ефірі","Немає закладок в ефірі","Завантаження…","Трансляція недоступна","торкніться, щоб повторити","Вікно заповнене","Авто","Якість","Буфер перемотування","Картинка в картинці","На весь екран","Збільшити","Пуск / пауза","Вимкнути звук","хв","Вимк.","Закрити всі трансляції?","Джерело","Захоплення (перекодування)","HLS (без перекодування)","HLS завантажує сегменти потоку як є, без перекодування, і зберігає MP4. Потрібна адреса потоку; якщо її немає, використовується захоплення. Якість оригінальна, як у вихідного потоку. Запис пишеться на диск невеликими порціями.","Відновити незбережені записи"],"ES":["Reproductor","Añadir transmisión","Cuadrícula","Minimizar","Cerrar","Añade transmisiones para ver varias a la vez. Toca un recuadro para pasarle el sonido.","Añadir","Hecho","Atrás","Favoritos en directo","O mediante enlace","Transmisión actual","Buscar favoritos en directo","No hay favoritos en directo","Cargando…","Transmisión no disponible","toca para reintentar","La ventana está llena","Auto","Calidad","Búfer de retroceso","Imagen en imagen","Pantalla completa","Ampliar","Reproducir / pausa","Silenciar","min","No","¿Cerrar todas las transmisiones?","Origen","Captura (recodificación)","HLS (sin recodificar)","HLS descarga los segmentos de la transmisión tal cual, sin recodificar, y guarda MP4. Necesita la dirección de la transmisión; si no está disponible, se usa la captura. La calidad es la original, la del flujo de origen. La grabación se escribe en disco en pequeños puntos de control.","Recuperar grabaciones sin guardar"],"DE":["Player","Stream hinzufügen","Layout","Minimieren","Schließen","Füge Streams hinzu, um mehrere gleichzeitig zu sehen. Tippe auf eine Kachel, um den Ton dorthin zu legen.","Hinzufügen","Fertig","Zurück","Live-Lesezeichen","Oder per Link","Aktueller Stream","Live-Lesezeichen durchsuchen","Keine Live-Lesezeichen","Wird geladen…","Stream nicht verfügbar","zum Wiederholen tippen","Fenster ist voll","Auto","Qualität","Zurückspul-Puffer","Bild-in-Bild","Vollbild","Vergrößern","Wiedergabe / Pause","Stumm","Min","Aus","Alle Streams schließen?","Quelle","Mitschnitt (Neukodierung)","HLS (ohne Neukodierung)","HLS lädt die Stream-Segmente unverändert ohne Neukodierung herunter und speichert MP4. Dafür wird die Stream-Adresse benötigt; ist sie nicht verfügbar, wird der Mitschnitt genutzt. Die Qualität entspricht dem Original-Stream. Die Aufzeichnung wird in kleinen Schritten auf die Festplatte geschrieben.","Nicht gespeicherte Aufnahmen wiederherstellen"],"FR":["Lecteur","Ajouter un flux","Disposition","Réduire","Fermer","Ajoutez des flux pour en regarder plusieurs à la fois. Touchez une vignette pour y basculer le son.","Ajouter","Terminé","Retour","Favoris en direct","Ou par lien","Flux actuel","Rechercher dans les favoris en direct","Aucun favori en direct","Chargement…","Flux indisponible","touchez pour réessayer","La fenêtre est pleine","Auto","Qualité","Tampon de retour arrière","Image dans l’image","Plein écran","Agrandir","Lecture / pause","Couper le son","min","Non","Fermer tous les flux ?","Source","Capture (réencodage)","HLS (sans réencodage)","HLS télécharge les segments du flux tels quels, sans réencodage, et enregistre en MP4. L’adresse du flux est nécessaire ; si elle est indisponible, la capture est utilisée. La qualité est celle d’origine, identique au flux source. L’enregistrement est écrit sur le disque par petits points de contrôle.","Récupérer les enregistrements non sauvegardés"],"PT":["Player","Adicionar transmissão","Layout","Minimizar","Fechar","Adicione transmissões para assistir a várias ao mesmo tempo. Toque em um bloco para passar o som para ele.","Adicionar","Concluído","Voltar","Favoritos ao vivo","Ou por link","Transmissão atual","Buscar favoritos ao vivo","Nenhum favorito ao vivo","Carregando…","Transmissão indisponível","toque para tentar de novo","A janela está cheia","Auto","Qualidade","Buffer de retrocesso","Imagem em imagem","Tela cheia","Ampliar","Reproduzir / pausar","Silenciar","min","Não","Fechar todas as transmissões?","Origem","Captura (recodificação)","HLS (sem recodificação)","O HLS baixa os segmentos da transmissão como estão, sem recodificar, e salva em MP4. Precisa do endereço da transmissão; se não estiver disponível, a captura é usada. A qualidade é a original, igual à da transmissão de origem. A gravação é escrita no disco em pequenos pontos de verificação.","Recuperar gravações não salvas"],"IT":["Player","Aggiungi stream","Griglia","Riduci","Chiudi","Aggiungi stream per guardarne più di uno insieme. Tocca un riquadro per portarci l’audio.","Aggiungi","Fatto","Indietro","Preferiti in diretta","Oppure tramite link","Stream attuale","Cerca tra i preferiti in diretta","Nessun preferito in diretta","Caricamento…","Stream non disponibile","tocca per riprovare","La finestra è piena","Auto","Qualità","Buffer di riavvolgimento","Picture-in-picture","Schermo intero","Ingrandisci","Riproduci / pausa","Disattiva audio","min","No","Chiudere tutti gli stream?","Sorgente","Acquisizione (ricodifica)","HLS (senza ricodifica)","HLS scarica i segmenti dello stream così come sono, senza ricodifica, e salva in MP4. Serve l’indirizzo dello stream; se non è disponibile, viene usata l’acquisizione. La qualità è quella originale dello stream di origine. La registrazione viene scritta su disco a piccoli passi.","Recupera le registrazioni non salvate"],"PL":["Odtwarzacz","Dodaj transmisję","Układ","Minimalizuj","Zamknij","Dodaj transmisje, aby oglądać kilka naraz. Dotknij kafelka, aby przenieść na niego dźwięk.","Dodaj","Gotowe","Wstecz","Ulubione na żywo","Lub przez link","Bieżąca transmisja","Szukaj w ulubionych na żywo","Brak ulubionych na żywo","Ładowanie…","Transmisja niedostępna","dotknij, aby spróbować ponownie","Okno jest pełne","Auto","Jakość","Bufor przewijania","Obraz w obrazie","Pełny ekran","Powiększ","Odtwarzaj / pauza","Wycisz","min","Wył.","Zamknąć wszystkie transmisje?","Źródło","Przechwytywanie (ponowne kodowanie)","HLS (bez ponownego kodowania)","HLS pobiera segmenty strumienia bez zmian, bez ponownego kodowania, i zapisuje MP4. Potrzebny jest adres strumienia; jeśli go brak, używane jest przechwytywanie. Jakość jest oryginalna, taka jak w strumieniu źródłowym. Nagranie jest zapisywane na dysku małymi porcjami.","Odzyskaj niezapisane nagrania"],"TR":["Oynatıcı","Yayın ekle","Düzen","Küçült","Kapat","Aynı anda birkaç yayın izlemek için yayın ekleyin. Sesi bir kutucuğa taşımak için ona dokunun.","Ekle","Bitti","Geri","Canlı yer imleri","Veya bağlantıyla","Mevcut yayın","Canlı yer imlerinde ara","Canlı yer imi yok","Yükleniyor…","Yayın kullanılamıyor","tekrar denemek için dokunun","Pencere dolu","Otomatik","Kalite","Geri sarma tamponu","Resim içinde resim","Tam ekran","Büyüt","Oynat / duraklat","Sessize al","dk","Kapalı","Tüm yayınlar kapatılsın mı?","Kaynak","Yakalama (yeniden kodlama)","HLS (yeniden kodlamasız)","HLS, yayın parçalarını olduğu gibi, yeniden kodlamadan indirir ve MP4 olarak kaydeder. Yayın adresi gerekir; yoksa yakalama kullanılır. Kalite, kaynak yayınla aynı, yani orijinaldir. Kayıt diske küçük kontrol noktalarıyla yazılır.","Kaydedilmemiş kayıtları kurtar"],"NL":["Speler","Stream toevoegen","Indeling","Minimaliseren","Sluiten","Voeg streams toe om er meerdere tegelijk te bekijken. Tik op een tegel om het geluid ernaartoe te verplaatsen.","Toevoegen","Klaar","Terug","Live bladwijzers","Of via link","Huidige stream","Live bladwijzers zoeken","Geen live bladwijzers","Laden…","Stream niet beschikbaar","tik om opnieuw te proberen","Venster is vol","Auto","Kwaliteit","Terugspoelbuffer","Beeld-in-beeld","Volledig scherm","Vergroten","Afspelen / pauzeren","Dempen","min","Uit","Alle streams sluiten?","Bron","Opname (hercodering)","HLS (zonder hercodering)","HLS downloadt de streamsegmenten ongewijzigd, zonder hercodering, en slaat MP4 op. Het streamadres is nodig; is dat er niet, dan wordt opname gebruikt. De kwaliteit is origineel, zoals in de brondstream. De opname wordt in kleine tussenstappen naar schijf geschreven.","Niet-opgeslagen opnames herstellen"],"CS":["Přehrávač","Přidat stream","Rozložení","Minimalizovat","Zavřít","Přidejte streamy, abyste mohli sledovat několik najednou. Klepnutím na dlaždici na ni přesunete zvuk.","Přidat","Hotovo","Zpět","Živé záložky","Nebo odkazem","Aktuální stream","Hledat v živých záložkách","Žádné živé záložky","Načítání…","Stream není dostupný","klepnutím to zkusíte znovu","Okno je plné","Auto","Kvalita","Vyrovnávací paměť pro přetáčení","Obraz v obraze","Celá obrazovka","Zvětšit","Přehrát / pozastavit","Ztlumit","min","Vyp.","Zavřít všechny streamy?","Zdroj","Zachytávání (překódování)","HLS (bez překódování)","HLS stahuje segmenty streamu tak, jak jsou, bez překódování, a ukládá MP4. Potřebuje adresu streamu; pokud není k dispozici, použije se zachytávání. Kvalita je původní, stejná jako ve zdrojovém streamu. Nahrávka se na disk zapisuje po malých částech.","Obnovit neuložené nahrávky"],"RO":["Player","Adaugă flux","Aspect","Minimizează","Închide","Adaugă fluxuri ca să urmărești mai multe deodată. Atinge o casetă ca să muți sunetul pe ea.","Adaugă","Gata","Înapoi","Favorite în direct","Sau prin link","Fluxul curent","Caută în favoritele în direct","Nu există favorite în direct","Se încarcă…","Flux indisponibil","atinge pentru a reîncerca","Fereastra este plină","Auto","Calitate","Buffer de derulare înapoi","Imagine în imagine","Ecran complet","Mărește","Redare / pauză","Fără sunet","min","Oprit","Închizi toate fluxurile?","Sursă","Captură (recodare)","HLS (fără recodare)","HLS descarcă segmentele fluxului ca atare, fără recodare, și salvează MP4. Are nevoie de adresa fluxului; dacă nu este disponibilă, se folosește captura. Calitatea este cea originală, ca în fluxul sursă. Înregistrarea se scrie pe disc în mici puncte de control.","Recuperează înregistrările nesalvate"],"HU":["Lejátszó","Adás hozzáadása","Elrendezés","Kicsinyítés","Bezárás","Adj hozzá adásokat, hogy többet nézhess egyszerre. Érints meg egy csempét, hogy átvidd rá a hangot.","Hozzáadás","Kész","Vissza","Élő könyvjelzők","Vagy hivatkozással","Jelenlegi adás","Keresés az élő könyvjelzők között","Nincs élő könyvjelző","Betöltés…","Az adás nem érhető el","érintsd meg az újrapróbáláshoz","Az ablak megtelt","Auto","Minőség","Visszatekerési puffer","Kép a képben","Teljes képernyő","Nagyítás","Lejátszás / szünet","Némítás","perc","Ki","Bezárod az összes adást?","Forrás","Rögzítés (újrakódolás)","HLS (újrakódolás nélkül)","A HLS az adás szegmenseit úgy tölti le, ahogy vannak, újrakódolás nélkül, és MP4-be menti. Szükség van az adás címére; ha nem érhető el, a rögzítés lesz használva. A minőség eredeti, megegyezik a forrásadáséval. A felvétel kis ellenőrzőpontokban íródik lemezre.","Nem mentett felvételek helyreállítása"],"BG":["Плеър","Добави поток","Подредба","Свиване","Затвори","Добавете потоци, за да гледате няколко наведнъж. Докоснете плочка, за да прехвърлите звука към нея.","Добави","Готово","Назад","Любими на живо","Или чрез линк","Текущ поток","Търсене в любимите на живо","Няма любими на живо","Зареждане…","Потокът не е наличен","докоснете за повторен опит","Прозорецът е пълен","Авто","Качество","Буфер за превъртане","Картина в картина","Цял екран","Увеличи","Пускане / пауза","Без звук","мин","Изкл.","Да се затворят ли всички потоци?","Източник","Заснемане (прекодиране)","HLS (без прекодиране)","HLS изтегля сегментите на потока такива, каквито са, без прекодиране, и записва MP4. Нужен е адресът на потока; ако няма, се използва заснемане. Качеството е оригинално, като на изходния поток. Записът се пише на диска на малки части.","Възстанови незапазените записи"],"EL":["Player","Προσθήκη ροής","Διάταξη","Ελαχιστοποίηση","Κλείσιμο","Προσθέστε ροές για να βλέπετε πολλές ταυτόχρονα. Πατήστε ένα πλακίδιο για να μεταφέρετε τον ήχο σε αυτό.","Προσθήκη","Τέλος","Πίσω","Αγαπημένα σε ζωντανή μετάδοση","Ή με σύνδεσμο","Τρέχουσα ροή","Αναζήτηση στα ζωντανά αγαπημένα","Δεν υπάρχουν ζωντανά αγαπημένα","Φόρτωση…","Η ροή δεν είναι διαθέσιμη","πατήστε για επανάληψη","Το παράθυρο είναι γεμάτο","Αυτόματο","Ποιότητα","Προσωρινή μνήμη επαναφοράς","Εικόνα σε εικόνα","Πλήρης οθόνη","Μεγέθυνση","Αναπαραγωγή / παύση","Σίγαση","λεπ.","Off","Κλείσιμο όλων των ροών;","Πηγή","Καταγραφή (επανακωδικοποίηση)","HLS (χωρίς επανακωδικοποίηση)","Το HLS κατεβάζει τα τμήματα της ροής ως έχουν, χωρίς επανακωδικοποίηση, και αποθηκεύει MP4. Χρειάζεται η διεύθυνση της ροής· αν δεν είναι διαθέσιμη, χρησιμοποιείται η καταγραφή. Η ποιότητα είναι η αρχική, όπως στην πηγαία ροή. Η εγγραφή γράφεται στον δίσκο σε μικρά σημεία ελέγχου.","Ανάκτηση μη αποθηκευμένων εγγραφών"],"JA":["プレーヤー","配信を追加","レイアウト","最小化","閉じる","配信を追加すると複数を同時に視聴できます。タイルをタップすると、その配信に音声が切り替わります。","追加","完了","戻る","ライブのブックマーク","またはリンクで","現在の配信","ライブのブックマークを検索","ライブのブックマークはありません","読み込み中…","配信は利用できません","タップして再試行","ウィンドウがいっぱいです","自動","画質","巻き戻しバッファ","ピクチャーインピクチャー","全画面","拡大","再生 / 一時停止","ミュート","分","オフ","すべての配信を閉じますか?","ソース","キャプチャ(再エンコード)","HLS(再エンコードなし)","HLSは、配信のセグメントを再エンコードせずそのままダウンロードし、MP4として保存します。配信のアドレスが必要で、利用できない場合はキャプチャが使われます。画質は元の配信と同じ、オリジナルのままです。録画は小さなチェックポイントごとにディスクへ書き込まれます。","未保存の録画を復元"],"ZH":["播放器","添加直播流","布局","最小化","关闭","添加直播流即可同时观看多个。点按某个画面,声音会切换到它。","添加","完成","返回","在线收藏","或通过链接","当前直播流","搜索在线收藏","没有在线收藏","加载中…","直播流不可用","点按重试","窗口已满","自动","画质","回退缓冲","画中画","全屏","放大","播放 / 暂停","静音","分钟","关闭","要关闭所有直播流吗?","来源","捕获(重新编码)","HLS(不重新编码)","HLS 会原样下载直播流分段,不重新编码,并保存为 MP4。需要直播流地址;如果没有,则使用捕获。画质与源直播流相同,保持原始画质。录制内容会以小检查点写入磁盘。","恢复未保存的录制"],"KO":["플레이어","스트림 추가","레이아웃","최소화","닫기","스트림을 추가해 여러 개를 동시에 시청하세요. 타일을 탭하면 소리가 그쪽으로 옮겨집니다.","추가","완료","뒤로","라이브 즐겨찾기","또는 링크로","현재 스트림","라이브 즐겨찾기 검색","라이브 즐겨찾기 없음","불러오는 중…","스트림을 사용할 수 없음","탭하여 다시 시도","창이 가득 찼습니다","자동","화질","되감기 버퍼","화면 속 화면","전체 화면","확대","재생 / 일시정지","음소거","분","끔","모든 스트림을 닫을까요?","소스","캡처(재인코딩)","HLS(재인코딩 없음)","HLS는 스트림 세그먼트를 재인코딩 없이 그대로 다운로드하여 MP4로 저장합니다. 스트림 주소가 필요하며, 없으면 캡처가 사용됩니다. 화질은 원본 스트림과 동일한 원본 화질입니다. 녹화는 작은 체크포인트 단위로 디스크에 기록됩니다.","저장되지 않은 녹화 복구"],"HE":["נגן","הוסף סטרים","פריסה","מזעור","סגור","הוסף סטרימים כדי לצפות בכמה בו־זמנית. הקש על אריח כדי להעביר אליו את הקול.","הוסף","סיום","חזרה","מועדפים בשידור חי","או באמצעות קישור","הסטרים הנוכחי","חיפוש במועדפים בשידור חי","אין מועדפים בשידור חי","טוען…","הסטרים אינו זמין","הקש לניסיון חוזר","החלון מלא","אוטומטי","איכות","מאגר הרצה לאחור","תמונה בתוך תמונה","מסך מלא","הגדל","הפעל / השהה","השתק","דק׳","כבוי","לסגור את כל הסטרימים?","מקור","לכידה (קידוד מחדש)","HLS (בלי קידוד מחדש)","HLS מוריד את קטעי הסטרים כפי שהם, בלי קידוד מחדש, ושומר MP4. נדרשת כתובת הסטרים; אם היא אינה זמינה, משתמשים בלכידה. האיכות מקורית, כמו בסטרים המקורי. ההקלטה נכתבת לדיסק בנקודות ביניים קטנות.","שחזור הקלטות שלא נשמרו"],"AR":["المشغّل","إضافة بث","التخطيط","تصغير","إغلاق","أضف بثوثًا لمشاهدة عدة بثوث معًا. اضغط على بلاطة لنقل الصوت إليها.","إضافة","تم","رجوع","المفضلة المباشرة","أو عبر رابط","البث الحالي","البحث في المفضلة المباشرة","لا توجد مفضلة مباشرة","جارٍ التحميل…","البث غير متاح","اضغط لإعادة المحاولة","النافذة ممتلئة","تلقائي","الجودة","مخزن الإرجاع","صورة داخل صورة","ملء الشاشة","تكبير","تشغيل / إيقاف مؤقت","كتم الصوت","د","إيقاف","إغلاق كل البثوث؟","المصدر","الالتقاط (إعادة ترميز)","HLS (بدون إعادة ترميز)","يقوم HLS بتنزيل مقاطع البث كما هي دون إعادة ترميز ويحفظ MP4. يحتاج إلى عنوان البث؛ وإن لم يتوفر يُستخدم الالتقاط. الجودة أصلية كما في البث المصدر. يُكتب التسجيل على القرص على نقاط حفظ صغيرة.","استعادة التسجيلات غير المحفوظة"],"HI":["प्लेयर","स्ट्रीम जोड़ें","लेआउट","छोटा करें","बंद करें","एक साथ कई स्ट्रीम देखने के लिए स्ट्रीम जोड़ें। आवाज़ किसी टाइल पर ले जाने के लिए उस पर टैप करें।","जोड़ें","हो गया","वापस","लाइव बुकमार्क","या लिंक से","मौजूदा स्ट्रीम","लाइव बुकमार्क खोजें","कोई लाइव बुकमार्क नहीं","लोड हो रहा है…","स्ट्रीम उपलब्ध नहीं","दोबारा कोशिश के लिए टैप करें","विंडो भरी हुई है","ऑटो","क्वालिटी","रिवाइंड बफ़र","पिक्चर-इन-पिक्चर","फुलस्क्रीन","बड़ा करें","चलाएँ / रोकें","म्यूट","मिनट","बंद","सभी स्ट्रीम बंद करें?","स्रोत","कैप्चर (री-एन्कोड)","HLS (बिना री-एन्कोड)","HLS स्ट्रीम के सेगमेंट जैसे हैं वैसे ही, बिना री-एन्कोड किए, डाउनलोड करता है और MP4 सेव करता है। इसके लिए स्ट्रीम का पता चाहिए; न हो तो कैप्चर इस्तेमाल होता है। क्वालिटी मूल रहती है, स्रोत स्ट्रीम जैसी। रिकॉर्डिंग छोटे चेकपॉइंट में डिस्क पर लिखी जाती है।","सेव न हुई रिकॉर्डिंग वापस पाएँ"],"ID":["Pemutar","Tambah stream","Tata letak","Perkecil","Tutup","Tambahkan stream untuk menonton beberapa sekaligus. Ketuk sebuah kotak untuk memindahkan suara ke sana.","Tambah","Selesai","Kembali","Bookmark live","Atau lewat tautan","Stream saat ini","Cari bookmark live","Tidak ada bookmark live","Memuat…","Stream tidak tersedia","ketuk untuk mencoba lagi","Jendela penuh","Otomatis","Kualitas","Buffer putar ulang","Gambar dalam gambar","Layar penuh","Perbesar","Putar / jeda","Bisukan","mnt","Mati","Tutup semua stream?","Sumber","Tangkap (encode ulang)","HLS (tanpa encode ulang)","HLS mengunduh segmen stream apa adanya, tanpa encode ulang, dan menyimpan MP4. Memerlukan alamat stream; jika tidak tersedia, digunakan Tangkap. Kualitasnya asli, sama seperti stream sumber. Rekaman ditulis ke disk dalam titik pemeriksaan kecil.","Pulihkan rekaman yang belum disimpan"],"VI":["Trình phát","Thêm luồng","Bố cục","Thu nhỏ","Đóng","Thêm luồng để xem nhiều luồng cùng lúc. Chạm vào một ô để chuyển âm thanh sang ô đó.","Thêm","Xong","Quay lại","Đánh dấu đang live","Hoặc bằng liên kết","Luồng hiện tại","Tìm trong đánh dấu đang live","Không có đánh dấu đang live","Đang tải…","Luồng không khả dụng","chạm để thử lại","Cửa sổ đã đầy","Tự động","Chất lượng","Bộ đệm tua lại","Hình trong hình","Toàn màn hình","Phóng to","Phát / tạm dừng","Tắt tiếng","phút","Tắt","Đóng tất cả luồng?","Nguồn","Ghi hình (mã hóa lại)","HLS (không mã hóa lại)","HLS tải các phân đoạn của luồng nguyên trạng, không mã hóa lại, và lưu thành MP4. Cần địa chỉ luồng; nếu không có, sẽ dùng ghi hình. Chất lượng là gốc, giống luồng nguồn. Bản ghi được ghi vào đĩa theo từng điểm kiểm tra nhỏ.","Khôi phục bản ghi chưa lưu"],"TH":["เครื่องเล่น","เพิ่มสตรีม","เลย์เอาต์","ย่อ","ปิด","เพิ่มสตรีมเพื่อดูหลายสตรีมพร้อมกัน แตะที่ช่องเพื่อย้ายเสียงไปที่ช่องนั้น","เพิ่ม","เสร็จ","กลับ","บุ๊กมาร์กที่กำลังไลฟ์","หรือผ่านลิงก์","สตรีมปัจจุบัน","ค้นหาบุ๊กมาร์กที่กำลังไลฟ์","ไม่มีบุ๊กมาร์กที่กำลังไลฟ์","กำลังโหลด…","สตรีมไม่พร้อมใช้งาน","แตะเพื่อลองใหม่","หน้าต่างเต็มแล้ว","อัตโนมัติ","คุณภาพ","บัฟเฟอร์ย้อนกลับ","ภาพซ้อนภาพ","เต็มจอ","ขยาย","เล่น / หยุดชั่วคราว","ปิดเสียง","นาที","ปิด","ปิดทุกสตรีมหรือไม่","แหล่งที่มา","แคปเจอร์ (เข้ารหัสใหม่)","HLS (ไม่เข้ารหัสใหม่)","HLS จะดาวน์โหลดส่วนของสตรีมตามที่เป็น โดยไม่เข้ารหัสใหม่ และบันทึกเป็น MP4 ต้องมีที่อยู่ของสตรีม หากไม่มีจะใช้แคปเจอร์ คุณภาพเป็นต้นฉบับ เหมือนสตรีมต้นทาง การบันทึกจะเขียนลงดิสก์เป็นจุดตรวจเล็ก ๆ","กู้คืนการบันทึกที่ยังไม่ได้บันทึก"]}});
Object.keys(I18N_MV).forEach(k => { i18n[k] = Object.assign(i18n[k] || {}, I18N_MV[k]); });
const I18N_MV2 = {"RU": {"mv_abr": "Авто", "mv_abr_row": "Автокачество", "mv_abr_t": "Автокачество: снижает качество при зависаниях или слабой сети и повышает, когда соединение ровное", "mv_auto_all": "Добавлять все стримы сразу", "mv_bg": "Играть в фоне", "mv_bg_t": "Потоки продолжают играть при переключении вкладок, закрытии окна и выходе из браузера"}, "EN": {"mv_abr": "Auto", "mv_abr_row": "Auto quality", "mv_abr_t": "Auto quality: lowers the quality when the video stutters or the network gets weak, raises it when the connection is stable", "mv_auto_all": "Add all live streams automatically", "mv_bg": "Play in background", "mv_bg_t": "Streams keep playing when you switch tabs, close this window or leave the browser"}, "UK": {"mv_abr": "Авто", "mv_abr_row": "Автоякість", "mv_abr_t": "Автоякість: знижує якість при підвисаннях або слабкій мережі та підвищує, коли з’єднання стабільне", "mv_auto_all": "Додавати всі стріми одразу", "mv_bg": "Грати у фоні", "mv_bg_t": "Потоки продовжують грати, коли ви перемикаєте вкладки, закриваєте вікно або виходите з браузера"}, "ES": {"mv_abr": "Auto", "mv_abr_row": "Calidad automática", "mv_abr_t": "Calidad automática: baja la calidad cuando el vídeo se corta o la red es débil y la sube cuando la conexión es estable", "mv_auto_all": "Añadir todas las transmisiones en directo automáticamente", "mv_bg": "Reproducir en segundo plano", "mv_bg_t": "Las transmisiones siguen reproduciéndose al cambiar de pestaña, cerrar esta ventana o salir del navegador"}, "DE": {"mv_abr": "Auto", "mv_abr_row": "Automatische Qualität", "mv_abr_t": "Automatische Qualität: senkt die Qualität bei Rucklern oder schwachem Netz und erhöht sie, wenn die Verbindung stabil ist", "mv_auto_all": "Alle Live-Streams automatisch hinzufügen", "mv_bg": "Im Hintergrund abspielen", "mv_bg_t": "Streams laufen weiter, wenn du den Tab wechselst, dieses Fenster schließt oder den Browser verlässt"}, "FR": {"mv_abr": "Auto", "mv_abr_row": "Qualité auto", "mv_abr_t": "Qualité auto : réduit la qualité en cas de saccades ou de réseau faible et l’augmente quand la connexion est stable", "mv_auto_all": "Ajouter automatiquement tous les flux en direct", "mv_bg": "Lecture en arrière-plan", "mv_bg_t": "Les flux continuent de jouer quand vous changez d’onglet, fermez cette fenêtre ou quittez le navigateur"}, "PT": {"mv_abr": "Auto", "mv_abr_row": "Qualidade automática", "mv_abr_t": "Qualidade automática: reduz a qualidade quando o vídeo trava ou a rede está fraca e aumenta quando a conexão está estável", "mv_auto_all": "Adicionar todas as transmissões ao vivo automaticamente", "mv_bg": "Reproduzir em segundo plano", "mv_bg_t": "As transmissões continuam tocando ao trocar de aba, fechar esta janela ou sair do navegador"}, "IT": {"mv_abr": "Auto", "mv_abr_row": "Qualità automatica", "mv_abr_t": "Qualità automatica: riduce la qualità in caso di scatti o rete debole e la aumenta quando la connessione è stabile", "mv_auto_all": "Aggiungi automaticamente tutti gli stream in diretta", "mv_bg": "Riproduci in background", "mv_bg_t": "Gli stream continuano a riprodursi quando cambi scheda, chiudi questa finestra o esci dal browser"}, "PL": {"mv_abr": "Auto", "mv_abr_row": "Jakość automatyczna", "mv_abr_t": "Jakość automatyczna: obniża jakość przy zacięciach lub słabej sieci i podnosi, gdy połączenie jest stabilne", "mv_auto_all": "Automatycznie dodawaj wszystkie transmisje na żywo", "mv_bg": "Odtwarzaj w tle", "mv_bg_t": "Transmisje grają dalej po przełączeniu karty, zamknięciu tego okna lub wyjściu z przeglądarki"}, "TR": {"mv_abr": "Otomatik", "mv_abr_row": "Otomatik kalite", "mv_abr_t": "Otomatik kalite: video takıldığında veya ağ zayıfladığında kaliteyi düşürür, bağlantı kararlı olduğunda yükseltir", "mv_auto_all": "Tüm canlı yayınları otomatik ekle", "mv_bg": "Arka planda oynat", "mv_bg_t": "Sekme değiştirdiğinizde, bu pencereyi kapattığınızda veya tarayıcıdan çıktığınızda yayınlar oynamaya devam eder"}, "NL": {"mv_abr": "Auto", "mv_abr_row": "Automatische kwaliteit", "mv_abr_t": "Automatische kwaliteit: verlaagt de kwaliteit bij haperingen of een zwak netwerk en verhoogt hem als de verbinding stabiel is", "mv_auto_all": "Alle live streams automatisch toevoegen", "mv_bg": "Afspelen op de achtergrond", "mv_bg_t": "Streams blijven spelen als je van tabblad wisselt, dit venster sluit of de browser verlaat"}, "CS": {"mv_abr": "Auto", "mv_abr_row": "Automatická kvalita", "mv_abr_t": "Automatická kvalita: snižuje kvalitu při sekání nebo slabé síti a zvyšuje ji, když je připojení stabilní", "mv_auto_all": "Automaticky přidat všechny živé streamy", "mv_bg": "Přehrávat na pozadí", "mv_bg_t": "Streamy se přehrávají dál, když přepnete kartu, zavřete toto okno nebo opustíte prohlížeč"}, "RO": {"mv_abr": "Auto", "mv_abr_row": "Calitate automată", "mv_abr_t": "Calitate automată: scade calitatea când video se blochează sau rețeaua e slabă și o crește când conexiunea e stabilă", "mv_auto_all": "Adaugă automat toate fluxurile live", "mv_bg": "Redare în fundal", "mv_bg_t": "Fluxurile continuă să ruleze când schimbi fila, închizi această fereastră sau ieși din browser"}, "HU": {"mv_abr": "Auto", "mv_abr_row": "Automatikus minőség", "mv_abr_t": "Automatikus minőség: csökkenti a minőséget akadozásnál vagy gyenge hálózatnál, és növeli, ha a kapcsolat stabil", "mv_auto_all": "Az összes élő adás automatikus hozzáadása", "mv_bg": "Lejátszás a háttérben", "mv_bg_t": "Az adások tovább szólnak, ha lapot váltasz, bezárod ezt az ablakot vagy kilépsz a böngészőből"}, "BG": {"mv_abr": "Авто", "mv_abr_row": "Автоматично качество", "mv_abr_t": "Автоматично качество: намалява качеството при засичане или слаба мрежа и го повишава, когато връзката е стабилна", "mv_auto_all": "Добавяй всички потоци на живо автоматично", "mv_bg": "Възпроизвеждане във фонов режим", "mv_bg_t": "Потоците продължават да се възпроизвеждат, когато смените раздела, затворите този прозорец или излезете от браузъра"}, "EL": {"mv_abr": "Αυτόματο", "mv_abr_row": "Αυτόματη ποιότητα", "mv_abr_t": "Αυτόματη ποιότητα: μειώνει την ποιότητα όταν το βίντεο κολλάει ή το δίκτυο είναι αδύναμο και την αυξάνει όταν η σύνδεση είναι σταθερή", "mv_auto_all": "Αυτόματη προσθήκη όλων των ζωντανών ροών", "mv_bg": "Αναπαραγωγή στο παρασκήνιο", "mv_bg_t": "Οι ροές συνεχίζουν να παίζουν όταν αλλάζετε καρτέλα, κλείνετε αυτό το παράθυρο ή φεύγετε από το πρόγραμμα περιήγησης"}, "JA": {"mv_abr": "自動", "mv_abr_row": "自動画質", "mv_abr_t": "自動画質:映像がカクついたりネットワークが弱いときは画質を下げ、接続が安定したら上げます", "mv_auto_all": "ライブ配信をすべて自動で追加", "mv_bg": "バックグラウンドで再生", "mv_bg_t": "タブを切り替えたり、このウィンドウを閉じたり、ブラウザを離れても配信の再生が続きます"}, "ZH": {"mv_abr": "自动", "mv_abr_row": "自动画质", "mv_abr_t": "自动画质:画面卡顿或网络较弱时降低画质,连接稳定时提高画质", "mv_auto_all": "自动添加所有在线直播流", "mv_bg": "后台播放", "mv_bg_t": "切换标签页、关闭此窗口或离开浏览器时,直播流会继续播放"}, "KO": {"mv_abr": "자동", "mv_abr_row": "자동 화질", "mv_abr_t": "자동 화질: 영상이 끊기거나 네트워크가 약하면 화질을 낮추고, 연결이 안정되면 높입니다", "mv_auto_all": "모든 라이브 스트림 자동 추가", "mv_bg": "백그라운드 재생", "mv_bg_t": "탭을 전환하거나 이 창을 닫거나 브라우저를 나가도 스트림이 계속 재생됩니다"}, "HE": {"mv_abr": "אוטומטי", "mv_abr_row": "איכות אוטומטית", "mv_abr_t": "איכות אוטומטית: מורידה את האיכות כשהווידאו נתקע או כשהרשת חלשה, ומעלה אותה כשהחיבור יציב", "mv_auto_all": "הוסף אוטומטית את כל הסטרימים החיים", "mv_bg": "ניגון ברקע", "mv_bg_t": "הסטרימים ממשיכים להתנגן כשעוברים בין לשוניות, סוגרים את החלון הזה או יוצאים מהדפדפן"}, "AR": {"mv_abr": "تلقائي", "mv_abr_row": "جودة تلقائية", "mv_abr_t": "جودة تلقائية: تخفّض الجودة عند التقطيع أو ضعف الشبكة وترفعها عندما يكون الاتصال مستقرًا", "mv_auto_all": "إضافة كل البثوث المباشرة تلقائيًا", "mv_bg": "التشغيل في الخلفية", "mv_bg_t": "تستمر البثوث في التشغيل عند تبديل علامة التبويب أو إغلاق هذه النافذة أو مغادرة المتصفح"}, "HI": {"mv_abr": "ऑटो", "mv_abr_row": "ऑटो क्वालिटी", "mv_abr_t": "ऑटो क्वालिटी: वीडियो अटकने या नेटवर्क कमज़ोर होने पर क्वालिटी घटाती है और कनेक्शन स्थिर होने पर बढ़ाती है", "mv_auto_all": "सभी लाइव स्ट्रीम अपने-आप जोड़ें", "mv_bg": "बैकग्राउंड में चलाएँ", "mv_bg_t": "टैब बदलने, यह विंडो बंद करने या ब्राउज़र छोड़ने पर भी स्ट्रीम चलती रहती हैं"}, "ID": {"mv_abr": "Otomatis", "mv_abr_row": "Kualitas otomatis", "mv_abr_t": "Kualitas otomatis: menurunkan kualitas saat video tersendat atau jaringan lemah, dan menaikkannya saat koneksi stabil", "mv_auto_all": "Tambahkan semua stream live secara otomatis", "mv_bg": "Putar di latar belakang", "mv_bg_t": "Stream tetap diputar saat Anda berpindah tab, menutup jendela ini, atau keluar dari browser"}, "VI": {"mv_abr": "Tự động", "mv_abr_row": "Chất lượng tự động", "mv_abr_t": "Chất lượng tự động: hạ chất lượng khi video giật hoặc mạng yếu và tăng lên khi kết nối ổn định", "mv_auto_all": "Tự động thêm tất cả luồng đang live", "mv_bg": "Phát trong nền", "mv_bg_t": "Luồng vẫn tiếp tục phát khi bạn chuyển tab, đóng cửa sổ này hoặc rời trình duyệt"}, "TH": {"mv_abr": "อัตโนมัติ", "mv_abr_row": "คุณภาพอัตโนมัติ", "mv_abr_t": "คุณภาพอัตโนมัติ: ลดคุณภาพเมื่อวิดีโอกระตุกหรือเครือข่ายอ่อน และเพิ่มขึ้นเมื่อการเชื่อมต่อเสถียร", "mv_auto_all": "เพิ่มสตรีมที่กำลังไลฟ์ทั้งหมดอัตโนมัติ", "mv_bg": "เล่นในพื้นหลัง", "mv_bg_t": "สตรีมจะเล่นต่อเมื่อสลับแท็บ ปิดหน้าต่างนี้ หรือออกจากเบราว์เซอร์"}};
Object.keys(I18N_MV2).forEach(k => { i18n[k] = Object.assign(i18n[k] || {}, I18N_MV2[k]); });
Object.assign(i18n.RU, {
    rs_resnote: 'Захват пишет в разрешении потока: настройки выше меняют только битрейт (сжатие), а 1080p–360p показывают ожидаемое качество картинки. В режиме HLS выбирается поток не выше указанной высоты.',
    rs_hint: 'Когда часть достигает выбранного размера, она сохраняется отдельным файлом, а запись продолжается в новой. Данные пишутся на диск небольшими порциями; если диск недоступен, они остаются в памяти телефона, и тогда лучше выбрать меньшие части.',
    rs_next: 'Изменения вступают в силу со следующей части или новой записи.'
});
Object.assign(i18n.EN, {
    rs_resnote: 'Capture records at the stream resolution: the options above only change the bitrate (compression), and 1080p–360p show the expected picture quality. In HLS mode, a stream no taller than the chosen height is picked.',
    rs_hint: 'When a part reaches the chosen size, it is saved as a separate file and recording continues in a new one. Data is written to disk in small chunks; if the disk is unavailable it stays in phone memory, so choose smaller parts.',
    rs_next: 'Changes apply from the next part or a new recording.'
});
Object.assign(i18n.UK, {
    rs_resnote: 'Захоплення пише в роздільній здатності потоку: налаштування вище змінюють лише бітрейт (стиснення), а 1080p–360p показують очікувану якість картинки. У режимі HLS обирається потік не вищий за вказану висоту.',
    rs_hint: 'Коли частина досягає вибраного розміру, вона зберігається окремим файлом, а запис триває в новій. Дані пишуться на диск невеликими порціями; якщо диск недоступний, вони лишаються в памʼяті телефона, і тоді краще обрати менші частини.',
    rs_next: 'Зміни діють з наступної частини або нового запису.'
});
Object.entries({
    "ES": {
        "rs_resnote": "La captura graba con la resolución de la transmisión: las opciones de arriba solo cambian el bitrate (compresión), y 1080p–360p indican la calidad de imagen esperada. En el modo HLS se elige una transmisión que no supere la altura indicada.",
        "rs_hint": "Cuando una parte alcanza el tamaño elegido, se guarda como archivo aparte y la grabación continúa en uno nuevo. Los datos se escriben en el disco en pequeños bloques; si el disco no está disponible, permanecen en la memoria del teléfono, así que conviene elegir partes más pequeñas.",
        "rs_next": "Los cambios se aplican desde la siguiente parte o una grabación nueva."
    },
    "DE": {
        "rs_resnote": "Der Capture-Modus nimmt in der Stream-Auflösung auf: Die Optionen oben ändern nur die Bitrate (Kompression), und 1080p–360p zeigen die erwartete Bildqualität. Im HLS-Modus wird ein Stream gewählt, der nicht höher als die angegebene Höhe ist.",
        "rs_hint": "Erreicht ein Teil die gewählte Größe, wird er als eigene Datei gespeichert und die Aufnahme läuft in einem neuen weiter. Die Daten werden in kleinen Blöcken auf den Datenträger geschrieben; ist er nicht verfügbar, bleiben sie im Arbeitsspeicher des Handys – wähle dann kleinere Teile.",
        "rs_next": "Änderungen gelten ab dem nächsten Teil oder einer neuen Aufnahme."
    },
    "FR": {
        "rs_resnote": "La capture enregistre à la résolution du flux : les options ci-dessus ne modifient que le débit (compression), et 1080p–360p indiquent la qualité d’image attendue. En mode HLS, un flux dont la hauteur ne dépasse pas celle choisie est sélectionné.",
        "rs_hint": "Quand une partie atteint la taille choisie, elle est enregistrée dans un fichier distinct et l’enregistrement continue dans un nouveau. Les données sont écrites sur le disque par petits blocs ; si le disque est indisponible, elles restent dans la mémoire du téléphone, mieux vaut alors choisir des parties plus petites.",
        "rs_next": "Les modifications s’appliquent à partir de la partie suivante ou d’un nouvel enregistrement."
    },
    "PT": {
        "rs_resnote": "A captura grava na resolução da transmissão: as opções acima alteram apenas o bitrate (compressão), e 1080p–360p indicam a qualidade de imagem esperada. No modo HLS, é escolhida uma transmissão com altura não superior à definida.",
        "rs_hint": "Quando uma parte atinge o tamanho escolhido, ela é salva como um arquivo separado e a gravação continua em um novo. Os dados são gravados no disco em pequenos blocos; se o disco não estiver disponível, ficam na memória do celular, e então é melhor escolher partes menores.",
        "rs_next": "As alterações valem a partir da próxima parte ou de uma nova gravação."
    },
    "IT": {
        "rs_resnote": "La cattura registra alla risoluzione dello stream: le opzioni sopra cambiano solo il bitrate (compressione), e 1080p–360p indicano la qualità d’immagine prevista. In modalità HLS viene scelto uno stream di altezza non superiore a quella indicata.",
        "rs_hint": "Quando una parte raggiunge la dimensione scelta, viene salvata come file separato e la registrazione continua in uno nuovo. I dati vengono scritti su disco in piccoli blocchi; se il disco non è disponibile, restano nella memoria del telefono, quindi conviene scegliere parti più piccole.",
        "rs_next": "Le modifiche valgono dalla parte successiva o da una nuova registrazione."
    },
    "PL": {
        "rs_resnote": "Przechwytywanie nagrywa w rozdzielczości strumienia: opcje powyżej zmieniają tylko bitrate (kompresję), a 1080p–360p pokazują oczekiwaną jakość obrazu. W trybie HLS wybierany jest strumień o wysokości nie większej niż wskazana.",
        "rs_hint": "Gdy część osiągnie wybrany rozmiar, jest zapisywana jako osobny plik, a nagrywanie trwa w nowym. Dane są zapisywane na dysk małymi porcjami; jeśli dysk jest niedostępny, zostają w pamięci telefonu, więc lepiej wybrać mniejsze części.",
        "rs_next": "Zmiany obowiązują od następnej części lub nowego nagrania."
    },
    "TR": {
        "rs_resnote": "Yakalama modu yayının çözünürlüğünde kaydeder: yukarıdaki seçenekler yalnızca bit hızını (sıkıştırmayı) değiştirir; 1080p–360p beklenen görüntü kalitesini gösterir. HLS modunda, seçilen yükseklikten büyük olmayan bir yayın seçilir.",
        "rs_hint": "Bir parça seçilen boyuta ulaştığında ayrı bir dosya olarak kaydedilir ve kayıt yeni bir parçada devam eder. Veriler diske küçük bloklar halinde yazılır; disk kullanılamazsa telefonun belleğinde kalır, bu durumda daha küçük parçalar seçmek iyi olur.",
        "rs_next": "Değişiklikler bir sonraki parçadan veya yeni kayıttan itibaren geçerli olur."
    },
    "NL": {
        "rs_resnote": "Capture neemt op in de resolutie van de stream: de opties hierboven wijzigen alleen de bitrate (compressie), en 1080p–360p geven de verwachte beeldkwaliteit aan. In HLS-modus wordt een stream gekozen die niet hoger is dan de opgegeven hoogte.",
        "rs_hint": "Wanneer een deel de gekozen grootte bereikt, wordt het als apart bestand opgeslagen en gaat de opname verder in een nieuw bestand. Gegevens worden in kleine blokken naar de schijf geschreven; is de schijf niet beschikbaar, dan blijven ze in het telefoongeheugen, kies dan kleinere delen.",
        "rs_next": "Wijzigingen gelden vanaf het volgende deel of een nieuwe opname."
    },
    "CS": {
        "rs_resnote": "Zachytávání nahrává v rozlišení streamu: možnosti výše mění jen bitrate (kompresi) a 1080p–360p ukazují očekávanou kvalitu obrazu. V režimu HLS se vybírá stream, který není vyšší než zadaná výška.",
        "rs_hint": "Když část dosáhne zvolené velikosti, uloží se jako samostatný soubor a nahrávání pokračuje v novém. Data se zapisují na disk po malých blocích; pokud disk není dostupný, zůstávají v paměti telefonu, a pak je lepší zvolit menší části.",
        "rs_next": "Změny platí od další části nebo nové nahrávky."
    },
    "RO": {
        "rs_resnote": "Captura înregistrează la rezoluția fluxului: opțiunile de mai sus schimbă doar bitrate-ul (compresia), iar 1080p–360p arată calitatea estimată a imaginii. În modul HLS se alege un flux cu înălțimea de cel mult cea indicată.",
        "rs_hint": "Când o parte atinge dimensiunea aleasă, este salvată ca fișier separat, iar înregistrarea continuă într-unul nou. Datele se scriu pe disc în blocuri mici; dacă discul nu este disponibil, rămân în memoria telefonului, așa că alege părți mai mici.",
        "rs_next": "Modificările se aplică de la următoarea parte sau de la o înregistrare nouă."
    },
    "HU": {
        "rs_resnote": "A Capture mód az adás felbontásában rögzít: a fenti beállítások csak a bitrátát (tömörítést) módosítják, az 1080p–360p pedig a várható képminőséget mutatja. HLS módban a megadott magasságot meg nem haladó adást választja a rendszer.",
        "rs_hint": "Amikor egy rész eléri a kiválasztott méretet, külön fájlként mentődik, a felvétel pedig egy újban folytatódik. Az adatok kis blokkokban íródnak a lemezre; ha a lemez nem érhető el, a telefon memóriájában maradnak, ilyenkor érdemes kisebb részeket választani.",
        "rs_next": "A módosítások a következő résztől vagy új felvételtől érvényesek."
    },
    "BG": {
        "rs_resnote": "Захватът записва в резолюцията на потока: опциите по-горе променят само битрейта (компресията), а 1080p–360p показват очакваното качество на картината. В режим HLS се избира поток с височина не по-голяма от зададената.",
        "rs_hint": "Когато една част достигне избрания размер, тя се запазва като отделен файл, а записът продължава в нов. Данните се пишат на диска на малки блокове; ако дискът не е достъпен, остават в паметта на телефона, затова изберете по-малки части.",
        "rs_next": "Промените важат от следващата част или от нов запис."
    },
    "EL": {
        "rs_resnote": "Η λήψη (Capture) καταγράφει στην ανάλυση του stream: οι επιλογές παραπάνω αλλάζουν μόνο το bitrate (συμπίεση), ενώ τα 1080p–360p δείχνουν την αναμενόμενη ποιότητα εικόνας. Στη λειτουργία HLS επιλέγεται stream με ύψος όχι μεγαλύτερο από το καθορισμένο.",
        "rs_hint": "Όταν ένα τμήμα φτάσει το επιλεγμένο μέγεθος, αποθηκεύεται ως ξεχωριστό αρχείο και η εγγραφή συνεχίζεται σε νέο. Τα δεδομένα γράφονται στον δίσκο σε μικρά κομμάτια· αν ο δίσκος δεν είναι διαθέσιμος, μένουν στη μνήμη του τηλεφώνου, οπότε επιλέξτε μικρότερα τμήματα.",
        "rs_next": "Οι αλλαγές ισχύουν από το επόμενο τμήμα ή τη νέα εγγραφή."
    },
    "JA": {
        "rs_resnote": "キャプチャはストリームの解像度で録画します。上の設定で変わるのはビットレート(圧縮)だけで、1080p〜360pは期待できる画質の目安です。HLSモードでは、指定した高さ以下のストリームが選ばれます。",
        "rs_hint": "パートが選択したサイズに達すると、別のファイルとして保存され、録画は新しいパートで続きます。データは小さな単位でディスクに書き込まれ、ディスクが使えない場合はスマホのメモリに残るため、その場合はパートを小さめにしてください。",
        "rs_next": "変更は次のパートまたは新しい録画から適用されます。"
    },
    "ZH": {
        "rs_resnote": "捕获模式按直播流的分辨率录制:上面的选项只改变码率(压缩),1080p–360p 表示预期的画质。HLS 模式下会选择高度不超过所选值的直播流。",
        "rs_hint": "当某一段达到所选大小时,会保存为单独的文件,录制继续写入新的一段。数据会分小块写入磁盘;如果磁盘不可用,则保留在手机内存中,此时建议选择更小的分段。",
        "rs_next": "更改将从下一段或新的录制开始生效。"
    },
    "KO": {
        "rs_resnote": "캡처 모드는 스트림 해상도로 녹화합니다. 위 옵션은 비트레이트(압축)만 바꾸며, 1080p–360p는 예상 화질을 나타냅니다. HLS 모드에서는 지정한 높이를 넘지 않는 스트림이 선택됩니다.",
        "rs_hint": "파트가 선택한 크기에 도달하면 별도 파일로 저장되고 녹화는 새 파트에서 이어집니다. 데이터는 작은 단위로 디스크에 기록되며, 디스크를 사용할 수 없으면 휴대폰 메모리에 남으므로 이때는 더 작은 파트를 고르세요.",
        "rs_next": "변경 사항은 다음 파트 또는 새 녹화부터 적용됩니다."
    },
    "HE": {
        "rs_resnote": "מצב לכידה מקליט ברזולוציית הסטרים: האפשרויות למעלה משנות רק את קצב הסיביות (דחיסה), ו-1080p–360p מציינות את איכות התמונה הצפויה. במצב HLS נבחר סטרים שגובהו אינו עולה על הגובה שנבחר.",
        "rs_hint": "כשחלק מגיע לגודל שנבחר, הוא נשמר כקובץ נפרד וההקלטה ממשיכה בחדש. הנתונים נכתבים לדיסק במקטעים קטנים; אם הדיסק אינו זמין, הם נשארים בזיכרון הטלפון, ולכן כדאי לבחור חלקים קטנים יותר.",
        "rs_next": "השינויים חלים מהחלק הבא או מהקלטה חדשה."
    },
    "AR": {
        "rs_resnote": "يسجّل وضع الالتقاط بدقة البث: الخيارات أعلاه تغيّر معدل البت (الضغط) فقط، وتشير 1080p–360p إلى جودة الصورة المتوقعة. في وضع HLS يُختار بث لا يزيد ارتفاعه على الارتفاع المحدد.",
        "rs_hint": "عندما يصل الجزء إلى الحجم المختار يُحفظ كملف مستقل ويتابع التسجيل في جزء جديد. تُكتب البيانات على القرص بكتل صغيرة؛ وإذا لم يكن القرص متاحًا تبقى في ذاكرة الهاتف، ولذلك يُفضَّل اختيار أجزاء أصغر.",
        "rs_next": "تسري التغييرات من الجزء التالي أو من تسجيل جديد."
    },
    "HI": {
        "rs_resnote": "कैप्चर मोड स्ट्रीम के रेज़ोल्यूशन पर रिकॉर्ड करता है: ऊपर के विकल्प सिर्फ़ बिटरेट (कंप्रेशन) बदलते हैं, और 1080p–360p अपेक्षित तस्वीर की क्वालिटी बताते हैं। HLS मोड में चुनी गई ऊँचाई से ज़्यादा ऊँची स्ट्रीम नहीं चुनी जाती।",
        "rs_hint": "जब कोई पार्ट चुने गए आकार तक पहुँचता है, तो वह अलग फ़ाइल के रूप में सेव हो जाता है और रिकॉर्डिंग नए पार्ट में जारी रहती है। डेटा छोटे-छोटे हिस्सों में डिस्क पर लिखा जाता है; डिस्क उपलब्ध न हो तो वह फ़ोन की मेमोरी में रहता है, तब छोटे पार्ट चुनना बेहतर है।",
        "rs_next": "बदलाव अगले पार्ट या नई रिकॉर्डिंग से लागू होते हैं।"
    },
    "ID": {
        "rs_resnote": "Capture merekam dengan resolusi stream: opsi di atas hanya mengubah bitrate (kompresi), dan 1080p–360p menunjukkan perkiraan kualitas gambar. Pada mode HLS, dipilih stream dengan tinggi tidak melebihi yang ditetapkan.",
        "rs_hint": "Saat sebuah bagian mencapai ukuran yang dipilih, bagian itu disimpan sebagai file terpisah dan rekaman berlanjut di bagian baru. Data ditulis ke disk dalam blok kecil; jika disk tidak tersedia, data tetap di memori ponsel, jadi pilih bagian yang lebih kecil.",
        "rs_next": "Perubahan berlaku mulai bagian berikutnya atau rekaman baru."
    },
    "VI": {
        "rs_resnote": "Chế độ Capture ghi theo độ phân giải của luồng: các tùy chọn ở trên chỉ thay đổi bitrate (nén), còn 1080p–360p cho biết chất lượng hình ảnh dự kiến. Ở chế độ HLS, hệ thống chọn luồng có chiều cao không vượt quá mức đã chọn.",
        "rs_hint": "Khi một phần đạt kích thước đã chọn, nó được lưu thành tệp riêng và việc ghi tiếp tục ở phần mới. Dữ liệu được ghi vào ổ đĩa theo từng khối nhỏ; nếu ổ đĩa không khả dụng, dữ liệu nằm trong bộ nhớ điện thoại, khi đó nên chọn các phần nhỏ hơn.",
        "rs_next": "Thay đổi có hiệu lực từ phần tiếp theo hoặc bản ghi mới."
    },
    "TH": {
        "rs_resnote": "โหมดแคปเจอร์อัดด้วยความละเอียดของสตรีม: ตัวเลือกด้านบนเปลี่ยนเฉพาะบิตเรต (การบีบอัด) ส่วน 1080p–360p แสดงคุณภาพภาพที่คาดไว้ ในโหมด HLS จะเลือกสตรีมที่สูงไม่เกินค่าที่กำหนด",
        "rs_hint": "เมื่อส่วนหนึ่งถึงขนาดที่เลือก ระบบจะบันทึกเป็นไฟล์แยก แล้วอัดต่อในส่วนใหม่ ข้อมูลถูกเขียนลงดิสก์เป็นก้อนเล็กๆ หากใช้ดิสก์ไม่ได้ ข้อมูลจะอยู่ในหน่วยความจำโทรศัพท์ ในกรณีนั้นควรเลือกส่วนที่เล็กลง",
        "rs_next": "การเปลี่ยนแปลงมีผลตั้งแต่ส่วนถัดไปหรือการอัดครั้งใหม่"
    }
}).forEach(([k, v]) => { if (i18n[k]) Object.assign(i18n[k], v); });
Object.keys(i18n).forEach(lg => {
    if (lg !== 'RU') i18n[lg].mv_add = i18n[lg].mv_go;
    if (lg !== 'RU' && lg !== 'UK') i18n[lg].bm_add = i18n[lg].star_add;
});
['ES', 'FR', 'PT', 'IT', 'EL'].forEach(lg => { i18n[lg].mv_dvr_off = i18n[lg].rs_off; });
i18n.HI.st_away = 'BRB'; i18n.ID.st_away = 'BRB';
Object.assign(i18n.ES, { rs_total: 'Detener al alcanzar el tamaño total' });
Object.assign(i18n.PT, { rs_total: 'Parar ao atingir o tamanho total' });
Object.assign(i18n.PL, { rs_total: 'Zatrzymaj przy łącznym rozmiarze' });
Object.assign(i18n.EL, { w_new: 'Μη προβεβλημένο', w_seen: 'Προβλήθηκε' });
Object.assign(i18n.DE, { sleep_left: 'Verbleibend' });
['no_model', 'bm_nomatch', 'st_prev', 'st_hide_topic', 'fr_none', 'hist_total', 'star_del_q'].forEach(k => { i18n.ZH[k] = i18n.ZH[k].replace(/主播/g, '模特'); });
LANGS.forEach(k => { i18n[k] = Object.assign({}, i18n.EN, i18n[k]); });
const HIDE_L = I18N_X.HIDE;
const I18N_HIDE2 = unpackI18N({"k":["sc_hdr","sc_sub","sc_promo","sc_cookie","sc_footer","sc_widgets","sc_goal","sc_topic","sc_king","sc_wm","sc_about","cb_hdr","cb_alerts","cb_footer","cb_sort","cb_tags","cb_tips","cb_status","cb_actions","cb_info"],"v":{"UK":[["Верх сайту","Шапка з логотипом, пошуком і кнопками входу"],["Рядок моделі","Аватар, підписка та меню над плеєром"],["Реклама та банери","Розіграші токенів, кнопка «Приєднатися» на плеєрі"],["Плашка cookies","Сповіщення про cookie внизу"],["Низ сторінки","Футер і юридичний текст"],["Віджети на плеєрі","Опитування та реакції поверх відео"],["Ціль (гол)","Смуга прогресу цілі"],["Тема кімнати","Текст теми над плеєром і всередині нього"],["Король чайових","Плашка короля чайових"],["Водяний знак","Логотип поверх відео"],["Блоки під плеєром","Про мене, розклад, схожі моделі"],["Верх сайту","Шапка з логотипом і меню"],["Сповіщення","Попередження про мову та нотатки, що закриваються"],["Низ сторінки","Футер і соцмережі"],["Сортування","Панель сортування в мобільній версії"],["Смуга тегів","Популярні теги над списком"],["Чайові в чаті","Повідомлення про токени та чайові"],["Статус кімнати","Плашка про офлайн і приват з підказками"],["Кнопка чайових","Кнопка та форма надсилання токенів"],["Назва кімнати","Імʼя та лічильник глядачів у шапці"]],"ES":[["Parte superior del sitio","Cabecera con logo, búsqueda y botones de acceso"],["Barra de la modelo","Avatar, seguir y menú sobre el reproductor"],["Anuncios y banners","Sorteos de tokens, botón «Unirse» en el reproductor"],["Aviso de cookies","Aviso de cookies en la parte inferior"],["Pie de página","Pie y texto legal"],["Widgets del reproductor","Encuestas y reacciones sobre el vídeo"],["Objetivo (goal)","Barra de progreso del objetivo"],["Tema de la sala","Texto del tema sobre y dentro del reproductor"],["Rey de propinas","Franja del rey de propinas"],["Marca de agua","Logo sobre el vídeo"],["Bloques bajo el reproductor","Sobre mí, horario, modelos similares"],["Parte superior del sitio","Cabecera con logo y menú"],["Avisos","Aviso de idioma y notas descartables"],["Pie de página","Pie y redes sociales"],["Orden","Panel de orden en la versión móvil"],["Franja de etiquetas","Etiquetas populares sobre la lista"],["Propinas en el chat","Mensajes de tokens y propinas"],["Estado de la sala","Aviso de desconexión y privado con sugerencias"],["Botón de propina","Botón y formulario para enviar tokens"],["Nombre de la sala","Nombre y contador de espectadores en la cabecera"]],"DE":[["Seitenkopf","Kopfzeile mit Logo, Suche und Anmelde-Buttons"],["Model-Leiste","Avatar, Folgen und Menü über dem Player"],["Werbung & Banner","Token-Verlosungen, „Beitreten“-Button am Player"],["Cookie-Hinweis","Cookie-Hinweis am unteren Rand"],["Seitenende","Footer und rechtliche Texte"],["Player-Widgets","Umfragen und Reaktionen über dem Video"],["Ziel (Goal)","Fortschrittsbalken des Ziels"],["Raumthema","Themen-Text über und im Player"],["Tip-König","Streifen des Tip-Königs"],["Wasserzeichen","Logo über dem Video"],["Bereiche unter dem Player","Über mich, Zeitplan, ähnliche Models"],["Seitenkopf","Kopfzeile mit Logo und Menü"],["Hinweise","Sprachhinweis und schließbare Notizen"],["Seitenende","Footer und Social Media"],["Sortierung","Sortierleiste in der mobilen Ansicht"],["Tag-Leiste","Beliebte Tags über der Liste"],["Trinkgelder im Chat","Meldungen zu Tokens und Trinkgeld"],["Raumstatus","Hinweis zu Offline und Privat mit Tipps"],["Trinkgeld-Button","Button und Formular zum Senden von Tokens"],["Raumname","Name und Zuschauerzahl in der Kopfzeile"]],"FR":[["Haut du site","En-tête avec logo, recherche et boutons de connexion"],["Barre du modèle","Avatar, abonnement et menu au-dessus du lecteur"],["Pubs et bannières","Tirages de tokens, bouton « Rejoindre » sur le lecteur"],["Bandeau cookies","Avis sur les cookies en bas"],["Bas de page","Pied de page et mentions légales"],["Widgets du lecteur","Sondages et réactions sur la vidéo"],["Objectif (goal)","Barre de progression de l’objectif"],["Sujet du salon","Texte du sujet au-dessus et dans le lecteur"],["Roi des pourboires","Bandeau du roi des pourboires"],["Filigrane","Logo sur la vidéo"],["Blocs sous le lecteur","À propos, planning, modèles similaires"],["Haut du site","En-tête avec logo et menu"],["Notifications","Alerte de langue et notes fermables"],["Bas de page","Pied de page et réseaux sociaux"],["Tri","Panneau de tri sur mobile"],["Bandeau de tags","Tags populaires au-dessus de la liste"],["Pourboires dans le chat","Messages de tokens et de pourboires"],["Statut du salon","Bandeau hors ligne et privé avec astuces"],["Bouton de pourboire","Bouton et formulaire d’envoi de tokens"],["Nom du salon","Nom et nombre de spectateurs dans l’en-tête"]],"PT":[["Topo do site","Cabeçalho com logo, busca e botões de login"],["Barra da modelo","Avatar, seguir e menu acima do player"],["Anúncios e banners","Sorteios de tokens, botão «Participar» no player"],["Aviso de cookies","Aviso de cookies na parte inferior"],["Rodapé","Rodapé e texto legal"],["Widgets do player","Enquetes e reações sobre o vídeo"],["Meta (goal)","Barra de progresso da meta"],["Tema da sala","Texto do tema acima e dentro do player"],["Rei das gorjetas","Faixa do rei das gorjetas"],["Marca d’água","Logo sobre o vídeo"],["Blocos abaixo do player","Sobre mim, agenda, modelos semelhantes"],["Topo do site","Cabeçalho com logo e menu"],["Avisos","Aviso de idioma e notas dispensáveis"],["Rodapé","Rodapé e redes sociais"],["Ordenação","Painel de ordenação na versão móvel"],["Faixa de tags","Tags populares acima da lista"],["Gorjetas no chat","Mensagens de tokens e gorjetas"],["Status da sala","Aviso de offline e privado com dicas"],["Botão de gorjeta","Botão e formulário para enviar tokens"],["Nome da sala","Nome e contador de espectadores no cabeçalho"]],"IT":[["Parte alta del sito","Intestazione con logo, ricerca e pulsanti di accesso"],["Barra della modella","Avatar, segui e menu sopra il player"],["Pubblicità e banner","Estrazioni di token, pulsante «Unisciti» sul player"],["Avviso cookie","Avviso sui cookie in basso"],["Fondo pagina","Footer e testo legale"],["Widget del player","Sondaggi e reazioni sopra il video"],["Obiettivo (goal)","Barra di avanzamento dell’obiettivo"],["Argomento della stanza","Testo dell’argomento sopra e dentro il player"],["Re delle mance","Striscia del re delle mance"],["Filigrana","Logo sopra il video"],["Blocchi sotto il player","Chi sono, programma, modelle simili"],["Parte alta del sito","Intestazione con logo e menu"],["Avvisi","Avviso sulla lingua e note chiudibili"],["Fondo pagina","Footer e social"],["Ordinamento","Pannello di ordinamento nella versione mobile"],["Striscia dei tag","Tag popolari sopra l’elenco"],["Mance in chat","Messaggi su token e mance"],["Stato della stanza","Avviso su offline e privato con suggerimenti"],["Pulsante mancia","Pulsante e modulo per inviare token"],["Nome della stanza","Nome e contatore spettatori nell’intestazione"]],"PL":[["Góra strony","Nagłówek z logo, wyszukiwarką i przyciskami logowania"],["Pasek modelki","Awatar, obserwowanie i menu nad odtwarzaczem"],["Reklamy i banery","Losowania tokenów, przycisk „Dołącz” na odtwarzaczu"],["Komunikat o cookies","Informacja o cookies na dole"],["Dół strony","Stopka i teksty prawne"],["Widżety odtwarzacza","Ankiety i reakcje nad wideo"],["Cel (goal)","Pasek postępu celu"],["Temat pokoju","Tekst tematu nad odtwarzaczem i w nim"],["Król napiwków","Pasek króla napiwków"],["Znak wodny","Logo na wideo"],["Bloki pod odtwarzaczem","O mnie, harmonogram, podobne modelki"],["Góra strony","Nagłówek z logo i menu"],["Powiadomienia","Ostrzeżenie o języku i zamykane notatki"],["Dół strony","Stopka i social media"],["Sortowanie","Panel sortowania w wersji mobilnej"],["Pasek tagów","Popularne tagi nad listą"],["Napiwki na czacie","Wiadomości o tokenach i napiwkach"],["Status pokoju","Plakietka offline i prywatny z podpowiedziami"],["Przycisk napiwku","Przycisk i formularz wysyłania tokenów"],["Nazwa pokoju","Nazwa i licznik widzów w nagłówku"]],"TR":[["Sitenin üst kısmı","Logo, arama ve giriş düğmeleri olan başlık"],["Model çubuğu","Oynatıcının üstünde avatar, takip ve menü"],["Reklamlar ve bannerlar","Token çekilişleri, oynatıcıdaki «Katıl» düğmesi"],["Çerez uyarısı","Altta çerez bildirimi"],["Sayfa altı","Alt bilgi ve yasal metin"],["Oynatıcı widget’ları","Videonun üstünde anketler ve tepkiler"],["Hedef (goal)","Hedef ilerleme çubuğu"],["Oda konusu","Oynatıcının üstündeki ve içindeki konu metni"],["Bahşiş kralı","Bahşiş kralı şeridi"],["Filigran","Videonun üzerindeki logo"],["Oynatıcı altı bloklar","Hakkımda, program, benzer modeller"],["Sitenin üst kısmı","Logo ve menü olan başlık"],["Bildirimler","Dil uyarısı ve kapatılabilir notlar"],["Sayfa altı","Alt bilgi ve sosyal ağlar"],["Sıralama","Mobil sürümde sıralama paneli"],["Etiket şeridi","Listenin üstündeki popüler etiketler"],["Sohbette bahşişler","Token ve bahşiş mesajları"],["Oda durumu","Çevrimdışı ve özel şeridi ile ipuçları"],["Bahşiş düğmesi","Token gönderme düğmesi ve formu"],["Oda adı","Başlıktaki ad ve izleyici sayacı"]],"NL":[["Site-header","Header met logo, zoekfunctie en inlogknoppen"],["Modelbalk","Avatar, volgen en menu boven de speler"],["Reclame en banners","Tokenverlotingen, «Doe mee»-knop op de speler"],["Cookiemelding","Cookiemelding onderaan"],["Voettekst","Footer en juridische tekst"],["Speler-widgets","Polls en reacties over de video"],["Doel (goal)","Voortgangsbalk van het doel"],["Kamerthema","Themateksten boven en in de speler"],["Tipkoning","Strook van de tipkoning"],["Watermerk","Logo over de video"],["Blokken onder de speler","Over mij, schema, vergelijkbare modellen"],["Site-header","Header met logo en menu"],["Meldingen","Taalwaarschuwing en sluitbare notities"],["Voettekst","Footer en socials"],["Sortering","Sorteerpaneel in de mobiele versie"],["Tagbalk","Populaire tags boven de lijst"],["Tips in de chat","Berichten over tokens en tips"],["Kamerstatus","Offline- en privémelding met hints"],["Tipknop","Knop en formulier om tokens te sturen"],["Kamernaam","Naam en kijkersteller in de header"]],"CS":[["Hlavička webu","Hlavička s logem, hledáním a tlačítky přihlášení"],["Lišta modelky","Avatar, sledování a menu nad přehrávačem"],["Reklama a bannery","Losování tokenů, tlačítko „Připojit se“ na přehrávači"],["Lišta cookies","Oznámení o cookies dole"],["Patička","Patička a právní text"],["Widgety přehrávače","Ankety a reakce přes video"],["Cíl (goal)","Ukazatel postupu cíle"],["Téma místnosti","Text tématu nad přehrávačem i v něm"],["Král tipů","Pruh krále tipů"],["Vodoznak","Logo přes video"],["Bloky pod přehrávačem","O mně, rozvrh, podobné modelky"],["Hlavička webu","Hlavička s logem a menu"],["Oznámení","Upozornění na jazyk a zavíratelné poznámky"],["Patička","Patička a sociální sítě"],["Řazení","Panel řazení v mobilní verzi"],["Pruh štítků","Oblíbené štítky nad seznamem"],["Tipy v chatu","Zprávy o tokenech a tipech"],["Stav místnosti","Lišta offline a privát s nápovědami"],["Tlačítko tipu","Tlačítko a formulář pro odeslání tokenů"],["Název místnosti","Jméno a počet diváků v hlavičce"]],"RO":[["Antetul site-ului","Antet cu logo, căutare și butoane de autentificare"],["Bara modelului","Avatar, urmărire și meniu deasupra playerului"],["Reclame și bannere","Tombole de tokeni, butonul „Alătură-te” de pe player"],["Aviz cookie","Notificare cookie în partea de jos"],["Subsolul paginii","Footer și text legal"],["Widgeturi pe player","Sondaje și reacții peste video"],["Obiectiv (goal)","Bara de progres a obiectivului"],["Tema camerei","Textul temei deasupra și în player"],["Regele tipurilor","Banda regelui tipurilor"],["Filigran","Logo peste video"],["Blocuri sub player","Despre mine, program, modele similare"],["Antetul site-ului","Antet cu logo și meniu"],["Notificări","Avertisment de limbă și note închise"],["Subsolul paginii","Footer și rețele sociale"],["Sortare","Panou de sortare în versiunea mobilă"],["Banda de taguri","Taguri populare deasupra listei"],["Tipuri în chat","Mesaje despre tokeni și tipuri"],["Starea camerei","Banda offline și privat cu indicii"],["Butonul de tip","Buton și formular pentru trimiterea tokenilor"],["Numele camerei","Nume și contor de spectatori în antet"]],"HU":[["Oldal fejléce","Fejléc logóval, kereséssel és belépés gombokkal"],["Modell sáv","Avatar, követés és menü a lejátszó felett"],["Reklámok és bannerek","Token-sorsolások, „Csatlakozz” gomb a lejátszón"],["Süti értesítés","Süti figyelmeztetés alul"],["Oldal alja","Lábléc és jogi szöveg"],["Lejátszó widgetek","Szavazások és reakciók a videó felett"],["Cél (goal)","A cél haladási sávja"],["Szoba témája","A téma szövege a lejátszó felett és benne"],["Borravalókirály","A borravalókirály sávja"],["Vízjel","Logó a videó felett"],["Blokkok a lejátszó alatt","Rólam, menetrend, hasonló modellek"],["Oldal fejléce","Fejléc logóval és menüvel"],["Értesítések","Nyelvi figyelmeztetés és bezárható jegyzetek"],["Oldal alja","Lábléc és közösségi média"],["Rendezés","Rendezési panel mobil nézetben"],["Címkesáv","Népszerű címkék a lista felett"],["Borravaló a csevegésben","Token és borravaló üzenetek"],["Szoba állapota","Offline és privát sáv tippekkel"],["Borravaló gomb","Gomb és űrlap tokenek küldéséhez"],["Szoba neve","Név és nézőszámláló a fejlécben"]],"BG":[["Хедър на сайта","Хедър с лого, търсене и бутони за вход"],["Лента на модела","Аватар, следване и меню над плеъра"],["Реклами и банери","Томболи с токени, бутон „Присъедини се“ на плеъра"],["Известие за бисквитки","Известие за бисквитки долу"],["Долна част на страницата","Футър и правен текст"],["Приспособления на плеъра","Анкети и реакции върху видеото"],["Цел (goal)","Лента за напредъка на целта"],["Тема на стаята","Текст на темата над и в плеъра"],["Цар на бакшишите","Ивица на царя на бакшишите"],["Воден знак","Лого върху видеото"],["Блокове под плеъра","За мен, разписание, подобни модели"],["Хедър на сайта","Хедър с лого и меню"],["Известия","Предупреждение за език и затваряеми бележки"],["Долна част на страницата","Футър и социални мрежи"],["Сортиране","Панел за сортиране в мобилната версия"],["Лента с тагове","Популярни тагове над списъка"],["Бакшиши в чата","Съобщения за токени и бакшиши"],["Статус на стаята","Лента за офлайн и приват с подсказки"],["Бутон за бакшиш","Бутон и форма за изпращане на токени"],["Име на стаята","Име и брояч на зрители в хедъра"]],"EL":[["Κεφαλίδα ιστότοπου","Κεφαλίδα με λογότυπο, αναζήτηση και κουμπιά σύνδεσης"],["Γραμμή μοντέλου","Avatar, ακολούθηση και μενού πάνω από τον player"],["Διαφημίσεις και banners","Κληρώσεις tokens, κουμπί «Γίνε μέλος» στον player"],["Ειδοποίηση cookies","Ειδοποίηση για cookies στο κάτω μέρος"],["Υποσέλιδο","Footer και νομικό κείμενο"],["Widgets του player","Δημοσκοπήσεις και αντιδράσεις πάνω από το βίντεο"],["Στόχος (goal)","Γραμμή προόδου του στόχου"],["Θέμα δωματίου","Κείμενο θέματος πάνω από και μέσα στον player"],["Βασιλιάς φιλοδωρημάτων","Λωρίδα του βασιλιά tip"],["Υδατογράφημα","Λογότυπο πάνω από το βίντεο"],["Μπλοκ κάτω από τον player","Σχετικά με μένα, πρόγραμμα, παρόμοια μοντέλα"],["Κεφαλίδα ιστότοπου","Κεφαλίδα με λογότυπο και μενού"],["Ειδοποιήσεις","Προειδοποίηση γλώσσας και σημειώσεις που κλείνουν"],["Υποσέλιδο","Footer και social"],["Ταξινόμηση","Πάνελ ταξινόμησης στην έκδοση για κινητά"],["Λωρίδα ετικετών","Δημοφιλείς ετικέτες πάνω από τη λίστα"],["Φιλοδωρήματα στο chat","Μηνύματα για tokens και tips"],["Κατάσταση δωματίου","Λωρίδα offline και private με υποδείξεις"],["Κουμπί tip","Κουμπί και φόρμα αποστολής tokens"],["Όνομα δωματίου","Όνομα και μετρητής θεατών στην κεφαλίδα"]],"JA":[["サイトのヘッダー","ロゴ、検索、ログインボタンのあるヘッダー"],["モデルバー","プレーヤー上のアバター、フォロー、メニュー"],["広告とバナー","トークン抽選、プレーヤー上の「参加」ボタン"],["Cookie通知","下部のCookie通知"],["ページ下部","フッターと法的テキスト"],["プレーヤーのウィジェット","動画上のアンケートとリアクション"],["目標(ゴール)","目標の進行バー"],["ルームのトピック","プレーヤー上と内部のトピック文"],["チップキング","チップキングの帯"],["透かし","動画上のロゴ"],["プレーヤー下のブロック","自己紹介、スケジュール、似たモデル"],["サイトのヘッダー","ロゴとメニューのあるヘッダー"],["通知","言語の警告と閉じられるメモ"],["ページ下部","フッターとSNS"],["並べ替え","モバイル版の並べ替えパネル"],["タグの帯","リスト上の人気タグ"],["チャットのチップ","トークンとチップのメッセージ"],["ルームの状態","オフラインとプライベートの案内帯"],["チップボタン","トークン送信のボタンとフォーム"],["ルーム名","ヘッダーの名前と視聴者数"]],"ZH":[["网站顶部","带有 Logo、搜索和登录按钮的页头"],["主播栏","播放器上方的头像、关注和菜单"],["广告和横幅","代币抽奖、播放器上的“加入”按钮"],["Cookie 提示","底部的 Cookie 通知"],["页面底部","页脚和法律文字"],["播放器小组件","视频上的投票和互动"],["目标(goal)","目标进度条"],["房间话题","播放器上方和内部的话题文字"],["打赏之王","打赏之王条"],["水印","视频上的 Logo"],["播放器下方区块","关于我、日程、相似主播"],["网站顶部","带有 Logo 和菜单的页头"],["通知","语言提示和可关闭的说明"],["页面底部","页脚和社交媒体"],["排序","移动版的排序面板"],["标签条","列表上方的热门标签"],["聊天打赏","代币和打赏消息"],["房间状态","离线和私密提示条"],["打赏按钮","发送代币的按钮和表单"],["房间名称","页头中的名称和观众数"]],"KO":[["사이트 상단","로고, 검색, 로그인 버튼이 있는 헤더"],["모델 바","플레이어 위의 아바타, 팔로우, 메뉴"],["광고 및 배너","토큰 추첨, 플레이어의 “참여” 버튼"],["쿠키 안내","하단의 쿠키 알림"],["페이지 하단","푸터와 법적 문구"],["플레이어 위젯","영상 위의 투표와 반응"],["목표(골)","목표 진행 바"],["방 주제","플레이어 위와 안의 주제 문구"],["팁 킹","팁 킹 띠"],["워터마크","영상 위의 로고"],["플레이어 아래 블록","소개, 일정, 비슷한 모델"],["사이트 상단","로고와 메뉴가 있는 헤더"],["알림","언어 경고와 닫을 수 있는 메모"],["페이지 하단","푸터와 SNS"],["정렬","모바일 버전의 정렬 패널"],["태그 띠","목록 위의 인기 태그"],["채팅 팁","토큰과 팁 메시지"],["방 상태","오프라인과 프라이빗 안내 띠"],["팁 버튼","토큰 전송 버튼과 양식"],["방 이름","헤더의 이름과 시청자 수"]],"HE":[["כותרת האתר","כותרת עם לוגו, חיפוש וכפתורי כניסה"],["סרגל המודל","אווטאר, מעקב ותפריט מעל הנגן"],["פרסומות ובאנרים","הגרלות טוקנים, כפתור “הצטרף” בנגן"],["הודעת קוקיז","הודעת קוקיז בתחתית"],["תחתית העמוד","פוטר וטקסט משפטי"],["ווידג׳טים בנגן","סקרים ותגובות מעל הוידאו"],["יעד (goal)","סרגל התקדמות היעד"],["נושא החדר","טקסט הנושא מעל הנגן ובתוכו"],["מלך הטיפים","רצועת מלך הטיפים"],["סימן מים","לוגו מעל הוידאו"],["בלוקים מתחת לנגן","אודותיי, לוח זמנים, מודלים דומים"],["כותרת האתר","כותרת עם לוגו ותפריט"],["התראות","אזהרת שפה והערות שניתן לסגור"],["תחתית העמוד","פוטר ורשתות חברתיות"],["מיון","לוח מיון בגרסה לנייד"],["רצועת תגיות","תגיות פופולריות מעל הרשימה"],["טיפים בצ׳אט","הודעות על טוקנים וטיפים"],["סטטוס החדר","רצועת לא מחובר ופרטי עם רמזים"],["כפתור טיפ","כפתור וטופס לשליחת טוקנים"],["שם החדר","שם ומונה צופים בכותרת"]],"AR":[["رأس الموقع","رأس مع الشعار والبحث وأزرار الدخول"],["شريط الموديل","الصورة الرمزية والمتابعة والقائمة فوق المشغّل"],["الإعلانات واللافتات","سحوبات الرموز وزر «انضم» على المشغّل"],["إشعار الكوكيز","إشعار الكوكيز في الأسفل"],["أسفل الصفحة","التذييل والنص القانوني"],["أدوات المشغّل","استطلاعات وتفاعلات فوق الفيديو"],["الهدف (goal)","شريط تقدّم الهدف"],["موضوع الغرفة","نص الموضوع فوق المشغّل وداخله"],["ملك البقشيش","شريط ملك البقشيش"],["العلامة المائية","الشعار فوق الفيديو"],["كتل أسفل المشغّل","نبذة عني والجدول وموديلات مشابهة"],["رأس الموقع","رأس مع الشعار والقائمة"],["الإشعارات","تنبيه اللغة وملاحظات قابلة للإغلاق"],["أسفل الصفحة","التذييل والشبكات الاجتماعية"],["الفرز","لوحة الفرز في نسخة الجوال"],["شريط الوسوم","وسوم شائعة فوق القائمة"],["البقشيش في الدردشة","رسائل الرموز والبقشيش"],["حالة الغرفة","شريط غير متصل وخاص مع تلميحات"],["زر البقشيش","زر ونموذج إرسال الرموز"],["اسم الغرفة","الاسم وعدّاد المشاهدين في الرأس"]],"HI":[["साइट का हेडर","लोगो, खोज और लॉगिन बटन वाला हेडर"],["मॉडल बार","प्लेयर के ऊपर अवतार, फ़ॉलो और मेन्यू"],["विज्ञापन और बैनर","टोकन ड्रॉ, प्लेयर पर “जुड़ें” बटन"],["कुकी सूचना","नीचे कुकी की सूचना"],["पेज का निचला हिस्सा","फ़ुटर और कानूनी टेक्स्ट"],["प्लेयर विजेट","वीडियो के ऊपर पोल और रिएक्शन"],["लक्ष्य (गोल)","लक्ष्य की प्रगति पट्टी"],["रूम का विषय","प्लेयर के ऊपर और अंदर विषय का टेक्स्ट"],["टिप किंग","टिप किंग की पट्टी"],["वॉटरमार्क","वीडियो के ऊपर लोगो"],["प्लेयर के नीचे के ब्लॉक","मेरे बारे में, शेड्यूल, मिलती-जुलती मॉडल"],["साइट का हेडर","लोगो और मेन्यू वाला हेडर"],["सूचनाएँ","भाषा की चेतावनी और बंद होने वाले नोट"],["पेज का निचला हिस्सा","फ़ुटर और सोशल मीडिया"],["सॉर्टिंग","मोबाइल संस्करण में सॉर्ट पैनल"],["टैग पट्टी","सूची के ऊपर लोकप्रिय टैग"],["चैट में टिप","टोकन और टिप के संदेश"],["रूम की स्थिति","ऑफ़लाइन और प्राइवेट पट्टी, सुझावों के साथ"],["टिप बटन","टोकन भेजने का बटन और फ़ॉर्म"],["रूम का नाम","हेडर में नाम और दर्शकों की गिनती"]],"ID":[["Header situs","Header dengan logo, pencarian, dan tombol masuk"],["Bilah model","Avatar, ikuti, dan menu di atas pemutar"],["Iklan dan banner","Undian token, tombol “Bergabung” di pemutar"],["Pemberitahuan cookie","Pemberitahuan cookie di bagian bawah"],["Bagian bawah halaman","Footer dan teks hukum"],["Widget pemutar","Polling dan reaksi di atas video"],["Target (goal)","Bilah progres target"],["Topik ruangan","Teks topik di atas dan di dalam pemutar"],["Raja tip","Pita raja tip"],["Watermark","Logo di atas video"],["Blok di bawah pemutar","Tentang saya, jadwal, model serupa"],["Header situs","Header dengan logo dan menu"],["Pemberitahuan","Peringatan bahasa dan catatan yang bisa ditutup"],["Bagian bawah halaman","Footer dan media sosial"],["Pengurutan","Panel urutkan di versi seluler"],["Pita tag","Tag populer di atas daftar"],["Tip di chat","Pesan token dan tip"],["Status ruangan","Pita offline dan privat dengan petunjuk"],["Tombol tip","Tombol dan formulir kirim token"],["Nama ruangan","Nama dan penghitung penonton di header"]],"VI":[["Phần đầu trang","Header có logo, tìm kiếm và nút đăng nhập"],["Thanh model","Avatar, theo dõi và menu phía trên trình phát"],["Quảng cáo và banner","Rút thăm token, nút “Tham gia” trên trình phát"],["Thông báo cookie","Thông báo cookie ở phía dưới"],["Chân trang","Footer và văn bản pháp lý"],["Tiện ích trên trình phát","Bình chọn và phản ứng trên video"],["Mục tiêu (goal)","Thanh tiến độ mục tiêu"],["Chủ đề phòng","Văn bản chủ đề phía trên và trong trình phát"],["Vua tip","Dải vua tip"],["Hình mờ","Logo trên video"],["Khối dưới trình phát","Giới thiệu, lịch, model tương tự"],["Phần đầu trang","Header có logo và menu"],["Thông báo","Cảnh báo ngôn ngữ và ghi chú có thể đóng"],["Chân trang","Footer và mạng xã hội"],["Sắp xếp","Bảng sắp xếp trên bản di động"],["Dải thẻ","Thẻ phổ biến phía trên danh sách"],["Tip trong chat","Tin nhắn về token và tip"],["Trạng thái phòng","Dải offline và riêng tư kèm gợi ý"],["Nút tip","Nút và biểu mẫu gửi token"],["Tên phòng","Tên và số người xem trong header"]],"TH":[["ส่วนหัวเว็บไซต์","ส่วนหัวที่มีโลโก้ ค้นหา และปุ่มเข้าสู่ระบบ"],["แถบโมเดล","อวาตาร์ ติดตาม และเมนูเหนือเครื่องเล่น"],["โฆษณาและแบนเนอร์","จับรางวัลโทเคน ปุ่ม “เข้าร่วม” บนเครื่องเล่น"],["แจ้งเตือนคุกกี้","การแจ้งเตือนคุกกี้ด้านล่าง"],["ท้ายหน้า","ส่วนท้ายและข้อความทางกฎหมาย"],["วิดเจ็ตบนเครื่องเล่น","โพลและรีแอคชันเหนือวิดีโอ"],["เป้าหมาย (goal)","แถบความคืบหน้าของเป้าหมาย"],["หัวข้อห้อง","ข้อความหัวข้อเหนือและในเครื่องเล่น"],["ราชาทิป","แถบราชาทิป"],["ลายน้ำ","โลโก้เหนือวิดีโอ"],["บล็อกใต้เครื่องเล่น","เกี่ยวกับฉัน ตารางเวลา โมเดลที่คล้ายกัน"],["ส่วนหัวเว็บไซต์","ส่วนหัวที่มีโลโก้และเมนู"],["การแจ้งเตือน","คำเตือนเรื่องภาษาและโน้ตที่ปิดได้"],["ท้ายหน้า","ส่วนท้ายและโซเชียล"],["การเรียง","แผงเรียงในเวอร์ชันมือถือ"],["แถบแท็ก","แท็กยอดนิยมเหนือรายการ"],["ทิปในแชต","ข้อความเกี่ยวกับโทเคนและทิป"],["สถานะห้อง","แถบออฟไลน์และส่วนตัวพร้อมคำแนะนำ"],["ปุ่มทิป","ปุ่มและฟอร์มส่งโทเคน"],["ชื่อห้อง","ชื่อและตัวนับผู้ชมในส่วนหัว"]]}});
Object.keys(I18N_HIDE2).forEach(k => { HIDE_L[k] = Object.assign(HIDE_L[k] || {}, I18N_HIDE2[k]); });
const I18N_CAL = {
    RU: {
        w_new: "Не просмотрено",
        w_seen: "Просмотрено",
        w_all: "Все",
        w_tip: "Просмотрено — если вы были в эфире модели 30 секунд и больше",
        cal_title: "Календарь",
        cal_today: "Сегодня",
        cal_hist: "Просмотры",
        cal_sched: "Расписание",
        cal_models: "Модели",
        cal_reset: "Сбросить",
        cal_all_tag: "Все теги",
        cal_none: "В этот день пусто",
        cal_watched: "Просмотрено",
        cal_on_air: "По расписанию",
        cal_no_sched: "Расписание берётся из профилей избранных. Откройте «Избранное» и подождите несколько секунд.",
        cal_allday: "Весь день",
        cal_loaded: "Расписание загружено: {a} из {b}",
        cal_c_model: "Модель",
        cal_c_time: "Вход",
        cal_c_dur: "Длит.",
        cal_c_n: "Заходы",
        cal_total: "Итого"
    },
    EN: {
        w_new: "Not watched",
        w_seen: "Watched",
        w_all: "All",
        w_tip: "Watched — you were in the model's stream for 30 seconds or more",
        cal_title: "Calendar",
        cal_today: "Today",
        cal_hist: "History",
        cal_sched: "Schedule",
        cal_models: "Models",
        cal_reset: "Reset",
        cal_all_tag: "All tags",
        cal_none: "Nothing on this day",
        cal_watched: "Viewed",
        cal_on_air: "Scheduled",
        cal_no_sched: "The schedule is read from the profiles of your favorites. Open Favorites and wait a few seconds.",
        cal_allday: "All day",
        cal_loaded: "Schedule loaded: {a} of {b}",
        cal_c_model: "Model",
        cal_c_time: "Joined",
        cal_c_dur: "Duration",
        cal_c_n: "Visits",
        cal_total: "Total"
    },
    UK: {
        w_new: "Не переглянуто",
        w_seen: "Переглянуто",
        w_all: "Усі",
        w_tip: "Переглянуто — якщо ви були в ефірі моделі 30 секунд і більше",
        cal_title: "Календар",
        cal_today: "Сьогодні",
        cal_hist: "Перегляди",
        cal_sched: "Розклад",
        cal_models: "Моделі",
        cal_reset: "Скинути",
        cal_all_tag: "Усі теги",
        cal_none: "У цей день порожньо",
        cal_watched: "Переглянуто",
        cal_on_air: "За розкладом",
        cal_no_sched: "Розклад береться з профілів обраних. Відкрийте «Обране» і зачекайте кілька секунд.",
        cal_allday: "Увесь день",
        cal_loaded: "Розклад завантажено: {a} з {b}",
        cal_c_model: "Модель",
        cal_c_time: "Вхід",
        cal_c_dur: "Трив.",
        cal_c_n: "Заходи",
        cal_total: "Разом"
    },
    ES: {
        w_new: "No visto",
        w_seen: "Visto",
        w_all: "Todos",
        w_tip: "Visto: has estado en la emisión de la modelo 30 segundos o más",
        cal_title: "Calendario",
        cal_today: "Hoy",
        cal_hist: "Vistos",
        cal_sched: "Horario",
        cal_models: "Modelos",
        cal_reset: "Restablecer",
        cal_all_tag: "Todas las etiquetas",
        cal_none: "No hay nada este día",
        cal_watched: "Visto",
        cal_on_air: "Según horario",
        cal_no_sched: "El horario se toma de los perfiles de tus favoritos. Abre Favoritos y espera unos segundos.",
        cal_allday: "Todo el día",
        cal_loaded: "Horario cargado: {a} de {b}",
        cal_c_model: "Modelo",
        cal_c_time: "Entrada",
        cal_c_dur: "Dur.",
        cal_c_n: "Visitas",
        cal_total: "Total"
    },
    DE: {
        w_new: "Nicht gesehen",
        w_seen: "Gesehen",
        w_all: "Alle",
        w_tip: "Gesehen – du warst mindestens 30 Sekunden im Stream des Models",
        cal_title: "Kalender",
        cal_today: "Heute",
        cal_hist: "Verlauf",
        cal_sched: "Zeitplan",
        cal_models: "Models",
        cal_reset: "Zurücksetzen",
        cal_all_tag: "Alle Tags",
        cal_none: "An diesem Tag ist nichts",
        cal_watched: "Gesehen",
        cal_on_air: "Laut Zeitplan",
        cal_no_sched: "Der Zeitplan stammt aus den Profilen deiner Favoriten. Öffne die Favoriten und warte ein paar Sekunden.",
        cal_allday: "Ganztägig",
        cal_loaded: "Zeitplan geladen: {a} von {b}",
        cal_c_model: "Model",
        cal_c_time: "Eintritt",
        cal_c_dur: "Dauer",
        cal_c_n: "Besuche",
        cal_total: "Gesamt"
    },
    FR: {
        w_new: "Non vu",
        w_seen: "Vu",
        w_all: "Tous",
        w_tip: "Vu : vous êtes resté au moins 30 secondes dans le live du modèle",
        cal_title: "Calendrier",
        cal_today: "Aujourd'hui",
        cal_hist: "Historique",
        cal_sched: "Horaires",
        cal_models: "Modèles",
        cal_reset: "Réinitialiser",
        cal_all_tag: "Tous les tags",
        cal_none: "Rien ce jour-là",
        cal_watched: "Vu",
        cal_on_air: "Selon l’horaire",
        cal_no_sched: "Les horaires sont lus dans les profils de vos favoris. Ouvrez les favoris et patientez quelques secondes.",
        cal_allday: "Toute la journée",
        cal_loaded: "Horaires chargés : {a} sur {b}",
        cal_c_model: "Modèle",
        cal_c_time: "Entrée",
        cal_c_dur: "Durée",
        cal_c_n: "Visites",
        cal_total: "Total"
    },
    PT: {
        w_new: "Não visto",
        w_seen: "Visto",
        w_all: "Todos",
        w_tip: "Visto: você ficou na transmissão da modelo por 30 segundos ou mais",
        cal_title: "Calendário",
        cal_today: "Hoje",
        cal_hist: "Histórico",
        cal_sched: "Horários",
        cal_models: "Modelos",
        cal_reset: "Redefinir",
        cal_all_tag: "Todas as tags",
        cal_none: "Nada neste dia",
        cal_watched: "Visto",
        cal_on_air: "Pelo horário",
        cal_no_sched: "Os horários vêm dos perfis dos seus favoritos. Abra os Favoritos e aguarde alguns segundos.",
        cal_allday: "O dia todo",
        cal_loaded: "Horários carregados: {a} de {b}",
        cal_c_model: "Modelo",
        cal_c_time: "Entrada",
        cal_c_dur: "Dur.",
        cal_c_n: "Visitas",
        cal_total: "Total"
    },
    IT: {
        w_new: "Non visto",
        w_seen: "Visto",
        w_all: "Tutti",
        w_tip: "Visto: sei rimasto nella diretta della modella per almeno 30 secondi",
        cal_title: "Calendario",
        cal_today: "Oggi",
        cal_hist: "Cronologia",
        cal_sched: "Orari",
        cal_models: "Modelle",
        cal_reset: "Reimposta",
        cal_all_tag: "Tutti i tag",
        cal_none: "Niente in questo giorno",
        cal_watched: "Visto",
        cal_on_air: "Da programma",
        cal_no_sched: "Gli orari vengono letti dai profili dei tuoi preferiti. Apri i Preferiti e attendi qualche secondo.",
        cal_allday: "Tutto il giorno",
        cal_loaded: "Orari caricati: {a} su {b}",
        cal_c_model: "Modella",
        cal_c_time: "Ingresso",
        cal_c_dur: "Durata",
        cal_c_n: "Visite",
        cal_total: "Totale"
    },
    PL: {
        w_new: "Nieoglądane",
        w_seen: "Oglądane",
        w_all: "Wszystkie",
        w_tip: "Oglądane — byłeś na transmisji modelki co najmniej 30 sekund",
        cal_title: "Kalendarz",
        cal_today: "Dziś",
        cal_hist: "Historia",
        cal_sched: "Grafik",
        cal_models: "Modelki",
        cal_reset: "Resetuj",
        cal_all_tag: "Wszystkie tagi",
        cal_none: "Tego dnia nic nie ma",
        cal_watched: "Oglądane",
        cal_on_air: "Wg grafiku",
        cal_no_sched: "Grafik jest pobierany z profili ulubionych. Otwórz Ulubione i poczekaj kilka sekund.",
        cal_allday: "Cały dzień",
        cal_loaded: "Grafik wczytany: {a} z {b}",
        cal_c_model: "Modelka",
        cal_c_time: "Wejście",
        cal_c_dur: "Czas",
        cal_c_n: "Wejścia",
        cal_total: "Razem"
    },
    TR: {
        w_new: "İzlenmedi",
        w_seen: "İzlendi",
        w_all: "Tümü",
        w_tip: "İzlendi — modelin yayınında en az 30 saniye bulundunuz",
        cal_title: "Takvim",
        cal_today: "Bugün",
        cal_hist: "Geçmiş",
        cal_sched: "Program",
        cal_models: "Modeller",
        cal_reset: "Sıfırla",
        cal_all_tag: "Tüm etiketler",
        cal_none: "Bu gün boş",
        cal_watched: "İzlendi",
        cal_on_air: "Programa göre",
        cal_no_sched: "Program, favorilerinizin profillerinden alınır. Favoriler'i açın ve birkaç saniye bekleyin.",
        cal_allday: "Tüm gün",
        cal_loaded: "Program yüklendi: {a} / {b}",
        cal_c_model: "Model",
        cal_c_time: "Giriş",
        cal_c_dur: "Süre",
        cal_c_n: "Ziyaret",
        cal_total: "Toplam"
    },
    NL: {
        w_new: "Niet bekeken",
        w_seen: "Bekeken",
        w_all: "Alle",
        w_tip: "Bekeken — je zat minstens 30 seconden in de uitzending van het model",
        cal_title: "Kalender",
        cal_today: "Vandaag",
        cal_hist: "Geschiedenis",
        cal_sched: "Rooster",
        cal_models: "Modellen",
        cal_reset: "Resetten",
        cal_all_tag: "Alle tags",
        cal_none: "Niets op deze dag",
        cal_watched: "Bekeken",
        cal_on_air: "Volgens rooster",
        cal_no_sched: "Het rooster komt uit de profielen van je favorieten. Open Favorieten en wacht een paar seconden.",
        cal_allday: "Hele dag",
        cal_loaded: "Rooster geladen: {a} van {b}",
        cal_c_model: "Model",
        cal_c_time: "Binnenkomst",
        cal_c_dur: "Duur",
        cal_c_n: "Bezoeken",
        cal_total: "Totaal"
    },
    CS: {
        w_new: "Neshlédnuto",
        w_seen: "Shlédnuto",
        w_all: "Vše",
        w_tip: "Shlédnuto — byli jste ve vysílání modelky 30 sekund a déle",
        cal_title: "Kalendář",
        cal_today: "Dnes",
        cal_hist: "Historie",
        cal_sched: "Rozvrh",
        cal_models: "Modelky",
        cal_reset: "Obnovit",
        cal_all_tag: "Všechny štítky",
        cal_none: "V tento den nic není",
        cal_watched: "Shlédnuto",
        cal_on_air: "Podle rozvrhu",
        cal_no_sched: "Rozvrh se načítá z profilů oblíbených. Otevřete Oblíbené a chvíli počkejte.",
        cal_allday: "Celý den",
        cal_loaded: "Rozvrh načten: {a} z {b}",
        cal_c_model: "Modelka",
        cal_c_time: "Vstup",
        cal_c_dur: "Doba",
        cal_c_n: "Vstupy",
        cal_total: "Celkem"
    },
    RO: {
        w_new: "Nevăzut",
        w_seen: "Văzut",
        w_all: "Toate",
        w_tip: "Văzut — ai stat în transmisia modelului cel puțin 30 de secunde",
        cal_title: "Calendar",
        cal_today: "Azi",
        cal_hist: "Istoric",
        cal_sched: "Program",
        cal_models: "Modele",
        cal_reset: "Resetează",
        cal_all_tag: "Toate etichetele",
        cal_none: "Nimic în această zi",
        cal_watched: "Văzut",
        cal_on_air: "Conform programului",
        cal_no_sched: "Programul este preluat din profilurile favoritelor. Deschide Favorite și așteaptă câteva secunde.",
        cal_allday: "Toată ziua",
        cal_loaded: "Program încărcat: {a} din {b}",
        cal_c_model: "Model",
        cal_c_time: "Intrare",
        cal_c_dur: "Durată",
        cal_c_n: "Vizite",
        cal_total: "Total"
    },
    HU: {
        w_new: "Nem nézett",
        w_seen: "Nézett",
        w_all: "Mind",
        w_tip: "Nézett — legalább 30 másodpercig a modell adásában voltál",
        cal_title: "Naptár",
        cal_today: "Ma",
        cal_hist: "Előzmények",
        cal_sched: "Menetrend",
        cal_models: "Modellek",
        cal_reset: "Visszaállítás",
        cal_all_tag: "Minden címke",
        cal_none: "Ezen a napon nincs semmi",
        cal_watched: "Nézett",
        cal_on_air: "Menetrend szerint",
        cal_no_sched: "A menetrend a kedvencek profiljából töltődik be. Nyisd meg a Kedvenceket, és várj pár másodpercet.",
        cal_allday: "Egész nap",
        cal_loaded: "Menetrend betöltve: {a} / {b}",
        cal_c_model: "Modell",
        cal_c_time: "Belépés",
        cal_c_dur: "Idő",
        cal_c_n: "Alkalom",
        cal_total: "Összesen"
    },
    BG: {
        w_new: "Негледан",
        w_seen: "Гледан",
        w_all: "Всички",
        w_tip: "Гледан — били сте в ефира на модела поне 30 секунди",
        cal_title: "Календар",
        cal_today: "Днес",
        cal_hist: "История",
        cal_sched: "Разписание",
        cal_models: "Модели",
        cal_reset: "Нулиране",
        cal_all_tag: "Всички тагове",
        cal_none: "Няма нищо за този ден",
        cal_watched: "Гледан",
        cal_on_air: "По разписание",
        cal_no_sched: "Разписанието се взема от профилите на любимите. Отворете „Любими“ и изчакайте няколко секунди.",
        cal_allday: "Цял ден",
        cal_loaded: "Разписание заредено: {a} от {b}",
        cal_c_model: "Модел",
        cal_c_time: "Вход",
        cal_c_dur: "Прод.",
        cal_c_n: "Пъти",
        cal_total: "Общо"
    },
    EL: {
        w_new: "Δεν το είδα",
        w_seen: "Το είδα",
        w_all: "Όλα",
        w_tip: "Το είδα — ήσασταν στο live του μοντέλου για 30 δευτερόλεπτα ή περισσότερο",
        cal_title: "Ημερολόγιο",
        cal_today: "Σήμερα",
        cal_hist: "Ιστορικό",
        cal_sched: "Πρόγραμμα",
        cal_models: "Μοντέλα",
        cal_reset: "Επαναφορά",
        cal_all_tag: "Όλες οι ετικέτες",
        cal_none: "Τίποτα αυτή την ημέρα",
        cal_watched: "Προβλήθηκε",
        cal_on_air: "Βάσει προγράμματος",
        cal_no_sched: "Το πρόγραμμα λαμβάνεται από τα προφίλ των αγαπημένων. Ανοίξτε τα Αγαπημένα και περιμένετε λίγα δευτερόλεπτα.",
        cal_allday: "Όλη μέρα",
        cal_loaded: "Πρόγραμμα φορτώθηκε: {a} από {b}",
        cal_c_model: "Μοντέλο",
        cal_c_time: "Είσοδος",
        cal_c_dur: "Διάρκ.",
        cal_c_n: "Φορές",
        cal_total: "Σύνολο"
    },
    JA: {
        w_new: "未視聴",
        w_seen: "視聴済み",
        w_all: "すべて",
        w_tip: "視聴済み — モデルの配信に30秒以上いた場合",
        cal_title: "カレンダー",
        cal_today: "今日",
        cal_hist: "視聴履歴",
        cal_sched: "スケジュール",
        cal_models: "モデル",
        cal_reset: "リセット",
        cal_all_tag: "すべてのタグ",
        cal_none: "この日は何もありません",
        cal_watched: "視聴済み",
        cal_on_air: "スケジュール通り",
        cal_no_sched: "スケジュールはお気に入りのプロフィールから取得されます。お気に入りを開いて数秒お待ちください。",
        cal_allday: "終日",
        cal_loaded: "スケジュール取得済み:{a}/{b}",
        cal_c_model: "モデル",
        cal_c_time: "入室",
        cal_c_dur: "時間",
        cal_c_n: "回数",
        cal_total: "合計"
    },
    ZH: {
        w_new: "未观看",
        w_seen: "已观看",
        w_all: "全部",
        w_tip: "已观看——您在模特直播间停留了30秒及以上",
        cal_title: "日历",
        cal_today: "今天",
        cal_hist: "观看记录",
        cal_sched: "时间表",
        cal_models: "模特",
        cal_reset: "重置",
        cal_all_tag: "所有标签",
        cal_none: "这一天没有内容",
        cal_watched: "已观看",
        cal_on_air: "按时间表",
        cal_no_sched: "时间表来自收藏模特的个人资料。请打开“收藏”并稍等几秒。",
        cal_allday: "全天",
        cal_loaded: "已加载时间表:{a}/{b}",
        cal_c_model: "模特",
        cal_c_time: "进入",
        cal_c_dur: "时长",
        cal_c_n: "次数",
        cal_total: "合计"
    },
    KO: {
        w_new: "미시청",
        w_seen: "시청함",
        w_all: "전체",
        w_tip: "시청함 — 모델의 방송에 30초 이상 있었던 경우",
        cal_title: "캘린더",
        cal_today: "오늘",
        cal_hist: "시청 기록",
        cal_sched: "일정",
        cal_models: "모델",
        cal_reset: "초기화",
        cal_all_tag: "모든 태그",
        cal_none: "이날은 비어 있습니다",
        cal_watched: "시청함",
        cal_on_air: "일정 기준",
        cal_no_sched: "일정은 즐겨찾기 프로필에서 가져옵니다. 즐겨찾기를 열고 몇 초 기다려 주세요.",
        cal_allday: "종일",
        cal_loaded: "일정 불러옴: {a}/{b}",
        cal_c_model: "모델",
        cal_c_time: "입장",
        cal_c_dur: "시간",
        cal_c_n: "횟수",
        cal_total: "합계"
    },
    HE: {
        w_new: "לא נצפה",
        w_seen: "נצפה",
        w_all: "הכול",
        w_tip: "נצפה — היית בשידור של המודל 30 שניות ומעלה",
        cal_title: "לוח שנה",
        cal_today: "היום",
        cal_hist: "היסטוריה",
        cal_sched: "לוח זמנים",
        cal_models: "מודלים",
        cal_reset: "איפוס",
        cal_all_tag: "כל התגיות",
        cal_none: "אין דבר ביום זה",
        cal_watched: "נצפה",
        cal_on_air: "לפי לוח הזמנים",
        cal_no_sched: "לוח הזמנים נלקח מפרופילי המועדפים. פתחו את המועדפים והמתינו כמה שניות.",
        cal_allday: "כל היום",
        cal_loaded: "לוח הזמנים נטען: {a} מתוך {b}",
        cal_c_model: "מודל",
        cal_c_time: "כניסה",
        cal_c_dur: "משך",
        cal_c_n: "פעמים",
        cal_total: "סה״כ"
    },
    AR: {
        w_new: "لم تتم المشاهدة",
        w_seen: "تمت المشاهدة",
        w_all: "الكل",
        w_tip: "تمت المشاهدة — إذا بقيت في بث العارضة 30 ثانية أو أكثر",
        cal_title: "التقويم",
        cal_today: "اليوم",
        cal_hist: "السجل",
        cal_sched: "الجدول",
        cal_models: "العارضات",
        cal_reset: "إعادة ضبط",
        cal_all_tag: "كل الوسوم",
        cal_none: "لا شيء في هذا اليوم",
        cal_watched: "تمت المشاهدة",
        cal_on_air: "حسب الجدول",
        cal_no_sched: "يؤخذ الجدول من ملفات المفضلة. افتح المفضلة وانتظر بضع ثوانٍ.",
        cal_allday: "طوال اليوم",
        cal_loaded: "تم تحميل الجدول: {a} من {b}",
        cal_c_model: "العارضة",
        cal_c_time: "الدخول",
        cal_c_dur: "المدة",
        cal_c_n: "المرات",
        cal_total: "المجموع"
    },
    HI: {
        w_new: "नहीं देखा",
        w_seen: "देखा",
        w_all: "सभी",
        w_tip: "देखा — यदि आप मॉडल के प्रसारण में 30 सेकंड या उससे अधिक रहे",
        cal_title: "कैलेंडर",
        cal_today: "आज",
        cal_hist: "इतिहास",
        cal_sched: "शेड्यूल",
        cal_models: "मॉडल",
        cal_reset: "रीसेट",
        cal_all_tag: "सभी टैग",
        cal_none: "इस दिन कुछ नहीं है",
        cal_watched: "देखा",
        cal_on_air: "शेड्यूल के अनुसार",
        cal_no_sched: "शेड्यूल पसंदीदा की प्रोफ़ाइल से लिया जाता है। पसंदीदा खोलें और कुछ सेकंड प्रतीक्षा करें।",
        cal_allday: "पूरे दिन",
        cal_loaded: "शेड्यूल लोड हुआ: {a}/{b}",
        cal_c_model: "मॉडल",
        cal_c_time: "प्रवेश",
        cal_c_dur: "अवधि",
        cal_c_n: "बार",
        cal_total: "कुल"
    },
    ID: {
        w_new: "Belum ditonton",
        w_seen: "Ditonton",
        w_all: "Semua",
        w_tip: "Ditonton — Anda berada di siaran model selama 30 detik atau lebih",
        cal_title: "Kalender",
        cal_today: "Hari ini",
        cal_hist: "Riwayat",
        cal_sched: "Jadwal",
        cal_models: "Model",
        cal_reset: "Atur ulang",
        cal_all_tag: "Semua tag",
        cal_none: "Tidak ada apa pun di hari ini",
        cal_watched: "Ditonton",
        cal_on_air: "Sesuai jadwal",
        cal_no_sched: "Jadwal diambil dari profil favorit Anda. Buka Favorit dan tunggu beberapa detik.",
        cal_allday: "Sepanjang hari",
        cal_loaded: "Jadwal dimuat: {a} dari {b}",
        cal_c_model: "Model",
        cal_c_time: "Masuk",
        cal_c_dur: "Durasi",
        cal_c_n: "Kali",
        cal_total: "Total"
    },
    VI: {
        w_new: "Chưa xem",
        w_seen: "Đã xem",
        w_all: "Tất cả",
        w_tip: "Đã xem — bạn đã ở trong buổi phát của người mẫu từ 30 giây trở lên",
        cal_title: "Lịch",
        cal_today: "Hôm nay",
        cal_hist: "Lịch sử",
        cal_sched: "Lịch phát",
        cal_models: "Người mẫu",
        cal_reset: "Đặt lại",
        cal_all_tag: "Tất cả thẻ",
        cal_none: "Hôm nay không có gì",
        cal_watched: "Đã xem",
        cal_on_air: "Theo lịch",
        cal_no_sched: "Lịch phát lấy từ hồ sơ của mục yêu thích. Hãy mở Yêu thích và chờ vài giây.",
        cal_allday: "Cả ngày",
        cal_loaded: "Đã tải lịch: {a}/{b}",
        cal_c_model: "Người mẫu",
        cal_c_time: "Vào",
        cal_c_dur: "Thời lượng",
        cal_c_n: "Lần",
        cal_total: "Tổng"
    },
    TH: {
        w_new: "ยังไม่ได้ดู",
        w_seen: "ดูแล้ว",
        w_all: "ทั้งหมด",
        w_tip: "ดูแล้ว — คุณอยู่ในการถ่ายทอดสดของโมเดลตั้งแต่ 30 วินาทีขึ้นไป",
        cal_title: "ปฏิทิน",
        cal_today: "วันนี้",
        cal_hist: "ประวัติ",
        cal_sched: "ตารางเวลา",
        cal_models: "โมเดล",
        cal_reset: "รีเซ็ต",
        cal_all_tag: "แท็กทั้งหมด",
        cal_none: "ไม่มีรายการในวันนี้",
        cal_watched: "ดูแล้ว",
        cal_on_air: "ตามตาราง",
        cal_no_sched: "ตารางเวลาดึงมาจากโปรไฟล์ของรายการโปรด เปิดรายการโปรดแล้วรอสักครู่",
        cal_allday: "ทั้งวัน",
        cal_loaded: "โหลดตารางแล้ว: {a}/{b}",
        cal_c_model: "โมเดล",
        cal_c_time: "เข้า",
        cal_c_dur: "เวลา",
        cal_c_n: "ครั้ง",
        cal_total: "รวม"
    }
};
Object.keys(i18n).forEach(lg => {
    const pack = I18N_CAL[lg] || I18N_CAL.EN;
    Object.keys(I18N_CAL.EN).forEach(k => { if (i18n[lg] && i18n[lg][k] == null) i18n[lg][k] = pack[k] ?? I18N_CAL.EN[k]; });
});
const I18N_SKIN = {
    RU: { st_skin: 'Стиль интерфейса', st_skin_classic: 'Классика', st_skin_modern: 'Минимализм' },
    EN: { st_skin: 'Interface style', st_skin_classic: 'Classic', st_skin_modern: 'Minimal' },
    UK: { st_skin: 'Стиль інтерфейсу', st_skin_classic: 'Класика', st_skin_modern: 'Мінімалізм' },
    ES: { st_skin: 'Estilo de interfaz', st_skin_classic: 'Clásico', st_skin_modern: 'Minimalista' },
    DE: { st_skin: 'Oberflächenstil', st_skin_classic: 'Klassisch', st_skin_modern: 'Minimal' },
    FR: { st_skin: "Style de l'interface", st_skin_classic: 'Classique', st_skin_modern: 'Minimaliste' },
    PT: { st_skin: 'Estilo da interface', st_skin_classic: 'Clássico', st_skin_modern: 'Minimalista' },
    IT: { st_skin: "Stile dell'interfaccia", st_skin_classic: 'Classico', st_skin_modern: 'Minimalista' },
    PL: { st_skin: 'Styl interfejsu', st_skin_classic: 'Klasyczny', st_skin_modern: 'Minimalistyczny' },
    TR: { st_skin: 'Arayüz stili', st_skin_classic: 'Klasik', st_skin_modern: 'Minimal' },
    NL: { st_skin: 'Interfacestijl', st_skin_classic: 'Klassiek', st_skin_modern: 'Minimalistisch' },
    CS: { st_skin: 'Styl rozhraní', st_skin_classic: 'Klasický', st_skin_modern: 'Minimalistický' },
    RO: { st_skin: 'Stilul interfeței', st_skin_classic: 'Clasic', st_skin_modern: 'Minimalist' },
    HU: { st_skin: 'Felület stílusa', st_skin_classic: 'Klasszikus', st_skin_modern: 'Minimalista' },
    BG: { st_skin: 'Стил на интерфейса', st_skin_classic: 'Класика', st_skin_modern: 'Минимализъм' },
    EL: { st_skin: 'Στυλ διεπαφής', st_skin_classic: 'Κλασικό', st_skin_modern: 'Μινιμαλιστικό' },
    JA: { st_skin: 'インターフェースのスタイル', st_skin_classic: 'クラシック', st_skin_modern: 'ミニマル' },
    ZH: { st_skin: '界面样式', st_skin_classic: '经典', st_skin_modern: '极简' },
    KO: { st_skin: '인터페이스 스타일', st_skin_classic: '클래식', st_skin_modern: '미니멀' },
    HE: { st_skin: 'סגנון הממשק', st_skin_classic: 'קלאסי', st_skin_modern: 'מינימליסטי' },
    AR: { st_skin: 'نمط الواجهة', st_skin_classic: 'كلاسيكي', st_skin_modern: 'بسيط' },
    HI: { st_skin: 'इंटरफ़ेस शैली', st_skin_classic: 'क्लासिक', st_skin_modern: 'मिनिमल' },
    ID: { st_skin: 'Gaya antarmuka', st_skin_classic: 'Klasik', st_skin_modern: 'Minimalis' },
    VI: { st_skin: 'Kiểu giao diện', st_skin_classic: 'Cổ điển', st_skin_modern: 'Tối giản' },
    TH: { st_skin: 'สไตล์อินเทอร์เฟซ', st_skin_classic: 'คลาสสิก', st_skin_modern: 'มินิมอล' }
};
Object.keys(i18n).forEach(lg => {
    const pack = I18N_SKIN[lg] || I18N_SKIN.EN;
    Object.keys(I18N_SKIN.EN).forEach(k => { if (i18n[lg]) i18n[lg][k] = pack[k]; });
});
const SUPPORT_REF_URL = 'https://chaturbate.com/in/?tour=g4pe&campaign=opzS6&track=default';
const SUPPORT_TXT = {
    RU: { or: "Chaturbate · реферальная ссылка", sub: "Создать аккаунт и поддержать автора скрипта", btn: "Открыть", note: "Это реферальная ссылка автора скрипта. Если вы зарегистрируетесь на Chaturbate по ней, автор получит вознаграждение от Chaturbate. Для вас это бесплатно: цена и условия не меняются. Ссылка откроется в новой вкладке только после вашего нажатия." },
    EN: { or: "Chaturbate · referral link", sub: "Create an account and support the script author", btn: "Open", note: "This is the script author's referral link. If you sign up on Chaturbate through it, the author receives a reward from Chaturbate. It's free for you: the price and terms don't change. The link opens in a new tab only after you click." },
    UK: { or: "Chaturbate · реферальне посилання", sub: "Створити акаунт і підтримати автора скрипта", btn: "Відкрити", note: "Це реферальне посилання автора скрипта. Якщо ви зареєструєтеся на Chaturbate за ним, автор отримає винагороду від Chaturbate. Для вас це безкоштовно: ціна й умови не змінюються. Посилання відкриється в новій вкладці лише після вашого натискання." },
    ES: { or: "Chaturbate · enlace de referido", sub: "Crear una cuenta y apoyar al autor del script", btn: "Abrir", note: "Este es el enlace de referido del autor del script. Si te registras en Chaturbate con él, el autor recibe una recompensa de Chaturbate. Para ti es gratis: el precio y las condiciones no cambian. El enlace se abre en una pestaña nueva solo cuando lo pulsas." },
    DE: { or: "Chaturbate · Empfehlungslink", sub: "Konto erstellen und den Autor des Skripts unterstützen", btn: "Öffnen", note: "Das ist der Empfehlungslink des Skript-Autors. Wenn du dich über ihn bei Chaturbate registrierst, erhält der Autor eine Vergütung von Chaturbate. Für dich ist das kostenlos: Preis und Bedingungen ändern sich nicht. Der Link öffnet sich nur nach deinem Klick in einem neuen Tab." },
    FR: { or: "Chaturbate · lien de parrainage", sub: "Créer un compte et soutenir l’auteur du script", btn: "Ouvrir", note: "Ceci est le lien de parrainage de l’auteur du script. Si vous vous inscrivez sur Chaturbate via ce lien, l’auteur reçoit une rémunération de Chaturbate. C’est gratuit pour vous : le prix et les conditions ne changent pas. Le lien s’ouvre dans un nouvel onglet uniquement après votre clic." },
    PT: { or: "Chaturbate · link de indicação", sub: "Criar uma conta e apoiar o autor do script", btn: "Abrir", note: "Este é o link de indicação do autor do script. Se você se cadastrar no Chaturbate por ele, o autor recebe uma recompensa do Chaturbate. Para você é grátis: o preço e as condições não mudam. O link só abre em uma nova aba depois do seu clique." },
    IT: { or: "Chaturbate · link di affiliazione", sub: "Crea un account e sostieni l’autore dello script", btn: "Apri", note: "Questo è il link di affiliazione dell’autore dello script. Se ti registri su Chaturbate tramite questo link, l’autore riceve un compenso da Chaturbate. Per te è gratis: prezzo e condizioni non cambiano. Il link si apre in una nuova scheda solo dopo il tuo clic." },
    PL: { or: "Chaturbate · link polecający", sub: "Załóż konto i wesprzyj autora skryptu", btn: "Otwórz", note: "To link polecający autora skryptu. Jeśli zarejestrujesz się na Chaturbate przez ten link, autor otrzyma wynagrodzenie od Chaturbate. Dla ciebie jest to bezpłatne: cena i warunki się nie zmieniają. Link otworzy się w nowej karcie dopiero po kliknięciu." },
    TR: { or: "Chaturbate · referans bağlantısı", sub: "Hesap oluştur ve betiğin yazarını destekle", btn: "Aç", note: "Bu, betiğin yazarının referans bağlantısıdır. Bu bağlantı üzerinden Chaturbate’e kayıt olursanız, yazar Chaturbate’ten bir ödül alır. Sizin için ücretsizdir: fiyat ve koşullar değişmez. Bağlantı yalnızca tıkladığınızda yeni sekmede açılır." },
    NL: { or: "Chaturbate · uitnodigingslink", sub: "Maak een account aan en steun de auteur van het script", btn: "Openen", note: "Dit is de uitnodigingslink van de scriptauteur. Als je je via deze link registreert op Chaturbate, ontvangt de auteur een vergoeding van Chaturbate. Voor jou is het gratis: prijs en voorwaarden veranderen niet. De link opent pas na jouw klik in een nieuw tabblad." },
    CS: { or: "Chaturbate · odkaz s doporučením", sub: "Vytvořit účet a podpořit autora skriptu", btn: "Otevřít", note: "Toto je odkaz autora skriptu s doporučením. Pokud se přes něj zaregistrujete na Chaturbate, autor získá odměnu od Chaturbate. Pro vás je to zdarma: cena ani podmínky se nemění. Odkaz se otevře v nové záložce až po kliknutí." },
    RO: { or: "Chaturbate · link de recomandare", sub: "Creează un cont și susține autorul scriptului", btn: "Deschide", note: "Acesta este linkul de recomandare al autorului scriptului. Dacă te înregistrezi pe Chaturbate prin el, autorul primește o recompensă de la Chaturbate. Pentru tine este gratuit: prețul și condițiile nu se schimbă. Linkul se deschide într-o filă nouă doar după ce dai clic." },
    HU: { or: "Chaturbate · meghívólink", sub: "Fiók létrehozása és a szkript szerzőjének támogatása", btn: "Megnyitás", note: "Ez a szkript szerzőjének meghívólinkje. Ha ezen keresztül regisztrálsz a Chaturbate-en, a szerző jutalmat kap a Chaturbate-től. Neked ingyenes: az ár és a feltételek nem változnak. A link csak a kattintásod után nyílik meg új lapon." },
    BG: { or: "Chaturbate · реферална връзка", sub: "Създайте акаунт и подкрепете автора на скрипта", btn: "Отвори", note: "Това е препоръчителната връзка на автора на скрипта. Ако се регистрирате в Chaturbate през нея, авторът получава възнаграждение от Chaturbate. За вас това е безплатно: цената и условията не се променят. Връзката се отваря в нов раздел само след като натиснете." },
    EL: { or: "Chaturbate · σύνδεσμος παραπομπής", sub: "Δημιουργία λογαριασμού και υποστήριξη του δημιουργού του script", btn: "Άνοιγμα", note: "Αυτός είναι ο σύνδεσμος παραπομπής του δημιουργού του script. Αν εγγραφείτε στο Chaturbate μέσω αυτού, ο δημιουργός λαμβάνει αμοιβή από το Chaturbate. Για εσάς είναι δωρεάν: η τιμή και οι όροι δεν αλλάζουν. Ο σύνδεσμος ανοίγει σε νέα καρτέλα μόνο μετά το κλικ σας." },
    JA: { or: "Chaturbate · 紹介リンク", sub: "アカウントを作成して、スクリプトの作者を支援", btn: "開く", note: "これはスクリプト作者の紹介リンクです。このリンクからChaturbateに登録すると、作者はChaturbateから報酬を受け取ります。あなたは無料で、料金や条件は変わりません。リンクはクリックしたときだけ新しいタブで開きます。" },
    ZH: { or: "Chaturbate · 推荐链接", sub: "创建账号并支持脚本作者", btn: "打开", note: "这是脚本作者的推荐链接。如果你通过它注册 Chaturbate,作者会从 Chaturbate 获得奖励。对你来说完全免费,价格和条款不会改变。链接只会在你点击后于新标签页打开。" },
    KO: { or: "Chaturbate · 추천 링크", sub: "계정을 만들고 스크립트 제작자를 후원하기", btn: "열기", note: "스크립트 작성자의 추천 링크입니다. 이 링크로 Chaturbate에 가입하면 작성자가 Chaturbate로부터 보상을 받습니다. 사용자에게는 무료이며 가격과 조건은 달라지지 않습니다. 링크는 클릭한 경우에만 새 탭에서 열립니다." },
    HE: { or: "Chaturbate · קישור הפניה", sub: "יצירת חשבון ותמיכה ביוצר הסקריפט", btn: "פתח", note: "זה קישור ההפניה של יוצר הסקריפט. אם תירשמו ל-Chaturbate דרכו, היוצר יקבל תגמול מ-Chaturbate. עבורכם זה בחינם: המחיר והתנאים לא משתנים. הקישור נפתח בלשונית חדשה רק לאחר הלחיצה שלכם." },
    AR: { or: "Chaturbate · رابط الإحالة", sub: "أنشئ حسابًا وادعم مطوّر السكربت", btn: "فتح", note: "هذا رابط الإحالة الخاص بمطوّر السكربت. إذا سجّلت في Chaturbate عبره، يحصل المطوّر على مكافأة من Chaturbate. هذا مجاني بالنسبة لك: السعر والشروط لا يتغيران. يُفتح الرابط في علامة تبويب جديدة فقط بعد نقرك عليه." },
    HI: { or: "Chaturbate · रेफ़रल लिंक", sub: "खाता बनाएँ और स्क्रिप्ट के लेखक का समर्थन करें", btn: "खोलें", note: "यह स्क्रिप्ट के लेखक का रेफ़रल लिंक है। अगर आप इसके ज़रिए Chaturbate पर रजिस्टर करते हैं, तो लेखक को Chaturbate से पुरस्कार मिलता है। आपके लिए यह मुफ़्त है: कीमत और शर्तें नहीं बदलतीं। लिंक आपके क्लिक करने के बाद ही नए टैब में खुलता है।" },
    ID: { or: "Chaturbate · tautan referral", sub: "Buat akun dan dukung pembuat skrip", btn: "Buka", note: "Ini adalah tautan referral pembuat skrip. Jika kamu mendaftar di Chaturbate lewat tautan ini, pembuat menerima imbalan dari Chaturbate. Untukmu ini gratis: harga dan ketentuan tidak berubah. Tautan hanya terbuka di tab baru setelah kamu mengkliknya." },
    VI: { or: "Chaturbate · liên kết giới thiệu", sub: "Tạo tài khoản và ủng hộ tác giả của script", btn: "Mở", note: "Đây là liên kết giới thiệu của tác giả tập lệnh. Nếu bạn đăng ký Chaturbate qua liên kết này, tác giả sẽ nhận phần thưởng từ Chaturbate. Với bạn thì hoàn toàn miễn phí: giá và điều khoản không thay đổi. Liên kết chỉ mở trong tab mới sau khi bạn nhấn." },
    TH: { or: "Chaturbate · ลิงก์แนะนำ", sub: "สร้างบัญชีและสนับสนุนผู้สร้างสคริปต์", btn: "เปิด", note: "นี่คือลิงก์แนะนำของผู้สร้างสคริปต์ หากคุณสมัคร Chaturbate ผ่านลิงก์นี้ ผู้สร้างจะได้รับรางวัลจาก Chaturbate สำหรับคุณไม่มีค่าใช้จ่าย ราคาและเงื่อนไขไม่เปลี่ยนแปลง ลิงก์จะเปิดในแท็บใหม่ก็ต่อเมื่อคุณกดเท่านั้น" }
};
const supT = k => (SUPPORT_TXT[AppState.lang] || SUPPORT_TXT.EN)[k];
const openSupportLink = () => { try { window.open(SUPPORT_REF_URL, '_blank', 'noopener,noreferrer'); } catch {} };

const L = () => i18n[AppState.lang] || i18n.EN;
const fmtAgo = ts => {
    const l = L(), m = Math.max(0, Date.now() - ts) / 60000;
    if (m < 1) return l.seen_now;
    const n = m < 60 ? `${Math.floor(m)} ${l.u_m}` : m < 1440 ? `${Math.floor(m / 60)} ${l.u_h}` : `${Math.floor(m / 1440)} ${l.u_d}`;
    return l.ago.replace('{n}', n);
};

const CSS_NOSEL = `*{-webkit-tap-highlight-color:transparent}.panel,.panel *{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}.panel button,.panel [role=button],.panel div,.panel span{outline:0}.panel input,.panel textarea{-webkit-user-select:text;user-select:text}`;
const CSS_UI = `:host{--th:${AppState.theme};pointer-events:none;position:fixed;left:0;right:0;bottom:0;top:auto;width:100%;height:0;z-index:2147483647;font-family:system-ui,-apple-system,sans-serif}*{box-sizing:border-box;margin:0;padding:0}.h-btn,.btn,.restore,.ghost,.vico,.z-btn,.bm-close,.bm-del,.bm-act-btn,.resize-handle{display:flex;align-items:center;justify-content:center}.panel{user-select:none;touch-action:none;position:absolute;width:304px;padding:12px;border-radius:14px;pointer-events:auto;background:rgba(15,15,18,.95);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}
.grid-2{display:grid;grid-template-columns:repeat(2,1fr);gap:6px;margin-top: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:8px;padding:6px 8px;margin-bottom:6px}.drag{flex:0 0 0;width:0;align-self:stretch;min-height:0;min-width:0;padding:0!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:rgba(255,255,255,.6);font-size:9.5px;font-weight:600;letter-spacing:1px;text-transform:uppercase;cursor:grab;display:flex;align-items:center;justify-content:flex-start;padding-left:4px;user-select:none;touch-action:none!important;margin-right:auto}.drag:active{cursor:grabbing}.stats{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1px;background:rgba(0,0,0,.5);color:#fff;font-size:9px;min-width:30px;height:26px;padding:2px 4px;border-radius:4px;font-weight:bold;margin-right:2px;box-sizing:border-box;line-height:1;flex-shrink:0}.hdr-btns{display:flex;gap:5px;align-items:center;flex:1 1 auto;margin-left:0;justify-content:space-between}.h-btn{width:24px;height:24px;background:rgba(255,255,255,.05);border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:12px;transition:.2s}.h-btn:hover{background:rgba(255,255,255,.2);transform:translateY(-1px)}.h-btn.close:hover{background:#e74c3c;transform:none}.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:8px;padding:8px 2px 6px;font-size:18px;cursor:pointer;flex-direction:column;width:100%;transition:all .2s ease;outline:none}.btn:hover{background:rgba(255,255,255,.15);transform:translateY(-2px);border-color:rgba(255,255,255,.2)}.btn:active{transform:scale(.92)}.btn.on{background:var(--th);box-shadow:0 4px 10px rgba(0,0,0,.3);border-color:transparent}.btn sub{font-size:8px;font-weight:600;margin-top:4px;letter-spacing:.5px;text-transform:uppercase;opacity:.8;pointer-events:none;text-align:center;word-break:break-word;line-height:1}.restore{width:48px;height:48px;border-radius:22px;font-size:22px;background:var(--th);border:2px solid rgba(255,255,255,.3);display:none;cursor:pointer;pointer-events:auto;box-shadow:0 4px 15px rgba(0,0,0,.6);transition:.2s;touch-action:none!important}.restore:hover{transform:scale(1.05)}.ghost{position:absolute;bottom:80px;right:16px;width:44px;height:44px;border-radius:20px;font-size:20px;background:var(--th);border:2px solid rgba(255,255,255,.35);display:none;cursor:pointer;pointer-events:auto;box-shadow:0 4px 15px rgba(0,0,0,.6);transition:transform .2s,opacity .2s;touch-action:manipulation;z-index:2147483647}.ghost:hover,.ghost:active{transform:scale(1.1)}.v-cap{display:flex;align-items:center;gap:8px;background:rgba(0,0,0,.4);border-radius:10px;padding:8px 12px;border:1px solid rgba(255,255,255,.05)}.vico{color:#fff;font-size:16px;cursor:pointer;flex-shrink:0;filter:drop-shadow(0 2px 4px rgba(0,0,0,.5))}.vslider{-webkit-appearance:none;appearance:none;flex:1;height:3px;border-radius:1px;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:4px;padding:2px}.z-btn{width:20px;height:20px;background:rgba(255,255,255,.05);border:none;border-radius:3px;color:#fff;cursor:pointer;font-size:14px;transition:.2s;outline:none}.z-btn:hover{background:rgba(255,255,255,.2)}.s-val{font-size:9px;color:#ccc;min-width:24px;text-align:center;font-weight:bold}.hotkeys{font-size:9px;color:rgba(255,255,255,.4);text-align:center;font-weight:500;letter-spacing:.5px;padding-top:2px}.bm-overlay{position:fixed;inset:0;background:rgba(0,0,0,.8);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:.3s;backdrop-filter:blur(5px);z-index:2147483647}.bm-overlay.open{opacity:1;pointer-events:auto}.bm-modal{width:370px;max-width:96vw;height:90vh;max-height:90vh;min-height:450px;background:rgb(20,20,25);border:1px solid rgba(255,255,255,.1);border-radius:14px;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 10px 40px rgba(0,0,0,.8)}.bm-hdr{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:rgba(0,0,0,.4);border-bottom:1px solid rgba(255,255,255,.05);flex-shrink:0}.bm-title{color:#fff;font-weight:bold;font-size:13px;white-space:nowrap;flex-shrink:0}.bm-close{background:none;border:none;color:#aaa;font-size:26px;cursor:pointer;padding:2px 8px;line-height:1;min-width:32px;min-height:32px}.bm-close:hover{color:#fff}.bm-list{flex:1;min-height:0;overflow-y:scroll;scrollbar-gutter:stable;padding:12px;display:flex;flex-direction:column;gap:10px}.bm-item{display:flex;flex-direction:column;gap:8px;padding:10px;background:rgba(255,255,255,.05);border-radius:10px;transition:.2s}.bm-item:hover{background:rgba(255,255,255,.08)}.bm-item-top{display:flex;align-items:center;justify-content:space-between;gap:10px}.bm-link{display:flex;align-items:center;gap:10px;text-decoration:none;color:#fff;flex:1;overflow:hidden}.bm-link:hover .bm-name{color:var(--th)}.bm-avatar{width:42px;height:42px;border-radius:50%;object-fit:cover;aspect-ratio:1/1;background:#333;border:1px solid rgba(255,255,255,.1);flex-shrink:0}.bm-name{font-size:15px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transition:.2s}.bm-del{width:30px;height:30px;border-radius:4px;background:rgba(231,76,60,.2);color:#e74c3c;border:none;cursor:pointer;transition:.2s;flex-shrink:0}.bm-del:hover{background:#e74c3c;color:#fff}.bm-del svg,.bm-modal .bm-del svg{width:14px;height:14px;margin:0!important;display:block;vertical-align:baseline}.bm-act-btn{height:28px;padding:0 8px;border-radius:4px;background:rgba(255,255,255,.06);color:#ccc;border:1px solid rgba(255,255,255,.08);gap:4px;cursor:pointer;font-size:11px;font-weight:600;transition:.2s}.bm-act-btn:hover{background:rgba(255,255,255,.15);color:#fff;border-color:rgba(255,255,255,.2)}.bm-act-btn svg{width:13px;height:13px;flex-shrink:0}.bm-note{width:100%;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.1);color:#fff;border-radius:6px;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:6px;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:6px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:#ddd;font-size:13px;font-weight:600;cursor:pointer;transition:.2s}.bm-btn:hover{background:rgba(255,255,255,.15);color:#fff}.bm-empty{text-align:center;padding:30px 10px;color:#777;font-size:14px;grid-column:1 / -1}.bm-gallery-mode{display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr));align-content:start;gap:10px;padding:10px}.bm-item{min-width:0}.bm-gallery-mode .bm-item{min-width:0;padding:10px;cursor:pointer;display:flex;flex-direction:column;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);border-radius:10px;position:relative;gap:6px}.bm-gallery-mode .bm-item:hover{transform:translateY(-2px);z-index:10;border-color:var(--th);background:rgba(255,255,255,.08);box-shadow:0 4px 12px rgba(0,0,0,.5)}.bm-gallery-mode .bm-topic,.bm-gallery-mode .bm-note{display:none!important}.bm-gallery-mode .bm-item-top{display:contents}.bm-gallery-mode .bm-link{display:contents}.bm-gallery-mode .bm-link>div{order:1;display:flex;align-items:center;width:calc(100% - 32px);max-width:calc(100% - 32px);pointer-events:none}.bm-gallery-mode .bm-del{order:2;position:absolute;top:6px;right:6px;width:24px;height:24px;z-index:2;margin:0;border-radius:4px}.bm-gallery-mode .bm-avatar{order:3;width:100%!important;height:auto!important;aspect-ratio:16/9!important;border-radius:6px;object-fit:cover;object-position:center;border:1px solid rgba(0,0,0,.3);pointer-events:none;margin:0;display:block!important;filter:brightness(.75)}.bm-gallery-mode .bm-item:has(img[data-frame]) .bm-avatar{display:none!important}.bm-gallery-mode img[data-frame]{order:3;width:100%!important;height:auto!important;aspect-ratio:16/9!important;border-radius:6px;object-fit:cover;border:1px solid rgba(0,0,0,.3)!important;margin:0!important;pointer-events:none;display:block!important}.bm-gallery-mode .bm-actions-row{order:4;display:grid!important;width:100%;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:auto;padding-top:2px}.bm-gallery-mode .bm-act-btn{height:28px;padding:0;background:rgba(0,0,0,.4);border-radius:4px;border:1px solid rgba(255,255,255,.05)}.bm-gallery-mode .bm-act-btn span{display:none}.bm-gallery-mode .bm-act-btn svg{margin:0;width:14px;height:14px}.bm-gallery-mode .bm-name{font-size:12px;color:#eee;font-weight:bold;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media (min-width:768px){.bm-modal{width:780px!important;max-width:92vw!important;height:85vh!important;max-height:85vh!important}.bm-list{display:grid!important;grid-template-columns:repeat(2,minmax(0,1fr))!important;align-content:start!important;gap:12px!important}.bm-gallery-mode{grid-template-columns:repeat(3,minmax(0,1fr))!important;gap:12px!important}}@media (min-width:1200px){.bm-modal{width:920px!important}.bm-gallery-mode{grid-template-columns:repeat(4,minmax(0,1fr))!important}}.bm-list::-webkit-scrollbar{width:6px}.bm-list::-webkit-scrollbar-thumb{background:rgba(255,255,255,.2);border-radius:2px}@keyframes pulseRec{0%,100%{background:rgba(255,255,255,.06);box-shadow:none}50%{background:#c0392b;box-shadow:0 2px 8px rgba(0,0,0,.5)}}.rec-active{animation:pulseRec 1.5s infinite!important;border-color:transparent!important}.resize-handle{position:absolute;right:2px;bottom:2px;width:16px;height:16px;cursor:nwse-resize;touch-action:none!important;opacity:.4;transition:opacity .2s;z-index:10}.resize-handle:hover{opacity:1}.resize-handle::after{content:none}.panel.mini .resize-handle{display:none!important}.rs-edge{position:absolute;z-index:9;touch-action:none!important}.rs-n{top:-5px;left:14px;right:14px;height:12px;cursor:ns-resize}.rs-s{bottom:-5px;left:14px;right:14px;height:12px;cursor:ns-resize}.rs-w{left:-5px;top:14px;bottom:14px;width:12px;cursor:ew-resize}.rs-e{right:-5px;top:14px;bottom:14px;width:12px;cursor:ew-resize}.rs-nw{top:-5px;left:-5px;width:18px;height:18px;cursor:nwse-resize}.rs-ne{top:-5px;right:-5px;width:18px;height:18px;cursor:nesw-resize}.rs-sw{bottom:-5px;left:-5px;width:18px;height:18px;cursor:nesw-resize}.panel.mini .rs-edge{display:none!important}@media(pointer:coarse){.rs-n,.rs-s{height:22px}.rs-n{top:-8px}.rs-s{bottom:-8px}.rs-w,.rs-e{width:22px}.rs-w{left:-8px}.rs-e{right:-8px}.rs-nw,.rs-ne,.rs-sw{width:28px;height:28px}.rs-nw{top:-8px;left:-8px}.rs-ne{top:-8px;right:-8px}.rs-sw{bottom:-8px;left:-8px}}.bm-prev:not([data-frame]){display:none!important}.bm-prev.bm-bad{visibility:hidden}.bm-stat{width:8px;height:8px;border-radius:50%;background:#888;margin-right:8px;flex-shrink:0;box-shadow:0 0 4px rgba(0,0,0,.5);transition:.3s}.bm-nwrap{display:flex;align-items:center;min-width:0;flex:1}.bm-acts{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:4px}.bm-topic{font-size:11px;color:#ddd;background:rgba(0,0,0,.35);padding:6px 10px;border-radius:4px;margin:6px 0;word-break:break-word;white-space:pre-wrap;display:none;border:1px solid rgba(255,255,255,.08)}.bm-prev{width:100%;max-height:180px;object-fit:cover;border-radius:3px;margin-top:8px;display:none;border:1px solid rgba(255,255,255,.1);cursor:pointer}.bm-tbtn{display:flex;align-items:center;font-size:11px;font-weight:bold;padding:4px 10px;border-radius:10px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.05);color:#fff;cursor:pointer;transition:.2s;margin-right:4px;flex-shrink:0;box-sizing:border-box}.bm-tbtn.on{background:var(--th)}.bm-hbtn{font-size:11px;cursor:pointer;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.15);color:#fff;border-radius:4px;display:flex;align-items:center;padding:3px 7px}.bm-sort-wrap{display:flex;align-items:center;gap:4px;margin-left:auto;margin-right:6px}.bm-tags-wrap{display:none;flex-wrap:wrap;gap:6px;padding:4px 12px 10px;background:rgba(0,0,0,.4);flex-shrink:0;border-bottom:1px solid rgba(255,255,255,.05)}.bm-modal button svg{vertical-align:middle}.bm-title svg{margin-right:4px;vertical-align:-2px}.bm-twrap{display:flex;align-items:center;gap:2px;flex:1}.bm-srch-in{margin-bottom:12px;font-size:18px;padding:12px;text-align:center;font-weight:bold}.bm-srch-btn{margin-bottom:8px;font-size:15px;padding:12px;transition:all .3s ease;border-left:4px solid transparent}.hd-sec{grid-column:1 / -1;color:#888;font-size:11px;font-weight:700;letter-spacing:1px;text-transform:uppercase;padding:6px 4px 0}.hd-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 12px;background:rgba(255,255,255,.05);border-radius:8px;color:#fff;font-size:13px;cursor:pointer;user-select:none;transition:.2s}.hd-row:hover{background:rgba(255,255,255,.09)}.hd-sw{position:relative;width:40px;height:22px;border-radius:9px;background:rgba(255,255,255,.15);flex-shrink:0;transition:.2s}.hd-sw::after{content:'';position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;transition:.2s}.hd-row.on .hd-sw{background:var(--th)}.hd-row.on .hd-sw::after{left:20px}.hd-rule{cursor:default}.hd-sel{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px;color:#bbb;font-family:monospace}.hd-bulk{grid-column:1 / -1;display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.hd-bulk .bm-btn{padding:9px 4px;font-size:12px}.hd-txt{display:flex;flex-direction:column;gap:2px;min-width:0}.hd-txt b{font-weight:600}.hd-txt small{color:#999;font-size:11px;font-weight:400;line-height:1.25}.hd-row.na{opacity:1}.hd-na{color:#e0a040!important}.hd-hint{grid-column:1 / -1;color:#777;font-size:11px;padding:0 4px}.bm-overlay.peek{opacity:0!important;background:transparent!important;backdrop-filter:none!important;transition:opacity .12s!important}.hd-empty{grid-column:1 / -1;text-align:center;color:#777;font-size:13px;padding:10px}.pick-box{position:fixed;display:none;pointer-events:none;border:2px solid #e74c3c;background:rgba(231,76,60,.12);box-sizing:border-box;z-index:2147483646}.pick-bar{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);display:none;flex-direction:column;gap:8px;width:min(92vw,360px);padding:10px;background:rgba(15,15,18,.95);border:1px solid rgba(255,255,255,.15);border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.7);pointer-events:auto;z-index:2147483647}.pick-bar.open{display:flex}.pick-info{color:#ddd;font-size:11px;font-family:monospace;word-break:break-all;max-height:56px;overflow:hidden}.pick-btns{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}.bm-grp{width:100%;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.1);color:#ddd;border-radius:6px;padding:7px 8px;font-size:12px;font-family:inherit;outline:none}.bm-grp option{background:#1b1b1f;color:#fff}.bm-gallery-mode .bm-grp{display:none!important}.bm-seen{flex-shrink:0;margin-left:8px;font-size:10px;color:#8a8a90;white-space:nowrap}.bm-modal.rs-modal{width:380px!important;max-width:94vw!important;height:auto!important;min-height:0!important;max-height:90vh!important}.rs-modal .bm-list{display:flex!important;flex-direction:column!important;flex:0 1 auto;overflow-y:auto;scrollbar-gutter:auto;gap:8px}.rs-qs{display:flex;flex-direction:column;gap:6px}.rs-qs .bm-btn{text-align:left}.rs-chips{display:flex;flex-wrap:wrap;gap:6px}.rs-chips .bm-tbtn{margin-right:0;padding:7px 12px;font-size:12px}.rs-info{font-size:12px;color:#ddd;min-height:16px;padding:2px 4px}.rs-note{font-size:11px;color:#8a8a90;padding:0 4px;line-height:1.35}.bm-btn.sel{background:var(--th);color:#fff;border-color:transparent}
.lp-modal .bm-list.lp-grid{display:grid!important;flex-direction:initial!important;grid-template-columns:repeat(2,minmax(0,1fr))!important;gap:8px!important;padding:12px!important}
.lp-btn{display:flex;align-items:center;justify-content:flex-start;gap:10px;min-height:46px;padding:8px 10px;border-radius:10px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:#ddd;font-size:13px;font-weight:600;text-align:left;cursor:pointer;transition:.2s;min-width:0}
.lp-btn .lp-code{flex:0 0 44px;width:44px;min-width:44px;box-sizing:border-box;height:26px;padding:0;font-variant-numeric:tabular-nums;display:flex;align-items:center;justify-content:center;border-radius:6px;background:rgba(255,255,255,.1);color:#fff;font-size:11px;font-weight:800;letter-spacing:.5px}
.lp-btn .lp-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lp-btn.lp-auto{grid-column:1/-1}
.lp-btn.sel{background:var(--th);color:#fff;border-color:transparent}
.lp-btn.sel .lp-code{background:rgba(0,0,0,.25)}

.bm-overlay.fav-ov{backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.8);transition:opacity .18s ease}
.fav-modal{width:min(980px,96vw)!important;max-width:96vw!important;height:88vh!important;max-height:88vh!important;min-height:0!important;border-radius:18px;background:linear-gradient(180deg,rgba(30,30,37,.97),rgba(15,15,19,.98));border:1px solid rgba(255,255,255,.08);box-shadow:0 24px 70px rgba(0,0,0,.7)}
.fav-modal .bm-hdr{position:relative;flex-wrap:wrap;justify-content:flex-start;gap:8px;padding:14px 16px 12px;padding-inline-end:58px;background:rgba(255,255,255,.03);border-bottom:1px solid rgba(255,255,255,.06)}
.fav-modal .bm-title{display:flex;align-items:center;font-size:17px;font-weight:800;letter-spacing:.2px}
.fav-modal .bm-close{position:absolute;top:10px;inset-inline-end:12px;width:36px;height:36px;min-width:0;min-height:0;padding:0;border-radius:50%;background:rgba(255,255,255,.07);font-size:22px;color:#ccc}
.fav-modal .bm-close:hover{background:rgba(231,76,60,.85);color:#fff}
.fav-modal .bm-hdr>.bm-hbtn{margin-left:0!important}
.fav-modal .bm-hdr .bm-title{flex:1 1 auto;min-width:0}.fav-modal .bm-tools{order:3;flex:1 0 100%;display:flex;align-items:center;gap:8px;justify-content:center;justify-content:safe center;overflow-x:auto;scrollbar-width:none}.fav-modal .bm-tools::-webkit-scrollbar{display:none}.fav-modal .bm-tools>*{flex:0 0 auto;margin:0!important}.fav-modal .bm-tools .bm-hbtn{height:36px;min-width:40px;justify-content:center}.fav-modal .bm-tools .bm-hbtn.on{background:var(--th);border-color:transparent}.fav-modal .bm-tools .bm-sort-wrap .bm-hbtn.on{background:transparent}
.fav-modal .bm-hbtn{display:inline-flex;align-items:center;justify-content:center;line-height:1}.fav-modal .bm-hbtn svg{display:block;flex:none;margin:0!important}.fav-modal .bm-hdr{padding-inline-end:16px!important}.fav-modal .bm-grp-btn{display:flex!important;align-items:center;justify-content:space-between!important;gap:8px;width:100%;text-align:start!important}.fav-modal .bm-grp-btn>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fav-modal .bm-grp-arr{flex:none}.fav-modal .bm-tags-wrap{flex-direction:column!important;flex-wrap:nowrap!important;align-items:stretch;gap:10px!important;max-height:none!important;overflow:visible!important;padding:12px 16px!important}.fav-modal .bm-seg{display:flex;border:1px solid rgba(255,255,255,.12);border-radius:8px;overflow:hidden;background:rgba(255,255,255,.05)}.fav-modal .bm-seg .bm-tbtn{flex:1 1 0;min-width:0;height:36px;margin:0;padding:0 8px;border:0;border-radius:0;background:transparent;box-shadow:none;display:flex;align-items:center;justify-content:center;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fav-modal .bm-seg .bm-tbtn+.bm-tbtn{border-left:1px solid rgba(255,255,255,.12)}.fav-modal .bm-seg .bm-tbtn.on{background:var(--th)}.fav-modal .bm-seg svg{display:block;flex:none;margin:0;width:16px;height:16px}.fav-modal .bm-chips{display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;margin:0 -16px;padding:0 16px}.fav-modal .bm-chips::-webkit-scrollbar{display:none}.fav-modal .bm-chips .bm-tbtn{flex:none;padding:6px 14px}.fav-modal .bm-hdr .bm-title{padding-inline-end:48px;min-height:38px}.fav-modal .bm-hdr{row-gap:10px}.fav-modal .bm-tools{overflow:visible;gap:10px}.fav-modal .bm-tools>.bm-sort-wrap{flex:1 1 auto!important;display:flex}.fav-modal .bm-tools .bm-sort-wrap .bm-hbtn:first-child{flex:1 1 auto}.fav-modal .bm-tools .bm-sort-wrap .bm-hbtn:last-child{width:46px}.fav-modal .bm-setp{padding:2px 14px;gap:0;border:1px solid rgba(255,255,255,.08)}.fav-modal .bm-setrow+.bm-setrow{border-top:1px solid rgba(255,255,255,.06)}.fav-modal .bm-tools .bm-gear{width:48px;flex:none}
.fav-modal .bm-gear{width:48px;padding:0}.fav-modal .bm-gear svg{width:18px;height:18px}.fav-modal .bm-setp{display:none;flex:1 0 100%;order:4;flex-direction:column;gap:2px;padding:4px 10px;border-radius:10px;background:rgba(0,0,0,.28)}.fav-modal .bm-setp.open{display:flex}.fav-modal .bm-setrow{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:44px;font-size:14px;color:#ddd}.fav-modal .bm-sw{position:relative;flex:none;width:46px;height:26px;padding:0;border:0;border-radius:11px;background:rgba(255,255,255,.18);cursor:pointer;transition:.2s}.fav-modal .bm-sw::after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:#fff;transition:.2s}.fav-modal .bm-sw.on{background:var(--th)}.fav-modal .bm-sw.on::after{left:23px}.fav-modal .bm-spd{height:30px;min-width:52px}.fav-modal.bm-no-topic .bm-topic{display:none!important}.fav-modal.bm-no-grp .bm-grp-btn{display:none!important}
.fav-modal .bm-sort-wrap{gap:0;margin:0;margin-inline-start:auto;border:1px solid rgba(255,255,255,.12);border-radius:8px;overflow:hidden;background:rgba(255,255,255,.05)}
.fav-modal .bm-hbtn{height:30px;padding:0 10px;gap:4px;border-radius:8px;font-weight:600;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);transition:.2s}
.fav-modal .bm-hbtn:hover{background:rgba(255,255,255,.14)}
.fav-modal .bm-sort-wrap .bm-hbtn{border:0;border-radius:0;background:transparent}
.fav-modal .bm-sort-wrap .bm-hbtn:hover{background:rgba(255,255,255,.1)}
.fav-modal .bm-sort-wrap .bm-hbtn+.bm-hbtn{border-left:1px solid rgba(255,255,255,.12)}
.fav-modal .bm-sort-wrap .bm-hbtn:last-child{padding:0 9px}
.fav-modal .bm-sort-wrap .bm-hbtn:last-child svg{margin:0}
.fav-modal .bm-tags-wrap{gap:6px;padding:10px 16px;max-height:92px;overflow-y:auto;background:transparent;border-bottom:1px solid rgba(255,255,255,.06)}
.fav-modal .bm-tbtn{margin:0;padding:6px 12px;border-radius:999px;font-size:12px;font-weight:600;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);transition:.2s}
.fav-modal .bm-tbtn:hover{background:rgba(255,255,255,.14)}
.fav-modal .bm-tbtn.on{background:var(--th);border-color:transparent;box-shadow:0 2px 10px rgba(0,0,0,.35)}
.fav-modal .bm-list{grid-template-columns:repeat(auto-fill,minmax(min(300px,100%),1fr))!important;gap:12px!important;padding:14px!important;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent}
.fav-modal .bm-list.bm-gallery-mode{grid-template-columns:repeat(auto-fill,minmax(min(150px,100%),1fr))!important;gap:10px!important}
@media(min-width:760px){.fav-modal .bm-list.bm-gallery-mode{grid-template-columns:repeat(auto-fill,minmax(min(200px,100%),1fr))!important}}
.fav-modal .bm-item{position:relative;padding:12px 12px 12px 16px;gap:10px;border-radius:14px;background:linear-gradient(180deg,rgba(255,255,255,.065),rgba(255,255,255,.03));border:1px solid rgba(255,255,255,.08);transition:transform .2s,border-color .2s,background .2s,box-shadow .2s}
@media (hover:hover) and (pointer:fine){.fav-modal .bm-item:hover{background:linear-gradient(180deg,rgba(255,255,255,.09),rgba(255,255,255,.05));border-color:rgba(255,255,255,.18)}}
.fav-modal .bm-item,.fav-modal button{-webkit-tap-highlight-color:transparent}
.fav-modal .bm-item[data-st="0"]{--st:rgba(255,255,255,.14)}
.fav-modal .bm-item[data-st="1"]{--st:#2ecc71}
.fav-modal .bm-item[data-st="2"]{--st:#f39c12}
.fav-modal .bm-item[data-st="3"]{--st:#9b59b6}
.fav-modal .bm-item::before{content:"";position:absolute;left:0;top:14px;bottom:14px;width:3px;border-radius:0 2px 2px 0;background:var(--st,rgba(255,255,255,.14));transition:background .3s}
.fav-modal .bm-list:not(.bm-gallery-mode) .bm-avatar{box-sizing:border-box;width:48px;height:48px;padding:0;border:2px solid var(--st,rgba(255,255,255,.14));background-clip:padding-box;box-shadow:none}
.fav-modal .bm-avatar{transition:filter .3s,border-color .3s,box-shadow .3s}
.fav-modal .bm-item[data-st="0"] .bm-avatar{filter:grayscale(.8) brightness(.8)}
.fav-modal .bm-name{font-size:15px;font-weight:700;letter-spacing:.1px}
.fav-modal .bm-seen{font-size:11px;color:#9a9aa2}
.fav-modal .bm-list .bm-stat{display:none}
.fav-modal .bm-list:not(.bm-gallery-mode) .bm-act-btn{background:rgba(0,0,0,.4);border-color:rgba(255,255,255,.05)}
.fav-modal .bm-list:not(.bm-gallery-mode) .bm-act-btn:hover{background:rgba(0,0,0,.25);border-color:rgba(255,255,255,.15)}
.fav-modal .bm-list:not(.bm-gallery-mode) .bm-pin:not(.on),.fav-modal .bm-list:not(.bm-gallery-mode) .bm-del{background:rgba(0,0,0,.55);border-color:rgba(255,255,255,.1)}
.fav-modal .bm-list:not(.bm-gallery-mode) .bm-del:hover{background:#e74c3c}
.fav-modal .bm-list:not(.bm-gallery-mode) .bm-nwrap .bm-view:not(:empty){margin-inline-start:8px;padding:3px 6px;gap:3px;border-radius:7px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);color:#cfcfd6;font-size:11px}
.fav-modal .bm-stat{width:9px;height:9px}
.fav-modal .bm-del{width:32px;height:32px;border-radius:8px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);color:#d9707e}
.fav-modal .bm-del:hover{background:#e74c3c;border-color:transparent;color:#fff}
.fav-modal .bm-acts{gap:6px}
.fav-modal .bm-act-btn{height:32px;border-radius:8px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);color:#d4d4da;font-size:12px}
.fav-modal .bm-act-btn:hover{background:var(--th);border-color:transparent;color:#fff}
.fav-modal .bm-note,.fav-modal .bm-grp{background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:8px 10px;font-size:13px;color:#eee}
.fav-modal .bm-note:focus,.fav-modal .bm-grp:focus{border-color:var(--th);background:rgba(0,0,0,.5);box-shadow:0 0 0 3px rgba(255,255,255,.06)}
.fav-modal .bm-topic{margin:0;padding:8px 10px;border-radius:8px;border:1px solid rgba(255,255,255,.06);border-left:3px solid var(--th);background:rgba(0,0,0,.28);font-size:12px;line-height:1.4;color:#d0d0d6}
.fav-modal .bm-prev{margin-top:0;border-radius:3px;width:100%;aspect-ratio:16/9;max-height:none;object-fit:cover;background:rgba(255,255,255,.05)}
.fav-modal .bm-gallery-mode .bm-item{padding:8px;border-radius:14px;gap:8px}
.fav-modal .bm-gallery-mode .bm-item::before{display:none}
.fav-modal .bm-gallery-mode .bm-item[data-st="1"]{border-color:rgba(46,204,113,.55)}
.fav-modal .bm-gallery-mode .bm-item[data-st="2"]{border-color:rgba(243,156,18,.55)}
.fav-modal .bm-gallery-mode .bm-item[data-st="3"]{border-color:rgba(155,89,182,.6)}
.fav-modal .bm-gallery-mode .bm-avatar{border-radius:10px}
.fav-modal .bm-gallery-mode .bm-name{font-size:12.5px}
.fav-modal .bm-gallery-mode .bm-del{width:26px;height:26px;border-radius:6px;background:rgba(0,0,0,.55);border-color:rgba(255,255,255,.1)}
.fav-modal .bm-gallery-mode .bm-del:hover{background:#e74c3c}
.fav-modal .bm-gallery-mode .bm-act-btn{height:30px;padding:0;background:rgba(0,0,0,.4);border-radius:6px}
.fav-modal .bm-gallery-mode .bm-act-btn:hover{background:var(--th)}
.fav-modal .bm-foot{padding:12px 16px;background:rgba(255,255,255,.03);border-top:1px solid rgba(255,255,255,.06)}
.fav-modal .bm-add{padding:13px;border-radius:10px;font-size:14px;box-shadow:0 6px 18px rgba(0,0,0,.35)}
@supports(background:color-mix(in srgb,red,blue)){.fav-modal .bm-add{background:linear-gradient(135deg,var(--th),color-mix(in srgb,var(--th) 65%,#000))}}
.fav-modal .bm-add:disabled{background:rgba(255,255,255,.08);box-shadow:none;color:#8a8a90;opacity:1}
.fav-modal .bm-empty{padding:70px 12px;color:#8a8a90;font-size:15px}
.fav-modal .bm-empty::before{content:"★";display:block;font-size:44px;line-height:1;margin-bottom:10px;opacity:.35}
.fav-ov,.fav-modal,.fav-modal .bm-list,.fav-modal .hs-pane,.fav-modal .cal-pane,.fav-modal .bm-tags-wrap{overscroll-behavior:contain}
@media(max-width:520px){.fav-modal{width:100vw!important;max-width:100vw!important;height:100%!important;max-height:100%!important;border-radius:0;border:0}.fav-modal .bm-foot{padding-bottom:calc(12px + env(safe-area-inset-bottom,0px))}}
.panel:not(.mini){background:linear-gradient(180deg,rgba(30,30,37,.94),rgba(15,15,19,.96));border:1px solid rgba(255,255,255,.08);border-radius:18px;box-shadow:0 24px 70px rgba(0,0,0,.7),inset 0 1px 1px rgba(255,255,255,.08)}
.panel .hdr{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06);border-radius:10px;padding:6px 8px;margin-bottom:8px}
.panel .drag{color:#9a9aa2;font-weight:700}
.panel .stats{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.08);border-radius:6px}.panel .stats circle{stroke:var(--th)}
.panel .h-btn{width:26px;height:26px;border-radius:50%;background:rgba(255,255,255,.07)}
.panel .h-btn.lang{width:30px;border-radius:8px;background:rgba(255,255,255,.1)}.hdr-btns .h-btn svg{width:19px!important;height:19px!important}.hdr-btns .h-btn.lang{border:1px solid var(--th)!important}.hdr-btns .stats{flex-direction:row!important;width:62px!important;min-width:62px!important;max-width:62px!important;flex:0 0 62px!important;justify-content:center!important;height:26px!important;gap:4px!important;padding:0 6px!important;font-size:12px!important;overflow:hidden!important}.hdr-btns .stats svg{width:15px!important;height:15px!important}.hdr-btns .stats span{font-size:12px!important;line-height:1!important}.row-2 .z-btn svg,.row-4 .z-btn svg{width:17px!important;height:17px!important}
.panel .h-btn:hover{background:rgba(255,255,255,.18)}
.panel .h-btn.close:hover{background:rgba(231,76,60,.9);transform:none}
.panel .btn{border-radius:10px;background:rgba(255,255,255,.065);border:1px solid rgba(255,255,255,.1);color:#e6e6ea}
.favnav{display:none;grid-template-columns:repeat(2,1fr);gap:6px;margin-top:6px}.favnav.on{display:grid}.favnav .btn{height:32px!important;min-height:0!important;padding:0!important}.favnav .btn sub{display:none!important}.favnav .btn svg{width:16px;height:16px}
.panel .btn:hover{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.2)}
.panel .btn.on{background:var(--th);border-color:transparent;color:#fff;box-shadow:0 4px 12px rgba(0,0,0,.35)}
@supports(background:color-mix(in srgb,red,blue)){.panel .btn.on{background:linear-gradient(135deg,var(--th),color-mix(in srgb,var(--th) 65%,#000))}}
.panel .btn.fav-on{position:relative;color:#ffc83d;border-color:rgba(255,200,61,.4)}
.panel .btn.fav-on sub{color:#e6e6ea}
.panel .v-cap{border-radius:10px;background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.08)}
.panel .z-btn{width:24px;height:24px;border-radius:6px;background:rgba(255,255,255,.1)}
.panel .z-btn:hover{background:rgba(255,255,255,.2)}
.panel:not(.mini)::before{content:'';position:absolute;top:-1px;left:16px;right:16px;height:2px;border-radius:0 0 1px 1px;background:linear-gradient(90deg,transparent,var(--th),transparent);pointer-events:none}
.panel .sep{background:linear-gradient(90deg,transparent,var(--th),transparent);opacity:.6}

.bm-overlay:not(.fav-ov){backdrop-filter:none;-webkit-backdrop-filter:none;background:rgba(0,0,0,.8);transition:opacity .18s ease}
.bm-modal:not(.fav-modal){border-radius:18px;background:linear-gradient(180deg,rgb(30,30,37),rgb(15,15,19))!important;border:1px solid rgba(255,255,255,.08);box-shadow:0 24px 70px rgba(0,0,0,.7)}
.bm-modal:not(.fav-modal) .bm-hdr{position:relative;gap:8px;padding:14px 16px 12px;padding-inline-end:58px;background:rgba(255,255,255,.03);border-bottom:1px solid rgba(255,255,255,.06)}
.bm-modal:not(.fav-modal) .bm-title{display:flex;align-items:center;gap:6px;font-size:17px;font-weight:800;letter-spacing:.2px}
.bm-modal:not(.fav-modal) .bm-close{position:absolute;top:50%;transform:translateY(-50%);inset-inline-end:12px;width:36px;height:36px;min-width:0;min-height:0;padding:0;border-radius:50%;background:rgba(255,255,255,.07);font-size:22px;color:#ccc;display:flex;align-items:center;justify-content:center;transition:.2s}
.bm-modal:not(.fav-modal) .bm-close:hover{background:rgba(231,76,60,.85);color:#fff}
.bm-modal:not(.fav-modal) .bm-hbtn{border-radius:8px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);transition:.2s}
.bm-modal:not(.fav-modal) .bm-list{padding:14px;gap:10px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent}
.bm-modal:not(.fav-modal) .bm-btn{border-radius:10px;background:rgba(255,255,255,.065);border:1px solid rgba(255,255,255,.1);color:#e2e2e6}
.bm-modal:not(.fav-modal) .bm-btn:hover{background:rgba(255,255,255,.12);border-color:rgba(255,255,255,.18)}
.bm-modal:not(.fav-modal) .bm-btn:active{background:var(--th);border-color:transparent;color:#fff}
.bm-modal:not(.fav-modal) .bm-btn.sel,.bm-modal:not(.fav-modal) .bm-btn.sel:hover{background:var(--th);border-color:transparent;color:#fff}
.bm-modal:not(.fav-modal) .bm-tbtn{border-radius:999px;font-weight:600;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);transition:.2s}
.bm-modal:not(.fav-modal) .bm-tbtn.on{background:var(--th);border-color:transparent;box-shadow:0 2px 10px rgba(0,0,0,.35)}
.bm-modal:not(.fav-modal) .bm-note{background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.08);border-radius:8px}
.bm-modal:not(.fav-modal) .bm-note:focus{border-color:var(--th);background:rgba(0,0,0,.5);box-shadow:0 0 0 3px rgba(255,255,255,.06)}
.bm-modal:not(.fav-modal) .bm-srch-btn{border-radius:10px}
.bm-modal:not(.fav-modal) .hd-row{border-radius:10px;background:linear-gradient(180deg,rgba(255,255,255,.065),rgba(255,255,255,.03));border:1px solid rgba(255,255,255,.08);transition:.2s}
.bm-modal:not(.fav-modal) .hd-row:hover{border-color:rgba(255,255,255,.18)}
.bm-modal:not(.fav-modal) .hd-sec{color:#9a9aa2}
.bm-modal:not(.fav-modal) .lp-btn{border-radius:10px;background:rgba(255,255,255,.065);border-color:rgba(255,255,255,.1)}
.bm-modal:not(.fav-modal) .bm-foot{padding:12px 16px;background:rgba(255,255,255,.03);border-top:1px solid rgba(255,255,255,.06)}
@media(max-width:520px){.bm-modal:not(.fav-modal){border-radius:16px}}
.bm-modal .bm-close{display:flex;align-items:center;justify-content:center;padding:0!important;line-height:0;text-align:center}
.bm-modal .bm-close svg{display:block;flex:none;margin:0!important;width:16px;height:16px}
.bm-modal .bm-close.bm-back{inset-inline-end:56px!important}
.bm-modal:not(.fav-modal).has-back .bm-hdr{padding-inline-end:104px!important}
.fav-modal.has-back .bm-hdr .bm-title{padding-inline-end:96px!important}
:host([data-rtl]) .bm-back svg{transform:scaleX(-1)}
.bm-overlay.bm-nofade{transition:none!important}
.fav-modal .bm-search-wrap{display:none;padding:10px 16px 0;flex-shrink:0}
.fav-modal .bm-search-box{position:relative}
.fav-modal .bm-search-ico{position:absolute;inset-inline-start:12px;top:50%;transform:translateY(-50%);display:flex;color:#8a8a90;pointer-events:none}
.fav-modal .bm-search-ico svg{width:16px;height:16px}
.fav-modal .bm-search-in{width:100%;height:38px;padding:0 40px;border-radius:10px;background:rgba(0,0,0,.32);border:1px solid rgba(255,255,255,.1);color:#f0f0f0;-webkit-text-fill-color:#f0f0f0;font-size:14px;font-family:inherit;outline:none;transition:.2s}
.fav-modal .bm-search-in:focus{border-color:var(--th);background:rgba(0,0,0,.5);box-shadow:0 0 0 3px rgba(255,255,255,.06)}
.fav-modal .bm-search-in::placeholder{color:#8a8a90}
.fav-modal .bm-hdr{padding:8px 12px 8px!important;padding-inline-end:50px!important;gap:6px!important;align-items:center}
.fav-modal .bm-title{font-size:16px!important;flex:0 0 auto!important}
.fav-modal .bm-close{top:9px!important;inset-inline-end:8px!important;width:32px!important;height:32px!important;font-size:20px!important}
.fav-modal .bm-title{flex:1 1 auto!important}
.fav-modal .bm-tools{order:2!important;flex:0 0 auto!important;gap:6px!important;justify-content:flex-end!important}
.fav-modal .bm-search-wrap{align-items:center;gap:8px}
.fav-modal .bm-search-box{flex:1 1 0;min-width:0}
.fav-modal .bm-search-wrap>.bm-sort-wrap{flex:0 0 auto!important;margin:0!important;height:34px}
.fav-modal .bm-search-wrap>.bm-sort-wrap .bm-hbtn{height:34px!important;padding:0 10px!important}
.fav-modal .bm-search-wrap>.bm-sort-wrap .bm-hbtn:last-child{width:38px!important;padding:0!important}
.fav-modal .bm-tools .bm-hbtn{height:32px!important;min-width:36px!important}
.fav-modal .bm-tools .bm-gear{width:38px!important}
.fav-modal .bm-tools>.bm-sort-wrap{min-width:0!important}
.fav-modal .bm-setp{order:4}
.fav-modal .bm-search-wrap{padding:6px 12px 0!important}
.fav-modal .bm-search-in{height:34px!important;font-size:13px!important}
.fav-modal .bm-tags-wrap{flex-direction:row!important;flex-wrap:wrap!important;align-items:center!important;gap:8px!important;padding:6px 12px!important;max-height:none!important;overflow:hidden!important}
.fav-modal .bm-seg{flex:1 1 100%;min-width:0;max-width:100%}
.fav-modal .bm-seg .bm-tbtn{height:30px!important;padding:0 12px!important;font-size:12px!important}
.fav-modal .bm-chips{flex:1 1 140px;min-width:0;margin:0!important;padding:0!important}
.fav-modal .bm-chips .bm-tbtn{padding:5px 12px!important}
.fav-modal .bm-hbtn svg,.fav-modal .bm-del svg,.fav-modal .bm-pin svg{display:block!important;margin:0 auto!important;flex:none}
.fav-modal .bm-act-btn svg{display:block!important;margin:0!important;flex:none}
.fav-modal .bm-act-btn{justify-content:center;text-align:center}
.fav-modal .bm-gear{display:inline-flex!important;align-items:center!important;justify-content:center!important;padding:0!important}
.fav-modal .bm-search-clr{position:absolute;inset-inline-end:6px;top:50%;transform:translateY(-50%);display:none;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:50%;background:rgba(255,255,255,.1);color:#ddd;font-size:17px;line-height:1;cursor:pointer}
.fav-modal .bm-search-clr:hover{background:rgba(255,255,255,.2);color:#fff}
.fav-modal .bm-pin{display:flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;flex-shrink:0;border-radius:8px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);color:#9a9aa2;cursor:pointer;transition:.2s}
.fav-modal .bm-pin svg{width:15px;height:15px;margin:0;display:block;vertical-align:baseline}
.fav-modal .bm-pin:hover{background:rgba(245,197,24,.18);color:#f5c518}
.fav-modal .bm-pin.on{color:#f5c518;background:rgba(245,197,24,.14);border-color:rgba(245,197,24,.4)}
.fav-modal .bm-pin.on svg{fill:currentColor}
.fav-modal .bm-item.pinned{border-color:rgba(245,197,24,.3)}
.fav-modal .bm-gallery-mode .bm-pin{position:absolute;top:6px;right:38px;order:2;z-index:2;width:26px;height:26px;border-radius:6px;background:rgba(0,0,0,.55);border-color:rgba(255,255,255,.1)}
.fav-modal .bm-gallery-mode .bm-pin.on{border-color:rgba(245,197,24,.6)}
.fav-modal .bm-gallery-mode .bm-link>div{width:calc(100% - 66px);max-width:calc(100% - 66px)}
.fav-modal .bm-gallery-mode .bm-grp-btn{order:5;width:100%;padding:5px 8px;font-size:12px;min-width:0}
.fav-modal .bm-gallery-mode .bm-grp-btn>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.stats,.panel .stats{width:42px;min-width:42px;max-width:42px;overflow:hidden;white-space:nowrap}
.bm-spd:disabled{cursor:default}
:host([data-rtl]) .bm-overlay{direction:rtl}
`;
const CSS_X = `.off{display:none!important}
.panel.dragging{-webkit-backdrop-filter:none!important;backdrop-filter:none!important}
:host([data-dfb]) :is(.bm-overlay,.cf-ov,.dlg-ov,.fr-ov){position:absolute!important;inset:auto 0 0 0!important;width:100%!important;height:100vh!important;height:100dvh!important}
.bm-modal.rs-modal.st-modal{width:460px!important}
.st-modal .bm-list>*{flex:none}
.st-row .bm-sw{position:relative;flex:none;width:46px;height:26px;padding:0;border:0;border-radius:11px;background:rgba(255,255,255,.18);cursor:pointer;transition:.2s}
.st-row .bm-sw::after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:#fff;transition:.2s}
.st-row .bm-sw.on{background:var(--th)}
.st-row .bm-sw.on::after{left:23px}
.st-modal .bm-btn{flex:none}
.st-row{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:42px;padding:6px 12px;border-radius:8px;background:rgba(255,255,255,.05);color:#ddd;font-size:13px}
.st-lbl{flex:1;min-width:0}
.st-block{display:flex;flex-direction:column;gap:8px;padding:8px 12px 10px;border-radius:8px;background:rgba(255,255,255,.05);color:#ddd;font-size:13px}
.st-hint{color:#888;font-size:11px;line-height:1.35}
.st-ic{display:inline-flex;align-items:center;justify-content:center;flex:none;width:20px;height:20px;color:var(--th)}
.st-ic svg{width:18px;height:18px}
.hd-sec .st-ic{width:16px;height:16px}
.hd-sec .st-ic svg{width:14px;height:14px}
.hd-sec{display:flex;align-items:center;gap:6px}
.st-lbl{display:flex;align-items:center;gap:10px}
.st-pill{display:inline-flex;align-items:center;justify-content:center;gap:6px;flex:none;padding:7px 14px;border-radius:999px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.08);color:#e8e8ec;font-size:12px;font-weight:700;cursor:pointer;transition:.2s}
.st-pill:hover{background:rgba(255,255,255,.16)}
.st-pill svg{width:14px;height:14px}
.st-wide{display:flex;width:100%;padding:11px;border-radius:10px}
.st-tiles{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}
.st-tile{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;min-height:60px;padding:7px 3px;border-radius:10px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.03);color:#6d6d75;font-size:9.5px;font-weight:700;letter-spacing:.3px;text-transform:uppercase;line-height:1.1;text-align:center;cursor:pointer;transition:.2s}
.st-tile .st-ic{color:#fff}
.st-tile .st-tl{max-width:100%;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.st-tile.on{background:rgba(255,255,255,.09);border-color:var(--th);color:#fff}
.st-swatch.st-custom{background:conic-gradient(#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)}
.st-hsl{display:flex;flex-direction:column;gap:12px;padding-top:4px}
.st-hrow{display:flex;flex-direction:column;gap:6px;font-size:11px;color:#9a9aa2}
.st-hsl input,.st-range{-webkit-appearance:none;appearance:none;margin:0;outline:none;border-radius:6px}
.st-hsl input{width:100%;height:14px}
.st-range{width:150px;height:8px;background:rgba(255,255,255,.18)}
.st-hsl input::-webkit-slider-thumb,.st-range::-webkit-slider-thumb{-webkit-appearance:none;width:22px;height:22px;border-radius:50%;background:#fff;border:2px solid rgba(0,0,0,.45);box-shadow:0 1px 5px rgba(0,0,0,.55);cursor:pointer}
.st-hsl input::-moz-range-thumb,.st-range::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:#fff;border:2px solid rgba(0,0,0,.45);cursor:pointer}
.st-prev{height:8px;border-radius:3px;background:var(--th)}
.cf-ov{position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;padding:20px;background:rgba(0,0,0,.75);pointer-events:auto}
.cf-card{width:320px;max-width:100%;padding:20px;border-radius:18px;background:linear-gradient(180deg,rgb(32,32,40),rgb(18,18,23));border:1px solid rgba(255,255,255,.1);box-shadow:0 20px 60px rgba(0,0,0,.7);color:#eee;font-size:14px;line-height:1.45}
.cf-txt{margin-bottom:16px}
.cf-btns{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.cf-btns button{padding:11px;border-radius:10px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.07);color:#eee;font-size:13px;font-weight:700;cursor:pointer}
.cf-btns .cf-yes{background:var(--th);border-color:transparent;color:#fff}
.st-chips{display:flex;flex-wrap:wrap;gap:6px}
.st-chips .bm-tbtn{flex:0 0 auto;height:auto;margin:0;padding:7px 12px;font-size:12px;border:1px solid rgba(255,255,255,.2);border-radius:10px;background:rgba(255,255,255,.05);white-space:normal;text-align:center}
.st-chips .bm-tbtn.on{background:var(--th)}
.st-sw-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
.st-swatch{width:26px;height:26px;padding:0;border-radius:50%;border:2px solid transparent;cursor:pointer}
.st-swatch.sel{border-color:#fff}
.st-color{width:34px;height:26px;padding:0;border:0;background:none;cursor:pointer}
.st-val{min-width:42px;text-align:right;color:#aaa;font-size:12px}
.bm-view{display:inline-flex;align-items:center;gap:3px;margin-left:8px;font-size:11px;color:#aaa;flex-shrink:0}
.bm-view svg{width:12px;height:12px}
.hs-tools{display:flex;justify-content:flex-end}
.hs-tools .bm-btn{padding:8px 16px;font-size:12px}
.hs-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:10px;background:rgba(255,255,255,.05);cursor:pointer;transition:.2s}
.hs-row:hover{background:rgba(255,255,255,.09)}
.hs-ava{flex:none;width:42px;height:42px;border-radius:50%;object-fit:cover;background:rgba(255,255,255,.1)}
.hs-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.hs-name{color:#fff;font-size:14px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hs-meta{color:#9a9aa2;font-size:11.5px}
.hs-btn{flex:none;width:32px;height:32px;padding:0;border:0;border-radius:6px;background:rgba(255,255,255,.08);color:#ddd;cursor:pointer;display:flex;align-items:center;justify-content:center}
.hs-btn:hover{background:rgba(255,255,255,.18)}
.hs-btn.on{color:#ffc83d}
.hs-btn.on svg{fill:currentColor}
:where(.bm-modal) button{display:inline-flex;align-items:center;justify-content:center;gap:5px}
.bm-modal button svg{flex:none;margin:0}
.hs-btn svg,.bm-close svg,.bm-del svg,.bm-pin svg{display:block;margin:0!important}
.hs-btn svg{width:16px;height:16px}
.bm-btn,.bm-add{display:inline-flex;align-items:center;justify-content:center;gap:6px}
.fav-modal .bm-tabs{flex:0 1 auto;min-width:0;max-width:100%}
.fav-modal .bm-hdr .bm-title{padding-inline-end:0!important;flex:1 1 auto!important;min-width:0;min-height:0}
.fav-modal .bm-tools{order:2!important;flex:0 0 auto!important;margin-inline-start:auto!important;justify-content:flex-end!important;gap:6px!important}
.fav-modal .bm-tabs .bm-tbtn{height:34px!important;padding:0 12px!important;font-size:13px!important;font-weight:700;gap:6px;color:#aaa;flex:1 1 auto}
.fav-modal .bm-tabs .bm-tbtn.on{color:#fff}
.fav-modal .bm-tabs .bm-tbtn svg{width:15px;height:15px}
.fav-modal .bm-hdr .bm-tabs{display:flex!important;width:100%!important;flex:1 1 auto!important;min-width:0!important;max-width:100%!important}
.fav-modal .bm-hdr .bm-tabs .bm-tbtn{flex:1 1 auto!important;width:auto;min-width:0!important;padding:0 3px!important;gap:3px!important;font-size:12.5px!important;overflow:hidden;justify-content:center}
.fav-modal .bm-tabs .bm-tbtn .bm-tl{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fav-modal .bm-tools{order:3!important;flex:1 0 100%!important;justify-content:flex-end!important}
.fav-modal .bm-tools .bm-wseg-host{margin-inline-end:auto;display:flex;min-width:0}
.fav-modal.mode-hist .bm-search-wrap,.fav-modal.mode-hist .bm-tags-wrap,.fav-modal.mode-hist .bm-list,.fav-modal.mode-hist .bm-foot,.fav-modal.mode-hist .bm-tools,.fav-modal.mode-hist .bm-setp{display:none!important}
.fav-modal .hs-pane{display:none}
.fav-modal.mode-hist .hs-pane{display:flex;flex-direction:column;gap:8px;flex:1 1 auto;min-height:0;overflow-y:auto;padding:14px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent}
.hs-pane>*{flex:none}
.hs-btn.del{color:#e5616f;background:rgba(229,97,111,.12)}
.hs-btn.del:hover{background:#e74c3c;color:#fff}
.st-chips{align-items:center}
.st-chips .bm-tbtn.hs-clear{margin-inline-start:auto;display:inline-flex;align-items:center;gap:6px;color:#ff8591;background:rgba(229,97,111,.12);border-color:rgba(229,97,111,.45)}
.st-chips .bm-tbtn.hs-clear:hover{background:#e74c3c;border-color:transparent;color:#fff}
.st-chips .bm-tbtn.hs-clear svg{width:14px;height:14px;display:block;flex:none;margin:0}
.hs-sep{flex:none;width:1px;height:22px;margin:0 6px;background:rgba(255,255,255,.22);border-radius:1px}
.ch-box{padding:10px 12px;border-radius:10px;background:rgba(255,255,255,.05);margin-bottom:8px}
.ch-ttl{display:flex;justify-content:space-between;gap:8px;font-size:12px;color:rgba(255,255,255,.65);margin-bottom:8px}
.ch-ttl b{font-weight:600;color:#fff}
.ch-bars{display:flex;align-items:flex-end;gap:3px;height:84px;direction:ltr}
.ch-col{flex:1 1 0;min-width:0;height:100%;display:flex;align-items:flex-end}
.ch-bar{width:100%;min-height:2px;border-radius:3px 3px 0 0;background:var(--th)}
.ch-bar.zero{background:rgba(255,255,255,.12)}
.ch-axis{display:flex;gap:3px;margin-top:4px;font-size:9px;color:rgba(255,255,255,.5);direction:ltr}
.ch-axis span{flex:1 1 0;min-width:0;text-align:center;white-space:nowrap}
.ch-row{display:flex;align-items:center;gap:8px;font-size:12px;margin-top:6px}
.ch-name{flex:0 0 32%;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ch-track{flex:1 1 auto;height:8px;border-radius:3px;background:rgba(255,255,255,.08);overflow:hidden}
.ch-fill{height:100%;border-radius:3px;background:var(--th)}
.ch-val{flex:0 0 auto;color:rgba(255,255,255,.65);font-variant-numeric:tabular-nums}
.bm-view{margin-left:0;margin-inline-start:8px}
:host([data-rtl]) .fav-modal .bm-gallery-mode .bm-pin{right:auto;left:auto;inset-inline-end:38px}
:host([data-rtl]) .bm-gallery-mode .bm-del{right:auto;left:6px}
:host([data-rtl]) .fav-modal .bm-gallery-mode .bm-pin{left:38px}
:host([data-rtl]) .fav-modal .bm-gallery-mode .bm-link>div{padding-inline:0}
:host([data-rtl]) .fav-modal .bm-item{padding:12px 16px 12px 12px}
:host([data-rtl]) .fav-modal .bm-gallery-mode .bm-item{padding:8px}
:host([data-rtl]) .fav-modal .bm-item::before{left:auto;right:0;border-radius:2px 0 0 2px}
:host([data-rtl]) .fav-modal .bm-topic{border-left:0;border-right:3px solid var(--th)}
:host([data-rtl]) .st-val{text-align:left}
:host([data-rtl]) .lp-btn{text-align:right}
:host([data-rtl]) .fav-modal .bm-sort-wrap .bm-hbtn+.bm-hbtn{border-left:0;border-right:1px solid rgba(255,255,255,.12)}
:host([data-rtl]) .fav-modal .bm-seg .bm-tbtn+.bm-tbtn{border-left:0;border-right:1px solid rgba(255,255,255,.12)}
.fav-modal .bm-seg .bm-tbtn{gap:6px}
.fav-modal .bm-chips .bm-tbtn svg{width:12px;height:12px;margin-inline-end:4px;vertical-align:-1px}
.fav-modal .bm-grp-btn svg{width:12px;height:12px;margin-inline-end:4px;vertical-align:-1px}
.dlg-txt{display:flex;align-items:center;gap:8px}
.dlg-txt svg{width:14px;height:14px;flex:none}
.fav-modal .bm-ptags{display:flex;flex-wrap:wrap;gap:4px;width:100%}
.fav-modal .bm-ptag{padding:2px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);color:#cfcfd6;font-size:11px;font-family:inherit;cursor:pointer;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fav-modal .bm-ptag.on{background:var(--th);color:#fff;border-color:transparent}
.fav-modal .bm-ptag.more{cursor:default;opacity:.7}
.fav-modal .bm-gallery-mode .bm-ptags{order:6}
.fav-modal .bm-gallery-mode .bm-ptag{font-size:10px;padding:1px 6px}
.fav-modal .bm-list.bm-gallery-mode .bm-item .bm-grp-btn{display:none!important}
.fav-modal .bm-gallery-mode .bm-nwrap{flex-wrap:wrap;row-gap:2px;align-content:flex-start}
.fav-modal .bm-gallery-mode .bm-nwrap .bm-stat{flex:none;margin:0 6px 0 0}
.fav-modal .bm-gallery-mode .bm-nwrap .bm-name{flex:1 0 calc(100% - 16px);min-width:0}
.fav-modal .bm-gallery-mode .bm-nwrap .bm-view{margin:0 8px 0 0}
.fav-modal .bm-gallery-mode .bm-nwrap .bm-view:not(:empty),.fav-modal .bm-gallery-mode .bm-nwrap .bm-view:empty+.bm-seen{margin-inline-start:15px}
.fav-modal .bm-gallery-mode .bm-nwrap .bm-seen{margin-left:0}
.fav-modal .bm-tabs .bm-tbtn{padding:0 8px!important;font-size:12.5px!important}
.fav-modal .cal-pane{display:none}
.fav-modal.mode-cal .cal-pane{display:flex;flex-direction:column;gap:10px;flex:1 1 auto;min-height:0;overflow-y:auto;padding:12px 14px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent}
.fav-modal.mode-cal .bm-search-wrap,.fav-modal.mode-cal .bm-tags-wrap,.fav-modal.mode-cal .bm-list,.fav-modal.mode-cal .bm-foot,.fav-modal.mode-cal .bm-tools,.fav-modal.mode-cal .bm-setp,.fav-modal.mode-cal .hs-pane{display:none!important}
.fav-modal.mode-hist .cal-pane{display:none!important}
.cal-pane{color:#e6e6e6}.cal-pane>*{flex:none}
.cal-pane{overflow-anchor:none}
.cal-head{display:flex;align-items:center;gap:8px}
.cal-title{flex:1;text-align:center;font-weight:700;font-size:15px;color:#f2f2f2}
.cal-nav svg{display:block;margin:auto}
.cal-nav,.cal-today{height:34px;min-width:34px;padding:0 10px;border-radius:8px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);color:#eee;font-size:16px;font-family:inherit;cursor:pointer}
.cal-today{font-size:12px}
.cal-filters{display:flex;flex-direction:column;gap:8px}
.cal-frow{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.cal-sel{flex:1 1 130px;min-width:0;height:34px;border-radius:8px;border:1px solid rgba(255,255,255,.12);background:rgba(0,0,0,.35);color:#eee;padding:0 8px;font:inherit;font-size:13px}
.cal-tl{display:inline-flex;align-items:center;gap:6px}
.cal-tl svg{width:14px;height:14px;flex:none}
.cal-pill{flex:1 1 100%;height:36px;padding:0 12px;text-align:center;border-radius:8px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);color:#ddd;font:inherit;font-size:13px;cursor:pointer}
.cal-pill.on{background:var(--th);color:#fff;border-color:transparent}
.cal-models{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;max-height:280px;overflow-y:auto;padding:8px;border-radius:10px;background:rgba(0,0,0,.28);border:1px solid rgba(255,255,255,.06);scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent}
.cal-mrow{display:flex;align-items:center;justify-content:flex-start;gap:8px;min-width:0;height:42px;padding:0 10px 0 7px;border-radius:10px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.05);color:#d4d4da;font:inherit;font-size:13px;font-weight:600;cursor:pointer;text-align:start}
.cal-mrow>span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cal-mrow svg{width:14px;height:14px;flex:none}
.cal-mrow.on{background:var(--th);border-color:transparent;color:#fff}
.cal-mrow.reset{grid-column:1/-1;justify-content:center;height:34px;font-weight:500;color:#aaa}
.cal-mrow.reset>span{flex:none}
.cal-ava.sm{width:26px;height:26px}
.cal-dd{flex:1 1 130px;min-width:0;height:36px;display:flex;align-items:center;gap:8px;padding:0 12px;border-radius:8px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);color:#ddd;font:inherit;font-size:13px;cursor:pointer;text-align:start}
.cal-dd svg{width:14px;height:14px;flex:none;opacity:.8}
.cal-dd>span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cal-dd::after,.cal-fold::after{content:'';flex:none;width:7px;height:7px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(45deg) translateY(-2px);opacity:.6;transition:transform .15s}
.cal-dd.open::after,.cal-fold.open::after{transform:rotate(225deg) translateY(-2px)}
.cal-dd.on{background:var(--th);color:#fff;border-color:transparent}
.cal-panel{display:flex;flex-wrap:wrap;gap:6px;max-height:230px;overflow-y:auto;padding:10px;border-radius:10px;background:rgba(0,0,0,.28);border:1px solid rgba(255,255,255,.06);scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent}
.cal-panel .cal-sep{flex:1 1 100%;height:0;border-top:1px solid rgba(255,255,255,.08);margin:2px 0}
.cal-opt{display:inline-flex;align-items:center;gap:6px;max-width:100%;height:30px;padding:0 11px;border-radius:999px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:#cfcfd6;font:inherit;font-size:12.5px;cursor:pointer;white-space:nowrap}
.cal-opt svg{width:13px;height:13px;flex:none}
.cal-opt>span{overflow:hidden;text-overflow:ellipsis}
.cal-opt i{font-style:normal;font-size:10.5px;opacity:.55}
.cal-opt.all{font-weight:700}
.cal-opt.on{background:var(--th);color:#fff;border-color:transparent}
.cal-wsec{display:flex;flex-direction:column;gap:8px}
.cal-dtitle+.cal-wsec>.cal-fold{margin-top:0;padding-top:0;border-top:0}
.cal-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:5px}
.cal-wd{text-align:center;font-size:11px;font-weight:600;color:#8a8a90;padding:2px 0;text-transform:capitalize;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cal-day{position:relative;min-height:54px;padding:7px 2px 5px;border-radius:10px;border:1px solid rgba(255,255,255,.07);background:rgba(255,255,255,.04);color:#e6e6ea;display:flex;flex-direction:column;align-items:center;justify-content:space-between;gap:4px;cursor:pointer;font:inherit;font-size:13px;font-weight:600;min-width:0;transition:background .15s}
.cal-day:hover{background:rgba(255,255,255,.09)}
.cal-day.blank{visibility:hidden;pointer-events:none}
.cal-day.past{color:#9a9aa2}
.cal-day.today{border-color:var(--th);box-shadow:inset 0 0 0 1px var(--th)}
.cal-day.sel{background:var(--th);color:#fff;border-color:transparent;box-shadow:0 2px 10px rgba(0,0,0,.35)}
.cal-n{line-height:1}
.cal-bs{display:flex;gap:3px;justify-content:center;align-items:center;min-height:15px}
.cal-b{font-style:normal;font-size:10px;font-weight:700;line-height:1;min-width:16px;padding:3px 4px;text-align:center;border-radius:999px;background:rgba(46,204,113,.22);color:#7ee2a8}
.cal-b.s{background:rgba(90,150,255,.22);color:#9cc0ff}
.cal-day.past .cal-b.s{opacity:.6}
.cal-day.sel .cal-b{background:rgba(0,0,0,.32);color:#fff}
.cal-det{display:flex;flex-direction:column;gap:8px;padding:12px;border-radius:12px;background:rgba(0,0,0,.28);border:1px solid rgba(255,255,255,.06)}
.cal-dtitle{font-weight:700;font-size:15px;color:#f2f2f2}
.cal-sec{font-size:11px;font-weight:600;letter-spacing:.5px;text-transform:uppercase;color:#8a8a90;margin-top:6px;padding-top:10px;border-top:1px solid rgba(255,255,255,.07)}
.cal-dtitle+.cal-sec{margin-top:0;padding-top:0;border-top:0}
.cal-tbl{display:flex;flex-direction:column;border-radius:10px;overflow:hidden;border:1px solid rgba(255,255,255,.07)}
.cal-trow{display:grid;grid-template-columns:minmax(0,1fr) 52px 78px 54px;align-items:center;gap:6px;padding:7px 10px;font-size:12.5px;font-variant-numeric:tabular-nums}
.cal-trow>span{text-align:end;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#cfcfd6}
.cal-trow+.cal-trow{border-top:1px solid rgba(255,255,255,.05)}
.cal-trow:nth-child(even){background:rgba(255,255,255,.03)}
.cal-thead{background:rgba(255,255,255,.08)!important;font-size:10px;font-weight:700;letter-spacing:.4px;text-transform:uppercase}
.cal-thead>span{color:#9a9aa2}
.cal-tfoot{background:rgba(255,255,255,.08)!important;font-weight:700}
.cal-tfoot>span{color:#fff}
.cal-tfoot>span:first-child{text-align:start}
.cal-who{display:flex;align-items:center;gap:8px;min-width:0}
.cal-who a{min-width:0;color:#fff;text-decoration:none;font-weight:600;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cal-thead>span:first-child{text-align:start}
.cal-trow>span:nth-child(2),.cal-trow>span:nth-child(3){text-align:center}
.cal-trow>span:nth-child(4){text-align:end}
.cal-axis{display:flex;justify-content:space-between;margin-left:38px;direction:ltr;font-size:10px;color:#6f6f76;font-variant-numeric:tabular-nums}
.cal-row{display:flex;align-items:center;gap:10px;min-width:0}
.cal-ava{flex:none;width:28px;height:28px;border-radius:50%;object-fit:cover;background:rgba(255,255,255,.08)}
.cal-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:6px}
.cal-line{display:flex;align-items:center;gap:6px;min-width:0;font-size:13px}
.cal-line a{flex:1;min-width:0;color:#fff;text-decoration:none;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cal-line a:hover{color:var(--th)}
.cal-t{flex:none;color:#aab;font-size:12px;font-variant-numeric:tabular-nums}
.cal-bar{position:relative;height:6px;border-radius:2px;background:rgba(255,255,255,.08);direction:ltr}
.cal-seg{position:absolute;top:0;bottom:0;border-radius:2px;background:#5a96ff}
.cal-now{position:absolute;top:-3px;bottom:-3px;width:2px;margin-left:-1px;border-radius:1px;background:#fff}
.cal-dot{flex:none;width:8px;height:8px;border-radius:50%;background:#2ecc71;box-shadow:0 0 6px #2ecc71}
.cal-muted{color:#8a8a90;font-size:12px}
.cal-fold{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;margin-top:6px;padding:10px 0 0;border:0;border-top:1px solid rgba(255,255,255,.07);background:none;font-family:inherit;cursor:pointer;text-align:start}
.cal-fold>span{flex:1;min-width:0}
.fav-modal .bm-list:not(.bm-gallery-mode) .bm-ptags{gap:5px;margin-top:4px;padding-top:8px;border-top:1px solid rgba(255,255,255,.06)}
.fav-modal .bm-ptags .bm-ptag{display:inline-flex;align-items:center;gap:5px;padding:2px 9px 2px 6px;border:0;border-radius:999px;background:rgba(255,255,255,.05);color:#9aa0b2;font-size:11.5px}
.fav-modal .bm-ptic{width:15px;height:15px;flex:none;object-fit:contain}
.fav-modal .bm-chips .bm-tbtn .bm-ptic{width:15px;height:15px;margin-inline-end:5px;vertical-align:-3px}
.cal-tic{width:16px;height:16px;flex:none;object-fit:contain}
.fav-modal .bm-ptags .bm-ptag.more{background:transparent;padding:2px 4px}
.fav-modal .bm-ptags .bm-ptag.on{background:var(--th);color:#fff}
.fav-modal.bm-no-ptags .bm-ptags{display:none!important}
.fav-modal.bm-no-note .bm-item .bm-note{display:none!important}
.fav-modal .bm-chips .bm-chips-sep{flex:none;width:1px;align-self:stretch;margin:4px 2px;background:rgba(255,255,255,.14)}.fav-modal .bm-chips>.bm-tbtn:first-child{position:sticky;left:0;z-index:3;background-color:#1b1b21;background-image:linear-gradient(rgba(255,255,255,.06),rgba(255,255,255,.06));box-shadow:5px 0 6px #1b1b21}.fav-modal .bm-chips>.bm-tbtn:first-child.on{background-image:linear-gradient(var(--th),var(--th))}
.fav-modal .bm-gallery-mode .bm-ptags{display:none!important}
.fav-modal .bm-gallery-mode .bm-link>div{width:100%;max-width:100%;pointer-events:auto}
.fav-modal .bm-gallery-mode .bm-link{position:relative}
.fav-modal .bm-gallery-mode .bm-nwrap{align-items:center;flex-wrap:nowrap;row-gap:0;min-height:26px;padding-inline-end:58px}
.fav-modal .bm-gallery-mode .bm-nwrap .bm-stat{display:none}
.fav-modal .bm-gallery-mode .bm-nwrap .bm-name{order:1;flex:1 1 auto;min-width:0;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fav-modal .bm-gallery-mode .bm-nwrap .bm-view,.fav-modal .bm-gallery-mode .bm-nwrap .bm-seen{position:absolute;bottom:51px;margin:0!important;padding:2px 7px;border-radius:999px;background:rgba(0,0,0,.6);color:#fff;font-size:11px;line-height:1.3;white-space:nowrap;z-index:1;pointer-events:none}
.fav-modal .bm-gallery-mode .bm-nwrap .bm-view{right:12px}
.fav-modal .bm-gallery-mode .bm-nwrap .bm-seen{left:12px}
.fav-modal .bm-gallery-mode .bm-nwrap .bm-view:empty,.fav-modal .bm-gallery-mode .bm-nwrap .bm-seen:empty{display:none}
.fav-modal .bm-tools{margin-inline-end:-38px!important;justify-content:stretch!important}
.fav-modal .bm-tools>.bm-wseg-host{flex:2 1 0!important;margin-inline-end:0!important}
.fav-modal .bm-tools>.bm-hbtn{flex:1 1 0!important;width:auto!important;min-width:0!important}
.fav-modal .bm-seg.bm-wseg{flex:1 1 auto;width:100%;max-width:none}
.fav-modal .bm-seg.bm-wseg .bm-tbtn{flex:1 1 0;width:auto;padding:0!important;height:30px!important}
.fav-modal .bm-seg.bm-wseg svg{width:21px;height:21px}
.fav-modal .bm-tools .bm-prevbtn svg,.fav-modal .bm-tools .bm-colsbtn svg{width:18px;height:18px}
.fav-modal .bm-tools .bm-hbtn.bm-gear{border:1px solid var(--th)!important}
.fav-modal .bm-tbar{flex:none;display:block;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.07)}
.fav-modal .bm-tbar .bm-tools{order:0!important;flex:none!important;margin:0!important;justify-content:stretch!important}
.fav-modal .bm-tbar .bm-setp{margin-top:8px}
.fav-modal.mode-hist .bm-tbar,.fav-modal.mode-cal .bm-tbar,.fav-modal.mode-mv .bm-tbar{display:none!important}
.panel .hdr-btns .h-btn{width:30px;height:30px}
.panel .hdr-btns .h-btn.lang{width:34px}
.panel .hdr-btns .h-btn svg{width:22px!important;height:22px!important}
.panel .hdr-btns{gap:3px;justify-content:flex-end;min-width:0}
.panel .hdr-btns .h-btn{position:relative}
.panel .hdr-btns{align-items:center}
.panel .hdr-btns .hsep{flex:0 0 1px;width:1px;height:16px;margin:0 2px;background:rgba(255,255,255,.16);pointer-events:none}
.panel .hdr-btns .stats+.hsep{margin-inline-start:0}.panel .hdr-btns{margin-right:2px}
.panel .hdr-btns .stats.off+.hsep{display:none}
.fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-del,.fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-pin{top:5px;width:32px;height:32px;border-radius:8px}
.fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-del{right:8px}
.fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-pin{right:46px}
.fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-del svg,.fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-pin svg{width:18px!important;height:18px!important}
.fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-nwrap{padding-inline-end:86px}
:host([data-rtl]) .fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-del{right:auto;left:8px}
:host([data-rtl]) .fav-modal .bm-list.bm-c1.bm-gallery-mode .bm-pin{right:auto;inset-inline-end:auto;left:46px}
.fav-modal .bm-hdr .bm-tabs{display:flex!important;width:100%!important;max-width:100%!important}
.fav-modal .bm-hdr .bm-tabs .bm-tbtn{flex:1 1 0!important;width:0!important;min-width:0!important;padding:0 4px!important;gap:4px!important;overflow:hidden;justify-content:center}
.fav-modal .bm-hdr .bm-tabs .bm-tbtn .bm-tl{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fav-modal .bm-hdr .bm-tabs .bm-tbtn svg{flex:none}
.fav-modal .mv-pane>.bm-seg.mv-seg{margin-inline:14px!important;width:calc(100% - 28px)!important;display:flex!important;flex:0 0 auto!important;flex-direction:row!important;flex-wrap:nowrap!important;max-width:100%!important;min-height:30px;box-sizing:border-box;overflow:hidden}
.fav-modal .mv-pane>.bm-seg.mv-seg .bm-tbtn{display:inline-flex!important;flex:1 1 0!important;width:0!important;min-width:0!important;padding:0 2px!important;gap:0!important;justify-content:center;align-items:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px!important}
`;
const CSS_POLISH = `@keyframes bcuIn{from{opacity:0;translate:0 8px}to{opacity:1;translate:0 0}}
@keyframes bcuPop{from{opacity:0;scale:.96}to{opacity:1;scale:1}}
@keyframes bcuRing{0%{box-shadow:0 0 0 0 var(--th)}100%{box-shadow:0 0 0 12px transparent}}
:host ::selection{background:var(--th);color:#fff}
:host .panel:not(.mini):not(.hidden){animation:bcuIn .28s cubic-bezier(.2,.8,.2,1)}
:host .dlg-card,:host .cf-card,:host .pick-bar{animation:bcuPop .2s cubic-bezier(.2,.8,.2,1)}
:host .bm-overlay .bm-modal{opacity:0;scale:.97;translate:0 10px;transition:opacity .2s ease,scale .26s cubic-bezier(.2,.8,.2,1),translate .26s cubic-bezier(.2,.8,.2,1)}
:host .bm-overlay.open .bm-modal{opacity:1;scale:1;translate:0 0}
:host .bm-overlay.bm-nofade .bm-modal{transition:none!important}
:host .panel .btn span{display:flex;transition:transform .18s cubic-bezier(.2,.8,.2,1)}
:host .panel .btn:hover span{transform:scale(1.1)}
:host .panel .btn:active span{transform:scale(.94)}
:host .panel .btn sub{font-size:9px;opacity:.9}
:host .panel .btn.on{box-shadow:inset 0 1px rgba(255,255,255,.12),0 2px 5px rgba(0,0,0,.4)}
:host .panel .btn:not(.on):hover{border-color:rgba(255,255,255,.22)}
:host .panel .h-btn,:host .panel .z-btn,:host .panel .btn{-webkit-tap-highlight-color:transparent}
:host .panel button:focus-visible,:host .bm-modal button:focus-visible,:host .dlg-row:focus-visible{outline:2px solid #fff;outline-offset:2px;box-shadow:0 0 0 4px var(--th)}
:host .panel .hotkeys{color:rgba(255,255,255,.5)}
:host :is(.vval,.s-val,.stats,.st-val,.bm-seen,.bm-view){font-variant-numeric:tabular-nums}
:host .vslider{height:5px;transition:filter .15s}
:host .vslider::-webkit-slider-thumb{width:15px;height:15px;transition:transform .15s,box-shadow .15s}
:host .vslider::-moz-range-thumb{width:13px;height:13px;border-radius:50%;background:#fff;border:2px solid var(--th);cursor:pointer}
:host .vslider:hover::-webkit-slider-thumb{transform:scale(1.15)}
:host .vslider:active::-webkit-slider-thumb{transform:scale(1.25);box-shadow:0 0 0 7px color-mix(in srgb,var(--th) 28%,transparent)}
:host :is(.bm-sw,.hd-sw){transition:background .22s ease}
:host :is(.bm-sw,.hd-sw)::after{transition:left .22s cubic-bezier(.3,1.3,.5,1),transform .15s;box-shadow:0 1px 3px rgba(0,0,0,.45)}
:host :is(.bm-sw,.hd-sw):active::after{transform:scaleX(1.12)}
:host .bm-modal .bm-close{transition:background .15s,color .15s}
:host .bm-modal:not(.fav-modal) .bm-hdr{position:sticky;top:0;z-index:3;-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px)}
:host .ghost:not([style*="display: none"]),:host .restore:not([style*="display: none"]){animation:bcuPop .25s cubic-bezier(.2,.8,.2,1)}
:host .bm-empty{opacity:.85;line-height:1.5}
:host .fav-modal .bm-list:not(.bm-gallery-mode) .bm-item{transition:background .2s,border-color .2s,transform .2s,box-shadow .2s}
:host .fav-modal .bm-item:hover{transform:translateY(-1px);box-shadow:0 8px 20px -12px rgba(0,0,0,.8)}
:host .fav-modal .bm-avatar{transition:transform .2s,filter .3s,border-color .3s}
:host .fav-modal .bm-item:hover .bm-avatar{transform:scale(1.04)}
:host *{scrollbar-width:thin}
:host ::-webkit-scrollbar{width:8px;height:8px}
:host ::-webkit-scrollbar-thumb{background:rgba(255,255,255,.16);border-radius:8px;border:2px solid transparent;background-clip:content-box}
:host ::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.3);background-clip:content-box;border:2px solid transparent}
:host ::-webkit-scrollbar-track{background:transparent}
@media (pointer:coarse){
:host .panel .btn{min-height:56px}
:host .bm-modal .bm-close{min-width:40px;min-height:40px}
:host .bm-modal .bm-btn,:host .bm-modal .bm-tbtn{min-height:40px}
:host .dlg-row{min-height:48px}
}
@media (max-width:520px){
:host .bm-modal{max-width:100vw!important;width:100vw!important;border-radius:16px 16px 0 0!important;align-self:flex-end}
:host .bm-overlay .bm-modal{translate:0 24px}
:host .bm-overlay.open .bm-modal{translate:0 0}
}
@media (prefers-reduced-motion:reduce){
:host *,:host *::before,:host *::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}
}
`;
const CSS_SKIN = `:host{color-scheme:dark;--sk-bg:#0a0a0a;--sk-card:#111111;--sk-fg:#f5f5f5;--sk-fg2:#d4d4d4;--sk-mfg:#8a8a8a;--sk-dim:#6b6b6b;--sk-line:rgba(255,255,255,.06);--sk-line2:rgba(255,255,255,.1);--sk-in:rgba(255,255,255,.08);--sk-fill:rgba(255,255,255,.03);--sk-fill2:rgba(255,255,255,.05);--sk-hov:rgba(255,255,255,.06);--sk-on:rgba(255,255,255,.1);--sk-ok:#10b981;--sk-ok-fg:#34d399;--sk-warn:#f59e0b;--sk-warn-fg:#fbbf24;--sk-err:#ef4444;--sk-err-fg:#f87171;--sk-info:#3b82f6;--sk-info-fg:#60a5fa;--sk-shadow:inset 0 1px rgba(255,255,255,.04),0 24px 72px -20px rgba(0,0,0,.9);--sk-hi:inset 0 1px rgba(255,255,255,.16);--sk-ctl:inset 0 1px rgba(255,255,255,.05);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
:host button,:host input,:host select,:host textarea{font-family:inherit}
:host *{scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.14) transparent}
:host :focus-visible{outline:2px solid var(--th);outline-offset:1px}
:host :is(input,textarea,select):focus-visible{outline:none}
:host .panel:not(.mini){background:rgba(17,17,17,.92);border:1px solid rgba(255,255,255,.08);border-radius:18px;box-shadow:var(--sk-shadow);-webkit-backdrop-filter:blur(16px) saturate(1.08);backdrop-filter:blur(16px) saturate(1.08)}
:host .panel:not(.mini)::before{display:none}
:host([data-dfb]) .panel:not(.mini),:host([data-dfb]) .bm-modal.bm-modal{background:#111111!important}
:host .panel .sep{background:var(--sk-line);opacity:1}
:host .panel .hdr{background:var(--sk-fill);border:1px solid var(--sk-line);border-radius:10px}
:host .panel .drag{color:var(--sk-mfg);font-weight:500;font-size:11px;letter-spacing:0;text-transform:none}
:host .panel .stats{background:var(--sk-fill2);border:1px solid var(--sk-line);border-radius:6px;color:var(--sk-fg);font-weight:700}
:host .panel .h-btn{border-radius:8px;background:transparent;color:var(--sk-fg);transition:background .15s,color .15s,transform .1s}
:host .panel .h-btn:hover{background:var(--sk-hov);color:var(--sk-fg);transform:none}
:host .panel .h-btn:active{transform:scale(.97)}
:host .panel .h-btn.close:hover,:host .panel .h-btn.close:active{transform:none}
:host .panel .h-btn.close:hover{background:rgba(239,68,68,.16);color:var(--sk-err-fg)}
:host .panel .h-btn.lang{background:var(--sk-fill);border:1px solid var(--sk-in);color:var(--sk-fg);box-shadow:var(--sk-ctl)}
:host .hdr-btns .h-btn.lang{border:1px solid var(--sk-in)!important}
:host .panel .hdr-btns .hsep{background:var(--sk-line2)}
:host .panel .btn{border-radius:10px;background:var(--sk-fill);border:1px solid var(--sk-in);color:var(--sk-fg);box-shadow:var(--sk-ctl);transition:background .15s,border-color .15s,box-shadow .15s,transform .1s}
:host .panel .btn:hover{background:var(--sk-hov);border-color:var(--sk-line2);transform:none}
:host .panel .btn:active{transform:scale(.97)}
:host .panel .btn.on,:host .panel .btn.on:hover{background:var(--th);border-color:var(--th);color:#fff;box-shadow:var(--sk-hi),0 1px 2px rgba(0,0,0,.3)}
:host .panel .btn sub{text-transform:none;letter-spacing:0;font-weight:500;font-size:9.5px;opacity:.85}
:host .panel .btn.fav-on{color:var(--sk-warn-fg);border-color:rgba(245,158,11,.32);background:rgba(245,158,11,.08)}
:host .panel .btn.fav-on sub{color:var(--sk-fg2)}
:host .panel .v-cap{border-radius:10px;background:var(--sk-fill);border:1px solid var(--sk-line)}
:host .vico{filter:none}
:host .vslider{height:4px;border-radius:999px}
:host .vslider::-webkit-slider-thumb{background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.45)}
:host .vval,:host .s-val{color:var(--sk-fg2);font-weight:600;font-variant-numeric:tabular-nums}
:host .scale-ctrl{background:var(--sk-fill);border:1px solid var(--sk-line);border-radius:8px}
:host .panel .z-btn{border-radius:6px;background:transparent;color:var(--sk-fg)}
:host .panel .z-btn:hover{background:var(--sk-hov);color:var(--sk-fg)}
:host .panel .z-btn:not(.scale-ctrl .z-btn){height:26px;border-radius:8px;background:var(--sk-fill);border:1px solid var(--sk-in);color:var(--sk-fg);box-shadow:var(--sk-ctl);transition:background .15s,border-color .15s,transform .1s}
:host .panel .z-btn:not(.scale-ctrl .z-btn):hover{background:var(--sk-hov);border-color:var(--sk-line2);color:var(--sk-fg)}
:host .panel .z-btn:not(.scale-ctrl .z-btn):active{transform:scale(.97)}
:host .panel .z-btn[style*="linear-gradient"]{background:var(--th)!important;border-color:var(--th)!important;color:#fff;box-shadow:var(--sk-hi)!important}
:host .hotkeys{color:var(--sk-dim);letter-spacing:0}
:host .restore,:host .ghost{border-radius:14px;border:1px solid rgba(255,255,255,.16);background:var(--th);box-shadow:var(--sk-hi),0 8px 24px -10px rgba(0,0,0,.8)}
@keyframes pulseRec{0%,100%{background:rgba(255,255,255,.03);box-shadow:none}50%{background:rgba(239,68,68,.3);box-shadow:none}}
:host .bm-overlay:not(.peek){background:rgba(10,10,10,.72)}
:host .bm-modal.bm-modal{background:var(--sk-card)!important;border:1px solid rgba(255,255,255,.08)!important;border-radius:18px!important;box-shadow:var(--sk-shadow)!important;color:var(--sk-fg)}
:host .bm-modal .bm-hdr{background:transparent!important;border-bottom:1px solid var(--sk-line)!important}
:host .bm-modal .bm-title{color:var(--sk-fg);font-weight:600;letter-spacing:0}
:host .bm-modal .bm-close{border-radius:8px!important;background:transparent!important;color:var(--sk-mfg)!important}
:host .bm-modal .bm-close:hover{background:var(--sk-hov)!important;color:var(--sk-fg)!important}
:host .bm-modal .bm-foot{background:transparent!important;border-top:1px solid var(--sk-line)!important}
:host .bm-modal .bm-tags-wrap{background:transparent!important;border-bottom:1px solid var(--sk-line)!important}
:host .bm-modal .bm-empty,:host .bm-modal .hd-empty,:host .bm-modal .hd-hint,:host .bm-modal .rs-note,:host .bm-modal .st-hint{color:var(--sk-mfg)!important}
:host .hd-sec,:host .cal-sec{color:var(--sk-mfg)!important;text-transform:none!important;letter-spacing:0!important;font-weight:500!important;font-size:12px!important}
:host .cf-ov{background:rgba(10,10,10,.72)}
:host .cf-card{background:var(--sk-card);border:1px solid rgba(255,255,255,.08);border-radius:18px;box-shadow:var(--sk-shadow);color:var(--sk-fg)}
:host .cf-btns button{border-radius:8px;border:1px solid var(--sk-in);background:var(--sk-fill);color:var(--sk-fg);font-weight:500;box-shadow:var(--sk-ctl);transition:background .15s,transform .1s}
:host .cf-btns button:hover{background:var(--sk-hov)}
:host .cf-btns button:active{transform:scale(.97)}
:host .cf-btns .cf-yes,:host .cf-btns .cf-yes:hover{background:var(--th);border-color:var(--th);color:#fff;box-shadow:var(--sk-hi)}
:host .pick-bar{background:rgba(17,17,17,.94);border:1px solid rgba(255,255,255,.08);border-radius:14px;box-shadow:var(--sk-shadow)}
:host .pick-box{border:2px solid var(--th);border-radius:4px}
:host .pick-info{color:var(--sk-fg2)}
:host :is(.bm-name,.hs-name,.cal-title,.cal-dtitle,.cal-who a,.cal-line a,.hd-row,.hd-txt b){color:var(--sk-fg)!important}
:host :is(.bm-seen,.hs-meta,.cal-muted,.hd-txt small,.cal-wd,.cal-axis,.bm-view,.st-val,.st-hrow,.cal-t,.ch-val,.ch-ttl){color:var(--sk-mfg)!important}
:host :is(.bm-name,.hs-name){font-weight:600!important;letter-spacing:0!important}
:host .cal-thead{text-transform:none!important;letter-spacing:0!important;font-weight:600!important;font-size:11px!important}
:host .st-tile{text-transform:none!important;letter-spacing:0!important;font-weight:500!important;font-size:10.5px!important;color:var(--sk-mfg)!important}
:host .st-tile .st-ic{color:var(--sk-fg2)}
:host .hd-txt small.hd-na,:host .hd-na{color:var(--sk-warn-fg)!important}
:host :is(.bm-btn:not(.bm-srch-btn),.bm-hbtn,.bm-act-btn,.hs-btn,.cal-nav,.cal-today,.cal-pill,.cal-dd,.st-pill,.bm-grp-btn,.lp-btn){background:var(--sk-fill)!important;border:1px solid var(--sk-in)!important;color:var(--sk-fg)!important;border-radius:8px!important;box-shadow:var(--sk-ctl)!important;font-weight:500;transition:background .15s,border-color .15s,color .15s,transform .1s}
:host :is(.bm-btn:not(.bm-srch-btn),.bm-hbtn,.bm-act-btn,.hs-btn,.cal-nav,.cal-today,.cal-pill,.cal-dd,.st-pill,.bm-grp-btn,.lp-btn):hover{background:var(--sk-hov)!important;border-color:var(--sk-line2)!important;color:var(--sk-fg)!important}
:host :is(.bm-btn,.bm-hbtn,.bm-act-btn,.hs-btn,.cal-nav,.cal-today,.cal-pill,.st-pill,.lp-btn):active{transform:scale(.97)}
:host :is(.bm-btn:not(.bm-srch-btn),.bm-hbtn,.cal-pill,.lp-btn,.bm-act-btn,.hs-btn).on,:host :is(.bm-btn:not(.bm-srch-btn),.lp-btn).sel,:host .bm-btn.sel:hover{background:var(--th)!important;border-color:var(--th)!important;color:#fff!important;box-shadow:var(--sk-hi)!important}
:host .bm-modal:not(.fav-modal) .bm-btn:not(.bm-srch-btn):active{background:var(--th)!important;border-color:var(--th)!important;color:#fff!important}
:host .bm-srch-btn{background:var(--sk-fill)!important;border-top:1px solid var(--sk-in)!important;border-right:1px solid var(--sk-in)!important;border-bottom:1px solid var(--sk-in)!important;color:var(--sk-fg)!important;border-radius:10px!important;box-shadow:var(--sk-ctl)!important;font-weight:500}
:host .bm-srch-btn:hover{background:var(--sk-hov)!important;color:var(--sk-fg)!important}
:host .bm-srch-btn:active{transform:scale(.99)}
:host .lp-btn .lp-code{background:var(--sk-fill2);border-radius:6px;font-weight:700}
:host .lp-btn.sel .lp-code{background:rgba(0,0,0,.25)}
:host .fav-modal .bm-act-btn:hover{background:var(--sk-hov)!important;border-color:var(--sk-line2)!important;color:var(--sk-fg)!important}
:host .bm-tools .bm-hbtn.bm-gear,:host .fav-modal .bm-tools .bm-hbtn.bm-gear{border:1px solid var(--sk-in)!important}
:host .fav-modal .bm-tools .bm-hbtn.bm-gear.on{border-color:var(--th)!important}
:host .bm-add{background:var(--th)!important;border:1px solid var(--th)!important;border-radius:10px!important;color:#fff!important;font-weight:500!important;box-shadow:var(--sk-hi),0 1px 2px rgba(0,0,0,.3)!important}
:host .bm-add:hover{filter:brightness(1.1)}
:host .bm-add:active{transform:scale(.98)}
:host .bm-add:disabled{background:var(--sk-fill2)!important;border-color:var(--sk-in)!important;color:var(--sk-dim)!important;box-shadow:none!important;opacity:1}
:host :is(.bm-tbtn,.cal-opt,.bm-ptag){background:var(--sk-fill)!important;border:1px solid var(--sk-in)!important;color:var(--sk-fg2)!important;border-radius:8px!important;font-weight:500!important;box-shadow:none!important;transition:background .15s,border-color .15s,color .15s}
:host :is(.bm-tbtn,.cal-opt,.bm-ptag):hover{background:var(--sk-hov)!important;color:var(--sk-fg)!important}
:host :is(.bm-tbtn,.cal-opt,.bm-ptag).on,:host .cal-mrow.on{background:var(--th)!important;border-color:var(--th)!important;color:#fff!important;box-shadow:var(--sk-hi)!important}
:host .fav-modal .bm-ptags .bm-ptag{background:var(--sk-fill2)!important;border:0!important;color:var(--sk-mfg)!important}
:host .fav-modal .bm-ptags .bm-ptag.on{background:var(--th)!important;color:#fff!important}
:host .fav-modal .bm-ptags .bm-ptag.more{background:transparent!important}
:host .st-chips .bm-tbtn.hs-clear{color:var(--sk-err-fg)!important;background:rgba(239,68,68,.12)!important;border-color:rgba(239,68,68,.32)!important}
:host .st-chips .bm-tbtn.hs-clear:hover{background:rgba(239,68,68,.2)!important;color:var(--sk-err-fg)!important}
:host .fav-modal .bm-chips>.bm-tbtn:first-child{background-color:#111111!important;background-image:linear-gradient(rgba(255,255,255,.03),rgba(255,255,255,.03))!important;box-shadow:5px 0 6px #111111!important}
:host .fav-modal .bm-chips>.bm-tbtn:first-child.on{background-image:linear-gradient(var(--th),var(--th))!important}
:host :is(.bm-seg,.bm-tabs){border:1px solid var(--sk-in)!important;border-radius:8px!important;background:var(--sk-fill)!important;box-shadow:var(--sk-ctl)}
:host .bm-tabs{overflow:hidden}
:host :is(.bm-seg,.bm-tabs) .bm-tbtn{background:transparent!important;border:0!important;border-radius:0!important;color:var(--sk-fg)!important;box-shadow:none!important}
:host .bm-tabs .bm-tbtn{color:#aaa!important}
:host :is(.bm-seg,.bm-tabs) .bm-tbtn+.bm-tbtn{border-inline-start:1px solid var(--sk-in)!important}
:host :is(.bm-seg,.bm-tabs) .bm-tbtn:hover{background:var(--sk-hov)!important;color:var(--sk-fg)!important}
:host :is(.bm-seg,.bm-tabs) .bm-tbtn.on{background:var(--th)!important;color:#fff!important;box-shadow:var(--sk-hi)!important}
:host .bm-sort-wrap{border:1px solid var(--sk-in)!important;border-radius:8px!important;background:var(--sk-fill)!important;box-shadow:var(--sk-ctl)}
:host .bm-sort-wrap .bm-hbtn{background:transparent!important;border:0!important;border-radius:0!important;box-shadow:none!important;color:var(--sk-fg)!important}
:host .bm-sort-wrap .bm-hbtn:hover{background:var(--sk-hov)!important;color:var(--sk-fg)!important}
:host .bm-sort-wrap .bm-hbtn+.bm-hbtn{border-inline-start:1px solid var(--sk-in)!important}
:host .bm-sort-wrap .bm-hbtn.on{background:var(--th)!important;color:#fff!important;box-shadow:var(--sk-hi)!important}
:host :is(.bm-note,.bm-grp,.bm-srch-in,.bm-search-in,.cal-sel){background:var(--sk-fill)!important;border:1px solid var(--sk-in)!important;border-radius:10px!important;color:var(--sk-fg)!important;box-shadow:var(--sk-ctl)}
:host :is(.bm-note,.bm-grp,.bm-srch-in,.bm-search-in,.cal-sel):focus{border-color:var(--th)!important;background:var(--sk-fill)!important}
:host .bm-search-in::placeholder{color:var(--sk-mfg)}
:host .bm-search-ico{color:var(--sk-mfg)}
:host .bm-search-clr{background:var(--sk-hov);color:var(--sk-fg2);border-radius:8px}
:host .bm-search-clr:hover{background:var(--sk-on);color:var(--sk-fg)}
:host :is(.bm-sw,.hd-sw){border-radius:999px!important;background:rgba(255,255,255,.12)!important}
:host :is(.bm-sw,.hd-sw)::after{background:#fff!important;box-shadow:0 1px 2px rgba(0,0,0,.35)}
:host :is(.bm-sw.on,.hd-row.on .hd-sw){background:var(--th)!important}
:host :is(.bm-sw.on,.hd-row.on .hd-sw)::after{background:#fff!important}
:host .st-range,:host .st-hsl input{border-radius:999px}
:host .st-range{height:6px;background:rgba(255,255,255,.12)}
:host .st-prev{border-radius:999px}
:host .st-swatch.sel{border-color:var(--sk-fg)}
:host :is(.bm-item,.hd-row,.hs-row,.st-row,.st-block,.ch-box,.cal-det,.cal-models,.cal-panel,.cal-tbl,.bm-setp,.mv-setp,.cal-mrow:not(.on),.cal-day:not(.sel),.st-tile:not(.on)){background:var(--sk-fill)!important;border:1px solid var(--sk-line)!important;border-radius:10px!important;box-shadow:none!important}
:host .fav-modal .bm-item{border-radius:14px!important}
:host .cal-day{border-radius:10px!important}
:host :is(.bm-item,.hd-row,.hs-row,.cal-day:not(.sel),.cal-mrow:not(.on),.st-tile:not(.on)):hover{background:var(--sk-hov)!important;border-color:var(--sk-line2)!important;transform:none!important;box-shadow:none!important}
:host .cal-day.today:not(.sel){border-color:var(--th)!important;box-shadow:inset 0 0 0 1px var(--th)!important}
:host .cal-day.sel{background:var(--th)!important;border-color:var(--th)!important;color:#fff!important;box-shadow:var(--sk-hi)!important}
:host .st-tile.on{background:var(--sk-hov)!important;border-color:var(--th)!important;color:var(--sk-fg)!important}
:host .fav-modal .bm-item.pinned{border-color:rgba(245,158,11,.32)!important}
:host .fav-modal .bm-gallery-mode .bm-item[data-st="1"]{border-color:rgba(16,185,129,.5)!important}
:host .fav-modal .bm-gallery-mode .bm-item[data-st="2"]{border-color:rgba(245,158,11,.5)!important}
:host .fav-modal .bm-gallery-mode .bm-item[data-st="3"]{border-color:rgba(139,92,246,.55)!important}
:host .fav-modal .bm-item[data-st="1"]{--st:#10b981}
:host .fav-modal .bm-item[data-st="2"]{--st:#f59e0b}
:host .fav-modal .bm-item[data-st="3"]{--st:#8b5cf6}
:host .bm-gallery-mode .bm-item:hover{box-shadow:none!important}
:host .bm-topic,:host .fav-modal .bm-topic{background:var(--sk-fill)!important;border:1px solid var(--sk-line)!important;border-left:2px solid var(--th)!important;border-radius:8px!important;color:var(--sk-fg2)!important}
:host([data-rtl]) .bm-topic,:host([data-rtl]) .fav-modal .bm-topic{border-left:1px solid var(--sk-line)!important;border-right:2px solid var(--th)!important}
:host .bm-avatar,:host .hs-ava,:host .cal-ava{background:var(--sk-fill2)}
:host .fav-modal .bm-prev,:host .bm-gallery-mode .bm-avatar,:host .bm-gallery-mode img[data-frame]{border-radius:8px!important}
:host .bm-empty::before{opacity:.25}
:host :is(.bm-del,.hs-btn.del){background:var(--sk-fill)!important;border:1px solid var(--sk-in)!important;color:var(--sk-err-fg)!important;border-radius:8px!important}
:host :is(.bm-del,.hs-btn.del):hover{background:rgba(239,68,68,.16)!important;border-color:rgba(239,68,68,.32)!important;color:var(--sk-err-fg)!important}
:host :is(.bm-pin){background:var(--sk-fill)!important;border:1px solid var(--sk-in)!important;color:var(--sk-mfg)!important;border-radius:8px!important}
:host .bm-pin:hover{background:rgba(245,158,11,.14)!important;color:var(--sk-warn-fg)!important;border-color:rgba(245,158,11,.32)!important}
:host .bm-pin.on,:host .hs-btn.on{color:var(--sk-warn-fg)!important}
:host .bm-pin.on{background:rgba(245,158,11,.14)!important;border-color:rgba(245,158,11,.4)!important}
:host .bm-gallery-mode :is(.bm-del,.bm-pin){background:rgba(10,10,10,.68)!important;border:1px solid rgba(255,255,255,.12)!important;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}
:host .bm-gallery-mode .bm-del:hover{background:rgba(239,68,68,.55)!important;color:#fff!important}
:host .bm-gallery-mode .bm-pin.on{background:rgba(245,158,11,.3)!important}
:host .bm-gallery-mode .bm-act-btn{background:var(--sk-fill)!important}
:host .fav-modal .bm-gallery-mode .bm-nwrap :is(.bm-view,.bm-seen){background:rgba(10,10,10,.68)!important;color:#fff!important;border-radius:6px!important;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}
:host .cal-b{background:rgba(16,185,129,.16);color:var(--sk-ok-fg);border-radius:6px}
:host .cal-b.s{background:rgba(59,130,246,.16);color:var(--sk-info-fg)}
:host .cal-day.sel .cal-b{background:rgba(0,0,0,.3);color:#fff}
:host .cal-seg{background:var(--sk-info)}
:host .cal-bar,:host .ch-track{background:var(--sk-hov);border-radius:999px}
:host .cal-dot{background:var(--sk-ok);box-shadow:none}
:host .cal-thead,:host .cal-tfoot{background:var(--sk-fill2)!important}
:host .cal-thead>span{color:var(--sk-mfg)}
:host .cal-trow:nth-child(even){background:rgba(255,255,255,.02)}
:host .cal-sep,:host .cal-panel .cal-sep,:host .cal-fold{border-color:var(--sk-line)!important}
:host .ch-bar{border-radius:4px 4px 0 0}
:host .ch-fill{border-radius:999px}
:host .hs-sep,:host .fav-modal .bm-chips .bm-chips-sep{background:var(--sk-line2)}
:host :is(.bm-list,.hs-pane,.cal-pane,.cal-models,.cal-panel,.bm-tags-wrap){scrollbar-color:rgba(255,255,255,.14) transparent!important}
:host .bm-list::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:999px}`;
const DLG_SKIN = `.dlg-ov{background:rgba(10,10,10,.64);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}
.dlg-card{background:#111111;border:1px solid rgba(255,255,255,.08);border-radius:18px;box-shadow:inset 0 1px rgba(255,255,255,.04),0 24px 72px -20px rgba(0,0,0,.9);color:#f5f5f5;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}
.dlg-row{border-radius:8px;color:#f5f5f5;font-weight:500}
.dlg-row:hover{background:rgba(255,255,255,.04)}
.dlg-row:active{background:rgba(255,255,255,.06)}
.dlg-row.on{background:rgba(255,255,255,.05)}
.dlg-dot{border:2px solid rgba(255,255,255,.25)}
.dlg-row.on .dlg-dot{border-color:var(--th,#346bf1);background:radial-gradient(circle,var(--th,#346bf1) 0 45%,transparent 50%)}
.dlg-new{border-top:1px solid rgba(255,255,255,.06)}
.dlg-in{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);border-radius:10px;color:#f5f5f5;-webkit-text-fill-color:#f5f5f5}
.dlg-in:focus{border-color:var(--th,#346bf1)}
.dlg-ok{background:var(--th,#346bf1);border:1px solid var(--th,#346bf1);border-radius:8px;font-weight:500;box-shadow:inset 0 1px rgba(255,255,255,.16)}
.dlg-ok:active{transform:scale(.97)}`;
const FRAME_SKIN = `.fr-ov{background:rgba(10,10,10,.8)}
.fr-img{border-radius:10px;box-shadow:0 24px 72px -20px rgba(0,0,0,.9)}
.fr-msg{background:#111111;border:1px solid rgba(255,255,255,.08);border-radius:14px;color:#f5f5f5;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif}`;
const STAR_SKIN = `.bcu-star{border-radius:8px;background:rgba(10,10,10,.7);border:1px solid rgba(255,255,255,.14);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}
.bcu-star:hover{transform:scale(1.06);background:rgba(10,10,10,.86)}
.bcu-star.on{color:#fbbf24}
.bcu-ring{border-color:#fbbf24}`;
const RAD_SKIN = `.rm-fab{border-radius:14px;border:1px solid rgba(255,255,255,.16);box-shadow:inset 0 1px rgba(255,255,255,.16),0 8px 24px -10px rgba(0,0,0,.8)}
.rm-fab.moving{box-shadow:0 0 0 4px rgba(255,255,255,.12),0 12px 28px -10px rgba(0,0,0,.8)}
.rm-ov{background:rgba(10,10,10,.5)}
.rm-ring{border-color:rgba(255,255,255,.06)}
.rm-hub{background:rgba(17,17,17,.94);border:1px solid rgba(255,255,255,.12);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;font-weight:600}
.rm-node{background:rgba(17,17,17,.94);border:1px solid rgba(255,255,255,.1);box-shadow:0 8px 20px -8px rgba(0,0,0,.8)}
.rm-node.grp::after{border:1px solid #0a0a0a}
.rm-node.hot{border-color:rgba(255,255,255,.4)}`;
const STAR_CSS = `.bcu-host{position:relative!important}
.bcu-star{box-sizing:border-box;padding:0;line-height:0;position:absolute;left:6px;bottom:6px;z-index:6;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:50%;background:rgba(0,0,0,.55);color:#fff;cursor:pointer;opacity:.92;transition:transform .15s,background .15s,color .15s;-webkit-tap-highlight-color:transparent;user-select:none;touch-action:manipulation}
a.thm_a>.bcu-star{left:auto;right:6px}
a.lst_link>.bcu-star{left:auto;right:6px;bottom:auto;top:50%;margin-top:-15px}
a[data-model-list-link]>.bcu-star{left:auto;right:6px;bottom:auto;top:50%;margin-top:-15px}
.bcu-star svg{display:block;flex:none;margin:0;width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2;stroke-linejoin:round;pointer-events:none}
.bcu-star:hover{transform:scale(1.12);background:rgba(0,0,0,.78)}
.bcu-star.on{color:#ffc83d}
.bcu-star.on svg{fill:currentColor}
.bcu-ring{display:none;position:absolute;inset:0;z-index:5;border:2px solid #ffc83d;border-radius:inherit;box-sizing:border-box;pointer-events:none}
.bcu-fav>.bcu-ring{display:block}
`;

class Helpers {
    static getModelSecretData() {
        try {
            for (const el of document.querySelectorAll('script[type="application/json"]')) {
                const t = el.textContent;
                if (!/chatHost|chatHeaderOptions|chatLocalData/.test(t)) continue;
                if (this._secretSrc === t) return this._secretData;
                this._secretSrc = t;
                this._secretData = null;
                return (this._secretData = JSON.parse(t));
            }
        } catch {}
        return null;
    }
    static createElement(tag, className = '') {
        const el = document.createElement(tag);
        if (className) el.className = className;
        return el;
    }
    static createButton(iconHTML, label, className = '') {
        const b = this.createElement('button', `btn ${className}`.trim());
        b.innerHTML = `<span>${iconHTML}</span><sub>${label}</sub>`;
        return b;
    }
    static formatTime(s) {
        const p = n => String(n | 0).padStart(2, '0');
        return `${p(s / 60)}:${p(s % 60)}`;
    }
    static getTimestamp() {
        const d = new Date(), p = n => String(n).padStart(2, '0');
        return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}-${p(d.getMinutes())}-${p(d.getSeconds())}`;
    }
    static download(blob, filename) {
        const url = URL.createObjectURL(blob), a = document.createElement('a');
        a.style.display = 'none';
        a.href = url;
        a.download = String(filename).replace(/[\\/:*?"<>|]+/g, '_');
        document.body.appendChild(a);
        a.click();
        setTimeout(() => { a.remove(); URL.revokeObjectURL(url); }, 60000);
    }
    static bindLongPress(el, onClick, onHold, ms = 400) {
        let timer = 0, fired = false, active = false, sx = 0, sy = 0;
        const clear = () => { clearTimeout(timer); timer = 0; };
        const reset = () => { active = false; fired = false; clear(); };
        const safe = (fn, e, what) => { try { fn(e); } catch (err) { console.error('[BCU] ' + what, err); } };
        el.oncontextmenu = e => e.preventDefault();
        el.onpointerdown = e => {
            if (!isPrimaryPress(e)) return;
            active = true;
            fired = false;
            sx = e.clientX; sy = e.clientY;
            clear();
            timer = setTimeout(() => { timer = 0; if (!active) return; fired = true; safe(onHold, e, 'hold'); }, ms);
        };
        el.onpointermove = e => {
            if (active && !fired && Math.hypot(e.clientX - sx, e.clientY - sy) > 14) reset();
        };
        el.onpointerup = e => {
            if (!active) return;
            const wasHold = fired;
            reset();
            if (!wasHold) safe(onClick, e, 'click');
        };
        el.onpointercancel = reset;
        el.onpointerleave = e => { if (e.pointerType === 'mouse') reset(); };
    }
    static snap(v, name, done) {
        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 done(false);
                Helpers.download(b, `${name || 'Cam'}_${Helpers.getTimestamp()}.jpg`);
                done(true);
            }, 'image/jpeg', 0.95);
        } catch { done(false); }
    }
    static bindHold(el, { ms = 450, dist = 10, ok, onHold, always = false }) {
        let t = 0, done = false, armed = false, x = 0, y = 0, type = 'mouse';
        const clear = () => { clearTimeout(t); t = 0; };
        el.addEventListener('pointerdown', e => {
            clear();
            done = false;
            armed = false;
            if (e.pointerType === 'mouse' && e.button !== 0) return;
            if (ok && !ok(e)) return;
            armed = true;
            type = e.pointerType;
            x = e.clientX;
            y = e.clientY;
            t = setTimeout(() => { t = 0; done = true; onHold(e); }, ms);
        });
        el.addEventListener('pointermove', e => { if (t && Math.hypot(e.clientX - x, e.clientY - y) > dist) clear(); });
        ['pointerup', 'pointercancel', 'pointerleave'].forEach(n => el.addEventListener(n, clear));
        el.addEventListener('click', e => { if (done) { done = false; e.preventDefault(); e.stopImmediatePropagation(); } }, true);
        el.addEventListener('contextmenu', e => { if (always || (armed && (type !== 'mouse' || done))) e.preventDefault(); });
    }
    static trackDrag(e, { onMove, onEnd, minDist = 2, capEl }) {
        const el = capEl || e.target, sX = e.clientX, sY = e.clientY;
        try { el.setPointerCapture(e.pointerId); } catch {}
        let moved = false;
        const move = ev => {
            const dx = ev.clientX - sX, dy = ev.clientY - sY;
            if (!moved && Math.hypot(dx, dy) > minDist) moved = true;
            if (moved) onMove(dx, dy, ev);
        };
        const end = ev => {
            try { el.releasePointerCapture(ev.pointerId); } catch {}
            window.removeEventListener('pointermove', move);
            window.removeEventListener('pointerup', end);
            window.removeEventListener('pointercancel', end);
            onEnd?.(moved, ev);
        };
        window.addEventListener('pointermove', move, { passive: false });
        window.addEventListener('pointerup', end);
        window.addEventListener('pointercancel', end);
    }
    static copy(text, ok, err) {
        text = String(text);
        const legacy = () => {
            try {
                const ta = document.createElement('textarea');
                ta.value = text;
                ta.setAttribute('readonly', '');
                ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none';
                (document.body || document.documentElement).appendChild(ta);
                ta.select();
                ta.setSelectionRange(0, text.length);
                const done = document.execCommand('copy');
                ta.remove();
                return done;
            } catch { return false; }
        };
        const fail = e => { if (legacy()) ok?.(); else err?.(e); };
        try {
            if (navigator.clipboard?.writeText) navigator.clipboard.writeText(text).then(() => ok?.(), fail);
            else fail();
        } catch (e) { fail(e); }
    }
    static flashBtn(btn, ok) {
        const orig = btn.innerHTML;
        btn.innerHTML = ok ? icons.ok : '<span style="color:#e74c3c">ERR</span>';
        setTimeout(() => { if (btn.isConnected) btn.innerHTML = orig; }, 1000);
    }
}

const mk = (tag, className) => Helpers.createElement(tag, className);
const bindGear = (btn, panel) => { btn.onclick = () => { const o = panel.classList.toggle('open'); btn.classList.toggle('on', o); }; };
const mkSetRow = (label, ctl) => { const r = mk('div', 'bm-setrow'); const t = mk('span'); t.textContent = label; r.append(t, ctl); return r; };
const isPrimaryPress = e => e.button === 0 || e.pointerType !== 'mouse';
const swallow = e => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); };
const bindEscape = close => {
    const onKey = e => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); close(); } };
    document.addEventListener('keydown', onKey, true);
    return () => document.removeEventListener('keydown', onKey, true);
};
const onRemoteChange = (key, handler) => {
    if (typeof GM_addValueChangeListener !== 'function') return;
    try { GM_addValueChangeListener(Store.key(key), (n, o, v, remote) => { if (remote) handler(v); }); } catch {}
};

const injectShadowCss = (shadow, key, css, skin) => {
    if (shadow[key]) return;
    shadow[key] = true;
    const st = document.createElement('style');
    if (skin == null) st.textContent = css; else skinCss(st, css, skin);
    shadow.appendChild(st);
};

const DLG_CSS = `
.bm-grp-btn{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;text-align:start;cursor:pointer;font-family:inherit}
.bm-grp-arr{opacity:.6;font-size:12px}
.dlg-ov{pointer-events:auto;position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;padding:16px;background:rgba(0,0,0,.55);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);animation:dlgIn .15s ease}
@keyframes dlgIn{from{opacity:0}to{opacity:1}}
.dlg-card{width:min(340px,100%);max-height:80%;display:flex;flex-direction:column;background:#1c1c20;border:1px solid rgba(255,255,255,.1);border-radius:14px;box-shadow:0 18px 50px rgba(0,0,0,.6);overflow:hidden;color:#eee;font-size:15px}
.dlg-list{overflow-y:auto;padding:6px}
.dlg-row{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;padding:13px 12px;border:0;border-radius:8px;background:transparent;color:#eee;font:inherit;text-align:start;cursor:pointer}
.dlg-row:active{background:rgba(255,255,255,.08)}
.dlg-row.on{background:rgba(255,255,255,.07)}
.dlg-dot{flex:none;width:18px;height:18px;border-radius:50%;border:2px solid rgba(255,255,255,.35)}
.dlg-row.on .dlg-dot{border-color:var(--th,#4aa3ff);background:radial-gradient(circle,var(--th,#4aa3ff) 0 45%,transparent 50%)}
.dlg-txt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.dlg-new{display:flex;gap:8px;padding:10px;border-top:1px solid rgba(255,255,255,.08)}
.dlg-in{flex:1;min-width:0;height:40px;padding:0 12px;border-radius:8px;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.12);color:#f0f0f0;-webkit-text-fill-color:#f0f0f0;font:inherit;outline:none}
.dlg-in:focus{border-color:var(--th,#4aa3ff)}
.dlg-ok{flex:none;height:40px;padding:0 14px;border:0;border-radius:8px;background:var(--th,#4aa3ff);color:#fff;font:inherit;font-weight:600;cursor:pointer}
`;

const openGroupDialog = (ui, { current, groups, onPick }) => {
    const shadow = ui.shadow;
    injectShadowCss(shadow, '_dlgCss', DLG_CSS, DLG_SKIN);
    const ov = mk('div', 'dlg-ov');
    const card = mk('div', 'dlg-card');
    const list = mk('div', 'dlg-list');
    const unbindEsc = bindEscape(() => close());
    const close = () => { ov.remove(); unbindEsc(); };
    const pick = v => { close(); if (v !== current) onPick(v); };

    const row = (label, value) => {
        const r = mk('button', 'dlg-row' + (value === current ? ' on' : ''));
        r.type = 'button';
        const t = mk('span', 'dlg-txt'); t.innerHTML = `${icons.folder}<span>${escHTML(label)}</span>`;
        r.append(t, mk('span', 'dlg-dot'));
        r.onclick = e => { e.stopPropagation(); pick(value); };
        return r;
    };
    list.append(row(L().grp_none, ''), ...groups.map(g => row(g, g)));

    const nw = mk('div', 'dlg-new');
    const inp = mk('input', 'dlg-in');
    inp.type = 'text'; inp.maxLength = 40; inp.placeholder = L().grp_new.replace(/^\+\s*/, '').replace(/…$/, '') + '…';
    inp.autocomplete = 'off'; inp.spellcheck = false;
    const ok = mk('button', 'dlg-ok'); ok.type = 'button'; ok.textContent = '+';
    const submit = () => { const v = inp.value.trim().slice(0, 40); if (v) pick(v); else inp.focus(); };
    ok.onclick = e => { e.stopPropagation(); submit(); };
    inp.onkeydown = e => { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); submit(); } };
    inp.onkeyup = e => e.stopPropagation();
    nw.append(inp, ok);

    card.append(list, nw);
    card.onclick = e => e.stopPropagation();
    ov.onclick = close;
    ov.append(card);
    shadow.appendChild(ov);
};

const FRAME_CSS = `.fr-ov{pointer-events:auto;position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;padding:16px;background:rgba(0,0,0,.8);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}.fr-card{max-width:100%;max-height:100%;display:flex;align-items:center;justify-content:center}.fr-img{max-width:100%;max-height:90vh;border-radius:3px;box-shadow:0 18px 50px rgba(0,0,0,.6);display:block}.fr-spd{position:absolute;left:50%;bottom:calc(18px + env(safe-area-inset-bottom,0px));transform:translateX(-50%);min-width:64px;padding:10px 18px;font:bold 15px/1 system-ui,sans-serif;color:#eee;background:rgba(28,28,32,.92);border:1px solid rgba(255,255,255,.18);border-radius:12px;cursor:pointer}.fr-msg{color:#eee;font-size:15px;text-align:center;padding:18px 22px;border-radius:12px;background:#1c1c20;border:1px solid rgba(255,255,255,.1)}`;

const loadFrame = async (url, gmFirst) => {
    if (gmFirst) { const g = await gmImgData(url, true).catch(() => ''); if (g) return g; }
    const ok = await new Promise(res => {
        const t = new Image();
        t.onload = () => res(true);
        t.onerror = () => res(false);
        t.src = url;
    });
    return ok ? url : gmImgData(url, true).catch(() => '');
};

const frameShowAvatar = async (b, fv) => {
    const list = [toHttps(b.avatar), toHttps(histLoad().m[bmKey(b.url)]?.a)].filter(Boolean);
    for (const u of list) {
        const d = await loadFrame(u);
        if (fv.closed) return;
        if (d) {
            fv.img.style.cssText = 'width:min(100%,560px);height:auto;object-fit:contain';
            fv.img.src = d;
            fv.card.replaceChildren(fv.img);
            return;
        }
    }
    fv.msg.textContent = L().fr_none;
    fv.card.replaceChildren(fv.msg);
};

const PREV_SPD = [1000, 500, 0];
const prevSpd = () => { const v = Store.get('bc_prev_spd', 1000); return PREV_SPD.includes(v) ? v : 1000; };
const prevSpdNext = () => PREV_SPD[(PREV_SPD.indexOf(prevSpd()) + 1) % PREV_SPD.length];
const prevSpdTitle = () => prevSpd() ? L().st_prev_spd : `${L().st_prev_spd}: ${L().st_prev_off}`;
const prevSpdLabel = v => v ? `${(v / 1000).toFixed(1)}s` : '0';

const frameLive = (b, fv, frame, onFirstFail) => {
    const tick = async () => {
        if (fv.closed || fv.busy) return;
        fv.busy = true;
        clearTimeout(fv.timer);
        const url = frameBust(frame);
        const d = await loadFrame(url, !!b._lastData);
        fv.busy = false;
        if (fv.closed) return;
        if (d) {
            fv.img.src = d;
            if (d === url) { b._lastImg = url; b._lastData = ''; } else b._lastData = d;
        } else if (!fv.img.isConnected) {
            return onFirstFail();
        }
        const sp = prevSpd();
        if (sp) fv.timer = setTimeout(tick, sp);
    };
    fv.tick = tick;
    tick();
};

const frameViaProfile = async (b, fv) => {
    const fail = () => { if (!fv.closed) frameShowAvatar(b, fv); };
    const h = await requestProfile(b);
    if (fv.closed) return;
    const info = h ? parseProfile(h, urlSite(b.url), modelSlug(b.url)) : null;
    const fr = info && info.status ? info.frame : '';
    if (!fr) return fail();
    frameLive(b, fv, fr, fail);
};

const openFrameView = (ui, b) => {
    const shadow = ui.shadow;
    injectShadowCss(shadow, '_frCss', FRAME_CSS, FRAME_SKIN);
    const ov = mk('div', 'fr-ov');
    const card = mk('div', 'fr-card');
    const msg = mk('div', 'fr-msg');
    msg.textContent = '…';
    const img = mk('img', 'fr-img');
    const fv = { closed: false, timer: 0, img, card, msg };
    const spdBtn = mk('button', 'fr-spd');
    const paintSpd = () => { spdBtn.textContent = prevSpdLabel(prevSpd()); spdBtn.title = prevSpdTitle(); };
    paintSpd();
    spdBtn.onclick = e => {
        e.stopPropagation();
        Store.set('bc_prev_spd', prevSpdNext());
        paintSpd();
        clearTimeout(fv.timer);
        if (prevSpd() && fv.tick) fv.tick();
    };
    img.onload = () => { if (!fv.closed && !img.isConnected) card.replaceChildren(img); };
    const unbindEsc = bindEscape(() => close());
    const close = () => { fv.closed = true; clearTimeout(fv.timer); ov.remove(); unbindEsc(); };
    ov.onclick = close;
    card.append(msg);
    ov.append(card, spdBtn);
    shadow.appendChild(ov);
    if (b.frame && (b.status ?? 0) !== 0) {
        const cached = b._lastData || b._lastImg;
        if (cached) img.src = cached;
        frameLive(b, fv, b.frame, () => frameViaProfile(b, fv));
    } else frameViaProfile(b, fv);
};

const BM_HLS = new Map();
const copyHls = (u, cb) => u ? Helpers.copy(u, () => cb(true), () => cb(false)) : cb(false);
const shareHls = (u, title, cb) => (u && navigator.share) ? navigator.share({ title, url: u }).catch(() => {}) : cb(false);

const largestVideo = () => {
    let best = null, max = 0;
    for (const cur of document.querySelectorAll('video')) {
        const area = (cur.videoWidth * cur.videoHeight) || (cur.offsetWidth * cur.offsetHeight) || 0;
        if (area > max) { max = area; best = cur; }
    }
    return best;
};
const selectorVideo = () => currentSite.videoSelectors.map(s => safeQuery(s)).find(el => el?.tagName === 'VIDEO');
const safeClosest = (el, sel) => { try { return el.closest(sel); } catch { return null; } };
const safeQuery = sel => { try { return document.querySelector(sel); } catch { return null; } };
const isQualityShown = q => q.style.display === 'block' || (!q.classList.contains('__hidden') && q.offsetWidth > 0);
const jq = () => window.unsafeWindow?.$ || window.$ || window.jQuery;
const fsActive = () => document.fullscreenElement || document.webkitFullscreenElement;
const fsRequest = el => (el.requestFullscreen || el.webkitRequestFullscreen)?.call(el);
const fsExit = () => (document.exitFullscreen || document.webkitExitFullscreen)?.call(document);
const containerOf = v => currentSite.containerSelectors.map(s => safeClosest(v, s)).find(Boolean) || v.parentElement;
const isTyping = e => {
    try { const t = e.composedPath()[0]; return !!(t?.matches?.('input, textarea, select') || t?.isContentEditable); } catch { return false; }
};
const volIcon = (vol, muted) => muted ? icons.mute : vol < 0.4 ? icons.volLow : vol < 0.75 ? icons.volMed : icons.vol;
const VOL_STEP = { ArrowUp: 0.1, ArrowDown: -0.1 };
const VJS_UI = '.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';

class VideoController {
    constructor(uiController) {
        this.ui = uiController;
        this.cachedVideo = null;
        this.lastVolume = 1;
        this.isTheater = false;
        this.theaterWrap = null;
        this._anc = null;
        this.bindEvents();
        this.bindGlobalTap();
        this._findTimer = setInterval(() => { if (!realHidden()) this.findVideo(); }, 2000);
        onPageEvent('visibilitychange', () => { if (!realHidden()) this.findVideo(); });
    }

    bindEvents() {
        const actions = {
            KeyM: () => this.toggleMute(),
            KeyF: () => this.toggleFullscreen(),
            KeyP: () => this.cachedVideo && this.togglePiP(),
            KeyT: () => this.toggleTheater(),
            KeyS: () => this.takeScreenshot(),
            KeyH: () => this.ui.toggleHide(!AppState.isHidden)
        };
        const hasMod = e => e.ctrlKey || e.altKey || e.metaKey;
        window.addEventListener('keydown', e => {
            if (isTyping(e) || hasMod(e)) return;
            const act = actions[e.code];
            if (act) { e.preventDefault(); if (!e.repeat) act(); return; }
            const step = VOL_STEP[e.code];
            if (e.shiftKey && step && this.cachedVideo) {
                e.preventDefault();
                this.setVolume(this.cachedVideo.volume + step);
            }
        });
    }

    findVideo() {
        const cv0 = this.cachedVideo;
        if (cv0?.isConnected) {
            if (cv0.videoWidth || cv0.offsetWidth) return;
            const alt = largestVideo();
            if (!alt || alt === cv0 || !(alt.videoWidth || alt.offsetWidth)) return;
        }
        if (this.cachedVideo) {
            if (this.isTheater) this.toggleTheater();
            this.cachedVideo = this.theaterWrap = null;
        }
        const v = largestVideo() || selectorVideo() || null;
        if (v !== this.cachedVideo) {
            this.cachedVideo = v;
            if (v) this.attachInteractions(v);
        }
    }

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

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

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

        if (!/(^|\.)chaturbate\.com$/.test(location.hostname)) {
            v.style.setProperty('z-index', '9999', 'important');
            v.parentElement?.style.setProperty('z-index', '9999', 'important');
        } else {
            const box = containerOf(v);
            if (box && box !== v) {
                try { if (getComputedStyle(box).position === 'static') box.style.setProperty('position', 'relative', 'important'); } catch {}
                box.style.setProperty('z-index', '9999', 'important');
            }
            addStyle('bc-cb-controls-z', '#mobileVideoControls,.MobileVideoControls{z-index:10000!important}');
        }
        const target = containerOf(v);
        if (target && !target._bcTouchBound) {
            target._bcTouchBound = true;
            this.bindWheel(target);
            this.bindDoubleClick(target);
        }
        this.syncUI();
    }

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

    bindDoubleClick(target) {
        let clicks = 0, timer = null, sX = 0, sY = 0;
        target.addEventListener('pointerdown', e => { sX = e.clientX; sY = e.clientY; }, { passive: true, capture: true });
        target.addEventListener('click', e => {
            if (Date.now() - (this._tapAt || 0) < 600) { swallow(e); return; }
            if (safeClosest(e.target, VJS_UI)) return;
            if (Math.abs(e.clientX - sX) > 10 || Math.abs(e.clientY - sY) > 10) return;
            if (++clicks === 1) { timer = setTimeout(() => { clicks = 0; }, 250); return; }
            swallow(e);
            clearTimeout(timer);
            clicks = 0;
            this.toggleFullscreen();
        }, true);
    }

    bindGlobalTap() {
        let dX = 0, dY = 0, dT = 0, lastT = 0, lastX = 0, lastY = 0;
        const inUi = e => { try { return e.composedPath().some(n => n && (n.id === 'bc-unchained-host' || n.id === 'bc-cb-qual')); } catch { return false; } };
        const box = () => { const v = this.cachedVideo; return v && v.isConnected ? (containerOf(v) || v) : null; };
        const smallControl = (el, r) => {
            const c = safeClosest(el, VJS_UI);
            if (!c) return false;
            const cr = c.getBoundingClientRect();
            return cr.width * cr.height < r.width * r.height * 0.5;
        };
        window.addEventListener('pointerdown', e => {
            if (e.pointerType === 'mouse') return;
            dX = e.clientX; dY = e.clientY; dT = Date.now();
        }, { capture: true, passive: true });
        window.addEventListener('pointerup', e => {
            if (e.pointerType === 'mouse' || !e.isPrimary || inUi(e)) return;
            const now = Date.now();
            if (now - dT > 350 || Math.abs(e.clientX - dX) > 10 || Math.abs(e.clientY - dY) > 10) { lastT = 0; return; }
            const b = box();
            if (!b) { lastT = 0; return; }
            const r = b.getBoundingClientRect();
            if (r.width < 1 || e.clientX < r.left || e.clientX > r.right || e.clientY < r.top || e.clientY > r.bottom || smallControl(e.target, r)) { lastT = 0; return; }
            if (lastT && now - lastT < 350 && Math.abs(e.clientX - lastX) < 40 && Math.abs(e.clientY - lastY) < 40) {
                lastT = 0;
                this._tapAt = now;
                this.toggleFullscreen();
                return;
            }
            lastT = now; lastX = e.clientX; lastY = e.clientY;
        }, { capture: true, passive: true });
    }

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

    clickSiteUnmute() {
        try {
            const sb = document.querySelector('.stream_muted_button');
            if (sb && sb.offsetParent !== null) sb.click();
        } catch {}
    }

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

    unmuteVideo(v) {
        if (AppState.gmute) setGMute(false);
        this.clickSiteUnmute();
        v.muted = false;
        v.volume = this.lastVolume || 1;
    }

    muteVideo(v) {
        v.muted = true;
        this.lastVolume = v.volume || 1;
    }

    pipPoster(v, on) {
        try {
            if (on) {
                if (v.hasAttribute('poster') && !v._bcPoster) return;
                if (!v.videoWidth || !v.videoHeight) return;
                const w = 320, h = Math.max(1, Math.round(w * v.videoHeight / v.videoWidth));
                const c = document.createElement('canvas');
                c.width = w; c.height = h;
                c.getContext('2d').drawImage(v, 0, 0, w, h);
                const url = c.toDataURL('image/jpeg', 0.7);
                if (url && url.length > 100) { v._bcPoster = true; v.setAttribute('poster', url); }
            } else if (v._bcPoster) {
                v._bcPoster = false;
                v.removeAttribute('poster');
            }
        } catch {}
    }

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

    toggleFullscreen() {
        const t = currentSite.containerSelectors.map(s => safeQuery(s)).find(Boolean) || (this.cachedVideo && containerOf(this.cachedVideo)) || this.cachedVideo;
        if (!t) return;
        try { fsActive() ? fsExit() : fsRequest(t); } catch {}
    }

    enterTheater(v) {
        this.theaterWrap = containerOf(v);
        this.theaterWrap?.classList.add('bc-thtr-wrap');
        v.classList.add('bc-thtr-video');
        this._anc = [];
        for (let e = v.parentElement; e && !isRootEl(e); e = e.parentElement) {
            e.classList.add('bc-thtr-anc');
            this._anc.push(e);
        }
        window.scrollTo(0, 0);
    }

    leaveTheater(v) {
        this.theaterWrap?.classList.remove('bc-thtr-wrap');
        v?.classList.remove('bc-thtr-video');
        this._anc?.forEach(e => e.classList.remove('bc-thtr-anc'));
        this._anc = null;
        this.theaterWrap = null;
    }

    cbTheaterControls(on) {
        const id = 'bc-cb-controls-theater';
        document.getElementById(id)?.remove();
        if (!on || !/(^|\.)chaturbate\.com$/.test(location.hostname)) return;
        const st = document.createElement('style');
        st.id = id;
        st.textContent = '#mobileVideoControls,.MobileVideoControls{position:fixed!important;top:0!important;left:0!important;width:100vw!important;height:100vh!important;height:100dvh!important;z-index:2147483645!important}';
        (document.head || document.documentElement).appendChild(st);
    }

    toggleTheater() {
        const v = this.cachedVideo, on = !this.isTheater;
        if (on && !v) return;
        this.isTheater = on;
        this.ui?.elements?.bThtr?.classList.toggle('on', on);
        document.body.classList.toggle('bc-theater-mode', on);
        document.documentElement.classList.toggle('bc-theater-html', on);
        on ? this.enterTheater(v) : this.leaveTheater(v);
        this.cbTheaterControls(on);
        window.dispatchEvent(new Event('resize'));
    }

    takeScreenshot() {
        const v = this.cachedVideo;
        if (!v) return;
        const b = this.ui.elements.bShot;
        Helpers.snap(v, currentSite.getName(), ok => this.ui.flash(b, ok ? 'OK' : 'ERR'));
    }

    paintMuteBtn(btn, muted, gMute) {
        const sub = btn.querySelector('sub');
        if (sub) sub.textContent = gMute ? 'LOCK' : L().sound;
        btn.firstChild.innerHTML = muted ? icons.mute : icons.vol;
        btn.classList.toggle('on', muted);
        btn.style.boxShadow = gMute ? `inset 0 0 0 2px ${SK('#ef4444', '#e74c3c')}` : '';
    }

    syncUI() {
        const v = this.cachedVideo, ui = this.ui, el = ui.elements;
        if (!v || !el.bMute) return;
        const vol = v.muted ? 0 : v.volume;
        const muted = v.muted || vol === 0;
        const gMute = AppState.gmute;
        const key = `${vol}|${muted}|${gMute}`;
        if (ui._volKey === key) return;
        ui._volKey = key;

        this.paintMuteBtn(el.bMute, muted, gMute);
        el.vSlider.value = String(vol);
        el.vSlider.style.setProperty('--p', `${Math.round(vol * 100)}%`);
        el.vIco.innerHTML = volIcon(vol, muted);
        el.vVal.textContent = `${Math.round(vol * 100)}%`;
    }
}

const WEBM_HEAD = 8192;
const vintReader = head => (p, raw) => {
    const f = head[p];
    if (!f) return null;
    const len = Math.clz32(f) - 23;
    if (len > 8 || p + len > head.length) return null;
    const mask = 0xFF >> len;
    let v = raw ? f : (f & mask), ones = (f & mask) === mask;
    for (let i = 1; i < len; i++) { v = v * 256 + head[p + i]; if (head[p + i] !== 0xFF) ones = false; }
    return { v, len, unknown: ones };
};
const webmSegmentStart = rd => {
    const ebml = rd(0, true);
    if (!ebml || ebml.v !== 0x1A45DFA3) return -1;
    const es = rd(ebml.len, false);
    if (!es) return -1;
    const p = ebml.len + es.len + es.v;
    const seg = rd(p, true);
    if (!seg || seg.v !== 0x18538067) return -1;
    const ss = rd(p + seg.len, false);
    return ss?.unknown ? p + seg.len + ss.len : -1;
};
const webmInfoScale = (rd, head, ds, de) => {
    let q = ds, scale = 1e6;
    while (q < de) {
        const cid = rd(q, true);
        const csz = cid && rd(q + cid.len, false);
        if (!csz || cid.v === 0x4489) return null;
        const cs = q + cid.len + csz.len;
        if (cid.v === 0x2AD7B1) { scale = 0; for (let i = 0; i < csz.v; i++) scale = scale * 256 + head[cs + i]; }
        q = cs + csz.v;
    }
    return scale;
};
const webmWithDuration = (blob, head, p, idLen, ds, de, durationMs, scale) => {
    const dur = new Uint8Array(11);
    dur.set([0x44, 0x89, 0x88]);
    new DataView(dur.buffer).setFloat64(3, durationMs * 1e6 / scale);
    const size8 = new Uint8Array(8);
    size8[0] = 0x01;
    new DataView(size8.buffer).setUint32(4, (de - ds) + dur.length);
    return new Blob([
        head.subarray(0, p), head.subarray(p, p + idLen), size8,
        head.subarray(ds, de), dur, head.subarray(de),
        blob.slice(WEBM_HEAD)
    ], { type: blob.type });
};
const webmElement = (rd, p) => {
    const id = rd(p, true);
    const sz = id && rd(p + id.len, false);
    if (!sz || sz.unknown) return null;
    const ds = p + id.len + sz.len;
    return { id, ds, de: ds + sz.v };
};
const fixWebmDuration = async (blob, durationMs) => {
    const head = new Uint8Array(await blob.slice(0, WEBM_HEAD).arrayBuffer());
    const rd = vintReader(head);
    let p = webmSegmentStart(rd);
    while (p >= 0 && p < head.length) {
        const el = webmElement(rd, p);
        if (!el) break;
        if (el.id.v === 0x114D9B74) return blob;
        if (el.id.v === 0x1549A966) {
            const scale = el.de > head.length ? null : webmInfoScale(rd, head, el.ds, el.de);
            return scale === null ? blob : webmWithDuration(blob, head, p, el.id.len, el.ds, el.de, durationMs, scale);
        }
        p = el.de;
    }
    return blob;
};

const MediaFix = (() => {
    const cat = parts => {
        let n = 0;
        for (const p of parts) n += p.length;
        const o = new Uint8Array(n);
        let q = 0;
        for (const p of parts) { o.set(p, q); q += p.length; }
        return o;
    };
    const rd32 = (b, p) => ((b[p] << 24) | (b[p + 1] << 16) | (b[p + 2] << 8) | b[p + 3]) >>> 0;
    const rd64 = (b, p) => rd32(b, p) * 4294967296 + rd32(b, p + 4);
    const tag = (b, p) => String.fromCharCode(b[p], b[p + 1], b[p + 2], b[p + 3]);
    const w32 = n => { const o = new Uint8Array(4); new DataView(o.buffer).setUint32(0, n >>> 0); return o; };
    const w64 = n => { const o = new Uint8Array(8), dv = new DataView(o.buffer); dv.setUint32(0, Math.floor(n / 4294967296)); dv.setUint32(4, n >>> 0); return o; };
    const asc = s => Uint8Array.from(s, c => c.charCodeAt(0));
    const box = (type, ...parts) => {
        const body = cat(parts), o = new Uint8Array(8 + body.length);
        new DataView(o.buffer).setUint32(0, o.length);
        o.set(asc(type), 4);
        o.set(body, 8);
        return o;
    };
    const fullBox = (type, ver, flags, ...parts) => box(type, Uint8Array.of(ver, (flags >> 16) & 255, (flags >> 8) & 255, flags & 255), ...parts);
    const words = (list, per) => {
        const o = new Uint8Array(list.length * per * 4), dv = new DataView(o.buffer);
        let q = 0;
        for (const e of list) for (let i = 0; i < per; i++, q += 4) dv.setUint32(q, (per === 1 ? e : e[i]) >>> 0);
        return o;
    };
    const rle = arr => {
        const out = [];
        for (const v of arr) {
            const l = out[out.length - 1];
            if (l && l[0] === v) l[1]++; else out.push([v, 1]);
        }
        return out;
    };
    const kids = (b, s, e) => {
        const out = [];
        let p = s;
        while (p + 8 <= e) {
            let sz = rd32(b, p), hdr = 8;
            if (sz === 1) { sz = rd64(b, p + 8); hdr = 16; } else if (sz === 0) sz = e - p;
            if (sz < hdr || p + sz > e) break;
            out.push({ t: tag(b, p + 4), s: p, e: p + sz, d: p + hdr });
            p += sz;
        }
        return out;
    };
    const kid = (list, t) => list.find(x => x.t === t);
    const setDur = (copy, off, v1, value) => {
        const dv = new DataView(copy.buffer);
        if (v1) { dv.setUint32(off, Math.floor(value / 4294967296)); dv.setUint32(off + 4, value >>> 0); } else dv.setUint32(off, value >>> 0);
    };

    const req = (msg, ...xs) => { if (xs.some(x => !x)) throw new Error(msg); };
    const slice = (moov, c) => moov.slice(c.s, c.e);
    const rest = (moov, list, skip) => list.filter(c => !skip.includes(c.t)).map(c => slice(moov, c));
    const patchDur = (moov, c, off, v1, val) => { const cp = slice(moov, c); setDur(cp, off - c.s, v1, val); return cp; };
    const rleBox = (type, ver, arr) => {
        const r = rle(arr);
        return fullBox(type, ver, 0, words([r.length], 1), words(r.map(([v, c]) => [c, v]), 2));
    };

    const boxHead = (b, p, avail) => {
        let sz = rd32(b, p), hdr = 8;
        if (sz === 1) { sz = rd64(b, p + 8); hdr = 16; } else if (sz === 0) sz = avail;
        return sz < hdr ? null : { sz, hdr };
    };
    const scanTop = async (blob, size) => {
        const top = [];
        for (let p = 0; p + 8 <= size;) {
            const h = new Uint8Array(await blob.slice(p, Math.min(size, p + 16)).arrayBuffer());
            const bh = boxHead(h, 0, size - p);
            if (!bh || p + bh.sz > size) break;
            top.push({ t: tag(h, 4), s: p, e: p + bh.sz, d: p + bh.hdr });
            p += bh.sz;
        }
        return top;
    };
    const readTrex = (moov, mk) => {
        const trex = {};
        const mvex = kid(mk, 'mvex');
        if (!mvex) return trex;
        for (const c of kids(moov, mvex.d, mvex.e).filter(x => x.t === 'trex')) {
            trex[rd32(moov, c.d + 4)] = { dur: rd32(moov, c.d + 12), size: rd32(moov, c.d + 16), flags: rd32(moov, c.d + 20) };
        }
        return trex;
    };
    const parseTrak = (moov, trak, trex) => {
        const tk = kids(moov, trak.d, trak.e);
        const tkhd = kid(tk, 'tkhd'), mdia = kid(tk, 'mdia');
        req('bad trak', tkhd, mdia);
        const tv1 = moov[tkhd.d] === 1, idOff = tkhd.d + 4 + (tv1 ? 16 : 8);
        const id = rd32(moov, idOff);
        const mk2 = kids(moov, mdia.d, mdia.e);
        const mdhd = kid(mk2, 'mdhd'), minf = kid(mk2, 'minf');
        req('bad mdia', mdhd, minf);
        const dv1 = moov[mdhd.d] === 1, tsOff = mdhd.d + 4 + (dv1 ? 16 : 8);
        const mi = kids(moov, minf.d, minf.e), stbl = kid(mi, 'stbl');
        const stsd = stbl && kid(kids(moov, stbl.d, stbl.e), 'stsd');
        req('no stsd', stbl, stsd);
        return {
            id, trak, tk, tkhd, tv1, durOffT: idOff + 8, mdia, mk2, mdhd, dv1, ts: rd32(moov, tsOff), durOffM: tsOff + 4,
            minf, mi, stbl, stsd: moov.slice(stsd.s, stsd.e),
            dur: [], sz: [], sync: [], cto: [], chunks: [], dts: 0, start: 0, negCto: false,
            trex: trex[id] || { dur: 0, size: 0, flags: 0 }
        };
    };
    const parseTracks = (moov, mk, trex) => {
        const order = mk.filter(x => x.t === 'trak').map(trak => parseTrak(moov, trak, trex));
        if (!order.length) throw new Error('no tracks');
        const tracks = {};
        for (const tr of order) tracks[tr.id] = tr;
        return { tracks, order };
    };

    const TFHD_DEFAULTS = [[0x08, 'dur'], [0x10, 'size'], [0x20, 'flags']];
    const readTfhd = (b, tfhd, trex) => {
        const fl = rd32(b, tfhd.d) & 0xffffff;
        const th = { fl, baseOff: null, dur: trex.dur, size: trex.size, flags: trex.flags };
        let o = tfhd.d + 8;
        if (fl & 1) { th.baseOff = rd64(b, o); o += 8; }
        if (fl & 2) o += 4;
        for (const [bit, key] of TFHD_DEFAULTS) if (fl & bit) { th[key] = rd32(b, o); o += 4; }
        return th;
    };
    const fragBase = (th, first, moofStart, lastEnd) => {
        if (th.baseOff !== null) return th.baseOff;
        return ((th.fl & 0x20000) || first) ? moofStart : lastEnd;
    };
    const applyTfdt = (b, tf, tr) => {
        const tfdt = kid(tf, 'tfdt');
        if (!tfdt) return;
        const t = b[tfdt.d] === 1 ? rd64(b, tfdt.d + 4) : rd32(b, tfdt.d + 4);
        if (!tr.dur.length) { tr.start = t; tr.dts = t; return; }
        if (t === tr.dts) return;
        const nl = tr.dur[tr.dur.length - 1] + (t - tr.dts);
        if (nl > 0) tr.dur[tr.dur.length - 1] = nl;
        tr.dts = t;
    };
    const TRUN_FIELDS = [[0x100, 'd'], [0x200, 's'], [0x400, 'f']];
    const readSample = (b, q, rf, rv, smp) => {
        for (const [bit, key] of TRUN_FIELDS) if (rf & bit) { smp[key] = rd32(b, q); q += 4; }
        if (rf & 0x800) { smp.c = rv ? (rd32(b, q) | 0) : rd32(b, q); q += 4; }
        return q;
    };
    const readTrun = (b, run, base, nextPos, th, tr) => {
        const rf = rd32(b, run.d) & 0xffffff, rv = b[run.d], n = rd32(b, run.d + 4);
        let q = run.d + 8, pos = nextPos, firstFlags = null;
        if (rf & 1) { pos = base + (rd32(b, q) | 0); q += 4; }
        if (rf & 4) { firstFlags = rd32(b, q); q += 4; }
        let total = 0;
        for (let i = 0; i < n; i++) {
            const smp = { d: th.dur, s: th.size, f: (i === 0 && firstFlags !== null) ? firstFlags : th.flags, c: 0 };
            q = readSample(b, q, rf, rv, smp);
            if (smp.c < 0) tr.negCto = true;
            tr.dur.push(smp.d); tr.sz.push(smp.s); tr.sync.push(!(smp.f & 0x10000)); tr.cto.push(smp.c);
            tr.dts += smp.d; total += smp.s;
        }
        if (n) tr.chunks.push({ pos, n });
        return pos + total;
    };
    const readTraf = (b, traf, tracks, mo, ti, lastEnd) => {
        const tf = kids(b, traf.d, traf.e);
        const tfhd = kid(tf, 'tfhd');
        req('no tfhd', tfhd);
        const tr = tracks[rd32(b, tfhd.d + 4)];
        req('unknown track', tr);
        const th = readTfhd(b, tfhd, tr.trex);
        const base = fragBase(th, ti === 0, mo.s, lastEnd);
        applyTfdt(b, tf, tr);
        let nextPos = base;
        for (const run of tf.filter(x => x.t === 'trun')) {
            nextPos = readTrun(b, run, base, nextPos, th, tr);
            lastEnd = Math.max(lastEnd, nextPos);
        }
        return lastEnd;
    };
    const readMoof = async (blob, mo, tracks) => {
        const b = new Uint8Array(await blob.slice(mo.s, mo.e).arrayBuffer());
        const mf = kids(b, mo.d - mo.s, b.length);
        let lastEnd = 0;
        mf.filter(x => x.t === 'traf').forEach((traf, ti) => { lastEnd = readTraf(b, traf, tracks, mo, ti, lastEnd); });
    };

    const findMdat = (mdats, pos) => {
        let lo = 0, hi = mdats.length - 1;
        while (lo <= hi) {
            const mid = (lo + hi) >> 1;
            if (pos < mdats[mid].d) hi = mid - 1; else if (pos >= mdats[mid].e) lo = mid + 1; else return mid;
        }
        return -1;
    };
    const checkOverrun = (tr, mdats) => {
        let si = 0;
        for (const ch of tr.chunks) {
            let bytes = 0;
            for (let i = 0; i < ch.n; i++) bytes += tr.sz[si + i];
            si += ch.n;
            if (ch.pos + bytes > mdats[ch.k].e) throw new Error('chunk overruns mdat');
        }
    };
    const mapChunks = (order, mdats) => {
        let payload = 0;
        const mdOff = mdats.map(m => { const o = payload; payload += m.e - m.d; return o; });
        for (const tr of order) for (const ch of tr.chunks) {
            ch.k = findMdat(mdats, ch.pos);
            if (ch.k < 0) throw new Error('chunk outside mdat');
        }
        order.forEach(tr => checkOverrun(tr, mdats));
        return { payload, mdOff };
    };

    const stssBox = sync => {
        const idx = [];
        sync.forEach((s, i) => { if (s) idx.push(i + 1); });
        return fullBox('stss', 0, 0, words([idx.length], 1), words(idx, 1));
    };
    const stscBox = chunks => {
        const sc = [];
        let prev = -1;
        chunks.forEach((ch, i) => { if (ch.n !== prev) { sc.push([i + 1, ch.n, 1]); prev = ch.n; } });
        return fullBox('stsc', 0, 0, words([sc.length], 1), words(sc, 3));
    };
    const stcoBox = (tr, mp, payloadStart) => {
        const offs = tr.chunks.map(ch => payloadStart + mp.mdOff[ch.k] + (ch.pos - mp.mdats[ch.k].d));
        return mp.use64
            ? fullBox('co64', 0, 0, words([offs.length], 1), cat(offs.map(w64)))
            : fullBox('stco', 0, 0, words([offs.length], 1), words(offs, 1));
    };
    const buildStbl = (tr, mp, payloadStart) => {
        const parts = [tr.stsd, rleBox('stts', 0, tr.dur)];
        if (tr.cto.some(v => v !== 0)) parts.push(rleBox('ctts', tr.negCto ? 1 : 0, tr.cto));
        if (!tr.sync.every(Boolean)) parts.push(stssBox(tr.sync));
        parts.push(stscBox(tr.chunks), fullBox('stsz', 0, 0, w32(0), w32(tr.sz.length), words(tr.sz, 1)), stcoBox(tr, mp, payloadStart));
        return box('stbl', ...parts);
    };
    const buildTrak = (tr, mp, payloadStart) => {
        const { moov, movTs } = mp;
        const sum = tr.dur.reduce((a, v) => a + v, 0);
        const t0 = movTs ? Math.round(tr.start / tr.ts * movTs) : 0;
        const tdur = movTs ? Math.round(sum / tr.ts * movTs) : 0;
        const minf = box('minf', ...rest(moov, tr.mi, ['stbl']), buildStbl(tr, mp, payloadStart));
        const mdia = box('mdia', patchDur(moov, tr.mdhd, tr.durOffM, tr.dv1, sum), ...rest(moov, tr.mk2, ['mdhd', 'minf']), minf);
        const tkhd = patchDur(moov, tr.tkhd, tr.durOffT, tr.tv1, t0 + tdur);
        const edts = t0 > 0
            ? [box('edts', fullBox('elst', 0, 0, w32(2), w32(t0), w32(0xFFFFFFFF), w32(0x00010000), w32(tdur), w32(0), w32(0x00010000)))]
            : [];
        return { trak: box('trak', tkhd, ...edts, ...rest(moov, tr.tk, ['tkhd', 'mdia', 'edts']), mdia), end: t0 + tdur };
    };
    const buildMoov = (mp, payloadStart) => {
        const { moov, mk, mvhd, mv1 } = mp;
        const built = mp.order.map(tr => buildTrak(tr, mp, payloadStart));
        const movDur = built.reduce((m, x) => Math.max(m, x.end), 0);
        const mvh = patchDur(moov, mvhd, mvhd.d + 4 + (mv1 ? 16 : 8) + 4, mv1, movDur);
        return box('moov', mvh, ...rest(moov, mk, ['mvhd', 'trak', 'mvex']), ...built.map(x => x.trak));
    };

    const mp4 = async blob => {
        const size = blob.size;
        const top = await scanTop(blob, size);
        const moofs = top.filter(x => x.t === 'moof'), mdats = top.filter(x => x.t === 'mdat');
        const moovTop = top.find(x => x.t === 'moov');
        if (!moovTop) throw new Error('no moov');
        if (!moofs.length) return blob;
        const moov = new Uint8Array(await blob.slice(moovTop.s, moovTop.e).arrayBuffer());
        const mk = kids(moov, moovTop.d - moovTop.s, moov.length);
        const mvhd = kid(mk, 'mvhd');
        req('no mvhd', mvhd);
        const mv1 = moov[mvhd.d] === 1, movTs = rd32(moov, mvhd.d + 4 + (mv1 ? 16 : 8));
        const { tracks, order } = parseTracks(moov, mk, readTrex(moov, mk));
        for (const mo of moofs) await readMoof(blob, mo, tracks);
        if (order.some(tr => !tr.dur.length)) throw new Error('empty track');

        const { payload, mdOff } = mapChunks(order, mdats);
        const hdr = payload + 8 < 4294967296 ? 8 : 16;
        const use64 = size + moov.length + 4096 > 4294967295;
        const ftyp = box('ftyp', asc('isom'), w32(512), asc('isom'), asc('iso2'), asc('avc1'), asc('mp41'));
        const mp = { moov, mk, mvhd, mv1, movTs, order, mdats, mdOff, use64 };
        const m1 = buildMoov(mp, 0);
        const payloadStart = ftyp.length + m1.length + hdr;
        const m2 = buildMoov(mp, payloadStart);
        if (m2.length !== m1.length) throw new Error('moov size changed');
        const mdatHead = hdr === 8 ? cat([w32(payload + 8), asc('mdat')]) : cat([w32(1), asc('mdat'), w64(payload + 16)]);
        return new Blob([ftyp, m2, mdatHead, ...mdats.map(m => blob.slice(m.d, m.e))], { type: 'video/mp4' });
    };

    const EBML = { HEADER: 0x1A45DFA3, SEGMENT: 0x18538067, CUES: 0x1C53BB6B, SEEKHEAD: 0x114D9B74, CLUSTER: 0x1F43B675, TRACKS: 0x1654AE6B, TRACK_ENTRY: 0xAE, TRACK_NUM: 0xD7, TRACK_TYPE: 0x83, TC: 0xE7, SBLOCK: 0xA3, GROUP: 0xA0, BLOCK: 0xA1, REFBLOCK: 0xFB };
    const CLUSTER_END = new Set([EBML.CLUSTER, EBML.CUES, 0x1254C367, 0x1043A770]);
    const vintLen = (b, max) => { let l = 1; while (l <= max && !(b & (0x80 >> (l - 1)))) l++; return l; };

    const ebmlReader = blob => {
        const size = blob.size;
        let ws = 0, wb = new Uint8Array(0);
        const need = async (p, n) => {
            if (p + n > size) return false;
            if (p >= ws && p + n <= ws + wb.length) return true;
            ws = p;
            wb = new Uint8Array(await blob.slice(p, Math.min(size, p + Math.max(n, 4194304))).arrayBuffer());
            return true;
        };
        const needAny = async (p, a, b) => (await need(p, a)) || (await need(p, b));
        const at = p => wb[p - ws];
        const uintAt = (p, n) => { let v = 0; for (let i = 0; i < n; i++) v = v * 256 + at(p + i); return v; };
        const readId = p => {
            const l = vintLen(at(p), 4);
            return l > 4 ? null : { len: l, v: uintAt(p, l) };
        };
        const readSize = p => {
            const b = at(p), l = vintLen(b, 8);
            if (l > 8) return null;
            const m = 0xff >> l;
            let v = b & m, ones = v === m;
            for (let i = 1; i < l; i++) { const x = at(p + i); v = v * 256 + x; if (x !== 255) ones = false; }
            return { len: l, v, unk: ones };
        };
        const readEl = async p => {
            if (!(await needAny(p, 24, 2))) return null;
            const id = readId(p);
            if (!id) return null;
            if (!(await needAny(p, id.len + 8, id.len + 1))) return null;
            const sz = readSize(p + id.len);
            return sz && { id: id.v, hdr: id.len + sz.len, size: sz.v, unk: sz.unk, p };
        };
        const blockHead = async pl => {
            if (!(await need(pl, 4))) return null;
            const b0 = at(pl), l = vintLen(b0, 8);
            if (!(await need(pl, l + 3))) return null;
            let tn = b0 & (0xff >> l);
            for (let i = 1; i < l; i++) tn = tn * 256 + at(pl + i);
            const rel = (at(pl + l) << 8) | at(pl + l + 1);
            return { tn, rel: rel > 32767 ? rel - 65536 : rel, flags: at(pl + l + 2) };
        };
        return { size, readEl, uintAt, blockHead };
    };

    const readTrackEntry = async (rd, q, te) => {
        let num = 0, type = 0, r = q + te.hdr;
        const tend = r + te.size;
        while (r < tend) {
            const ce = await rd.readEl(r);
            if (!ce) break;
            if (ce.id === EBML.TRACK_NUM) num = rd.uintAt(r + ce.hdr, ce.size);
            if (ce.id === EBML.TRACK_TYPE) type = rd.uintAt(r + ce.hdr, ce.size);
            r += ce.hdr + ce.size;
        }
        return { num, type };
    };
    const videoTrackIn = async (rd, el) => {
        const end = el.p + el.hdr + el.size;
        let q = el.p + el.hdr, found = 0;
        while (q < end) {
            const te = await rd.readEl(q);
            if (!te) break;
            if (te.id === EBML.TRACK_ENTRY && !found) {
                const { num, type } = await readTrackEntry(rd, q, te);
                if (type === 1) found = num;
            }
            q += te.hdr + te.size;
        }
        return found;
    };
    const headerStop = (el, videoTrack, p) => {
        if (el.id === EBML.CUES || el.id === EBML.SEEKHEAD) return { r: null };
        if (el.id === EBML.CLUSTER) return { r: videoTrack ? { videoTrack, firstCluster: p } : null };
        if (el.unk) throw new Error('unknown size header element');
        return null;
    };
    const scanHeader = async (rd, start) => {
        let p = start, videoTrack = 0;
        while (p < rd.size) {
            const el = await rd.readEl(p);
            if (!el) throw new Error('bad element');
            const stop = headerStop(el, videoTrack, p);
            if (stop) return stop.r;
            if (el.id === EBML.TRACKS) videoTrack ||= await videoTrackIn(rd, el);
            p += el.hdr + el.size;
        }
        return null;
    };

    const readGroup = async (rd, pl, ch) => {
        let r = pl, h = null, ref = false;
        const gend = pl + ch.size;
        while (r < gend) {
            const ge = await rd.readEl(r);
            if (!ge) break;
            if (ge.id === EBML.BLOCK) h = await rd.blockHead(r + ge.hdr);
            else if (ge.id === EBML.REFBLOCK) ref = true;
            r += ge.hdr + ge.size;
        }
        return { h, ref };
    };
    const keyframeRel = async (rd, ch, pl, videoTrack) => {
        if (ch.id === EBML.SBLOCK) {
            const h = await rd.blockHead(pl);
            return h && h.tn === videoTrack && (h.flags & 0x80) ? h.rel : null;
        }
        if (ch.id === EBML.GROUP) {
            const { h, ref } = await readGroup(rd, pl, ch);
            return h && h.tn === videoTrack && !ref ? h.rel : null;
        }
        return null;
    };
    const scanChild = async (rd, ch, pl, st, cp, addCue) => {
        if (ch.id === EBML.TC) { st.tc = rd.uintAt(pl, ch.size); return; }
        const rel = await keyframeRel(rd, ch, pl, st.videoTrack);
        if (rel !== null) addCue(st.tc + rel, cp);
    };
    const scanCluster = async (rd, cl, cp, videoTrack, addCue) => {
        let q = cp + cl.hdr;
        const st = { tc: 0, videoTrack };
        const cend = cl.unk ? rd.size : cp + cl.hdr + cl.size;
        while (q < cend) {
            const ch = await rd.readEl(q);
            if (!ch) return rd.size;
            if (CLUSTER_END.has(ch.id)) break;
            if (ch.unk) throw new Error('unknown size child');
            const pl = q + ch.hdr;
            await scanChild(rd, ch, pl, st, cp, addCue);
            q = pl + ch.size;
        }
        return q;
    };
    const collectCues = async (rd, firstCluster, videoTrack, sds) => {
        const cues = [];
        let lastT = -1e9;
        const addCue = (t, pos) => { if (!cues.length || t - lastT >= 500) { cues.push({ t: Math.max(0, t), pos: pos - sds }); lastT = t; } };
        let cp = firstCluster;
        while (cp < rd.size) {
            const cl = await rd.readEl(cp);
            if (!cl || cl.id !== EBML.CLUSTER) break;
            cp = await scanCluster(rd, cl, cp, videoTrack, addCue);
        }
        return cues;
    };
    const writeCues = (cues, videoTrack, total) => {
        const out = new Uint8Array(total), dv = new DataView(out.buffer);
        out.set([0x1C, 0x53, 0xBB, 0x6B, 0x01], 0);
        dv.setUint32(8, 19 * cues.length);
        let o = 12;
        for (const c of cues) {
            out.set([0xBB, 0x91, 0xB3, 0x84], o); dv.setUint32(o + 4, c.t);
            out.set([0xB7, 0x89, 0xF7, 0x81, videoTrack & 255, 0xF1, 0x84], o + 8); dv.setUint32(o + 15, c.pos + total);
            o += 19;
        }
        return out;
    };

    const intactEnd = async (rd, p) => {
        let ok = p;
        while (p < rd.size) {
            const cl = await rd.readEl(p);
            if (!cl || cl.id !== EBML.CLUSTER) break;
            if (!cl.unk) { const e = p + cl.hdr + cl.size; if (e > rd.size) break; ok = p = e; continue; }
            let q = p + cl.hdr;
            ok = q;
            while (q < rd.size) {
                const ch = await rd.readEl(q);
                if (!ch || CLUSTER_END.has(ch.id) || ch.unk) break;
                const e = q + ch.hdr + ch.size;
                if (e > rd.size) return ok;
                ok = q = e;
            }
            p = q;
        }
        return ok;
    };
    const webm = async blob => {
        const rd = ebmlReader(blob), size = blob.size;
        const eh = await rd.readEl(0);
        if (!eh || eh.id !== EBML.HEADER) throw new Error('no EBML');
        const sp = eh.hdr + eh.size;
        const seg = await rd.readEl(sp);
        if (!seg || seg.id !== EBML.SEGMENT) throw new Error('no segment');
        if (!seg.unk) return blob;
        const sds = sp + seg.hdr;
        const info = await scanHeader(rd, sds);
        if (!info) return blob;
        const { videoTrack, firstCluster } = info;
        const cues = await collectCues(rd, firstCluster, videoTrack, sds);
        if (!cues.length) return blob;
        const total = 12 + 19 * cues.length;
        if (size - sds + total > 4294967295) return blob;
        const out = writeCues(cues, videoTrack, total);
        const cut = await intactEnd(rd, firstCluster);
        return new Blob([blob.slice(0, firstCluster), out, cut > firstCluster && cut < size ? blob.slice(firstCluster, cut) : blob.slice(firstCluster)], { type: blob.type });
    };
    const clusterEnd = (rd, cl, cp) => cl.unk ? rd.size : cp + cl.hdr + cl.size;
    const blockHeadOf = async (rd, ch, pl) => {
        if (ch.id === EBML.SBLOCK) return rd.blockHead(pl);
        if (ch.id === EBML.GROUP) return (await readGroup(rd, pl, ch)).h;
        return null;
    };
    const clusterMax = async (rd, cl, cp) => {
        let q = cp + cl.hdr, tc = 0, max = null;
        const cend = clusterEnd(rd, cl, cp);
        while (q < cend) {
            const ch = await rd.readEl(q);
            if (!ch) { q = rd.size; break; }
            if (CLUSTER_END.has(ch.id)) break;
            if (ch.unk) throw new Error('unknown size child');
            const pl = q + ch.hdr;
            if (ch.id === EBML.TC) tc = rd.uintAt(pl, ch.size);
            else {
                const h = await blockHeadOf(rd, ch, pl);
                if (h) max = Math.max(max === null ? -1 : max, tc + h.rel);
            }
            q = pl + ch.size;
        }
        return { end: q, max: max === null ? tc : max };
    };
    const readSegment = async rd => {
        const eh = await rd.readEl(0);
        if (!eh || eh.id !== EBML.HEADER) return null;
        const seg = await rd.readEl(eh.hdr + eh.size);
        return seg && seg.id === EBML.SEGMENT ? seg : null;
    };
    const readTimescale = async (rd, info, scale) => {
        let q = info.p + info.hdr;
        const end = q + info.size;
        while (q < end) {
            const ce = await rd.readEl(q);
            if (!ce) break;
            if (ce.id === 0x2AD7B1) scale = rd.uintAt(q + ce.hdr, ce.size);
            q += ce.hdr + ce.size;
        }
        return scale;
    };
    const scanSegmentHead = async (rd, seg) => {
        let p = seg.p + seg.hdr, scale = 1e6;
        while (p < rd.size) {
            const el = await rd.readEl(p);
            if (!el) return null;
            if (el.id === EBML.CLUSTER) break;
            if (el.unk) return null;
            if (el.id === 0x1549A966) scale = await readTimescale(rd, el, scale);
            p += el.hdr + el.size;
        }
        return { p, scale };
    };
    const lastCluster = async (rd, p) => {
        let last = null;
        while (p < rd.size) {
            const cl = await rd.readEl(p);
            if (!cl || cl.id !== EBML.CLUSTER) break;
            last = { cl, p };
            if (cl.unk) { const r = await clusterMax(rd, cl, p); last.max = r.max; p = r.end; } else p += cl.hdr + cl.size;
        }
        return last;
    };
    const webmDuration = async blob => {
        const rd = ebmlReader(blob);
        const seg = await readSegment(rd);
        if (!seg) return null;
        const head = await scanSegmentHead(rd, seg);
        if (!head) return null;
        const last = await lastCluster(rd, head.p);
        if (!last) return null;
        if (last.max === undefined) last.max = (await clusterMax(rd, last.cl, last.p)).max;
        const ms = last.max * head.scale / 1e6;
        return ms > 0 ? ms : null;
    };
    const blankSeekHead = async (blob, p, tot) => {
        if (p + tot > WEBM_HEAD) return blob;
        const head = new Uint8Array(await blob.slice(0, WEBM_HEAD).arrayBuffer());
        const small = tot - 3 < 16384, hl = small ? 3 : 9, len = tot - hl;
        head[p] = 0xEC;
        if (small) { head[p + 1] = 0x40 | (len >> 8); head[p + 2] = len & 255; }
        else { head[p + 1] = 0x01; head.fill(0, p + 2, p + 5); new DataView(head.buffer).setUint32(p + 5, len); }
        head.fill(0, p + hl, p + tot);
        return new Blob([head, blob.slice(WEBM_HEAD)], { type: blob.type });
    };
    const stripSeekHead = async blob => {
        const rd = ebmlReader(blob);
        const seg = await readSegment(rd);
        if (!seg || !seg.unk) return blob;
        let p = seg.p + seg.hdr;
        while (p < Math.min(rd.size, WEBM_HEAD)) {
            const el = await rd.readEl(p);
            if (!el || el.unk || el.id === EBML.CLUSTER) return blob;
            const tot = el.hdr + el.size;
            if (el.id === EBML.SEEKHEAD) return blankSeekHead(blob, p, tot);
            p += tot;
        }
        return blob;
    };
    return { mp4, webm, webmDuration, stripSeekHead };
})();
const HlsKit = (() => {
    const TWO32 = 4294967296, TWO33 = 8589934592;
    const cat = parts => {
        let n = 0;
        for (const p of parts) n += p.length;
        const o = new Uint8Array(n);
        let q = 0;
        for (const p of parts) { o.set(p, q); q += p.length; }
        return o;
    };
    const asc = s => Uint8Array.from(s, c => c.charCodeAt(0));
    const be = (n, len) => { const o = new Uint8Array(len); let v = n; for (let i = len - 1; i >= 0; i--) { o[i] = v % 256; v = Math.floor(v / 256); } return o; };
    const b32 = n => be(n >>> 0, 4);
    const z = n => new Uint8Array(n);
    const box = (t, ...p) => {
        const body = cat(p), o = new Uint8Array(8 + body.length);
        new DataView(o.buffer).setUint32(0, o.length);
        o.set(asc(t), 4); o.set(body, 8);
        return o;
    };
    const fbox = (t, ver, fl, ...p) => box(t, Uint8Array.of(ver, (fl >> 16) & 255, (fl >> 8) & 255, fl & 255), ...p);
    const MATRIX = cat([b32(0x10000), z(8), b32(0), b32(0x10000), z(8), b32(0), b32(0x40000000)].flatMap(x => [x]));
    const eq = (a, b) => !!a && !!b && a.length === b.length && a.every((v, i) => v === b[i]);
    class NeedNewInit extends Error {}

    const unescapeRbsp = (nal, skip = 1) => {
        const rb = []; let zeros = 0;
        for (let i = skip; i < nal.length; i++) {
            const b = nal[i];
            if (zeros >= 2 && b === 3) { zeros = 0; continue; }
            zeros = b === 0 ? zeros + 1 : 0;
            rb.push(b);
        }
        return rb;
    };
    const bitReader = rb => {
        let pos = 0;
        const u = n => { let v = 0; for (let i = 0; i < n; i++, pos++) v = v * 2 + ((rb[pos >> 3] >> (7 - (pos & 7))) & 1); return v; };
        const ue = () => { let l = 0; while (l < 32 && u(1) === 0) l++; return l ? (2 ** l - 1) + u(l) : 0; };
        const se = () => { const k = ue(); return k & 1 ? (k + 1) / 2 : -(k / 2); };
        return { u, ue, se };
    };
    const HIGH_PROFILES = [100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135];
    const skipScalingList = (r, size) => {
        let last = 8, next = 8;
        for (let j = 0; j < size; j++) { if (next) next = (last + r.se() + 256) % 256; last = next || last; }
    };
    const skipScalingMatrix = (r, chroma) => {
        for (let i = 0; i < (chroma !== 3 ? 8 : 12); i++) if (r.u(1)) skipScalingList(r, i < 6 ? 16 : 64);
    };
    const readHighProfile = (r, prof) => {
        const out = { chroma: 1, bdl: 8, bdc: 8 };
        if (!HIGH_PROFILES.includes(prof)) return out;
        out.chroma = r.ue();
        if (out.chroma === 3) r.u(1);
        out.bdl = 8 + r.ue(); out.bdc = 8 + r.ue(); r.u(1);
        if (r.u(1)) skipScalingMatrix(r, out.chroma);
        return out;
    };
    const skipPocType = r => {
        const poc = r.ue();
        if (poc === 0) r.ue();
        else if (poc === 1) { r.u(1); r.se(); r.se(); const n = r.ue(); for (let i = 0; i < n; i++) r.se(); }
    };
    const readCrop = r => r.u(1) ? { cl: r.ue(), cr: r.ue(), ct: r.ue(), cb: r.ue() } : { cl: 0, cr: 0, ct: 0, cb: 0 };
    const parseSps = nal => {
        const r = bitReader(unescapeRbsp(nal));
        const prof = r.u(8), compat = r.u(8), level = r.u(8);
        r.ue();
        const { chroma, bdl, bdc } = readHighProfile(r, prof);
        r.ue();
        skipPocType(r);
        r.ue(); r.u(1);
        const wm = r.ue(), hm = r.ue(), fmo = r.u(1);
        if (!fmo) r.u(1);
        r.u(1);
        const { cl, cr, ct, cb } = readCrop(r);
        const cux = chroma === 1 || chroma === 2 ? 2 : 1, cuy = (chroma === 1 ? 2 : 1) * (2 - fmo);
        const w = (wm + 1) * 16 - cux * (cl + cr), h = (2 - fmo) * (hm + 1) * 16 - cuy * (ct + cb);
        if (!(w > 0 && h > 0 && w < 16384 && h < 16384)) throw new Error('bad sps');
        return { prof, compat, level, chroma, bdl, bdc, w, h };
    };

    const parseHevcPtl = r => {
        const space = r.u(2), tier = r.u(1), prof = r.u(5);
        const compat = [r.u(8), r.u(8), r.u(8), r.u(8)];
        const cons = [r.u(8), r.u(8), r.u(8), r.u(8), r.u(8), r.u(8)];
        return { space, tier, prof, compat, cons, level: r.u(8) };
    };
    const skipHevcSubLayers = (r, n) => {
        const pp = [], lp = [];
        for (let i = 0; i < n; i++) { pp.push(r.u(1)); lp.push(r.u(1)); }
        if (n > 0) for (let i = n; i < 8; i++) r.u(2);
        for (let i = 0; i < n; i++) { if (pp[i]) for (let k = 0; k < 11; k++) r.u(8); if (lp[i]) r.u(8); }
    };
    const parseHevcSps = nal => {
        const r = bitReader(unescapeRbsp(nal, 2));
        r.u(4);
        const sub = r.u(3), nest = r.u(1);
        const ptl = parseHevcPtl(r);
        skipHevcSubLayers(r, sub);
        r.ue();
        const chroma = r.ue();
        if (chroma === 3) r.u(1);
        const pw = r.ue(), ph = r.ue();
        let cl = 0, cr = 0, ct = 0, cb = 0;
        if (r.u(1)) { cl = r.ue(); cr = r.ue(); ct = r.ue(); cb = r.ue(); }
        const bdl = 8 + r.ue(), bdc = 8 + r.ue();
        const sw = chroma === 1 || chroma === 2 ? 2 : 1, sh = chroma === 1 ? 2 : 1;
        const w = pw - sw * (cl + cr), h = ph - sh * (ct + cb);
        if (!(w > 0 && h > 0 && w < 16384 && h < 16384)) throw new Error('bad hevc sps');
        return { codec: 'hevc', ...ptl, chroma, bdl, bdc, w, h, layers: sub + 1, nest };
    };
    const hevcCodecString = v => {
        const hex = n => n.toString(16);
        const fl = ((v.compat[0] << 24) | (v.compat[1] << 16) | (v.compat[2] << 8) | v.compat[3]) >>> 0;
        let rev = 0;
        for (let i = 0; i < 32; i++) if (fl & (1 << i)) rev |= 1 << (31 - i);
        const c = v.cons.slice();
        while (c.length > 1 && c[c.length - 1] === 0) c.pop();
        return `hvc1.${['', 'A', 'B', 'C'][v.space]}${v.prof}.${hex(rev >>> 0)}.${v.tier ? 'H' : 'L'}${v.level}.${c.map(x => hex(x).toUpperCase()).join('.')}`;
    };

    const AAC_SR = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
    const tsTime = (b, o) => (b[o] & 0x0e) * 536870912 + b[o + 1] * 4194304 + (b[o + 2] & 0xfe) * 16384 + b[o + 3] * 128 + (b[o + 4] >> 1);
    const splitNals = b => {
        const out = [], n = b.length;
        let i = 0, start = -1;
        const push = e => { while (e > start && b[e - 1] === 0) e--; if (e > start) out.push(b.subarray(start, e)); };
        while (i < n - 2) {
            if (b[i + 2] > 1) i += 3;
            else if (b[i] === 0 && b[i + 1] === 0 && b[i + 2] === 1) { if (start >= 0) push(i); start = i + 3; i += 3; }
            else i++;
        }
        if (start >= 0) push(n);
        return out;
    };
    const findTsSync = buf => {
        let p = 0;
        while (p + 376 <= buf.length && !(buf[p] === 0x47 && buf[p + 188] === 0x47)) p++;
        return p;
    };
    const parsePes = d => {
        if (d.length < 9 || d[0] || d[1] || d[2] !== 1) return null;
        const fl = d[7] >> 6, hl = d[8];
        if (!(fl & 2) || d.length < 9 + hl) return null;
        const pts = tsTime(d, 9);
        return { pts, dts: fl === 3 ? tsTime(d, 14) : pts, pay: d.subarray(9 + hl) };
    };
    const collectNals = (nals, res) => {
        const keep = [];
        let key = false;
        for (const nl of nals) {
            const t = nl[0] & 31;
            if (t === 7) { if (!res.sps) res.sps = nl.slice(); }
            else if (t === 8) { if (!res.pps) res.pps = nl.slice(); }
            else if (t >= 1 && t <= 5) { keep.push(nl); if (t === 5) key = true; }
        }
        return { keep, key };
    };
    const collectHevcNals = (nals, res) => {
        const keep = [];
        let key = false;
        for (const nl of nals) {
            if (nl.length < 3 || ((nl[0] & 1) << 5 | nl[1] >> 3) !== 0) continue;
            const t = (nl[0] >> 1) & 63;
            if (t === 32) { if (!res.vps) res.vps = nl.slice(); }
            else if (t === 33) { if (!res.sps) res.sps = nl.slice(); }
            else if (t === 34) { if (!res.pps) res.pps = nl.slice(); }
            else if (t <= 21) { keep.push(nl); if (t >= 16) key = true; }
        }
        return { keep, key };
    };
    const avcSample = keep => {
        let n = 0;
        for (const k of keep) n += 4 + k.length;
        const data = new Uint8Array(n);
        let q = 0;
        for (const k of keep) { new DataView(data.buffer).setUint32(q, k.length); data.set(k, q + 4); q += 4 + k.length; }
        return data;
    };
    const addVideo = (res, pay, pts, dts) => {
        const nals = splitNals(pay);
        const { keep, key } = res.codec === 'hevc' ? collectHevcNals(nals, res) : collectNals(nals, res);
        if (!keep.length) return;
        res.video.push({ pts, dts, key, data: avcSample(keep) });
    };
    const parseAdts = (pay, q) => {
        const prot = pay[q + 1] & 1, prof = (pay[q + 2] >> 6) & 3, sfi = (pay[q + 2] >> 2) & 15;
        const ch = ((pay[q + 2] & 1) << 2) | (pay[q + 3] >> 6), fl = ((pay[q + 3] & 3) << 11) | (pay[q + 4] << 3) | (pay[q + 5] >> 5);
        return { prof, sfi, ch, fl, hl: prot ? 7 : 9 };
    };
    const addAudio = (res, pay, pts) => {
        let q = 0, i = 0;
        while (q + 7 <= pay.length) {
            if (pay[q] !== 0xff || (pay[q + 1] & 0xf0) !== 0xf0) { q++; continue; }
            const f = parseAdts(pay, q);
            if (f.sfi > 12 || f.fl < f.hl || q + f.fl > pay.length) break;
            const sr = AAC_SR[f.sfi];
            if (!res.aac) res.aac = { aot: f.prof + 1, sfi: f.sfi, sr, ch: f.ch || 2 };
            res.audio.push({ pts: pts + i * 1024 * 90000 / sr, data: pay.subarray(q + f.hl, q + f.fl) });
            i++; q += f.fl;
        }
    };
    const flushPes = (st, pid) => {
        const pe = st.pes.get(pid);
        if (!pe || !pe.chunks.length) return;
        const d = cat(pe.chunks); pe.chunks = [];
        const pes = parsePes(d);
        if (!pes) return;
        if (pid === st.vPid) addVideo(st.res, pes.pay, pes.pts, pes.dts);
        else if (st.aType === 0x0f) addAudio(st.res, pes.pay, pes.pts);
    };
    const parsePat = (buf, s, end, st) => {
        for (let o = s + 8; o + 4 <= end; o += 4) {
            const prog = (buf[o] << 8) | buf[o + 1];
            if (prog) { st.pmtPid = ((buf[o + 2] & 0x1f) << 8) | buf[o + 3]; break; }
        }
    };
    const parsePmt = (buf, s, end, st) => {
        let o = s + 12 + (((buf[s + 10] & 0x0f) << 8) | buf[s + 11]);
        while (o + 5 <= end) {
            const type = buf[o], sp = ((buf[o + 1] & 0x1f) << 8) | buf[o + 2];
            if (type === 0x1b && st.vPid < 0) { st.vPid = sp; st.res.codec = 'avc'; }
            else if (type === 0x0f && st.aPid < 0) { st.aPid = sp; st.aType = type; }
            else if (type === 0x24 && st.vPid < 0) { st.vPid = sp; st.res.codec = 'hevc'; }
            o += 5 + (((buf[o + 3] & 0x0f) << 8) | buf[o + 4]);
        }
    };
    const parsePsi = (buf, p, q, pid, st) => {
        const s = q + 1 + buf[q], len = ((buf[s + 1] & 0x0f) << 8) | buf[s + 2], end = Math.min(p + 188, s + 3 + len - 4);
        if (pid === 0 && buf[s] === 0) parsePat(buf, s, end, st);
        else if (pid === st.pmtPid && buf[s] === 2) parsePmt(buf, s, end, st);
    };
    const tsPayloadOffset = (buf, p) => {
        const afc = (buf[p + 3] >> 4) & 3;
        let q = p + 4;
        if (afc & 2) q += 1 + buf[p + 4];
        return !(afc & 1) || q >= p + 188 ? -1 : q;
    };
    const addPesChunk = (st, pid, pusi, chunk) => {
        if (pusi) flushPes(st, pid);
        let pe = st.pes.get(pid);
        if (!pe) st.pes.set(pid, pe = { chunks: [] });
        if (pusi || pe.chunks.length) pe.chunks.push(chunk);
    };
    const tsPacket = (buf, p, st) => {
        const q = tsPayloadOffset(buf, p);
        if (q < 0) return;
        const pusi = buf[p + 1] & 0x40, pid = ((buf[p + 1] & 0x1f) << 8) | buf[p + 2];
        if (pid === 0 || pid === st.pmtPid) { if (pusi) parsePsi(buf, p, q, pid, st); return; }
        if (pid === st.vPid || pid === st.aPid) addPesChunk(st, pid, pusi, buf.subarray(q, p + 188));
    };
    const demuxTs = buf => {
        const st = { res: { video: [], audio: [], sps: null, pps: null, vps: null, codec: 'avc', aac: null }, pmtPid: -1, vPid: -1, aPid: -1, aType: 0, pes: new Map() };
        for (let p = findTsSync(buf); p + 188 <= buf.length; p += 188) {
            if (buf[p] !== 0x47) { const nx = buf.indexOf(0x47, p + 1); if (nx < 0) break; p = nx - 188; continue; }
            tsPacket(buf, p, st);
        }
        flushPes(st, st.vPid); flushPes(st, st.aPid);
        return st.res;
    };

    const desc = (tag, ...p) => { const b = cat(p); if (b.length > 127) throw new Error('desc too long'); return cat([Uint8Array.of(tag, b.length), b]); };
    const esds = a => fbox('esds', 0, 0, desc(3, be(0, 2), Uint8Array.of(0), desc(4, Uint8Array.of(0x40, 0x15), be(0, 3), be(0, 4), be(0, 4), desc(5, a)), desc(6, Uint8Array.of(2))));
    const avcC = v => {
        const parts = [Uint8Array.of(1, v.prof, v.compat, v.level, 0xff, 0xe1), be(v.sps.length, 2), v.sps, Uint8Array.of(1), be(v.pps.length, 2), v.pps];
        if ([100, 110, 122, 144].includes(v.prof)) parts.push(Uint8Array.of(0xfc | v.chroma, 0xf8 | (v.bdl - 8), 0xf8 | (v.bdc - 8), 0));
        return box('avcC', ...parts);
    };
    const hvcC = v => {
        const arr = (type, n) => [Uint8Array.of(0x80 | type, 0, 1), be(n.length, 2), n];
        return box('hvcC', Uint8Array.of(1, (v.space << 6) | (v.tier << 5) | v.prof), Uint8Array.from(v.compat), Uint8Array.from(v.cons),
            Uint8Array.of(v.level, 0xf0, 0x00, 0xfc, 0xfc | v.chroma, 0xf8 | (v.bdl - 8), 0xf8 | (v.bdc - 8), 0, 0, (v.layers << 3) | (v.nest << 2) | 3, 3),
            ...arr(32, v.vps), ...arr(33, v.sps), ...arr(34, v.pps));
    };
    const trakBox = (id, ts, kind, entry, w, h) => box('trak',
        fbox('tkhd', 0, 7, b32(0), b32(0), b32(id), b32(0), b32(0), z(8), be(0, 2), be(0, 2), be(kind === 'soun' ? 0x0100 : 0, 2), z(2), MATRIX, b32(w * 65536), b32(h * 65536)),
        box('mdia',
            fbox('mdhd', 0, 0, b32(0), b32(0), b32(ts), b32(0), be(0x55c4, 2), z(2)),
            fbox('hdlr', 0, 0, b32(0), asc(kind), z(12), asc(kind === 'vide' ? 'Video\0' : 'Audio\0')),
            box('minf',
                kind === 'vide' ? fbox('vmhd', 0, 1, z(8)) : fbox('smhd', 0, 0, z(4)),
                box('dinf', fbox('dref', 0, 0, b32(1), fbox('url ', 0, 1))),
                box('stbl', fbox('stsd', 0, 0, b32(1), entry), fbox('stts', 0, 0, b32(0)), fbox('stsc', 0, 0, b32(0)), fbox('stsz', 0, 0, b32(0), b32(0)), fbox('stco', 0, 0, b32(0))))));
    const buildInit = (v, a) => {
        const hevc = v.codec === 'hevc';
        const avc1 = box(hevc ? 'hvc1' : 'avc1', z(6), be(1, 2), z(16), be(v.w, 2), be(v.h, 2), b32(0x480000), b32(0x480000), z(4), be(1, 2), z(32), be(0x18, 2), be(0xffff, 2), hevc ? hvcC(v) : avcC(v));
        const traks = [trakBox(1, 90000, 'vide', avc1, v.w, v.h)];
        const trex = [fbox('trex', 0, 0, b32(1), b32(1), b32(0), b32(0), b32(0))];
        if (a) {
            const ascb = be(((a.aot << 11) | (a.sfi << 7) | (a.ch << 3)), 2);
            const mp4a = box('mp4a', z(6), be(1, 2), z(8), be(a.ch, 2), be(16, 2), z(4), be(a.sr < 65536 ? a.sr * 65536 : 0, 4), esds(ascb));
            traks.push(trakBox(2, a.sr, 'soun', mp4a, 0, 0));
            trex.push(fbox('trex', 0, 0, b32(2), b32(1), b32(0), b32(0), b32(0)));
        }
        return cat([
            box('ftyp', asc('isom'), b32(512), asc('isom'), asc('iso5'), asc('iso6'), asc(v.codec === 'hevc' ? 'hvc1' : 'avc1'), asc('mp41')),
            box('moov', fbox('mvhd', 0, 0, b32(0), b32(0), b32(1000), b32(0), b32(0x10000), be(0x0100, 2), z(10), MATRIX, z(24), b32(a ? 3 : 2)), ...traks, box('mvex', ...trex))
        ]);
    };
    const buildFragment = (seq, tracks) => {
        const mk = base => {
            let off = base;
            const trafs = tracks.map(t => {
                const run = fbox('trun', 1, 0xf01, b32(t.samples.length), b32(off), ...t.samples.map(s => cat([b32(s.dur), b32(s.data.length), b32(s.flags), b32(s.cts)])));
                off += t.samples.reduce((n, s) => n + s.data.length, 0);
                return box('traf', fbox('tfhd', 0, 0x20000, b32(t.id)), fbox('tfdt', 1, 0, be(t.t, 8)), run);
            });
            return box('moof', fbox('mfhd', 0, 0, b32(seq)), ...trafs);
        };
        const moofLen = mk(0).length, moof = mk(moofLen + 8);
        let n = 0;
        for (const t of tracks) for (const s of t.samples) n += s.data.length;
        const mdat = new Uint8Array(8 + n);
        new DataView(mdat.buffer).setUint32(0, mdat.length);
        mdat.set(asc('mdat'), 4);
        let q = 8;
        for (const t of tracks) for (const s of t.samples) { mdat.set(s.data, q); q += s.data.length; }
        return cat([moof, mdat]);
    };
    const KEY_FLAGS = 0x02000000, DELTA_FLAGS = 0x01010000;
    const unwrapper = () => { let off = 0, last = null; return v => { v += off; if (last !== null) { const d = v - last; if (d < -TWO32) { off += TWO33; v += TWO33; } else if (d > TWO32) { off -= TWO33; v -= TWO33; } } last = v; return v; }; };
    const modCts = d => (((d + TWO32) % TWO33) + TWO33) % TWO33 - TWO32;

    const differs = (a, b) => !!a && !!b && !eq(a, b);
    const audioChanged = (a, b) => !!a && !!b && (a.sr !== b.sr || a.ch !== b.ch);
    class Fmp4Mux {
        constructor() {
            this.init = null; this.vi = null; this.ai = null; this.sps = null; this.pps = null; this.vps = null; this.codec = null;
            this.t0 = null; this.shift = 0; this.seq = 0; this.pend = null; this.lastDur = 3000;
            this.aNext = null; this.uwV = unwrapper(); this.uwA = unwrapper();
        }
        _check(seg) {
            if (seg.sps && this.codec && seg.codec !== this.codec) throw new NeedNewInit('codec');
            if (differs(seg.sps, this.sps) || differs(seg.pps, this.pps) || differs(seg.vps, this.vps)) throw new NeedNewInit('params');
            if (audioChanged(seg.aac, this.ai)) throw new NeedNewInit('audio');
        }
        _ensureInit(seg, out) {
            if (this.init) return true;
            const hevc = seg.codec === 'hevc';
            if (!seg.sps || !seg.pps || (hevc && !seg.vps)) return false;
            this.sps = seg.sps; this.pps = seg.pps; this.vps = hevc ? seg.vps : null; this.codec = hevc ? 'hevc' : 'avc';
            this.vi = { ...(hevc ? parseHevcSps(seg.sps) : parseSps(seg.sps)), sps: seg.sps, pps: seg.pps, vps: this.vps };
            this.ai = seg.aac || null;
            this.init = buildInit(this.vi, this.ai);
            out.push(this.init);
            return true;
        }
        _align(vs, disc) {
            if (this.t0 === null) {
                const k = vs.findIndex(s => s.key);
                if (k < 0) return null;
                vs = vs.slice(k);
                this.t0 = vs[0].dts;
            } else if (vs.length) {
                const firstAdj = vs[0].dts - this.t0 + this.shift, exp = this.pend ? this.pend.adj + this.lastDur : firstAdj;
                if (disc || Math.abs(firstAdj - exp) > 180000) this.shift += exp - firstAdj;
            }
            return vs;
        }
        _videoTrack(vs) {
            const q = this.pend ? [this.pend, ...vs] : vs;
            if (!q.length) return null;
            this.pend = q[q.length - 1];
            const samples = [];
            for (let i = 0; i < q.length - 1; i++) {
                let dur = q[i + 1].adj - q[i].adj;
                if (dur <= 0) dur = this.lastDur; else this.lastDur = dur;
                samples.push({ dur, cts: q[i].cts, flags: q[i].key ? KEY_FLAGS : DELTA_FLAGS, data: q[i].data });
            }
            return samples.length ? { id: 1, t: Math.max(0, q[0].adj), samples } : null;
        }
        _audioTrack(au) {
            if (!this.ai || !au.length) return null;
            const sr = this.ai.sr, a = au.map(s => ({ t: (s.pts - this.t0 + this.shift) * sr / 90000, data: s.data })).filter(s => s.t >= -1024);
            if (!a.length) return null;
            const t0 = Math.round(Math.max(0, a[0].t));
            const start = this.aNext === null || Math.abs(t0 - this.aNext) > sr * 0.04 ? t0 : this.aNext;
            this.aNext = start + a.length * 1024;
            return { id: 2, t: start, samples: a.map(s => ({ dur: 1024, cts: 0, flags: KEY_FLAGS, data: s.data })) };
        }
        push(seg, disc) {
            const out = [];
            this._check(seg);
            if (!this._ensureInit(seg, out)) return out;
            let vs = seg.video.map(s => ({ dts: this.uwV(s.dts), cts: modCts(s.pts - s.dts), key: s.key, data: s.data }));
            const au = seg.audio.map(s => ({ pts: this.uwA(s.pts), data: s.data }));
            vs = this._align(vs, disc);
            if (!vs) return out;
            for (const s of vs) s.adj = s.dts - this.t0 + this.shift;
            const tracks = [this._videoTrack(vs), this._audioTrack(au)].filter(Boolean);
            if (tracks.length) out.push(buildFragment(++this.seq, tracks));
            return out;
        }
        flush() {
            if (!this.pend) return [];
            const s = this.pend; this.pend = null;
            return [buildFragment(++this.seq, [{ id: 1, t: Math.max(0, s.adj), samples: [{ dur: this.lastDur, cts: s.cts, flags: s.key ? KEY_FLAGS : DELTA_FLAGS, data: s.data }] }])];
        }
    }

    const attrs = s => { const o = {}, re = /([A-Z0-9-]+)=("[^"]*"|[^,]*)/g; let m; while ((m = re.exec(s))) o[m[1]] = m[2].replace(/^"|"$/g, ''); return o; };
    const TAG_HANDLERS = {
        '#EXT-X-STREAM-INF': (s, val) => { s.sv = attrs(val); },
        '#EXT-X-MEDIA': (s, val) => {
            const a = attrs(val);
            if (a.TYPE === 'AUDIO' && a.URI) s.res.audio.push({ url: s.abs(a.URI), group: a['GROUP-ID'] || '', def: a.DEFAULT === 'YES', auto: a.AUTOSELECT === 'YES' });
        },
        '#EXT-X-TARGETDURATION': (s, val) => { s.res.target = +val || 6; },
        '#EXT-X-MEDIA-SEQUENCE': (s, val) => { s.seq = +val || 0; },
        '#EXTINF': (s, val) => { s.dur = parseFloat(val) || 0; },
        '#EXT-X-DISCONTINUITY': s => { s.disc = true; },
        '#EXT-X-ENDLIST': s => { s.res.end = true; },
        '#EXT-X-BYTERANGE': (s, val) => { const [n, o] = val.trim().split('@'); s.br = { n: +n, o: o === undefined ? null : +o }; },
        '#EXT-X-KEY': (s, val) => { const a = attrs(val); s.key = a.METHOD === 'NONE' ? null : { method: a.METHOD, uri: a.URI ? s.abs(a.URI) : '', iv: a.IV || '' }; },
        '#EXT-X-MAP': (s, val) => {
            const a = attrs(val), uri = s.abs(a.URI);
            let range = null;
            if (a.BYTERANGE) { const [n, o] = a.BYTERANGE.split('@').map(Number); if (!(n > 0)) throw new Error('bad byte-range'); range = { n, o: o || 0 }; }
            s.map = { uri, range, key: `${uri}#${range ? `${range.o}-${range.n}` : ''}` };
        }
    };
    const takeRange = (s, uri) => {
        if (!s.br) return null;
        const { n, o } = s.br;
        if (!(n > 0)) throw new Error('bad byte-range');
        const off = o !== null && !Number.isNaN(o) ? o : (s.ends.get(uri) || 0);
        s.ends.set(uri, off + n); s.br = null;
        return { n, o: off };
    };
    const addUri = (s, l) => {
        if (s.sv) {
            const [w, h] = (s.sv.RESOLUTION || '').split('x').map(Number);
            s.res.variants.push({ url: s.abs(l), bw: +s.sv.BANDWIDTH || 0, w: w || 0, h: h || 0, aud: s.sv.AUDIO || '', codecs: s.sv.CODECS || '' });
            s.res.master = true; s.sv = null;
        } else {
            const uri = s.abs(l);
            s.res.segs.push({ uri, seq: s.seq++, dur: s.dur, disc: s.disc, key: s.key, map: s.map, range: takeRange(s, uri) });
            s.disc = false;
        }
    };
    const parsePlaylist = (text, base) => {
        const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
        if (!lines.length || !lines[0].startsWith('#EXTM3U')) throw new Error('not an m3u8');
        const s = { abs: u => new URL(u, base).href, res: { master: false, variants: [], audio: [], segs: [], target: 6, end: false }, seq: 0, dur: 0, disc: false, key: null, map: null, sv: null, br: null, ends: new Map() };
        for (const l of lines) {
            if (l[0] !== '#') { addUri(s, l); continue; }
            const c = l.indexOf(':'), tag = c < 0 ? l : l.slice(0, c), val = c < 0 ? '' : l.slice(c + 1);
            const handler = TAG_HANDLERS[tag];
            if (handler) handler(s, val);
        }
        return s.res;
    };
    const pickVariant = (vs, maxH) => {
        if (!vs.length) throw new Error('empty master playlist');
        const withH = vs.filter(v => v.h);
        if (withH.length) {
            const ok = withH.filter(v => v.h <= maxH);
            return (ok.length ? ok : withH).sort((a, b) => ok.length ? (b.h - a.h || b.bw - a.bw) : (a.h - b.h || a.bw - b.bw))[0];
        }
        const s = [...vs].sort((a, b) => b.bw - a.bw);
        return maxH >= 720 ? s[0] : s[s.length - 1];
    };

    const pickAudio = (pl, v) => {
        if (!pl.audio.length) return null;
        const own = v.aud ? pl.audio.filter(a => a.group === v.aud) : [];
        if (!own.length && (!v.codecs || /mp4a|ac-3|ec-3|opus|flac/i.test(v.codecs))) return null;
        const c = own.length ? own : pl.audio;
        return c.find(a => a.def) || c.find(a => a.auto) || c[0];
    };

    const DIR = 'bcu_rec';
    const defRoot = () => navigator.storage.getDirectory();
    class DiskBuffer {
        constructor(o = {}) {
            this.id = o.id || `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
            this.meta = o.meta || {};
            this.chunk = o.chunkBytes || 4 * 1048576;
            this.getRoot = o.getRoot || defRoot;
            this.mem = []; this.memBytes = 0; this.files = []; this.size = 0; this.n = 0; this.dir = null; this.base = null;
            this._q = this._init();
        }
        get onDisk() { return !!this.dir; }
        async _init() {
            try {
                const root = await this.getRoot();
                this.base = await root.getDirectoryHandle(DIR, { create: true });
                this.dir = await this.base.getDirectoryHandle(this.id, { create: true });
                await this._put('meta.json', new Blob([JSON.stringify({ ...this.meta, id: this.id, t: Date.now() })]));
            } catch { this.dir = null; }
        }
        async _put(name, blob) {
            const fh = await this.dir.getFileHandle(name, { create: true });
            const w = await fh.createWritable();
            try { await w.write(blob); } finally { await w.close(); }
            return fh;
        }
        async _flush() {
            if (!this.dir || !this.memBytes) return;
            const parts = this.mem, bytes = this.memBytes;
            this.mem = []; this.memBytes = 0;
            try {
                const fh = await this._put(`${String(++this.n).padStart(6, '0')}.bin`, new Blob(parts));
                this.files.push(fh);
            } catch { this.mem = parts.concat(this.mem); this.memBytes += bytes; this.dir = null; }
        }
        append(data) {
            const len = data.size ?? data.byteLength;
            if (!len) return;
            this.size += len; this.mem.push(data); this.memBytes += len;
            if (this.memBytes >= this.chunk) this._q = this._q.then(() => this._flush());
        }
        async finish() {
            await this._q;
            const f = await Promise.all(this.files.map(h => h.getFile()));
            return new Blob([...f, ...this.mem], { type: this.meta.mime || '' });
        }
        async markDone() { await this._q; if (this.dir) { try { await this._put('.done', new Blob([])); } catch {} } }
        async remove() { await this._q.catch(() => {}); try { await this.base?.removeEntry(this.id, { recursive: true }); } catch {} this.dir = null; this.files = []; }
        dispose(delay = 900000) { setTimeout(() => this.remove(), delay); }
        static async list(getRoot = defRoot, minAge = 120000) {
            const out = [];
            try {
                const base = await (await getRoot()).getDirectoryHandle(DIR);
                for await (const [id, h] of base.entries()) {
                    if (h.kind !== 'directory') continue;
                    const e = { id, meta: {}, bins: [], done: false, last: 0, bytes: 0 };
                    for await (const [name, fh] of h.entries()) {
                        const f = await fh.getFile();
                        e.last = Math.max(e.last, f.lastModified);
                        if (name === '.done') e.done = true;
                        else if (name === 'meta.json') { try { e.meta = JSON.parse(await f.text()); } catch {} }
                        else if (name.endsWith('.bin')) { e.bins.push([name, f]); e.bytes += f.size; }
                    }
                    e.bins.sort((a, b) => a[0] < b[0] ? -1 : 1);
                    e.stale = Date.now() - e.last >= minAge;
                    out.push(e);
                }
            } catch {}
            return out;
        }
        static async orphans(getRoot, minAge) { return (await DiskBuffer.list(getRoot, minAge)).filter(e => !e.done && e.stale && e.bytes > 0); }
        static blobOf(e) { return new Blob(e.bins.map(b => b[1]), { type: e.meta.mime || '' }); }
        static async cleanup(getRoot = defRoot, doneAge = 1800000) {
            try {
                const base = await (await getRoot()).getDirectoryHandle(DIR);
                for (const e of await DiskBuffer.list(getRoot, 0)) {
                    const old = Date.now() - e.last;
                    if ((e.done && old > doneAge) || (!e.bytes && old > 300000)) { try { await base.removeEntry(e.id, { recursive: true }); } catch {} }
                }
            } catch {}
        }
        static async markDoneById(id, getRoot = defRoot) { try { const d = await (await (await getRoot()).getDirectoryHandle(DIR)).getDirectoryHandle(id); const w = await (await d.getFileHandle('.done', { create: true })).createWritable(); await w.close(); } catch {} }
        static async removeById(id, getRoot = defRoot) { try { await (await (await getRoot()).getDirectoryHandle(DIR)).removeEntry(id, { recursive: true }); } catch {} }
    }

    const sleepMs = (o, ms) => new Promise(r => { const t = setTimeout(r, ms); o._wake = () => { clearTimeout(t); r(); }; });
    const ivBytes = (iv, seq) => {
        if (!iv) return be(seq, 16);
        const h = iv.replace(/^0x/i, '').padStart(32, '0'), o = new Uint8Array(16);
        for (let i = 0; i < 16; i++) o[i] = parseInt(h.substr(i * 2, 2), 16);
        return o;
    };
    const rangeSlice = (d, r) => d.length === r.n ? d : d.length >= r.o + r.n ? d.subarray(r.o, r.o + r.n) : null;
    const isFmp4Media = d => d.length > 8 && /^(ftyp|styp|moof|sidx)$/.test(String.fromCharCode(d[4], d[5], d[6], d[7]));
    class HlsCapture {
        constructor(o) {
            this.o = { maxHeight: Infinity, partBytes: 150 * 1048576, totalBytes: 0, maxBacklog: 6, retries: 3, ...o };
            this.stats = { segments: 0, missed: 0, gaps: 0, bytes: 0, parts: 0, variant: null };
            this.stopped = false; this.error = null;
            this.sink = null; this.mux = null; this.partNo = 0; this.saved = 0;
            this.initBytes = null; this.initUri = null; this.keys = new Map(); this.pending = [];
        }
        stop() { this.stopped = true; this._wake?.(); }
        start() { this.done = this._run().catch(e => { this.error = e; }).then(() => Promise.all(this.pending)).then(() => ({ error: this.error, stats: this.stats })); return this.done; }
        async _bin(url, tries = this.o.retries, range = null) {
            for (let i = 0; i < tries && !this.stopped; i++) {
                let d = null;
                try { d = await this.o.fetchBin(url, range || undefined); } catch {}
                if (d && range) d = rangeSlice(d, range);
                if (d && d.length) return d;
                if (i < tries - 1) await sleepMs(this, 400 * 2 ** i);
            }
            return null;
        }
        async _pl(url, tries) {
            const d = await this._bin(url, tries);
            return d ? parsePlaylist(new TextDecoder().decode(d), url) : null;
        }
        async _newSink() { this.partNo++; this.sink = await this.o.makeSink(this.partNo); this.mux = new Fmp4Mux(); if (this.initBytes) this.sink.append(this.initBytes); }
        async _closePart(final) {
            if (!this.sink) return;
            const sink = this.sink, no = this.partNo;
            this.sink = null;
            if (this.mux) for (const c of this.mux.flush()) sink.append(c);
            this.mux = null;
            this.saved += sink.size;
            if (sink.size > (this.initBytes ? this.initBytes.length : 0)) {
                this.stats.parts++;
                this.pending.push(Promise.resolve().then(async () => this.o.onPart(await sink.finish(), no, { sink, final: !!final })).catch(e => { this.error ||= e; }));
            } else { try { await sink.remove?.(); } catch {} }
        }
        async _rotate() { await this._closePart(false); if (!this.stopped) await this._newSink(); }
        _progress() { this.stats.part = this.sink ? this.sink.size : 0; this.stats.saved = this.saved; this.stats.bytes = this.saved + this.stats.part; this.o.onProgress?.(this.stats); }
        async _decrypt(data, seg) {
            if (seg.key.method !== 'AES-128') throw new Error(`unsupported encryption ${seg.key.method}`);
            let k = this.keys.get(seg.key.uri);
            if (!k) { k = await this._bin(seg.key.uri); if (!k || k.length !== 16) throw new Error('cannot get key'); this.keys.set(seg.key.uri, k); }
            const ck = await crypto.subtle.importKey('raw', k, 'AES-CBC', false, ['decrypt']);
            return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-CBC', iv: ivBytes(seg.key.iv, seg.seq) }, ck, data));
        }
        async _appendTs(data, seg) {
            const parsed = demuxTs(data);
            let out;
            try { out = this.mux.push(parsed, seg.disc); }
            catch (e) { if (!(e instanceof NeedNewInit)) throw e; await this._rotate(); out = this.mux.push(parsed, true); }
            for (const c of out) this.sink.append(c);
        }
        async _loadInit(map) {
            const uri = map.key, init = await this._bin(map.uri, undefined, map.range);
            if (!init) throw new Error('cannot get init segment');
            const changed = this.initUri !== null;
            this.initUri = uri; this.initBytes = init;
            if (changed && this.sink.size > 0) await this._rotate(); else if (this.sink.size === 0) this.sink.append(init);
        }
        async _appendFmp4(data, seg) {
            if (seg.map && seg.map.key !== this.initUri) await this._loadInit(seg.map);
            this.sink.append(data);
        }
        async _segment(seg) {
            let data = await this._bin(seg.uri, undefined, seg.range);
            if (!data) { this.stats.missed++; return false; }
            if (seg.key) data = await this._decrypt(data, seg);
            if (data[0] === 0x47) await this._appendTs(data, seg);
            else if (isFmp4Media(data)) await this._appendFmp4(data, seg);
            else throw new Error('unsupported segment format');
            this.stats.segments++;
            return true;
        }
        async _openHinted() {
            const h = this.o.hint;
            if (!h || !Array.isArray(h.variants) || !h.variants.length) return null;
            const v = pickVariant(h.variants, this.o.maxHeight);
            const pl = await this._pl(v.url, 1);
            if (!pl || pl.master || !pl.segs.length) return null;
            this.stats.variant = v; this.stats.variants = h.variants;
            const au = pickAudio({ audio: h.audio || [] }, v);
            this.stats.audioUrl = au ? au.url : '';
            if (au) this.o.onAudio?.(au);
            this._refreshLadder();
            return { url: v.url, pl };
        }
        _refreshLadder() {
            this._pl(this.o.url, 1).then(m => {
                if (!m || !m.master || this.stopped) return;
                this.stats.variants = m.variants;
                this.o.onOpened?.({ variants: m.variants, audio: m.audio });
            }).catch(() => {});
        }
        async _openPlaylist() {
            const o = this.o;
            const quick = await this._openHinted();
            if (quick) return quick;
            let url = o.url, pl = await this._pl(url, o.hint ? 1 : undefined);
            if (!pl) throw new Error('playlist unavailable');
            if (pl.master) {
                const masterAudio = pl.audio;
                const v = pickVariant(pl.variants, o.maxHeight);
                this.stats.variant = v; this.stats.variants = pl.variants; url = v.url;
                const au = pickAudio(pl, v);
                this.stats.audioUrl = au ? au.url : '';
                if (au) this.o.onAudio?.(au);
                pl = await this._pl(url);
                if (!pl) throw new Error('media playlist unavailable');
                this.o.onOpened?.({ variants: this.stats.variants, audio: masterAudio });
            }
            return { url, pl };
        }
        _freshSegs(pl, last) {
            let fresh = pl.segs.filter(s => s.seq > last);
            if (last < 0 && !pl.end && fresh.length > this.o.maxBacklog) fresh = fresh.slice(-this.o.maxBacklog);
            if (last >= 0 && fresh.length && fresh[0].seq > last + 1) {
                this.stats.gaps += fresh[0].seq - last - 1;
                fresh[0] = { ...fresh[0], disc: true };
            }
            return fresh;
        }
        async _waitGate() {
            const o = this.o;
            if (!o.gate || !o.gate()) return false;
            while (o.gate() && !this.stopped) await sleepMs(this, 400);
            return true;
        }
        async _consume(fresh, st) {
            const o = this.o;
            for (const seg of fresh) {
                if (this.stopped) break;
                if (await this._waitGate()) return true;
                if (await this._segment(seg)) st.idleSince = Date.now();
                st.last = seg.seq;
                this._progress();
                if (this.sink.size >= o.partBytes) await this._rotate();
                if (o.totalBytes && this.saved + this.sink.size >= o.totalBytes) this.stopped = true;
            }
            return false;
        }
        async _refreshPl(url, pl) { return (await this._pl(url, 1).catch(() => null)) || pl; }
        _shouldStop(pl, fresh, idleSince) {
            return this.stopped || pl.end || (!fresh.length && Date.now() - idleSince > Math.max(20000, pl.target * 4000));
        }
        async _run() {
            let { url, pl } = await this._openPlaylist();
            await this._newSink();
            const st = { last: -1, idleSince: Date.now(), fails: 0 };
            while (!this.stopped) {
                const fresh = this._freshSegs(pl, st.last);
                const held = await this._consume(fresh, st);
                if (held && !this.stopped) { pl = await this._refreshPl(url, pl); continue; }
                if (this._shouldStop(pl, fresh, st.idleSince)) break;
                await sleepMs(this, Math.max(500, pl.target * 500));
                if (this.stopped) break;
                const next = await this._pl(url, 1).catch(() => null);
                if (next) { pl = next; st.fails = 0; } else if (++st.fails > 12) break;
            }
            await this._closePart(true);
        }
    }
    const boxAt = (b, tag) => {
        const c0 = tag.charCodeAt(0), c1 = tag.charCodeAt(1), c2 = tag.charCodeAt(2), c3 = tag.charCodeAt(3);
        for (let i = 4; i + 4 <= b.length; i++) if (b[i] === c0 && b[i + 1] === c1 && b[i + 2] === c2 && b[i + 3] === c3) return i + 4;
        return -1;
    };
    const aotOf = (b, e) => {
        let i = e + 4;
        const len = () => { let n = 0, c; do { c = b[i++]; n = (n << 7) | (c & 127); } while ((c & 128) && i < b.length); return n; };
        try {
            if (b[i++] !== 3) return 2;
            len();
            const fl = b[i + 2];
            i += 3;
            if (fl & 0x80) i += 2;
            if (fl & 0x40) i += 1 + b[i];
            if (fl & 0x20) i += 2;
            if (b[i++] !== 4) return 2;
            len();
            i += 13;
            if (b[i++] !== 5) return 2;
            len();
            const aot = b[i] >> 3;
            return aot === 31 || !aot ? 2 : aot;
        } catch { return 2; }
    };
    const codecsOf = b => {
        const hex = n => n.toString(16).padStart(2, '0');
        const v = boxAt(b, 'avcC'), h = v < 0 ? boxAt(b, 'hvcC') : -1;
        if (v < 0 && h < 0) return null;
        const out = [v >= 0 ? `avc1.${hex(b[v + 1])}${hex(b[v + 2])}${hex(b[v + 3])}` : hevcCodecString({ space: b[h + 1] >> 6, tier: (b[h + 1] >> 5) & 1, prof: b[h + 1] & 31, compat: [...b.subarray(h + 2, h + 6)], cons: [...b.subarray(h + 6, h + 12)], level: b[h + 12] })];
        if (boxAt(b, 'mp4a') >= 0) {
            const e = boxAt(b, 'esds');
            out.push(`mp4a.40.${e >= 0 ? aotOf(b, e) : 2}`);
        }
        return out.join(',');
    };
    const audioCodecOf = b => {
        if (boxAt(b, 'mp4a') >= 0) { const e = boxAt(b, 'esds'); return `mp4a.40.${e >= 0 ? aotOf(b, e) : 2}`; }
        if (boxAt(b, 'Opus') >= 0) return 'opus';
        if (boxAt(b, 'ac-3') >= 0) return 'ac-3';
        if (boxAt(b, 'ec-3') >= 0) return 'ec-3';
        return null;
    };
    const timescaleOf = b => {
        const i = boxAt(b, 'mdhd');
        if (i < 0) return 0;
        const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
        return b[i] === 1 ? dv.getUint32(i + 20) : dv.getUint32(i + 12);
    };
    const tfdtSec = (b, ts) => {
        const i = boxAt(b.subarray(0, Math.min(b.length, 1024)), 'tfdt');
        if (i < 0 || !ts) return null;
        const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
        const t = b[i] === 1 ? dv.getUint32(i + 4) * TWO32 + dv.getUint32(i + 8) : dv.getUint32(i + 4);
        return t / ts;
    };
    const isInit = d => d.length > 8 && d[4] === 0x66 && d[5] === 0x74 && d[6] === 0x79 && d[7] === 0x70;

    const bufferedHas = (b, t) => {
        for (let i = 0; i < b.length; i++) if (t >= b.start(i) - 0.05 && t <= b.end(i)) return true;
        return false;
    };
    class HlsPlayer {
        constructor(o) {
            this.o = { maxHeight: Infinity, maxBacklog: 2, keep: 30, lag: 10, ...o };
            this.v = this.o.video;
            this.ms = null; this.sb = null; this.src = ''; this.q = []; this.codec = ''; this.ts = 90000;
            this.align = false; this.dead = false; this.started = false; this.cap = null; this.timer = 0; this.error = null; this.shift = false; this.full = false;
            this.asb = null; this.aq = []; this.acap = null; this.aCodec = ''; this.wantAudio = false; this.aDead = false; this.t0 = Date.now();
        }
        get stats() { return this.cap ? this.cap.stats : {}; }
        async start() {
            if (typeof MediaSource === 'undefined') throw new Error('MediaSource is not supported');
            const ms = this.ms = new MediaSource();
            this.src = URL.createObjectURL(ms);
            this.v.src = this.src;
            await new Promise((res, rej) => { ms.addEventListener('sourceopen', res, { once: true }); setTimeout(() => rej(new Error('sourceopen timeout')), 10000); });
            if (this.dead) return { error: null, stats: {} };
            this._onErr = () => this.fail(new Error('video error'));
            this.v.addEventListener('error', this._onErr);
            this._onPause = () => { if (!this.dead && this.started) { this.o.hold = true; this.shift = true; } };
            this._onPlay = () => { this.o.hold = false; if (this.full) { this.full = false; this._pump(); } };
            this.v.addEventListener('pause', this._onPause);
            this.v.addEventListener('play', this._onPlay);
            this.timer = setInterval(() => this._tick(), 1000);
            this.cap = new HlsCapture({
                url: this.o.url, fetchBin: this.o.fetchBin, maxHeight: this.o.maxHeight, maxBacklog: this.o.maxBacklog, gate: () => this._gate(),
                hint: this.o.hint, onOpened: this.o.onOpened,
                partBytes: Infinity, totalBytes: 0,
                onAudio: au => this._startAudio(au),
                makeSink: () => this._sink(), onPart: async () => {}
            });
            const res = await this.cap.start();
            this.acap?.stop();
            const out = { error: this.error || res.error, stats: res.stats };
            if (!this.dead) this.o.onEnd?.(out);
            return out;
        }
        _sink() {
            const p = this;
            return { size: 0, append(d) { this.size += d.byteLength; p._enqueue(d); }, finish: async () => new Blob([]), remove: async () => {} };
        }
        _enqueue(d) {
            if (this.dead) return;
            this.q.push({ init: isInit(d), data: d });
            this._pump();
        }
        _startAudio(au) {
            if (this.dead || this.acap) return;
            this.wantAudio = true;
            this.acap = new HlsCapture({
                url: au.url, fetchBin: this.o.fetchBin, maxBacklog: this.o.maxBacklog, gate: () => this._aGate(),
                partBytes: Infinity, totalBytes: 0,
                makeSink: () => this._aSink(), onPart: async () => {}
            });
            this.acap.start().then(r => { if (!this.dead && r && r.error) this._dropAudio(r.error); }).catch(e => this._dropAudio(e));
        }
        _aSink() {
            const p = this;
            return { size: 0, append(d) { this.size += d.byteLength; p._aEnqueue(d); }, finish: async () => new Blob([]), remove: async () => {} };
        }
        _aEnqueue(d) {
            if (this.dead || this.aDead) return;
            this.aq.push({ init: isInit(d), data: d });
            this._pump();
            this._aPump();
        }
        _aGate() { return this.full || this.aq.length > 6 || (!!this.o.hold && this._ahead() > 90); }
        _aMime(data) {
            const codec = audioCodecOf(data);
            if (!codec) throw new Error('unsupported audio codec');
            const mime = `audio/mp4; codecs="${codec}"`;
            if (!MediaSource.isTypeSupported(mime)) throw new Error(`unsupported audio ${codec}`);
            return { codec, mime };
        }
        _aCreate(data) {
            const { codec, mime } = this._aMime(data);
            const sb = this.asb = this.ms.addSourceBuffer(mime);
            sb.addEventListener('updateend', () => { this._after(); this._aPump(); });
            sb.addEventListener('error', () => this._dropAudio(new Error('audio append error')));
            this.aCodec = codec;
        }
        _aInit(data) {
            if (!this.asb) { this._aCreate(data); return; }
            const { codec, mime } = this._aMime(data);
            if (codec !== this.aCodec) { this.asb.changeType(mime); this.aCodec = codec; }
        }
        _audioReady() {
            if (!this.wantAudio || this.aDead || this.asb) return true;
            const a = this.aq[0];
            if (!a || !a.init) return false;
            try { this._aCreate(a.data); queueMicrotask(() => this._aPump()); } catch (e) { this._dropAudio(e); }
            return true;
        }
        _aOpen() { return !this.dead && !this.aDead && !!this.ms && this.ms.readyState === 'open'; }
        _aCanPump() { return this._aOpen() && this.aq.length > 0 && !(this.asb && this.asb.updating); }
        _aPrepare(op) {
            if (op.init) { this._aInit(op.data); return true; }
            if (!this.asb) return false;
            if (this.sb) this.asb.timestampOffset = this.sb.timestampOffset;
            return true;
        }
        _aPump() {
            if (!this._aCanPump() || !this.asb) return;
            const op = this.aq[0];
            try {
                const ok = this._aPrepare(op);
                this.aq.shift();
                if (ok) this.asb.appendBuffer(op.data);
            } catch (e) { this._aErr(e, op); }
        }
        _aErr(e, op) {
            if (!e || e.name !== 'QuotaExceededError') { this._dropAudio(e); return; }
            if (this.aq[0] !== op) this.aq.unshift(op);
            if (!this._trim(true) && !(this.v.paused || this.o.hold)) this._dropAudio(e);
        }
        _dropAudio(e) {
            if (this.aDead) return;
            this.aDead = true;
            this.o.onAudioFail?.(e);
            try { this.acap?.stop(); } catch {}
            this.aq.length = 0;
            if (this.asb) {
                try { this.asb.abort(); } catch {}
                try { this.ms.removeSourceBuffer(this.asb); } catch {}
                this.asb = null;
            }
            this._after();
            this._pump();
        }
        _ahead() { const b = this.v.buffered; return b.length ? b.end(b.length - 1) - this.v.currentTime : 0; }
        _gate() { return this.full || this.q.length > 6 || (!!this.o.hold && this._ahead() > 90); }
        _end() { const b = this.sb && this.sb.buffered; return b && b.length ? b.end(b.length - 1) : 0; }
        _canPump() {
            const ms = this.ms;
            if (this.dead || !ms || ms.readyState !== 'open' || !this.q.length) return false;
            return !(this.sb && this.sb.updating);
        }
        _createBuffer(mime, codec) {
            if (!MediaSource.isTypeSupported(mime)) throw new Error(`unsupported codec ${codec}`);
            const sb = this.sb = this.ms.addSourceBuffer(mime);
            sb.addEventListener('updateend', () => { this._after(); this._pump(); });
            sb.addEventListener('error', () => this.fail(new Error('append error')));
            this.codec = codec;
        }
        _applyInit(data) {
            const codec = codecsOf(data);
            if (!codec) throw new Error('unsupported codec');
            const mime = `video/mp4; codecs="${codec}"`;
            if (!this.sb) this._createBuffer(mime, codec);
            else {
                if (codec !== this.codec) { this.sb.changeType(mime); this.codec = codec; }
                this.align = true;
            }
            this.ts = timescaleOf(data) || 90000;
        }
        _alignOffset(data) {
            this.align = false;
            const t0 = tfdtSec(data, this.ts), end = this._end();
            this.sb.timestampOffset = (t0 !== null && t0 + 0.5 < end) ? end - t0 : 0;
        }
        _onAppendError(e, op) {
            if (!e || e.name !== 'QuotaExceededError') { this.fail(e); return; }
            if (this.q[0] !== op) this.q.unshift(op);
            if (this._trim(true)) return;
            if (this.v.paused || this.o.hold) this.full = true; else this.fail(e);
        }
        _pump() {
            if (!this._canPump()) return;
            const op = this.q[0];
            if (op.init && !this.sb && !this._audioReady()) return;
            try {
                if (op.init) this._applyInit(op.data);
                else if (this.align) this._alignOffset(op.data);
                this.q.shift();
                this.sb.appendBuffer(op.data);
            } catch (e) { this._onAppendError(e, op); }
        }
        _trimAudio(cut) {
            const a = this.asb;
            if (!a || a.updating || !a.buffered.length || cut <= a.buffered.start(0)) return;
            try { a.remove(a.buffered.start(0), cut); } catch {}
        }
        _trim(force) {
            const sb = this.sb;
            if (!sb || sb.updating || !sb.buffered.length) return false;
            const s = sb.buffered.start(0), cut = this.v.currentTime - (force ? 2 : this.o.keep);
            if (cut - s <= 1) return false;
            this._trimAudio(cut);
            try { sb.remove(s, cut); return true; } catch { return false; }
        }
        _after() {
            const v = this.v;
            if (!this.started && v.buffered.length && !(this.wantAudio && !this.asb && !this.aDead)) {
                this.started = true; this.tStart = Date.now();
                try { v.currentTime = v.buffered.start(0) + 0.001; } catch {}
                v.play().catch(() => { if (!v.muted) { v.muted = true; this.o.onMuted?.(); v.play().catch(() => {}); } });
            }
        }
        _seekIntoBuffer(b) {
            const v = this.v, cur = v.currentTime;
            for (let i = 0; i < b.length; i++) if (b.start(i) > cur) { v.currentTime = b.start(i) + 0.001; return; }
            if (cur < b.start(0)) v.currentTime = b.start(0) + 0.001;
        }
        _chaseLive(b) {
            const v = this.v, cur = v.currentTime, end = b.end(b.length - 1);
            if (end - cur > this.o.lag && !this.shift) v.currentTime = end - 1.5;
            else if (v.paused && !v.ended && !this.o.hold) v.play().catch(() => {});
        }
        _audioTick() {
            if (!this.wantAudio || this.aDead) return;
            this._pump();
            this._aPump();
            if (!this.started && Date.now() - this.t0 > 15000) this._dropAudio(new Error('audio timeout'));
        }
        _diag() {
            const r = b => { try { return b && b.length ? `${b.start(0).toFixed(1)}-${b.end(b.length - 1).toFixed(1)}` : '-'; } catch { return '?'; } };
            const v = this.v;
            return `no video v${this.sb ? r(this.sb.buffered) : 'x'} a${this.asb ? r(this.asb.buffered) : 'x'} s${v.readyState} t${v.currentTime.toFixed(1)}${this.aDead ? ' ad' : ''}`;
        }
        _videoWatch() {
            if (!this.started || this.error || this.v.videoWidth || Date.now() - this.tStart < 8000) return;
            this.fail(new Error(this._diag()));
        }
        _syncPlayback() {
            const v = this.v, b = v.buffered;
            if (!this.started || !b.length) return;
            if (bufferedHas(b, v.currentTime)) this._chaseLive(b); else this._seekIntoBuffer(b);
        }
        _tick() {
            if (this.dead) return;
            this._audioTick();
            const v = this.v, sb = this.sb;
            if (!sb) return;
            if (!sb.updating) this._trim(false);
            if (this.full && !v.paused) { this.full = false; this._pump(); }
            this._videoWatch();
            this._syncPlayback();
        }
        fail(e) {
            if (this.dead) return;
            this.error = this.error || e;
            this.o.onError?.(e);
            this.cap?.stop();
            this.acap?.stop();
        }
        stop() {
            if (this.dead) return;
            this.dead = true;
            this.cap?.stop();
            try { this.acap?.stop(); } catch {}
            clearInterval(this.timer);
            this.q.length = 0; this.aq.length = 0;
            try { this.v.removeEventListener('error', this._onErr); this.v.removeEventListener('pause', this._onPause); this.v.removeEventListener('play', this._onPlay); } catch {}
            try { this.v.pause(); this.v.removeAttribute('src'); this.v.load(); } catch {}
            if (this.src) { try { URL.revokeObjectURL(this.src); } catch {} }
        }
    }
    return { Fmp4Mux, NeedNewInit, demuxTs, parseSps, parseHevcSps, hevcCodecString, codecsOf, parsePlaylist, pickVariant, DiskBuffer, HlsCapture, HlsPlayer, buildInit };
})();

const REC_MIME_MP4 = ['video/mp4;codecs=avc1,mp4a.40.2', 'video/mp4'];
const REC_MIME_WEBM = ['video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/webm'];
const recMimeFor = fmt => {
    const order = fmt === 'webm' ? [...REC_MIME_WEBM, ...REC_MIME_MP4] : [...REC_MIME_MP4, ...REC_MIME_WEBM];
    return order.find(t => MediaRecorder.isTypeSupported(t)) || 'video/webm';
};
const REC_FMT_T = {
    EN: ['Format', 'MP4 is not supported by this browser, WebM will be used'],
    RU: ['Формат', 'Этот браузер не поддерживает MP4, будет использован WebM'],
    UK: ['Формат', 'Цей браузер не підтримує MP4, буде використано WebM'],
    ES: ['Formato', 'Este navegador no admite MP4, se usará WebM'],
    DE: ['Format', 'MP4 wird von diesem Browser nicht unterstützt, es wird WebM verwendet'],
    FR: ['Format', 'MP4 n’est pas pris en charge par ce navigateur, WebM sera utilisé'],
    PT: ['Formato', 'Este navegador não suporta MP4, será usado WebM'],
    IT: ['Formato', 'MP4 non è supportato da questo browser, verrà usato WebM'],
    PL: ['Format', 'Ta przeglądarka nie obsługuje MP4, zostanie użyty WebM'],
    TR: ['Biçim', 'Bu tarayıcı MP4 desteklemiyor, WebM kullanılacak'],
    NL: ['Formaat', 'Deze browser ondersteunt MP4 niet, WebM wordt gebruikt'],
    CS: ['Formát', 'Tento prohlížeč nepodporuje MP4, bude použit WebM'],
    RO: ['Format', 'Acest browser nu acceptă MP4, se va folosi WebM'],
    HU: ['Formátum', 'Ez a böngésző nem támogatja az MP4-et, WebM lesz használva'],
    BG: ['Формат', 'Този браузър не поддържа MP4, ще се използва WebM'],
    EL: ['Μορφή', 'Αυτό το πρόγραμμα περιήγησης δεν υποστηρίζει MP4, θα χρησιμοποιηθεί WebM'],
    JA: ['形式', 'このブラウザはMP4に対応していないため、WebMが使用されます'],
    ZH: ['格式', '此浏览器不支持 MP4,将使用 WebM'],
    KO: ['형식', '이 브라우저는 MP4를 지원하지 않아 WebM이 사용됩니다'],
    HE: ['פורמט', 'הדפדפן הזה אינו תומך ב-MP4, ייעשה שימוש ב-WebM'],
    AR: ['الصيغة', 'هذا المتصفح لا يدعم MP4، وسيتم استخدام WebM'],
    HI: ['फ़ॉर्मैट', 'यह ब्राउज़र MP4 का समर्थन नहीं करता, WebM का उपयोग किया जाएगा'],
    ID: ['Format', 'Browser ini tidak mendukung MP4, WebM akan digunakan'],
    VI: ['Định dạng', 'Trình duyệt này không hỗ trợ MP4, sẽ dùng WebM'],
    TH: ['รูปแบบ', 'เบราว์เซอร์นี้ไม่รองรับ MP4 จะใช้ WebM แทน']
};
const recFmtText = () => REC_FMT_T[AppState.lang] || REC_FMT_T.EN;

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

        this.mRec = null;
        this.disk = null;
        this.interval = null;
        this.seconds = 0;
        this.totalSize = 0;
        this.savedBytes = 0;
        this.cfg = recCfg();
        this.partNum = 1;
        this.partStart = 0;
        this.recStartTime = '';
        this.splitRequested = false;
        this.wakeLock = null;
        this.currentStream = null;
        this._onTrackEnded = () => this.stopActive();

        onPageEvent('visibilitychange', () => {
            if (!realHidden() && this.mRec && this.mRec.state === 'recording') this.acquireWakeLock();
        });
        setTimeout(async () => {
            try {
                await HlsKit.DiskBuffer.cleanup();
                const n = (await HlsKit.DiskBuffer.orphans()).length;
                const b = this.ui?.elements?.bRec;
                if (n && b) this.ui.flash(b, `↻ ${n}`);
            } catch {}
        }, 6000);
        window.addEventListener('beforeunload', e => {
            if (this.mRec && this.mRec.state === 'recording') {
                e.preventDefault();
                e.returnValue = '';
            }
        });
    }

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

    acquireWakeLock() {
        if (!('wakeLock' in navigator) || this.wakeLock || this._wlPending) return;
        this._wlPending = true;
        navigator.wakeLock.request('screen').then(wl => {
            this._wlPending = false;
            if (!(this.mRec && this.mRec.state !== 'inactive')) { wl.release().catch(() => {}); return; }
            this.wakeLock = wl;
            wl.addEventListener('release', () => { if (this.wakeLock === wl) this.wakeLock = null; });
        }).catch(() => { this._wlPending = false; });
    }

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

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

    saveBlob(blob, ext, durationMs, name, disk) {
        return saveRecording(blob, ext, durationMs, name, disk);
    }

    async recoverOrphans() {
        let n = 0;
        for (const e of await HlsKit.DiskBuffer.orphans(undefined, 0)) {
            const ext = e.meta.ext || 'webm';
            await this.saveBlob(HlsKit.DiskBuffer.blobOf(e), ext, 0, `${e.meta.name || 'REC'}_recovered_${e.id}.${ext}`, null);
            await HlsKit.DiskBuffer.markDoneById(e.id);
            n++;
            await new Promise(r => setTimeout(r, 800));
        }
        return n;
    }

    async fillRecovery(box) {
        try {
            const n = (await HlsKit.DiskBuffer.orphans(undefined, 0)).length;
            if (!n || !box.isConnected) return;
            const b = mk('button', 'bm-tbtn');
            b.textContent = `${L().rs_recover} (${n})`;
            b.onclick = async () => { b.disabled = true; await this.recoverOrphans(); b.remove(); };
            box.append(b);
        } catch {}
    }

    hlsFetch(url, range) {
        return gmBytes(url, null, false, range);
    }

    startHls(btn, sub) {
        const url = getStreamUrl();
        if (!url) throw new Error('no hls url');
        const cfg = this.cfg = recCfg();
        const base = `${currentSite.getName() || 'REC'}_${this.recStartTime = Helpers.getTimestamp()}`;
        this.partNum = 1; this.seconds = 0; this.savedBytes = 0; this.totalSize = 0; this.currentStream = null;
        let userStop = false;
        const cap = newHlsCapture({
            url, cfg, base, fetchBin: (u, r) => this.hlsFetch(u, r),
            onProgress: st => { this.totalSize = st.part; this.savedBytes = st.saved; },
            save: (blob, name, sink) => this.saveBlob(blob, 'mp4', 0, name, sink)
        });
        const shim = this.mRec = { state: 'recording', stop: () => { userStop = true; shim.state = 'inactive'; cap.stop(); } };
        this.hls = cap;
        btn.classList.add('on', 'rec-active');
        const label = () => `${Helpers.formatTime(this.seconds)} | ${fmtShort(this.savedBytes + this.totalSize)}`;
        if (sub) sub.textContent = label();
        clearInterval(this.interval);
        const t0 = Date.now();
        this.interval = setInterval(() => { this.seconds = Math.floor((Date.now() - t0) / 1000); if (sub) sub.textContent = label(); }, 1000);
        this.acquireWakeLock();
        cap.start().then(res => {
            shim.state = 'inactive';
            if (this.hls !== cap || this.mRec !== shim) return;
            const v = this.vc.cachedVideo;
            if (res.error && !userStop && !res.stats.segments && v) {
                this.ui.flash(btn, '! HLS');
                try { this.start(v, btn, sub); return; } catch {}
            }
            if (res.error && !userStop) this.ui.flash(btn, `! ${L().err}`);
            this.cleanupSession(btn, sub);
        });
    }

    updateInfo() {
        const el = this._rs?.info;
        if (!el || !el.isConnected) return;
        const l = L(), on = this.mRec && this.mRec.state !== 'inactive';
        el.textContent = on ? `${l.rs_now}: ${l.rs_p} ${fmtBytes(this.totalSize)} · ${l.rs_t} ${fmtBytes(this.savedBytes + this.totalSize)}` : '';
    }

    buildRsShell() {
        const { overlay, title, closeBtn, body } = buildModalShell(this.ui.shadow, 'bm-modal rs-modal');
        const close = () => { overlay.classList.remove('open'); clearInterval(this._rsTimer); overlay._fromSt = false; overlay._bk?.(); };
        closeBtn.onclick = close;
        overlay.onclick = e => { if (e.target === overlay && Date.now() - (this._rsAt || 0) > 400) close(); };
        this._rs = { overlay, title, body, close, info: null };
    }

    renderRecSettings() {
        const { title, body } = this._rs;
        const l = L(), cfg = this.cfg = recCfg();
        title.innerHTML = `${icons.rec} ${escHTML(l.rs_title)}`;
        const set = patch => { Store.set('bc_rec', { ...recCfg(), ...patch }); this.renderRecSettings(); };
        const chip = (txt, on, fn) => { const b = mk('button', `bm-tbtn${on ? ' on' : ''}`); b.textContent = txt; b.onclick = fn; return b; };
        const size = v => v >= 1024 ? `${v / 1024} ${l.u_gb}` : `${v} ${l.u_mb}`;

        const qs = mk('div', 'rs-qs');
        Object.keys(REC_Q).forEach(q => {
            const mb = (REC_Q[q][0] + REC_Q[q][1]) / 8 * 60 / 1048576;
            const b = mk('button', `bm-btn${cfg.q === q ? ' sel' : ''}`);
            b.textContent = `${REC_RES[q]} · ${l['rs_q_' + q]} · ${l.rs_upto} ${Math.round(mb)} ${l.rs_mbmin} (${(mb * 60 / 1024).toFixed(1)} ${l.rs_gbh})`;
            b.onclick = () => set({ q });
            qs.append(b);
        });
        const fmts = mk('div', 'rs-chips');
        [['mp4', 'MP4'], ['webm', 'WebM']].forEach(([v, n]) => fmts.append(chip(n, cfg.fmt === v, () => set({ fmt: v }))));
        const fmtNote = cfg.fmt === 'mp4' && typeof MediaRecorder !== 'undefined' && !REC_MIME_MP4.some(t => MediaRecorder.isTypeSupported(t))
            ? textEl('rs-note', recFmtText()[1]) : null;
        const srcs = mk('div', 'rs-chips');
        [['cap', l.rs_src_cap], ['hls', l.rs_src_hls]].forEach(([v, n]) => srcs.append(chip(n, cfg.src === v, () => set({ src: v }))));
        const recov = mk('div', 'rs-chips');
        this.fillRecovery(recov);
        const parts = mk('div', 'rs-chips');
        REC_PART.forEach(v => parts.append(chip(size(v), cfg.part === v, () => set({ part: v }))));
        const totals = mk('div', 'rs-chips');
        REC_TOTAL.forEach(v => totals.append(chip(v ? size(v) : l.rs_off, cfg.total === v, () => set({ total: v }))));
        const info = mk('div', 'rs-info');
        this._rs.info = info;
        const vid = (typeof largestVideo === 'function' && largestVideo()) || null;
        const res = vid && vid.videoHeight ? `${vid.videoWidth}×${vid.videoHeight}` : '—';
        const resEl = textEl('rs-note', `${l.rs_cur} ${res}`);
        const resNote = textEl('rs-note', l.rs_resnote);
        body.replaceChildren(
            textEl('hd-sec', l.rs_quality), qs,
            textEl('hd-sec', l.rs_src), srcs, textEl('rs-note', l.rs_src_note), recov,
            textEl('hd-sec', recFmtText()[0]), fmts, ...(fmtNote ? [fmtNote] : []),
            textEl('hd-sec', l.rs_part), parts,
            textEl('hd-sec', l.rs_total), totals,
            info,
            resEl, resNote,
            textEl('rs-note', l.rs_hint),
            textEl('rs-note', l.rs_next)
        );
        this.updateInfo();
    }

    openSettings() {
        if (!this._rs) this.buildRsShell();
        const { overlay } = this._rs;
        this.renderRecSettings();
        this._rsAt = Date.now();
        overlay.classList.add('open');
        clearInterval(this._rsTimer);
        this._rsTimer = setInterval(() => {
            if (!overlay.classList.contains('open')) return clearInterval(this._rsTimer);
            this.updateInfo();
        }, 1000);
    }

    toggle() {
        const v = this.vc.cachedVideo;
        const btn = this.ui.elements.bRec;
        const sub = btn.querySelector('sub');
        if (this.mRec && this.mRec.state !== 'inactive') return this.stop(btn, sub);
        if (recCfg().src === 'hls' && getStreamUrl()) { try { return this.startHls(btn, sub); } catch {} }
        if (!v) return this.ui.flash(btn, `! ${L().no_vid}`);
        try { this.start(v, btn, sub); } catch { this.ui.flash(btn, `! ${L().err}`); }
    }

    stop(btn, sub) {
        this.splitRequested = false;
        try { this.mRec.stop(); } catch { this.currentStream?.getTracks().forEach(t => t.stop()); }
        this.cleanupSession(btn, sub);
    }

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

        const mt = recMimeFor(recCfg().fmt);
        this.recStartTime = Helpers.getTimestamp();
        this.partNum = 1;
        this.seconds = 0;
        this.savedBytes = 0;
        this.cfg = recCfg();
        this.startNewRecord(s, mt, mt.includes('mp4') ? 'mp4' : 'webm', btn, sub);
        btn.classList.add('on', 'rec-active');
        const label = () => `${Helpers.formatTime(this.seconds)} | ${fmtShort(this.savedBytes + this.totalSize)}`;
        if (sub) sub.textContent = label();
        clearInterval(this.interval);
        const t0 = Date.now();
        this.interval = setInterval(() => {
            this.seconds = Math.floor((Date.now() - t0) / 1000);
            if (sub) sub.textContent = label();
        }, 1000);
    }

    initMediaRecorder(stream, mimeType) {
        this.totalSize = 0;
        this.mRec = null;
        this.mRec = newMediaRecorder(stream, mimeType, this.cfg.q);
        if (!this.mRec) throw new Error('MediaRecorder init failed');
    }

    bindRecorderEvents(stream, mimeType, ext, btn, sub) {
        const stopRec = () => { try { this.mRec.stop(); } catch {} };
        const savePart = (disk, dur) => {
            const suffix = (this.partNum > 1 || this.splitRequested) ? `_PT${this.partNum}` : '';
            const name = `${currentSite.getName() || 'REC'}_${this.recStartTime}${suffix}.${ext}`;
            disk.finish().then(blob => this.saveBlob(blob, ext, dur, name, disk)).catch(() => {});
        };
        const continueSplit = () => {
            this.partNum++;
            this.splitRequested = false;
            const v = this.vc.cachedVideo;
            const nextStream = (!stream.active && v ? this.getStream(v) : null) || stream;
            this.currentStream = nextStream;
            try { this.startNewRecord(nextStream, mimeType, ext, btn, sub); return true; } catch { return false; }
        };

        this.mRec.ondataavailable = e => {
            if (!(e.data?.size > 0)) return;
            this.disk.append(e.data);
            this.totalSize += e.data.size;
            const limTotal = this.cfg.total * 1048576;
            if (limTotal && this.savedBytes + this.totalSize >= limTotal) { this.splitRequested = false; return stopRec(); }
            if (this.totalSize >= this.cfg.part * 1048576 && !this.splitRequested) { this.splitRequested = true; stopRec(); }
        };
        this.mRec.onerror = () => this.stopActive();
        this.mRec.onstop = () => {
            const disk = this.disk;
            this.disk = null;
            const dur = performance.now() - this.partStart;
            if (disk && disk.size > 0) savePart(disk, dur); else disk?.remove();
            this.savedBytes += this.totalSize;
            if (this.splitRequested && continueSplit()) return;
            this.cleanupSession(btn, sub);
        };
    }

    startNewRecord(stream, mimeType, ext, btn, sub) {
        this.initMediaRecorder(stream, mimeType);
        this.disk = new HlsKit.DiskBuffer({ meta: { name: `${currentSite.getName() || 'REC'}_${this.recStartTime}_PT${this.partNum}`, ext, mime: mimeType.split(';')[0] } });
        stream.getTracks().forEach(t => t.addEventListener('ended', this._onTrackEnded));
        this.bindRecorderEvents(stream, mimeType, ext, btn, sub);

        try {
            this.partStart = performance.now();
            this.mRec.start(1000);
            this.acquireWakeLock();
        } catch (err) {
            this.cleanupSession(btn, sub);
            this.ui.flash(btn, `! ${L().err}`);
            throw err;
        }
    }
}

const MV_MAX = 100;
const MV_CSS = `.fav-modal .mv-pane{display:none}
.bm-overlay.pipkeep:not(.open){opacity:.02!important}
.fav-modal:not(.mode-mv) .mv-pane.keep{display:flex!important;position:fixed!important;left:0!important;top:0!important;width:100%!important;height:auto!important;opacity:.02!important;pointer-events:none!important;z-index:0!important}
.bm-overlay.pipkeep:not(.open) .fav-modal:not(.mode-mv) .mv-pane.keep{opacity:1!important}
.fav-modal.mode-mv .mv-pane{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;gap:10px;padding:12px 14px}
.fav-modal.mode-mv .bm-search-wrap,.fav-modal.mode-mv .bm-tags-wrap,.fav-modal.mode-mv .bm-list,.fav-modal.mode-mv .bm-foot,.fav-modal.mode-mv .bm-tools,.fav-modal.mode-mv .bm-setp,.fav-modal.mode-mv .hs-pane,.fav-modal.mode-mv .cal-pane{display:none!important}
.mv-bar{display:flex;flex:none;flex-wrap:wrap;align-items:center;gap:8px}
.mv-bar .bm-tbtn{display:inline-flex;align-items:center;justify-content:center;gap:6px}
.mv-bar .bm-tbtn svg{display:block;flex:none;margin:0}
.mv-bar{flex-wrap:nowrap}
.mv-bar>*{flex:1 1 0!important;min-width:0!important;margin:0!important}
.mv-bar>:first-child{flex:2.4 1 0!important}
.mv-bar>:not(:first-child){padding:0!important}
.mv-bar>.bm-hbtn{width:auto!important;height:36px;display:inline-flex;align-items:center;justify-content:center}
.mv-bar>.bm-hbtn svg{width:18px!important;height:18px!important}
.mv-bar>.bm-hbtn.on{background:var(--th);border-color:transparent}
.mv-bar>.bm-gear{border:1px solid var(--th)!important}
.mv-bar>.bm-gear.on{border-color:transparent!important}
.mv-bar>.mv-clr{color:#e5707e!important;border-color:rgba(229,112,126,.45)!important}
.mv-stage{position:relative;flex:1 1 auto;min-height:0;display:flex}
.mv-grid{flex:1;display:grid;gap:8px;align-content:start;min-width:0;min-height:0;overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent}
.mv-tile{position:relative;display:flex;flex-direction:column;min-width:0;background:#000;border-radius:12px;overflow:hidden;outline:2px solid transparent;outline-offset:-2px;cursor:pointer}
.mv-tile.act{outline-color:var(--th,#3b82f6)}
.mv-tile video{width:100%;height:100%;object-fit:contain;display:block;background:#000;pointer-events:none}
.mv-stg{position:relative;flex:none;aspect-ratio:16/9;min-width:0;background:#000}
.mv-nm{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
.mv-vw{flex:none;display:inline-flex;align-items:center;gap:4px;margin-inline-start:auto}
.mv-vw svg{width:12px;height:12px}
.mv-lbl{display:flex;align-items:center;gap:8px;flex:none;padding:5px 10px;background:rgba(255,255,255,.08);color:#fff;font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;pointer-events:none}
.mv-tb{position:static;flex:none;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;padding:6px 8px;background:rgba(255,255,255,.04);cursor:default}
.mv-tb button{width:100%;min-width:0;height:34px;display:inline-flex;align-items:center;justify-content:center;padding:0;border:0;border-radius:8px;background:rgba(0,0,0,.65);color:#fff;cursor:pointer}
.mv-tb button:hover{background:rgba(0,0,0,.85)}
.mv-msg{white-space:pre-line;word-break:break-word;position:absolute;inset:0;display:flex;align-items:center;justify-content:center;text-align:center;padding:8px;font-size:12px;color:#ddd;background:rgba(0,0,0,.5);pointer-events:none}
.mv-msg.err{pointer-events:auto;cursor:pointer}
.mv-msg.park{pointer-events:auto;cursor:pointer;font-size:clamp(48px,20vw,92px);background:rgba(0,0,0,.18);text-shadow:0 2px 8px rgba(0,0,0,.7)}
.mv-grid.multi .mv-msg.park{font-size:clamp(36px,12vw,56px)}
.mv-grid.multi .mv-ctl{display:none!important}
.mv-tile.bad{min-height:120px}
.mv-tile.bad .mv-stg{aspect-ratio:auto;min-height:120px}
.mv-tile.bad video{display:none}
.mv-tile.bad .mv-msg{position:relative;inset:auto;padding:34px 12px 14px;font-size:11px;line-height:1.35}
.mv-msg.hide{display:none}
.mv-tile:fullscreen{aspect-ratio:auto;width:100%;height:100%;border-radius:0;outline:0}
.mv-tile:-webkit-full-screen{aspect-ratio:auto;width:100%;height:100%;border-radius:0;outline:0}
.mv-tile:fullscreen .mv-stg,.mv-tile:-webkit-full-screen .mv-stg{aspect-ratio:auto;flex:1;min-height:0}
.mv-ctl{position:absolute;left:0;right:0;bottom:0;padding:22px 8px 6px;background:linear-gradient(transparent,rgba(0,0,0,.82));display:none;flex-direction:column;gap:2px;cursor:default;user-select:none;-webkit-user-select:none}
.mv-tile.act.ui .mv-ctl{display:flex}
.mv-tile.bad .mv-ctl{display:none!important}
.mv-tile.nodvr .mv-sk,.mv-tile.nodvr .mv-tm{display:none}
.mv-sk{position:relative;height:24px;touch-action:none;cursor:pointer;display:flex;align-items:center}
.mv-sk-t{position:absolute;left:0;right:0;height:4px;border-radius:2px;background:rgba(255,255,255,.28)}
.mv-sk-f{position:absolute;left:0;height:4px;border-radius:2px;background:var(--th,#3b82f6);width:0}
.mv-sk-k{position:absolute;top:6px;width:12px;height:12px;margin-left:-6px;border-radius:50%;background:#fff;left:0;box-shadow:0 0 0 2px rgba(0,0,0,.35)}
.mv-cr{display:flex;align-items:center;gap:2px}
.mv-cb{flex:none;min-width:34px;height:32px;display:inline-flex;align-items:center;justify-content:center;gap:4px;padding:0 6px;border:0;border-radius:8px;background:transparent;color:#fff;font-size:11px;font-weight:600;line-height:1;cursor:pointer}
.mv-cb:hover{background:rgba(255,255,255,.16)}
.mv-vol{--p:100%;flex:0 1 110px;min-width:64px;width:auto;height:36px;margin:0 6px;padding:0;background:transparent;touch-action:none;cursor:pointer;-webkit-appearance:none;appearance:none;-webkit-tap-highlight-color:transparent}
.mv-vol::-webkit-slider-runnable-track{height:4px;border-radius:2px;background:linear-gradient(90deg,var(--th,#3b82f6) var(--p),rgba(255,255,255,.3) var(--p))}
.mv-vol::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:22px;height:22px;margin-top:-9px;border-radius:50%;background:#fff;border:0;box-shadow:0 0 0 2px rgba(0,0,0,.35)}
.mv-vol::-moz-range-track{height:4px;border-radius:2px;background:rgba(255,255,255,.3)}
.mv-vol::-moz-range-progress{height:4px;border-radius:2px;background:var(--th,#3b82f6)}
.mv-vol::-moz-range-thumb{width:22px;height:22px;border:0;border-radius:50%;background:#fff;box-shadow:0 0 0 2px rgba(0,0,0,.35)}
.mv-lv{flex:none;height:22px;padding:0 9px;margin-left:4px;border:0;border-radius:6px;background:rgba(255,255,255,.2);color:#fff;font-size:10px;font-weight:700;line-height:22px;letter-spacing:.4px;cursor:pointer}
.mv-lv.on{background:#e53935}
.mv-qb{min-width:58px}
.mv-tm{flex:none;margin-left:6px;color:#ddd;font-size:11px;font-variant-numeric:tabular-nums}
.mv-sp{flex:1}
.mv-qm{position:absolute;right:8px;bottom:50px;min-width:150px;max-height:70%;overflow-y:auto;padding:4px;border-radius:10px;background:rgba(20,20,24,.97);box-shadow:0 6px 20px rgba(0,0,0,.5);display:none;flex-direction:column;z-index:3}
.mv-qm.open{display:flex}
.mv-qi{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 10px;border-radius:7px;color:#eee;font-size:13px;cursor:pointer;white-space:nowrap}
.mv-qi:hover{background:rgba(255,255,255,.1)}
.mv-qi.sel{color:var(--th,#3b82f6);font-weight:700}
.mv-qs{height:1px;margin:4px 6px;background:rgba(255,255,255,.14)}
.mv-empty{position:absolute;inset:0;display:flex;flex-direction:column;gap:12px;align-items:center;justify-content:center;text-align:center;padding:16px;color:var(--sk-mfg,#aaa);font-size:13px}
.mv-empty .bm-tbtn{display:inline-flex;align-items:center;gap:6px}
.mv-add{position:absolute;inset:0;display:none;flex-direction:column;gap:8px;background:transparent;z-index:2;overflow:hidden}
.mv-add.open{display:flex}
.mv-setp{display:none;flex:none;flex-direction:column;gap:2px;padding:4px 10px;border-radius:10px;background:rgba(0,0,0,.28)}
.mv-setp.open{display:flex}
.mv-stage.adding>.mv-grid,.mv-stage.adding>.mv-empty{visibility:hidden}
.mv-r2{display:flex;flex:none;gap:8px;align-items:center}
.mv-inp{flex:1;min-width:0;box-sizing:border-box;height:36px;padding:0 12px;border-radius:8px;border:1px solid var(--sk-in,rgba(255,255,255,.18));background:var(--sk-fill,rgba(255,255,255,.06));color:var(--sk-fg,#fff);font:inherit;font-size:14px;outline:0;user-select:text;-webkit-user-select:text}
.mv-add .bm-tbtn{flex:none;white-space:nowrap}
.mv-hd{display:flex;flex:none;align-items:center;gap:10px;font-weight:600;font-size:14px;color:var(--sk-fg,#fff)}
.mv-sec{flex:none;font-size:12px;color:var(--sk-mfg,#aaa)}
.mv-it.cur{background:rgba(255,255,255,.1);font-weight:600}
.mv-add>.mv-inp{flex:none}
.mv-list{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:6px;color:var(--sk-mfg,#aaa);font-size:13px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.2) transparent}
.mv-it{display:flex;flex:none;align-items:center;gap:10px;padding:6px 10px;border-radius:10px;background:rgba(255,255,255,.05);cursor:pointer}
.mv-it:hover{background:rgba(255,255,255,.11)}
.mv-it.dis{opacity:.4;pointer-events:none}
.mv-it img{width:52px;height:34px;object-fit:cover;border-radius:6px;background:#000;flex:none}
.mv-it b{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;font-size:13px;color:var(--sk-fg,#fff)}
.mv-it i{font-style:normal;color:var(--sk-mfg,#9aa);font-size:11px;flex:none}
.mv-it{user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
.mv-it img{pointer-events:none}
.mv-ck{display:none;flex:none;width:20px;height:20px;box-sizing:border-box;border-radius:50%;border:2px solid rgba(255,255,255,.4);align-items:center;justify-content:center;font-size:12px;line-height:1;color:#fff}
.mv-list.selmode .mv-ck{display:flex}
.mv-it.sel{background:rgba(255,255,255,.16);box-shadow:inset 0 0 0 2px var(--th,#3b82f6)}
.mv-it.sel .mv-ck{background:var(--th,#3b82f6);border-color:var(--th,#3b82f6)}
.mv-it.sel .mv-ck::after{content:"\\2713"}
.mv-hd .mv-gob{margin-left:auto}
.mv-tb button{font-size:10px;font-weight:700}
.mv-tb button.rec{width:100%;padding:0 4px;background:#e53935;font-size:11px;font-variant-numeric:tabular-nums}
.fav-modal.mode-mv .mv-pane{gap:12px;padding:12px}
.mv-bar{gap:8px;padding-bottom:10px;border-bottom:1px solid rgba(255,255,255,.07)}
.mv-bar .bm-tbtn,.mv-bar .bm-hbtn{height:40px;margin:0;padding:0 14px;border-radius:12px;font-size:13px;font-weight:600;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.06);transition:background .15s,transform .1s,border-color .15s}
.mv-bar .bm-tbtn.on{background:var(--th,#3b82f6);border-color:transparent;box-shadow:0 2px 10px rgba(0,0,0,.35)}
.mv-bar .bm-tbtn:not(.on):hover,.mv-bar .bm-hbtn:hover{background:rgba(255,255,255,.14);border-color:rgba(255,255,255,.22)}
.mv-bar .bm-tbtn:active,.mv-bar .bm-hbtn:active{transform:scale(.96)}
.mv-grid{gap:12px;padding:2px 2px 16px}
.mv-tile{background:var(--sk-card,#16161a);border:1px solid rgba(255,255,255,.1);border-radius:16px;outline:0;box-shadow:0 4px 16px rgba(0,0,0,.45);transition:border-color .2s,box-shadow .2s}
.mv-tile.act{border-color:var(--th,#3b82f6);box-shadow:0 0 0 1px var(--th,#3b82f6),0 6px 22px rgba(0,0,0,.55)}
.mv-lbl{padding:9px 12px;font-size:13px;font-weight:700;background:linear-gradient(180deg,rgba(255,255,255,.1),rgba(255,255,255,.04));border-bottom:1px solid rgba(255,255,255,.06)}
.mv-nm{letter-spacing:.2px}
.mv-vw{padding:2px 8px;border-radius:10px;background:rgba(0,0,0,.35);font-size:11px;font-weight:600}
.mv-stg{border-radius:0;overflow:hidden}
.mv-tb{gap:8px;padding:8px 10px 10px;background:transparent;border-top:1px solid rgba(255,255,255,.06)}
.mv-tb button{height:38px;border-radius:12px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);transition:background .15s,transform .1s,border-color .15s}
.mv-tb button:hover{background:rgba(255,255,255,.16);border-color:rgba(255,255,255,.22)}
.mv-tb button:active{transform:scale(.93)}
.mv-tb button svg{display:block;flex:none}
.mv-tb button.rec{background:#e53935;border-color:transparent;box-shadow:0 2px 10px rgba(229,57,53,.45)}
.mv-tile.act .mv-tb button:nth-child(4){background:var(--th,#3b82f6);border-color:transparent}
.mv-tile:fullscreen .mv-tb,.mv-tile:-webkit-full-screen .mv-tb{display:none}
.mv-tile.bad .mv-tb{opacity:.85}
.mv-ctl{padding:26px 10px 8px}
.mv-qm{border-radius:12px;border:1px solid rgba(255,255,255,.1)}
.mv-empty{gap:14px;font-size:14px}
.mv-empty .bm-tbtn{height:42px;padding:0 20px;border-radius:14px;font-size:14px}
.mv-add{padding:2px;gap:10px}
.mv-hd{font-size:16px;font-weight:700;gap:12px}
.mv-hd .bm-hbtn,.mv-hd .bm-tbtn{height:36px;padding:0 14px;border-radius:12px}
.mv-sec{font-size:12px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;padding:2px 4px 0}
.mv-inp{height:42px;border-radius:12px;padding:0 14px;transition:border-color .15s,box-shadow .15s}
.mv-inp:focus{border-color:var(--th,#3b82f6);box-shadow:0 0 0 3px rgba(255,255,255,.08)}
.mv-r2 .bm-tbtn{height:42px;padding:0 18px;border-radius:12px}
.mv-list{gap:8px}
.mv-it{gap:12px;padding:8px 12px;border-radius:14px;border:1px solid rgba(255,255,255,.07);transition:background .15s,border-color .15s,transform .1s}
.mv-it:hover{border-color:rgba(255,255,255,.18)}
.mv-it:active{transform:scale(.985)}
.mv-it img{width:68px;height:42px;border-radius:8px}
.mv-it b{font-size:14px}
.mv-it i{padding:2px 8px;border-radius:10px;background:rgba(0,0,0,.3);font-weight:600}
.mv-bar{flex-wrap:nowrap;gap:6px;padding-bottom:8px}
.mv-bar .bm-tbtn,.mv-bar .bm-hbtn{height:34px;padding:0 10px;border-radius:10px;font-size:12px;gap:5px;white-space:nowrap;flex:0 1 auto;min-width:0}
.mv-bar .bm-tbtn svg,.mv-bar .bm-hbtn svg{width:14px;height:14px}
.mv-bar .bm-hbtn{min-width:34px;justify-content:center}
.mv-grid{gap:10px}
.mv-tile{border-radius:14px}
.mv-lbl{padding:6px 10px;font-size:12px}
.mv-vw{padding:1px 7px;font-size:10px}
.mv-tb{gap:6px;padding:6px 8px 8px}
.mv-tb button{height:30px;border-radius:9px;background:rgba(0,0,0,.45)}
.mv-tb button svg{width:14px;height:14px}
.mv-tb button.rec{font-size:10px}
.mv-grid{grid-auto-rows:max-content;align-content:start}
.mv-tile{flex:none;min-height:min-content}
.mv-tile.bad{min-height:120px}
.mv-tile:fullscreen,.mv-tile:-webkit-full-screen{min-height:0}
.mv-grid::after{content:'';display:block;height:1px}
.mv-tb{padding-bottom:10px}
.mv-tb button svg,.mv-cb svg{display:block;flex:none;margin:0;overflow:visible}
.mv-tile,.mv-tile *,.mv-grid,.mv-bar,.mv-bar *{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}
.mv-tile *::selection,.mv-tile::selection,.mv-bar *::selection{background:transparent;color:inherit}
.mv-tile video::selection{background:transparent}
.mv-tile button:focus,.mv-tile button:focus-visible,.mv-bar button:focus{outline:0}
.mv-inp,.mv-inp *{-webkit-user-select:text;user-select:text}
.mv-stage{flex:1 1 0;overflow:hidden}
.mv-grid{position:absolute;inset:0;overflow-y:scroll;overflow-x:hidden;-webkit-overflow-scrolling:touch;touch-action:pan-y;overscroll-behavior:contain;scrollbar-gutter:stable}
.mv-list{overflow-y:scroll;-webkit-overflow-scrolling:touch;touch-action:pan-y;overscroll-behavior:contain}
.fav-modal.mode-mv .mv-pane{padding:10px 0 0;gap:10px}
.fav-modal.mode-mv .mv-bar{margin-inline:0;padding-inline:14px;padding-bottom:10px;margin-bottom:0}
.mv-grid{scrollbar-gutter:auto;padding:0 14px 16px}
.mv-list{padding-inline:14px}
.mv-add{padding:2px 14px}
.mv-tb .mv-tbfs{display:none}
.mv-grid.multi .mv-tb{grid-template-columns:repeat(5,minmax(0,1fr))}
.mv-grid.multi .mv-tb .mv-tbfs{display:inline-flex}
.mv-grid.multi .mv-tile.act.ui:fullscreen .mv-ctl,.mv-grid.multi .mv-tile.act.ui:-webkit-full-screen .mv-ctl{display:flex!important}
.mv-tile:fullscreen,.mv-tile:-webkit-full-screen{box-sizing:border-box;border:0!important;border-radius:0!important;box-shadow:none!important;outline:0!important;background:#000!important;margin:0!important;padding:0!important;display:block!important;overflow:hidden!important;cursor:default}
.mv-tile:fullscreen .mv-stg,.mv-tile:-webkit-full-screen .mv-stg{position:absolute!important;inset:0!important;width:100%!important;height:100%!important;aspect-ratio:auto!important;border-radius:0!important;flex:none!important}
.mv-tile:fullscreen video,.mv-tile:-webkit-full-screen video{position:absolute;inset:0;width:100%!important;height:100%!important;object-fit:contain!important;background:#000}
.mv-tile:fullscreen .mv-lbl,.mv-tile:-webkit-full-screen .mv-lbl{position:absolute;top:0;left:0;right:0;z-index:3;border:0;border-radius:0;background:linear-gradient(rgba(0,0,0,.78),transparent);padding:calc(10px + env(safe-area-inset-top,0px)) calc(14px + env(safe-area-inset-right,0px)) 22px calc(14px + env(safe-area-inset-left,0px));opacity:0;transition:opacity .2s;pointer-events:none}
.mv-tile.ui:fullscreen .mv-lbl,.mv-tile.ui:-webkit-full-screen .mv-lbl{opacity:1}
.mv-tile:fullscreen .mv-ctl,.mv-tile:-webkit-full-screen .mv-ctl{z-index:3;padding:26px calc(14px + env(safe-area-inset-right,0px)) calc(8px + env(safe-area-inset-bottom,0px)) calc(14px + env(safe-area-inset-left,0px))}
.mv-tile:fullscreen .mv-qm,.mv-tile:-webkit-full-screen .mv-qm{z-index:4}
`;
const MV_IC = {
    add: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>',
    close: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>'
};
MV_IC.play = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>';
MV_IC.pause = '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M7 5h4v14H7zM13 5h4v14h-4z"/></svg>';
MV_IC.rew = '<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M11 6v12l-8.5-6zM21 6v12l-8.5-6z"/></svg>';
MV_IC.pip = icons.pip.replace('width="20" height="20"', 'width="18" height="18"');
MV_IC.fs = icons.fs.replace('width="20" height="20"', 'width="18" height="18"');
MV_IC.on = icons.vol.replace('width="20" height="20"', 'width="16" height="16"');
MV_IC.off = icons.mute.replace('width="20" height="20"', 'width="16" height="16"');
MV_IC.snap = icons.snap.replace('width="20" height="20"', 'width="16" height="16"');
MV_IC.rec = icons.rec.replace('width="20" height="20"', 'width="16" height="16"');
const mvT = k => L()[k] || i18n.EN[k] || '';
const saveRecording = async (blob, ext, durMs, name, disk) => {
    const fix = async (fn, fb) => { try { return await fn(); } catch { return fb; } };
    if (ext === 'webm') {
        blob = await fix(() => MediaFix.stripSeekHead(blob), blob);
        const d = await fix(() => MediaFix.webmDuration(blob), 0);
        blob = await fix(() => fixWebmDuration(blob, d > 0 ? d : durMs), blob);
        blob = await fix(() => MediaFix.webm(blob), blob);
    } else if (ext === 'mp4') blob = await fix(() => MediaFix.mp4(blob), blob);
    Helpers.download(blob, name);
    if (disk) disk.markDone().finally(() => disk.dispose());
};
const newMediaRecorder = (stream, mimeType, q) => {
    const [vBps, aBps] = REC_Q[q] || REC_Q.hi;
    for (const opts of [{ mimeType, videoBitsPerSecond: vBps, audioBitsPerSecond: aBps }, { mimeType }, undefined]) {
        try { return new MediaRecorder(stream, opts); } catch {}
    }
    return null;
};
const newHlsCapture = ({ url, fetchBin, cfg, base, save, onProgress }) => new HlsKit.HlsCapture({
    url, fetchBin, onProgress, maxHeight: REC_H[cfg.q] ?? Infinity,
    partBytes: cfg.part * 1048576, totalBytes: cfg.total * 1048576,
    makeSink: no => new HlsKit.DiskBuffer({ meta: { name: base, ext: 'mp4', mime: 'video/mp4', part: no, hls: 1 } }),
    onPart: (blob, no, info) => save(blob, `${base}${no > 1 || !info.final ? `_PT${no}` : ''}.mp4`, info.sink)
});
const MV_DVR_OPTS = [0, 60, 120, 300];
const mvKeep = () => Math.max(2, mvDvr());
const mvDvr = () => { const v = Store.get('bc_mv_dvr', 120); return MV_DVR_OPTS.includes(v) ? v : 120; };
const mvAbr = () => Store.get('bc_mv_abr', true) !== false;
const mvBg = () => Store.get('bc_mv_bg', true) !== false;
const mvAbrLast = () => { const o = Store.get('bc_mv_abr_h', null); return isObj(o) && o.h > 0 && Date.now() - o.t < 1800000 ? o.h : 0; };
const ABR_UP_MS = 90000;
const ABR_BACK0 = 120000;
const ABR_LOW = 0.3;
const MvNet = {
    c: navigator.connection || navigator.mozConnection || navigator.webkitConnection || null,
    cap() {
        const t = this.c && this.c.effectiveType;
        if (t === 'slow-2g' || t === '2g') return 1;
        if (t === '3g') return 480;
        return 0;
    },
    watch(fn) { if (this.c && this.c.addEventListener) this.c.addEventListener('change', fn); }
};
const mvGet = (url, referer, range) => gmBytes(url, referer, true, range);
const mvUrl = u => (isStr(u) ? u.replace(/\\\//g, '/').replace(/^\/\//, 'https://') : '');
const mvFindM3u8 = (x, d = 0) => {
    if (d > 6 || x == null) return '';
    if (isStr(x)) { const u = mvUrl(x); return /^https?:\/\/[^\s"']+\.m3u8(\?[^\s"']*)?$/i.test(u) ? u : ''; }
    if (typeof x !== 'object') return '';
    for (const k of Object.keys(x)) { const r = mvFindM3u8(x[k], d + 1); if (r) return r; }
    return '';
};
const mvFindKey = (x, key, d = 0) => {
    if (d > 6 || !x || typeof x !== 'object') return '';
    if (isStr(x[key]) && x[key]) return x[key];
    for (const k of Object.keys(x)) { const r = mvFindKey(x[k], key, d + 1); if (r) return r; }
    return '';
};
const mvJson = async (url, ref) => {
    try { return JSON.parse((await requestOnce(url, { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/plain, */*', 'Referer': ref })) || ''); } catch { return null; }
};
const mvShort = v => String(v).replace(/[A-Za-z0-9_\-+/=]{20,}/g, '<t>').slice(0, 56);
const mvScan = (x, path = '', out = [], d = 0) => {
    if (d > 6 || x == null || out.length >= 8) return out;
    if (typeof x === 'string') { if (x && /video|stream|hls|server|host|edge/i.test(path)) out.push(`${path}=${mvShort(x)}`); return out; }
    if (typeof x !== 'object') return out;
    for (const k of Object.keys(x)) mvScan(x[k], path ? `${path}.${k}` : k, out, d + 1);
    return out;
};
const BC_TOKEN_RES = [/data-csrf_value=["']([^"']+)["']/i, /name=["']_csrf_token["'][^>]*?value=["']([^"']+)["']/i, /value=["']([^"']+)["'][^>]*?name=["']_csrf_token["']/i, /csrf[\w-]*["'\s:=]+["']?([a-f0-9]{24,128})(?![\w-])/i];
const bcCsrfField = html => (String(html).match(/data-csrf_field=["']([^"']+)["']/i) || [])[1] || '_csrf_token';
const bcTokenBag = () => {
    const list = [];
    return { list, add: (t, from, field) => { if (t && !list.some(x => x.t === t)) list.push({ t, from, field: field || '_csrf_token' }); } };
};
const bcHtmlTokens = (html, bag, own) => {
    for (const re of BC_TOKEN_RES) { const m = html.match(re); if (m) { bag.add(m[1], 'html', bcCsrfField(html)); break; } }
    if (own) bag.add((document.cookie.match(/csrf[\w-]*=([^;]+)/i) || [])[1], 'cookie');
};
const bcHtmlDebug = (html, tokens) => {
    const cs = html.search(/csrf/i);
    return `html:${html.length}b tokens:${tokens.map(x => x.from).join('+') || 'no'} csrf@${cs}${cs >= 0 ? `[${mvShort(html.slice(Math.max(0, cs - 12), cs + 48).replace(/\s+/g, ' '))}]` : ''}`;
};
const BC_VS_KEYS = ['videoServerUrl', 'videoServer', 'hlsServerUrl', 'streamServerUrl'];
const bcFindVs = x => {
    for (const k of BC_VS_KEYS) {
        const v = mvFindKey(x?.localData, k) || mvFindKey(x, k);
        if (isStr(v) && /^(https?:)?\/\//.test(mvUrl(v))) return mvUrl(v);
    }
    return '';
};
const bcPick = (st, x) => {
    if (!x) return;
    st.vs = st.vs || bcFindVs(x);
    st.direct = st.direct || mvFindM3u8(x);
    st.guess = st.guess || bcEdgeGuess(mvFindKey(x, 'frame'));
};
const bcFound = st => !!(st.vs || st.direct);
const bcScanHtml = (html, st) => {
    const hv = html.match(new RegExp(`(?:${BC_VS_KEYS.join('|')})["']?\\s*[:=]\\s*["']([^"']+)["']`, 'i'));
    if (hv) st.vs = mvUrl(hv[1]);
    st.guess = st.guess || bcEdgeGuess(html);
    const hm = html.match(/https?:\\?\/\\?\/[^\s"'<>\\]+?\.m3u8[^\s"'<>\\]*/i);
    if (hm) st.direct = mvUrl(hm[0]);
};
const bcAmfTry = async (o, slug, k, st) => {
    const body = `method=getRoomData&args%5B%5D=${encodeURIComponent(slug)}&args%5B%5D=&args%5B%5D=${k.t ? `&${encodeURIComponent(k.field || '_csrf_token')}=${encodeURIComponent(k.t)}` : ''}`;
    const raw = await requestPostOnce(`${o}/tools/amf.php?m=1&t=${Date.now()}`, body, { Referer: `${o}/${encodeURIComponent(slug)}`, Origin: o });
    const j = JSON.parse(raw || '');
    st.dbg.push(`amf(${k.from}):${String(raw).length}b ${j?.status || ''}`);
    bcPick(st, j);
    const un = j?.performerData?.username;
    if (isStr(un) && un) st.name = un;
    if (bcFound(st)) return true;
    const sc = mvScan(j);
    if (sc.length) st.dbg.push(`amf-scan:${sc.join(' ')}`);
    return false;
};
const bcAmf = async (o, slug, tokens, st) => {
    const tries = tokens.length ? tokens.slice(0, 2) : [{ t: '', from: 'none' }];
    for (const k of tries) {
        let found = false;
        try { found = await bcAmfTry(o, slug, k, st); } catch { st.dbg.push(`amf(${k.from}):fail`); }
        if (found) break;
    }
};
const bcChat = async (o, slug, st) => {
    const j = await mvJson(`${o}/get-member-chat-data?username=${encodeURIComponent(slug)}&withMiniProfile=1&liveTab=female`, `${o}/${encodeURIComponent(slug)}`);
    st.dbg.push(j ? 'chat:ok' : 'chat:fail');
    bcPick(st, j);
    if (!j || bcFound(st)) return;
    const sc = mvScan(j);
    st.dbg.push(`chat-scan:${sc.join(' ') || 'nothing'}`);
    st.dbg.push(`chat-keys:${Object.keys(j.result || {}).join(',')}`.slice(0, 260));
};
const bcServerTag = st => st.vs ? originOf(st.vs) || 'bad' : (st.direct ? 'direct' : 'none');
const BC_PLAYLIST_PATHS = ['/hls/stream_{n}/playlist.m3u8'];
const bcEdgeGuess = txt => { const m = String(txt || '').replace(/\\\//g, '/').match(/\/\/mobile-edge(\d+)\.(bcvcdn\.com)/i); return m ? `https://live-edge${m[1]}.${m[2]}` : ''; };
const bcPlaylistCandidates = st => {
    if (st.direct) return [st.direct];
    const bases = [...new Set([st.vs, st.guess].filter(Boolean).map(b => b.replace(/\/$/, '')))];
    const names = [...new Set([st.name, String(st.name).toLowerCase()])];
    return bases.flatMap(b => BC_PLAYLIST_PATHS.flatMap(t => names.map(n => b + t.replace('{n}', n))));
};
const bcProbePlaylist = async (st, ref) => {
    const list = bcPlaylistCandidates(st);
    if (st.direct || !list.length) return list[0] || '';
    for (const u of list) {
        let txt = '';
        try { txt = await requestOnce(u, { Referer: ref }); } catch {}
        if (isStr(txt) && txt.startsWith('#EXTM3U')) { st.dbg.push(`playlist:ok ${u === list[0] ? 'primary' : 'alt'}`); return u; }
    }
    st.dbg.push('playlist:unverified');
    return st.vs ? list[0] : '';
};
const cbDossier = html => {
    try {
        const m = String(html).match(/initialRoomDossier\s*=\s*("(?:[^"\\]|\\.)*")/);
        if (!m) return null;
        let o = JSON.parse(m[1]);
        if (isStr(o)) o = JSON.parse(o);
        return o && typeof o === 'object' ? o : null;
    } catch { return null; }
};
const cbPageHls = html => {
    const o = cbDossier(html);
    const u = o ? (mvUrl(o.hls_source) || mvFindM3u8(o)) : '';
    if (u) return u;
    const r = String(html).match(/(?:"|\\u0022)hls_source(?:"|\\u0022)\s*:\s*(?:"|\\u0022)(https?:(?:(?!\\u0022|")(?:[^\\]|\\u[0-9a-f]{4}))+)/i);
    if (!r) return '';
    try { return mvUrl(JSON.parse(`"${r[1]}"`)); } catch { return ''; }
};
const mvVault = (() => {
    const DB = 'bcu_warm', ST = 'hls', TTL = 2 * 3600000, MAX = 80;
    const mem = new Map();
    let dbp = null, loaded = null;
    const open = () => dbp || (dbp = new Promise(res => {
        try {
            const r = indexedDB.open(DB, 2);
            r.onupgradeneeded = () => { if (!r.result.objectStoreNames.contains(ST)) r.result.createObjectStore(ST); };
            r.onsuccess = () => res(r.result);
            r.onerror = r.onblocked = () => res(null);
        } catch { res(null); }
    }));
    const store = async mode => { const db = await open(); try { return db ? db.transaction(ST, mode).objectStore(ST) : null; } catch { return null; } };
    const preload = () => loaded || (loaded = (async () => {
        const s = await store('readonly');
        if (!s) return;
        await new Promise(res => {
            const c = s.openCursor();
            c.onsuccess = () => { const cur = c.result; if (!cur) return res(); mem.set(cur.key, cur.value); cur.continue(); };
            c.onerror = () => res();
        });
    })());
    const fresh = r => !!r && Date.now() - r.at < TTL;
    const trim = () => {
        if (mem.size <= MAX) return;
        const old = [...mem.entries()].sort((a, b) => a[1].at - b[1].at).slice(0, mem.size - MAX);
        for (const [k] of old) { mem.delete(k); store('readwrite').then(s => s && s.delete(k)).catch(() => {}); }
    };
    return {
        ready: () => Promise.race([preload(), new Promise(r => setTimeout(r, 300))]),
        peek: k => { const r = mem.get(k); return fresh(r) ? r : null; },
        keep: (k, patch) => {
            const r = { ...(mem.get(k) || {}), ...patch, at: Date.now() };
            mem.set(k, r);
            trim();
            store('readwrite').then(s => s && s.put(r, k)).catch(() => {});
        },
        forget: k => { mem.delete(k); store('readwrite').then(s => s && s.delete(k)).catch(() => {}); }
    };
})();
const mvVaultKey = e => {
    try {
        if (e.t === 'url') { const u = new URL(e.url); return `u:${u.origin}${u.pathname}`; }
        const site = urlSite(e.url), slug = modelSlug(e.url);
        return site && slug ? `${site}/${String(slug).toLowerCase()}` : '';
    } catch { return ''; }
};
const mvIsSelf = e => {
    try {
        return e.t !== 'url' && urlSite(e.url) === siteKey(location.hostname)
            && String(e.name).toLowerCase() === String(currentSite.getName()).toLowerCase() && !!getStreamUrl();
    } catch { return false; }
};

const MV_RES = {
    cb: async (e, slug) => {
        const j = await mvJson(`https://chaturbate.com/api/chatvideocontext/${encodeURIComponent(slug)}/`, `https://chaturbate.com/${encodeURIComponent(slug)}/`);
        let url = mvUrl(j?.hls_source) || mvFindM3u8(j);
        if (!url) url = cbPageHls((await requestHtml(e.url)) || '');
        return { url, ref: 'https://chaturbate.com/' };
    },
    sc: async (e, slug) => {
        const o = originOf(e.url) || 'https://stripchat.com', ref = `${o}/`;
        const j = await mvJson(`${o}/api/front/v2/models/username/${encodeURIComponent(slug)}/cam`, `${o}/${encodeURIComponent(slug)}`);
        const direct = mvFindM3u8(j);
        if (direct) return { url: direct, ref };
        const ids = [j?.cam?.streamName, j?.user?.user?.id, j?.user?.id].filter(v => v != null && v !== '').map(String);
        for (const id of [...new Set(ids)]) for (const host of ['doppiocdn.com', 'doppiocdn.net', 'doppiocdn.org', 'doppiocdn.media']) {
            const cand = `https://edge-hls.${host}/hls/${id}/master/${id}_auto.m3u8`;
            const txt = await requestOnce(cand, { Referer: ref });
            if (txt && txt.startsWith('#EXTM3U')) return { url: cand, ref };
        }
        return { url: '', ref };
    },
    bc: async (e, slug) => {
        const o = originOf(e.url), own = o === location.origin, bag = bcTokenBag();
        const st = { name: slug, vs: '', direct: '', dbg: [] };
        if (own) bag.add(bcCsrfToken(), 'page');
        const html = (await requestHtml(e.url)) || '';
        bcHtmlTokens(html, bag, own);
        st.dbg.push(bcHtmlDebug(html, bag.list));
        bcScanHtml(html, st);
        if (!bcFound(st)) await bcAmf(o, slug, bag.list, st);
        if (!bcFound(st)) await bcChat(o, slug, st);
        st.dbg.push(`server:${bcServerTag(st)}`);
        const url = await bcProbePlaylist(st, `${o}/`);
        return { url, ref: `${o}/`, dbg: st.dbg.join('\n') };
    }
};
const mvResolve = async e => {
    if (e.t === 'url') return { url: e.url, ref: `${originOf(e.url)}/` };
    const site = urlSite(e.url), slug = modelSlug(e.url);
    if (site === siteKey(location.hostname) && String(e.name).toLowerCase() === String(currentSite.getName()).toLowerCase()) {
        const cur = getStreamUrl();
        if (cur) return { url: cur, ref: `${location.origin}/` };
    }
    const fn = MV_RES[site];
    return fn && slug ? fn(e, slug) : { url: '' };
};

const MV_FLT = [
    ['bc_mv_hide_off', true, 'bm_online', 'bm_online_t', ''],
    ['bc_mv_hide_priv', true, 'bm_priv', 'st_private', 'st_private'],
    ['bc_mv_hide_group', false, 'st_group', 'st_group', 'st_group'],
    ['bc_mv_hide_vip', false, 'st_vip', 'st_vip', 'st_vip'],
    ['bc_mv_hide_away', false, 'st_away', 'st_away', 'st_away']
];
const mvFltOn = (key, def) => Store.get(key, def) === true;
const mvErr = x => (x ? String(x.message || x).slice(0, 80) : 'ended');

class MvTile {
    constructor(mv, e) {
        this.mv = mv;
        this.e = e;
        this.gen = 0;
        this.tries = 0;
        this.act = false;
        this.player = null;
        this.rt = 0;
        this.q = 0;
        this.vh = [];
        this.um = false;
        this.drag = false;
        this.uat = 0;
        this.ti = 0;
        this.abrH = 0;
        this.abrAt = 0;
        this.abrTimer = 0;
        this.abrBack = ABR_BACK0;
        this.abrBlock = {};
        this.stalls = [];
        this.lastStall = 0;
        this.waitAt = 0;
        this.okSince = 0;
        this.lowN = 0;
        this.savedAt = 0;
        const el = this.el = mk('div', 'mv-tile');
        const v = this.video = document.createElement('video');
        v.muted = true;
        v.volume = Math.min(1, Math.max(0, Number(Store.get('bc_mv_vol', 1)) || 0));
        v.playsInline = true;
        v.setAttribute('playsinline', '');
        this.lbl = textEl('mv-lbl', e.name || '', 'span');
        this.msg = mk('div', 'mv-msg');
        const tb = mk('div', 'mv-tb');
        this.sndBtn = mk('button');
        this.sndBtn.innerHTML = MV_IC.off;
        this.sndBtn.onclick = ev => { ev.stopPropagation(); this.mv.select(this); };
        const rm = mk('button');
        rm.innerHTML = MV_IC.close;
        Helpers.bindLongPress(rm, () => this.idle(), () => this.mv.remove(this), 700);
        rm.onclick = ev => ev.stopPropagation();
        this.shotBtn = mk('button');
        this.shotBtn.innerHTML = MV_IC.snap;
        this.shotBtn.title = L().snap;
        this.shotBtn.onclick = ev => { ev.stopPropagation(); this.shot(); };
        this.recBtn = mk('button');
        this.recBtn.innerHTML = MV_IC.rec;
        this.recBtn.title = L().rec_tip || L().rec;
        this.recBtn.style.touchAction = 'none';
        this.recBtn.onclick = ev => ev.stopPropagation();
        Helpers.bindLongPress(this.recBtn, () => this.toggleRec(), () => this.mv.ui.recorder?.openSettings());
        this.recS = null;
        this.tbFs = mk('button', 'mv-tbfs');
        this.tbFs.innerHTML = MV_IC.fs;
        this.tbFs.title = mvT('mv_fs');
        this.tbFs.onclick = ev => { ev.stopPropagation(); if (!this.act) this.mv.select(this); this.showUi(); this.toggleFs(); };
        tb.append(this.shotBtn, this.recBtn, this.sndBtn, rm, this.tbFs);
        const stg = this.stg = mk('div', 'mv-stg');
        stg.append(v, this.msg);
        el.append(this.lbl, stg, tb);
        this.buildCtl();
        this.setPoster();
        el.onclick = () => { if (this.mv.grid.classList.contains('multi') && !this.isFs()) { if (this.player) this.togglePlay(); return; } if (!this.act) { this.mv.select(this); this.showUi(); } else this.toggleUi(); };
        this.msg.onclick = ev => { if (this.msg.classList.contains('err') || this.msg.classList.contains('park')) { ev.stopPropagation(); this.tries = 0; if (!this.act) this.mv.select(this); this.start(); } };
        v.onplaying = () => { this.msg.classList.add('hide'); this.tries = 0; this.sw = 0; this.played = true; this.setWant(!this.userPause); this.syncPlay(); };
        v.onpause = () => { this.syncPlay(); if (mvBg() && !this.userPause && !this.parked && this.player) setTimeout(() => this.bgResume(), 200); };
        v.addEventListener('enterpictureinpicture', () => { this.pip = true; this.pipKeep(true); });
        v.addEventListener('leavepictureinpicture', () => {
            this.pip = false;
            this.pipKeep(false);
            if (!this.mv.isOpen() && this.player && !mvBg()) this.idle();
        });
        v.onplay = () => { this.syncPlay(); if (mvDvr() === 0 && this.player && this.player.shift) this.goLive(); };
        v.onresize = () => this.paintLbl();
        v.addEventListener('waiting', () => {
            const now = Date.now();
            if (!this.abrReady(now)) return;
            if (!this.waitAt) this.waitAt = now;
            this.abrStall(false);
        });
        v.addEventListener('playing', () => { this.waitAt = 0; });
        this.paintLbl();
        this.lblTimer = setInterval(() => { if (this.el.getClientRects().length) this.paintLbl(); }, 4000);
    }

    viewersNow() {
        try {
            const k = bmKey(this.e.url || '');
            const b = k && AppState.bookmarks.find(x => isObj(x) && isStr(x.url) && bmKey(x.url) === k);
            return b ? bmViews(b) : 0;
        } catch { return 0; }
    }

    paintLbl() {
        const h = this.video.videoHeight, vw = this.viewersNow();
        const key = `${h || 0}|${vw}`;
        if (this._lk === key) return;
        this._lk = key;
        this.lbl.innerHTML = `<span class="mv-nm">${escHTML(this.e.name || '')}${h ? ` · ${h}p` : ''}</span>${vw ? `<span class="mv-vw">${icons.eye}${fmtCount(vw)}</span>` : ''}`;
    }

    flashBtn(btn, txt, back) {
        btn.classList.remove('rec');
        btn.textContent = txt;
        setTimeout(back, 1200);
    }

    paintShot() { this.shotBtn.innerHTML = MV_IC.snap; }

    paintRec(sec) {
        const b = this.recBtn;
        if (!this.recS) { b.classList.remove('rec'); b.innerHTML = MV_IC.rec; return; }
        b.classList.add('rec');
        b.textContent = Helpers.formatTime(sec || 0);
    }

    shot() {
        const v = this.video, back = () => this.paintShot();
        if (!v.videoWidth) return this.flashBtn(this.shotBtn, 'ERR', back);
        Helpers.snap(v, this.e.name, ok => this.flashBtn(this.shotBtn, ok ? 'OK' : 'ERR', back));
    }

    toggleRec() {
        if (this.recS) { try { this.recS.stop(); } catch {} return; }
        const cfg = recCfg(), base = `${this.e.name || 'REC'}_${Helpers.getTimestamp()}`;
        const t0 = Date.now();
        let timer = 0;
        const begin = ctl => {
            this.recS = ctl;
            this.paintRec(0);
            timer = setInterval(() => this.paintRec(Math.floor((Date.now() - t0) / 1000)), 1000);
        };
        const end = () => { clearInterval(timer); this.recS = null; this.paintRec(0); };
        const fail = t => this.flashBtn(this.recBtn, t || 'ERR', () => this.paintRec(0));
        const capture = () => {
            const v = this.video;
            const st = v.captureStream ? v.captureStream(30) : (v.mozCaptureStream ? v.mozCaptureStream(30) : null);
            if (!st) return fail();
            const mt = recMimeFor(cfg.fmt), ext = mt.includes('mp4') ? 'mp4' : 'webm';
            const mr = newMediaRecorder(st, mt, cfg.q);
            if (!mr) return fail();
            const disk = new HlsKit.DiskBuffer({ meta: { name: base, ext, mime: mt.split(';')[0] } });
            const t1 = performance.now();
            let bytes = 0;
            mr.ondataavailable = e => {
                if (!(e.data && e.data.size > 0)) return;
                disk.append(e.data);
                bytes += e.data.size;
                if (cfg.total && bytes >= cfg.total * 1048576) { try { mr.stop(); } catch {} }
            };
            mr.onerror = () => { try { mr.stop(); } catch {} };
            mr.onstop = () => {
                try { st.getTracks().forEach(t => t.stop()); } catch {}
                if (disk.size > 0) disk.finish().then(b => saveRecording(b, ext, performance.now() - t1, `${base}.${ext}`, disk)).catch(() => {});
                else disk.remove();
                end();
            };
            try { mr.start(1000); } catch { end(); return fail(); }
            begin({ stop: () => mr.stop() });
        };
        const si = this.srcInfo;
        if (cfg.src === 'hls' && si && si.url) {
            let user = false;
            const cap = newHlsCapture({
                url: si.url, cfg, base, fetchBin: (u, r) => mvGet(u, si.ref, r),
                save: (blob, name, sink) => saveRecording(blob, 'mp4', 0, name, sink)
            });
            begin({ stop: () => { user = true; cap.stop(); } });
            cap.start().then(res => {
                end();
                if (res.error && !user && !res.stats.segments) return capture();
                if (res.error && !user) fail();
            });
            return;
        }
        capture();
    }

    say(text, err) {
        this.msg.textContent = text;
        this.msg.classList.toggle('err', !!err);
        this.el.classList.toggle('bad', !!err);
        this.msg.classList.remove('hide');
    }

    setActive(on) {
        this.act = on;
        this.el.classList.toggle('act', on);
        this.applyAudio();
        if (on) { this.showUi(); clearInterval(this.ti); this.ti = setInterval(() => this.tickUi(), 250); } else this.endUi();
    }

    applyAudio() {
        const on = this.act && !this.um && this.video.volume > 0;
        this.video.muted = !this.act || this.um;
        this.sndBtn.innerHTML = this.act && !this.um ? MV_IC.on : MV_IC.off;
        if (this.muteBtn) this.muteBtn.innerHTML = on ? MV_IC.on : MV_IC.off;
        if (this.volEl) this.volEl.style.setProperty('--p', (this.video.volume * 100) + '%');
    }

    endUi() {
        clearInterval(this.ti);
        this.ti = 0;
        this.el.classList.remove('ui');
        this.menu?.classList.remove('open');
        this.drag = false;
    }

    showUi() {
        this.el.classList.add('ui');
        this.uat = Date.now();
    }

    toggleUi() {
        if (this.el.classList.contains('ui')) { this.el.classList.remove('ui'); this.menu.classList.remove('open'); } else this.showUi();
    }

    syncPlay() {
        if (this.playBtn) this.playBtn.innerHTML = this.video.paused ? MV_IC.play : MV_IC.pause;
    }

    range() {
        const b = this.video.buffered;
        return b.length ? [b.start(0), b.end(b.length - 1)] : null;
    }

    seekTo(t) {
        const v = this.video, r = this.range(), p = this.player;
        if (!r) return;
        const to = Math.min(Math.max(t, r[0] + 0.05), r[1] - 0.3);
        if (p) p.shift = r[1] - to > 3;
        try { v.currentTime = to; } catch {}
    }

    goLive() {
        const v = this.video, r = this.range(), p = this.player;
        if (p) { p.shift = false; p.o.hold = false; }
        if (r) { try { v.currentTime = r[1] - 1.5; } catch {} }
        v.play().catch(() => {});
    }

    togglePlay() {
        const v = this.video, p = this.player;
        if (v.paused) { this.userPause = false; this.setWant(true); if (p) p.o.hold = false; if (mvDvr() === 0) this.goLive(); else v.play().catch(() => {}); } else { this.userPause = true; this.setWant(false); if (p) { p.o.hold = true; p.shift = true; } v.pause(); }
    }

    bgResume() {
        const v = this.video, p = this.player;
        if (!mvBg() || this.userPause || this.parked || !p || p.dead || !v.isConnected || v.ended || !v.paused) return;
        if (Store.get('bc_sleep_lock', 0)) return;
        p.o.hold = false;
        if (mvDvr() === 0) this.goLive(); else v.play().catch(() => {});
    }

    isFs() {
        const el = this.el;
        try { return el.matches(':fullscreen'); } catch { try { return el.matches(':-webkit-full-screen'); } catch { return false; } }
    }

    toggleFs() {
        if (this.isFs()) fsExit(); else fsRequest(this.el);
    }

    togglePip() {
        const v = this.video;
        if (document.pictureInPictureElement) { document.exitPictureInPicture().catch(() => {}); return; }
        if (document.pictureInPictureEnabled && v.requestPictureInPicture) v.requestPictureInPicture().catch(() => {});
    }

    buildCtl() {
        const v = this.video;
        const ctl = this.ctl = mk('div', 'mv-ctl');
        const stop = ev => { ev.stopPropagation(); this.uat = Date.now(); };
        ctl.onclick = stop;
        ctl.onpointerdown = ev => { this.uat = Date.now(); ev.stopPropagation(); };
        const sk = this.sk = mk('div', 'mv-sk');
        this.skFill = mk('div', 'mv-sk-f');
        this.skKnob = mk('div', 'mv-sk-k');
        sk.append(mk('div', 'mv-sk-t'), this.skFill, this.skKnob);
        const frac = ev => { const r = sk.getBoundingClientRect(); return Math.min(1, Math.max(0, (ev.clientX - r.left) / (r.width || 1))); };
        const paint = f => { this.skFill.style.width = `${f * 100}%`; this.skKnob.style.left = `${f * 100}%`; };
        sk.onpointerdown = ev => {
            ev.stopPropagation();
            this.drag = true;
            try { sk.setPointerCapture(ev.pointerId); } catch {}
            paint(frac(ev));
        };
        sk.onpointermove = ev => { if (this.drag) paint(frac(ev)); };
        const fin = ev => {
            if (!this.drag) return;
            this.drag = false;
            const r = this.range();
            if (r) this.seekTo(r[0] + frac(ev) * (r[1] - r[0]));
        };
        sk.onpointerup = fin;
        sk.onpointercancel = () => { this.drag = false; };
        this.paintSk = paint;
        const row = mk('div', 'mv-cr');
        const btn = (cls, title, fn) => { const b = mk('button', cls); b.title = title; b.onclick = ev => { ev.stopPropagation(); this.uat = Date.now(); fn(ev); }; return b; };
        this.playBtn = btn('mv-cb', mvT('mv_play'), () => this.togglePlay());
        this.muteBtn = btn('mv-cb', mvT('mv_mute'), () => { this.um = !this.um; if (!this.um && v.volume === 0) { v.volume = 0.5; this.volEl.value = '0.5'; } this.applyAudio(); });
        const vol = this.volEl = mk('input', 'mv-vol');
        vol.type = 'range'; vol.min = '0'; vol.max = '1'; vol.step = '0.05'; vol.value = String(v.volume);
        vol.oninput = () => { v.volume = +vol.value; if (v.volume > 0) this.um = false; Store.set('bc_mv_vol', v.volume); this.applyAudio(); this.uat = Date.now(); };
        vol.onclick = ev => ev.stopPropagation();
        this.lvBtn = btn('mv-lv', mvT('mv_live'), () => this.goLive());
        this.lvBtn.textContent = mvT('mv_live');
        this.tm = mk('span', 'mv-tm');
        const sp = mk('div', 'mv-sp');
        this.qBtn = btn('mv-cb mv-qb', mvT('mv_qual'), () => this.toggleMenu());
        this.pipBtn = btn('mv-cb', mvT('mv_pip'), () => this.togglePip());
        this.pipBtn.innerHTML = MV_IC.pip;
        this.fsBtn = btn('mv-cb', mvT('mv_fs'), () => this.toggleFs());
        this.fsBtn.innerHTML = MV_IC.fs;
        if (!(document.pictureInPictureEnabled && v.requestPictureInPicture)) this.pipBtn.style.display = 'none';
        row.append(this.playBtn, this.muteBtn, vol, this.lvBtn, this.tm, sp, this.qBtn, this.pipBtn, this.fsBtn);
        ctl.append(sk, row);
        const menu = this.menu = mk('div', 'mv-qm');
        menu.onclick = stop;
        menu.onpointerdown = ev => ev.stopPropagation();
        this.stg.append(ctl, menu);
        this.syncPlay();
        this.applyAudio();
        this.applyDvr();
    }

    applyDvr() {
        this.el.classList.toggle('nodvr', mvDvr() === 0);
    }

    toggleMenu() {
        const m = this.menu;
        if (m.classList.contains('open')) { m.classList.remove('open'); return; }
        const st = this.player && this.player.stats;
        if (st && st.variants && st.variants.length) this.vh = st.variants;
        const hs = [...new Set(this.vh.map(x => x.h).filter(Boolean))].sort((a, b) => b - a);
        const rows = [];
        const item = (label, sel, fn) => { const r = mk('div', `mv-qi${sel ? ' sel' : ''}`); r.textContent = label; r.onclick = ev => { ev.stopPropagation(); fn(); }; rows.push(r); };
        item(mvT('mv_auto'), !this.q, () => { this.q = 0; m.classList.remove('open'); this.start(); });
        hs.forEach(h => item(`${h}p`, this.q === h, () => { this.q = h; m.classList.remove('open'); this.start(); }));
        m.replaceChildren(...rows);
        m.classList.add('open');
    }

    paintProgress(r) {
        if (!r || this.drag) return;
        const span = Math.max(r[1] - r[0], 0.001);
        this.paintSk(Math.min(1, Math.max(0, (this.video.currentTime - r[0]) / span)));
    }

    qualityLabel() {
        if (this.q) return `${this.q}p`;
        const h = this.video.videoHeight;
        return h ? `${mvT('mv_auto')} ${h}p` : mvT('mv_auto');
    }

    shouldHideUi() {
        return !this.video.paused && !this.drag && !this.menu.classList.contains('open') && Date.now() - this.uat > 3500;
    }

    tickUi() {
        const v = this.video, el = this.el;
        if (!el.classList.contains('ui')) return;
        const r = this.range();
        this.paintProgress(r);
        this.qBtn.textContent = this.qualityLabel();
        if (this.sw && Date.now() - this.sw < 15000) { if (this.shouldHideUi()) el.classList.remove('ui'); return; }
        const behind = r ? Math.max(0, r[1] - v.currentTime) : 0;
        const live = this.isLv = this.isLv ? behind < 9 : behind < 4;
        this.lvBtn.classList.toggle('on', live && !v.paused);
        this.tm.textContent = live ? '' : `\u2212${fmtDur(behind)}`;
        if (this.shouldHideUi()) el.classList.remove('ui');
    }

    setPoster() {
        const u = this.e.url;
        const b = this.mv.ui.bookmarkManager?.prevEn === false ? null : (AppState.bookmarks || []).find(x => isObj(x) && x.frame && (x.url === u || (typeof bmKey === 'function' && bmKey(x.url) === bmKey(u))));
        this.posterBase = b ? b.frame : '';
        if (b) { this.video.poster = b.frame; this.video.setAttribute('referrerpolicy', 'no-referrer'); } else { this.video.removeAttribute('poster'); if (this.mv.ui.bookmarkManager?.prevEn !== false) this.avatarPoster(); }
    }

    async avatarPoster() {
        const u = this.e.url, k = (this.apTok = (this.apTok || 0) + 1);
        const bm = (AppState.bookmarks || []).find(x => isObj(x) && (x.url === u || bmKey(x.url) === bmKey(u)));
        let h = null; try { h = histLoad().m[bmKey(u)]; } catch (_) {}
        const list = [bm && bm.avatar, h && h.a].map(toHttps).filter(Boolean);
        for (const a of list) {
            const d = await loadFrame(a);
            if (k !== this.apTok || this.posterBase || this.mv.ui.bookmarkManager?.prevEn === false) return;
            if (d) { this.video.poster = d; return; }
        }
        if (k === this.apTok && !this.posterBase) this.video.poster = initialsAva((bm && (bm.name || bm.title)) || (this.e && this.e.name) || u);
    }

    stopPoster() {
        clearTimeout(this.pt);
        this.pt = 0;
        this.pBusy = false;
    }

    canRefreshPoster() { return this.mv.isOpen() && !realHidden() && !this.pBusy; }

    async refreshPoster() {
        this.pBusy = true;
        const d = await loadFrame(frameBust(this.posterBase));
        this.pBusy = false;
        if (this.parked && d) this.video.poster = d;
    }

    prevOk() { return !!prevSpd() && this.mv.ui.bookmarkManager?.prevEn !== false; }

    kickPoster() {
        this.stopPoster();
        if (!this.parked || !this.posterBase || !this.prevOk()) return;
        const run = async () => {
            this.pt = 0;
            if (!this.parked || !this.posterBase || !this.prevOk()) return;
            if (this.canRefreshPoster()) await this.refreshPoster();
            if (this.parked && this.prevOk() && !this.pt) this.pt = setTimeout(run, prevSpd());
        };
        run();
    }

    inPip() { return !!this.pip; }

    pipKeep(on) {
        this.mv.pane?.classList.toggle('keep', on);
        this.mv.ui.bookmarkManager?.overlay?.classList.toggle('pipkeep', on);
    }

    setWant(on) {
        if (this.want === on) return;
        this.want = on;
        this.mv.saveOn();
    }

    idle() {
        this.setWant(false);
        this.setPoster();
        this.stop();
        this.endUi();
        this.parked = true;
        this.say('\u25B6');
        this.msg.classList.add('park');
        this.el.classList.remove('bad');
        this.kickPoster();
    }

    shotUrl(src) {
        if (this.e.t === 'url') return '';
        const site = urlSite(this.e.url);
        if (site === 'cb') { const slug = modelSlug(this.e.url); return slug ? cbRiUrl(slug) : ''; }
        const m = site === 'bc' && src && String(src.url || '').match(/^https:\/\/live-edge([\w-]*)\.bcvcdn\.com\/hls\/(stream_[^/]+)\//i);
        return m ? `https://mobile-edge${m[1]}.bcvcdn.com/${m[2]}.jpg` : '';
    }

    async liveShot(id, src) {
        if (this.mv.ui.bookmarkManager?.prevEn === false) return;
        const u = this.shotUrl(src);
        if (!u) return;
        const d = await loadFrame(frameBust(u));
        if (d && id === this.gen && !this.played) this.video.poster = d;
    }

    async resolveSource() {
        try { return { src: await mvResolve(this.e), err: '' }; }
        catch (x) { return { src: null, err: String((x && x.message) || x).slice(0, 60) }; }
    }

    async start() {
        this.stop();
        const id = ++this.gen;
        this.parked = false;
        this.userPause = false;
        this.sw = Date.now();
        this.lazy = false;
        this.stopPoster();
        this.msg.classList.remove('park');
        if (this.act && !this.ti) { this.showUi(); this.ti = setInterval(() => this.tickUi(), 250); }
        this.say(L().mv_load);
        const vk = this.vkey = mvVaultKey(this.e);
        const tryWarm = !!vk && !this.skipWarm && !mvIsSelf(this.e);
        this.skipWarm = false;
        if (vk && tryWarm) await mvVault.ready();
        if (id !== this.gen) return;
        const warm = tryWarm ? mvVault.peek(vk) : null;
        this.usedWarm = !!(warm && warm.src && warm.src.url);
        this.liveShot(id, this.usedWarm ? warm.src : null);
        const { src, err } = this.usedWarm ? { src: warm.src, err: '' } : await this.resolveSource();
        if (id !== this.gen) return;
        this.srcInfo = src;
        if (!this.usedWarm && src && this.e.t !== 'url' && urlSite(this.e.url) === 'bc') this.liveShot(id, src);
        this.dbg = [src && src.dbg, err].filter(Boolean).join(' ');
        if (!src || !src.url) { this.why = 'E1'; return this.ended(id); }
        const player = this.player = new HlsKit.HlsPlayer({
            video: this.video, url: src.url, maxHeight: this.maxH(), keep: mvKeep(),
            hint: warm && warm.variants ? { variants: warm.variants, audio: warm.audio } : null,
            onOpened: rec => { if (vk && src.url) mvVault.keep(vk, { src: { url: src.url, ref: src.ref }, variants: rec.variants, audio: rec.audio }); },
            fetchBin: (u, r) => mvGet(u, src.ref, r),
            onMuted: () => { this.um = true; this.applyAudio(); },
            onEnd: out => { this.why = `E2 ${mvErr(out && out.error)}`; this.ended(id); }
        });
        player.start().catch(x => { this.why = `E2 ${mvErr(x)}`; this.ended(id); });
        this.abrStart();
        this.wdStart();
    }

    wdStart() {
        clearInterval(this.wd);
        const v = this.video;
        let lt = -1, at = Date.now();
        this.played = false;
        this.wdT0 = Date.now();
        this.wd = setInterval(() => {
            const p = this.player, now = Date.now();
            if (!p || p.dead) { lt = v.currentTime; at = now; return; }
            if (!this.played) {
                if (now - this.wdT0 > 25000) { this.why = 'WD0'; this.start(); }
                return;
            }
            if (v.paused && mvBg()) this.bgResume();
            if (v.paused || p.shift || v.currentTime !== lt) { lt = v.currentTime; at = now; return; }
            if (now - at > 10000) { this.why = 'WD'; this.tries = 0; this.start(); }
        }, 2000);
    }

    abrOn() { return !this.q && mvAbr(); }

    maxH() {
        if (this.q) return this.q;
        if (!mvAbr()) return Infinity;
        const a = [this.abrH || mvAbrLast(), MvNet.cap()].filter(x => x > 0);
        return a.length ? Math.min(...a) : Infinity;
    }

    ladder() {
        const st = this.player && this.player.stats;
        return st && st.variants ? [...new Set(st.variants.map(x => x.h).filter(Boolean))].sort((a, b) => b - a) : [];
    }

    curH() {
        const st = this.player && this.player.stats;
        return st && st.variant ? st.variant.h || 0 : 0;
    }

    abrReady(now) {
        const p = this.player, v = this.video;
        return !!p && !p.dead && p.started && this.abrOn() && !v.paused && !v.seeking && !p.shift && !realHidden() && now - this.abrAt >= 8000 && now - p.tStart >= 6000;
    }

    abrStart() {
        this.abrStop();
        this.abrAt = Date.now();
        this.okSince = 0;
        this.lowN = 0;
        this.waitAt = 0;
        this.stalls = [];
        this.abrTimer = setInterval(() => this.abrTick(), 1000);
    }

    abrStop() {
        clearInterval(this.abrTimer);
        this.abrTimer = 0;
    }

    abrTick() {
        const now = Date.now();
        if (!this.abrReady(now)) { this.okSince = 0; this.lowN = 0; return; }
        if (now - this.abrAt > 300000 && this.abrBack > ABR_BACK0) this.abrBack = ABR_BACK0;
        const v = this.video, ahead = this.player._ahead();
        if (v.readyState < 3 && this.waitAt && now - this.waitAt > 6000) { this.waitAt = now; this.abrStall(true); return; }
        this.lowN = ahead < ABR_LOW ? this.lowN + 1 : 0;
        if (this.lowN >= 3) { this.lowN = 0; if (this.abrStall(false)) return; }
        if (v.readyState < 3 || ahead < ABR_LOW) { this.okSince = 0; return; }
        if (!this.okSince) this.okSince = now;
        if (now - this.okSince >= ABR_UP_MS) this.abrUp(now);
    }

    abrStall(hard) {
        const now = Date.now();
        this.okSince = 0;
        if (!hard && now - this.lastStall < 3000) return false;
        this.lastStall = now;
        this.stalls = this.stalls.filter(t => now - t < 30000);
        this.stalls.push(now);
        if (hard) this.stalls.push(now);
        return this.stalls.length >= 2 ? this.abrDown() : false;
    }

    abrDown() {
        const cur = this.curH(), lower = this.ladder().filter(h => h < cur)[0];
        if (!cur || !lower) return false;
        this.abrBlock[cur] = Date.now() + this.abrBack;
        this.abrBack = Math.min(this.abrBack * 2, 600000);
        this.abrSwitch(lower);
        return true;
    }

    abrUp(now) {
        const cur = this.curH(), up = this.ladder().filter(h => h > cur).pop(), cap = MvNet.cap();
        if (cur && now - this.savedAt > 60000) { this.savedAt = now; Store.set('bc_mv_abr_h', { h: cur, t: now }); }
        if (!cur || !up || (cap && up > cap) || (this.abrBlock[up] || 0) > now) return;
        this.abrSwitch(up);
    }

    abrNet() {
        if (!this.abrOn() || !this.player) return;
        const cap = MvNet.cap(), cur = this.curH(), hs = this.ladder();
        if (!cap || !cur || cur <= cap || hs.length < 2) return;
        const to = hs.filter(h => h <= cap)[0] || hs[hs.length - 1];
        if (to < cur) this.abrSwitch(to);
    }

    abrSwitch(h) {
        this.abrH = h;
        this.stalls = [];
        this.okSince = 0;
        Store.set('bc_mv_abr_h', { h, t: Date.now() });
        this.start();
    }

    abrOff() {
        if (!this.abrH) return;
        this.abrH = 0;
        if (!this.q && this.player) this.start();
    }

    ended(id) {
        if (id !== this.gen) return;
        if (this.usedWarm && !this.played) {
            this.usedWarm = false;
            this.skipWarm = true;
            mvVault.forget(this.vkey);
            return this.start();
        }
        if (++this.tries <= 3 || this.inPip() || (mvBg() && this.want)) {
            this.say(L().mv_load);
            this.rt = setTimeout(() => { if (id === this.gen) this.start(); }, this.tries === 1 ? 300 : Math.min(5000 * (this.tries - 1), 15000));
        } else this.say(`${L().mv_off} · ${L().mv_retry}\n${this.why || 'E0'}${this.dbg ? `\n${this.dbg}` : ''}`, true);
    }

    stop() {
        this.gen++;
        clearTimeout(this.rt);
        clearInterval(this.wd);
        this.abrStop();
        if (this.player) { this.player.stop(); this.player = null; }
    }

    destroy() {
        this.lazy = false;
        this.setWant(false);
        if (this.inPip()) { this.pip = false; this.pipKeep(false); }
        try { if (this.recS) this.recS.stop(); } catch {}
        this.stop();
        this.stopPoster();
        this.endUi();
        clearInterval(this.lblTimer);
        this.el.remove();
    }
}

class MultiView {
    constructor(ui) {
        this.ui = ui;
        this.tiles = [];
        this.act = null;
        this.pane = null;
        this.loaded = false;
        this.dismissed = new Set();
        this._chk = 0;
        onPageEvent('visibilitychange', () => { if (!realHidden()) this.tiles.forEach(t => t.bgResume()); });
        setInterval(() => { if (this.pane && this.autoOn() && this.isOpen() && !realHidden()) this.autoFill(); }, 30000);
    }

    isOpen() {
        const bm = this.ui.bookmarkManager;
        return !!bm && bm.isOpen() && bm.mode === 'mv';
    }

    open() { this.ui.bookmarkManager?.open('mv'); }

    show() {
        if (!this.pane) this.build();
        this.labels();
        if (!this.loaded) {
            const wasOn = Store.get('bc_mv_on', []);
            this.loaded = true;
            this.restoring = true;
            const saved = Store.get('bc_mv', []);
            (Array.isArray(saved) ? saved : []).filter(e => isObj(e) && isStr(e.url) && (e.t === 'bm' || e.t === 'url')).slice(0, MV_MAX).forEach(e => this.add({ t: e.t, name: String(e.name || ''), url: e.url }, true));
            this.restoring = false;
            const was = new Set(Array.isArray(wasOn) ? wasOn : []);
            this.tiles.forEach(t => {
                const on = mvBg() && was.has(this.key(t.e)) && !Store.get('bc_sleep_lock', 0);
                t.idle();
                if (on && t !== this.act) t.lazy = true;
            });
        }
        if (this.autoOn()) {
            this.autoFill();
            const bm = this.ui.bookmarkManager;
            if (bm) bm.checkStatuses().then(() => { if (this.isOpen() && this.autoOn()) this.autoFill(); }).catch(() => {});
        }
        this.layout();
        requestAnimationFrame(() => { this.layout(); this.lazyCheck(); });
        setTimeout(() => this.lazyCheck(), 350);
    }

    paintFlt() {
        if (!this.fltSeg) return;
        const l = L();
        this.fltSeg.replaceChildren(...MV_FLT.map(([key, def, label, tip]) => bmTbtn(
            escHTML(l[label]), mvFltOn(key, def),
            () => {
                Store.set(key, !mvFltOn(key, def));
                this.paintFlt();
                this.layout();
                if (this.addPanel.classList.contains('open')) this.fillAdd();
                if (this.autoOn()) this.autoFill();
                const bm = this.ui.bookmarkManager;
                if (bm) bm.checkStatuses().then(() => { this.layout(); if (this.autoOn()) this.autoFill(); }).catch(() => {});
            },
            l[tip]
        )));
    }

    lazyCheck() {
        if (!this.isOpen() || realHidden()) return;
        const H = window.innerHeight;
        for (const t of this.tiles) {
            if (!t.lazy) continue;
            const r = t.el.getBoundingClientRect();
            if (!r.height) continue;
            const vis = Math.min(r.bottom, H) - Math.max(r.top, 0);
            if (vis >= Math.min(r.height, H) * 0.4) { t.lazy = false; t.start(); }
        }
    }

    dropBmKey(k) {
        const same = e => e && e.t === 'bm' && isStr(e.url) && bmKey(e.url) === k;
        if (!this.loaded) {
            const saved = Store.get('bc_mv', []);
            if (Array.isArray(saved) && saved.some(same)) Store.set('bc_mv', saved.filter(e => !same(e)));
            return;
        }
        this.tiles.filter(t => same(t.e)).forEach(t => this.remove(t));
    }

    autoOn() { return Store.get('bc_mv_auto', true) !== false; }

    mvPass(b) {
        const st = b.status ?? 0;
        if (st === 0 && mvFltOn(MV_FLT[0][0], MV_FLT[0][1])) return false;
        const lk = bmStatusKey(b);
        return !MV_FLT.some(([key, def,,, k]) => k && k === lk && mvFltOn(key, def));
    }

    autoFill() {
        if (!this.pane || !this.autoOn()) return;
        this.layout();
        this.restoring = true;
        const fresh = [];
        try {
            for (const b of (AppState.bookmarks || [])) {
                if (this.tiles.length >= MV_MAX) break;
                if (!isObj(b) || !isStr(b.url) || !isSafeUrl(b.url) || !((b.status ?? 0) > 0) || !this.mvPass(b)) continue;
                const e = { t: 'bm', name: String(b.name), url: b.url };
                if (this.dismissed.has(this.key(e))) continue;
                const t = this.add(e, true);
                if (t) fresh.push(t);
            }
        } finally { this.restoring = false; }
        fresh.forEach(t => t.idle());
        if (fresh.length) this.save();
    }

    setAuto(on) {
        Store.set('bc_mv_auto', !!on);
        if (on) {
            this.dismissed.clear();
            this.autoFill();
            const bm = this.ui.bookmarkManager;
            if (bm) bm.checkStatuses().then(() => { if (this.pane && this.autoOn()) this.autoFill(); }).catch(() => {});
        }
        if (this.pane) this.labels();
    }

    suspend() {
        if (mvBg()) return;
        this.tiles.forEach(t => { if (!t.inPip()) t.idle(); });
    }

    build() {
        const st = document.createElement('style');
        st.textContent = MV_CSS;
        this.ui.shadow.appendChild(st);
        const onScroll = () => {
            if (this._chk) return;
            this._chk = requestAnimationFrame(() => { this._chk = 0; this.lazyCheck(); });
        };
        this.ui.shadow.addEventListener('scroll', onScroll, true);
        window.addEventListener('scroll', onScroll, { passive: true, capture: true });
        const pane = this.pane = mk('div', 'mv-pane');
        const bar = mk('div', 'mv-bar');
        this.btnAdd = mk('button', 'bm-tbtn on');
        this.btnAdd.onclick = () => this.toggleAdd();
        this.btnCols = mk('button', 'bm-hbtn bm-colsbtn');
        this.btnCols.onclick = () => { Store.set(COLS_KEY, colsGet() === 2 ? 1 : 2); this.layout(); };
        this.btnDvr = mk('button', 'bm-tbtn');
        this.btnDvr.onclick = () => this.cycleDvr();
        this.btnPrev = mk('button', 'bm-hbtn bm-prevbtn');
        this.btnPrev.onclick = () => {
            const bm = this.ui.bookmarkManager;
            if (!bm) return;
            bm.prevEn = !bm.prevEn;
            Store.set('bc_previews', bm.prevEn);
            bm.updatePrevUI?.();
            bm.stopPreviews?.();
            this.syncPrev();
        };
        this.btnSpd = mk('button', 'bm-hbtn bm-spd');
        this.btnSpd.style.fontWeight = 'bold';
        this.btnSpd.onclick = () => {
            Store.set('bc_prev_spd', prevSpdNext());
            this.labels();
            this.tiles.forEach(t => t.kickPoster());
            const bm = this.ui.bookmarkManager;
            bm?.updatePrevUI?.();
            bm?.stopPreviews?.();
        };
        this.btnClr = mk('button', 'bm-tbtn mv-clr');
        this.btnClr.innerHTML = MV_IC.close;
        this.btnClr.onclick = () => askConfirm(this.ui, mvT('mv_clear_q'), () => this.clear());
        this.swAbr = mk('button', 'bm-sw');
        this.swAbr.onclick = () => this.toggleAbr();
        this.swAuto = mk('button', 'bm-sw');
        this.swAuto.onclick = () => this.setAuto(!this.autoOn());
        this.swBg = mk('button', 'bm-sw');
        this.swBg.onclick = () => { Store.set('bc_mv_bg', !mvBg()); this.labels(); };
        this.btnGear = mk('button', 'bm-hbtn bm-gear');
        this.btnGear.innerHTML = icons.gear;
        this.setPanel = mk('div', 'mv-setp');
        const setRow = (k, ctl) => { const r = mkSetRow('', ctl); this[k] = r.firstChild; return r; };
        this.setPanel.append(setRow('lblAuto', this.swAuto), setRow('lblBg', this.swBg), setRow('lblDvr', this.btnDvr), setRow('lblAbr', this.swAbr), setRow('lblSpd', this.btnSpd));
        bindGear(this.btnGear, this.setPanel);
        bar.append(this.btnAdd, this.btnPrev, this.btnCols, this.btnGear, this.btnClr);
        MvNet.watch(() => this.tiles.forEach(t => t.abrNet()));
        const stage = this.stage = mk('div', 'mv-stage');
        this.grid = mk('div', 'mv-grid');
        this.empty = mk('div', 'mv-empty');
        this.emptyTxt = mk('div');
        this.emptyBtn = mk('button', 'bm-tbtn on');
        this.emptyBtn.onclick = () => this.toggleAdd(true);
        this.empty.append(this.emptyTxt, this.emptyBtn);
        this.addPanel = mk('div', 'mv-add');
        stage.append(this.grid, this.empty, this.addPanel);
        this.fltSeg = mk('div', 'bm-seg mv-seg');
        pane.append(bar, this.fltSeg, this.setPanel, stage);
        this.ui.bookmarkManager.modal.appendChild(pane);
    }

    labels() {
        this.paintFlt();
        const l = L();
        this.btnAdd.innerHTML = `${MV_IC.add} ${escHTML(l.mv_add)}`;
        this.emptyBtn.innerHTML = `${MV_IC.add} ${escHTML(l.mv_add)}`;
        this.emptyTxt.textContent = l.mv_hint;
        this.btnCols.title = l.st_cols || l.mv_cols;
        this.btnDvr.innerHTML = `${MV_IC.rew}<span>${escHTML(this.dvrLabel())}</span>`;
        this.btnDvr.title = mvT('mv_dvr');
        this.swAbr.classList.toggle('on', mvAbr());
        this.swAuto.classList.toggle('on', this.autoOn());
        this.swBg.classList.toggle('on', mvBg());
        this.swBg.title = mvT('mv_bg_t');
        this.lblAuto.textContent = mvT('mv_auto_all');
        this.lblBg.textContent = mvT('mv_bg');
        this.swAbr.title = mvT('mv_abr_t');
        this.lblDvr.textContent = mvT('mv_dvr');
        this.lblAbr.textContent = mvT('mv_abr_row');
        this.lblSpd.textContent = l.st_prev_spd;
        this.btnGear.title = l.settings;
        this.btnClr.title = l.mv_close;
        this.btnSpd.textContent = prevSpdLabel(prevSpd());
        this.btnSpd.title = prevSpdTitle();
        const pe = this.ui.bookmarkManager?.prevEn !== false;
        this.btnPrev.classList.toggle('on', pe);
        this.btnPrev.innerHTML = pe ? icons.eye : icons.eyeOff;
        this.btnPrev.title = l.st_prev;
        this.btnSpd.disabled = !pe;
        this.btnSpd.style.opacity = pe ? '' : '.4';
    }

    syncPrev() {
        if (!this.pane) return;
        this.labels();
        this.tiles.forEach(t => { t.setPoster(); t.kickPoster(); });
    }

    toggleAbr() {
        const on = !mvAbr();
        Store.set('bc_mv_abr', on);
        if (!on) this.tiles.forEach(t => t.abrOff());
        this.labels();
    }

    dvrLabel() {
        const v = mvDvr();
        return v ? `${v / 60} ${mvT('mv_min_u')}` : mvT('mv_dvr_off');
    }

    cycleDvr() {
        const nx = MV_DVR_OPTS[(MV_DVR_OPTS.indexOf(mvDvr()) + 1) % MV_DVR_OPTS.length];
        Store.set('bc_mv_dvr', nx);
        this.tiles.forEach(t => {
            t.applyDvr();
            if (t.player) t.player.o.keep = mvKeep();
            if (!nx && t.act) t.goLive();
        });
        this.labels();
    }

    tilePass(t) {
        if (t.e.t !== 'bm') return true;
        const b = (AppState.bookmarks || []).find(x => isObj(x) && x.url === t.e.url);
        return !b || this.mvPass(b);
    }

    layout() {
        if (!this.pane) return;
        let shown = 0;
        this.tiles.forEach(t => {
            const ok = this.tilePass(t);
            if (ok) shown++;
            else if (t.el.style.display !== 'none') { try { t.idle(); } catch {} }
            t.el.style.display = ok ? '' : 'none';
        });
        const n = shown;
        const c = Math.min(Math.max(n, 1), colsGet());
        this.btnCols.innerHTML = COLS_ICONS[colsGet() - 1];
        this.grid.style.gridTemplateColumns = `repeat(${c},minmax(0,1fr))`;
        this.grid.classList.toggle('multi', c > 1);
        this.empty.style.display = n ? 'none' : 'flex';
        this.grid.style.display = n ? 'grid' : 'none';
        this.btnClr.style.display = n ? '' : 'none';
    }

    key(e) { return `${e.t}|${e.url}`; }

    add(e, quiet) {
        if (this.tiles.length >= MV_MAX) {
            this.btnAdd.textContent = L().mv_full;
            setTimeout(() => this.labels(), 2000);
            return null;
        }
        if (this.tiles.some(t => this.key(t.e) === this.key(e))) return null;
        const nm = String(e.name).toLowerCase();
        if (this.tiles.some(t => t.e.t !== e.t && String(t.e.name).toLowerCase() === nm)) return null;
        if (!this.restoring) this.dismissed.delete(this.key(e));
        const t = new MvTile(this, e);
        this.tiles.push(t);
        this.grid.append(t.el);
        this.layout();
        if (this.isOpen() && !this.restoring) { if (!this.act) this.select(t); t.start(); }
        if (!quiet) this.save();
        if (this.addPanel.classList.contains('open')) this.fillAdd();
        return t;
    }

    remove(t) {
        this.dismissed.add(this.key(t.e));
        t.destroy();
        this.tiles = this.tiles.filter(x => x !== t);
        if (this.act === t) this.act = null;
        this.layout();
        this.save();
        if (this.addPanel.classList.contains('open')) this.fillAdd();
    }

    clear() {
        this.tiles.forEach(t => this.dismissed.add(this.key(t.e)));
        this.tiles.forEach(t => t.destroy());
        this.tiles = [];
        this.act = null;
        this.layout();
        this.save();
    }

    select(t) {
        const prev = this.act;
        this.act = prev === t ? null : t;
        if (prev) prev.setActive(false);
        if (this.act) this.act.setActive(true);
    }

    save() {
        Store.set('bc_mv', this.tiles.map(t => ({ t: t.e.t, name: t.e.name, url: t.e.url })));
    }

    saveOn() {
        if (!this.loaded || this.restoring || Store.get('bc_sleep_lock', 0)) return;
        Store.set('bc_mv_on', this.tiles.filter(t => t.want).map(t => this.key(t.e)));
    }

    bootResume() {
        const on = Store.get('bc_mv_on', []);
        if (!mvBg() || !Array.isArray(on) || !on.length || Store.get('bc_sleep_lock', 0) || this.loaded) return;
        try { this.show(); } catch {}
    }

    toggleAdd(force) {
        const on = force === undefined ? !this.addPanel.classList.contains('open') : force;
        this.addPanel.classList.toggle('open', on);
        this.stage.classList.toggle('adding', on);
        if (on) this.fillAdd();
    }

    fillAdd() {
        const l = L(), p = this.addPanel, keep = p.querySelector('.mv-f')?.value || '';
        const finish = t => { if (t) this.toggleAdd(false); };
        const sel = new Map();
        const back = mk('button', 'bm-tbtn');
        back.textContent = `← ${l.mv_back}`;
        const hd = mk('div', 'mv-hd');
        const gob = mk('button', 'bm-tbtn on mv-gob');
        gob.style.display = 'none';
        hd.append(back, textEl('', l.mv_add, 'span'), gob);
        const flt = mk('input', 'mv-inp mv-f');
        flt.placeholder = l.mv_search;
        flt.value = keep;
        const list = mk('div', 'mv-list');
        const paintSel = () => {
            list.classList.toggle('selmode', sel.size > 0);
            gob.style.display = sel.size ? '' : 'none';
            gob.textContent = `${l.mv_go} (${sel.size})`;
        };
        back.onclick = () => {
            if (!sel.size) return this.toggleAdd(false);
            sel.clear();
            list.querySelectorAll('.mv-it.sel').forEach(x => x.classList.remove('sel'));
            paintSel();
        };
        const toggleRow = (it, b) => {
            if (sel.has(b.url)) sel.delete(b.url);
            else {
                if (this.tiles.length + sel.size >= MV_MAX) {
                    gob.textContent = l.mv_full;
                    setTimeout(paintSel, 1500);
                    return;
                }
                sel.set(b.url, b);
            }
            it.classList.toggle('sel', sel.has(b.url));
            paintSel();
        };
        gob.onclick = () => {
            const arr = [...sel.values()];
            this.toggleAdd(false);
            for (const b of arr) if (!this.add({ t: 'bm', name: String(b.name), url: b.url }, true)) break;
            this.save();
        };
        const draw = () => {
            const q = flt.value.trim().toLowerCase();
            const have = new Set(this.tiles.map(t => this.key(t.e)));
            const rows = [];
            const cur = getStreamUrl();
            const curName = String(currentSite.getName() || '');
            if (cur && !q && !(curName && this.tiles.some(t => String(t.e.name).toLowerCase() === curName.toLowerCase()))) {
                const it = mk('div', 'mv-it cur');
                it.append(textEl('', `${l.mv_cur}: ${curName}`, 'b'));
                it.onclick = () => finish(this.add(curName && isSafeUrl(location.origin + location.pathname)
                    ? { t: 'bm', name: curName, url: location.origin + location.pathname }
                    : { t: 'url', name: curName || 'LIVE', url: cur }));
                rows.push(it);
            }
            const items = (AppState.bookmarks || []).filter(b => isObj(b) && this.mvPass(b) && (!q || String(b.name).toLowerCase().includes(q))).sort((a, b) => bmViews(b) - bmViews(a));
            items.forEach(b => {
                const it = mk('div', `mv-it${have.has(this.key({ t: 'bm', url: b.url })) ? ' dis' : ''}${sel.has(b.url) ? ' sel' : ''}`);
                const im = document.createElement('img');
                im.referrerPolicy = 'no-referrer';
                im._bRef = b;
                im.onerror = () => { if (!tryGm(im, b.frame, () => frameFail(im))) frameFail(im); };
                if (b.frame) im.src = b.frame;
                else setTimeout(() => frameFail(im), 0);
                it.append(mk('span', 'mv-ck'), im, textEl('', b.name, 'b'), textEl('', `${urlSite(b.url).toUpperCase()}${bmViews(b) ? ` · ${fmtCount(bmViews(b))}` : ''}`, 'i'));
                let timer = 0, sx = 0, sy = 0;
                const stop = () => { clearTimeout(timer); timer = 0; };
                it.onpointerdown = ev => {
                    if (ev.pointerType === 'mouse' && ev.button !== 0) return;
                    it._lp = false;
                    sx = ev.clientX; sy = ev.clientY;
                    stop();
                    timer = setTimeout(() => {
                        timer = 0;
                        it._lp = true;
                        try { if (navigator.vibrate) navigator.vibrate(15); } catch {}
                        toggleRow(it, b);
                    }, 450);
                };
                it.onpointermove = ev => { if (timer && Math.hypot(ev.clientX - sx, ev.clientY - sy) > 10) stop(); };
                it.onpointerup = stop;
                it.onpointercancel = stop;
                it.onpointerleave = stop;
                it.oncontextmenu = ev => ev.preventDefault();
                it.onclick = () => {
                    if (it._lp) { it._lp = false; return; }
                    if (sel.size) return toggleRow(it, b);
                    finish(this.add({ t: 'bm', name: String(b.name), url: b.url }));
                };
                rows.push(it);
            });
            list.replaceChildren(...(rows.length ? rows : [textEl('', l.mv_none)]));
            paintSel();
        };
        flt.oninput = draw;
        draw();
        const inp = mk('input', 'mv-inp');
        inp.placeholder = l.mv_url;
        const go = mk('button', 'bm-tbtn on');
        go.textContent = l.mv_go;
        const submit = () => {
            const u = inp.value.trim();
            if (!/^https?:\/\/\S+$/i.test(u)) return;
            let host = '';
            try { host = new URL(u).hostname; } catch {}
            finish(this.add({ t: 'url', name: host, url: u }));
        };
        go.onclick = submit;
        inp.onkeydown = ev => { if (ev.key === 'Enter') submit(); };
        const r2 = mk('div', 'mv-r2');
        r2.append(inp, go);
        p.replaceChildren(hd, textEl('mv-sec', l.mv_bm_h), flt, list, textEl('mv-sec', l.mv_link_h), r2);
    }
}

const STATUS_RULES = [
    [/"is(?:Full)?PrivateChat"\s*:\s*true/i, 3, 'st_private'],
    [/"isGroupChat"\s*:\s*true/i, 3, 'st_group'],
    [/"isVipShow"\s*:\s*true/i, 3, 'st_vip'],
    [/"isAway"\s*:\s*true/i, 2, 'st_away'],
    [/"showType"\s*:\s*"(?:private|full_?private|voyeur)"/i, 3, 'st_private'],
    [/"showType"\s*:\s*"group"/i, 3, 'st_group'],
    [/"showType"\s*:\s*"vip"/i, 3, 'st_vip'],
    [/__page_profile|"isOnline"\s*:\s*false|"online"\s*:\s*false|"isOffline"\s*:\s*true|"room_status"\s*:\s*["']offline["']|"isChatShowPaused"\s*:\s*true/i, 0],
    [/"isOnline"\s*:\s*true|"online"\s*:\s*true|"showType"\s*:\s*"public"|"isOffline"\s*:\s*false|"room_status"\s*:\s*["'](?:public|free|chat|group|private)["']/i, 1]
];
const STATUS_RULES_SITE = {
    sc: [
        [/"status"\s*:\s*"(?:private|p2p|p2pVoice|virtualPrivate|spy)"/i, 3, 'st_private'],
        [/"isInPrivate"\s*:\s*true|"isInP2p"\s*:\s*true/i, 3, 'st_private'],
        [/"status"\s*:\s*"(?:group|groupShow|ticketShow)"|"isInGroupShow"\s*:\s*true/i, 3, 'st_group'],
        [/"status"\s*:\s*"public"|"isLive"\s*:\s*true|"isBroadcasting"\s*:\s*true/i, 1],
        [/"status"\s*:\s*"(?:off|offline|idle)"|"isLive"\s*:\s*false|"isOnline"\s*:\s*false/i, 0]
    ],
    cb: [
        [/(?:"|\\u0022)room_status(?:"|\\u0022)\s*:\s*(?:"|\\u0022)(?:private|hidden|password protected)/i, 3, 'st_private'],
        [/(?:"|\\u0022)room_status(?:"|\\u0022)\s*:\s*(?:"|\\u0022)group/i, 3, 'st_group'],
        [/(?:"|\\u0022)room_status(?:"|\\u0022)\s*:\s*(?:"|\\u0022)away/i, 2, 'st_away'],
        [/(?:"|\\u0022)room_status(?:"|\\u0022)\s*:\s*(?:"|\\u0022)public/i, 1],
        [/(?:"|\\u0022)room_status(?:"|\\u0022)\s*:\s*(?:"|\\u0022)offline/i, 0],
        [/(?:"|\\u0022)hls_source(?:"|\\u0022)\s*:\s*(?:"|\\u0022)https?:/i, 1]
    ]
};
const SC_JSON_STATUS = [
    [/^(?:private|p2p|p2pVoice|virtualPrivate|spy)$/i, 3, 'st_private'],
    [/^(?:group|groupShow|ticketShow)$/i, 3, 'st_group']
];
const scJsonUser = h => {
    try {
        const u = JSON.parse(h)?.user?.user;
        return u && typeof u.status === 'string' ? u : null;
    } catch { return null; }
};
const scJsonStatus = h => {
    if (!h || h.charCodeAt(0) !== 123) return null;
    const u = scJsonUser(h);
    if (!u) return null;
    const hit = SC_JSON_STATUS.find(([re]) => re.test(u.status));
    if (hit) return [hit[1], hit[2]];
    return /^public$/i.test(u.status) && u.isLive !== false ? [1, ''] : [0, ''];
};
const parseStatus = (h, site) => {
    if (site === 'sc') { const j = scJsonStatus(h); if (j) return j; }
    const rule = h && (STATUS_RULES_SITE[site] || STATUS_RULES).find(([re]) => re.test(h));
    if (!rule) return [0, '', false];
    const [, st, lk] = rule;
    return [st, lk || '', true];
};

const decodeJson = s => { try { return JSON.parse(`"${s}"`); } catch { return s; } };
const jsonField = (h, re) => { const m = h.match(re); return m?.[1] ? decodeJson(m[1]) : ''; };
const TOPIC_RE = /"currentTopic"\s*:\s*"((?:[^"\\]|\\.)*)"/i;
const FRAME_RE = /"frame"\s*:\s*"((?:[^"\\]|\\.)*)"/i;
const htmlDec = x => { try { return new DOMParser().parseFromString(x, 'text/html').documentElement.textContent || ''; } catch { return x; } };
const metaContent = (h, prop) => {
    const m = h.match(new RegExp(`(?:property|name)=["']${prop}["'][^>]*?content=["']([^"']*)["']`, 'i')) || h.match(new RegExp(`content=["']([^"']*)["'][^>]*?(?:property|name)=["']${prop}["']`, 'i'));
    return m ? htmlDec(m[1]) : '';
};
const intField = (h, re) => { const m = h.match(re); return m ? +m[1] : null; };
const siteProfile = {
    cb: (h, status, slug) => {
        if (!status) return { topic: '', frame: '' };
        const d = cbDossier(h), dv = d && [d.num_users, d.num_viewers, d.viewer_count, d.viewers_count].find(x => Number.isFinite(+x) && x !== null && x !== '');
        const v = dv != null ? +dv : intField(h, /(?:"|\\u0022)(?:num_users|num_viewers|viewer_count|viewers_count)(?:"|\\u0022)\s*:\s*(\d+)/i);
        const dt = d && [d.room_title, d.room_subject, d.subject].find(x => isStr(x) && x.trim());
        return {
            topic: String(dt || jsonField(h, /"(?:room_title|room_subject|subject)"\s*:\s*"((?:[^"\\]|\\.)*)"/i) || metaContent(h, 'og:description')).trim(),
            frame: slug ? cbFrameUrl(slug) : '',
            ...(v != null ? { viewers: v } : {})
        };
    },
    sc: (h, status) => {
        if (!status) return { topic: '', frame: '' };
        const v = intField(h, /"(?:viewersCount|usersCount|viewers)"\s*:\s*(\d+)/i);
        const fr = (jsonField(h, /"(?:previewUrlThumbBig|previewUrlThumbSmall|previewUrl)"\s*:\s*"((?:[^"\\]|\\.)*)"/i) || metaContent(h, 'og:image')).replace(/^\/\//, 'https://');
        return {
            topic: (jsonField(h, /"(?:topic|goalMessage)"\s*:\s*"((?:[^"\\]|\\.)*)"/i) || metaContent(h, 'og:description')).trim(),
            frame: fr.startsWith('https://') ? fr : '',
            ...(v != null ? { viewers: v } : {})
        };
    }
};
const stripTags = s => String(s).replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\s+/g, ' ').trim();
const ptIcoUrl = p => (isStr(p) && /^[a-z0-9_\/-]{1,60}$/i.test(p) ? `https://i.bgicdn.com/images/tags/${p}/18/2x/img.png` : '');
const ptIcoMap = () => {
    const m = new Map();
    for (const b of AppState.bookmarks) {
        if (!isObj(b) || !Array.isArray(b.ptags) || !Array.isArray(b.ptic)) continue;
        b.ptags.forEach((x, i) => { const u = ptIcoUrl(b.ptic[i]), k = String(x).toLowerCase(); if (u && !m.has(k)) m.set(k, u); });
    }
    return m;
};
const ptImg = (u, cls) => (u ? `<img class="${cls}" src="${escHTML(u)}" alt="" decoding="sync" referrerpolicy="no-referrer">` : '');
const pilgSection = (h, cls) => {
    const i = h.indexOf(cls);
    if (i < 0) return '';
    const j = h.indexOf('pilg_section', i + cls.length);
    return h.slice(i, j < 0 ? i + 12000 : j);
};
const parsePilgTags = ts => {
    const tags = [], icos = [];
    const add = raw => {
        const x = stripTags(raw);
        if (x && x.length <= 40 && tags.length < 30 && !tags.includes(x)) {
            tags.push(x);
            const im = String(raw).match(/images\/tags\/([a-z0-9_\/-]{1,60}?)\/\d+\/(?:\d+x\/)?img\./i);
            icos.push(im ? im[1] : '');
        }
    };
    for (const m of ts.matchAll(/<a\b[^>]*\/tags\/[^"'\/?#>\s]+[^>]*>([\s\S]*?)<\/a>/gi)) add(m[1]);
    if (!tags.length) for (const m of ts.matchAll(/class=["']stl_item["'][^>]*>([\s\S]*?)<\/div>/gi)) add(m[1]);
    return tags.length ? { ptags: tags, ptic: icos.some(Boolean) ? icos : [] } : null;
};
const parsePilgRow = row => {
    const rng = [];
    for (const x of row.matchAll(/pilgs_badge[^>]*>([^<]*)</g)) {
        const m = x[1].match(/(\d{1,2}):(\d{2})\s*[-\u2013\u2014]\s*(\d{1,2}):(\d{2})/);
        if (m && !(+m[1] === +m[3] && m[2] === m[4])) rng.push(`${m[1].padStart(2, '0')}:${m[2]}-${m[3].padStart(2, '0')}:${m[4]}`);
    }
    return rng.join(',');
};
const parsePilgSchedule = ss => {
    let hi = -1;
    const days = ss.split('pilg_shelf').slice(1).map((row, i) => {
        if (hi < 0 && row.indexOf('__highlight') >= 0) hi = i;
        return parsePilgRow(row);
    });
    if (days.length !== 7) return null;
    const sh = hi < 0 ? 0 : (((new Date().getDay() + 6) % 7) - hi + 7) % 7;
    return days.map((_, i) => days[(i - sh + 7) % 7]);
};
const bcProfileUrl = b => {
    const slug = modelSlug(b.url);
    if (urlSite(b.url) !== 'bc' || !slug) return '';
    try { return `${new URL(b.url).origin}/profile/${encodeURIComponent(slug)}/`; } catch { return ''; }
};
const tagText = item => {
    if (isStr(item)) return item;
    return item && typeof item === 'object' ? (item.name || item.title || item.tag || item.value || '') : '';
};
const collectProfileTags = raw => {
    const tags = [], icos = [];
    for (const item of raw) {
        const tag = stripTags(String(tagText(item))).trim();
        if (!tag || tag.length > 40 || tags.includes(tag) || tags.length >= 30) continue;
        tags.push(tag);
        icos.push(item && typeof item === 'object' && isStr(item.icon) ? item.icon : '');
    }
    return { tags, icos };
};
const parseBcJsonTags = h => {
    const re = /"(?:modelTags|profileTags|tags)"\s*:\s*(\[(?:[^\[\]]|\[[^\]]*\])*\])/ig;
    for (const m of h.matchAll(re)) {
        try {
            const raw = JSON.parse(m[1]);
            if (!Array.isArray(raw)) continue;
            const { tags, icos } = collectProfileTags(raw);
            if (tags.length) return { ptags: tags, ...(icos.some(Boolean) ? { ptic: icos } : {}) };
        } catch {}
    }
    return null;
};
const isWeekSchedule = a => Array.isArray(a) && a.length === 7 && a.every(x => isStr(x) || x == null);
const scheduleDays = a => a.map(x => {
    const hits = String(x || '').match(/\d{1,2}:\d{2}\s*[-–—]\s*\d{1,2}:\d{2}/g) || [];
    return hits.map(v => v.replace(/\s+/g, '').replace(/[–—]/g, '-')).join(',');
});
const parseBcJsonSchedule = h => {
    const re = /"(?:modelSchedule|schedule)"\s*:\s*(\[(?:[^\[\]]|\[[^\]]*\])*\])/ig;
    for (const m of h.matchAll(re)) {
        try {
            const a = JSON.parse(m[1]);
            if (!isWeekSchedule(a)) continue;
            const days = scheduleDays(a);
            if (days.some(Boolean)) return days;
        } catch {}
    }
    return null;
};
const pilgPart = (h, name) => (h.indexOf('pilg_section') >= 0 ? pilgSection(h, name) : '');
const bcViewers = h => {
    const m = h && h.match(/class=["'][^"']*\boc_count\b[^"']*["'][^>]*>\s*(\d[\d\s,.\u00a0]*)\s*</i);
    const n = m ? parseInt(m[1].replace(/\D/g, ''), 10) : NaN;
    return Number.isFinite(n) && n < 10000000 ? n : null;
};
const splitBcd = (h, site) => {
    if (site !== 'bc' || !h) return [h, null];
    const i = h.lastIndexOf(BCD_MARK);
    if (i < 0) return [h, null];
    let bcd = null;
    try { bcd = JSON.parse(h.slice(i + BCD_MARK.length)); } catch {}
    return [h.slice(0, i), bcd];
};
const BC_FACT_SCOPES = ['chatShowStatusOptions', 'performerData', 'chatHost'];
const BC_SCHEMA = {
    facts: {
        off: { keys: ['isOffline'], scopes: BC_FACT_SCOPES },
        on: { keys: ['isOnline', 'online'], scopes: ['performerData', 'chatHost'] },
        priv: { keys: ['isPrivateChat', 'isFullPrivateChat', 'isVoyeurChat', 'isInPrivate'], scopes: BC_FACT_SCOPES },
        grp: { keys: ['isGroupChat'], scopes: BC_FACT_SCOPES },
        vip: { keys: ['isVipShow'], scopes: BC_FACT_SCOPES },
        away: { keys: ['isAway'], scopes: ['performerData', 'chatHost', 'chatShowStatusOptions'] }
    },
    showType: { keys: ['showType', 'room_status', 'roomStatus'], scopes: ['chatHost', 'performerData'] },
    topic: { keys: ['currentTopic', 'chatTopic', 'roomTopic'], scopes: ['chatTopicOptions'] },
    frame: { keys: ['frame', 'frameUrl', 'previewFrame', 'previewUrl', 'livePreviewUrl'], scopes: ['initialVideoPoster', 'chatHost', 'performerData'] },
    viewers: { keys: ['viewers', 'viewersCount', 'viewerCount', 'usersCount'], scopes: ['chatHost', 'performerData'] },
    tags: { keys: ['modelTags', 'profileTags'], scopes: ['performerData', 'chatHost'] },
    schedule: { keys: ['modelSchedule', 'schedule'], scopes: ['performerData', 'chatHost'] }
};
const PD_WARN_RATIO = 0.3, PD_WARN_MIN = 5, PD_WINDOW = 600000, PD_STALE_MS = 1800000;

const PD_T = {
    RU: {
        title: "Отчёт парсера",
        copy: "Копировать",
        copied: "Скопировано",
        reset: "Сбросить",
        nodata: "Данных пока нет: отчёт заполняется по мере обновления закладок.",
        ok: "Всё в порядке: без статуса {u} из {n}.",
        warn: "У {u} из {n} моделей статус не определён (порог {p}%). Скорее всего, сайт изменил ответ. Нажмите «{copy}» и пришлите JSON, чтобы поправить таблицу ключей BC_SCHEMA.",
        age: "возраст",
        unknown: "неизвестно",
        diag: "Диагностика",
        nostatus: "без статуса",
        hint: "Цвета: зелёный - API, синий - JSON страницы, серый - HTML, оранжевый - запасной regex (основной путь не сработал), фиолетовый - listing. «—» оранжевым: у онлайн-модели поле пустое.",
        toast: "Парсер {site}: у {u} из {n} моделей не определился статус. Возможно, сайт изменил разметку. Нажмите, чтобы открыть отчёт.",
    },
    EN: {
        title: "Parser report",
        copy: "Copy",
        copied: "Copied",
        reset: "Reset",
        nodata: "No data yet: the report fills in as bookmarks refresh.",
        ok: "All good: {u} of {n} without status.",
        warn: "Status is unknown for {u} of {n} models (threshold {p}%). The site likely changed its response. Press \"{copy}\" and send the JSON to adjust the BC_SCHEMA key table.",
        age: "age",
        unknown: "unknown",
        diag: "Diagnostics",
        nostatus: "no status",
        hint: "Colors: green - API, blue - page JSON, grey - HTML, orange - fallback regex (primary path failed), purple - listing. Orange \"—\": field empty for an online model.",
        toast: "{site} parser: status is unknown for {u} of {n} models. The site markup may have changed. Click to open the report.",
    },
    UK: {
        title: "Звіт парсера",
        copy: "Копіювати",
        copied: "Скопійовано",
        reset: "Скинути",
        nodata: "Даних поки немає: звіт заповнюється в міру оновлення закладок.",
        ok: "Усе гаразд: без статусу {u} із {n}.",
        warn: "У {u} із {n} моделей статус не визначено (поріг {p}%). Найімовірніше, сайт змінив відповідь. Натисніть «{copy}» і надішліть JSON, щоб виправити таблицю ключів BC_SCHEMA.",
        age: "вік",
        unknown: "невідомо",
        diag: "Діагностика",
        nostatus: "без статусу",
        hint: "Кольори: зелений - API, синій - JSON сторінки, сірий - HTML, помаранчевий - запасний regex (основний шлях не спрацював), фіолетовий - listing. «—» помаранчевим: в онлайн-моделі поле порожнє.",
        toast: "Парсер {site}: у {u} із {n} моделей не визначився статус. Можливо, сайт змінив розмітку. Натисніть, щоб відкрити звіт.",
    },
    ES: {
        title: "Informe del analizador",
        copy: "Copiar",
        copied: "Copiado",
        reset: "Restablecer",
        nodata: "Aún no hay datos: el informe se rellena a medida que se actualizan los favoritos.",
        ok: "Todo en orden: {u} de {n} sin estado.",
        warn: "El estado es desconocido en {u} de {n} modelos (umbral {p}%). Probablemente el sitio cambió su respuesta. Pulsa «{copy}» y envía el JSON para ajustar la tabla de claves BC_SCHEMA.",
        age: "antigüedad",
        unknown: "desconocido",
        diag: "Diagnóstico",
        nostatus: "sin estado",
        hint: "Colores: verde - API, azul - JSON de la página, gris - HTML, naranja - regex de respaldo (falló la vía principal), morado - listado. «—» en naranja: campo vacío en un modelo en línea.",
        toast: "Analizador {site}: estado desconocido en {u} de {n} modelos. Puede que el sitio haya cambiado su estructura. Haz clic para abrir el informe.",
    },
    DE: {
        title: "Parser-Bericht",
        copy: "Kopieren",
        copied: "Kopiert",
        reset: "Zurücksetzen",
        nodata: "Noch keine Daten: Der Bericht füllt sich beim Aktualisieren der Favoriten.",
        ok: "Alles in Ordnung: {u} von {n} ohne Status.",
        warn: "Bei {u} von {n} Models ist der Status unbekannt (Schwelle {p} %). Wahrscheinlich hat die Seite ihre Antwort geändert. Klicke auf „{copy}“ und sende das JSON, um die Schlüsseltabelle BC_SCHEMA anzupassen.",
        age: "Alter",
        unknown: "unbekannt",
        diag: "Diagnose",
        nostatus: "ohne Status",
        hint: "Farben: grün - API, blau - Seiten-JSON, grau - HTML, orange - Ersatz-Regex (Hauptweg fehlgeschlagen), violett - Liste. Oranges „—“: Feld bei einem Online-Model leer.",
        toast: "{site}-Parser: Bei {u} von {n} Models ist der Status unbekannt. Möglicherweise hat sich das Seitenlayout geändert. Klicken, um den Bericht zu öffnen.",
    },
    FR: {
        title: "Rapport de l’analyseur",
        copy: "Copier",
        copied: "Copié",
        reset: "Réinitialiser",
        nodata: "Pas encore de données : le rapport se remplit à mesure que les favoris sont actualisés.",
        ok: "Tout va bien : {u} sur {n} sans statut.",
        warn: "Le statut est inconnu pour {u} modèles sur {n} (seuil {p} %). Le site a probablement modifié sa réponse. Cliquez sur « {copy} » et envoyez le JSON pour ajuster la table de clés BC_SCHEMA.",
        age: "âge",
        unknown: "inconnu",
        diag: "Diagnostic",
        nostatus: "sans statut",
        hint: "Couleurs : vert - API, bleu - JSON de la page, gris - HTML, orange - regex de secours (voie principale en échec), violet - liste. « — » orange : champ vide pour un modèle en ligne.",
        toast: "Analyseur {site} : statut inconnu pour {u} modèles sur {n}. La structure du site a peut-être changé. Cliquez pour ouvrir le rapport.",
    },
    PT: {
        title: "Relatório do analisador",
        copy: "Copiar",
        copied: "Copiado",
        reset: "Redefinir",
        nodata: "Ainda sem dados: o relatório é preenchido conforme os favoritos são atualizados.",
        ok: "Tudo certo: {u} de {n} sem status.",
        warn: "O status é desconhecido em {u} de {n} modelos (limite {p}%). Provavelmente o site mudou a resposta. Clique em “{copy}” e envie o JSON para ajustar a tabela de chaves BC_SCHEMA.",
        age: "idade",
        unknown: "desconhecido",
        diag: "Diagnóstico",
        nostatus: "sem status",
        hint: "Cores: verde - API, azul - JSON da página, cinza - HTML, laranja - regex de reserva (o caminho principal falhou), roxo - listagem. “—” laranja: campo vazio em modelo online.",
        toast: "Analisador {site}: status desconhecido em {u} de {n} modelos. A estrutura do site pode ter mudado. Clique para abrir o relatório.",
    },
    IT: {
        title: "Report del parser",
        copy: "Copia",
        copied: "Copiato",
        reset: "Reimposta",
        nodata: "Ancora nessun dato: il report si compila man mano che i preferiti si aggiornano.",
        ok: "Tutto a posto: {u} su {n} senza stato.",
        warn: "Lo stato è sconosciuto per {u} profili su {n} (soglia {p}%). Probabilmente il sito ha cambiato la risposta. Premi «{copy}» e invia il JSON per correggere la tabella di chiavi BC_SCHEMA.",
        age: "età",
        unknown: "sconosciuto",
        diag: "Diagnostica",
        nostatus: "senza stato",
        hint: "Colori: verde - API, blu - JSON della pagina, grigio - HTML, arancione - regex di riserva (il percorso principale è fallito), viola - elenco. «—» arancione: campo vuoto per un profilo online.",
        toast: "Parser {site}: stato sconosciuto per {u} profili su {n}. Il sito potrebbe aver cambiato struttura. Clicca per aprire il report.",
    },
    PL: {
        title: "Raport parsera",
        copy: "Kopiuj",
        copied: "Skopiowano",
        reset: "Resetuj",
        nodata: "Brak danych: raport wypełnia się w miarę odświeżania zakładek.",
        ok: "Wszystko w porządku: {u} z {n} bez statusu.",
        warn: "Status jest nieznany dla {u} z {n} profili (próg {p}%). Prawdopodobnie serwis zmienił odpowiedź. Kliknij „{copy}” i wyślij JSON, aby poprawić tabelę kluczy BC_SCHEMA.",
        age: "wiek",
        unknown: "nieznany",
        diag: "Diagnostyka",
        nostatus: "bez statusu",
        hint: "Kolory: zielony - API, niebieski - JSON strony, szary - HTML, pomarańczowy - zapasowy regex (główna ścieżka zawiodła), fioletowy - lista. Pomarańczowe „—”: puste pole dla profilu online.",
        toast: "Parser {site}: status nieznany dla {u} z {n} profili. Układ strony mógł się zmienić. Kliknij, aby otworzyć raport.",
    },
    TR: {
        title: "Ayrıştırıcı raporu",
        copy: "Kopyala",
        copied: "Kopyalandı",
        reset: "Sıfırla",
        nodata: "Henüz veri yok: rapor, yer imleri yenilendikçe dolar.",
        ok: "Her şey yolunda: {n} modelden {u} tanesinin durumu yok.",
        warn: "{n} modelden {u} tanesinin durumu bilinmiyor (eşik %{p}). Site büyük olasılıkla yanıtını değiştirdi. “{copy}” düğmesine basıp JSON’u gönderin; BC_SCHEMA anahtar tablosu düzeltilsin.",
        age: "yaş",
        unknown: "bilinmiyor",
        diag: "Tanılama",
        nostatus: "durum yok",
        hint: "Renkler: yeşil - API, mavi - sayfa JSON’u, gri - HTML, turuncu - yedek regex (ana yol başarısız), mor - liste. Turuncu “—”: çevrimiçi modelde alan boş.",
        toast: "{site} ayrıştırıcı: {n} modelden {u} tanesinin durumu bilinmiyor. Sitenin yapısı değişmiş olabilir. Raporu açmak için tıklayın.",
    },
    NL: {
        title: "Parserrapport",
        copy: "Kopiëren",
        copied: "Gekopieerd",
        reset: "Resetten",
        nodata: "Nog geen gegevens: het rapport vult zich naarmate favorieten worden vernieuwd.",
        ok: "Alles in orde: {u} van {n} zonder status.",
        warn: "De status is onbekend bij {u} van {n} modellen (drempel {p}%). De site heeft waarschijnlijk zijn respons gewijzigd. Klik op “{copy}” en stuur de JSON om de BC_SCHEMA-sleuteltabel aan te passen.",
        age: "leeftijd",
        unknown: "onbekend",
        diag: "Diagnostiek",
        nostatus: "geen status",
        hint: "Kleuren: groen - API, blauw - pagina-JSON, grijs - HTML, oranje - reserve-regex (hoofdroute mislukt), paars - lijst. Oranje “—”: veld leeg bij een online model.",
        toast: "{site}-parser: status onbekend bij {u} van {n} modellen. De opmaak van de site is mogelijk gewijzigd. Klik om het rapport te openen.",
    },
    CS: {
        title: "Zpráva parseru",
        copy: "Kopírovat",
        copied: "Zkopírováno",
        reset: "Obnovit",
        nodata: "Zatím žádná data: zpráva se plní s aktualizací záložek.",
        ok: "Vše v pořádku: {u} z {n} bez stavu.",
        warn: "U {u} z {n} modelů není stav známý (práh {p} %). Web pravděpodobně změnil odpověď. Klikněte na „{copy}“ a pošlete JSON, aby šla upravit tabulka klíčů BC_SCHEMA.",
        age: "stáří",
        unknown: "neznámý",
        diag: "Diagnostika",
        nostatus: "bez stavu",
        hint: "Barvy: zelená - API, modrá - JSON stránky, šedá - HTML, oranžová - záložní regex (hlavní cesta selhala), fialová - výpis. Oranžová „—“: prázdné pole u online modelu.",
        toast: "Parser {site}: u {u} z {n} modelů není stav známý. Web možná změnil strukturu. Kliknutím otevřete zprávu.",
    },
    RO: {
        title: "Raport parser",
        copy: "Copiază",
        copied: "Copiat",
        reset: "Resetează",
        nodata: "Încă nu există date: raportul se completează pe măsură ce favoritele se actualizează.",
        ok: "Totul e în regulă: {u} din {n} fără stare.",
        warn: "Starea este necunoscută pentru {u} din {n} modele (prag {p}%). Probabil site-ul și-a schimbat răspunsul. Apasă „{copy}” și trimite JSON-ul pentru a ajusta tabelul de chei BC_SCHEMA.",
        age: "vârstă",
        unknown: "necunoscut",
        diag: "Diagnostic",
        nostatus: "fără stare",
        hint: "Culori: verde - API, albastru - JSON-ul paginii, gri - HTML, portocaliu - regex de rezervă (calea principală a eșuat), violet - listă. „—” portocaliu: câmp gol la un model online.",
        toast: "Parser {site}: stare necunoscută pentru {u} din {n} modele. Structura site-ului s-ar putea să se fi schimbat. Dă clic pentru a deschide raportul.",
    },
    HU: {
        title: "Elemző jelentés",
        copy: "Másolás",
        copied: "Másolva",
        reset: "Visszaállítás",
        nodata: "Még nincs adat: a jelentés a könyvjelzők frissítésével töltődik.",
        ok: "Minden rendben: {n} modellből {u} állapot nélküli.",
        warn: "{n} modellből {u} esetében ismeretlen az állapot (küszöb: {p}%). A webhely valószínűleg megváltoztatta a válaszát. Kattints a „{copy}” gombra, és küldd el a JSON-t a BC_SCHEMA kulcstábla javításához.",
        age: "kor",
        unknown: "ismeretlen",
        diag: "Diagnosztika",
        nostatus: "állapot nélkül",
        hint: "Színek: zöld - API, kék - az oldal JSON-ja, szürke - HTML, narancs - tartalék regex (a fő út nem sikerült), lila - lista. Narancssárga „—”: üres mező egy online modellnél.",
        toast: "{site} elemző: {n} modellből {u} esetében ismeretlen az állapot. Lehet, hogy megváltozott az oldal szerkezete. Kattints a jelentés megnyitásához.",
    },
    BG: {
        title: "Отчет на парсера",
        copy: "Копирай",
        copied: "Копирано",
        reset: "Нулирай",
        nodata: "Все още няма данни: отчетът се попълва при обновяване на любимите.",
        ok: "Всичко е наред: {u} от {n} без статус.",
        warn: "Статусът е неизвестен за {u} от {n} модела (праг {p}%). Най-вероятно сайтът е променил отговора си. Натиснете „{copy}“ и изпратете JSON, за да се поправи таблицата с ключове BC_SCHEMA.",
        age: "възраст",
        unknown: "неизвестно",
        diag: "Диагностика",
        nostatus: "без статус",
        hint: "Цветове: зелено - API, синьо - JSON на страницата, сиво - HTML, оранжево - резервен regex (основният път не успя), лилаво - списък. Оранжево „—“: празно поле при онлайн модел.",
        toast: "Парсер {site}: статусът е неизвестен за {u} от {n} модела. Възможно е сайтът да е променил разметката. Кликнете, за да отворите отчета.",
    },
    EL: {
        title: "Αναφορά parser",
        copy: "Αντιγραφή",
        copied: "Αντιγράφηκε",
        reset: "Επαναφορά",
        nodata: "Δεν υπάρχουν δεδομένα ακόμη: η αναφορά συμπληρώνεται καθώς ανανεώνονται τα αγαπημένα.",
        ok: "Όλα καλά: {u} από {n} χωρίς κατάσταση.",
        warn: "Η κατάσταση είναι άγνωστη για {u} από {n} μοντέλα (όριο {p}%). Πιθανότατα ο ιστότοπος άλλαξε την απάντησή του. Πατήστε «{copy}» και στείλτε το JSON για να διορθωθεί ο πίνακας κλειδιών BC_SCHEMA.",
        age: "ηλικία",
        unknown: "άγνωστη",
        diag: "Διάγνωση",
        nostatus: "χωρίς κατάσταση",
        hint: "Χρώματα: πράσινο - API, μπλε - JSON σελίδας, γκρι - HTML, πορτοκαλί - εφεδρικό regex (απέτυχε η κύρια διαδρομή), μοβ - λίστα. Πορτοκαλί «—»: κενό πεδίο σε διαδικτυακό μοντέλο.",
        toast: "Parser {site}: άγνωστη κατάσταση για {u} από {n} μοντέλα. Η δομή του ιστότοπου μπορεί να άλλαξε. Κάντε κλικ για να ανοίξει η αναφορά.",
    },
    JA: {
        title: "パーサーレポート",
        copy: "コピー",
        copied: "コピーしました",
        reset: "リセット",
        nodata: "まだデータがありません。お気に入りの更新に合わせてレポートが埋まります。",
        ok: "問題ありません: {n}件中{u}件がステータスなし。",
        warn: "{n}件中{u}件のモデルでステータス不明です(しきい値 {p}%)。サイト側の応答が変わった可能性があります。「{copy}」を押して JSON を送ってください。BC_SCHEMA のキー表を修正します。",
        age: "経過",
        unknown: "不明",
        diag: "診断",
        nostatus: "ステータスなし",
        hint: "色: 緑 - API、青 - ページの JSON、灰 - HTML、橙 - 予備の正規表現(主経路が失敗)、紫 - 一覧。橙の「—」: オンラインのモデルで項目が空。",
        toast: "{site} パーサー: {n}件中{u}件のモデルでステータスが不明です。サイトの構造が変わった可能性があります。クリックでレポートを開きます。",
    },
    ZH: {
        title: "解析器报告",
        copy: "复制",
        copied: "已复制",
        reset: "重置",
        nodata: "暂无数据:报告会随收藏更新逐步填充。",
        ok: "一切正常:{n} 个中有 {u} 个无状态。",
        warn: "{n} 个模特中有 {u} 个状态未知(阈值 {p}%)。网站很可能更改了响应。请点击“{copy}”并发送 JSON,以便调整 BC_SCHEMA 键表。",
        age: "时长",
        unknown: "未知",
        diag: "诊断",
        nostatus: "无状态",
        hint: "颜色:绿 - API,蓝 - 页面 JSON,灰 - HTML,橙 - 备用正则(主路径失败),紫 - 列表。橙色“—”:在线模特的字段为空。",
        toast: "{site} 解析器:{n} 个模特中有 {u} 个状态未知。网站结构可能已更改。点击打开报告。",
    },
    KO: {
        title: "파서 보고서",
        copy: "복사",
        copied: "복사됨",
        reset: "초기화",
        nodata: "아직 데이터가 없습니다: 즐겨찾기가 갱신되면서 보고서가 채워집니다.",
        ok: "정상: {n}개 중 {u}개 상태 없음.",
        warn: "{n}개 모델 중 {u}개의 상태를 알 수 없습니다(임계값 {p}%). 사이트가 응답을 변경했을 가능성이 큽니다. \"{copy}\"를 눌러 JSON을 보내 주시면 BC_SCHEMA 키 표를 수정할 수 있습니다.",
        age: "경과",
        unknown: "알 수 없음",
        diag: "진단",
        nostatus: "상태 없음",
        hint: "색상: 초록 - API, 파랑 - 페이지 JSON, 회색 - HTML, 주황 - 대체 정규식(기본 경로 실패), 보라 - 목록. 주황색 \"—\": 온라인 모델의 항목이 비어 있음.",
        toast: "{site} 파서: {n}개 모델 중 {u}개의 상태를 알 수 없습니다. 사이트 구조가 바뀌었을 수 있습니다. 클릭하면 보고서를 엽니다.",
    },
    HE: {
        title: "דוח מנתח",
        copy: "העתק",
        copied: "הועתק",
        reset: "איפוס",
        nodata: "אין עדיין נתונים: הדוח מתמלא עם רענון המועדפים.",
        ok: "הכול תקין: {u} מתוך {n} ללא סטטוס.",
        warn: "הסטטוס לא ידוע עבור {u} מתוך {n} מודלים (סף {p}%). כנראה האתר שינה את התשובה. לחצו על \"{copy}\" ושלחו את ה-JSON כדי לתקן את טבלת המפתחות BC_SCHEMA.",
        age: "גיל",
        unknown: "לא ידוע",
        diag: "אבחון",
        nostatus: "ללא סטטוס",
        hint: "צבעים: ירוק - API, כחול - JSON של הדף, אפור - HTML, כתום - regex חלופי (הנתיב הראשי נכשל), סגול - רשימה. \"—\" בכתום: שדה ריק במודל מחובר.",
        toast: "מנתח {site}: הסטטוס לא ידוע עבור {u} מתוך {n} מודלים. ייתכן שמבנה האתר השתנה. לחצו כדי לפתוח את הדוח.",
    },
    AR: {
        title: "تقرير المحلّل",
        copy: "نسخ",
        copied: "تم النسخ",
        reset: "إعادة تعيين",
        nodata: "لا توجد بيانات بعد: يمتلئ التقرير مع تحديث المفضلة.",
        ok: "كل شيء على ما يرام: {u} من {n} بلا حالة.",
        warn: "الحالة غير معروفة لدى {u} من {n} نماذج (الحد {p}%). على الأرجح غيّر الموقع استجابته. اضغط «{copy}» وأرسل JSON لتعديل جدول المفاتيح BC_SCHEMA.",
        age: "العمر",
        unknown: "غير معروف",
        diag: "التشخيص",
        nostatus: "بلا حالة",
        hint: "الألوان: أخضر - API، أزرق - JSON الصفحة، رمادي - HTML، برتقالي - regex احتياطي (فشل المسار الرئيسي)، بنفسجي - القائمة. «—» برتقالية: الحقل فارغ لنموذج متصل.",
        toast: "محلّل {site}: الحالة غير معروفة لدى {u} من {n} نماذج. ربما تغيّرت بنية الموقع. انقر لفتح التقرير.",
    },
    HI: {
        title: "पार्सर रिपोर्ट",
        copy: "कॉपी करें",
        copied: "कॉपी हो गया",
        reset: "रीसेट करें",
        nodata: "अभी कोई डेटा नहीं: पसंदीदा रिफ्रेश होने पर रिपोर्ट भरती जाती है।",
        ok: "सब ठीक है: {n} में से {u} बिना स्थिति के।",
        warn: "{n} में से {u} मॉडल की स्थिति अज्ञात है (सीमा {p}%)। संभवतः साइट ने अपना उत्तर बदल दिया है। \"{copy}\" दबाएँ और BC_SCHEMA कुंजी तालिका ठीक करने के लिए JSON भेजें।",
        age: "आयु",
        unknown: "अज्ञात",
        diag: "निदान",
        nostatus: "बिना स्थिति",
        hint: "रंग: हरा - API, नीला - पेज JSON, स्लेटी - HTML, नारंगी - वैकल्पिक regex (मुख्य रास्ता विफल), बैंगनी - सूची। नारंगी \"—\": ऑनलाइन मॉडल के लिए फ़ील्ड खाली है।",
        toast: "{site} पार्सर: {n} में से {u} मॉडल की स्थिति अज्ञात है। हो सकता है साइट की संरचना बदल गई हो। रिपोर्ट खोलने के लिए क्लिक करें।",
    },
    ID: {
        title: "Laporan parser",
        copy: "Salin",
        copied: "Tersalin",
        reset: "Atur ulang",
        nodata: "Belum ada data: laporan terisi seiring favorit diperbarui.",
        ok: "Semua baik: {u} dari {n} tanpa status.",
        warn: "Status tidak diketahui pada {u} dari {n} model (ambang {p}%). Kemungkinan situs mengubah responsnya. Tekan \"{copy}\" lalu kirim JSON untuk menyesuaikan tabel kunci BC_SCHEMA.",
        age: "usia",
        unknown: "tidak diketahui",
        diag: "Diagnostik",
        nostatus: "tanpa status",
        hint: "Warna: hijau - API, biru - JSON halaman, abu-abu - HTML, oranye - regex cadangan (jalur utama gagal), ungu - daftar. \"—\" oranye: kolom kosong pada model online.",
        toast: "Parser {site}: status tidak diketahui pada {u} dari {n} model. Struktur situs mungkin berubah. Klik untuk membuka laporan.",
    },
    VI: {
        title: "Báo cáo trình phân tích",
        copy: "Sao chép",
        copied: "Đã sao chép",
        reset: "Đặt lại",
        nodata: "Chưa có dữ liệu: báo cáo được điền dần khi mục yêu thích được làm mới.",
        ok: "Mọi thứ ổn: {u}/{n} không có trạng thái.",
        warn: "Không xác định được trạng thái của {u}/{n} model (ngưỡng {p}%). Nhiều khả năng trang đã đổi phản hồi. Nhấn “{copy}” và gửi JSON để chỉnh bảng khóa BC_SCHEMA.",
        age: "tuổi",
        unknown: "không rõ",
        diag: "Chẩn đoán",
        nostatus: "không có trạng thái",
        hint: "Màu: xanh lá - API, xanh dương - JSON của trang, xám - HTML, cam - regex dự phòng (đường chính thất bại), tím - danh sách. “—” màu cam: trường trống ở model đang online.",
        toast: "Trình phân tích {site}: không xác định được trạng thái của {u}/{n} model. Cấu trúc trang có thể đã thay đổi. Nhấp để mở báo cáo.",
    },
    TH: {
        title: "รายงานตัวแยกวิเคราะห์",
        copy: "คัดลอก",
        copied: "คัดลอกแล้ว",
        reset: "รีเซ็ต",
        nodata: "ยังไม่มีข้อมูล: รายงานจะเติมเมื่อรายการโปรดรีเฟรช",
        ok: "ปกติดี: {u} จาก {n} ไม่มีสถานะ",
        warn: "ไม่ทราบสถานะของ {u} จาก {n} โมเดล (เกณฑ์ {p}%) เป็นไปได้ว่าเว็บไซต์เปลี่ยนการตอบกลับ กด \"{copy}\" แล้วส่ง JSON เพื่อปรับตารางคีย์ BC_SCHEMA",
        age: "อายุ",
        unknown: "ไม่ทราบ",
        diag: "การวินิจฉัย",
        nostatus: "ไม่มีสถานะ",
        hint: "สี: เขียว - API, น้ำเงิน - JSON ของหน้า, เทา - HTML, ส้ม - regex สำรอง (เส้นทางหลักล้มเหลว), ม่วง - รายการ \"—\" สีส้ม: ช่องว่างสำหรับโมเดลที่ออนไลน์",
        toast: "ตัวแยกวิเคราะห์ {site}: ไม่ทราบสถานะของ {u} จาก {n} โมเดล โครงสร้างเว็บไซต์อาจเปลี่ยนไป คลิกเพื่อเปิดรายงาน",
    },
};
const pdT = (k, v) => {
    const d = PD_T[String(AppState.lang).toUpperCase()] || PD_T.EN;
    return String(d[k] ?? PD_T.EN[k]).replace(/\{(\w+)\}/g, (m, x) => (x === 'copy' ? (d.copy ?? PD_T.EN.copy) : (v && v[x] != null ? v[x] : m)));
};

const bcRoots = h => {
    const roots = [];
    if (!isStr(h) || !h) return roots;
    const c0 = h.charCodeAt(0);
    if (c0 === 123 || c0 === 91) { try { const j = JSON.parse(h); if (isObj(j)) roots.push(j); } catch {} }
    if (h.indexOf('<script') >= 0) {
        for (const m of h.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi)) {
            if (roots.length >= 12) break;
            const body = m[2].trim(), c = body.charCodeAt(0);
            if (body.length < 2 || body.length > 3e6 || (c !== 123 && c !== 91) || !/json|data-type/i.test(m[1])) continue;
            try { const j = JSON.parse(body); if (isObj(j)) roots.push(j); } catch {}
        }
    }
    return roots;
};
const jsonFind = (roots, names, pred, limit = 30000) => {
    const set = new Set(names.map(n => String(n).toLowerCase()));
    const q = roots.filter(isObj);
    let n = 0;
    for (let i = 0; i < q.length && n < limit; i++) {
        const o = q[i];
        const ent = Array.isArray(o) ? o.map((v, k) => [String(k), v]) : Object.entries(o);
        for (const [k, v] of ent) {
            n++;
            if (set.has(k.toLowerCase()) && (!pred || pred(v))) return { value: v, key: k };
            if (isObj(v)) q.push(v);
        }
    }
    return null;
};
const bcFind = (roots, spec, pred, scopedOnly) => {
    if (!roots.length) return null;
    for (const sc of spec.scopes || []) {
        const s = jsonFind(roots, [sc], v => isObj(v) && !Array.isArray(v));
        const r = s && jsonFind([s.value], spec.keys, pred);
        if (r) return { value: r.value, via: `json:${sc}` };
    }
    if (scopedOnly) return null;
    const r = jsonFind(roots, spec.keys, pred);
    return r ? { value: r.value, via: 'json' } : null;
};
const bcFact = (roots, spec) => {
    let seen = null;
    for (const k of spec.keys) {
        const r = bcFind(roots, { keys: [k], scopes: spec.scopes }, v => typeof v === 'boolean');
        if (!r) continue;
        if (r.value) return { v: true, via: r.via };
        seen = seen || { v: false, via: r.via };
    }
    return seen;
};

const BC_ROOM = new Map();
const bcListHint = slug => {
    const e = slug && BC_ROOM.get(String(slug).toLowerCase());
    if (!e || Date.now() - e.t > 180000) return null;
    const r = String(e.room).toLowerCase();
    if (/private|voyeur/.test(r)) return { st: 3, lk: 'st_private' };
    if (/group/.test(r)) return { st: 3, lk: 'st_group' };
    if (/vip/.test(r)) return { st: 3, lk: 'st_vip' };
    if (/away/.test(r)) return { st: 2, lk: 'st_away' };
    if (/^(?:public|free|chat)$/.test(r)) return { st: 1, lk: '' };
    return null;
};
const bcJsonStatus = roots => {
    if (!roots.length) return null;
    const F = {};
    for (const k of Object.keys(BC_SCHEMA.facts)) F[k] = bcFact(roots, BC_SCHEMA.facts[k]);
    const sh = bcFind(roots, BC_SCHEMA.showType, isStr);
    const T = sh ? sh.value.trim().toLowerCase() : '';
    const hit = f => F[f]?.v === true;
    const first = F.off || F.priv || F.grp || F.vip || F.away || F.on || sh;
    if (!first) return null;
    const via = first.via;
    if (hit('off')) return [0, '', via];
    if (hit('priv') || /^(?:private|full_?private|voyeur|p2p)/.test(T)) return [3, 'st_private', via];
    if (hit('grp') || /^group/.test(T)) return [3, 'st_group', via];
    if (hit('vip') || T === 'vip') return [3, 'st_vip', via];
    if (hit('away')) return [2, 'st_away', via];
    if (F.off?.v === false || hit('on') || /^(?:public|free|chat)$/.test(T)) return [1, '', via];
    if (F.on?.v === false || /^(?:offline|off)$/.test(T)) return [0, '', via];
    return null;
};
const resolveStatusEx = (h, site, bcd, roots, slug) => {
    if (site === 'bc') {
        if (bcd && typeof bcd.o === 'boolean') {
            let r = bcdStatus(bcd, h);
            if (bcd.o && h) { const a = parseStatus(h, site); if (a[0] === 2) r = a; }
            return { st: r[0], lk: r[1] || '', src: 'api:chat' };
        }
        const j = bcJsonStatus(roots);
        if (j) return { st: j[0], lk: j[1], src: j[2] };
        if (bcd && bcd.n === false) return { st: 0, lk: '', src: 'api:amf' };
        if (bcd && bcd.a === true) return { st: 2, lk: 'st_away', src: 'api:amf' };
        if (bcd && bcd.n === true) return { st: 1, lk: '', src: 'api:amf' };
        const ls = bcListHint(slug);
        if (ls) return { ...ls, src: 'listing' };
    }
    const r = parseStatus(h, site);
    if (r[2] === false) return { st: 0, lk: '', src: '', unk: true };
    return { st: r[0], lk: r[1] || '', src: 'regex' };
};

const bcTopic = (h, bcd, roots) => {
    if (isStr(bcd?.t) && bcd.t.trim()) return [bcd.t.trim(), 'api:chat'];
    const j = bcFind(roots, BC_SCHEMA.topic, v => isStr(v) && !!v.trim());
    if (j) return [j.value.trim(), j.via];
    const r = jsonField(h, TOPIC_RE).trim();
    return r ? [r, 'regex'] : ['', ''];
};
const bcFrameNorm = s => String(s).replace(/\\\//g, '/').replace(/^\/\//, 'https://').replace(/^http:\/\//i, 'https://');
const bcFrame = (h, bcd, roots) => {
    const j = bcFind(roots, BC_SCHEMA.frame, v => isStr(v) && HTTP_RE.test(bcFrameNorm(v)));
    if (j) return [bcFrameNorm(j.value), j.via];
    const rx = bcFrameNorm(jsonField(h, FRAME_RE) || jsonField(h, FRAME_ALT_RE));
    if (HTTP_RE.test(rx)) return [rx, 'regex'];
    return isStr(bcd?.f) && HTTP_RE.test(bcd.f) ? [bcFrameNorm(bcd.f), 'api:amf'] : ['', ''];
};
const bcViewersEx = (h, roots) => {
    const n = bcViewers(h);
    if (n != null) return [n, 'html:oc_count'];
    const j = bcFind(roots, BC_SCHEMA.viewers, v => (typeof v === 'number' || (isStr(v) && v.trim() !== '')) && Number.isFinite(+v) && +v >= 0 && +v < 1e7, true);
    return j ? [Math.round(+j.value), j.via] : [null, ''];
};
const bcExtraSrc = (h, roots) => {
    const out = {}, src = { tags: '', sch: '' };
    if (!h) return { out, src };
    const ts = pilgPart(h, 'pilg_section __tags');
    let tg = ts ? parsePilgTags(ts) : null;
    if (tg) src.tags = 'html:pilg';
    if (!tg) {
        const j = bcFind(roots, BC_SCHEMA.tags, a => Array.isArray(a) && collectProfileTags(a).tags.length > 0);
        if (j) {
            const { tags, icos } = collectProfileTags(j.value);
            tg = { ptags: tags, ...(icos.some(Boolean) ? { ptic: icos } : {}) };
            src.tags = j.via;
        }
    }
    if (!tg) { tg = parseBcJsonTags(h); if (tg) src.tags = 'regex'; }
    if (tg) Object.assign(out, tg);
    const ss = pilgPart(h, 'pilg_section __schedule');
    let sch = ss ? parsePilgSchedule(ss) : null;
    if (sch) src.sch = 'html:pilg';
    if (!sch) {
        const j = bcFind(roots, BC_SCHEMA.schedule, a => isWeekSchedule(a) && scheduleDays(a).some(Boolean));
        if (j) { sch = scheduleDays(j.value); src.sch = j.via; }
    }
    if (!sch) { sch = parseBcJsonSchedule(h); if (sch) src.sch = 'regex'; }
    if (sch) out.sch = sch;
    return { out, src };
};
const parseBcExtra = h => bcExtraSrc(h, bcRoots(h)).out;

const pdToast = msg => {
    try {
        const d = document.createElement('div');
        d.textContent = msg;
        d.style.cssText = 'all:initial;position:fixed;left:50%;bottom:24px;transform:translateX(-50%);max-width:min(560px,92vw);z-index:2147483647;background:#3a2410;color:#ffd9a8;border:1px solid #c77d2a;border-radius:10px;padding:10px 14px;font:13px/1.35 system-ui,sans-serif;cursor:pointer;box-shadow:0 6px 24px rgba(0,0,0,.5)';
        d.onclick = () => { d.remove(); ParserReport.open(); };
        (document.body || document.documentElement).appendChild(d);
        setTimeout(() => d.remove(), 15000);
    } catch {}
};
const ParserDiag = {
    recs: new Map(), warned: false, flag: null,
    rec(site, slug, src, unk, status, sig) {
        try {
            if (!slug) return;
            const k = `${site}|${String(slug).toLowerCase()}`;
            this.recs.delete(k);
            this.recs.set(k, { t: Date.now(), site, slug, st: status, unk: !!unk, src: { ...src }, ...(unk && sig ? { sig } : {}) });
            if (this.recs.size > 300) this.recs.delete(this.recs.keys().next().value);
            this.check(site);
        } catch {}
    },
    stats(site) {
        const now = Date.now();
        let n = 0, u = 0;
        for (const r of this.recs.values()) {
            if ((site && r.site !== site) || now - r.t > PD_WINDOW) continue;
            n++;
            if (r.unk) u++;
        }
        return { n, u, ratio: n ? u / n : 0 };
    },
    check(site) {
        const s = this.stats(site);
        const bad = s.n >= PD_WARN_MIN && s.ratio >= PD_WARN_RATIO;
        this.flag = bad ? { site, ...s } : null;
        if (bad && !this.warned) {
            this.warned = true;
            pdToast(pdT('toast', { site: site.toUpperCase(), u: s.u, n: s.n }));
        } else if (!bad && s.ratio < PD_WARN_RATIO / 3) this.warned = false;
    },
    clear() { this.recs.clear(); this.flag = null; this.warned = false; }
};
const PD_FIELDS = ['status', 'topic', 'frame', 'viewers', 'tags', 'sch'];
const PD_ST = ['offline', 'online', 'away', 'show'];
const ParserReport = {
    host: null,
    close() { this.host?.remove(); this.host = null; },
    exportText() {
        return JSON.stringify({
            version: '4.2.3', at: new Date().toISOString(), warn: { ratio: PD_WARN_RATIO, min: PD_WARN_MIN },
            stats: ParserDiag.stats(), records: [...ParserDiag.recs.values()]
        }, null, 1);
    },
    copy() {
        const txt = this.exportText();
        try {
            if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(txt).catch(() => this.copyOld(txt));
        } catch {}
        this.copyOld(txt);
    },
    copyOld(txt) {
        try {
            const ta = document.createElement('textarea');
            ta.value = txt;
            ta.style.cssText = 'position:fixed;left:-9999px';
            document.body.appendChild(ta);
            ta.select();
            document.execCommand('copy');
            ta.remove();
        } catch {}
    },
    open() {
        this.close();
        const E = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
        const host = document.createElement('div');
        host.style.cssText = 'all:initial;position:fixed;inset:0;z-index:2147483647';
        const sh = host.attachShadow({ mode: 'open' });
        const st = E('style');
        st.textContent = `
            .ov{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;font:13px/1.4 system-ui,sans-serif}
            .card{background:#1c1c20;color:#e6e6ea;border:1px solid #3a3a42;border-radius:12px;width:min(1000px,96vw);max-height:92vh;display:flex;flex-direction:column;overflow:hidden}
            .hd{display:flex;align-items:center;gap:8px;padding:12px 14px;border-bottom:1px solid #2e2e36}
            .hd h3{margin:0;font-size:15px;flex:1}
            button{background:#2c2c34;color:#e6e6ea;border:1px solid #44444e;border-radius:8px;padding:5px 11px;cursor:pointer;font:inherit}
            button:hover{background:#383842}
            .bd{padding:12px 14px;overflow:auto}
            .warn{background:#3a2410;border:1px solid #c77d2a;color:#ffd9a8;border-radius:8px;padding:8px 10px;margin-bottom:10px}
            .ok{background:#12301f;border:1px solid #2b7a4b;color:#b6f0cf;border-radius:8px;padding:8px 10px;margin-bottom:10px}
            .sum{display:grid;grid-template-columns:90px 1fr;gap:4px 10px;margin-bottom:12px}
            .sum b{color:#9a9aa6;font-weight:600}
            table{border-collapse:collapse;width:100%;font-size:12px}
            th,td{text-align:left;padding:4px 8px;border-bottom:1px solid #2a2a32;white-space:nowrap}
            th{color:#9a9aa6;position:sticky;top:0;background:#1c1c20}
            .wrap{overflow:auto;max-height:52vh;border:1px solid #2a2a32;border-radius:8px}
            .api{color:#6fd39a}.json{color:#7db7ff}.html{color:#b0b0ba}.regex{color:#ffb454}.listing{color:#c79bff}
            .miss{color:#ffb454}.dim{color:#5c5c68}.bad{color:#ff6b6b;font-weight:600}
            .hint{color:#8a8a96;margin-top:8px;font-size:12px}`;
        const wrap = E('div', 'ov');
        wrap.onclick = e => { if (e.target === wrap) this.close(); };
        const card = E('div', 'card');
        const hd = E('div', 'hd');
        hd.append(E('h3', '', pdT('title')));
        const bCopy = E('button', '', pdT('copy'));
        bCopy.onclick = async () => { await this.copy(); bCopy.textContent = pdT('copied'); setTimeout(() => { bCopy.textContent = pdT('copy'); }, 1500); };
        const bClr = E('button', '', pdT('reset'));
        bClr.onclick = () => { ParserDiag.clear(); this.open(); };
        const bX = E('button', '', '✕');
        bX.onclick = () => this.close();
        hd.append(bCopy, bClr, bX);
        const bd = E('div', 'bd');

        const recs = [...ParserDiag.recs.values()].sort((a, b) => (b.unk - a.unk) || (b.t - a.t));
        const stats = ParserDiag.stats();
        if (!recs.length) {
            bd.append(E('div', 'ok', pdT('nodata')));
        } else if (ParserDiag.flag) {
            bd.append(E('div', 'warn', pdT('warn', { u: stats.u, n: stats.n, p: Math.round(PD_WARN_RATIO * 100) })));
        } else {
            bd.append(E('div', 'ok', pdT('ok', { u: stats.u, n: stats.n })));
        }
        const srcCls = s => (s ? s.split(':')[0] : '');
        const sum = E('div', 'sum');
        for (const f of PD_FIELDS) {
            const cnt = new Map();
            for (const r of recs) {
                const s = f === 'status' && r.unk ? '?' : (r.src[f] || '');
                if (!s && (r.st === 0 && f !== 'status')) continue;
                cnt.set(s || '—', (cnt.get(s || '—') || 0) + 1);
            }
            const line = E('div');
            [...cnt.entries()].sort((a, b) => b[1] - a[1]).forEach(([s, n], i) => {
                if (i) line.append(' · ');
                const sp = E('span', s === '?' ? 'bad' : (s === '—' ? 'dim' : srcCls(s)), `${s} ${n}`);
                line.append(sp);
            });
            sum.append(E('b', '', f), line);
        }
        bd.append(sum);

        const wr = E('div', 'wrap'), tb = E('table'), thr = E('tr');
        ['model', 'site', ...PD_FIELDS, pdT('age')].forEach(c => thr.append(E('th', '', c)));
        const thead = E('thead'); thead.append(thr); tb.append(thead);
        const tbody = E('tbody');
        for (const r of recs.slice(0, 150)) {
            const tr = E('tr');
            tr.append(E('td', '', r.slug), E('td', 'dim', r.site));
            PD_FIELDS.forEach(f => {
                const s = r.src[f] || '';
                let td;
                if (f === 'status') td = r.unk ? E('td', 'bad', `? ${pdT('unknown')}`) : E('td', srcCls(s), `${PD_ST[r.st] || r.st} · ${s || '—'}`);
                else if (s) td = E('td', srcCls(s), s);
                else td = E('td', r.st > 0 && !r.unk && (f === 'topic' || f === 'frame' || f === 'viewers') ? 'miss' : 'dim', '—');
                tr.append(td);
            });
            const age = Math.round((Date.now() - r.t) / 1000);
            tr.append(E('td', 'dim', age < 90 ? `${age}s` : `${Math.round(age / 60)}m`));
            if (r.unk && r.sig) tr.title = JSON.stringify(r.sig);
            tbody.append(tr);
        }
        tb.append(tbody); wr.append(tb); bd.append(wr);
        bd.append(E('div', 'hint', pdT('hint')));
        card.append(hd, bd);
        wrap.append(card);
        sh.append(st, wrap);
        (document.body || document.documentElement).appendChild(host);
        this.host = host;
        const onKey = e => { if (e.key === 'Escape') { this.close(); document.removeEventListener('keydown', onKey, true); } };
        document.addEventListener('keydown', onKey, true);
    }
};
const HTTP_RE = /^https?:\/\//i;
const FRAME_ALT_RE = /"(?:frameUrl|previewFrame|previewUrl|livePreviewUrl)"\s*:\s*"((?:[^"\\]|\\.)*)"/i;
const pickFrame = (h, bcd) => {
    const frame = (jsonField(h, FRAME_RE) || jsonField(h, FRAME_ALT_RE)).replace(/\\\//g, '/').replace(/^\/\//, 'https://');
    if (HTTP_RE.test(frame)) return frame;
    return isStr(bcd?.f) && HTTP_RE.test(bcd.f) ? bcd.f.replace(/^http:\/\//i, 'https://') : '';
};
const bcdExtras = bcd => ({
    ...(Array.isArray(bcd?.ptags) && bcd.ptags.length ? { ptags: bcd.ptags.slice(0, 30) } : {}),
    ...(Array.isArray(bcd?.sch) && bcd.sch.length === 7 ? { sch: bcd.sch } : {})
});
const parseProfile = (h, site = 'bc', slug = '') => {
    let bcd;
    [h, bcd] = splitBcd(h, site);
    const roots = site === 'bc' ? bcRoots(h) : [];
    const R = resolveStatusEx(h, site, bcd, roots, slug);
    const status = R.st, lk = R.lk;
    const src = { status: R.src };
    const done = info => {
        Object.defineProperty(info, 'unk', { value: !!R.unk, enumerable: false, writable: true, configurable: true });
        const sig = R.unk ? { len: isStr(h) ? h.length : 0, head: String(h || '').slice(0, 100).replace(/\s+/g, ' '), roots: roots.map(r => Object.keys(r).slice(0, 8).join(',')) } : null;
        ParserDiag.rec(site, slug, src, R.unk, info.status, sig);
        return info;
    };
    if (siteProfile[site]) {
        const info = { status, lk, ...siteProfile[site](h, status, slug) };
        src.topic = info.topic ? 'regex' : '';
        src.frame = info.frame ? 'regex' : '';
        src.viewers = info.viewers != null ? 'regex' : '';
        return done(info);
    }
    const [topic, tSrc] = status ? bcTopic(h, bcd, roots) : ['', ''];
    const [frame, fSrc] = bcFrame(h, bcd, roots);
    const [vn, vSrc] = bcViewersEx(h, roots);
    const ex = site === 'bc' ? bcExtraSrc(h, roots) : { out: {}, src: { tags: '', sch: '' } };
    const extra = site === 'bc' ? { ...ex.out, ...bcdExtras(bcd) } : {};
    src.topic = tSrc; src.frame = fSrc; src.viewers = vn != null ? vSrc : '';
    src.tags = extra.ptags ? (bcd?.ptags?.length ? 'api:amf' : ex.src.tags) : '';
    src.sch = extra.sch ? (bcd?.sch?.length === 7 ? 'api:amf' : ex.src.sch) : '';
    return done({
        status, lk,
        topic,
        frame,
        ...(vn != null ? { viewers: vn } : {}),
        ...extra
    });
};

const withBackoff = async (fn, { tries = 2, base = 500, signal } = {}) => {
    for (let i = 0; ; i++) {
        const res = await fn();
        if (res !== null || i >= tries - 1 || signal?.aborted) return res;
        await new Promise(r => setTimeout(r, base * 2 ** i * (0.75 + Math.random() * 0.5)));
    }
};
const MOBILE_UA = 'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36';
const badStatus = c => c >= 500 || c === 403 || c === 429;
const requestOnce = (url, extra, signal, timeout) => {
    let headers = {};
    if (/\/api\//.test(url)) {
        headers = { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/plain, */*' };
        try { headers.Referer = `${new URL(url).origin}/`; } catch {}
    }
    if (extra) headers = { ...headers, ...extra };
    return gmReq({ url, headers, signal, ...(timeout && { timeout }) }).then(r => (r.fail || badStatus(r.status) ? null : r.body));
};
const requestPostOnce = (url, data, extra, signal, timeout) => {
    const headers = { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/plain, */*', 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', ...(extra || {}) };
    return gmReq({ method: 'POST', url, data, headers, signal, credentials: 'include', ...(timeout && { timeout }) }).then(r => (r.fail || badStatus(r.status) ? null : r.body));
};
const requestHtml = async (url, signal, timeout) => (await withBackoff(() => requestOnce(url, undefined, signal, timeout), { signal })) ?? '';
const STATUS_KEY = /room_status|"status"\s*:|"isLive"|"isOnline"/i;
const profileUrls = b => {
    const slug = modelSlug(b.url), site = urlSite(b.url);
    if (!slug) return [b.url];
    try {
        if (site === 'cb') return [`https://chaturbate.com/api/chatvideocontext/${encodeURIComponent(slug)}/`, b.url];
        if (site === 'sc') {
            const o = new URL(b.url).origin, e = encodeURIComponent(slug);
            return [...new Set([`${o}/api/front/v2/models/username/${e}/cam`, `https://stripchat.com/api/front/v2/models/username/${e}/cam`])].concat(b.url);
        }
    } catch {}
    return [b.url];
};
const requestProfileHtml = async (b, signal) => {
    for (const u of profileUrls(b)) {
        const h = await requestHtml(u, signal, 4000);
        if (h && (u === b.url || STATUS_KEY.test(h))) return h;
    }
    return '';
};

const BCD_MARK = '\n/*BCD*/';
const bcChatInfo = j => {
    const r = j?.status === 'success' ? j.result : null, s = r?.chatShowStatusOptions;
    if (!isObj(s) || s.isAvailable === false || typeof s.isOffline !== 'boolean') return null;
    return {
        o: s.isOffline,
        p: !!(s.isPrivateChat || s.isFullPrivateChat || s.isVoyeurChat),
        g: !!s.isGroupChat,
        v: !!s.isVipShow,
        t: isStr(r.chatTopicOptions?.currentTopic) ? r.chatTopicOptions.currentTopic : ''
    };
};
const bcChatData = async (b, signal) => {
    const o = originOf(b.url), slug = modelSlug(b.url);
    if (!o || !slug) return null;
    const url = `${o}/get-member-chat-data?username=${encodeURIComponent(slug)}&withMiniProfile=1&liveTab=female`;
    const hdr = { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/plain, */*', 'Referer': `${o}/${encodeURIComponent(slug)}` };
    try {
        return bcChatInfo(JSON.parse((await requestOnce(url, hdr, signal, 4000)) || ''));
    } catch { return null; }
};
const bcCsrfToken = () => {
    return document.documentElement.getAttribute('data-csrf_value')
        || document.querySelector('input[name="_csrf_token"]')?.value
        || document.querySelector('meta[name="csrf-token"]')?.content
        || ((document.documentElement.innerHTML || '').match(/(?:_csrf_token|csrfToken)["'\s:=]+([a-f0-9]{24,64})/i) || [])[1];
};
const bcRoomInfo = p => {
    const tags = Array.isArray(p.modelTags) ? p.modelTags.map(x => isStr(x) ? x : (x?.name || x?.title || x?.tag || '')).filter(isStr) : [];
    const schedule = isWeekSchedule(p.schedule) ? scheduleDays(p.schedule) : null;
    return {
        ...(typeof p.isAway === 'boolean' ? { a: p.isAway } : {}),
        ...(typeof p.isOnline === 'boolean' ? { n: p.isOnline } : {}),
        ...(tags.length ? { ptags: tags.slice(0, 30) } : {}),
        ...(isStr(p.frame) ? { f: p.frame } : {}),
        ...(schedule && schedule.some(Boolean) ? { sch: schedule } : {})
    };
};
const parseRoomData = raw => {
    const j = JSON.parse(raw || ''), p = j?.status === 'success' ? j.performerData : null;
    return isObj(p) ? bcRoomInfo(p) : null;
};
const bcRoomData = async (b, signal) => {
    const o = originOf(b.url), slug = modelSlug(b.url);
    if (!o || !slug || location.origin !== o || !document.documentElement) return null;
    const token = bcCsrfToken();
    if (!token) return null;
    const body = `method=getRoomData&args%5B%5D=${encodeURIComponent(slug)}&args%5B%5D=&args%5B%5D=&_csrf_token=${encodeURIComponent(token)}`;
    try {
        const raw = await requestPostOnce(`${o}/tools/amf.php?m=1&t=${Date.now()}`, body, { Referer: `${o}/` }, signal, 4000);
        return parseRoomData(raw);
    } catch { return null; }
};
const htmlSaysAway = html => /"isAway"\s*:\s*true/i.test(html) || /(?:"|\\u0022)room_status(?:"|\\u0022)\s*:\s*(?:"|\\u0022)away/i.test(html);
const bcdStatus = (d, html = '') => {
    if (d.o) return [0, ''];
    if (d.p) return [3, 'st_private'];
    if (d.g) return [3, 'st_group'];
    if (d.v) return [3, 'st_vip'];
    if (d.a === true || htmlSaysAway(html)) return [2, 'st_away'];
    return [1, ''];
};
const requestProfile = async (b, signal) => {
    if (urlSite(b.url) !== 'bc') return requestProfileHtml(b, signal);
    const [h, d, room] = await Promise.all([requestProfileHtml(b, signal), bcChatData(b, signal), bcRoomData(b, signal)]);
    const payload = { ...(d || {}), ...(room || {}) };
    return (d || room) ? (h || '') + BCD_MARK + JSON.stringify(payload) : h;
};

const BC_LIST_TTL = 20000, BC_LIST_PAGE = 500, BC_LIST_MAX = 4000;
const BC_EXTRA_TABS = ['male', 'couples', 'trans'];
const bcListCache = new Map();
const originOf = u => { try { return new URL(u).origin; } catch { return ''; } };
const bcListCandidates = j => {
    const c = Array.isArray(j?.models) ? [...j.models] : [];
    const nav = j?.nav;
    if (!nav || typeof nav !== 'object') return c;
    if (nav.current && typeof nav.current === 'object') c.push(nav.current);
    for (const key of ['next', 'prev']) if (Array.isArray(nav[key])) c.push(...nav[key]);
    return c;
};
const bcListEat = (map, txt) => {
    try {
        const j = JSON.parse(txt);
        for (const m of bcListCandidates(j)) {
            if (m && isStr(m.username) && m.viewers != null && Number.isFinite(+m.viewers)) {
                map.set(m.username.toLowerCase(), +m.viewers);
                if (isStr(m.room) && m.room) BC_ROOM.set(m.username.toLowerCase(), { room: m.room, t: Date.now() });
            }
        }
        return +(j?.total_count ?? j?.nav?.total_count) || 0;
    } catch { return 0; }
};
const bcListTab = async (origin, tab, signal) => {
    const ck = `${origin}|${tab}`, hit = bcListCache.get(ck);
    if (hit && Date.now() - hit.t < BC_LIST_TTL) return hit.map;
    const base = `${origin}/tools/listing_v3.php?livetab=${encodeURIComponent(tab)}&is_mobile=true&limit=${BC_LIST_PAGE}`;
    const hdr = { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/plain, */*', 'Referer': `${origin}/` };
    const map = new Map();
    const eat = txt => bcListEat(map, txt);
    const total = eat((await requestOnce(base, hdr, signal, 12000)) || '');
    const offs = [];
    for (let o = BC_LIST_PAGE; o < Math.min(total, BC_LIST_MAX); o += BC_LIST_PAGE) offs.push(o);
    const rest = await Promise.all(offs.map(o => requestOnce(`${base}&offset=${o}`, hdr, signal, 12000)));
    rest.forEach(t => t && eat(t));
    if (map.size) bcListCache.set(ck, { t: Date.now(), map });
    return map;
};
const applyProfile = (b, { frame, ...info }) => {
    const same = (x, y) => (Array.isArray(x) || Array.isArray(y)) ? JSON.stringify(x) === JSON.stringify(y) : x === y;
    const changed = Object.keys(info).some(k => !same(b[k], info[k])) || !!(frame && b.frame !== frame);
    if (info.viewers != null) b.vts = Date.now();
    Object.assign(b, info);
    if (frame) b.frame = frame;
    return changed;
};
const AVATAR_RES = [
    /(?:property=["']og:image["']|name=["']og:image["'])[^>]*?content=["']([^"']+)["']/i,
    /content=["']([^"']+)["'][^>]*?property=["']og:image["']/i,
    /"(?:profileImage|avatarUrl)"\s*:\s*"([^"]+)"/i
];
const extractAvatar = html => {
    for (const re of AVATAR_RES) {
        const m = html.match(re);
        if (m?.[1]) return toHttps(m[1].replace(/\\/g, ''));
    }
    return '';
};
const ogImage = () => toHttps(document.querySelector('meta[property="og:image"]')?.content);
const bcAvatar = name => {
    const sd = Helpers.getModelSecretData();
    const own = (sd?.chatHost?.username || '').toLowerCase() === name.toLowerCase();
    return toHttps(own ? sd?.chatHeaderOptions?.profileImage : '')
        || toHttps(document.querySelector('.chat-info-avatar img, .chat-header-avatar img, .profile-photo img')?.src);
};
const scAvatar = () => toHttps(document.querySelector('img.image-background--round[src*="avatars"]')?.src) || ogImage();
const cbAvatar = name => name ? `https://thumb.live.mmcdn.com/ri/${encodeURIComponent(name.toLowerCase())}.jpg` : ogImage();
const AVATAR_BY_SITE = new Map([['bc', bcAvatar], ['cb', cbAvatar]]);
const pageAvatar = name => (AVATAR_BY_SITE.get(SITE) || scAvatar)(name);

const GM_IMG = new Map();
const CB_HQ_FRAME = true;
const cbRiUrl = slug => `https://thumb.live.mmcdn.com/ri/${encodeURIComponent(String(slug).toLowerCase())}.jpg`;
const cbFrameUrl = slug => CB_HQ_FRAME ? `https://jpeg.live.mmcdn.com/stream?room=${encodeURIComponent(String(slug).toLowerCase())}` : cbRiUrl(slug);
const frameBust = u => {
    try {
        const o = new URL(u);
        o.searchParams.set(/^jpeg\.live\./.test(o.hostname) ? 'f' : 't', /^jpeg\.live\./.test(o.hostname) ? String(Math.random()) : String(Date.now()));
        return o.href;
    } catch { return `${String(u).split('?')[0]}?t=${Date.now()}`; }
};
const cbHqToRi = u => { const m = /^https?:\/\/jpeg\.live\.mmcdn\.com\/stream\?(?:[^#]*&)?room=([^&#]+)/i.exec(String(u)); return m ? cbRiUrl(decodeURIComponent(m[1])) : ''; };
const IMG_DEBUG = Store.get('cbhp_debug', false) === true;
const imgReferer = url => {
    let h = '';
    try { h = new URL(url).hostname; } catch { return ''; }
    if (/bongocams/.test(h)) return 'https://bongocams.com/';
    if (/bongacams|bgicdn|bcvcdn|webcamsluts/.test(h)) return 'https://bongacams.com/';
    if (/stripchat|doppiocdn/.test(h)) return 'https://stripchat.com/';
    if (/chaturbate|mmcdn/.test(h)) return 'https://chaturbate.com/';
    return '';
};
const blobToData = blob => new Promise(resolve => {
    const fr = new FileReader();
    fr.onload = () => resolve(typeof fr.result === 'string' ? fr.result : '');
    fr.onerror = () => resolve('');
    fr.readAsDataURL(blob);
});
const gmImgFetch = async url => {
    const ref = imgReferer(url);
    const hdr = { Accept: 'image/avif,image/webp,image/apng,image/*,*/*;q=0.8' };
    const tries = ref && ref !== `${location.origin}/` ? [{ ...hdr, Referer: ref }, hdr] : [hdr];
    for (const headers of tries) {
        const r = await gmReq({ url, type: 'blob', timeout: 12000, headers });
        if (r.status >= 200 && r.status < 300 && r.body && r.body.size) {
            const d = await blobToData(r.body);
            if (d) return d;
        }
        if (IMG_DEBUG) console.warn('[cams-unchained] img fail', r.status, r.fail, !!headers.Referer, url);
        if (r.status === 404 || r.status === 410) break;
    }
    return '';
};
const gmImgDataRaw = (url, fresh) => {
    if (!url || /^data:/.test(url) || typeof GM_xmlhttpRequest !== 'function') return Promise.resolve('');
    if (!fresh && GM_IMG.has(url)) return GM_IMG.get(url);
    const p = gmImgFetch(url);
    if (fresh) return p;
    GM_IMG.set(url, p);
    if (GM_IMG.size > 400) GM_IMG.delete(GM_IMG.keys().next().value);
    p.then(d => { if (!d && GM_IMG.get(url) === p) GM_IMG.delete(url); });
    return p;
};
const gmImgData = (url, fresh) => gmImgDataRaw(url, fresh).then(d => {
    if (d) return d;
    const ri = cbHqToRi(url);
    return ri ? gmImgDataRaw(frameBust(ri), true) : d;
});
const tryGm = (img, u, fail) => {
    if (!u || /^data:/.test(u) || img._gm === u || typeof GM_xmlhttpRequest !== 'function') return false;
    img._gm = u;
    gmImgData(u).then(d => { if (d && img.isConnected !== false && (!img.classList.contains('bm-prev') || img.hasAttribute('data-frame'))) img.src = d; else if (fail) fail(); });
    return true;
};

const frameFail = img => {
    const b = img._bRef;
    const giveUp = () => {
        img._gen = (img._gen || 0) + 1;
        img._isUpdating = false;
        img.removeAttribute('data-frame');
        img.style.display = 'none';
    };
    if (!b || !img.isConnected) return giveUp();
    if (img._rec && Date.now() - img._rec < 60000) return giveUp();
    img._rec = Date.now();
    requestProfile(b).then(h => {
        const info = h ? parseProfile(h, urlSite(b.url), modelSlug(b.url)) : null;
        const fr = info && info.status ? info.frame : '';
        if (!fr || !img.isConnected) return giveUp();
        b.frame = fr;
        const next = frameBust(fr);
        const tmp = new Image();
        tmp.onload = () => {
            if (!img.isConnected) return;
            img.setAttribute('data-frame', fr);
            img._u = next; b._lastImg = next; img._gm = null;
            img.src = next; img.style.display = 'block';
        };
        tmp.onerror = () => gmImgData(next, true).then(d => {
            if (d && img.isConnected) { img.setAttribute('data-frame', fr); img._u = next; b._lastImg = next; img.src = d; img.style.display = 'block'; }
            else giveUp();
        });
        tmp.src = next;
    }).catch(giveUp);
};

const paintViewers = (stats, txt) => {
    const key = txt + '|' + AppState.lang;
    if (stats._last === key) return;
    const n = parseCount(txt);
    const shown = Number.isFinite(n) ? fmtCount(n) : txt;
    const sp = stats.querySelector('span');
    if (sp && stats.querySelector('svg')) sp.textContent = shown;
    else stats.innerHTML = `${icons.eye}<span>${escHTML(shown)}</span>`;
    stats.title = Number.isFinite(n) ? n.toLocaleString() : '';
    stats._last = key;
};
const needsExtra = (x, now) => isObj(x) && isStr(x.url) && isSafeUrl(x.url) && urlSite(x.url) === 'bc' && (!x._pf || (!x.sch && now - x._pf > 600000));
const frameThrottled = (img, b) => {
    if (!b || b.status !== 2) return false;
    img._skip = (img._skip || 0) + 1;
    return img._skip % 5 !== 0;
};
const paintStatusDot = (stat, st) => {
    const col = (SK(STATUS_COLORS, STATUS_COLORS_CLASSIC))[st] || '#888';
    stat.style.background = col;
    stat.style.boxShadow = st ? `0 0 8px ${col}` : '0 0 4px rgba(0,0,0,0.5)';
};
const paintViewsBadge = (viewEl, b) => {
    const vw = bmViews(b), vwTxt = vw ? fmtCount(vw) : '';
    if (viewEl._t === vwTxt) return;
    viewEl._t = vwTxt;
    viewEl.innerHTML = vwTxt ? `${icons.eye}${vwTxt}` : '';
};
const refreshFrame = img => {
    const url = img?.getAttribute('data-frame');
    if (!url || img._isUpdating) return;
    const b = img._bRef;
    if (frameThrottled(img, b)) return;
    img._isUpdating = true;
    const gen = img._gen = (img._gen || 0) + 1;
    const next = frameBust(url);
    const alive = () => img.isConnected && img._gen === gen && img.getAttribute('data-frame') === url;
    const done = () => { if (img._gen === gen) img._isUpdating = false; };
    const show = src => {
        img._u = next;
        if (b) b._lastImg = next;
        img._k = (b && b._lastData ? 'd' : '') + next;
        img.src = src;
        img.style.display = 'block';
    };
    const viaGm = () => gmImgData(next, true).then(d => {
        if (d && alive()) {
            if (b) b._lastData = d;
            if (d !== img._lastD) { img._lastD = d; show(d); }
        } else if (alive() && !(img.complete && img.naturalWidth > 0)) frameFail(img);
        done();
    });
    if (img._gmOnly) return viaGm();
    const tmp = new Image();
    const guard = setTimeout(() => { tmp.onload = tmp.onerror = null; done(); }, 20000);
    tmp.onload = () => {
        clearTimeout(guard);
        if (alive()) { if (b) b._lastData = ''; img._lastD = ''; show(next); }
        done();
    };
    tmp.onerror = () => { clearTimeout(guard); img._gmOnly = true; viaGm(); };
    tmp.src = next;
};

const initialsAva = name => {
    const s = String(name || '?'), t = (s.replace(/[^\p{L}\p{N}]/gu, '').slice(0, 2) || '?').toUpperCase();
    let h = 0; for (const ch of s) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
    const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'><rect width='64' height='64' fill='hsl(${h % 360},50%,42%)'/><text x='32' y='41' font-size='26' font-weight='700' text-anchor='middle' fill='#fff' font-family='sans-serif'>${t}</text></svg>`;
    return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
};
const bindAvatar = (img, name, getList, onFail) => {
    let i = 0, list = null;
    const next = () => {
        list ||= [...new Set(getList().filter(Boolean))];
        while (i < list.length) {
            const u = list[i++];
            if (u) { img._cur = u; img.src = u; return; }
        }
        img.onerror = null;
        img._fb = 1;
        img.src = initialsAva(name);
        onFail?.();
    };
    img._fb = 0;
    img.onerror = () => { if (img._fb === 0 && tryGm(img, img._cur, next)) return; next(); };
    next();
};

const STATUS_COLORS = { 0: '#ef4444', 1: '#10b981', 2: '#f59e0b', 3: '#8b5cf6' };
const STATUS_COLORS_CLASSIC = { 0: '#e74c3c', 1: '#2ecc71', 2: '#f39c12', 3: '#9b59b6' };
const bmLive = b => +((b.status ?? 0) > 0);
const bmGroups = () => [...new Set(AppState.bookmarks.map(b => b.g).filter(Boolean))].sort((a, b) => a.localeCompare(b));
const bmLabel = (b, l) => b.name + (b.lk && l[b.lk] ? ` (${l[b.lk]})` : '');
const bmSeen = (b, st) => st === 0 && b.seen ? fmtAgo(b.seen) : '';
const bmViews = b => (b.status > 0 && b.vts && Date.now() - b.vts < 7200000) ? (b.viewers || 0) : 0;
const WMARKS = [[0, 'eyeNo', 'w_new'], [1, 'eyeOk', 'w_seen']];
const bmWatch = (b, hist) => (((hist || histLoad()).m[bmKey(b.url)]?.s || 0) >= 30 ? 1 : 0);
const BM_CMP = {
    date: (x, y) => x.i - y.i,
    name: (x, y) => x.b.name.localeCompare(y.b.name),
    online: (x, y) => (bmLive(y.b) - bmLive(x.b)) || (x.i - y.i),
    last: (x, y) => (bmLive(y.b) - bmLive(x.b)) || ((y.b.seen || 0) - (x.b.seen || 0)) || (x.i - y.i),
    viewers: (x, y) => (bmLive(y.b) - bmLive(x.b)) || (bmViews(y.b) - bmViews(x.b)) || (x.i - y.i)
};

const ST_FILTERS = [
    ['hidePrivate', 'bc_bm_hide_priv', 'st_private', 'bm_priv'],
    ['hideGroup', 'bc_bm_hide_group', 'st_group', 'st_group'],
    ['hideVip', 'bc_bm_hide_vip', 'st_vip', 'st_vip'],
    ['hideAway', 'bc_bm_hide_away', 'st_away', 'st_away']
];
const bmStatusKey = b => {
    const st = b.status ?? 0;
    return b.lk || (st === 3 ? 'st_private' : st === 2 ? 'st_away' : '');
};

class BookmarkManager {
    constructor(uiController) {
        this.ui = uiController;
        this.overlay = null;
        this.list = null;
        if (!Store.get('bc_mig_stf1', false)) {
            if (Store.get('bc_bm_hide_priv', false)) { Store.set('bc_bm_hide_group', true); Store.set('bc_bm_hide_vip', true); }
            Store.set('bc_mig_stf1', true);
        }
        ST_FILTERS.forEach(([prop, key]) => { this[prop] = Store.get(key, false); });
        this.hideOffline = Store.get('bc_bm_hide_off', false);
        { const wf = Store.get('bc_bm_wf', -1); this.wFilter = [0, 1].includes(wf) ? wf : null; }
        this._els = new WeakMap();
        this._lang = AppState.lang;
        this.build();
        this._xTimer = setInterval(() => { this.bgExtra(); }, 8000);
        this._statusTimer = setInterval(() => {
            if (!realHidden() && this.isOpen()) this.checkStatuses();
        }, 20000);
        onPageEvent('visibilitychange', () => {
            if (!realHidden() && this.isOpen()) this.checkStatuses();
        });
    }

    applyRemote(json) {
        try {
            const arr = JSON.parse(json);
            if (!Array.isArray(arr)) return;
            const editing = this.ui.shadow.activeElement?.matches?.('textarea');
            if (!mergeRemoteBookmarks(arr, editing ? ['note'] : [])) return;
            if (this.isOpen() && !editing) this.render();
        } catch {}
    }

    async checkStatuses() {
        if (!AppState.bookmarks?.length) return;
        if (!this._abortCtrl || this._abortCtrl.signal.aborted) this._abortCtrl = new AbortController();
        const { signal } = this._abortCtrl;
        const runs = this._runs || (this._runs = new Set());
        const busy = this._busy || (this._busy = new Set());
        const sortType = Store.get('bc_bm_sort_type', 'date');
        const now = Date.now();
        const queue = AppState.bookmarks.filter(b => isSafeUrl(b.url) && !busy.has(b) && (!b._chk || now - b._chk > 15000))
            .sort((x, y) => (y.status > 0) - (x.status > 0) || (x._chk || 0) - (y._chk || 0));
        if (queue.length) {
            let changedAny = false;
            const mark = () => { changedAny = true; };
            this._vp = this.prefetchViewers(queue, signal);
            const worker = async () => {
                while (queue.length && !signal.aborted) {
                    while (bcPtrDown && !signal.aborted) await new Promise(r => setTimeout(r, 150));
                    if (signal.aborted) break;
                    const b = queue.shift();
                    if (!b) break;
                    busy.add(b);
                    try { await this.refreshOne(b, signal, sortType, mark); } catch {}
                    busy.delete(b);
                }
            };
            const run = Promise.all(Array.from({ length: Math.min(4, queue.length) }, worker)).then(() => { if (changedAny) saveBookmarks(); });
            runs.add(run);
            run.finally(() => runs.delete(run));
        }
        await Promise.all([...runs]);
    }

    async loadExtra(b, signal) {
        const pu = bcProfileUrl(b);
        if (!pu) return {};
        for (const hd of [{ 'User-Agent': MOBILE_UA }, null]) {
            const h = await requestOnce(pu, hd, signal);
            if (signal?.aborted) break;
            const ex = h ? parseBcExtra(h) : {};
            if (ex.sch || ex.ptags) return ex;
        }
        return {};
    }

    async bgExtra(force) {
        if (this._xBusy || (!force && (realHidden() || bcPtrDown))) return 'busy';
        const now = Date.now();
        const b = AppState.bookmarks.find(x => needsExtra(x, now));
        if (!b) return 'none';
        this._xBusy = true;
        b._pf = now;
        let changed = false;
        try {
            const ex = await this.loadExtra(b);
            if (applyProfile(b, ex)) {
                changed = true;
                saveBookmarks();
                if (this.isOpen() && this.mode === 'fav') this.render();
            }
        } catch {}
        this._xBusy = false;
        return changed;
    }

    async prefetchViewers(queue, signal) {
        this._vmap = this._vmap || new Map();
        this._vx = this._vx || new Map();
        const origins = new Set(queue.filter(b => urlSite(b.url) === 'bc').map(b => originOf(b.url)).filter(Boolean));
        origins.forEach(o => this._vx.delete(o));
        await Promise.all([...origins].map(async o => {
            try { this._vmap.set(o, new Map(await bcListTab(o, 'female', signal))); } catch {}
        }));
    }

    async viewersFor(b, signal) {
        await this._vp;
        const o = originOf(b.url), slug = String(modelSlug(b.url) || '').toLowerCase();
        const maps = this._vmap;
        if (!maps || !o || !slug) return null;
        const own = () => maps.get(o)?.get(slug);
        if (own() != null) return own();
        if (!this._vx.has(o)) {
            this._vx.set(o, Promise.all(BC_EXTRA_TABS.map(t => bcListTab(o, t, signal).catch(() => new Map()))).then(ms => {
                const base = maps.get(o) || new Map();
                ms.forEach(m => m.forEach((v, k) => { if (!base.has(k)) base.set(k, v); }));
                maps.set(o, base);
            }).catch(() => {}));
        }
        await this._vx.get(o);
        return own() ?? null;
    }

    async refreshOne(b, signal, sortType, mark) {
        const h = await requestProfile(b, signal);
        if (signal.aborted || !h) return;
        const prevChk = b._chk, prevMob = b._mob;
        b._chk = Date.now();
        const info = await this.enrichInfo(b, h, signal);
        if (signal.aborted) { b._chk = prevChk; b._mob = prevMob; return; }
        if (info.status > 0 && !info.unk) this.touchSeen(b, mark);
        if (!applyProfile(b, info)) return;
        mark();
        this.onStatusChange(b, sortType);
    }

    async enrichInfo(b, h, signal) {
        const info = parseProfile(h, urlSite(b.url), modelSlug(b.url));
        if (info.unk) {
            const keep = Number.isFinite(+b.status) && (!b._kt || Date.now() - b._kt < PD_STALE_MS);
            if (keep) { info.status = +b.status; delete info.lk; }
            delete info.topic; delete info.frame;
        } else b._kt = Date.now();
        if (urlSite(b.url) === 'bc' && info.status > 0) {
            const vn = await this.viewersFor(b, signal);
            if (vn != null) info.viewers = vn;
        }
        if (this.needsExtra(b, info)) {
            b._mob = Date.now();
            Object.assign(info, await this.loadExtra(b, signal));
        }
        if (info.status === 0 && b._live && Date.now() - b._live < 90000) info.status = 1;
        return info;
    }

    needsExtra(b, info) {
        return urlSite(b.url) === 'bc' && !info.sch && (!b._mob || Date.now() - b._mob > 600000);
    }

    touchSeen(b, mark) {
        b.seen = Date.now();
        if (b._sv && b.seen - b._sv <= 300000) return;
        b._sv = b.seen;
        mark();
    }

    isOpen() { return !!this.overlay?.classList.contains('open'); }

    onStatusChange(b, sortType) {
        if (!this.isOpen()) return;
        b._updateItemDOM?.();
        if (RESORT_SORTS.has(sortType) || this.hideOffline || ST_FILTERS.some(([prop]) => this[prop])) {
            clearTimeout(this._resortTimer);
            const tick = () => {
                if (!this.overlay.classList.contains('open') || this.mode !== 'fav') return;
                if (this.ui.shadow.activeElement?.matches?.('textarea,select')) { this._resortTimer = setTimeout(tick, 1000); return; }
                this.render();
            };
            this._resortTimer = setTimeout(tick, 600);
        }
    }

    build() {
        this.buildOverlay();
        const modal = mk('div', 'bm-modal fav-modal');
        this.modal = modal;
        this.mode = 'fav';
        const hdr = mk('div', 'bm-hdr');
        this.buildTabs();
        const closeBtn = mk('button', 'bm-close');
        closeBtn.innerHTML = '×';
        closeBtn.onclick = () => this.hide();
        const { btnTogglePrev, btnSpeed } = this.buildPreviewControls();
        const btnFavNav = this.buildFavNavSwitch();
        const hideSw = this.buildHideSwitches(modal);
        const sortWrap = this.buildSortControls();
        const btnCols = this.buildColsButton();
        const btnAv = this.buildAvatarRefresh();
        const setPanel = this.buildSettingsPanel({ btnFavNav, btnSpeed, btnAv, ...hideSw });
        const btnGear = mk('button', 'bm-hbtn bm-gear');
        btnGear.innerHTML = icons.gear;
        bindGear(btnGear, setPanel);
        const tools = mk('div', 'bm-tools');
        this.wsegHost = mk('div', 'bm-wseg-host');
        tools.append(this.wsegHost, btnTogglePrev, btnCols, btnGear);
        hdr.append(this.title, closeBtn);
        const tbar = mk('div', 'bm-tbar');
        tbar.append(tools, setPanel);

        this.buildSearch(sortWrap);
        this.tagsWrap = mk('div', 'bm-tags-wrap');
        this.list = mk('div', 'bm-list');
        this.applyCols?.();
        this.bindHostEvents();
        const foot = mk('div', 'bm-foot');
        this.btnAdd = mk('button', 'bm-add');
        this.btnAdd.onclick = () => this.add();

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

    buildOverlay() {
        this.overlay = mk('div', 'bm-overlay fav-ov');
        this.overlay.onclick = e => { if (e.target === this.overlay && Date.now() - (this._opAt || 0) > 500) this.hide(); };
        this.overlay.addEventListener('click', e => {
            if (e.timeStamp - (this._opPerf || 0) < 400) { e.preventDefault(); e.stopPropagation(); }
        }, true);
        this.overlay.addEventListener('touchmove', e => {
            if (this.onSlider(e) || this.hasScrollableAncestor(e)) return;
            if (e.cancelable) e.preventDefault();
        }, { passive: false });

    }

    isScrollable(n) {
        const cs = getComputedStyle(n);
        return (/(auto|scroll)/.test(cs.overflowY) && n.scrollHeight > n.clientHeight + 1)
            || (/(auto|scroll)/.test(cs.overflowX) && n.scrollWidth > n.clientWidth + 1);
    }

    onSlider(e) {
        return e.composedPath().some(n => n instanceof Element && n.matches('input[type=range],.mv-sk'));
    }

    hasScrollableAncestor(e) {
        for (const n of e.composedPath()) {
            if (n === this.overlay) return false;
            if (n instanceof Element && this.isScrollable(n)) return true;
        }
        return false;
    }

    buildTabs() {
        this.title = mk('span', 'bm-title');
        this.tabFav = mk('button', 'bm-tbtn on');
        this.tabHist = mk('button', 'bm-tbtn');
        this.tabFav.onclick = () => this.setMode('fav');
        this.tabHist.onclick = () => this.setMode('hist');
        const tabs = mk('div', 'bm-seg bm-tabs');
        this.tabCal = mk('button', 'bm-tbtn');
        this.tabCal.onclick = () => this.setMode('cal');
        this.tabMv = mk('button', 'bm-tbtn');
        this.tabMv.onclick = () => this.setMode('mv');
        tabs.append(this.tabFav, this.tabMv, this.tabHist, this.tabCal);
        this.title.append(tabs);

    }

    buildPreviewControls() {
        const btnTogglePrev = mk('button', 'bm-hbtn bm-prevbtn');
        const btnSpeed = mk('button', 'bm-hbtn bm-spd');
        btnSpeed.style.fontWeight = 'bold';
        if (!Store.get('bc_mig_prev1', false)) { Store.set('bc_previews', true); Store.set('bc_mig_prev1', true); }
        this.prevEn = Store.get('bc_previews', true) !== false;
        this.updatePrevUI = () => {
            btnTogglePrev.classList.toggle('on', !!this.prevEn);
            btnTogglePrev.innerHTML = this.prevEn ? icons.eye : icons.eyeOff;
            btnTogglePrev.title = L().st_prev;
            btnSpeed.disabled = !this.prevEn;
            btnSpeed.style.opacity = this.prevEn ? '' : '.4';
            btnSpeed.textContent = prevSpdLabel(prevSpd());
            btnSpeed.title = prevSpdTitle();
        };
        this.updatePrevUI();
        btnTogglePrev.onclick = () => {
            this.prevEn = !this.prevEn;
            Store.set('bc_previews', this.prevEn);
            this.updatePrevUI();
            this.ui.multiView?.syncPrev();
            this.render();
        };
        btnSpeed.onclick = () => {
            Store.set('bc_prev_spd', prevSpdNext());
            this.updatePrevUI();
            this.stopPreviews();
            this.render();
        };
        return { btnTogglePrev, btnSpeed };
    }

    buildFavNavSwitch() {
        const btnFavNav = mk('button', 'bm-sw');
        this.updateFavNavUI = () => {
            btnFavNav.classList.toggle('on', FavNav.on);
        };
        this.updateFavNavUI();
        btnFavNav.onclick = () => { FavNav.set(!FavNav.on); this.updateFavNavUI(); };
        return btnFavNav;
    }

    buildHideSwitches(modal) {
        const btnHideTopic = mk('button', 'bm-sw');
        const btnHideGrp = mk('button', 'bm-sw');
        const btnHidePT = mk('button', 'bm-sw');
        const btnHideNote = mk('button', 'bm-sw');
        const applyHide = () => {
            const t = Store.get('bc_bm_hide_topic', true) !== false;
            const g = Store.get('bc_bm_hide_grp', true) !== false;
            btnHideTopic.classList.toggle('on', t);
            btnHideGrp.classList.toggle('on', g);
            modal.classList.toggle('bm-no-topic', t);
            modal.classList.toggle('bm-no-grp', g);
            const pt = Store.get('bc_bm_hide_ptags', true) !== false;
            btnHidePT.classList.toggle('on', pt);
            modal.classList.toggle('bm-no-ptags', pt);
            const hn = Store.get('bc_bm_hide_note', true) !== false;
            btnHideNote.classList.toggle('on', hn);
            modal.classList.toggle('bm-no-note', hn);
        };
        applyHide();
        btnHideTopic.onclick = () => { Store.set('bc_bm_hide_topic', !(Store.get('bc_bm_hide_topic', true) !== false)); applyHide(); };
        btnHideGrp.onclick = () => { Store.set('bc_bm_hide_grp', !(Store.get('bc_bm_hide_grp', true) !== false)); applyHide(); };
        btnHideNote.onclick = () => { Store.set('bc_bm_hide_note', !(Store.get('bc_bm_hide_note', true) !== false)); applyHide(); };
        btnHidePT.onclick = () => { Store.set('bc_bm_hide_ptags', !(Store.get('bc_bm_hide_ptags', true) !== false)); applyHide(); this.render(); };
        return { btnHideTopic, btnHideGrp, btnHidePT, btnHideNote };
    }

    buildSortControls() {
        const sortWrap = mk('div', 'bm-sort-wrap');
        this.btnSortType = mk('button', 'bm-hbtn');
        this.btnSortDir = mk('button', 'bm-hbtn');

        this.updateSortUI = () => {
            const type = Store.get('bc_bm_sort_type', 'date');
            const isRev = Store.get('bc_bm_sort_rev', false);
            const l = L();
            const map = { date: [icons.clock, l.sort_date], name: [icons.name, l.sort_name], online: [icons.dot, l.sort_live], last: [icons.eye, l.sort_last], viewers: [icons.eye, l.sort_viewers] };
            const [icon, label] = map[type] || map.date;
            this.btnSortType.innerHTML = icon + label;
            this.btnSortDir.innerHTML = isRev ? icons.arrowUp : icons.arrowDown;
        };

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

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

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

    buildColsButton() {
        const btnCols = mk('button', 'bm-hbtn bm-colsbtn');
        const colsIcons = COLS_ICONS;
        const colsKey = COLS_KEY;
        this.applyCols = () => {
            const n = Store.get(colsKey, 1) === 2 ? 2 : 1;
            this.list.style.setProperty('grid-template-columns', `repeat(${n},minmax(0,1fr))`, 'important');
            this.list.classList.toggle('bm-c1', n === 1);
            btnCols.innerHTML = colsIcons[n - 1];
            btnCols.title = L().st_cols;
        };
        btnCols.onclick = () => {
            Store.set(colsKey, Store.get(colsKey, 1) === 2 ? 1 : 2);
            this.applyCols();
        };
        return btnCols;
    }

    buildAvatarRefresh() {
        const btnAv = mk('button', 'bm-hbtn bm-spd');
        btnAv.textContent = '↻';
        let avBusy = false;
        btnAv.onclick = async () => {
            if (avBusy) return;
            avBusy = true;
            btnAv.disabled = true;
            const queue = AppState.bookmarks.slice(), total = queue.length;
            let done = 0;
            const upd = () => { btnAv.textContent = `${done}/${total}`; };
            upd();
            const worker = async () => {
                while (queue.length) {
                    const b = queue.shift();
                    try { await this.fetchAvatar(b); } catch {}
                    done++;
                    upd();
                }
            };
            await Promise.all([worker(), worker(), worker()]);
            btnAv.textContent = '✓';
            setTimeout(() => { btnAv.textContent = '↻'; btnAv.disabled = false; avBusy = false; }, 1500);
        };
        return btnAv;
    }

    buildSettingsPanel({ btnFavNav, btnHideTopic, btnHideGrp, btnHidePT, btnHideNote, btnSpeed, btnAv }) {
        const row = mkSetRow;
        const setPanel = mk('div', 'bm-setp');
        setPanel.append(
            row(L().st_favnav, btnFavNav),
            row(L().st_hide_topic, btnHideTopic),
            row(L().st_hide_grp, btnHideGrp),
            row(L().st_ptags || i18n.EN.st_ptags, btnHidePT),
            row(L().st_hide_note || i18n.EN.st_hide_note, btnHideNote),
            row(L().st_prev_spd, btnSpeed),
            row(L().st_av_refresh, btnAv)
        );
        return setPanel;
    }

    buildSearch(sortWrap) {
        this.query = '';
        this.searchWrap = mk('div', 'bm-search-wrap');
        const sBox = mk('div', 'bm-search-box');
        const sIco = mk('span', 'bm-search-ico');
        sIco.innerHTML = icons.search;
        this.searchIn = mk('input', 'bm-search-in');
        this.searchIn.type = 'text';
        this.searchIn.enterKeyHint = 'search';
        this.searchIn.autocomplete = 'off';
        this.searchIn.spellcheck = false;
        this.searchIn.setAttribute('autocapitalize', 'off');
        this.searchClr = mk('button', 'bm-search-clr');
        this.searchClr.textContent = '×';
        let sTimer = 0;
        const onQuery = () => {
            const v = this.searchIn.value;
            this.query = v.trim().toLowerCase();
            this.searchClr.style.display = v ? 'flex' : 'none';
            clearTimeout(sTimer);
            sTimer = setTimeout(() => this.render(), 120);
        };
        this.searchIn.oninput = onQuery;
        this.searchIn.onkeydown = e => {
            e.stopPropagation();
            if (e.key === 'Escape' && this.searchIn.value) { e.preventDefault(); this.searchIn.value = ''; onQuery(); }
        };
        this.searchClr.onclick = () => { this.searchIn.value = ''; onQuery(); this.searchIn.focus(); };
        sBox.append(sIco, this.searchIn, this.searchClr);
        this.searchWrap.append(sBox, sortWrap);

    }

    bindHostEvents() {
        window.addEventListener('bc_topic', e => {
            try {
                const t = String(JSON.parse(e.detail).t || '').trim(), nm = String(currentSite.getName() || '').toLowerCase();
                if (!nm || !t) return;
                AppState.bookmarks.forEach(b => {
                    if (String(b.name).toLowerCase() !== nm || urlSite(b.url) !== siteKey(location.hostname) || !(b.status > 0)) return;
                    if (applyProfile(b, { topic: t })) b._updateItemDOM?.();
                });
            } catch {}
        });
        window.addEventListener('bc_viewers', e => {
            try {
                const d = JSON.parse(e.detail), nm = String(currentSite.getName() || '').toLowerCase();
                if (!nm || !(d.n >= 0)) return;
                AppState.bookmarks.forEach(b => {
                    if (String(b.name).toLowerCase() !== nm || urlSite(b.url) !== siteKey(location.hostname)) return;
                    if (applyProfile(b, { viewers: d.n })) b._updateItemDOM?.();
                });
            } catch {}
        });
        window.addEventListener('bc_hls', () => {
            if (this.isOpen()) this.list.querySelectorAll('.bm-item').forEach(i => i._syncHls?.());
        });
        this._observer = new IntersectionObserver(entries => entries.forEach(e => e.target._inView = e.isIntersecting), { root: this.list, threshold: 0.05 });

    }

    hide() {
        this.overlay.classList.remove('open');
        this.ui.multiView?.suspend();
        clearTimeout(this._resortTimer);
        this.stopPreviews?.();
        try { this._abortCtrl?.abort(); } catch {}
    }

    enterMode(mode, fromOpen) {
        if (mode === 'fav') {
            if (!fromOpen) { this.render(); this.checkStatuses(); }
            return;
        }
        this.stopPreviews?.();
        const manager = { hist: 'historyManager', cal: 'calendarManager', mv: 'multiView' }[mode];
        this.ui[manager]?.show();
    }

    setMode(m, fromOpen) {
        const mode = this.mode = ['hist', 'cal', 'mv'].includes(m) ? m : 'fav';
        for (const k of ['hist', 'cal', 'mv']) this.modal.classList.toggle(`mode-${k}`, mode === k);
        const tabs = { fav: this.tabFav, hist: this.tabHist, cal: this.tabCal, mv: this.tabMv };
        for (const k of Object.keys(tabs)) tabs[k].classList.toggle('on', mode === k);
        if (mode !== 'mv') this.ui.multiView?.suspend();
        this.enterMode(mode, fromOpen);
    }

    updateLabels() {
        this.tabFav.innerHTML = `${icons.star}<span class="bm-tl">${escHTML(L().bm_title)}</span>`;
        this.tabHist.innerHTML = `${icons.clock}<span class="bm-tl">${escHTML(L().hist_title)}</span>`;
        this.tabCal.innerHTML = `${icons.cal}<span class="bm-tl">${escHTML(L().cal_title)}</span>`;
        this.tabMv.innerHTML = `${icons.grid}<span class="bm-tl">${escHTML(mvT('mv_title'))}</span>`;
        this.tabMv.title = mvT('mv_title');
        if (this.ui.calendarManager?.isOpen()) this.ui.calendarManager.render();
        this.searchIn.placeholder = L().bm_find;
        this.updateSortUI?.(); this.updatePrevUI?.(); this.applyCols?.();
    }

    open(mode) {
        syncBookmarks();
        this.updateLabels();
        this.query = '';
        this.searchIn.value = '';
        this.searchClr.style.display = 'none';
        this.setMode(mode, true);
        if (this.mode === 'fav') {
            this.render();
            this.list.scrollTop = 0;
            this.checkStatuses();
        }
        this._opAt = Date.now();
        this._opPerf = performance.now();
        this.overlay.classList.add('open');
    }

    statusSeg(l) {
        const toggle = (prop, key) => () => { this[prop] = !this[prop]; Store.set(key, this[prop]); this.render(); };
        const seg = mk('div', 'bm-seg');
        seg.append(
            bmTbtn(l.bm_online, this.hideOffline, toggle('hideOffline', 'bc_bm_hide_off'), l.bm_online_t),
            ...ST_FILTERS.map(([prop, key, lk, label]) => bmTbtn(l[label], this[prop], toggle(prop, key), l[lk]))
        );
        return seg;
    }

    watchSeg(l) {
        const setW = v => () => { this.wFilter = this.wFilter === v ? null : v; Store.set('bc_bm_wf', this.wFilter == null ? -1 : this.wFilter); this.render(); };
        const wseg = mk('div', 'bm-seg bm-wseg');
        wseg.append(
            ...WMARKS.map(([v, ic, key]) => bmTbtn(icons[ic], this.wFilter === v, setW(v), l[key]))
        );
        return wseg;
    }

    appendGroupChips(chips, l) {
        const groups = bmGroups();
        if (this.currentGroup && !groups.includes(this.currentGroup)) this.currentGroup = null;
        if (!groups.length) return;
        const setGrp = g => () => { this.currentGroup = this.currentGroup === g ? null : g; this.render(); };
        chips.append(
            bmTbtn(`${icons.folder}${escHTML(l.grp_all)}`, !this.currentGroup, () => { this.currentGroup = null; this.render(); }),
            ...groups.map(g => bmTbtn(`${icons.folder}${escHTML(g)}`, this.currentGroup === g, setGrp(g)))
        );
    }

    topPtags(hidePT, ptc) {
        if (hidePT) return [];
        return [...ptc.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, 30).map(e => e[0]);
    }

    appendTagChips(chips, l) {
        const allTags = new Set(AppState.bookmarks.flatMap(b => noteHashTags(b).map(t => t.toLowerCase())));
        const ptc = new Map();
        AppState.bookmarks.forEach(b => (b.ptags || []).forEach(t => countInto(ptc, t)));
        const hidePT = Store.get('bc_bm_hide_ptags', true) !== false;
        const ptop = this.topPtags(hidePT, ptc);
        if (this.currentTagFilter && !allTags.has(this.currentTagFilter) && (hidePT || !ptc.has(this.currentTagFilter))) this.currentTagFilter = null;
        if (!allTags.size && !ptop.length) { this.currentTagFilter = null; return; }
        const setTag = tag => () => { this.currentTagFilter = this.currentTagFilter === tag ? null : tag; this.render(); };
        const pim = ptIcoMap();
        chips.append(
            bmTbtn(l.bm_all, !this.currentTagFilter, () => { this.currentTagFilter = null; this.render(); }),
            ...[...allTags].sort().map(tag => bmTbtn(tag, this.currentTagFilter === tag, setTag(tag))),
            ...(allTags.size && ptop.length ? [mk('span', 'bm-chips-sep')] : []),
            ...ptop.map(tag => bmTbtn(`${ptImg(pim.get(tag), 'bm-ptic') || icons.tag}${escHTML(tag)}`, this.currentTagFilter === tag, setTag(tag)))
        );
    }

    renderTags() {
        if (!this.tagsWrap) return;
        const l = L();
        const chips = mk('div', 'bm-chips');
        this.tagsWrap.replaceChildren(this.statusSeg(l));
        this.wsegHost && this.wsegHost.replaceChildren(this.watchSeg(l));
        this.tagsWrap.style.display = 'flex';
        this.appendGroupChips(chips, l);
        this.appendTagChips(chips, l);
        if (chips.children.length) {
            this.tagsWrap.append(chips);
            const sx = this._chipsX || 0;
            chips.scrollLeft = sx;
            requestAnimationFrame(() => { if (chips.isConnected && chips.scrollLeft !== sx) chips.scrollLeft = sx; });
            chips.addEventListener('scroll', () => { this._chipsX = chips.scrollLeft; }, { passive: true });
        }
    }

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

        const { img, applyAva } = this.itemAvatar(b);
        const stat = mk('span', 'bm-stat');
        const nameWrap = mk('div', 'bm-nwrap');
        const nameEl = mk('span', 'bm-name');
        nameEl.textContent = b.name;
        const seenEl = mk('span', 'bm-seen');
        const viewEl = mk('span', 'bm-view');
        nameWrap.append(stat, nameEl, viewEl, seenEl);
        link.append(img, nameWrap);
        this.bindItemFrameHold(item, b);
        img.draggable = false;
        img.style.webkitTouchCallout = 'none';
        this.bindNameCopyHold(nameWrap, nameEl, b);

        const { pinBtn, delBtn } = this.itemPinDel(b);
        top.append(link, pinBtn, delBtn);
        return { top, link, img, stat, nameEl, seenEl, viewEl, applyAva, pinBtn, delBtn };
    }

    itemStatusFn(item, b, p) {
        const { link, img, stat, nameEl, seenEl, viewEl, applyAva, pinBtn, updateTopicDOM, applyPT, applyPreview } = p;
        const applyW = () => { item.dataset.w = String(bmWatch(b, this._hist || (this._hist = histLoad()))); };
        return () => {
            const l = L(), st = b.status ?? 0;
            link.href = isSafeUrl(b.url) ? linkFor(b.url) : '#';
            applyAva();
            img.title = l.fr_hold;
            item.classList.toggle('pinned', !!b.pin);
            pinBtn.classList.toggle('on', !!b.pin);
            pinBtn.title = b.pin ? l.bm_unpin : l.bm_pin;
            item.dataset.st = String(st);
            paintStatusDot(stat, st);
            nameEl.textContent = bmLabel(b, l);
            seenEl.textContent = bmSeen(b, st);
            seenEl.title = l.seen_tip;
            paintViewsBadge(viewEl, b);
            viewEl.title = l.viewers_tip;
            updateTopicDOM(b.topic || '');
            applyW();
            applyPT();
            applyPreview(st);
        };
    }

    renderItem(b) {
        const cached = this._els.get(b);
        if (cached) {
            cached.fillGrp();
            cached.apply();
            cached.item._syncNote?.();
            cached.item._syncHls?.();
            b._updateItemDOM = cached.apply;
            return cached.item;
        }
        const item = mk('div', 'bm-item');
        const { top, link, img, stat, nameEl, seenEl, viewEl, applyAva, pinBtn } = this.itemTopRow(item, b);

        const { actionsRow, syncHls } = this.itemActions(b);
        item._syncHls = syncHls;
        item._syncHls();

        const { noteInput, autoResize, syncNote } = this.itemNote(b);
        item._syncNote = syncNote;
        const { topicEl, updateTopicDOM } = this.itemTopic(b);
        const { grpSel, fillGrp } = this.itemGroup(b);
        const { ptagsEl, applyPT } = this.itemPtags(b);
        const { previewImg, applyPreview } = this.itemPreview(b);

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

        const applyStatus = this.itemStatusFn(item, b, { link, img, stat, nameEl, seenEl, viewEl, applyAva, pinBtn, updateTopicDOM, applyPT, applyPreview });
        applyStatus();
        b._updateItemDOM = applyStatus;
        this._observer?.observe(item);
        this._els.set(b, { item, fillGrp, apply: applyStatus, resize: () => requestAnimationFrame(autoResize) });
        return item;
    }

    itemAvatar(b) {
        const img = mk('img', 'bm-avatar');
        const applyAva = () => {
            const k = b.avatar || '';
            if (img._k === k) return;
            img._k = k;
            bindAvatar(img, b.name, () => [
                toHttps(b.avatar),
                toHttps((this._hist || (this._hist = histLoad())).m[bmKey(b.url)]?.a)
            ], () => { if (!b._avFix) { b._avFix = 1; this.fetchAvatar(b); } });
        };
        applyAva();
        return { img, applyAva };
    }

    bindItemFrameHold(item, b) {
        Helpers.bindHold(item, {
            ok: e => !e.target.closest('button, textarea, select, input, a.tp-btn, .bm-nwrap'),
            onHold: () => openFrameView(this.ui, b)
        });
    }

    bindNameCopyHold(nameWrap, nameEl, b) {
        nameWrap.style.userSelect = nameWrap.style.webkitUserSelect = nameWrap.style.webkitTouchCallout = 'none';
        const flash = ok => {
            nameEl.textContent = ok ? '✓' : 'ERR';
            setTimeout(() => { nameEl.textContent = b.name; }, 800);
        };
        Helpers.bindHold(nameWrap, { always: true, onHold: () => Helpers.copy(b.name, () => flash(true), () => flash(false)) });
    }

    itemPinDel(b) {
        const delBtn = mk('button', 'bm-del');
        delBtn.innerHTML = icons.del;
        delBtn.title = L().bm_del;
        delBtn.onclick = e => {
            e.preventDefault(); e.stopPropagation();
            dropBookmark(b);
            this.render();
        };
        const pinBtn = mk('button', 'bm-pin');
        pinBtn.innerHTML = icons.star;
        pinBtn.onclick = e => {
            e.preventDefault(); e.stopPropagation();
            if (b.pin) delete b.pin; else b.pin = true;
            saveBookmarks();
            this.render();
        };
        return { pinBtn, delBtn };
    }

    itemActions(b) {
        const makeAct = (html, fn, onHold) => {
            const btn = mk('button', 'bm-act-btn');
            btn.innerHTML = html;
            if (onHold) Helpers.bindLongPress(btn, () => fn(btn), onHold);
            else btn.onclick = e => { e.preventDefault(); e.stopPropagation(); fn(btn); };
            return btn;
        };
        const onPage = () => String(b.name).toLowerCase() === String(currentSite.getName()).toLowerCase() && urlSite(b.url) === siteKey(location.hostname);
        const canHls = () => onPage() || !!MV_RES[urlSite(b.url)];
        const getHls = () => {
            const hit = BM_HLS.get(b.url);
            if (hit && Date.now() - hit.at < 45000) return hit.p;
            const p = Promise.resolve().then(async () => {
                if (onPage()) { const cur = getStreamUrl(); if (cur) return cur; }
                const r = await mvResolve({ t: 'bm', name: String(b.name), url: b.url });
                return (r && r.url) || '';
            }).catch(() => '');
            BM_HLS.set(b.url, { at: Date.now(), p });
            if (BM_HLS.size > 40) { const now = Date.now(); for (const [k, v] of BM_HLS) if (now - v.at > 45000) BM_HLS.delete(k); }
            p.then(u => { if (!u && BM_HLS.get(b.url)?.p === p) BM_HLS.delete(b.url); });
            return p;
        };
        const busy = (btn, on) => { btn.style.opacity = on ? '.55' : ''; };
        const actionsRow = mk('div', 'bm-actions-row bm-acts');
        actionsRow.append(
            makeAct(`${icons.search} <span>${L().search}</span>`, () => { noFade(this.overlay, this.ui.searchManager?.overlay); this.overlay.classList.remove('open'); this.ui.searchManager?.open(b.name, true); }),
            makeAct(`${icons.hls} <span>HLS</span>`,
                async btn => { busy(btn, true); const u = await getHls(); busy(btn, false); copyHls(u, ok => Helpers.flashBtn(btn, ok)); },
                async () => { const btn = actionsRow.children[1]; busy(btn, true); const u = await getHls(); busy(btn, false); shareHls(u, `${b.name} Stream`, () => Helpers.flashBtn(btn, false)); }
            ),
            makeAct(`${icons.copy} <span>${L().copy}</span>`, btn => {
                Helpers.copy(linkFor(b.url), () => Helpers.flashBtn(btn, true), () => Helpers.flashBtn(btn, false));
            })
        );
        const hlsBtn = actionsRow.children[1];
        hlsBtn.addEventListener('pointerdown', () => { if (canHls()) getHls(); });
        const syncHls = () => {
            hlsBtn.style.opacity = canHls() ? '' : '.4';
            hlsBtn.title = 'HLS';
        };
        return { actionsRow, syncHls };
    }

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

        let noteT = 0;
        noteInput.oninput = e => {
            requestAnimationFrame(autoResize);
            b.note = e.target.value.trim();
            clearTimeout(noteT);
            bmDirty = true; noteT = setTimeout(saveBookmarks, 400);
        };
        noteInput.onblur = () => { if (noteT) { clearTimeout(noteT); noteT = 0; saveBookmarks(); } };
        noteInput.onchange = () => { clearTimeout(noteT); noteT = 0; saveBookmarks(); this.ui.flash(this.btnAdd, 'OK'); this.render(); };
        noteInput.onkeydown = e => e.stopPropagation();
        return { noteInput, autoResize, syncNote };
    }

    itemGroup(b) {
        const grpSel = mk('button', 'bm-grp bm-grp-btn');
        grpSel.type = 'button';
        const groupList = () => {
            const gs = bmGroups();
            if (b.g && !gs.includes(b.g)) gs.push(b.g);
            return gs;
        };
        const fillGrp = () => {
            const label = b.g || L().grp_none;
            if (grpSel._lbl === label) return;
            grpSel._lbl = label;
            grpSel.innerHTML = `<span>${icons.folder}${escHTML(label)}</span><span class="bm-grp-arr">▾</span>`;
        };
        fillGrp();
        grpSel.onclick = e => {
            e.stopPropagation();
            openGroupDialog(this.ui, {
                current: b.g || '',
                groups: groupList(),
                onPick: v => { b.g = v; saveBookmarks(); this.render(); }
            });
        };
        grpSel.onkeydown = e => e.stopPropagation();
        return { grpSel, fillGrp };
    }

    itemTopic(b) {
        const topicEl = mk('div', 'bm-topic');
        const updateTopicDOM = txt => {
            const key = `${txt}|${!!b._topicExp}|${AppState.lang}`;
            if (topicEl._k === key) return;
            topicEl._k = key;
            if (!txt) { topicEl.style.display = 'none'; return; }
            topicEl.style.display = 'block';
            if (txt.length > 55) {
                const txtMore = L().tp_more;
                const txtLess = L().tp_less;
                topicEl.innerHTML = `<span>${escHTML(b._topicExp ? txt : txt.slice(0, 55) + '...')} </span><a class="tp-btn" style="color:var(--th);cursor:pointer;font-weight:bold;text-decoration:underline;">${escHTML(b._topicExp ? txtLess : txtMore)}</a>`;
                topicEl.querySelector('.tp-btn')?.addEventListener('click', ev => {
                    ev.preventDefault();
                    b._topicExp = !b._topicExp;
                    updateTopicDOM(txt);
                });
            } else topicEl.textContent = txt;
        };
        return { topicEl, updateTopicDOM };
    }

    itemPtags(b) {
        const ptagsEl = mk('div', 'bm-ptags');
        const applyPT = () => {
            const tg = Array.isArray(b.ptags) ? b.ptags : [], cf = this.currentTagFilter || '';
            const key = `${tg.join('|')}#${(Array.isArray(b.ptic) ? b.ptic : []).join('|')}#1`;
            const syncOn = () => ptagsEl.querySelectorAll('.bm-ptag[data-t]').forEach(c => c.classList.toggle('on', c.dataset.t === cf));
            if (ptagsEl._k === key) { syncOn(); return; }
            ptagsEl._k = key;
            ptagsEl.replaceChildren();
            ptagsEl.style.display = tg.length ? 'flex' : 'none';
            const max = 3;
            tg.slice(0, max).forEach((t, ix) => ptagsEl.append(this.ptagChip(b, t, ix, cf)));
            if (tg.length > max) { const more = mk('span', 'bm-ptag more'); more.textContent = `+${tg.length - max}`; ptagsEl.append(more); }
        };
        return { ptagsEl, applyPT };
    }

    ptagChip(b, t, ix, cf) {
        const c = mk('button', `bm-ptag${cf === t.toLowerCase() ? ' on' : ''}`);
        c.type = 'button';
        c.dataset.t = t.toLowerCase();
        const u = ptIcoUrl(Array.isArray(b.ptic) ? b.ptic[ix] : '');
        if (u) {
            const im = mk('img', 'bm-ptic');
            im.alt = '';
            im.decoding = 'sync';
            im.referrerPolicy = 'no-referrer';
            im.onerror = () => im.remove();
            im.src = u;
            c.append(im);
        }
        c.append(document.createTextNode(t));
        c.onclick = e => {
            e.preventDefault(); e.stopPropagation();
            this.currentTagFilter = this.currentTagFilter === t.toLowerCase() ? null : t.toLowerCase();
            this.render();
        };
        return c;
    }

    itemPreview(b) {
        const previewImg = mk('img', 'bm-prev');
        previewImg.onload = () => previewImg.classList.remove('bm-bad');
        previewImg.onerror = () => {
            previewImg.classList.add('bm-bad');
            if (!previewImg.hasAttribute('data-frame')) { previewImg.style.display = 'none'; return; }
            if (/^data:/.test(previewImg.src) || !tryGm(previewImg, previewImg._u, () => frameFail(previewImg))) frameFail(previewImg);
        };
        const showImg = on => {
            const d = on ? 'block' : 'none';
            if (previewImg.style.display !== d) previewImg.style.display = d;
        };
        const applyPreview = st => {
            if (!(this.prevEn && b.frame && st !== 0)) {
                previewImg.removeAttribute('data-frame');
                previewImg._gen = (previewImg._gen || 0) + 1;
                previewImg._isUpdating = false;
                return showImg(false);
            }
            previewImg.setAttribute('data-frame', b.frame);
            showImg(true);
            previewImg._bRef = b;
            b._lastImg ||= frameBust(b.frame);
            const key = (b._lastData ? 'd' : '') + b._lastImg;
            if (previewImg._k !== key) {
                previewImg._k = key;
                previewImg._u = b._lastImg;
                previewImg.src = b._lastData || b._lastImg;
            }
        };
        return { previewImg, applyPreview };
    }

    scanPage() {
        if (!IS_BC) return false;
        const key = pageKey();
        if (!key) return false;
        const b = AppState.bookmarks.find(x => isObj(x) && isStr(x.url) && bmKey(x.url) === key);
        if (!b) return false;
        const info = parseBcExtra(document.documentElement.outerHTML);
        if (!info.ptags && !info.sch) return false;
        if (applyProfile(b, info)) {
            saveBookmarks();
            if (this.isOpen()) this.render();
        }
        return true;
    }

    markLive(key, known) {
        const b = known || AppState.bookmarks.find(x => bmKey(x.url) === key);
        if (!b) return;
        b._live = Date.now();
        b.seen = b._live;
        if ((b.status ?? 0) !== 0) return;
        b.status = 1;
        clearTimeout(this._vwSave);
        bmDirty = true; this._vwSave = setTimeout(saveBookmarks, 3000);
        if (this.isOpen()) this.onStatusChange(b, Store.get('bc_bm_sort_type', 'date'));
    }

    setViewers(key, n, known) {
        const b = known || AppState.bookmarks.find(x => bmKey(x.url) === key);
        if (!b || !(n >= 0)) return;
        const changed = b.viewers !== n;
        b.viewers = n;
        b.vts = Date.now();
        if (!changed) return;
        clearTimeout(this._vwSave);
        bmDirty = true; this._vwSave = setTimeout(saveBookmarks, 3000);
        if (this.isOpen()) this.onStatusChange(b, Store.get('bc_bm_sort_type', 'date'));
    }

    addEntry({ name, url, avatar, viewers }) {
        syncBookmarks();
        if (!isSafeUrl(url) || isFavKey(bmKey(url))) return false;
        const bm = { name: clip(name, 100) || modelSlug(url), url: normUrl(url), avatar: toHttps(avatar), note: '' };
        if (viewers > 0) { bm.viewers = viewers; bm.vts = Date.now(); }
        AppState.bookmarks.push(bm);
        saveBookmarks();
        if (this.isOpen()) this.render();
        this.checkStatuses();
        if (!bm.avatar) this.fetchAvatar(bm);
        return true;
    }

    removeKey(key, after) {
        syncBookmarks();
        const ex = AppState.bookmarks.find(b => isObj(b) && isStr(b.url) && bmKey(b.url) === key);
        if (!ex) return false;
        const run = () => {
            dropBookmark(ex);
            if (this.isOpen()) this.render();
            if (after) after();
        };
        if (ex.note || ex.g) askConfirm(this.ui, L().star_del_q, run); else run();
        return true;
    }

    updateAddBtn() {
        const name = currentSite.getName(), l = L();
        const show = !!name && !isFavName(name);
        this.btnAdd.disabled = !show;
        if (show) this.btnAdd.textContent = `${l.bm_add} (${name})`;
        const foot = this.btnAdd.parentNode;
        if (foot) foot.style.display = show ? '' : 'none';
    }

    passesFilters(b) {
        const st = b.status ?? 0, lk = bmStatusKey(b);
        return !(this.hideOffline && st === 0) && !ST_FILTERS.some(([prop,, key]) => this[prop] && key === lk) && this.matchesText(b)
            && (this.wFilter == null || bmWatch(b, this._hist) === this.wFilter);
    }

    matchesText(b) {
        const tag = this.currentTagFilter, q = this.query, g = this.currentGroup;
        const note = b.note || '', grp = b.g || '';
        return bmTagMatch(b, tag)
            && (!g || grp === g)
            && (!q || `${b.name} ${note} ${grp} ${(b.ptags || []).join(' ')}`.toLowerCase().includes(q));
    }

    syncList(els) {
        const keep = new Set(els);
        Array.from(this.list.children).forEach(c => { if (!keep.has(c)) c.remove(); });
        els.forEach((el, i) => {
            const cur = this.list.children[i];
            if (cur !== el) this.list.insertBefore(el, cur || null);
        });
    }

    sortedFiltered() {
        const dir = Store.get('bc_bm_sort_rev', false) ? -1 : 1;
        const cmp = BM_CMP[Store.get('bc_bm_sort_type', 'date')] || BM_CMP.date;
        return AppState.bookmarks.map((b, i) => ({ b, i }))
            .sort((x, y) => (!!y.b.pin - !!x.b.pin) || dir * cmp(x, y))
            .filter(({ b }) => this.passesFilters(b));
    }

    render() {
        this.ui.thumbStars?.schedule();
        this._hist = histLoad();
        if (this._lang !== AppState.lang) { this._els = new WeakMap(); this._lang = AppState.lang; }
        const keepScroll = this.list.scrollTop;
        this.searchWrap.style.display = AppState.bookmarks.length ? 'flex' : 'none';
        AppState.bookmarks.forEach(b => { if (b?._updateItemDOM) b._updateItemDOM = null; });
        this.updateAddBtn();

        if (!AppState.bookmarks.length) {
            this.stopPreviews();
            this.list.innerHTML = `<div class="bm-empty">${L().bm_empty}</div>`;
            if (this.tagsWrap) { this.tagsWrap.replaceChildren(); this.tagsWrap.style.display = 'none'; }
            this.wsegHost?.replaceChildren();
            return;
        }

        this.renderTags();
        this.list.classList.add('bm-gallery-mode');
        this.applyCols?.();

        const sortedList = this.sortedFiltered();

        if (!sortedList.length) {
            this.list.innerHTML = `<div class="bm-empty">${L().bm_nomatch}</div>`;
            return;
        }

        this.syncList(sortedList.map(({ b }) => this.renderItem(b)));
        this.list.scrollTop = keepScroll;

        if (!this._previewTimer && this.prevEn && prevSpd()) {
            this._previewTimer = setInterval(() => this.tickPreviews(), prevSpd());
        }
    }

    stopPreviews() {
        clearInterval(this._previewTimer);
        this._previewTimer = null;
    }

    tickPreviews() {
        if (!this.overlay.classList.contains('open') || !this.prevEn) return this.stopPreviews();
        if (realHidden()) return;
        this.list.querySelectorAll('.bm-item').forEach(item => {
            if (item._inView) refreshFrame(item.querySelector('img[data-frame]'));
        });
    }

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

        const bm = { name, url: normUrl(location.href), avatar: pageAvatar(name), note: '' };
        if (!validBm(bm)) return this.ui.flash(this.btnAdd, 'ERR');
        const vn = parseCount(readViewers());
        if (vn > 0) { bm.viewers = vn; bm.vts = Date.now(); }
        AppState.bookmarks.push(bm);
        saveBookmarks();
        this.render();
        this.checkStatuses();
        if (!bm.avatar) this.fetchAvatar(bm);
    }

    async fetchAvatar(bm) {
        const avatar = extractAvatar((await withBackoff(() => requestOnce(bm.url))) || '');
        if (!avatar) return;
        if (avatar === bm.avatar) return;
        bm.avatar = avatar;
        saveBookmarks();
        if (this.isOpen()) this.render();
    }
}

const pageHasError = (url, res) => {
    const raw = res.responseText || '', txt = raw.toLowerCase();
    const title = txt.match(/<title[^>]*>(.*?)<\/title>/i)?.[1] || '';
    return [
        () => res.status === 404 || res.status >= 500,
        () => ['not found', '404', 'error'].some(t => title.includes(t)),
        () => /no\s*(?:videos?|results?)\s*found|does\s*not\s*exist|\b0\s*results?\b/i.test(txt),
        () => url.includes('camshowrecordings') && !raw.includes('class="h1modelpage"'),
        () => url.includes('pornolab.net') && (/не найден[оа]/.test(txt) || !txt.includes('tlink'))
    ].some(check => check());
};

const SEARCH_ENGINES = [
        ['PornoLab', 'https://pornolab.net/forum/tracker.php?nm=%s'],
        ['BestCam.tv', 'https://bestcam.tv/model/%s'],
        ['CamShowRecordings', 'https://www.camshowrecordings.com/?s=%s'],
        ['LiveCamsRip', 'https://www.livecamsrip.com/%s/profile'],
        ['CamDownload', 'https://camdownload.net/performer/%s'],
        ['CamShow.cc', 'https://camshow.cc/streamer/%s'],
        ['SimpCity', 'https://simpcity.cr/search/?q=%s&o=date', true],
        ['CamsClips', 'https://www.camsclips.com/search/%s/', true]
];

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

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

    build() {
        this.overlay = mk('div', 'bm-overlay');
        this.overlay.onclick = e => { if (e.target === this.overlay && Date.now() - (this._opAt || 0) > 500) this.overlay.classList.remove('open'); };

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

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

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

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

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

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

        this.buildSearchButtons(list);
        this.bindSearchInput();

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

    buildSearchButtons(list) {
        list.append(this.input, ...SEARCH_ENGINES.map(([txt, tmpl, skipCheck]) => {
            const urlFn = v => tmpl.replace('%s', encodeURIComponent(v));
            const btn = mk('button', 'bm-btn bm-srch-btn');
            btn.textContent = txt;
            btn.onclick = () => {
                const v = this.input.value.trim();
                if (v) window.open(urlFn(v), '_blank');
                else this.ui.flash(btn, 'ERR');
            };
            this.btns.push({ btn, urlFn, skipCheck: !!skipCheck });
            return btn;
        }));
    }

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

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

    checkPage(url, btn) {
        if (!btn.isConnected) return;
        btn.style.borderLeft = '4px solid orange';
        btn.style.opacity = '0.8';
        const gen = this._gen;
        const stale = () => gen !== this._gen || !btn.isConnected;
        const markErr = () => { if (!stale()) { btn.style.borderLeft = '4px solid #ef4444'; btn.style.opacity = '0.5'; } };
        const req = new AbortController();
        this._live.add(req);
        gmReq({ url, timeout: 8000, signal: req.signal }).then(res => {
            this._live.delete(req);
            if (res.fail === 'abort') return;
            if (res.fail) return markErr();
            if (stale()) return;
            if (pageHasError(url, { status: res.status, responseText: res.body })) return markErr();
            btn.style.borderLeft = '4px solid #10b981';
            btn.style.opacity = '1';
        });
    }

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

class PickManager {
    constructor(uiController, onDone) {
        this.ui = uiController;
        this.onDone = onDone;
        this.active = false;
        this.cur = null;
        this._sx = 0;
        this._sy = 0;
        this.box = mk('div', 'pick-box');
        this.bar = mk('div', 'pick-bar');
        this.info = mk('div', 'pick-info');
        const btns = mk('div', 'pick-btns');
        this.btnUp = mk('button', 'bm-btn');
        this.btnOk = mk('button', 'bm-btn');
        this.btnNo = mk('button', 'bm-btn');
        btns.append(this.btnUp, this.btnOk, this.btnNo);
        this.bar.append(this.info, btns);
        this.ui.shadow.append(this.box, this.bar);

        this.btnUp.onclick = () => {
            const p = this.cur?.parentElement;
            if (p && p !== document.body && p !== document.documentElement) this.setTarget(p);
        };
        this.btnOk.onclick = () => this.confirm();
        this.btnNo.onclick = () => this.stop();

        this._events = ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'click', 'touchend'];
        this._handler = e => {
            if (e.composedPath().includes(this.ui.host)) return;
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            if (e.type === 'pointerdown') {
                this._sx = e.clientX;
                this._sy = e.clientY;
            } else if (e.type === 'pointerup' && Math.hypot(e.clientX - this._sx, e.clientY - this._sy) < 10) {
                this.pickAt(e.clientX, e.clientY);
            }
        };
        this._sync = () => this.placeBox();
    }

    start() {
        if (this.active) return;
        this.active = true;
        this.cur = null;
        this.box.style.display = 'none';
        this.btnUp.textContent = `↑ ${L().pk_up}`;
        this.btnOk.textContent = L().pk_ok;
        this.btnNo.textContent = L().pk_cancel;
        this.info.textContent = L().pk_hint;
        this.bar.classList.add('open');
        this._events.forEach(ev => window.addEventListener(ev, this._handler, { capture: true, passive: false }));
        window.addEventListener('scroll', this._sync, { capture: true, passive: true });
        window.addEventListener('resize', this._sync, { passive: true });
    }

    stop() {
        if (!this.active) return;
        this.active = false;
        this.cur = null;
        this._events.forEach(ev => window.removeEventListener(ev, this._handler, true));
        window.removeEventListener('scroll', this._sync, true);
        window.removeEventListener('resize', this._sync);
        this.box.style.display = 'none';
        this.bar.classList.remove('open');
        this.onDone?.();
    }

    pickAt(x, y) {
        const el = document.elementFromPoint(x, y);
        if (!el || el === document.body || el === document.documentElement || el === this.ui.host) return;
        this.setTarget(el);
    }

    placeBox() {
        if (!this.cur || !this.cur.isConnected) return;
        const r = this.cur.getBoundingClientRect();
        const s = this.box.style;
        s.display = 'block';
        s.left = `${r.left}px`;
        s.top = `${r.top}px`;
        s.width = `${r.width}px`;
        s.height = `${r.height}px`;
    }

    setTarget(el) {
        this.cur = el;
        this.placeBox();
        const sel = buildSelector(el);
        let n = 0;
        try { n = document.querySelectorAll(sel).length; } catch {}
        this.info.textContent = `${sel} (${n} ${L().pk_found})`;
    }

    confirm() {
        if (!this.cur) return;
        const sel = buildSelector(this.cur);
        try { document.querySelector(sel); } catch { return; }
        addCustom(sel);
        applyHideCSS();
        this.stop();
    }
}

const textEl = (cls, text, tag = 'div') => { const e = mk(tag, cls); e.textContent = text; return e; };
const firstMatch = c => {
    for (const x of c.sel) { try { const e = document.querySelector(x); if (e) return e; } catch {} }
    return null;
};

class HideManager {
    constructor(uiController) {
        this.ui = uiController;
        this.build();
        this.picker = new PickManager(uiController, () => this.open());
    }

    build() {
        this.overlay = mk('div', 'bm-overlay');
        this.overlay.onclick = e => { if (e.target === this.overlay && Date.now() - (this._opAt || 0) > 500) this.close(); };

        const modal = mk('div', 'bm-modal');
        const hdr = mk('div', 'bm-hdr');
        this.title = mk('span', 'bm-title');
        const closeBtn = mk('button', 'bm-close');
        closeBtn.innerHTML = '×';
        closeBtn.onclick = () => this.close();
        hdr.append(this.title, closeBtn);

        this.list = mk('div', 'bm-list');

        const foot = mk('div', 'bm-foot');
        this.btnPick = mk('button', 'bm-add');
        this.btnPick.onclick = () => { this.close(); this.picker.start(); };
        this.btnReset = mk('button', 'bm-btn');
        this.btnReset.style.gridColumn = 'span 2';
        this.btnReset.onclick = () => {
            const cfgNow = Store.get('bc_hide_cfg', {});
            HIDE_CATS.forEach(c => { if (c.site === SITE) delete cfgNow[c.id]; });
            Store.set('bc_hide_cfg', cfgNow);
            setCustom([]);
            applyHideCSS();
            this.render();
        };
        foot.append(this.btnPick, this.btnReset);

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

    updateLabels() {
        this.render();
    }

    open() {
        this.render();
        this._opAt = Date.now();
        this.overlay.classList.add('open');
    }

    close() {
        this.overlay.classList.remove('open');
    }

    render() {
        const l = L();
        this.title.innerHTML = `${icons.eyeOff} ${l.hd_title}`;
        this.btnPick.innerHTML = `${icons.target} ${l.hd_pick}`;
        this.btnReset.textContent = l.hd_reset;
        this.list.replaceChildren();
        const frag = document.createDocumentFragment();
        this.renderCats(frag, l);
        this.renderRules(frag, l);
        this.list.append(frag);
    }

    renderCats(frag, l) {
        const plat = detectPlatform();
        const cats = HIDE_CATS.filter(c => c.site === SITE && (c.plat === 'b' || c.plat === plat));
        const cfg = Store.get('bc_hide_cfg', {});
        const rows = [];
        const setAll = fn => {
            const cur = Store.get('bc_hide_cfg', {});
            cats.forEach(c => { cur[c.id] = fn(c); });
            Store.set('bc_hide_cfg', cur);
            rows.forEach(({ c, row }) => row.classList.toggle('on', catOn(c, cur)));
            applyHideCSS();
        };
        const bulk = (defs, cols) => {
            const box = mk('div', 'hd-bulk');
            if (cols) box.style.gridTemplateColumns = `repeat(${cols},1fr)`;
            defs.forEach(([label, fn]) => {
                const b = textEl('bm-btn', label, 'button');
                b.onclick = () => setAll(fn);
                box.append(b);
            });
            return box;
        };
        frag.append(
            textEl('hd-sec', l.hd_cats),
            bulk([[l.hd_p_min, c => HIDE_PRESETS.min.includes(c.id)], [l.hd_p_clean, c => HIDE_PRESETS.clean.includes(c.id)]], 2),
            bulk([[l.hd_all, () => true], [l.hd_off, () => false]], 2),
            textEl('hd-hint', l.hd_peek)
        );
        cats.forEach(c => {
            const row = this.catRow(c, cfg, l);
            rows.push({ c, row });
            frag.append(row);
        });
    }

    catRow(c, cfg, l) {
        const found = !!firstMatch(c);
        const pair = HIDE_L[AppState.lang]?.[c.id] || (AppState.lang === 'RU' ? [c.ru, c.dru] : [c.en, c.den]);
        const row = mk('div', `hd-row${catOn(c, cfg) ? ' on' : ''}${found ? '' : ' na'}`);
        const txt = mk('span', 'hd-txt');
        txt.append(textEl('', pair[0], 'b'));
        const desc = pair[1];
        if (desc) txt.append(textEl('', desc, 'small'));
        if (!found) txt.append(textEl('hd-na', l.hd_na, 'small'));
        row.append(txt, mk('span', 'hd-sw'));
        this.bindCatRow(row, c);
        return row;
    }

    bindCatRow(row, c) {
        let timer = null, peeking = false, savedY = 0;
        const peekOn = () => {
            const el = firstMatch(c);
            if (!el) return;
            peeking = true;
            savedY = window.scrollY;
            applyHideCSS(c.id);
            let st = document.getElementById('bc-unchained-peek');
            if (!st) { st = mk('style'); st.id = 'bc-unchained-peek'; (document.head || document.documentElement).appendChild(st); }
            st.textContent = c.sel.map(x => `${x}{outline:3px solid #ff2d95!important;outline-offset:-3px!important}`).join('\n');
            this.overlay.classList.add('peek');
            const r = el.getBoundingClientRect();
            if (r.bottom < 0 || r.top > window.innerHeight) el.scrollIntoView({ block: 'center' });
        };
        const peekOff = () => {
            clearTimeout(timer);
            if (!peeking) return;
            document.getElementById('bc-unchained-peek')?.remove();
            applyHideCSS();
            this.overlay.classList.remove('peek');
            if (Math.abs(window.scrollY - savedY) > 1) window.scrollTo(0, savedY);
            setTimeout(() => { peeking = false; }, 350);
        };
        row.addEventListener('pointerdown', () => { clearTimeout(timer); timer = setTimeout(peekOn, 450); });
        ['pointerup', 'pointercancel', 'pointerleave'].forEach(ev => row.addEventListener(ev, peekOff));
        row.addEventListener('contextmenu', e => e.preventDefault());
        row.onclick = () => {
            if (peeking) return;
            const cur = Store.get('bc_hide_cfg', {});
            cur[c.id] = !catOn(c, cur);
            Store.set('bc_hide_cfg', cur);
            row.classList.toggle('on', cur[c.id]);
            applyHideCSS();
        };
    }

    renderRules(frag, l) {
        frag.append(textEl('hd-sec', l.hd_rules));
        const rules = getCustom();
        if (!rules.length) return frag.append(textEl('hd-empty', l.hd_none));
        const clearAll = () => { setCustom([]); applyHideCSS(); this.render(); };
        rules.forEach(sel => {
            const row = mk('div', 'hd-row hd-rule');
            const t = textEl('hd-sel', sel, 'span');
            t.title = sel;
            const del = mk('button', 'bm-del');
            del.innerHTML = icons.del;
            del.onclick = () => { removeCustom(sel); applyHideCSS(); this.render(); };
            row.append(t, del);
            frag.append(row);
        });
        const clr = textEl('bm-btn', l.hd_clear, 'button');
        clr.onclick = clearAll;
        frag.append(clr);
    }
}

let HS_AV_TRIED = null, HS_AV_Q = Promise.resolve();
const histOn = () => Store.get('bc_hist_on', true) !== false;
const histLoad = () => {
    const h = Store.get('bc_hist', {});
    return {
        m: h && h.m && typeof h.m === 'object' && !Array.isArray(h.m) ? h.m : {},
        s: h && Array.isArray(h.s) ? h.s : []
    };
};
const histPos = v => (Number.isFinite(v) && v > 0 ? v : 0);
const newHistModel = e => ({ n: clip(e.n, 100), u: e.u, a: toHttps(e.a), s: histPos(e.s), v: histPos(e.v), f: histPos(e.f), l: histPos(e.l) });
const updateHistModel = (c, e) => {
    const num = histPos;
    c.s = Math.max(c.s, num(e.s));
    c.v = Math.max(c.v, num(e.v));
    c.f = Math.min(c.f || Infinity, num(e.f) || Infinity);
    if (!Number.isFinite(c.f)) c.f = 0;
    if (num(e.l) > c.l) { c.l = num(e.l); c.n = clip(e.n, 100); c.u = e.u; if (e.a) c.a = toHttps(e.a); }
};
const mergeHistModel = (h, k, e) => {
    if (!e || !isStr(e.n) || !isStr(e.u)) return;
    const c = h.m[k];
    if (c) updateHistModel(c, e);
    else h.m[k] = newHistModel(e);
};
const mergeHistSessions = (h, list) => {
    const seen = new Set(h.s.map(x => `${x.k}|${x.t}`));
    (Array.isArray(list) ? list : []).forEach(x => {
        if (!x || !isStr(x.k) || !histPos(x.t) || seen.has(`${x.k}|${x.t}`)) return;
        h.s.push({ k: x.k, n: clip(x.n, 100), t: x.t, d: histPos(x.d) });
    });
};
const histMerge = src => {
    const h = histLoad();
    const m = src.m && typeof src.m === 'object' && !Array.isArray(src.m) ? src.m : {};
    Object.keys(m).forEach(k => mergeHistModel(h, k, m[k]));
    mergeHistSessions(h, src.s);
    h.s.sort((a, b) => a.t - b.t);
    Store.set('bc_hist', h);
};
const fmtDur = sec => {
    const s = Math.max(0, Math.round(sec) || 0), h = Math.floor(s / 3600), m = Math.floor(s % 3600 / 60), p = n => String(n).padStart(2, '0');
    return h ? `${h}:${p(m)}:${p(s % 60)}` : `${m}:${p(s % 60)}`;
};
const fmtStamp = ts => {
    if (!ts) return '';
    const d = new Date(ts), p = n => String(n).padStart(2, '0');
    return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
};
const csvCell = v => {
    let s = String(v ?? '');
    if (s[0] === '-' && s.length <= 250 && !/[\r\n\t]/.test(s)) {
        return `"${`="${s.replace(/"/g, '""')}"`.replace(/"/g, '""')}"`;
    }
    if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`;
    return `"${s.replace(/"/g, '""')}"`;
};
const csvText = rows => `\ufeff${rows.map(r => r.map(csvCell).join(',')).join('\r\n')}`;

const askConfirm = (ui, text, onYes) => {
    const ov = mk('div', 'cf-ov'), card = mk('div', 'cf-card'), btns = mk('div', 'cf-btns');
    const unbindEsc = bindEscape(() => close());
    const close = () => { ov.remove(); unbindEsc(); };
    const no = textEl('', L().cf_cancel, 'button'), yes = textEl('cf-yes', L().cf_ok, 'button');
    no.onclick = close;
    yes.onclick = () => { close(); onYes(); };
    btns.append(no, yes);
    card.append(textEl('cf-txt', text), btns);
    card.onclick = e => e.stopPropagation();
    ov.onclick = close;
    ov.append(card);
    ui.shadow.appendChild(ov);
};

const tagRows = (m, pim) => [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([v, n]) => ({ v, t: v, n, ic: ptImg(pim.get(v), 'cal-tic') }));
class CalendarManager {
    constructor(ui, bm) {
        this.ui = ui;
        this.bm = bm;
        const now = new Date();
        this.y = now.getFullYear();
        this.m = now.getMonth();
        this.sel = this.dkey(now);
        Object.assign(this, CalendarManager.loadPrefs());
        this.pick = false;
        this.dd = null;
        this.wOpen = false;
        this.pane = mk('div', 'cal-pane');
        this.bm.modal.appendChild(this.pane);
        this.sk = '';
        this.sn = 0;
        setInterval(() => this.scan(), 8000);
        onRemoteChange('bc_hist', () => { if (this.isOpen()) this.render(); });
    }

    static loadPrefs() {
        const lay = Store.get('bc_cal_lay', 3);
        const wf = Store.get('bc_cal_wf', -1);
        const ms = Store.get('bc_cal_models', []);
        return {
            lay: Number.isInteger(lay) && lay >= 0 && lay <= 3 ? lay : 3,
            wf: [0, 1].includes(wf) ? wf : null,
            grp: Store.get('bc_cal_grp', '') || '',
            tag: Store.get('bc_cal_tag', '') || '',
            models: new Set(Array.isArray(ms) ? ms.filter(isStr) : [])
        };
    }

    scan() {
        const key = pageKey();
        if (!key || realHidden()) return;
        if (this.sk !== key) { this.sk = key; this.sn = 0; }
        if (this.sn >= 6) return;
        this.sn++;
        if (this.bm.scanPage()) this.sn = 99;
    }

    isOpen() { return this.bm.overlay.classList.contains('open') && this.bm.mode === 'cal'; }

    show() {
        this.render();
        this.bm.checkStatuses().then(() => { if (this.isOpen()) this.render(); }).catch(() => {});
        (async () => {
            for (let i = 0; i < 60 && this.isOpen(); i++) {
                const r = await this.bm.bgExtra(true);
                if (r === 'none') break;
                if (r === 'busy') { await new Promise(res => setTimeout(res, 1500)); continue; }
                if (r && this.isOpen()) this.render();
            }
        })();
    }

    dkey(d) { const p = n => String(n).padStart(2, '0'); return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; }

    fmt(opts, d) {
        let s;
        try { s = new Intl.DateTimeFormat((AppState.lang || 'en').toLowerCase(), opts).format(d); }
        catch { s = new Intl.DateTimeFormat('en', opts).format(d); }
        return s;
    }

    cap(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : s; }

    dur(sec) {
        const l = L();
        if (sec < 60) return fmtDur(sec);
        const m = Math.round(sec / 60), h = Math.floor(m / 60);
        return h ? `${h} ${l.u_h} ${m % 60} ${l.u_m}` : `${m} ${l.u_m}`;
    }

    ib(cls, svg, text, tag = 'button') {
        const e = mk(tag, cls);
        e.innerHTML = text ? `${svg}<span>${escHTML(text)}</span>` : svg;
        return e;
    }

    span(x) {
        const m = String(x).match(/^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})/);
        if (!m) return [0, 0];
        const s = +m[1] * 60 + +m[2];
        let e = +m[3] * 60 + +m[4];
        if (e <= s) e = 1440;
        return [s, e];
    }

    hm(min) { const p = n => String(n).padStart(2, '0'); return `${p(Math.floor(min / 60))}:${p(min % 60)}`; }

    rangesText(r, l) {
        return r.split(',').map(x => {
            const [s, e] = this.span(x);
            return s === 0 && e >= 1440 ? l.cal_allday : `${this.hm(s)}\u2013${this.hm(e)}`;
        }).join(' \u00b7 ');
    }

    bar(r, nowMin) {
        const bar = mk('div', 'cal-bar');
        r.split(',').forEach(x => {
            const [s, e] = this.span(x);
            if (e <= s) return;
            const seg = mk('i', 'cal-seg');
            seg.style.left = `${s / 14.4}%`;
            seg.style.width = `${Math.max((e - s) / 14.4, 1)}%`;
            bar.append(seg);
        });
        if (nowMin != null) { const n = mk('i', 'cal-now'); n.style.left = `${nowMin / 14.4}%`; bar.append(n); }
        return bar;
    }

    startMin(r) { const m = String(r).match(/^(\d{1,2}):(\d{2})/); return m ? +m[1] * 60 + +m[2] : 0; }

    favs(hist) {
        return AppState.bookmarks.filter(b => isObj(b) && isStr(b.url)
            && (this.wf == null || bmWatch(b, hist) === this.wf)
            && (!this.grp || b.g === this.grp)
            && bmTagMatch(b, this.tag)
            && (!this.models.size || this.models.has(bmKey(b.url))));
    }

    filtersOn() { return this.wf != null || !!this.grp || !!this.tag || this.models.size > 0; }

    collect() {
        const hist = histLoad();
        const favs = this.favs(hist), fk = new Set(favs.map(b => bmKey(b.url)));
        const on = this.filtersOn();
        const sess = (this.lay & 1) ? this.groupSessions(hist, on, fk) : new Map();
        const sched = (this.lay & 2) ? this.buildSched(favs) : [[], [], [], [], [], [], []];
        const anySched = AppState.bookmarks.some(b => Array.isArray(b.sch) && b.sch.some(x => this.clean(x)));
        return { hist, sess, sched, anySched };
    }

    groupSessions(hist, on, fk) {
        const sess = new Map();
        for (const s of hist.s) {
            if (on && !fk.has(s.k)) continue;
            const dk = this.dkey(new Date(s.t));
            if (!sess.has(dk)) sess.set(dk, []);
            sess.get(dk).push(s);
        }
        return sess;
    }

    buildSched(favs) {
        const sched = [[], [], [], [], [], [], []];
        for (const b of favs) if (Array.isArray(b.sch)) b.sch.forEach((x, i) => {
            const r = this.clean(x);
            if (r && i < 7) sched[i].push({ b, r, s: this.startMin(r) });
        });
        sched.forEach(a => a.sort((x, y) => x.s - y.s));
        return sched;
    }

    clean(r) {
        return String(r || '').split(',').filter(x => {
            const m = x.match(/^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})/);
            return m && !(+m[1] === +m[3] && m[2] === m[4]);
        }).join(',');
    }

    shift(d) { const x = new Date(this.y, this.m + d, 1); this.y = x.getFullYear(); this.m = x.getMonth(); this.render(); }

    save(k, v) { Store.set(k, v); this.render(); }

    render() {
        const l = L(), { hist, sess, sched, anySched } = this.collect();
        const pane = this.pane, keepTop = pane.scrollTop;
        pane.replaceChildren();
        pane.append(this.renderHead(l));
        pane.append(this.renderFilters(l, hist));
        const now = new Date(), tk = this.dkey(now);
        pane.append(this.renderGrid(sess, sched, now, tk));
        pane.append(this.renderDetail(l, hist, sess, sched, anySched, now, tk));
        pane.scrollTop = keepTop;
    }

    renderHead(l) {
        const head = mk('div', 'cal-head');
        const prev = this.ib('cal-nav', icons.chevL, ''), next = this.ib('cal-nav', icons.chevR, '');
        prev.onclick = () => this.shift(-1);
        next.onclick = () => this.shift(1);
        const today = textEl('cal-today', l.cal_today, 'button');
        today.onclick = () => { const n = new Date(); this.y = n.getFullYear(); this.m = n.getMonth(); this.sel = this.dkey(n); this.render(); };
        head.append(prev, textEl('cal-title', this.cap(this.fmt({ month: 'long', year: 'numeric' }, new Date(this.y, this.m, 1)))), next, today);
        return head;
    }

    filterRow1(l) {
        const r1 = mk('div', 'cal-frow');
        const layers = mk('div', 'bm-seg');
        [[1, icons.eye, l.cal_hist], [2, icons.clock, l.cal_sched]].forEach(([bit, ic, t]) => {
            const b = this.ib(`bm-tbtn${this.lay & bit ? ' on' : ''}`, ic, t);
            b.onclick = () => { const v = this.lay ^ bit; this.lay = v; this.save('bc_cal_lay', v); };
            layers.append(b);
        });
        const wseg = mk('div', 'bm-seg');
        WMARKS.forEach(([v, ic, key]) => {
            const b = this.ib(`bm-tbtn${this.wf === v ? ' on' : ''}`, icons[ic], l[key]);
            b.title = l[key];
            b.onclick = () => { this.wf = this.wf === v ? null : v; this.save('bc_cal_wf', this.wf == null ? -1 : this.wf); };
            wseg.append(b);
        });
        r1.append(layers, wseg);
        return r1;
    }

    mkDD(kind, icon, label, on) {
        const b = this.ib(`cal-dd${on ? ' on' : ''}${this.dd === kind ? ' open' : ''}`, icon, label);
        b.onclick = () => { this.dd = this.dd === kind ? null : kind; this.pick = false; this.render(); };
        return b;
    }

    mkPanel(opts, cur, set) {
        const box = mk('div', 'cal-panel');
        opts.forEach(o => {
            if (o.sep) { box.append(mk('div', 'cal-sep')); return; }
            const c = this.ib(`cal-opt${o.v === cur ? ' on' : ''}${o.all ? ' all' : ''}`, o.ic || '', o.t);
            if (o.n) c.insertAdjacentHTML('beforeend', `<i>${o.n}</i>`);
            c.onclick = () => { this.dd = null; set(o.v); };
            box.append(c);
        });
        return box;
    }

    grpDropdown(l, r2) {
        const groups = bmGroups();
        if (this.grp && !groups.includes(this.grp)) this.grp = '';
        if (!groups.length) return null;
        r2.append(this.mkDD('grp', icons.folder, this.grp || l.grp_all, !!this.grp));
        if (this.dd !== 'grp') return null;
        return this.mkPanel([{ v: '', t: l.grp_all, all: true }, ...groups.map(g => ({ v: g, t: g }))], this.grp, v => { this.grp = v; this.save('bc_cal_grp', v); });
    }

    countTags(hidePT) {
        const noteTags = new Map(), profTags = new Map();
        AppState.bookmarks.forEach(b => {
            noteHashTags(b).forEach(x => countInto(noteTags, x));
            if (!hidePT) (b.ptags || []).forEach(x => countInto(profTags, x));
        });
        return { noteTags, profTags };
    }

    tagDropdown(l, r2) {
        const hidePT = Store.get('bc_bm_hide_ptags', true) !== false;
        const pim = ptIcoMap();
        const { noteTags, profTags } = this.countTags(hidePT);
        if (this.tag && !noteTags.has(this.tag) && !profTags.has(this.tag)) this.tag = '';
        if (!(noteTags.size + profTags.size)) return null;
        r2.append(this.mkDD('tag', ptImg(pim.get(this.tag), 'cal-tic') || icons.tag, this.tag || l.cal_all_tag, !!this.tag));
        if (this.dd !== 'tag') return null;
        return this.mkPanel(this.tagItems(l, noteTags, profTags, pim), this.tag, v => { this.tag = v; this.save('bc_cal_tag', v); });
    }

    tagItems(l, noteTags, profTags, pim) {
        const srt = m => tagRows(m, pim);
        const sep = noteTags.size && profTags.size ? [{ sep: true }] : [];
        return [{ v: '', t: l.cal_all_tag, all: true }, ...srt(noteTags), ...sep, ...srt(profTags)];
    }

    modelPicker(l, hist) {
        const box = mk('div', 'cal-models');
        if (this.models.size) {
            const rs = this.ib('cal-mrow reset', icons.undo, l.cal_reset);
            rs.onclick = () => { this.models.clear(); this.save('bc_cal_models', []); };
            box.append(rs);
        }
        const nk = s => String(s).replace(/^[^\p{L}\p{N}]+/u, '');
        AppState.bookmarks.filter(b => isObj(b) && isStr(b.url)).slice()
            .sort((a, b) => nk(a.name).localeCompare(nk(b.name), undefined, { sensitivity: 'base', numeric: true }))
            .forEach(b => {
                const k = bmKey(b.url), on = this.models.has(k);
                const r = mk('button', `cal-mrow${on ? ' on' : ''}`);
                const img = mk('img', 'cal-ava sm');
                bindAvatar(img, b.name, () => [toHttps(b.avatar), toHttps(hist.m[k]?.a)]);
                r.append(img, textEl('', b.name, 'span'));
                if (on) r.insertAdjacentHTML('beforeend', icons.check);
                r.onclick = () => { if (on) this.models.delete(k); else this.models.add(k); this.save('bc_cal_models', [...this.models]); };
                box.append(r);
            });
        return box;
    }

    renderFilters(l, hist) {
        const flt = mk('div', 'cal-filters');
        const r2 = mk('div', 'cal-frow');
        const grpPanel = this.grpDropdown(l, r2);
        const tagPanel = this.tagDropdown(l, r2);
        const ddPanel = tagPanel || grpPanel;
        flt.append(this.filterRow1(l));
        if (r2.children.length) flt.append(r2);
        if (ddPanel) flt.append(ddPanel);
        const mb = textEl(`cal-pill${this.models.size ? ' on' : ''}`, `${l.cal_models}${this.models.size ? ` (${this.models.size})` : ''}`, 'button');
        mb.onclick = () => { this.pick = !this.pick; this.dd = null; this.render(); };
        const r3 = mk('div', 'cal-frow');
        r3.append(mb);
        flt.append(r3);
        if (this.pick) flt.append(this.modelPicker(l, hist));
        return flt;
    }

    renderGrid(sess, sched, now, tk) {
        const grid = mk('div', 'cal-grid');
        for (let i = 0; i < 7; i++) grid.append(textEl('cal-wd', this.fmt({ weekday: 'short' }, new Date(2024, 0, 1 + i))));
        const first = new Date(this.y, this.m, 1), off = (first.getDay() + 6) % 7, dim = new Date(this.y, this.m + 1, 0).getDate();
        const t0 = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
        for (let i = 0; i < off; i++) grid.append(mk('div', 'cal-day blank'));
        for (let d = 1; d <= dim; d++) grid.append(this.dayCell(d, sess, sched, tk, t0));
        return grid;
    }

    dayCell(d, sess, sched, tk, t0) {
        const dt = new Date(this.y, this.m, d), key = this.dkey(dt), wd = (dt.getDay() + 6) % 7;
        const cell = mk('button', `cal-day${key === tk ? ' today' : ''}${key === this.sel ? ' sel' : ''}${dt.getTime() < t0 ? ' past' : ''}`);
        cell.type = 'button';
        cell.append(textEl('cal-n', String(d), 'span'));
        const bs = mk('div', 'cal-bs'), sn = new Set((sess.get(key) || []).map(s => s.k)).size, sc = sched[wd].length;
        if (sn) bs.append(textEl('cal-b', String(sn), 'i'));
        if (sc) bs.append(textEl('cal-b s', String(sc), 'i'));
        cell.append(bs);
        cell.onclick = () => { this.sel = key; this.render(); };
        return cell;
    }

    watchedBox(l, hist, sess, ctx) {
        const wbox = mk('div', 'cal-wsec');
        const agg = new Map();
        (sess.get(this.sel) || []).forEach(s => {
            const e = agg.get(s.k) || { k: s.k, n: s.n, first: s.t, d: 0, c: 0 };
            e.d += s.d || 0;
            e.c++;
            e.first = Math.min(e.first, s.t);
            agg.set(s.k, e);
        });
        const list = [...agg.values()].sort((a, b) => a.first - b.first);
        const total = list.reduce((s, x) => s + x.d, 0);
        const fh = this.ib(`cal-sec cal-fold${this.wOpen ? ' open' : ''}`, '', `${l.cal_watched}${total ? ` · ${this.dur(total)}` : ''}`);
        fh.onclick = () => { this.wOpen = !this.wOpen; this.render(); };
        wbox.append(fh);
        if (!this.wOpen) return wbox;
        if (!list.length) wbox.append(textEl('cal-muted', l.cal_none));
        else wbox.append(this.watchedTable(l, hist, list, total, ctx));
        return wbox;
    }

    watchedTable(l, hist, list, total, { favMap, href, ava }) {
        const p2 = n => String(n).padStart(2, '0');
        const tbl = mk('div', 'cal-tbl');
        const hd = mk('div', 'cal-trow cal-thead');
        [l.cal_c_model, l.cal_c_time, l.cal_c_dur, l.cal_c_n].forEach(x => hd.append(textEl('', x, 'span')));
        tbl.append(hd);
        list.forEach(e => {
            const b = favMap.get(e.k), row = mk('div', 'cal-trow'), name = b ? b.name : e.n;
            const who = mk('div', 'cal-who');
            const a = textEl('', name, 'a');
            a.href = href(b ? b.url : hist.m[e.k]?.u);
            who.append(ava(b, e.k, name), a);
            const t = new Date(e.first);
            row.append(who, textEl('', `${p2(t.getHours())}:${p2(t.getMinutes())}`, 'span'), textEl('', this.dur(e.d), 'span'), textEl('', `×${e.c}`, 'span'));
            tbl.append(row);
        });
        const ft = mk('div', 'cal-trow cal-tfoot');
        ft.append(textEl('', `${l.cal_total} · ${list.length}`, 'span'), textEl('', '', 'span'), textEl('', this.dur(total), 'span'), textEl('', `×${list.reduce((s, x) => s + x.c, 0)}`, 'span'));
        tbl.append(ft);
        return tbl;
    }

    schedRow(l, b, r, nowMin, { href, ava }) {
        const row = mk('div', 'cal-row'), line = mk('div', 'cal-line'), body = mk('div', 'cal-body');
        if (nowMin != null && (b.status ?? 0) > 0) line.append(mk('span', 'cal-dot'));
        const a = textEl('', b.name, 'a');
        a.href = href(b.url);
        line.append(a, textEl('cal-t', this.rangesText(r, l), 'span'));
        body.append(line, this.bar(r, nowMin));
        row.append(ava(b, bmKey(b.url), b.name), body);
        return row;
    }

    appendSched(det, l, list, anySched, nowMin, ctx) {
        det.append(textEl('cal-sec', l.cal_sched));
        if (!list.length) det.append(textEl('cal-muted', anySched ? l.cal_none : l.cal_no_sched));
        else {
            const ax = mk('div', 'cal-axis');
            ['00', '06', '12', '18', '24'].forEach(h => ax.append(textEl('', h, 'span')));
            det.append(ax);
        }
        list.forEach(({ b, r }) => det.append(this.schedRow(l, b, r, nowMin, ctx)));
        const bc = AppState.bookmarks.filter(b => isObj(b) && isStr(b.url) && urlSite(b.url) === 'bc');
        const got = bc.filter(b => Array.isArray(b.sch)).length;
        if (bc.length && got < bc.length) det.append(textEl('cal-muted', l.cal_loaded.replace('{a}', got).replace('{b}', bc.length)));
    }

    renderDetail(l, hist, sess, sched, anySched, now, tk) {
        const sp = this.sel.split('-').map(Number), sd = new Date(sp[0], sp[1] - 1, sp[2]), swd = (sd.getDay() + 6) % 7;
        const det = mk('div', 'cal-det');
        det.append(textEl('cal-dtitle', this.cap(this.fmt({ weekday: 'long', day: 'numeric', month: 'long' }, sd))));
        const favMap = new Map(AppState.bookmarks.filter(b => isObj(b) && isStr(b.url)).map(b => [bmKey(b.url), b]));
        const href = u => (u && isSafeUrl(u) ? linkFor(u) : '#');
        const ava = (b, k, name) => {
            const img = mk('img', 'cal-ava');
            bindAvatar(img, name, () => [toHttps(b?.avatar), toHttps(hist.m[k]?.a)]);
            return img;
        };
        const ctx = { favMap, href, ava };
        const wbox = (this.lay & 1) ? this.watchedBox(l, hist, sess, ctx) : null;
        if (this.lay & 2) {
            const nowMin = this.sel === tk ? now.getHours() * 60 + now.getMinutes() : null;
            this.appendSched(det, l, sched[swd], anySched, nowMin, ctx);
        }
        if (wbox) det.append(wbox);
        return det;
    }
}

class HistoryManager {
    constructor(ui, bookmarks) {
        this.ui = ui;
        this.bm = bookmarks;
        this.cur = null;
        this.pend = 0;
        this.fresh = false;
        this.lastTick = Date.now();
        const sort = Store.get('bc_hist_sort', 'last');
        this.sort = ['last', 'time', 'visits'].includes(sort) ? sort : 'last';
        this.build();
        setInterval(() => this.tick(), 5000);
        onPageEvent('pagehide', () => this.endSession(), window);
        onPageEvent('visibilitychange', () => { if (realHidden()) this.flush(); });
        onRemoteChange('bc_hist', () => { if (this.isOpen()) this.render(); });
    }

    build() {
        this.list = mk('div', 'hs-pane');
        this.bm.modal.appendChild(this.list);
    }

    isOpen() {
        return this.bm.overlay.classList.contains('open') && this.bm.mode === 'hist';
    }

    show() {
        this.flush();
        this.render();
    }

    open() {
        this.bm.open('hist');
    }

    hide() {
        this.bm.hide();
    }

    updateLabels() {
        if (this.isOpen()) this.render();
    }

    tick() {
        const now = Date.now(), dt = Math.min(65, (now - this.lastTick) / 1000);
        this.lastTick = now;
        if (!histOn()) return;
        const key = pageKey(), name = key ? currentSite.getName() : '';
        if (!key || !name) { this.endSession(); return; }
        this.ensureSession(key, name, now);
        if (isPlaying(selectorVideo())) this.pend += dt;
        if (this.pend >= 15) this.flush();
    }

    ensureSession(key, name, now) {
        if (!this.cur || this.cur.k !== key) {
            this.endSession();
            this.cur = { k: key, t: now, name, url: normUrl(location.href), avatar: '' };
            this.fresh = true;
        }
        this.cur.name = name;
        if (!this.cur.avatar) this.cur.avatar = toHttps(pageAvatar(name)) || AppState.bookmarks.find(b => bmKey(b.url) === key)?.avatar || '';
    }

    flush() {
        const c = this.cur, secs = Math.round(this.pend);
        if (!c || secs < 1) return;
        this.pend = 0;
        const h = histLoad();
        this.recordSession(h, c, secs, Date.now());
        Store.set('bc_hist', h);
        if (this.isOpen()) this.render();
    }

    recordSession(h, c, secs, now) {
        const e = h.m[c.k] || (h.m[c.k] = { n: c.name, u: c.url, a: '', s: 0, v: 0, f: c.t, l: c.t });
        e.n = c.name;
        e.u = c.url;
        if (c.avatar) e.a = c.avatar;
        e.s += secs;
        e.l = now;
        if (this.fresh) { e.v++; this.fresh = false; }
        let s = h.s.find(x => x.k === c.k && x.t === c.t);
        if (!s) { s = { k: c.k, n: c.name, t: c.t, d: 0 }; h.s.push(s); }
        s.d += secs;
    }

    endSession() {
        this.flush();
        this.cur = null;
        this.pend = 0;
    }

    remove(k) {
        const h = histLoad();
        delete h.m[k];
        h.s = h.s.filter(x => x.k !== k);
        Store.set('bc_hist', h);
        if (this.cur?.k === k) { this.cur = null; this.pend = 0; }
        this.render();
    }

    clear() {
        askConfirm(this.ui, L().hist_clear_q, () => {
            Store.set('bc_hist', { m: {}, s: [] });
            this.cur = null;
            this.pend = 0;
            this.render();
        });
    }

    exportCsv(sessions) {
        const h = histLoad();
        const rows = sessions
            ? [['name', 'url', 'start', 'seconds', 'duration'],
                ...h.s.slice().sort((a, b) => b.t - a.t).map(x => [x.n, h.m[x.k]?.u || '', fmtStamp(x.t), x.d, fmtDur(x.d)])]
            : [['name', 'url', 'seconds', 'duration', 'visits', 'first_seen', 'last_seen'],
                ...Object.keys(h.m).map(k => h.m[k]).sort((a, b) => b.l - a.l).map(e => [e.n, e.u, e.s, fmtDur(e.s), e.v, fmtStamp(e.f), fmtStamp(e.l)])];
        Helpers.download(new Blob([csvText(rows)], { type: 'text/csv;charset=utf-8' }), `BC_${sessions ? 'Sessions' : 'History'}_${Helpers.getTimestamp().slice(0, 10)}.csv`);
    }

    loadAvatar(e, img) {
        HS_AV_TRIED ||= new Set();
        if (!e.u || HS_AV_TRIED.has(e.k)) return;
        HS_AV_TRIED.add(e.k);
        HS_AV_Q = HS_AV_Q.then(async () => {
            const av = extractAvatar((await withBackoff(() => requestOnce(e.u))) || '');
            if (!av) return;
            const h = histLoad();
            if (h.m[e.k]) { h.m[e.k].a = av; Store.set('bc_hist', h); }
            if (img.isConnected) bindAvatar(img, e.n || '?', () => [av]);
        }).catch(() => {});
    }

    row(e, l) {
        const row = mk('div', 'hs-row');
        const img = mk('img', 'hs-ava');
        bindAvatar(img, e.n || '?', () => {
            const bmk = AppState.bookmarks.find(x => bmKey(x.url) === e.k);
            return [toHttps(e.a), toHttps(bmk?.avatar)];
        }, () => this.loadAvatar(e, img));
        const info = mk('div', 'hs-info');
        info.append(textEl('hs-name', e.n || e.k), textEl('hs-meta', `${fmtDur(e.s)} · ${e.v} ${l.hist_visits} · ${fmtAgo(e.l)}`));
        const fav = isFavKey(e.k);
        const favBtn = mk('button', `hs-btn${fav ? ' on' : ''}`);
        favBtn.innerHTML = icons.star;
        favBtn.title = fav ? l.star_del : l.star_add;
        favBtn.onclick = ev => {
            ev.stopPropagation();
            if (fav) return this.bm.removeKey(e.k, () => this.render());
            this.bm.addEntry({ name: e.n, url: e.u, avatar: e.a });
            this.render();
        };
        const delBtn = mk('button', 'hs-btn del');
        delBtn.innerHTML = icons.del;
        delBtn.title = l.hist_del;
        delBtn.onclick = ev => { ev.stopPropagation(); this.remove(e.k); };
        row.onclick = () => { if (isSafeUrl(e.u)) location.href = linkFor(e.u); };
        row.append(img, info, favBtn, delBtn);
        return row;
    }

    vbars(vals, labels, tips) {
        const max = Math.max(...vals, 1), bars = mk('div', 'ch-bars'), axis = mk('div', 'ch-axis');
        vals.forEach((v, i) => {
            const col = mk('div', 'ch-col'), bar = mk('div', `ch-bar${v > 0 ? '' : ' zero'}`);
            bar.style.height = `${v > 0 ? Math.max(3, Math.round(v / max * 100)) : 0}%`;
            col.title = tips[i];
            col.append(bar);
            bars.append(col);
            axis.append(textEl('', labels[i], 'span'));
        });
        return [bars, axis];
    }

    chartBuckets(h) {
        const DAY = 864e5, HOUR = 36e5, now = Date.now();
        const base = new Date(); base.setHours(0, 0, 0, 0);
        const start0 = base.getTime() - 13 * DAY;
        const days = new Array(14).fill(0), hours = new Array(24).fill(0);
        h.s.forEach(x => {
            let a = x.t, rem = x.d * 1000;
            while (rem > 0 && a <= now + DAY) {
                const dt = new Date(a), hs = new Date(a);
                hs.setMinutes(0, 0, 0);
                const part = Math.min(rem, hs.getTime() + HOUR - a);
                hours[dt.getHours()] += part;
                dt.setHours(0, 0, 0, 0);
                const di = Math.round((dt.getTime() - start0) / DAY);
                if (di >= 0 && di < 14) days[di] += part;
                a += part;
                rem -= part;
            }
        });
        return { days, hours, start0 };
    }

    chartTop(box, entries, l) {
        const top = entries.slice().sort((x, y) => y.s - x.s).slice(0, 5);
        if (!top.length) return null;
        const b3 = box(l.ch_top), tMax = Math.max(top[0].s, 1);
        top.forEach(e => {
            const row = mk('div', 'ch-row'), track = mk('div', 'ch-track'), fill = mk('div', 'ch-fill');
            fill.style.width = `${Math.max(2, Math.round(e.s / tMax * 100))}%`;
            track.append(fill);
            row.append(textEl('ch-name', e.n || e.k, 'span'), track, textEl('ch-val', fmtDur(e.s), 'span'));
            b3.append(row);
        });
        return b3;
    }

    charts(h, entries, l) {
        const DAY = 864e5;
        const { days, hours, start0 } = this.chartBuckets(h);
        const sec = ms => Math.round(ms / 1000);
        const box = (title, extra) => {
            const b = mk('div', 'ch-box'), t = mk('div', 'ch-ttl');
            t.append(textEl('', title, 'span'));
            if (extra) t.append(textEl('', extra, 'span'));
            b.append(t);
            return b;
        };
        const wrap = document.createDocumentFragment();

        const dayLbl = days.map((_, i) => new Date(start0 + i * DAY + DAY / 2).getDate());
        const dayTip = days.map((v, i) => `${this.dayName(start0 + i * DAY + DAY / 2)}: ${fmtDur(sec(v))}`);
        const avg = days.reduce((a, v) => a + v, 0) / 14;
        const b1 = box(l.ch_days, `${l.ch_avg}: ${fmtDur(sec(avg))}`);
        b1.append(...this.vbars(days, dayLbl, dayTip));
        wrap.append(b1);

        const hMax = Math.max(...hours);
        const peak = hMax > 0 ? hours.indexOf(hMax) : -1;
        const hrLbl = hours.map((_, i) => (i % 6 === 0 ? String(i) : ''));
        const hrTip = hours.map((v, i) => `${String(i).padStart(2, '0')}:00: ${fmtDur(sec(v))}`);
        const b2 = box(l.ch_hours, peak >= 0 ? `${l.ch_peak}: ${String(peak).padStart(2, '0')}:00` : '');
        b2.append(...this.vbars(hours, hrLbl, hrTip));
        wrap.append(b2);
        const b3 = this.chartTop(box, entries, l);
        if (b3) wrap.append(b3);
        return wrap;
    }

    dayName(ts) {
        try { return new Date(ts).toLocaleDateString(undefined, { weekday: 'short', day: 'numeric', month: 'short' }); } catch { return ''; }
    }

    render() {
        const l = L(), h = histLoad(), keep = this.list.scrollTop;
        const entries = Object.keys(h.m).map(k => ({ k, ...h.m[k] }));
        const total = entries.reduce((a, e) => a + e.s, 0);
        const cmps = {
            last: (x, y) => y.l - x.l,
            time: (x, y) => y.s - x.s,
            visits: (x, y) => (y.v - x.v) || (y.s - x.s)
        };
        entries.sort(cmps[this.sort]);
        const frag = document.createDocumentFragment();
        const sorts = mk('div', 'st-chips');
        [['last', l.hist_s_last], ['time', l.hist_s_time], ['visits', l.hist_s_vis]].forEach(([id, t]) => {
            const b = textEl(`bm-tbtn${this.sort === id ? ' on' : ''}`, t, 'button');
            b.onclick = () => { this.sort = id; Store.set('bc_hist_sort', id); this.render(); };
            sorts.append(b);
        });
        const chOn = Store.get('bc_hist_ch', false) === true;
        const chBtn = textEl(`bm-tbtn${chOn ? ' on' : ''}`, l.ch_btn, 'button');
        chBtn.onclick = () => { Store.set('bc_hist_ch', !chOn); this.render(); };
        sorts.append(mk('span', 'hs-sep'), chBtn);
        const clr = mk('button', 'bm-tbtn hs-clear');
        clr.innerHTML = icons.del;
        clr.append(textEl('', l.hist_clear, 'span'));
        clr.onclick = () => this.clear();
        const top = mk('div', 'st-chips hs-top');
        top.append(textEl('hd-hint', l.hist_total.replace('{n}', entries.length).replace('{t}', fmtDur(total))), clr);
        frag.append(top, sorts);
        if (!entries.length) frag.append(textEl('bm-empty', l.hist_empty));
        else if (chOn) frag.append(this.charts(h, entries, l));
        entries.slice(0, 300).forEach(e => frag.append(this.row(e, l)));
        this.list.replaceChildren(frag);
        this.list.scrollTop = keep;
    }
}

const STAR_SVG = '<svg viewBox="0 0 24 24"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>';
const THUMB_CFG = {
    bc: { links: ['a.thm_a', 'a.lst_link'], box: '.mls_item, .ls_thumb', views: '.bage_viewers, .lst_viewers', name: '' },
    sc: { links: ['a[data-model-list-link="true"]'], box: '[data-testid="model-list-item"]', views: '[class*="iewers"]', name: '[data-testid="model-list-item-username"]' },
    cb: { links: ['a.RoomCardThumbnail', 'a[data-testid="room-card-image-anchor"]'], box: '.RoomCard, li.room_list_room, li.roomCard', views: '.viewers', name: '[data-testid="room-card-username"], .username a' }
}[SITE] || { links: [], box: '', views: '', name: '' };
const THUMB_LINKS = THUMB_CFG.links;
const THUMB_SEL = THUMB_LINKS.join(',');
const THUMB_BOX = THUMB_CFG.box;
const viewersOf = a => {
    const t = THUMB_CFG.views ? ((THUMB_BOX && a.closest(THUMB_BOX)) || a).querySelector(THUMB_CFG.views)?.textContent || '' : '';
    const n = parseCount(t);
    return Number.isFinite(n) ? n : null;
};

class ThumbStars {
    constructor(bookmarks) {
        this.bm = bookmarks;
        this.on = Store.get('bc_star', true) !== false;
        this.timer = 0;
        const st = document.createElement('style');
        st.id = 'bcu-star-css';
        skinCss(st, STAR_CSS, STAR_SKIN);
        (document.head || document.documentElement).appendChild(st);
        this.bindGlobal();
        const resync = () => {
            try { syncBookmarks(); } catch {}
            this._idx = null;
            this.scan();
        };
        window.addEventListener('pageshow', e => { if (e.persisted) resync(); });
        onPageEvent('visibilitychange', () => { if (!realHidden()) resync(); });
        DomBus.subscribe((recs, isOwn) => { if (!isOwn()) this.schedule(); });
        setInterval(() => { if (!realHidden()) this.scan(); if (!realHidden()) this.checkPage(); }, 6000);
        this.schedule();
    }

    checkPage() {
        const key = pageKey();
        if (!key || !isFavKey(key)) return;
        const v = document.querySelector('video');
        if (v && isPlaying(v)) this.bm.markLive(key);
    }

    bindGlobal() {
        const starOf = e => (e.target && e.target.closest ? e.target.closest('.bcu-star') : null);
        const opt = { capture: true, passive: false };
        let down = null;
        const fire = star => {
            const a = star.parentElement;
            if (a) this.toggle(a);
        };
        const block = e => { e.stopImmediatePropagation(); e.stopPropagation(); if (e.cancelable) e.preventDefault(); };
        ['mousedown', 'mouseup', 'dblclick', 'auxclick', 'contextmenu'].forEach(t =>
            window.addEventListener(t, e => { if (starOf(e)) block(e); }, opt));
        ['touchstart', 'touchend', 'touchmove'].forEach(t =>
            window.addEventListener(t, e => { if (starOf(e)) { e.stopImmediatePropagation(); e.stopPropagation(); } }, { capture: true, passive: true }));
        window.addEventListener('pointerdown', e => {
            const st = starOf(e); if (!st) return;
            down = { st, x: e.clientX, y: e.clientY };
            block(e);
        }, opt);
        window.addEventListener('pointerup', e => {
            const st = starOf(e); if (!st) return;
            block(e);
            const ok = down && down.st === st && Math.abs(e.clientX - down.x) < 12 && Math.abs(e.clientY - down.y) < 12;
            down = null;
            if (!ok || (e.button != null && e.button > 0)) return;
            st._t = Date.now();
            fire(st);
        }, opt);
        window.addEventListener('click', e => {
            const st = starOf(e); if (!st) return;
            block(e);
            if (Date.now() - (st._t || 0) > 800) fire(st);
        }, opt);
    }

    schedule() {
        if (this.timer || !this.on) return;
        this.timer = setTimeout(() => { this.timer = 0; this.scan(); }, 350);
    }

    setEnabled(v) {
        this.on = !!v;
        Store.set('bc_star', this.on);
        this._idx = null;
        if (this.on) return this.scan();
        document.querySelectorAll('.bcu-star,.bcu-ring').forEach(x => x.remove());
        THUMB_LINKS.forEach(sel => document.querySelectorAll(sel).forEach(a => { a._bcu = null; a.classList.remove('bcu-fav'); }));
    }

    scan() {
        if (!this.on) return;
        if (!THUMB_LINKS.length) return;
        const bms = AppState.bookmarks;
        if (!this._idx || this._idxRef !== bms || this._idxLen !== bms.length) {
            this._idx = new Map(bms.map(b => [bmKey(b.url), b]));
            this._idxRef = bms; this._idxLen = bms.length;
        }
        const idx = this._idx;
        const links = document.querySelectorAll(THUMB_SEL);
        const pos = Array.from(links, a => (a._bcu ? null : getComputedStyle(a).position));
        links.forEach((a, i) => this.sync(a, idx, pos[i]));
    }

    keyOf(a) {
        const h = a.href;
        if (a._bcuH === h) return a._bcuK;
        const s = modelSlug(h);
        a._bcuK = s ? `${SITE}|${s.toLowerCase()}` : '';
        a._bcuH = h;
        return a._bcuK;
    }

    sync(a, idx, pos) {
        const key = this.keyOf(a);
        if (!key) return;
        const st = this.ensureStar(a, key, pos);
        const b = idx.get(key);
        this.paintFav(a, st, !!b);
        if (b) this.syncViewers(a, key, b);
    }

    ensureStar(a, key, pos) {
        const st = a._bcu;
        if (st && st.key === key && st.star.isConnected) return st;
        if (st) { st.star.remove(); st.ring.remove(); }
        return (a._bcu = this.make(a, key, pos ?? getComputedStyle(a).position));
    }

    paintFav(a, st, fav) {
        if (st.fav !== fav) {
            st.fav = fav;
            st.star.classList.toggle('on', fav);
            a.classList.toggle('bcu-fav', fav);
        }
        const tip = fav ? L().star_del : L().star_add;
        if (st.star.title !== tip) st.star.title = tip;
    }

    syncViewers(a, key, b) {
        const n = viewersOf(a);
        if (n == null) return;
        this.bm.markLive(key, b);
        if (n !== b.viewers) this.bm.setViewers(key, n, b);
    }

    make(a, key, pos) {
        if (pos === 'static') a.classList.add('bcu-host');
        const ring = document.createElement('span');
        ring.className = 'bcu-ring';
        const star = document.createElement('span');
        star.className = 'bcu-star';
        star.setAttribute('role', 'button');
        star.innerHTML = STAR_SVG;
        ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'touchstart', 'touchend', 'dblclick', 'contextmenu'].forEach(ev => star.addEventListener(ev, e => e.stopPropagation()));
        star.addEventListener('click', e => {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            this.toggle(a);
        });
        a.append(ring, star);
        return { key, star, ring, fav: null };
    }

    thumbImg(a) {
        return a.querySelector('img:not([class*="Blur"])') || a.querySelector('img');
    }

    attrName(a) {
        return a.getAttribute('data-name') || a.dataset.room || a.closest('[data-name]')?.getAttribute('data-name');
    }

    boxName(a) {
        const box = THUMB_BOX ? a.closest(THUMB_BOX) : null;
        return THUMB_CFG.name && box?.querySelector(THUMB_CFG.name)?.textContent.trim();
    }

    thumbName(a, img) {
        return this.attrName(a) || this.boxName(a) || (IS_BC && img?.alt) || modelSlug(a.href);
    }

    toggle(a) {
        const key = this.keyOf(a);
        if (!key) return;
        if (isFavKey(key)) return this.bm.removeKey(key, () => this.scan());
        const img = this.thumbImg(a);
        this.bm.addEntry({ name: this.thumbName(a, img), url: a.href, avatar: '', viewers: viewersOf(a) });
        this.scan();
    }
}

const THEME_SWATCHES_CLASSIC = ['rgba(220, 20, 60, 0.9)', 'rgba(233, 30, 99, 0.9)', 'rgba(156, 39, 176, 0.9)', 'rgba(63, 81, 181, 0.9)', 'rgba(3, 155, 229, 0.9)', 'rgba(0, 150, 136, 0.9)', 'rgba(67, 160, 71, 0.9)', 'rgba(245, 124, 0, 0.9)', 'rgba(96, 125, 139, 0.9)'];
const themeSwatches = () => (skinModern ? [SKIN_THEME.modern, ...THEME_SWATCHES_CLASSIC] : THEME_SWATCHES_CLASSIC);
const TILE_ICONS = {
    bBg: 'bgOn', bMute: 'vol', bShot: 'snap', bRec: 'rec', vol: 'volMed', bQual: 'qual', bPip: 'pip', bFs: 'fs', bThtr: 'thtr',
    bSearch: 'search', bHls: 'hls', bCopy: 'copy', bFav: 'fav', stats: 'eye', bHome: 'home', bRel: 'rel',
    bColor: 'color', bHideMgr: 'eyeOff', scale: 'grid', bDark: 'moon', bBkEx: 'dl', bBkIm: 'ul', bDon: 'coffee', hints: 'menu'
};
const colorToHsl = c => {
    try {
        const s = document.createElement('span');
        s.style.color = c;
        document.documentElement.appendChild(s);
        const v = getComputedStyle(s).color;
        s.remove();
        const m = v.match(/(\d+)\D+(\d+)\D+(\d+)/);
        if (!m) return SK([223, 87, 57], [348, 83, 47]);
        const r = m[1] / 255, g = m[2] / 255, b = m[3] / 255;
        const mx = Math.max(r, g, b), mn = Math.min(r, g, b), d = mx - mn, l = (mx + mn) / 2;
        let h = 0, sat = 0;
        if (d) {
            sat = d / (1 - Math.abs(2 * l - 1));
            h = mx === r ? ((g - b) / d) % 6 : mx === g ? (b - r) / d + 2 : (r - g) / d + 4;
            h = (h * 60 + 360) % 360;
        }
        return [Math.round(h), Math.round(sat * 100), Math.round(l * 100)];
    } catch { return SK([223, 87, 57], [348, 83, 47]); }
};

const noFade = (...els) => {
    const list = els.filter(Boolean);
    list.forEach(e => e.classList.add('bm-nofade'));
    requestAnimationFrame(() => requestAnimationFrame(() => list.forEach(e => e.classList.remove('bm-nofade'))));
};

const stBack = (ui, overlay) => {
    if (!overlay) return;
    const modal = overlay.querySelector('.bm-modal'), hdr = overlay.querySelector('.bm-hdr');
    if (!modal || !hdr) return;
    if (!overlay._bk) {
        const btn = mk('button', 'bm-close bm-back');
        btn.innerHTML = icons.back;
        btn.style.display = 'none';
        const sync = () => {
            btn.style.display = overlay._fromSt ? 'flex' : 'none';
            modal.classList.toggle('has-back', !!overlay._fromSt);
            btn.title = L().st_back || '';
        };
        btn.onclick = e => {
            e.stopPropagation();
            overlay._fromSt = false;
            sync();
            noFade(overlay, ui.settingsManager?.overlay);
            ui.settingsManager?.open();
            overlay.classList.remove('open');
        };
        overlay.addEventListener('click', e => {
            if (!overlay._fromSt) return;
            const t = e.target;
            const leave = t === overlay
                ? Date.now() - (overlay._stAt || 0) > 400
                : !!(t.closest && t.closest('.bm-close') && !t.closest('.bm-back'));
            if (leave) { overlay._fromSt = false; sync(); }
        }, true);
        hdr.appendChild(btn);
        overlay._bk = sync;
    }
    overlay._fromSt = true;
    overlay._stAt = Date.now();
    overlay._bk();
};

class SettingsManager {
    constructor(ui) {
        this.ui = ui;
        this.hslOpen = false;
        this.build();
    }

    build() {
        const sh = buildModalShell(this.ui.shadow, 'bm-modal rs-modal st-modal');
        this.overlay = sh.overlay;
        this.title = sh.title;
        this.body = sh.body;
        this.overlay.onclick = e => { if (e.target === this.overlay && Date.now() - (this._opAt || 0) > 500) this.close(); };
        sh.closeBtn.onclick = () => this.close();
    }

    open() {
        this.render();
        this._opAt = Date.now();
        this.overlay.classList.add('open');
    }

    close() {
        this.overlay.classList.remove('open');
    }

    go(open, getOverlay) {
        const prev = this.overlay;
        noFade(prev);
        open();
        const next = getOverlay();
        noFade(next);
        prev.classList.remove('open');
        stBack(this.ui, next);
    }

    updateLabels() {
        if (this.overlay.classList.contains('open')) this.render();
        this.ui.hideManager?.overlay?._bk?.();
        this.ui.recorder?._rs?.overlay?._bk?.();
        this.ui.sleepTimer?.modal?.overlay?._bk?.();
        this.ui.bookmarkManager?.overlay?._bk?.();
    }

    setTheme(c) {
        AppState.theme = c;
        Store.set('bc_theme', c);
        this.ui.host.style.setProperty('--th', c);
    }

    setSkin(v) {
        const prevDefault = SKIN_THEME[Skin.name];
        if (!Skin.set(v)) return;
        if (AppState.theme === prevDefault) this.setTheme(SKIN_THEME[Skin.name]);
        this.hslOpen = false;
        this.render();
    }

    randomTheme() {
        const r = (a, b) => Math.floor(a + Math.random() * (b - a));
        this.setTheme(`hsla(${r(0, 360)},${r(35, 55)}%,${r(35, 50)}%,0.85)`);
    }

    hslPicker(l, swRow) {
        const [h0, s0, l0] = colorToHsl(AppState.theme);
        const st = { h: h0, s: s0, l: clamp(l0, 15, 85) };
        const box = mk('div', 'st-hsl');
        const prev = mk('div', 'st-prev');
        const tracks = [];
        const slider = (label, key, max, min = 0) => {
            const wrap = mk('div', 'st-hrow');
            const inp = mk('input');
            inp.type = 'range';
            inp.min = String(min);
            inp.max = String(max);
            inp.step = '1';
            inp.value = String(st[key]);
            inp.oninput = () => {
                st[key] = +inp.value;
                this.setTheme(`hsla(${st.h},${st.s}%,${st.l}%,0.9)`);
                swRow.querySelectorAll('.st-swatch.sel').forEach(x => x.classList.remove('sel'));
                swRow.querySelector('.st-custom')?.classList.add('sel');
                paint();
            };
            wrap.append(textEl('', label, 'span'), inp);
            tracks.push([key, inp]);
            return wrap;
        };
        const paint = () => {
            tracks.forEach(([key, inp]) => {
                inp.style.background = key === 'h'
                    ? 'linear-gradient(90deg,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)'
                    : key === 's'
                        ? `linear-gradient(90deg,hsl(${st.h},0%,${st.l}%),hsl(${st.h},100%,${st.l}%))`
                        : `linear-gradient(90deg,hsl(${st.h},${st.s}%,15%),hsl(${st.h},${st.s}%,50%),hsl(${st.h},${st.s}%,85%))`;
            });
        };
        box.append(slider(l.st_hue, 'h', 360), slider(l.st_sat, 's', 100), slider(l.st_lig, 'l', 85, 15), prev);
        paint();
        return box;
    }

    renderKit(frag) {
        const ic = k => `<span class="st-ic">${icons[k] || ''}</span>`;
        const sec = (k, t) => {
            const e = mk('div', 'hd-sec');
            e.innerHTML = `${ic(k)}<span>${escHTML(t)}</span>`;
            frag.append(e);
        };
        const lbl = (k, t) => {
            const e = mk('span', 'st-lbl');
            e.innerHTML = `${ic(k)}<span>${escHTML(t)}</span>`;
            return e;
        };
        const row = (k, label, ...ctl) => {
            const r = mk('div', 'st-row');
            r.append(lbl(k, label), ...ctl);
            frag.append(r);
        };
        const pill = (t, fn, k, extra = '') => {
            const b = mk('button', `st-pill${extra}`);
            b.innerHTML = `${k ? icons[k] : ''}<span>${escHTML(t)}</span>`;
            b.onclick = fn;
            return b;
        };
        const sw = (on, fn) => {
            const b = mk('button', `bm-sw${on ? ' on' : ''}`);
            b.onclick = () => { on = !on; b.classList.toggle('on', on); fn(on); };
            return b;
        };
        return { frag, ic, sec, lbl, row, pill, sw };
    }

    stSkinBlock(k, l) {
        const { frag, lbl } = k;
        const box = mk('div', 'st-block');
        const chips = mk('div', 'st-chips');
        [['classic', l.st_skin_classic], ['modern', l.st_skin_modern]].forEach(([id, name]) => {
            const b = textEl(`bm-tbtn${Skin.name === id ? ' on' : ''}`, name, 'button');
            b.onclick = () => this.setSkin(id);
            chips.append(b);
        });
        box.append(lbl('color', l.st_skin), chips);
        frag.append(box);
    }

    stThemeBlock(k, l) {
        const { frag, lbl, pill } = k;
        const colBox = mk('div', 'st-block');
        const swRow = mk('div', 'st-sw-row');
        const swatches = themeSwatches();
        const known = swatches.includes(AppState.theme);
        swatches.forEach(c => {
            const s = mk('button', `st-swatch${AppState.theme === c ? ' sel' : ''}`);
            s.style.background = c;
            s.onclick = () => { this.setTheme(c); this.hslOpen = false; this.render(); };
            swRow.append(s);
        });
        const custom = mk('button', `st-swatch st-custom${!known ? ' sel' : ''}`);
        custom.title = l.st_custom;
        custom.onclick = () => { this.hslOpen = !this.hslOpen; this.render(); };
        swRow.append(custom, pill(l.st_color_rnd, () => { this.randomTheme(); this.hslOpen = false; this.render(); }, 'dice'));
        colBox.append(lbl('color', l.st_color), swRow);
        if (this.hslOpen) colBox.append(this.hslPicker(l, swRow));
        frag.append(colBox);
    }

    stLook(k, l, ui) {
        const { sec, row, pill, sw } = k;
        sec('color', l.st_look);
        row('globe', l.st_lang, pill(`${AppState.lang} · ${LANG_NAMES[AppState.lang] || ''}`, () => ui.openLangPicker()));
        this.stSkinBlock(k, l);
        this.stThemeBlock(k, l);
        const rng = mk('input', 'st-range');
        rng.type = 'range';
        rng.min = '60';
        rng.max = '150';
        rng.step = '10';
        rng.value = String(Math.round(AppState.scale * 100));
        const val = textEl('st-val', `${rng.value}%`, 'span');
        rng.oninput = () => { val.textContent = `${rng.value}%`; ui.setScale(rng.value / 100); };
        row('grid', l.st_scale, rng, val);
        row('moon', l.st_site_dark, sw(AppState.dark, v => ui.toggleDark(v)));
        row('eyeOff', l.hd_title, pill(l.st_open, () => this.go(() => ui.hideManager?.open(), () => ui.hideManager?.overlay)));
    }

    stButtons(k, l, ui) {
        const { frag, ic, sec, pill } = k;
        sec('grid', l.st_btns);
        frag.append(textEl('hd-hint', l.st_btns_hint));
        const tiles = mk('div', 'st-tiles');
        const hid = ui.hiddenButtons();
        ui.panelItems().forEach(([id, el, key]) => {
            if (!el) return;
            const name = l[key] || key;
            const t = mk('button', `st-tile${hid[id] ? '' : ' on'}`);
            t.title = name;
            t.innerHTML = `${ic(TILE_ICONS[id])}<span class="st-tl">${escHTML(name)}</span>`;
            t.onclick = () => {
                ui.setButtonHidden(id, t.classList.contains('on'));
                t.classList.toggle('on');
            };
            tiles.append(t);
        });
        frag.append(tiles, pill(l.st_btns_reset, () => { ui.resetButtons(); this.render(); }, 'rel', ' st-wide'));
    }

    stTools(k, l, ui) {
        const { sec, row, pill, sw } = k;
        sec('rec', l.st_rec);
        row('rec', l.rs_title, pill(l.st_open, () => this.go(() => ui.recorder?.openSettings(), () => ui.recorder?._rs?.overlay)));

        sec('grid', mvT('mv_title'));
        row('grid', mvT('mv_title'), pill(l.st_open, () => this.go(() => ui.multiView?.open(), () => ui.bookmarkManager?.overlay)));
        row('grid', mvT('mv_auto_all'), sw(Store.get('bc_mv_auto', true) !== false, v => ui.multiView?.setAuto(v)));
        row('grid', mvT('mv_bg'), sw(mvBg(), v => { Store.set('bc_mv_bg', v); if (ui.multiView?.pane) ui.multiView.labels(); }));

        sec('sleep', l.sleep_title);
        const slp = ui.sleepTimer;
        row('sleep', slp?.end ? `${l.sleep_left}: ${slp.fmt()}` : l.rs_off, pill(l.st_open, () => this.go(() => ui.sleepTimer?.open(), () => ui.sleepTimer?.modal?.overlay)));

        sec('target', l.rad_title);
        row('target', l.rad_title, sw(ui.radial?.on === true, v => ui.radial?.setEnabled(v)));
        row('rel', l.rad_reset, pill(l.cal_reset, () => ui.radial?.resetPos()));
    }

    stPreview(k) {
        const api = CBHP.api;
        if (SITE !== 'cb' || !api) return;
        const { sec, row, sw } = k;
        const ru = /^(RU|UK|BE)$/i.test(AppState.lang);
        const t = (a, b) => (ru ? a : b);
        const labels = { off: t('Выкл', 'Off'), anim: t('Обложка', 'Cover'), video: t('Видео', 'Video') };
        const chips = (items, cur, pick) => {
            const box = mk('div', 'st-chips');
            items.forEach(([val, text]) => {
                const b = textEl(`bm-tbtn${val === cur ? ' on' : ''}`, text, 'button');
                b.onclick = () => { pick(val); this.render(); };
                box.append(b);
            });
            return box;
        };
        sec('eye', t('Превью при наведении', 'Hover preview'));
        row('eye', t('Режим', 'Mode'), chips(api.modes.map(m => [m, labels[m]]), api.cfg.mode, api.setMode));
        row('vol', t('Звук', 'Sound'), sw(api.cfg.sound, v => api.setSound(v)));
        row('qual', t('Качество видео', 'Video quality'), chips(api.heights.map(h => [h, `${h}p`]), api.cfg.maxH, api.setMaxH));
    }

    stFavHist(k, l, ui) {
        const { sec, row, pill, sw } = k;
        sec('fav', l.st_fav);
        row('star', l.st_star, sw(ui.thumbStars?.on !== false, v => ui.thumbStars?.setEnabled(v)));
        row('bookmark', l.st_list, pill(l.st_open, () => this.go(() => ui.bookmarkManager?.open(), () => ui.bookmarkManager?.overlay)));

        sec('history', l.st_hist);
        row('eye', l.st_hist_on, sw(histOn(), v => Store.set('bc_hist_on', v)));
        row('list', l.st_list, pill(l.st_open, () => this.go(() => ui.historyManager?.open(), () => ui.bookmarkManager?.overlay)));
        row('cal', l.cal_title, pill(l.st_open, () => this.go(() => ui.bookmarkManager?.open('cal'), () => ui.bookmarkManager?.overlay)));
    }

    stData(k, l, ui) {
        const { frag, sec, lbl, row, pill } = k;
        sec('dl', l.st_data);
        const bkBox = mk('div', 'st-block');
        const bkChips = mk('div', 'st-chips');
        const bkParts = Backup.parts();
        [['s', l.bk_p_set], ['f', l.bk_p_fav], ['h', l.bk_p_hist]].forEach(([key, t]) => {
            const b = textEl(`bm-tbtn${bkParts[key] ? ' on' : ''}`, t, 'button');
            b.onclick = () => { if (Backup.setPart(key, !bkParts[key])) this.render(); };
            bkChips.append(b);
        });
        bkBox.append(lbl('dl', l.bk_parts), bkChips);
        frag.append(bkBox);
        row('dl', l.bk_exp, pill('JSON', () => Backup.export()));
        row('ul', l.bk_imp, pill('JSON', () => ui.backupImport()));
        const csvBox = mk('div', 'st-block');
        const csvChips = mk('div', 'st-chips');
        csvChips.append(
            pill(l.bk_csv_fav, () => Backup.exportFavCsv()),
            pill(l.bk_csv_hist, () => ui.historyManager?.exportCsv(false)),
            pill(l.bk_csv_ses, () => ui.historyManager?.exportCsv(true))
        );
        csvBox.append(lbl('table', l.bk_csv), csvChips);
        frag.append(csvBox);

        const pst = ParserDiag.stats();
        sec('list', pdT('diag'));
        row('list', `${pdT('title')}${pst.n ? ` (${pst.u}/${pst.n} ${pdT('nostatus')})` : ''}`, pill(l.st_open, () => ParserReport.open()));
    }

    render() {
        const l = L(), ui = this.ui, keep = this.body.scrollTop;
        this.title.innerHTML = `${icons.gear} ${escHTML(l.settings)}`;
        const frag = document.createDocumentFragment();
        const k = this.renderKit(frag);
        this.stLook(k, l, ui);
        this.stButtons(k, l, ui);
        this.stTools(k, l, ui);
        this.stPreview(k);
        this.stFavHist(k, l, ui);
        this.stData(k, l, ui);

        this.body.replaceChildren(frag);
        this.body.scrollTop = keep;
    }
}

const clampBarX = x => `${clamp(parseFloat(x) || 0, 0, Math.max(0, window.innerWidth - 40))}px`;
const clampBarB = v => `${clamp(Math.round(parseFloat(v) || 0), 0, Math.max(0, window.innerHeight - 40))}px`;

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

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

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

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

    buildShell() {
        this.host = document.createElement('div');
        this.host.id = 'bc-unchained-host';
        this.shadow = this.host.attachShadow({ mode: 'open' });
        if (document.documentElement.classList.contains('bc-dark-fb')) this.host.setAttribute('data-dfb', '');

        const style = document.createElement('style');
        skinCss(style, CSS_UI + CSS_X + CSS_NOSEL + CSS_POLISH, CSS_SKIN + CSS_POLISH);
        this.shadow.appendChild(style);

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

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

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

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

    setupFilterDrag() {
        const fs = { hasMoved: false, dragTarget: null, iL: 0, iT: 0, cFx: Store.get('bc_fx', null), cFb: readFb() };
        const applyPos = this.filterBarPositioner(fs);
        this.bindFilterBarDrag(fs, applyPos);
    }

    filterBarPositioner(fs) {
        const setImp = (el, props) => {
            const st = el.style;
            for (const k in props) {
                if (st.getPropertyValue(k) !== props[k] || st.getPropertyPriority(k) !== 'important') st.setProperty(k, props[k], 'important');
            }
        };
        const applyPos = () => {
            const b = document.querySelector('.bc_sticky_bar');
            if (!b || fs.dragTarget || realHidden() || window.innerHeight < 200) return;
            if (!(fs.cFx && fs.cFb)) return setImp(b, { left: '50%', transform: 'translateX(-50%)' });
            const x = clampBarX(fs.cFx), v = clampBarB(fs.cFb);
            setImp(b, { left: x, top: 'auto', bottom: v, right: 'auto', transform: 'none', margin: '0px', width: 'max-content' });
            if (!b.hasAttribute('data-bcu')) b.setAttribute('data-bcu', '1');
            if (!watched.has(b)) { watched.add(b); barObs.observe(b, { attributes: true, attributeFilter: ['style', 'class'] }); }
        };
        const watched = new WeakSet();
        let pend = false;
        const schedule = () => { if (pend) return; pend = true; requestAnimationFrame(() => { pend = false; applyPos(); }); };
        const barObs = new MutationObserver(schedule);
        DomBus.subscribe(muts => {
            for (const m of muts) for (const n of m.addedNodes) {
                if (n.nodeType === 1 && (n.matches?.('.bc_sticky_bar') || n.querySelector?.('.bc_sticky_bar'))) return schedule();
            }
        });
        const css0 = document.getElementById('bc-unchained-site-css');
        if (fs.cFx && fs.cFb && css0 && !css0.textContent.includes('data-bcu')) { css0.remove(); injectSiteCSS(); }
        setInterval(() => { if (!realHidden()) applyPos(); }, 5000);
        window.addEventListener('resize', applyPos, { passive: true });
        applyPos();
        return applyPos;
    }

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

            Helpers.trackDrag(e, {
                minDist: 4,
                onMove: (dx, dy) => {
                    if (!fs.dragTarget.isConnected) return;
                    fs.dragTarget.style.setProperty('left', `${fs.iL + dx}px`, 'important');
                    fs.dragTarget.style.setProperty('top', `${fs.iT + dy}px`, 'important');
                },
                onEnd: (moved) => {
                    fs.hasMoved = moved;
                    if (moved && fs.dragTarget?.isConnected) {
                        const r = fs.dragTarget.getBoundingClientRect();
                        fs.cFx = clampBarX(r.left);
                        fs.cFb = clampBarB(window.innerHeight - r.bottom);
                        Store.set('bc_fx', fs.cFx); Store.set('bc_fb', fs.cFb);
                    }
                    handle.style.setProperty('cursor', 'grab', 'important');
                    fs.dragTarget = null;
                    applyPos();
                }
            });
        }, { passive: false, capture: true });

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

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

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

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

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

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

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

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

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

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

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

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

        this.elements.bQual = Helpers.createButton(icons.qual, '');
        this.elements.bQual.onclick = () => this.toggleQuality();

        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);
    }

    bindQualityClose(qBlock) {
        if (qBlock._bcCloseBound) return;
        qBlock._bcCloseBound = true;
        qBlock.addEventListener('click', e => {
            if (!e.target.closest('.js-qsb_close, .qsb_close, .qsb_list_wrp, [class*="qsb"]')) return;
            setTimeout(() => { qBlock.classList.add('__hidden'); qBlock.style.display = 'none'; }, 50);
        });
    }

    clickQualityFallback() {
        const q = document.querySelector('#quality-btn, .player-resolution, button.vjs-icon-hd, .vjs-resolution-button, .vjs-cog-menu-button, .vjs-icon-cog, [class*="quality"], [class*="resolution"]');
        return q ? q.click() : this.flash(this.elements.bQual, `! ${L().err}`);
    }

    cbQualityMenu() {
        const old = document.getElementById('bc-cb-qual');
        if (old) { old.remove(); return true; }
        const btn = document.querySelector('button.vjs-icon-hd');
        const items = btn?.parentElement ? [...btn.parentElement.querySelectorAll('.vjs-menu-item')] : [];
        if (!items.length) return false;
        const wrap = document.createElement('div');
        wrap.id = 'bc-cb-qual';
        wrap.style.cssText = 'position:fixed;inset:0;z-index:2147483646;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.45)';
        const card = document.createElement('div');
        card.style.cssText = 'min-width:200px;max-width:86vw;padding:10px;border-radius:14px;background:rgba(15,15,18,.97);border:1px solid rgba(255,255,255,.12);box-shadow:0 10px 30px rgba(0,0,0,.7);display:flex;flex-direction:column;gap:6px;font:600 15px system-ui,sans-serif';
        items.forEach(li => {
            const on = li.classList.contains('vjs-selected') || li.getAttribute('aria-checked') === 'true';
            const b = document.createElement('button');
            b.type = 'button';
            b.textContent = (li.querySelector('.vjs-menu-item-text') || li).textContent.trim();
            b.style.cssText = `padding:12px 16px;border:1px solid rgba(255,255,255,.1);border-radius:10px;color:#fff;font:inherit;text-align:left;cursor:pointer;background:${on ? AppState.theme : 'rgba(255,255,255,.07)'}`;
            b.onclick = e => { e.stopPropagation(); li.click(); wrap.remove(); };
            card.appendChild(b);
        });
        wrap.onclick = e => { if (e.target === wrap) wrap.remove(); };
        wrap.appendChild(card);
        if (document.documentElement.classList.contains('bc-dark-fb')) wrap.style.filter = 'invert(1) hue-rotate(180deg)';
        document.documentElement.appendChild(wrap);
        return true;
    }

    toggleQuality() {
        const qBlock = document.querySelector('#quality_settings_block, .js-quality_settings_block');
        if (!qBlock) return this.cbQualityMenu() || this.clickQualityFallback();
        this.bindQualityClose(qBlock);
        const vis = isQualityShown(qBlock);
        qBlock.classList.toggle('__hidden', vis);
        qBlock.style.display = vis ? 'none' : 'block';
        if (vis) return;
        qBlock.style.zIndex = '2147483645';
        try { jq()?.('#quality-btn').trigger('click'); } catch {}
    }

    buildMisc() {
        const row = mk('div', 'grid-4');
        this.elements.bSearch = Helpers.createButton(icons.search, '');
        this.elements.bHls = Helpers.createButton(icons.hls, 'HLS');
        const hlsFlash = ok => this.flash(this.elements.bHls, ok ? 'OK' : 'ERR');
        Helpers.bindLongPress(this.elements.bHls,
            () => copyHls(getStreamUrl(), hlsFlash),
            () => shareHls(getStreamUrl(), `${currentSite.getName()} Stream`, hlsFlash)
        );
        this.elements.bCopy = Helpers.createButton(icons.copy, '');
        this.elements.bCopy.onclick = () => {
            const name = currentSite.getName();
            if (!name) return this.flash(this.elements.bCopy, 'ERR');
            const link = modelSlug(location.href) ? linkFor(location.href) : `${location.origin}${location.pathname}`;
            Helpers.copy(link, () => this.flash(this.elements.bCopy, 'OK'), () => this.flash(this.elements.bCopy, 'ERR'));
        };
        this.elements.bFav = Helpers.createButton(icons.fav, '');
        row.append(this.elements.bSearch, this.elements.bHls, this.elements.bCopy, this.elements.bFav);
        this.fullContainer.appendChild(row);
        const navRow = mk('div', 'favnav');
        this.elements.bFavPrev = Helpers.createButton(icons.favPrev, '');
        this.elements.bFavNext = Helpers.createButton(icons.favNext, '');
        navRow.append(this.elements.bFavPrev, this.elements.bFavNext);
        this.fullContainer.appendChild(navRow);
        this.elements.favNavRow = navRow;
        FavNav.hook = on => navRow.classList.toggle('on', on);
        FavNav.apply();
    }

    buildSettings() {
        this.fullContainer.appendChild(mk('div', 'sep'));
        this.buildLookRow();
        this.buildActionRow();
    }

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

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

        scaleCtrl.append(btnZoomOut, scaleVal, btnZoomIn);
        return scaleCtrl;
    }

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

        const scaleCtrl = this.buildScaleCtrl();
        const btnHide = mk('button', 'z-btn');
        btnHide.innerHTML = icons.eyeOff;
        btnHide.style.cssText = 'width:100%;height:24px;';
        btnHide.onclick = () => this.hideManager?.open();
        this.elements.bHideMgr = btnHide;
        row.style.cssText = 'display:grid;grid-template-columns:repeat(4,1fr);gap:6px;align-items:center';
        scaleCtrl.style.cssText = 'grid-column:span 2;justify-content:space-between';
        row.append(btnCol, btnHide, scaleCtrl);
        this.fullContainer.appendChild(row);
    }

    buildActionRow() {
        const row2 = mk('div', 'row-2');
        row2.style.cssText = 'display:flex;gap:6px';
        const sBtn = (key, fn) => {
            const b = mk('button', 'z-btn');
            b.style.cssText = 'flex:1;height:24px;font-size:10px;font-weight:700;display:flex;align-items:center;justify-content:center;white-space:nowrap';
            b.onclick = fn;
            this.elements[key] = b;
            return b;
        };
        row2.append(
            sBtn('bDark', () => this.toggleDark()),
            sBtn('bBkEx', () => { Backup.export(); this.flash(this.elements.bBkEx, 'OK'); }),
            sBtn('bBkIm', () => this.backupImport()),
            sBtn('bDon', () => this.openDonate())
        );
        this.fullContainer.appendChild(row2);

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

    panelItems() {
        const e = this.elements;
        return [
            ['bBg', e.bBg, 'bg'], ['bMute', e.bMute, 'sound'], ['bShot', e.bShot, 'snap'], ['bRec', e.bRec, 'rec'],
            ['vol', e.vIco?.parentElement, 'bt_vol'], ['bQual', e.bQual, 'qual'], ['bPip', e.bPip, 'pip'], ['bFs', e.bFs, 'fs'], ['bThtr', e.bThtr, 'thtr'],
            ['bSearch', e.bSearch, 'search'], ['bHls', e.bHls, 'HLS'], ['bCopy', e.bCopy, 'copy'], ['bFav', e.bFav, 'favs'],
            ['stats', e.stats, 'bt_stats'], ['bHome', e.bHome, 'home'], ['bRel', e.bRel, 'rel'],
            ['bColor', e.bColor, 'st_color'], ['bHideMgr', e.bHideMgr, 'hd_title'], ['scale', e.scaleCtrl, 'st_scale'],
            ['bDark', e.bDark, 'st_site_dark'], ['bBkEx', e.bBkEx, 'bk_exp'], ['bBkIm', e.bBkIm, 'bk_imp'], ['bDon', e.bDon, 'don_tip'],
            ['hints', e.hints, 'bt_hints']
        ];
    }

    hiddenButtons() {
        const o = Store.get('bc_btnhide', {});
        return o && typeof o === 'object' && !Array.isArray(o) ? o : {};
    }

    setButtonHidden(id, hide) {
        const o = { ...this.hiddenButtons() };
        if (hide) o[id] = true; else delete o[id];
        Store.set('bc_btnhide', o);
        this.applyButtons();
    }

    resetButtons() {
        Store.set('bc_btnhide', {});
        this.applyButtons();
    }

    applyButtons() {
        const hid = this.hiddenButtons();
        this.panelItems().forEach(([id, el]) => { if (el) el.classList.toggle('off', hid[id] === true); });
        [...this.fullContainer.children].filter(c => c.matches('.grid-4,.row-4,.row-2')).forEach(r => {
            const n = [...r.children].filter(c => !c.classList.contains('off')).length;
            r.classList.toggle('off', n === 0);
            if (r.matches('.grid-4')) r.style.gridTemplateColumns = n > 0 && n < 4 ? `repeat(${n},1fr)` : '';
        });
    }

    openLangPicker() {
        if (!this._lp) {
            const { overlay, title, closeBtn, body: list } = buildModalShell(this.shadow, 'bm-modal rs-modal lp-modal', 'bm-list lp-grid');
            closeBtn.onclick = () => overlay.classList.remove('open');
            overlay.onclick = e => { if (e.target === overlay && Date.now() - (this._lpAt || 0) > 400) overlay.classList.remove('open'); };
            this._lp = { overlay, title, list };
        }
        const { overlay, title, list } = this._lp;
        title.textContent = L().lang_title;
        const choose = (code, auto) => {
            AppState.langAuto = auto;
            Store.set('bc_lang_auto', auto);
            AppState.lang = auto ? detectLang() : code;
            if (!auto) Store.set('bc_lang', code);
            this.updateLabels();
            overlay.classList.remove('open');
        };
        const item = (code, text, on, fn, extra) => {
            const b = mk('button', `lp-btn${on ? ' sel' : ''}${extra ? ' ' + extra : ''}`);
            const c = mk('span', 'lp-code');
            c.textContent = code;
            const n = mk('span', 'lp-name');
            n.textContent = text;
            b.append(c, n);
            b.onclick = fn;
            return b;
        };
        const dl = detectLang();
        list.replaceChildren(
            item(dl, `${L().lang_auto} · ${LANG_NAMES[dl]}`, AppState.langAuto, () => choose('', true), 'lp-auto'),
            ...LANGS.map(c => item(c, LANG_NAMES[c], !AppState.langAuto && AppState.lang === c, () => choose(c, false)))
        );
        this._lpAt = Date.now();
        overlay.classList.add('open');
    }

    updateLabels() {
        const l = L();
        this.host.toggleAttribute('data-rtl', RTL_LANGS.includes(AppState.lang));
        this.elements.dragZone.textContent = '';
        this.elements.bLang.title = l.settings;
        this.elements.bHome.title = l.home;
        this.elements.bRel.title = l.rel;
        this.elements.bMin.title = l.min;
        this.elements.bHide.title = l.hide;

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

        this.elements.hints.innerHTML = l.hints;
        if (this.elements.bRec) this.elements.bRec.title = l.rec_tip;
        const e = this.elements;
        if (e.bDark) { e.bDark.innerHTML = (AppState.dark ? icons.moon : icons.sun); e.bDark.title = l.dark; e.bDark.style.background = AppState.dark ? 'linear-gradient(145deg,var(--th),rgba(255,255,255,.14))' : ''; e.bDark.style.boxShadow = AppState.dark ? 'inset 0 0 0 1px rgba(255,255,255,.22)' : ''; }
        [['bBkEx', 'dl', 'bk_exp'], ['bBkIm', 'ul', 'bk_imp'], ['bDon', 'coffee', 'don_tip']].forEach(([k, ico, tk]) => {
            if (e[k]) { e[k].innerHTML = icons[ico]; e[k].title = l[tk]; }
        });
        if (e.bDon) e.bDon.style.color = '#ffb347';
        this.bookmarkManager?.updateLabels();
        this.hideManager?.updateLabels();
        this.settingsManager?.updateLabels();
        this.historyManager?.updateLabels();
    }

    toggleDark(on = !AppState.dark) {
        AppState.dark = !!on;
        Store.set('bc_dark', AppState.dark);
        applyDark();
        this.updateLabels();
    }

    backupImport() {
        Backup.import(
            () => { this.flash(this.elements.bBkIm, 'OK'); setTimeout(() => location.reload(), 700); },
            () => this.flash(this.elements.bBkIm, 'ERR')
        );
    }

    donateWallets() {
        return [
            { name: 'TON', net: 'Gram · USDT', sym: 'T', svg: '<svg viewBox="12 14 32 32" width="26" height="26" xmlns="http://www.w3.org/2000/svg" style="display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.3))"><path fill="#fff" d="M37.56 15.63H18.44c-3.52 0-5.75 3.79-3.98 6.86l11.8 20.45c.77 1.34 2.7 1.34 3.47 0l11.8-20.45c1.77-3.06-.46-6.86-3.97-6.86zM26.25 36.81l-2.57-4.98-6.2-11.09c-.41-.71.1-1.62.95-1.62h7.82v17.69zm12.26-16.07-6.2 11.1-2.57 4.97V19.12h7.82c.86 0 1.36.91.95 1.62z"/></svg>', c1: '#2aa6f0', c2: '#0b6fc2', addr: 'UQAhsfD61tpWIWI_VUnvJzDXE5LkfZQd1uZoWFO0uDEY81Nq' },
            { name: 'Litecoin', net: 'LTC', sym: 'Ł', svg: '<svg viewBox="0 0 24 24" width="26" height="26" xmlns="http://www.w3.org/2000/svg" style="display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.3))"><path d="M10.8 4.8 8.6 16.4h7.4" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/><path d="M6.4 12.9l8.2-3.3" fill="none" stroke="#fff" stroke-width="2.1" stroke-linecap="round"/></svg>', c1: '#9aa7b8', c2: '#4b5a70', addr: 'ltc1q7kdhehwusk6qupe325trqe9xjhp6qq4rpgy88j' },
            { name: 'BNB Smart Chain', net: 'BEP-20 · USDT · BNB', sym: 'B', svg: '<svg viewBox="0 0 24 24" width="26" height="26" xmlns="http://www.w3.org/2000/svg" style="display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.3))"><g fill="#fff"><path d="M12 3 7.5 7.5l1.7 1.7L12 6.4l2.8 2.8 1.7-1.7zM12 21l-4.5-4.5 1.7-1.7 2.8 2.8 2.8-2.8 1.7 1.7z"/><path d="M5.2 9.9 7.3 12l-2.1 2.1L3.1 12zM18.8 9.9 20.9 12l-2.1 2.1-2.1-2.1zM12 9.2 14.8 12 12 14.8 9.2 12z"/></g></svg>', c1: '#f8cf4d', c2: '#c9930a', addr: '0xC1602C8CE455A4f7E694DF8524f59317de375310' },
            { name: 'Ethereum', net: 'ERC-20 · ETH', sym: 'Ξ', svg: '<svg viewBox="0 0 256 417" width="17" height="28" xmlns="http://www.w3.org/2000/svg" style="display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.3))"><path fill="#fff" fill-opacity=".6" d="M127.96 0v154.16l128 56.16z"/><path fill="#fff" d="M127.96 0 0 210.32l127.96-56.16z"/><path fill="#fff" fill-opacity=".6" d="M127.96 312.19V417L256 236.59z"/><path fill="#fff" d="M127.96 417V312.19L0 236.59z"/><path fill="#fff" fill-opacity=".2" d="m127.96 287.96 128-75.64-128-58.16z"/><path fill="#fff" fill-opacity=".6" d="m0 212.32 127.96 75.64V154.16z"/></svg>', c1: '#8a9bff', c2: '#4a5bd0', addr: '0xC1602C8CE455A4f7E694DF8524f59317de375310' },
            { name: 'Solana', net: 'SOL · USDT', sym: 'S', svg: '<svg viewBox="0 0 398 312" width="24" height="19" xmlns="http://www.w3.org/2000/svg" style="display:block;filter:drop-shadow(0 1px 2px rgba(0,0,0,.3))"><g fill="#fff"><path d="M64.6 237.9c2.4-2.4 5.7-3.8 9.2-3.8h317.4c5.8 0 8.7 7 4.6 11.1l-62.7 62.7c-2.4 2.4-5.7 3.8-9.2 3.8H6.5c-5.8 0-8.7-7-4.6-11.1l62.7-62.7z"/><path d="M64.6 3.8C67.1 1.4 70.4 0 73.8 0h317.4c5.8 0 8.7 7 4.6 11.1l-62.7 62.7c-2.4 2.4-5.7 3.8-9.2 3.8H6.5c-5.8 0-8.7-7-4.6-11.1L64.6 3.8z"/><path d="M333.1 120.1c-2.4-2.4-5.7-3.8-9.2-3.8H6.5c-5.8 0-8.7 7-4.6 11.1l62.7 62.7c2.4 2.4 5.7 3.8 9.2 3.8h317.4c5.8 0 8.7-7 4.6-11.1l-62.7-62.7z"/></g></svg>', c1: '#9945ff', c2: '#14c9a0', addr: '7gp6seGi5aVJiKbMKxztPX8NBbtFsySRPfwmWmDEFM6P' }
        ];
    }

    donateStyle() {
        const css = document.createElement('style');
        css.textContent = SK('.dn-steam{display:none}@keyframes dnSteam{0%{transform:translateY(6px) scaleX(.8);opacity:0}25%{opacity:.55}60%{transform:translateY(-6px) scaleX(1.15);opacity:.35}100%{transform:translateY(-16px) scaleX(.9);opacity:0}}@keyframes dnFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}@keyframes dnPop{0%{transform:scale(.92);opacity:0}100%{transform:scale(1);opacity:1}}.dn-steam i{position:absolute;bottom:100%;width:2px;height:14px;border-radius:1px;background:linear-gradient(rgba(255,226,180,0),rgba(255,226,180,.75) 55%,rgba(255,226,180,0));filter:blur(.4px);animation:dnSteam 3.4s cubic-bezier(.45,0,.55,1) infinite}.dn-row{transition:border-color .25s,background .25s,transform .15s}.dn-row:active{transform:scale(.985)}.dn-cp{transition:background .2s,color .2s}.dn-cp:hover{background:rgba(255,255,255,.06)!important;color:#fff!important}.dn-cp svg{margin:0!important;vertical-align:baseline!important;display:block}.dn-row:hover:not(.dn-ok){background:rgba(255,255,255,.05)!important;border-color:var(--c)!important}.dn-row:active:not(.dn-ok){background:rgba(255,255,255,.07)!important;border-color:var(--c)!important}.dn-row.dn-ok{background:rgba(16,185,129,.12)!important;border-color:rgba(16,185,129,.5)!important}', '@keyframes dnSteam{0%{transform:translateY(6px) scaleX(.8);opacity:0}25%{opacity:.55}60%{transform:translateY(-6px) scaleX(1.15);opacity:.35}100%{transform:translateY(-16px) scaleX(.9);opacity:0}}@keyframes dnFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}@keyframes dnPop{0%{transform:scale(.92);opacity:0}100%{transform:scale(1);opacity:1}}.dn-steam i{position:absolute;bottom:100%;width:2px;height:14px;border-radius:1px;background:linear-gradient(rgba(255,226,180,0),rgba(255,226,180,.75) 55%,rgba(255,226,180,0));filter:blur(.4px);animation:dnSteam 3.4s cubic-bezier(.45,0,.55,1) infinite}.dn-row{transition:border-color .25s,background .25s,transform .15s}.dn-row:active{transform:scale(.985)}.dn-cp{transition:background .2s,color .2s}.dn-cp:hover{background:rgba(255,255,255,.14)!important;color:#fff!important}.dn-cp svg{margin:0!important;vertical-align:baseline!important;display:block}.dn-row:hover:not(.dn-ok){background:rgba(255,255,255,.075)!important;border-color:var(--c)!important}.dn-row:active:not(.dn-ok){background:rgba(255,255,255,.1)!important;border-color:var(--c)!important}.dn-row.dn-ok{background:rgba(61,220,132,.08)!important;border-color:rgba(61,220,132,.7)!important}');
        return css;
    }

    donateHead(tx) {
        const top = mk('div');
        top.style.cssText = 'position:relative;display:flex;flex-direction:column;align-items:center;text-align:center;padding:26px 20px 14px';
        const cup = mk('div');
        cup.style.cssText = SK('position:relative;width:64px;height:64px;border-radius:18px;display:flex;align-items:center;justify-content:center;color:#fff;background:var(--th);box-shadow:inset 0 1px rgba(255,255,255,.16),0 8px 24px -10px rgba(0,0,0,.8);margin-bottom:14px', 'position:relative;width:68px;height:68px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;background:linear-gradient(145deg,#ffb347,#ff7a18);box-shadow:0 8px 24px rgba(255,122,24,.45),inset 0 1px 0 rgba(255,255,255,.35);margin-bottom:14px');
        cup.innerHTML = icons.coffee.replace(/width="\d+" height="\d+"/, 'width="34" height="34"').replace('stroke-width="2"', 'stroke-width="1.8"');
        const cupSvg = cup.querySelector('svg');
        if (cupSvg) cupSvg.style.transform = 'translateX(2px)';
        const steam = mk('div', 'dn-steam');
        steam.style.cssText = 'position:absolute;left:0;right:0;top:6px;height:0;pointer-events:none';
        [[26, 0], [32, .7], [38, 1.4]].forEach(([x, d]) => { const i = document.createElement('i'); i.style.cssText = `left:${x}px;animation-delay:${d}s`; steam.appendChild(i); });
        cup.appendChild(steam);
        const h = mk('div');
        h.style.cssText = SK('color:#f5f5f5;font-size:16px;font-weight:600;letter-spacing:0', 'color:#fff;font-size:18px;font-weight:800;letter-spacing:.2px');
        h.textContent = tx.title;
        const p = mk('div');
        p.style.cssText = SK('color:#8a8a8a;font-size:12.5px;line-height:1.45;margin-top:6px;max-width:290px', 'color:#b9b3ad;font-size:12.5px;line-height:1.45;margin-top:6px;max-width:290px');
        p.textContent = tx.sub;
        const mascot = mk('div');
        mascot.style.cssText = SK('position:absolute;left:calc(50% + 30px);top:20px;width:24px;height:24px;pointer-events:none;animation:dnFloat 2.6s ease-in-out infinite', 'position:absolute;left:calc(50% + 30px);top:20px;width:24px;height:24px;pointer-events:none;filter:drop-shadow(0 2px 6px rgba(255,93,115,.55));animation:dnFloat 2.6s ease-in-out infinite');
        mascot.innerHTML = '<svg viewBox="0 0 24 24" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg"><path d="M12 21s-8-5.2-8-11a4.6 4.6 0 0 1 8-3.1A4.6 4.6 0 0 1 20 10c0 5.8-8 11-8 11z" fill="#ff5d73"/><path d="M7.5 8.5c.4-1.2 1.4-1.8 2.5-1.7" fill="none" stroke="#fff" stroke-width="1.4" stroke-linecap="round" opacity=".6"/></svg>';
        top.append(mascot, cup, h, p);
        return top;
    }

    donateRow(w, tx, toast) {
        const icCopy = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
        const icOk = SK('<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#34d399" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>', '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="#3ddc84" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>');
        const short = a => a.length > 20 ? `${a.slice(0, 8)}…${a.slice(-8)}` : a;

        const row = mk('div', 'dn-row');
        row.style.cssText = SK('display:flex;align-items:center;gap:12px;padding:11px 12px;border-radius:10px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06)', 'display:flex;align-items:center;gap:12px;padding:11px 12px;border-radius:12px;background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.08)');
        row.style.setProperty('--c', w.c1);
        const badge = mk('div');
        badge.style.cssText = `flex:0 0 40px;width:40px;height:40px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:18px;background:linear-gradient(145deg,${w.c1},${w.c2});box-shadow:0 4px 12px rgba(0,0,0,.35)`;
        if (w.svg) badge.innerHTML = w.svg; else badge.textContent = w.sym;
        const mid = mk('div');
        mid.style.cssText = 'flex:1;min-width:0;display:flex;flex-direction:column;gap:2px';
        const nm = mk('div');
        nm.style.cssText = SK('color:#f5f5f5;font-size:13.5px;font-weight:600;line-height:1.2', 'color:#fff;font-size:13.5px;font-weight:700;line-height:1.2');
        nm.textContent = w.name;
        const ad = mk('div');
        ad.style.cssText = SK('margin-top:3px;color:#a3a3a3;font:12px/1.35 ui-monospace,Menlo,Consolas,monospace;cursor:pointer;word-break:break-all', 'margin-top:3px;color:#ffc87a;font:12px/1.35 ui-monospace,Menlo,Consolas,monospace;cursor:pointer;word-break:break-all');
        let full = false;
        ad.textContent = short(w.addr);
        ad.onclick = () => { full = !full; ad.textContent = full ? w.addr : short(w.addr); };
        mid.append(nm, ad);
        const cp = mk('button', 'dn-cp');
        cp.innerHTML = icCopy;
        cp.style.cssText = SK('flex:0 0 44px;width:44px;height:44px;border-radius:8px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.03);color:#d4d4d4;display:flex;align-items:center;justify-content:center;cursor:pointer', 'flex:0 0 44px;width:44px;height:44px;border-radius:10px;border:1px solid rgba(255,255,255,.12);background:rgba(255,255,255,.07);color:#ddd;display:flex;align-items:center;justify-content:center;cursor:pointer');
        cp.onclick = () => Helpers.copy(w.addr, () => {
            cp.innerHTML = icOk;
            row.classList.add('dn-ok');
            setTimeout(() => { if (cp.isConnected) { cp.innerHTML = icCopy; row.classList.remove('dn-ok'); } }, 1400);
        }, () => toast(tx.fail, false));
        row.append(badge, mid, cp);
        return row;
    }

    donateToast() {
        const toastEl = mk('div');
        toastEl.style.cssText = 'position:sticky;bottom:0;height:0;display:flex;justify-content:center;overflow:visible';
        const toastIn = mk('div');
        toastIn.style.cssText = SK('position:absolute;bottom:12px;padding:8px 14px;border-radius:10px;font-size:13px;font-weight:500;color:#fff;background:#059669;box-shadow:0 8px 24px -8px rgba(0,0,0,.7);opacity:0;transform:translateY(8px);transition:.25s;pointer-events:none;white-space:nowrap', 'position:absolute;bottom:12px;padding:8px 16px;border-radius:999px;font-size:12.5px;font-weight:700;color:#fff;background:#1f9d5c;box-shadow:0 6px 18px rgba(0,0,0,.5);opacity:0;transform:translateY(8px);transition:.25s;pointer-events:none;white-space:nowrap');
        toastEl.appendChild(toastIn);
        let tt = 0;
        const toast = (msg, ok) => {
            toastIn.textContent = msg;
            toastIn.style.background = SK(ok ? '#059669' : '#dc2626', ok ? '#1f9d5c' : '#c0392b');
            toastIn.style.opacity = '1';
            toastIn.style.transform = 'none';
            clearTimeout(tt);
            tt = setTimeout(() => { toastIn.style.opacity = '0'; toastIn.style.transform = 'translateY(8px)'; }, 1500);
        };
        return { el: toastEl, toast };
    }

    openDonate() {
        const l = L();
        const tx = { title: l.don_title, sub: l.don_sub, copied: l.don_copied, fail: l.don_fail, tap: l.don_tap };
        if (this._donate && this._donate.lang !== AppState.lang) { this._donate.overlay.remove(); this._donate = null; }
        if (this._donate) {
            this._donate.overlay.classList.add('open');
            return;
        }
        const overlay = mk('div', 'bm-overlay');
        overlay.onclick = ev => { if (ev.target === overlay) overlay.classList.remove('open'); };
        overlay.appendChild(this.donateStyle());

        const dnFs = document.createElement('style');
        dnFs.textContent = '@media (max-width:520px){.bm-overlay:has(.dn-modal){align-items:stretch}.bm-modal.dn-modal{height:100%!important;max-height:100%!important;min-height:0!important;width:100vw!important;max-width:100vw!important;border-radius:0!important;border-left:0!important;border-right:0!important;border-bottom:0!important;align-self:stretch!important;overflow-y:auto!important}.dn-modal>*{flex-shrink:0}.dn-modal>:nth-child(2){padding-top:18px!important;padding-bottom:8px!important}}';
        overlay.appendChild(dnFs);

        const modal = mk('div', 'bm-modal');
        modal.classList.add('dn-modal');
        modal.style.cssText = SK('position:relative;height:auto;min-height:0;max-height:92vh;width:370px;max-width:94vw;overflow:hidden auto;border:1px solid rgba(255,255,255,.08);border-radius:18px;background:#111111!important;box-shadow:inset 0 1px rgba(255,255,255,.04),0 24px 72px -20px rgba(0,0,0,.9);animation:dnPop .2s ease', 'position:relative;height:auto;min-height:0;max-height:92vh;width:370px;max-width:94vw;overflow:hidden auto;border:1px solid rgba(255,179,71,.28);border-radius:20px;background:linear-gradient(180deg,#2a1d14 0%,#17161b 38%,#131317 100%)!important;box-shadow:0 20px 60px rgba(0,0,0,.65),0 0 60px rgba(255,159,28,.12);animation:dnPop .28s ease');

        const close = mk('button', 'bm-close');
        close.innerHTML = '×';
        close.style.cssText += ';position:absolute;top:10px;right:10px;inset-inline-end:10px;transform:none;z-index:2';
        close.onclick = () => overlay.classList.remove('open');

        const top = this.donateHead(tx);
        const { el: toastEl, toast } = this.donateToast();

        const list = mk('div');
        list.style.cssText = 'display:flex;flex-direction:column;gap:9px;padding:8px 14px 8px';
        this.donateWallets().forEach(w => list.appendChild(this.donateRow(w, tx, toast)));

        const refBox = mk('div');
        refBox.style.cssText = 'display:flex;flex-direction:column;gap:7px;padding:10px 14px 2px';
        const refLbl = mk('div');
        refLbl.style.cssText = 'color:#8a8a8a;font-size:11px;font-weight:600;letter-spacing:.4px;text-transform:uppercase;padding:0 4px';
        refLbl.textContent = supT('or');
        const refRow = this.donateRow({ name: 'Chaturbate', sym: 'C', c1: '#ff9a3c', c2: '#e8590c', addr: '' }, tx, toast);
        const refAd = refRow.children[1].children[1];
        refAd.textContent = supT('sub');
        refAd.style.wordBreak = 'normal';
        refAd.style.fontFamily = 'inherit';
        refAd.style.fontSize = '12.5px';
        refAd.style.lineHeight = '1.35';
        refAd.onclick = openSupportLink;
        const refBtn = refRow.children[2];
        refBtn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h6v6"/><path d="M20 4l-9 9"/><path d="M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/></svg>';
        refBtn.title = supT('btn');
        refBtn.onclick = openSupportLink;
        refRow.style.cursor = 'pointer';
        refRow.onclick = e => { if (!e.target.closest('.dn-cp') && e.target !== refAd) openSupportLink(); };
        refBox.append(refLbl, refRow);
        const refNote = mk('div');
        refNote.style.cssText = 'color:#6f6a64;font-size:10.5px;line-height:1.4;padding:2px 20px 8px;text-align:center';
        refNote.textContent = supT('note');

        const hint = mk('div');
        hint.style.cssText = SK('text-align:center;color:#6b6b6b;font-size:10.5px;padding:2px 16px 8px', 'text-align:center;color:#6f6a64;font-size:10.5px;padding:2px 16px 8px');
        hint.textContent = tx.tap;

        modal.append(close, top, refBox, refNote, list, hint, toastEl);
        overlay.appendChild(modal);
        this.shadow.appendChild(overlay);
        this._donate = { overlay, lang: AppState.lang };
        requestAnimationFrame(() => overlay.classList.add('open'));
    }

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

    ensureVisible() {
        try {
            if (!this.host) return;
            if (!this.host.isConnected) document.documentElement.appendChild(this.host);
            const cs = getComputedStyle(this.host);
            if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') {
                ['display:block', 'visibility:visible', 'opacity:1'].forEach(d => { const [k, v] = d.split(':'); this.host.style.setProperty(k, v, 'important'); });
            }
            if (bcPtrDown || realHidden() || window.innerWidth < 100 || window.innerHeight < 200) return;
            const el = AppState.isHidden ? this.ghostBtn : AppState.isMinimized ? this.restoreBtn : this.panel;
            const r = el.getBoundingClientRect();
            const off = r.width < 1 || r.height < 1 || r.right < 24 || r.bottom < 24 || r.left > window.innerWidth - 24 || r.top > window.innerHeight - 24;
            if (off) this.resetPlacement();
        } catch {}
    }

    resetPlacement() {
        AppState.hasPos = false;
        Store.set('bc_pos', false);
        this.applyPosition();
    }

    resetPanel() {
        this.toggleHide(false);
        this.toggleMinimize(false, true);
        AppState.scale = 1;
        Store.set('bc_scale', 1);
        this.panel.style.transform = 'scale(1)';
        this.resetPlacement();
        setTimeout(() => this.ensureVisible(), 300);
    }

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

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

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

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

        this.fullContainer.style.display = minimized ? 'none' : 'flex';
        this.restoreBtn.style.display = minimized ? 'flex' : 'none';
        this.restoreBtn.style.transformOrigin = 'top left';
        this.restoreBtn.style.transform = `scale(${1 / AppState.scale})`;
        this.panel.className = minimized ? 'panel mini' : 'panel';
        if (!initial) this.clampPosition();
    }

    hostTop() { return this.host.getBoundingClientRect().top || 0; }

    onDragStart(e) {
        if (!isPrimaryPress(e)) return;
        e.preventDefault();
        const rect = this.panel.getBoundingClientRect();
        const iL = rect.left, iT = rect.top - this.hostTop();
        this.panel.style.cssText += `;left:${iL}px;top:${iT}px;right:auto;bottom:auto`;
        this.panel.classList.add('dragging');

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

    setupDrag() {
        let swallowClick = false;
        this.panel.addEventListener('click', ev => {
            if (!swallowClick) return;
            swallowClick = false;
            ev.stopPropagation();
            ev.preventDefault();
        }, true);
        this.panel.addEventListener('pointerdown', e => {
            if (AppState.isMinimized) return;
            if (!isPrimaryPress(e)) return;
            if (e.target.closest('input, textarea, .vslider, .resize-handle, .bm-overlay')) return;
            if (!e.target.closest('button, a, .btn, .h-btn, .z-btn')) { this.onDragStart(e); return; }
            const sx = e.clientX, sy = e.clientY, pid = e.pointerId, tgt = e.target;
            const stop = () => {
                window.removeEventListener('pointermove', mv, true);
                window.removeEventListener('pointerup', stop, true);
                window.removeEventListener('pointercancel', stop, true);
            };
            const mv = ev => {
                if (ev.pointerId !== pid || Math.hypot(ev.clientX - sx, ev.clientY - sy) < 10) return;
                stop();
                swallowClick = true;
                window.addEventListener('pointerup', () => setTimeout(() => { swallowClick = false; }, 350), { once: true, capture: true });
                try { tgt.dispatchEvent(new PointerEvent('pointercancel', { pointerId: pid, bubbles: true })); } catch {}
                this.onDragStart(e);
            };
            window.addEventListener('pointermove', mv, { capture: true, passive: true });
            window.addEventListener('pointerup', stop, true);
            window.addEventListener('pointercancel', stop, true);
        });
    }

    buildResizeHandles() {
        this.resizeHandle = mk('div', 'resize-handle');
        this.resizeHandle.dataset.edge = 'se';
        const edges = ['n', 's', 'e', 'w', 'nw', 'ne', 'sw'].map(k => {
            const d = mk('div', `rs-edge rs-${k}`);
            d.dataset.edge = k;
            return d;
        });
        this.panel.append(this.resizeHandle, ...edges);
        return [this.resizeHandle, ...edges];
    }

    startResize(e) {
        if (!isPrimaryPress(e)) return;
        e.preventDefault();
        e.stopPropagation();
        const edge = e.currentTarget.dataset.edge;
        const hasN = edge.includes('n'), hasS = edge.includes('s'), hasW = edge.includes('w'), hasE = edge.includes('e');
        const rect = this.panel.getBoundingClientRect();
        const s0 = AppState.scale, w0 = rect.width, h0 = rect.height;
        const scaleVal = this.shadow.querySelector('.s-val');
        const ht = this.hostTop();
        this.panel.style.left = `${rect.left}px`;
        this.panel.style.top = `${rect.top - ht}px`;
        this.panel.style.right = 'auto';
        this.panel.style.bottom = 'auto';

        let raf = 0, ns = s0;
        const paint = () => {
            raf = 0;
            const f = ns / s0;
            this.panel.style.transform = `scale(${ns})`;
            this.panel.style.left = `${hasW ? rect.right - w0 * f : rect.left}px`;
            this.panel.style.top = `${(hasN ? rect.bottom - h0 * f : rect.top) - ht}px`;
            if (scaleVal) scaleVal.textContent = `${Math.round(ns * 100)}%`;
        };
        Helpers.trackDrag(e, {
            minDist: 1,
            onMove: (dx, dy) => {
                const kx = axisK(hasE, hasW, w0, dx), ky = axisK(hasS, hasN, h0, dy);
                ns = clamp(s0 * (kx !== null && ky !== null ? (kx + ky) / 2 : kx ?? ky), 0.6, 1.5);
                AppState.scale = ns;
                if (!raf) raf = requestAnimationFrame(paint);
            },
            onEnd: moved => {
                if (!moved) return;
                if (raf) { cancelAnimationFrame(raf); paint(); }
                Store.set('bc_scale', ns);
                this.clampPosition();
                this.savePos(this.panel.style.left, this.panel.style.top);
            }
        });
    }

    setupResize() {
        this.buildResizeHandles().forEach(el => el.addEventListener('pointerdown', e => this.startResize(e)));
    }

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

    applyPosition() {
        if (AppState.hasPos && AppState.posX && AppState.posY) {
            const py = parseFloat(AppState.posY);
            if (py >= 0 && window.innerHeight >= 200) { AppState.posY = `${Math.round(py - window.innerHeight)}px`; Store.set('bc_y', 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 = '10vh';
            this.panel.style.left = 'auto';
            this.panel.style.top = 'auto';
        }
    }

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

    injectSiteCSS() {
        injectSiteCSS();
        ensureHideCSS();
    }
}

class BgSaver {
    constructor(videoController, recorder) {
        this.vc = videoController;
        this.rec = recorder;
        this.timer = 0;
        this.paused = null;
        this.lowered = null;
        onPageEvent('visibilitychange', () => this.onVis());
    }

    busy() {
        return !!document.pictureInPictureElement || this.rec?.mRec?.state === 'recording';
    }

    onVis() {
        clearTimeout(this.timer);
        if (realHidden()) {
            this.timer = setTimeout(() => this.enter(), 10000);
        } else {
            this.leave();
        }
    }

    enter() {
        if (!realHidden() || this.busy()) return;
        try {
            if (AppState.bgPlay) {
                this.setLowest();
            } else {
                const v = this.vc.cachedVideo;
                if (v && !v.paused) { v.pause(); this.paused = v; }
            }
        } catch {}
    }

    leave() {
        try {
            const v = this.paused;
            this.paused = null;
            if (v?.isConnected) {
                const b = v.buffered;
                if (b.length) {
                    const end = b.end(b.length - 1);
                    if (end - v.currentTime > 3) v.currentTime = end - 0.5;
                }
                v.play().catch(() => {});
            }
            if (this.lowered) { const h = this.lowered.prev; this.lowered = null; this.pickQuality(h); }
        } catch {}
    }

    qualityItems() {
        const out = [];
        document.querySelectorAll('.js-qsb_list_wrp *, [class*="qsb_"] *, [class*="rendition"] *, [class*="quality_"] *, .vjs-menu-item *, [class*="resolution"] *, [class*="Resolution"] *, [role="menu"] *, [role="listbox"] *').forEach(el => {
            if (el.children.length || el.closest('button.player-resolution, button.vjs-icon-hd')) return;
            const m = (el.textContent || '').trim().match(/^(\d{3,4})\s*p?$/i);
            if (m) {
                const row = el.closest('li,a,button,div') || el;
                const mark = /(^|[\s_-])(active|selected|current|checked|__selected|__active)($|[\s_-])/;
                out.push({ el, h: +m[1], sel: mark.test(row.className) || mark.test(el.className) });
            }
        });
        return out;
    }

    openQualityMenu() {
        document.querySelector('.controls_current_rendition, #quality-btn, .player-resolution, button.vjs-icon-hd')?.click();
    }

    setLowest() {
        this.openQualityMenu();
        setTimeout(() => {
            try {
                const items = this.qualityItems();
                if (items.length < 2) return;
                const low = items.reduce((a, b) => b.h < a.h ? b : a);
                const cur = items.find(i => i.sel);
                if (cur && cur.h === low.h) return;
                this.lowered = { prev: cur ? cur.h : Math.max(...items.map(i => i.h)) };
                low.el.click();
            } catch {}
        }, 500);
    }

    pickQuality(h) {
        this.openQualityMenu();
        setTimeout(() => {
            try { this.qualityItems().find(i => i.h === h)?.el.click(); } catch {}
        }, 500);
    }
}

const autoAge = () => {
    if (!BC_RE.test(location.hostname)) return;
    const sel = '.js-18_plus_warning a.js-close_warning, #popup_18_plus a.button_18_plus';
    let clicked = false;
    const cleanup = () => {
        try { document.body?.classList.remove('bd_18plus'); document.documentElement.classList.remove('ht_18plus'); } catch {}
    };
    const accept = () => {
        const a = document.querySelector(sel);
        if (!a) return false;
        if (clicked) return true;
        clicked = true;
        a.addEventListener('click', e => e.preventDefault(), { once: true, capture: true });
        try { a.click(); } catch {}
        const done = () => setTimeout(cleanup, 150);
        try { fetch(a.href, { credentials: 'include', cache: 'no-store' }).then(done, done); } catch { done(); }
        return true;
    };
    if (accept()) return;
    const mo = new MutationObserver(() => { if (accept()) mo.disconnect(); });
    mo.observe(document.documentElement, { childList: true, subtree: true });
    setTimeout(() => mo.disconnect(), 20000);
};
autoAge();

const VIEWERS_SEL = '#bChatHeader .js-oc_count, .chat-header-inner .js-oc_count, #online-chat .js-oc_count, .chat-info-viewers, .v-viewers, .js-oc_count, .oc_count, .player-viewers-count, .HeaderRoomInfo__numViewers, [data-testid="viewers-count"]';
const VIEWERS_FALLBACK = '[class*="numViewers" i], [class*="viewerCount" i], [class*="viewersCount" i], [class*="viewers-count" i], [data-testid*="viewer" i]';
const readViewers = () => {
    const pick = sel => {
        try {
            for (const el of document.querySelectorAll(sel)) {
                const t = (el.textContent || '').trim();
                if (t && t.length < 60 && /\d/.test(t)) return t;
            }
        } catch {}
        return '';
    };
    return pick(VIEWERS_SEL) || pick(VIEWERS_FALLBACK);
};
const markFav = (btn, on) => {
    if (!btn) return;
    btn.style.background = '';
    btn.style.borderColor = '';
    btn.classList.toggle('fav-on', !!on);
};

const SLEEP_OPTS = [0, 15, 30, 45, 60, 90, 120];

class SleepTimer {
    constructor(ui, rec, bg) {
        this.ui = ui;
        this.rec = rec;
        this.bg = bg;
        this.lock = false;
        this.guard = 0;
        this.cover = null;
        this.mo = null;
        this.end = 0;
        this.mins = 0;
        this.t = 0;
        this.iv = 0;
        this.modal = null;
        this.at = 0;
        const end = +Store.get('bc_sleep_end', 0) || 0;
        if (end > Date.now()) {
            this.mins = +Store.get('bc_sleep_min', 0) || 0;
            this.arm(end);
        } else if (end) {
            Store.set('bc_sleep_end', 0);
            Store.set('bc_sleep_min', 0);
        }
        const lockAt = +Store.get('bc_sleep_lock', 0) || 0;
        if (lockAt && Date.now() - lockAt < 7200000) this.engage(); else if (lockAt) Store.set('bc_sleep_lock', 0);
        ['play', 'playing', 'loadstart', 'loadedmetadata', 'canplay'].forEach(t => document.addEventListener(t, e => this.onPlay(e), true));
        document.addEventListener('timeupdate', () => this.check(), true);
        onPageEvent('visibilitychange', () => this.check());
    }

    left() { return Math.max(0, this.end - Date.now()); }

    fmt() {
        const t = Math.ceil(this.left() / 1000), p = n => String(n).padStart(2, '0');
        return t >= 3600 ? `${Math.floor(t / 3600)}:${p(Math.floor(t / 60) % 60)}:${p(t % 60)}` : Helpers.formatTime(t);
    }

    set(min) { this.setMs(min * 60000); }

    setMs(ms) {
        if (!(ms > 0)) return this.cancel();
        ms = Math.min(Math.round(ms), 86400000);
        this.mins = ms / 60000;
        const end = Date.now() + ms;
        Store.set('bc_sleep_end', end);
        Store.set('bc_sleep_min', this.mins);
        this.arm(end);
    }

    arm(end) {
        clearTimeout(this.t);
        clearInterval(this.iv);
        this.end = end;
        this.t = setTimeout(() => this.check(), Math.max(0, end - Date.now()) + 50);
        this.iv = setInterval(() => { this.paint(); this.check(); }, 1000);
        this.paint();
    }

    cancel() {
        clearTimeout(this.t);
        clearInterval(this.iv);
        this.t = this.iv = 0;
        this.end = 0;
        this.mins = 0;
        Store.set('bc_sleep_end', 0);
        Store.set('bc_sleep_min', 0);
        this.paint();
    }

    check() {
        if (this.end && Date.now() >= this.end) this.fire();
    }

    engage() {
        this.lock = true;
        this.strip();
        this.showCover();
        clearInterval(this.guard);
        this.guard = setInterval(() => this.strip(), 2000);
        if (!this.mo) {
            const hasMedia = n => n.nodeType === 1 && (n.matches('video,audio') || !!n.querySelector('video,audio'));
            this.mo = new MutationObserver(recs => {
                for (const r of recs) for (const n of r.addedNodes) if (hasMedia(n)) return this.strip();
            });
            this.mo.observe(document.documentElement, { childList: true, subtree: true });
        }
    }

    unlock() {
        this.lock = false;
        clearInterval(this.guard);
        this.guard = 0;
        this.mo?.disconnect();
        this.mo = null;
        this.cover?.remove();
        this.cover = null;
        Store.set('bc_sleep_lock', 0);
    }

    wake() {
        this.unlock();
        location.reload();
    }

    strip() {
        try { window.dispatchEvent(new Event('bc_sleep_kill')); } catch {}
        try {
            document.querySelectorAll('video,audio').forEach(v => {
                try {
                    v.pause();
                    v.srcObject?.getTracks?.().forEach(t => t.stop());
                    v.srcObject = null;
                    v.removeAttribute('src');
                    v.load();
                } catch {}
                v.remove();
            });
        } catch {}
    }

    showCover() {
        if (this.cover?.isConnected) return;
        const c = mk('div', '');
        c.style.cssText = 'position:fixed;inset:0;z-index:0;background:#000;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;text-align:center;padding:24px;color:#8a8a90;font-size:15px;pointer-events:auto;touch-action:manipulation;cursor:pointer';
        String(L().sleep_wake).split('\n').forEach(t => c.append(textEl('', t, 'div')));
        c.onclick = () => this.wake();
        this.ui.shadow.insertBefore(c, this.ui.shadow.firstChild);
        this.cover = c;
    }

    onPlay(e) {
        if (!this.lock || !(e.target instanceof HTMLMediaElement)) return;
        this.strip();
    }

    fire() {
        this.cancel();
        Store.set('bc_sleep_lock', Date.now());
        let wasRec = false;
        try {
            const btn = this.ui.elements.bRec;
            if (this.rec?.mRec && this.rec.mRec.state !== 'inactive') { wasRec = true; this.rec.stop(btn, btn.querySelector('sub')); }
        } catch {}
        if (this.bg) this.bg.paused = null;
        try { this.ui.flash(this.ui.elements.bBg, 'OK'); } catch {}
        if (wasRec) setTimeout(() => { if (Store.get('bc_sleep_lock', 0)) this.engage(); }, 2000);
        else this.engage();
    }

    paint() {
        const sub = this.ui.elements.bBg?.querySelector('sub');
        if (sub && !sub._bcFlashT) {
            if (this.end) {
                sub._slp = true;
                sub.textContent = this.fmt();
            } else if (sub._slp) {
                sub._slp = false;
                sub.textContent = L().bg;
            }
        }
        this.paintModal();
    }

    paintModal() {
        const info = this.modal?.info;
        if (!info || !info.isConnected) return;
        info.textContent = this.end ? `${L().sleep_left}: ${this.fmt()}` : '';
    }

    open() {
        if (!this.modal) {
            const { overlay, title, closeBtn, body } = buildModalShell(this.ui.shadow, 'bm-modal rs-modal');
            const close = () => { overlay.classList.remove('open'); overlay._fromSt = false; overlay._bk?.(); };
            closeBtn.onclick = close;
            overlay.onclick = e => { if (e.target === overlay && Date.now() - this.at > 400) close(); };
            this.modal = { overlay, title, body, close, info: null };
        }
        this.at = Date.now();
        this.render();
        this.modal.overlay.classList.add('open');
    }

    sleepField(unit, max) {
        const wrap = mk('label', 'rs-note');
        wrap.style.cssText = 'flex:1;display:flex;flex-direction:column;gap:4px;min-width:0';
        const inp = mk('input', 'bm-note');
        inp.type = 'number';
        inp.min = '0';
        inp.max = String(max);
        inp.step = '1';
        inp.placeholder = '0';
        inp.inputMode = 'numeric';
        inp.style.textAlign = 'center';
        inp.onkeydown = e => e.stopPropagation();
        wrap.append(textEl('', unit, 'span'), inp);
        return { wrap, inp };
    }

    render() {
        const m = this.modal;
        if (!m) return;
        const l = L();
        m.title.innerHTML = `${icons.sleep} ${escHTML(l.sleep_title)}`;
        const chips = mk('div', 'rs-chips');
        SLEEP_OPTS.forEach(v => {
            const on = v === 0 ? !this.end : !!this.end && v === this.mins;
            const b = mk('button', `bm-tbtn${on ? ' on' : ''}`);
            b.textContent = v ? `${v} ${l.u_m}` : l.rs_off;
            b.onclick = () => { this.set(v); m.close(); };
            chips.append(b);
        });
        const fh = this.sleepField(l.u_h, 24), fm = this.sleepField(l.u_m, 1440), fs = this.sleepField(l.sleep_sec, 86400);
        const row = mk('div', '');
        row.style.cssText = 'display:flex;gap:8px';
        row.append(fh.wrap, fm.wrap, fs.wrap);
        const go = mk('button', 'bm-btn sel');
        go.textContent = l.sleep_start;
        const start = () => {
            const n = f => Math.max(0, parseInt(f.inp.value, 10) || 0);
            const ms = (n(fh) * 3600 + n(fm) * 60 + n(fs)) * 1000;
            if (!ms) return;
            this.setMs(ms);
            m.close();
        };
        go.onclick = start;
        [fh, fm, fs].forEach(f => { f.inp.onkeydown = e => { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); start(); } }; });
        m.info = mk('div', 'rs-info');
        m.body.replaceChildren(chips, textEl('hd-sec', l.sleep_custom), row, go, m.info, textEl('rs-note', l.sleep_hint));
        this.paintModal();
    }
}

const RAD = { NODE: 42, R0: 84, RSTEP: 62, DEAD: 26, ARC: 0.44, HOLD: 380, FAB: 48 };
const radDiff = (a, b) => {
    let d = (a - b) % (Math.PI * 2);
    if (d > Math.PI) d -= Math.PI * 2;
    if (d < -Math.PI) d += Math.PI * 2;
    return d;
};
const RAD_CSS = `.rm-fab{position:fixed;width:48px;height:48px;border-radius:50%;display:none;align-items:center;justify-content:center;color:#fff;background:var(--th);border:2px solid rgba(255,255,255,.35);box-shadow:0 4px 15px rgba(0,0,0,.6);pointer-events:auto;touch-action:none!important;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none;cursor:pointer;opacity:.85;z-index:2147483640;transition:transform .15s,opacity .15s,box-shadow .15s}
.rm-fab.on{display:flex}.rm-fab:hover{opacity:1}.rm-fab.moving{transform:scale(1.25);opacity:1;box-shadow:0 0 0 6px rgba(255,255,255,.2),0 8px 24px rgba(0,0,0,.7)}.rm-fab svg{width:24px;height:24px;pointer-events:none}
.rm-ov{position:fixed;inset:0;display:none;pointer-events:none;touch-action:none;z-index:2147483645;background:rgba(0,0,0,.38)}.rm-ov.open{display:block}.rm-ov.sticky{pointer-events:auto}
.rm-stage{position:absolute;width:0;height:0;transform-origin:0 0}
.rm-ring{position:absolute;border-radius:50%;box-sizing:content-box;border-style:solid;border-color:rgba(255,255,255,.07);transform:translate(-50%,-50%);pointer-events:none;z-index:0}
.rm-hub{position:absolute;z-index:1;width:64px;height:64px;margin:-32px 0 0 -32px;border-radius:50%;background:rgba(15,15,18,.95);border:2px solid rgba(255,255,255,.25);color:#fff;font:700 10px/1.1 system-ui,sans-serif;display:flex;align-items:center;justify-content:center;text-align:center;padding:4px;overflow:hidden;word-break:break-word;pointer-events:auto;cursor:pointer;transition:background .1s}
.rm-hub.lit{background:var(--th)}.rm-hub svg{width:24px;height:24px}
.rm-node{position:absolute;z-index:2;width:42px;height:42px;margin:-21px 0 0 -21px;border-radius:50%;background:rgba(15,15,18,.95);border:1px solid rgba(255,255,255,.2);color:#fff;display:flex;align-items:center;justify-content:center;pointer-events:auto;cursor:pointer;box-shadow:0 3px 10px rgba(0,0,0,.5);animation:rmPop .14s ease-out;transition:transform .1s,background .1s}
.rm-node svg{width:20px;height:20px;pointer-events:none}
.rm-node.grp::after{content:'';position:absolute;right:2px;bottom:2px;width:8px;height:8px;border-radius:50%;background:var(--th);border:1px solid #fff}
.rm-node.hot{background:var(--th);transform:scale(1.28);border-color:#fff;z-index:3}
@keyframes rmPop{from{transform:scale(.3);opacity:0}to{transform:scale(1);opacity:1}}`;

const nearestAngle = (pos, ang) => {
    let best = -1, bd = 1e9;
    pos.forEach((p, i) => { const df = Math.abs(radDiff(ang, p.a)); if (df < bd) { bd = df; best = i; } });
    return { best, bd };
};
class RadialMenu {
    constructor(ui, app) {
        this.ui = ui;
        this.app = app;
        this.on = Store.get('bc_rad', false) === true;
        this.mode = '';
        this.path = [];
        this.lv = [];
        this.items = [];
        this.scale = 1;
        const st = document.createElement('style');
        skinCss(st, RAD_CSS, RAD_SKIN);
        ui.shadow.appendChild(st);
        this.fab = mk('div', 'rm-fab');
        this.fab.innerHTML = icons.target;
        this.ov = mk('div', 'rm-ov');
        this.stage = mk('div', 'rm-stage');
        this.hubEl = mk('div', 'rm-hub');
        this.ov.appendChild(this.stage);
        ui.shadow.append(this.ov, this.fab);
        this.bind();
        this.placeFab();
        this.fab.classList.toggle('on', this.on);
        window.addEventListener('resize', () => { this.close(); this.placeFab(); }, { passive: true });
        window.addEventListener('keydown', e => { if (e.key === 'Escape' && this.mode) this.close(); }, true);
    }

    setEnabled(v) {
        this.on = !!v;
        Store.set('bc_rad', this.on);
        this.fab.classList.toggle('on', this.on);
        if (!this.on) this.close();
        this.placeFab();
    }

    resetPos() {
        Store.set('bc_rad_x', null);
        Store.set('bc_rad_y', null);
        this.placeFab();
    }

    fabPos() {
        const W = window.innerWidth, H = window.innerHeight, S = RAD.FAB;
        const x = parseFloat(Store.get('bc_rad_x', null)), y = parseFloat(Store.get('bc_rad_y', null));
        return {
            x: clamp(Number.isFinite(x) ? x : W - S - 14, 4, Math.max(4, W - S - 4)),
            y: clamp(Number.isFinite(y) ? y : Math.round(H * 0.55), 4, Math.max(4, H - S - 4))
        };
    }

    placeFab() {
        const p = this.fabPos();
        this.fab.style.left = `${p.x}px`;
        this.fab.style.top = `${p.y}px`;
    }

    treeCtx() {
        const l = L(), ui = this.ui, app = this.app, vc = app.videoController, el = ui.elements;
        const press = b => {
            if (!b) return;
            if (b.onpointerup) {
                const o = { button: 0, pointerType: 'touch', isPrimary: true, bubbles: true };
                b.dispatchEvent(new PointerEvent('pointerdown', o));
                b.dispatchEvent(new PointerEvent('pointerup', o));
            } else b.click();
        };
        const vol = d => () => { const v = vc.cachedVideo; if (v) vc.setVolume(v.volume + d); };
        const addFav = () => {
            const name = currentSite.getName();
            if (name && !isFavName(name)) { app.bookmarks.add(); ui.flash(el.bFav, 'OK'); app.updateViewersCount(); }
            else ui.flash(el.bFav, 'ERR');
        };
        return { l, ui, app, vc, el, press, vol, addFav };
    }

    treeMedia(c) {
        const { l, app, vc, el, press, vol } = c;
        return { icon: icons.vol, label: l.rad_media, kids: [
            { icon: icons.bgOn, label: l.bg, run: () => press(el.bBg) },
            { icon: icons.mute, label: l.sound, run: () => press(el.bMute) },
            { icon: icons.volMed, label: l.rad_vup, run: vol(0.1) },
            { icon: icons.volLow, label: l.rad_vdn, run: vol(-0.1) },
            { icon: icons.snap, label: l.snap, run: () => vc.takeScreenshot() },
            { icon: icons.rec, label: l.rec, run: () => app.recorder.toggle() }
        ] };
    }

    treeView(c) {
        const { l, ui, vc } = c;
        return { icon: icons.fs, label: l.rad_view, kids: [
            { icon: icons.qual, label: l.qual, run: () => ui.toggleQuality() },
            { icon: icons.pip, label: l.pip, run: () => vc.togglePiP() },
            { icon: icons.fs, label: l.fs, run: () => vc.toggleFullscreen() },
            { icon: icons.thtr, label: l.thtr, run: () => vc.toggleTheater() }
        ] };
    }

    treeNav(c) {
        const { l, app, el, addFav } = c;
        return { icon: icons.fav, label: l.rad_nav, kids: [
            { icon: icons.search, label: l.search, run: () => app.search.open() },
            { icon: icons.bookmark, label: l.favs, run: () => app.bookmarks.open() },
            { icon: icons.fav, label: l.bm_add, run: addFav },
            { icon: icons.favPrev, label: l.rad_prev, run: () => el.bFavPrev?.click() },
            { icon: icons.favNext, label: l.rad_next, run: () => el.bFavNext?.click() },
            { icon: icons.copy, label: l.copy, run: () => el.bCopy?.click() }
        ] };
    }

    treePage(c) {
        const { l, ui, app } = c;
        return { icon: icons.grid, label: l.rad_page, kids: [
            { icon: icons.home, label: l.home, run: () => { location.href = '/'; } },
            { icon: icons.rel, label: l.rel, run: () => location.reload() },
            { icon: AppState.dark ? icons.sun : icons.moon, label: l.dark, run: () => ui.toggleDark() },
            { icon: icons.sleep, label: l.sleep_title, run: () => app.sleep.open() },
            { icon: icons.history, label: l.hist_title, run: () => app.history.open() },
            { icon: icons.min, label: l.min, run: () => ui.toggleMinimize(true) },
            { icon: icons.close, label: l.hide, run: () => ui.toggleHide(true) }
        ] };
    }

    tree() {
        const c = this.treeCtx(), { l, app } = c;
        return [
            this.treeMedia(c),
            this.treeView(c),
            this.treeNav(c),
            this.treePage(c),
            { icon: icons.gear, label: l.settings, run: () => app.settings.open() }
        ];
    }

    fan(r) {
        const { x: hx, y: hy } = this.hub, W = window.innerWidth, H = window.innerHeight, M = RAD.NODE / 2 + 4, b = this.base;
        const ok = a => { const x = hx + Math.cos(a) * r, y = hy + Math.sin(a) * r; return x >= M && x <= W - M && y >= M && y <= H - M; };
        let lo = 0, hi = 0;
        const st = 0.04;
        while (hi < Math.PI && ok(b + hi + st)) hi += st;
        while (lo > -Math.PI && ok(b + lo - st)) lo -= st;
        return { lo, hi, full: hi - lo >= Math.PI * 2 - 0.12 };
    }

    spread(c, n, step) {
        return Array.from({ length: n }, (_, i) => c + (i - (n - 1) / 2) * step);
    }

    layoutRoot(n, f, b) {
        if (f.full) {
            const step = (Math.PI * 2) / n;
            return { step, angles: Array.from({ length: n }, (_, i) => -Math.PI / 2 + i * step) };
        }
        const step = n > 1 ? Math.min(1.3, (f.hi - f.lo) / (n - 1)) : 0;
        return { step, angles: this.spread(b + (f.lo + f.hi) / 2, n, step) };
    }

    layoutKids(n, f, b, parentAng) {
        if (f.full) return { step: RAD.ARC, angles: this.spread(parentAng, n, RAD.ARC) };
        const span = f.hi - f.lo;
        let step = RAD.ARC;
        if ((n - 1) * step > span) step = n > 1 ? span / (n - 1) : 0;
        const w = ((n - 1) * step) / 2;
        const c = b + clamp(radDiff(parentAng, b), f.lo + w, f.hi - w);
        return { step, angles: this.spread(c, n, step) };
    }

    layout(list, d, parentAng) {
        const r = RAD.R0 + d * RAD.RSTEP, n = list.length, b = this.base;
        const { step, angles } = d === 0
            ? this.layoutRoot(n, this.f0, b)
            : this.layoutKids(n, this.f1, b, parentAng);
        const out = angles.map(a => ({ a, x: Math.cos(a) * r, y: Math.sin(a) * r }));
        out.step = step;
        return out;
    }

    kidsHit(list, pos, ang, dist) {
        if (dist < RAD.R0 + RAD.RSTEP - RAD.NODE / 2 - 8) return false;
        const c = this.path[0], cn = c != null ? list[c] : null;
        if (!cn || !cn.kids) return false;
        const kp = this.layout(cn.kids, 1, pos[c].a), tol = Math.max(kp.step, 0.2) * 0.6;
        return kp.some(p => Math.abs(radDiff(ang, p.a)) <= tol);
    }

    beyondRing(d, dist) {
        return d > 0 && dist < RAD.R0 + d * RAD.RSTEP - RAD.NODE / 2 - 8;
    }

    tooFar(d, pos, bd) {
        return d > 0 && bd > Math.max(pos.step, 0.2) * 0.6;
    }

    pick(vx, vy) {
        const dist = Math.hypot(vx, vy);
        if (dist < RAD.DEAD) return [];
        const ang = Math.atan2(vy, vx), path = [];
        let list = this.items, parentAng = null;
        for (let d = 0; list && !this.beyondRing(d, dist); d++) {
            const pos = this.layout(list, d, parentAng);
            let { best, bd } = nearestAngle(pos, ang);
            if (d === 0 && this.kidsHit(list, pos, ang, dist)) best = this.path[0];
            if (this.tooFar(d, pos, bd)) break;
            path.push(best);
            const node = list[best];
            if (!node.kids) break;
            parentAng = pos[best].a;
            list = node.kids;
        }
        return path;
    }

    moveFab(f, e) {
        const S = RAD.FAB;
        f.style.left = `${clamp(e.clientX - S / 2, 4, window.innerWidth - S - 4)}px`;
        f.style.top = `${clamp(e.clientY - S / 2, 4, window.innerHeight - S - 4)}px`;
    }

    dragMove(st, e) {
        const vx = e.clientX - st.ox, vy = e.clientY - st.oy;
        if (!st.moved && Math.hypot(vx, vy) > 10) { st.moved = true; clearTimeout(st.timer); }
        if (st.moved) this.setPath(this.pick(vx / this.scale, vy / this.scale));
        if (e.cancelable) e.preventDefault();
    }

    endMove(f) {
        f.classList.remove('moving');
        this.mode = '';
        Store.set('bc_rad_x', f.style.left);
        Store.set('bc_rad_y', f.style.top);
    }

    endDrag(e, s) {
        if (e.type === 'pointercancel') return this.close();
        const node = this.nodeAtPath(this.path);
        if (node && !node.kids) { this.close(); this.run(node); return; }
        if (node || !s.moved) return this.sticky();
        this.close();
    }

    nodeAtPath(path) {
        let list = this.items, node = null;
        for (const i of path) { node = list && list[i] || null; list = node && node.kids; }
        return node;
    }

    openMenu(c, mode) {
        this.items = this.tree();
        this.path = [];
        this.scale = 1;
        this.hub = { x: c.x, y: c.y };
        const cx = window.innerWidth / 2, cy = window.innerHeight / 2;
        this.base = Math.hypot(cx - c.x, cy - c.y) < 20 ? -Math.PI / 2 : Math.atan2(cy - c.y, cx - c.x);
        this.f0 = this.fan(RAD.R0);
        this.f1 = this.fan(RAD.R0 + RAD.RSTEP);
        this.stage.style.cssText = `left:${c.x}px;top:${c.y}px`;
        this.lv = [];
        this.stage.replaceChildren(this.hubEl);
        this.mode = mode;
        this.ov.className = `rm-ov open${mode === 'sticky' ? ' sticky' : ''}`;
        this.render();
        this.updateHub();
    }

    buildLevel({ list, pos, key, d }) {
        const el = mk('div', 'rm-lvl');
        const r = RAD.R0 + d * RAD.RSTEP, band = RAD.NODE + 12;
        const ring = mk('div', 'rm-ring');
        ring.style.cssText = `width:${2 * r - band}px;height:${2 * r - band}px;border-width:${band}px`;
        el.appendChild(ring);
        const nodes = list.map((it, i) => {
            const n = mk('div', `rm-node${it.kids ? ' grp' : ''}`);
            n.innerHTML = it.icon;
            n.title = it.label;
            n.dataset.d = d;
            n.dataset.i = i;
            n.style.left = `${pos[i].x}px`;
            n.style.top = `${pos[i].y}px`;
            el.appendChild(n);
            return n;
        });
        this.stage.appendChild(el);
        return { key, list, nodes, el };
    }

    render() {
        const levels = [];
        let list = this.items, parentAng = null, key = '';
        for (let d = 0; list; d++) {
            const pos = this.layout(list, d, parentAng);
            levels.push({ list, pos, key, d });
            const i = this.path[d];
            if (i == null || !list[i] || !list[i].kids) break;
            parentAng = pos[i].a;
            key += `/${i}`;
            list = list[i].kids;
        }
        let from = levels.findIndex((lv, d) => !this.lv[d] || this.lv[d].key !== lv.key);
        if (from < 0) from = levels.length;
        while (this.lv.length > from) this.lv.pop().el.remove();
        for (let d = from; d < levels.length; d++) this.lv.push(this.buildLevel(levels[d]));
        this.lv.forEach((lv, d) => lv.nodes.forEach((n, i) => n.classList.toggle('hot', this.path[d] === i)));
    }

    updateHub() {
        const n = this.nodeAtPath(this.path);
        this.hubEl.classList.toggle('lit', !!n);
        if (n) this.hubEl.textContent = n.label; else this.hubEl.innerHTML = icons.target;
    }

    setPath(p) {
        if (p.length === this.path.length && p.every((v, i) => v === this.path[i])) return;
        this.path = p;
        this.render();
        this.updateHub();
        if (p.length && navigator.vibrate) navigator.vibrate(8);
    }

    sticky() {
        this._stAt = Date.now();
        this.mode = 'sticky';
        this.ov.classList.add('sticky');
    }

    close() {
        if (!this.mode) return;
        this.mode = '';
        this.ov.className = 'rm-ov';
        this.path = [];
        this.lv = [];
        this.stage.replaceChildren();
    }

    run(it) {
        try { it.run(); } catch (err) { console.warn('[BCU radial]', err); }
    }

    bind() {
        this.bindFabGuards();
        this.bindFabPointer();
    }

    bindFabGuards() {
        const f = this.fab;
        f.oncontextmenu = e => e.preventDefault();
        ['touchstart', 'touchmove', 'touchend'].forEach(t => f.addEventListener(t, e => { if (e.cancelable) e.preventDefault(); }, { passive: false }));
        ['touchstart', 'touchmove'].forEach(t => this.ov.addEventListener(t, e => { if (e.cancelable) e.preventDefault(); }, { passive: false }));
        window.addEventListener('contextmenu', e => {
            const path = e.composedPath ? e.composedPath() : [];
            if (this.mode || path.includes(f) || path.includes(this.ov)) { e.preventDefault(); e.stopImmediatePropagation(); }
        }, true);
    }

    bindFabPointer() {
        const f = this.fab;
        let st = null;
        f.onpointerdown = e => {
            if (!isPrimaryPress(e)) return;
            swallow(e);
            this.close();
            try { f.setPointerCapture(e.pointerId); } catch {}
            const p = this.fabPos();
            st = { id: e.pointerId, ox: e.clientX, oy: e.clientY, moved: false, timer: 0 };
            this.openMenu({ x: p.x + RAD.FAB / 2, y: p.y + RAD.FAB / 2 }, 'drag');
            st.timer = setTimeout(() => {
                if (!st || st.moved || this.mode !== 'drag') return;
                this.close();
                this.mode = 'move';
                f.classList.add('moving');
                if (navigator.vibrate) navigator.vibrate(15);
            }, RAD.HOLD);
        };
        f.onpointermove = e => {
            if (!st || e.pointerId !== st.id) return;
            if (this.mode === 'move') return this.moveFab(f, e);
            if (this.mode !== 'drag') return;
            this.dragMove(st, e);
        };
        const end = e => {
            if (!st || e.pointerId !== st.id) return;
            clearTimeout(st.timer);
            const s = st;
            st = null;
            try { f.releasePointerCapture(e.pointerId); } catch {}
            if (this.mode === 'move') return this.endMove(f);
            if (this.mode !== 'drag') return;
            this.endDrag(e, s);
        };
        f.onpointerup = end;
        f.onpointercancel = end;
        f.onlostpointercapture = e => { if (st && e.pointerId === st.id) end({ type: 'pointercancel', pointerId: e.pointerId }); };
        window.addEventListener('blur', () => { if (st) end({ type: 'pointercancel', pointerId: st.id }); });
        document.addEventListener('visibilitychange', () => { if (document.hidden && st) end({ type: 'pointercancel', pointerId: st.id }); });
        this.bindStickyUp();
    }

    bindStickyUp() {
        this.ov.onpointerup = e => {
            if (this.mode !== 'sticky' || !isPrimaryPress(e) || Date.now() - (this._stAt || 0) < 150) return;
            swallow(e);
            const n = e.target.closest ? e.target.closest('.rm-node') : null;
            if (!n) return this.close();
            this.stickyPick(+n.dataset.d, +n.dataset.i);
        };
    }

    stickyPick(d, i) {
        const it = this.lv[d] && this.lv[d].list[i];
        if (!it) return;
        if (!it.kids) { this.close(); this.run(it); return; }
        const same = this.path[d] === i && this.path.length === d + 1;
        this.setPath(same ? this.path.slice(0, d) : this.path.slice(0, d).concat(i));
    }
}

class App {
    constructor() {
        this.createModules();
        const vc = this.videoController, el = this.ui.elements;
        this.bindMediaButtons(vc, el);
        this.bindFavButtons(vc, el);
        this.restoreUiState();
        this.bindRemoteAndWindow(vc);
        this.startTimers();
    }

    createModules() {
        this.ui = new UIController();
        this.videoController = new VideoController(this.ui);
        this.recorder = new MediaRecorderManager(this.videoController, this.ui);
        this.bgSaver = new BgSaver(this.videoController, this.recorder);
        this.bookmarks = new BookmarkManager(this.ui);
        this.search = new SearchManager(this.ui);

        this.ui.bookmarkManager = this.bookmarks;
        this.ui.searchManager = this.search;
        this.hideManager = new HideManager(this.ui);
        this.ui.hideManager = this.hideManager;
        this.multiView = new MultiView(this.ui);
        this.ui.multiView = this.multiView;
        bmDropHooks.push(k => this.multiView.dropBmKey(k));
        this.history = new HistoryManager(this.ui, this.bookmarks);
        this.thumbStars = new ThumbStars(this.bookmarks);
        this.ui.historyManager = this.history;
        this.calendar = new CalendarManager(this.ui, this.bookmarks);
        this.ui.calendarManager = this.calendar;
        this.ui.thumbStars = this.thumbStars;
        this.ui.recorder = this.recorder;
        this.settings = new SettingsManager(this.ui);
        this.ui.settingsManager = this.settings;
        this.sleep = new SleepTimer(this.ui, this.recorder, this.bgSaver);
        this.ui.sleepTimer = this.sleep;
        this.ui.radial = new RadialMenu(this.ui, this);
    }

    bindMediaButtons(vc, el) {
        const bgClick = el.bBg.onclick;
        el.bBg.onclick = null;
        Helpers.bindLongPress(el.bBg, () => bgClick.call(el.bBg), () => this.sleep.open());
        Helpers.bindLongPress(el.bMute,
            () => {
                if (AppState.gmute) setGMute(false);
                vc.toggleMute();
            },
            () => {
                setGMute(true);
                const v = vc.cachedVideo;
                if (v) { v.muted = true; v.volume = 0; }
                this.ui.flash(el.bMute, 'LOCK');
                vc.syncUI();
            }
        );
        el.bPip.onclick = () => vc.togglePiP();
        el.bFs.onclick = () => vc.toggleFullscreen();
        el.bThtr.onclick = () => vc.toggleTheater();
        el.bShot.onclick = () => vc.takeScreenshot();
        el.vIco.onclick = () => vc.toggleMute();
        Helpers.bindLongPress(el.bRec, () => this.recorder.toggle(), () => this.recorder.openSettings());
        el.bSearch.onclick = () => this.search.open();
    }

    async goFav(dir, btn) {
        if (btn._busy) return;
        btn._busy = true;
        btn.style.opacity = '.5';
        try {
            await this.bookmarks.checkStatuses();
            const here = siteKey(location.hostname);
            const curName = currentSite.getName();
            const list = AppState.bookmarks
                .filter(b => b.status === 1 && isSafeUrl(b.url) && urlSite(b.url) === here)
                .sort((x, y) => (!!y.pin - !!x.pin) || x.name.localeCompare(y.name));
            const idx = list.findIndex(b => String(b.name).toLowerCase() === String(curName).toLowerCase());
            if (!list.length || (list.length === 1 && idx === 0)) return this.ui.flash(btn, 'ERR');
            const next = idx < 0 ? (dir > 0 ? list[0] : list[list.length - 1]) : list[(idx + dir + list.length) % list.length];
            location.href = linkFor(next.url);
        } catch { this.ui.flash(btn, 'ERR'); }
        finally { btn._busy = false; btn.style.opacity = ''; }
    }

    bindFavButtons(vc, el) {
        el.bFavPrev.onclick = () => this.goFav(-1, el.bFavPrev);
        el.bFavNext.onclick = () => this.goFav(1, el.bFavNext);
        el.bFavPrev.title = el.bFavNext.title = '';
        el.vSlider.oninput = e => vc.setVolume(parseFloat(e.target.value));
        el.vSlider.onwheel = e => {
            e.preventDefault();
            if (vc.cachedVideo) vc.setVolume(clamp(vc.cachedVideo.volume + (e.deltaY < 0 ? 0.05 : -0.05), 0, 1));
        };
        Helpers.bindLongPress(el.bFav,
            () => setTimeout(() => this.bookmarks.open(), 50),
            () => {
                const name = currentSite.getName();
                if (name && !isFavName(name)) {
                    this.bookmarks.add();
                    this.ui.flash(el.bFav, 'OK');
                    this.updateViewersCount();
                } else this.ui.flash(el.bFav, 'ERR');
            }
        );
        el.bFav.onclick = e => { e.preventDefault(); e.stopPropagation(); };
    }

    restoreUiState() {
        this.ui.updateLabels();
        this.ui.injectSiteCSS();
        if (AppState.bgPlay) this.ui.enforceBackgroundPlay();
        if (AppState.isMinimized) this.ui.toggleMinimize(true, true);
        if (AppState.hasPos) this.ui.clampPosition();
        if (AppState.isHidden) this.ui.toggleHide(true);
        setTimeout(() => this.ui.ensureVisible(), 1500);
        setTimeout(() => this.ui.ensureVisible(), 5000);
        try { if (typeof GM_registerMenuCommand === 'function') GM_registerMenuCommand('Сбросить панель / Reset panel', () => this.ui.resetPanel()); } catch {}
        try { if (typeof GM_registerMenuCommand === 'function') GM_registerMenuCommand(pdT('title'), () => ParserReport.open()); } catch {}
        let evT = 0;
        const later = () => { clearTimeout(evT); evT = setTimeout(() => this.ui.ensureVisible(), 700); };
        ['resize', 'orientationchange'].forEach(t => window.addEventListener(t, later, { passive: true }));
        ['fullscreenchange', 'webkitfullscreenchange'].forEach(t => document.addEventListener(t, later, true));
        setTimeout(() => this.multiView?.bootResume(), 2500);
    }

    bindRemoteAndWindow(vc) {
        onRemoteChange('bc_bookmarks', v => this.bookmarks.applyRemote(v));
        onRemoteChange('bc_gmute', v => {
            try { AppState.gmute = JSON.parse(v) === true; } catch {}
            const cv = vc.cachedVideo;
            if (AppState.gmute && cv) { cv.muted = true; cv.volume = 0; }
            vc.syncUI();
        });

        window.addEventListener('resize', () => {
            if (AppState.hasPos) this.ui.clampPosition();
        }, { passive: true });
        onPageEvent('visibilitychange', () => {
            if (realHidden() || !AppState.hasPos) return;
            setTimeout(() => { this.ui.applyPosition(); this.ui.clampPosition(); }, 150);
        });
    }

    startTimers() {
        let tick = 0;
        setInterval(() => {
            if (realHidden()) return;
            tick++;
            this.ui.injectSiteCSS();
            this.updateViewersCount();
            if (tick % 5 === 0) this.slowTasks();
        }, 3000);
        setTimeout(() => this.updateViewersCount(), 1000);
    }

    slowTasks() {
        try { applyDark(); } catch {}
        this.ui.ensureVisible();
    }

    updateViewersCount() {
        const name = currentSite.getName();
        const { stats, bFav } = this.ui.elements;
        if (stats) stats.style.visibility = name ? 'visible' : 'hidden';
        markFav(bFav, !!name && isFavName(name));
        if (!name) return;

        const txt = stats && readViewers();
        if (txt) paintViewers(stats, txt);
        const vn = parseCount(txt);
        if (Number.isFinite(vn)) this.bookmarks.setViewers(pageKey(), vn);
    }
}

const CBHP = { api: null };
const cbHoverPreview = () => {
    'use strict';
    if (document.documentElement.dataset.cbhp && document.getElementById('cbhp-css')) return;
    document.documentElement.dataset.cbhp = '1';

    const MODES = ['off', 'anim', 'video'];
    const MODE_LABEL = { off: 'выключен', anim: 'только анимация обложки', video: 'анимация + мини-видео' };
    const HEIGHTS = [240, 360, 480, 720];

    const cfg = {
        mode: MODES.includes(Store.get('cbhp_mode', 'video')) ? Store.get('cbhp_mode', 'video') : 'video',
        sound: Store.get('cbhp_sound', false) === true,
        maxH: HEIGHTS.includes(+Store.get('cbhp_maxh', 360)) ? +Store.get('cbhp_maxh', 360) : 360
    };

    const T = {
        animDelay: 140,
        videoDelay: 420,
        frameMs: 50,
        sameMs: 250,
        okTtl: 180000,
        badTtl: 45000
    };

    const CARD = '.RoomCard, li.roomCard, li.room_list_room';
    const IMG = 'img.RoomCardThumbnail__image, img[data-testid="room-card-image"], img.room_thumbnail';
    const SKIP = '[class*="thumbnail_label_c_private"], [class*="thumbnail_label_offline"], [class*="thumbnail_label_c_away"], [class*="thumbnail_label_c_hidden"]';
    const NAME_RE = /^[A-Za-z0-9_]{2,40}$/;

    const userOf = card => {
        let u = card.querySelector('a[data-room]')?.dataset.room;
        if (!u) {
            const href = card.querySelector('a[href]')?.getAttribute('href') || '';
            u = (/^(?:https?:\/\/[^/]+)?\/([A-Za-z0-9_]+)\/?(?:[?#].*)?$/.exec(href) || [])[1];
        }
        return u && NAME_RE.test(u) ? u : '';
    };

    const css = `
.cbhp-layer{position:absolute;inset:0;z-index:0;overflow:hidden;border-radius:inherit;pointer-events:none;opacity:0;transition:opacity .18s ease}
.cbhp-layer.cbhp-in{opacity:1}
.cbhp-f,.cbhp-v{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;display:block;margin:0;padding:0;border:0;background:none}
.cbhp-f.cbhp-on{opacity:1}
.cbhp-layer.cbhp-live .cbhp-v{opacity:1;transition:opacity .35s ease}

.cbhp-bar{position:absolute;left:0;bottom:0;height:2px;width:34%;opacity:0;background:linear-gradient(90deg,transparent,#f47321,transparent)}
.cbhp-load .cbhp-bar{display:none}
.cbhp-dot{position:absolute;top:6px;right:6px;width:8px;height:8px;border-radius:50%;background:#ff3b30;box-shadow:0 0 0 0 rgba(255,59,48,.6);opacity:0;transition:opacity .25s}
.cbhp-dot{display:none}
.RoomCard .bcu-star,.roomCard .bcu-star,.RoomCard .RoomCardThumbnail__labelContainer,.RoomCard .RoomCard__followStar{z-index:2147483000!important;transform:translateZ(0)}
.RoomCard .RoomCardThumbnail{isolation:isolate}
@keyframes cbhp-slide{from{transform:translateX(-100%)}to{transform:translateX(300%)}}
@keyframes cbhp-pulse{0%{box-shadow:0 0 0 0 rgba(255,59,48,.55)}100%{box-shadow:0 0 0 8px rgba(255,59,48,0)}}
.cbhp-toast{position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:2147483000;padding:8px 14px;border-radius:8px;background:rgba(20,20,22,.92);color:#fff;font:13px/1.3 system-ui,sans-serif;pointer-events:none;opacity:0;transition:opacity .2s}
.cbhp-toast.cbhp-show{opacity:1}
@media (hover:none){.RoomCard,.RoomCard a,.RoomCard img{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;-webkit-user-drag:none}}
@media (prefers-reduced-motion:reduce){.cbhp-f,.cbhp-v,.cbhp-layer{transition:none}.cbhp-load .cbhp-bar,.cbhp-live .cbhp-dot{animation:none}}
`;
    try { addStyle('cbhp-css', css, document.documentElement); } catch {}

    const touchy = !matchMedia('(hover:hover)').matches;
    const DEBUG = Store.get('cbhp_debug', false) === true;
    const log = (...a) => { if (DEBUG) { try { console.log('[cbhp]', ...a); } catch {} } };
    log('init ok, mode=' + cfg.mode + ', GM_xhr=' + (typeof GM_xmlhttpRequest) + ', Hls=' + (typeof Hls));
    const el = (tag, cls) => { const n = document.createElement(tag); if (cls) n.className = cls; return n; };

    let toastEl = null, toastTimer = 0;
    const toast = text => {
        if (!toastEl) { toastEl = el('div', 'cbhp-toast'); document.body.append(toastEl); }
        toastEl.textContent = text;
        toastEl.classList.add('cbhp-show');
        clearTimeout(toastTimer);
        toastTimer = setTimeout(() => toastEl.classList.remove('cbhp-show'), 1800);
    };

    const reg = (id, caption, fn) => {
        if (typeof GM_registerMenuCommand !== 'function') return;
        try { GM_registerMenuCommand(caption, fn, { id }); } catch { try { GM_registerMenuCommand(caption, fn); } catch {} }
    };
    const setMode = m => {
        if (!MODES.includes(m)) return;
        cfg.mode = m;
        Store.set('cbhp_mode', m); stop(); buildMenu();
    };
    const setSound = on => {
        cfg.sound = on === true;
        Store.set('cbhp_sound', cfg.sound); stop(); buildMenu();
    };
    const setMaxH = h => {
        if (!HEIGHTS.includes(+h)) return;
        cfg.maxH = +h;
        Store.set('cbhp_maxh', cfg.maxH); stop(); buildMenu();
    };
    const buildMenu = () => {
        reg('mode', `Превью при наведении: ${MODE_LABEL[cfg.mode]}`, () => {
            setMode(MODES[(MODES.indexOf(cfg.mode) + 1) % MODES.length]); toast(`Превью: ${MODE_LABEL[cfg.mode]}`);
        });
        reg('sound', `Звук при наведении: ${cfg.sound ? 'вкл' : 'выкл'}`, () => {
            setSound(!cfg.sound); toast(`Звук превью: ${cfg.sound ? 'вкл' : 'выкл'}`);
        });
        reg('q', `Качество мини-видео: до ${cfg.maxH}p`, () => {
            setMaxH(HEIGHTS[(HEIGHTS.indexOf(cfg.maxH) + 1) % HEIGHTS.length]); toast(`Мини-видео: до ${cfg.maxH}p`);
        });
        reg('dbg', `Отладка в консоли: ${DEBUG ? 'вкл' : 'выкл'} (нужна перезагрузка)`, () => {
            Store.set('cbhp_debug', !DEBUG); toast(`Отладка: ${!DEBUG ? 'вкл' : 'выкл'}, перезагрузите страницу`);
        });
    };
    buildMenu();
    CBHP.api = { modes: MODES, heights: HEIGHTS, cfg, setMode, setSound, setMaxH };

    const streams = new Map();
    const pending = new Map();

    const getStream = user => {
        const c = streams.get(user);
        if (c && Date.now() - c.t < (c.url ? T.okTtl : T.badTtl)) return Promise.resolve(c.url);
        if (pending.has(user)) return pending.get(user);
        const p = (async () => {
            try {
                const r = await fetch(`/api/chatvideocontext/${encodeURIComponent(user)}/`, {
                    credentials: 'same-origin',
                    headers: { 'X-Requested-With': 'XMLHttpRequest' }
                });
                log('chatvideocontext', user, r.status);
                if (!r.ok) return '';
                const j = await r.json();
                log('ctx', user, j && j.room_status, !!(j && j.hls_source));
                const url = j && j.room_status === 'public' && typeof j.hls_source === 'string' && /^https:\/\//.test(j.hls_source) ? j.hls_source : '';
                streams.set(user, { t: Date.now(), url });
                if (streams.size > 300) streams.delete(streams.keys().next().value);
                return url;
            } catch (err) { log('ctx error', err && err.message); return ''; }
            finally { pending.delete(user); }
        })();
        pending.set(user, p);
        return p;
    };
    const forget = user => streams.delete(user);

    const frameUrls = u => [
        `https://thumb.live.mmcdn.com/minifwap/${u}.jpg?${Date.now()}`,
        `https://jpeg.live.mmcdn.com/stream?room=${u}&f=${Math.random()}`
    ];

    let needGM = false;
    const gmBlob = url => new Promise((res, rej) => {
        if (typeof GM_xmlhttpRequest !== 'function') return rej(new Error('no gm'));
        GM_xmlhttpRequest({
            method: 'GET', url, responseType: 'blob', timeout: 8000,
            onload: r => (r.status >= 200 && r.status < 300 && r.response && r.response.size > 500) ? res({ url: URL.createObjectURL(r.response), size: r.response.size }) : rej(new Error('bad')),
            onerror: () => rej(new Error('err')), ontimeout: () => rej(new Error('timeout'))
        });
    });

    const OFFLINE_SIZES = new Set([4824, 4456, 9505, 2688, 3189, 4411, 3847]);

    const decodeInto = async (img, src) => { img.src = src; await img.decode(); };

    async function pushFrame(s) {
        const next = s.frames[s.flip ^ 1], prev = s.frames[s.flip];
        for (const url of frameUrls(s.user)) {
            if (s.dead) return false;
            try {
                if (needGM) {
                    const b = await gmBlob(url);
                    if (s.dead) { URL.revokeObjectURL(b.url); return false; }
                    const old = next.dataset.blob;
                    if (OFFLINE_SIZES.has(b.size)) { URL.revokeObjectURL(b.url); return 'same'; }
                    if (b.size === s.lastSize) { URL.revokeObjectURL(b.url); return 'same'; }
                    s.lastSize = b.size;
                    try { await decodeInto(next, b.url); } catch (e) { URL.revokeObjectURL(b.url); throw e; }
                    next.dataset.blob = b.url;
                    if (old) URL.revokeObjectURL(old);
                } else {
                    await decodeInto(next, url);
                }
                if (s.dead) return false;
                next.classList.add('cbhp-on');
                prev.classList.remove('cbhp-on');
                s.flip ^= 1;
                s.layer.classList.add('cbhp-in');
                return 'new';
            } catch (err) {
                log('frame fail', url, needGM ? 'gm' : 'direct', err && (err.message || err.name));
                if (!needGM && typeof GM_xmlhttpRequest === 'function') {
                    try { const b = await gmBlob(url); URL.revokeObjectURL(b.url); needGM = true; } catch {}
                }
            }
        }
        return false;
    }

    async function animLoop(s) {
        let fails = 0;
        while (!s.dead && !s.live) {
            if (!s.card.isConnected) { stop(); return; }
            const t0 = performance.now();
            const ok = await pushFrame(s);
            if (s.dead || s.live) return;
            fails = ok ? 0 : fails + 1;
            if (fails >= 3) return;
            await s.sleep(Math.max(ok === 'same' ? T.sameMs : T.frameMs - (performance.now() - t0), 0));
        }
    }

    function pickLevel(levels) {
        let idx = -1, best = -1;
        levels.forEach((l, i) => { const h = l.height || 0; if (h && h <= cfg.maxH && h > best) { best = h; idx = i; } });
        if (idx >= 0) return idx;
        let low = 0;
        levels.forEach((l, i) => { if ((l.height || 1e9) < (levels[low].height || 1e9)) low = i; });
        return low;
    }

    async function videoStart(s) {
        if (s.dead) return;
        s.layer.classList.add('cbhp-load');
        const url = await getStream(s.user);
        if (s.dead) return;
        if (!url) { s.layer.classList.remove('cbhp-load'); return; }
        attach(s, url);
    }

    const tryPlay = (v, s) => {
        const pr = v.play();
        if (pr && pr.catch) pr.catch(err => {
            log('play rejected', err && err.name, 'muted=' + v.muted);
            if (!v.muted && !s.dead) { v.muted = true; v.play().catch(() => {}); }
        });
    };

    function attach(s, url) {
        try { previewDirs.add(hlsDir(url)); if (previewDirs.size > 40) previewDirs.delete(previewDirs.values().next().value); } catch {}
        const v = s.video = el('video', 'cbhp-v');
        v.muted = !(cfg.sound && navigator.userActivation && navigator.userActivation.hasBeenActive);
        log('video attach, sound=' + cfg.sound + ', activation=' + !!(navigator.userActivation && navigator.userActivation.hasBeenActive) + ', muted=' + v.muted);
        v.playsInline = true;
        v.disablePictureInPicture = true;
        v.disableRemotePlayback = true;
        v.preload = 'auto';
        s.layer.insertBefore(v, s.layer.querySelector('.cbhp-bar'));

        const ready = () => {
            if (s.dead || s.live) return;
            s.live = true;
            s.layer.classList.remove('cbhp-load');
            s.layer.classList.add('cbhp-in', 'cbhp-live');
        };
        if ('requestVideoFrameCallback' in v) v.requestVideoFrameCallback(ready);
        else v.addEventListener('playing', ready, { once: true });

        const fail = () => { if (!s.dead) { s.layer.classList.remove('cbhp-load'); killVideo(s); } };

        if (typeof Hls !== 'undefined' && Hls.isSupported()) {
            const hls = s.hls = new Hls({
                autoStartLoad: false,
                lowLatencyMode: false,
                enableWorker: true,
                backBufferLength: 0,
                maxBufferLength: 4,
                maxMaxBufferLength: 8,
                maxBufferSize: 8 * 1000 * 1000,
                liveSyncDurationCount: 2,
                manifestLoadingMaxRetry: 1,
                levelLoadingMaxRetry: 1,
                fragLoadingMaxRetry: 2
            });
            hls.on(Hls.Events.MANIFEST_PARSED, (_, d) => {
                if (s.dead || s.hls !== hls) return;
                const idx = pickLevel(d.levels);
                hls.startLevel = idx;
                hls.currentLevel = idx;
                hls.startLoad(-1);
                tryPlay(v, s);
            });
            hls.on(Hls.Events.ERROR, (_, d) => {
                log('hls error', d.type, d.details, d.fatal);
                if (!d.fatal || s.dead) return;
                if (!s.retried && d.type === Hls.ErrorTypes.NETWORK_ERROR) {
                    s.retried = true; forget(s.user); killVideo(s);
                    getStream(s.user).then(u => { if (!s.dead && u) attach(s, u); else fail(); });
                } else fail();
            });
            hls.loadSource(url);
            hls.attachMedia(v);
        } else if (v.canPlayType('application/vnd.apple.mpegurl')) {
            v.src = url;
            tryPlay(v, s);
            v.addEventListener('error', fail, { once: true });
        } else fail();
    }

    function killVideo(s) {
        if (s.hls) { try { s.hls.stopLoad(); s.hls.detachMedia(); s.hls.destroy(); } catch {} s.hls = null; }
        if (s.video) { try { s.video.pause(); s.video.removeAttribute('src'); s.video.load(); } catch {} s.video.remove(); s.video = null; }
    }

    let cur = null;

    function start(card) {
        stop();
        if (cfg.mode === 'off' || card.querySelector(SKIP)) return;
        const img = card.querySelector(IMG), user = userOf(card);
        log('start', { user, img: !!img, skip: !!card.querySelector(SKIP) });
        if (!img || !user || !img.parentElement) return;

        const host = img.parentElement;
        const s = cur = {
            card, img, host, user, dead: false, live: false, flip: 0, retried: false,
            cancel: [], layer: null, frames: [], video: null, hls: null, restorePos: false,
            sleep(ms) { return new Promise(res => { const t = setTimeout(res, ms); s.cancel.push(() => { clearTimeout(t); res(); }); }); },
            after(ms, fn) { const t = setTimeout(() => { if (!s.dead) fn(); }, ms); s.cancel.push(() => clearTimeout(t)); }
        };

        s.after(touchy ? 0 : T.animDelay, () => {
            if (getComputedStyle(host).position === 'static') { host.style.position = 'relative'; s.restorePos = true; }
            const layer = s.layer = el('div', 'cbhp-layer');
            const a = el('img', 'cbhp-f'), b = el('img', 'cbhp-f');
            a.alt = b.alt = ''; a.decoding = b.decoding = 'async';
            s.frames = [a, b];
            layer.append(a, b, el('div', 'cbhp-bar'), el('div', 'cbhp-dot'));
            img.after(layer);
            log('layer added for', s.user);
            animLoop(s);
            if (cfg.mode === 'video') s.after(touchy ? 150 : Math.max(0, T.videoDelay - T.animDelay), () => videoStart(s));
        });
    }

    function stop() {
        const s = cur;
        if (!s) return;
        cur = null;
        s.dead = true;
        s.cancel.forEach(fn => { try { fn(); } catch {} });
        killVideo(s);
        const layer = s.layer;
        if (layer) {
            layer.classList.remove('cbhp-in');
            setTimeout(() => {
                layer.remove();
                s.frames.forEach(f => { if (f.dataset.blob) URL.revokeObjectURL(f.dataset.blob); f.removeAttribute('src'); });
                if (s.restorePos) s.host.style.position = '';
            }, 220);
        }
    }

    const onOver = e => {
        log('event', e.type, e.pointerType || '', e.target && e.target.className);
        if (cfg.mode === 'off' || e.buttons) return;
        const card = e.target.closest && e.target.closest(CARD);
        if (!card || (cur && cur.card === card)) return;
        if (e.relatedTarget && card.contains(e.relatedTarget)) return;
        start(card);
    };
    const onOut = e => {
        if (!cur) return;
        if (e.pointerType === 'touch') return;
        const card = e.target.closest && e.target.closest(CARD);
        if (!card || card !== cur.card) return;
        if (e.relatedTarget && card.contains(e.relatedTarget)) return;
        stop();
    };
    ['pointerover', 'mouseover'].forEach(t => document.addEventListener(t, onOver, true));
    ['pointerout', 'mouseout'].forEach(t => document.addEventListener(t, onOut, true));

    document.addEventListener('contextmenu', e => {
        if (touchy && cfg.mode !== 'off' && e.target.closest && e.target.closest(CARD)) e.preventDefault();
    }, true);

    document.addEventListener('click', e => { if (!touchy && cur && e.target.closest && e.target.closest(CARD)) stop(); }, true);

    let scrollTimer = 0;
    window.addEventListener('scroll', () => {
        if (!cur) return;
        clearTimeout(scrollTimer);
        scrollTimer = setTimeout(() => { if (cur && (touchy || !cur.card.matches(':hover'))) stop(); }, 120);
    }, { passive: true, capture: true });

    window.addEventListener('blur', stop);
    document.addEventListener('visibilitychange', () => { if (document.hidden) stop(); });
};

const init = () => {

    if (SITE === 'cb') { try { cbHoverPreview(); } catch (err) { console.error('[cb-hover-preview]', err); } }
    try { new App(); } catch (err) { console.error('[cams-unchained] App init failed', err); }
};
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();

})();