Universal Video Controls PRO

Speed/vol, autoplay blocking, per-site isolation. Transparent overlay. Auto-hide. Seek duration.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Universal Video Controls PRO
// @namespace    http://tampermonkey.net/
// @version      1.0.8
// @description  Speed/vol, autoplay blocking, per-site isolation. Transparent overlay. Auto-hide. Seek duration.
// @author       CLumsyKnight48
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        unsafeWindow
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

/*
  v1.0.8 — AUDIO / SPEED / BLOCKING / AUTO-HIDE

  FIX 1  Audio glitching on replay
    Boost.connect() was removing the video from the srcs WeakMap on the
    'ended' event without disconnecting the WebAudio MediaElementSourceNode.
    On replay Boost.has(v)=false so gain was never updated, but the now-
    untracked node still fed into the graph at a stale level — crackling.
    Fixed: the 'ended' listener is removed; the source stays live for replays.
    Instead an 'emptied' listener disconnects & resets when the src changes
    (element recycled) so a fresh source can be created for the new content.

  FIX 2  Speed boost intermittently fails on X homepage
    lockSpd() returns early when v.duration is NaN (stream not yet loaded),
    marking the video as "live". The loadedmetadata retry requires
    v===state.activeVideo which may have changed by then. Added short
    [200 ms, 600 ms] retries in activate() for autoStop platforms, mirroring
    the existing YouTube-specific retries.

  FIX 3  Strict autoplay blocking inconsistent on X
    Two related bugs. (A) _uvce_mp=true (set when a play is blocked) caused
    shouldBlock() to return false for that element. If X re-played via MSE or
    a non-prototype path, the play-event listener no longer blocked it — "blocked
    once = never blocked again". Fixed: removed !v._uvce_mp from shouldBlock()
    so re-plays are always evaluated. (B) The 5-second strict scan skipped
    videos with _uvce_mp=true even when they were actively playing. Fixed:
    removed the _uvce_mp guard from the scan. The scan is also now on its own
    1.5-second interval (was every 5 s) so gaps are shorter.

  FIX 4  VM.best() still used full isActuallyVisible (Reddit regression)
    The Reddit fix in v2.5.x only removed isActuallyVisible from consider().
    VM.best() still used it, so best() returned null for Reddit's lazy-loaded
    videos that have offsetWidth/offsetHeight=0 at IO-fire time. Changed to
    only filter elements where BOTH offsetWidth AND offsetHeight are 0
    (truly invisible / not-rendered), which catches X's invisible pre-loaded
    elements while allowing Reddit's normal-sized videos.

  FIX 5  Dead _uvce_sp flag removed
    _uvce_sp was checked in three places but never set anywhere. The dead
    guards were making blocking conditions subtly wrong. Removed.

  NEW   Optional auto-hide panel (Settings → Display → Auto-hide)
    When enabled the panel fades out after 3 seconds with no mouse activity.
    Moving the mouse back over the panel (or over the dot) restores it.
    Uses the existing userClosed/dot mechanism so it integrates with showMini.
    Panel stays visible while settings overlay is open.
*/

(function () {
    'use strict';

    const UW  = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
    const IS_TOP = window === window.top;
    const HAS_GM = typeof GM_getValue === 'function' && typeof GM_setValue === 'function';
    const HAS_MC = typeof GM_registerMenuCommand === 'function';

    let trustedHTML = s => s;
    if (window.trustedTypes && window.trustedTypes.createPolicy) {
        try { const p = window.trustedTypes.createPolicy('uvce-safe',{createHTML:s=>s}); trustedHTML=s=>p.createHTML(s); } catch(e){}
    }

    const RIG_ID  = 'uvce_v17';
    const GM_KEY  = 'uvce_settings';
    const LS_BACK = 'uvce_p17';
    const Z_TOP   = 2147483647;
    const VOL_MAX = 6.0;
    const RS_MIN  = 0.3;
    const RS_MAX  = 3.0;
    const AUTO_HIDE_DELAY = 3000; // ms

    const getMaxVol = () => { if(!eff('volumeBoost'))return 1.0; if(PLAT.name==='reddit')return 1.0; return VOL_MAX; };

    const SPD_EV  = ['ratechange','canplay','canplaythrough','playing','loadedmetadata','seeked','emptied','loadstart'];
    const IO_THR  = [0,.05,.1,.2,.4,.6,.8,1];
    const CORNERS = [{id:'TL',ar:'↖'},{id:'TR',ar:'↗'},{id:'BL',ar:'↙'},{id:'BR',ar:'↘'}];
    const MAX_SPDS= [1.5,2,3,4,5,6,10];

    const ISOLATABLE = new Set([
        'speed','maxSpeed','seekStep','skipGifs',
        'blockAutoplay','blockAutoplayStrict','autoUnmute','autoplay','noLoop','safeMode',
        'volumeBoost','autoHide',
        'hotkeys','hkSpace','hkSpeed','hkVol','hkMute','hkSeek',
        'showSeek','showSpeed','showVol','showSafe','showMute','showMini',
        'cleanCookies','cleanModals','cleanAds',
    ]);

    const CLEAN = {
        cookies:['#onetrust-banner-sdk','#onetrust-consent-sdk','#CybotCookiebotDialog','#CybotCookiebotDialogBodyUnderlay','#didomi-host','.didomi-popup','.cc-banner','.cc-window','[class*="cookie-banner"]','[id*="cookie-banner"]','[class*="cookie-consent"]','[class*="gdpr-banner"]'],
        modals: ['[class*="paywall"]','[id*="paywall"]','[class*="subscribe-wall"]','[class*="newsletter-popup"]','[class*="tp-modal"]','[class*="tp-backdrop"]'],
        ads:    ['[id*="taboola"]','[class*="taboola"]','[id*="outbrain"]','[class*="outbrain"]','[class*="dfp-ad"]'],
    };

    const PLAT = (() => {
        const h = location.hostname.replace(/^(www\.|m\.)/,'');
        let n = 'generic';
        if(h.includes('facebook.com')||h==='fb.watch') n='facebook';
        else if(h.includes('instagram.com'))           n='instagram';
        else if(h.includes('twitter.com')||h.includes('x.com')) n='twitter';
        else if(h.includes('youtube.com'))             n='youtube';
        else if(h.includes('tiktok.com'))              n='tiktok';
        else if(h.includes('twitch.tv'))               n='twitch';
        else if(h.includes('vimeo.com'))               n='vimeo';
        else if(h.includes('dailymotion.com'))         n='dailymotion';
        else if(h.includes('reddit.com'))              n='reddit';
        const autoStop = !['youtube','twitch','vimeo','dailymotion'].includes(n);
        const aggMute  = ['facebook','instagram','tiktok'].includes(n);
        return { name:n, label:n.charAt(0).toUpperCase()+n.slice(1), autoStop, aggMute };
    })();

    const fmtTime = s => {
        if(!isFinite(s)||s<0)return '--:--';
        const m=Math.floor(s/60),sec=Math.floor(s%60).toString().padStart(2,'0');
        return `${m}:${sec}`;
    };

    function volPosToGain(p){
        const mx=getMaxVol(); if(mx===1.0)return Math.max(0,Math.min(1,p/100));
        p=Math.max(0,Math.min(100,p)); if(p<=50)return p/50;
        const b=(p-50)/50; return 1+b*b*(mx-1);
    }
    function volGainToPos(g){
        const mx=getMaxVol(); if(mx===1.0)return Math.max(0,Math.min(100,g*100));
        g=Math.max(0,Math.min(mx,g)); if(g<=1)return g*50;
        return 50+Math.sqrt((g-1)/(mx-1))*50;
    }

    const DEFAULTS = {
        speed:2.0, maxSpeed:3.0, volume:1.0,
        safeMode:false, autoplay:true,
        blockAutoplay:false, blockAutoplayStrict:false, autoUnmute:false,
        noLoop:false, threshold:.2, corner:'BL',
        uiScaleW:1.0, uiScaleH:1.0,
        volumeBoost:true, verticalMode:false, autoHide:false,
        showSeek:true, showSpeed:true, showVol:true, showSafe:true, showMute:true, showMini:true,
        hotkeys:true, hkSpace:true, hkSpeed:true, hkVol:true, hkMute:true, hkSeek:true,
        seekStep:5, skipGifs:true,
        cleanCookies:false, cleanModals:false, cleanAds:false,
    };

    const Store = (() => {
        const read=()=>{if(HAS_GM){try{const r=GM_getValue(GM_KEY,'');if(r)return r;}catch{}}try{return localStorage.getItem(LS_BACK)||'';}catch{}return '';};
        const write=j=>{if(HAS_GM){try{GM_setValue(GM_KEY,j);}catch{}}try{localStorage.setItem(LS_BACK,j);}catch{}};
        return {
            load(){try{const d=Object.assign({},DEFAULTS,JSON.parse(read()));if(d.uiScale!=null&&d.uiScaleW===1.0)d.uiScaleW=d.uiScaleH=Math.max(RS_MIN,Math.min(RS_MAX,d.uiScale));return d;}catch{return{...DEFAULTS};}},
            save(s){const d={};for(const k of Object.keys(DEFAULTS))if(k in s&&k!=='volume')d[k]=s[k];write(JSON.stringify(d));},
        };
    })();

    const Site = (() => {
        const host=location.hostname.replace(/^(www\.|m\.)/,'');
        const lsKey='uvce_s2_'+host, gmKey='uvce_site_'+host;
        let gm={}; if(HAS_GM){try{gm=JSON.parse(GM_getValue(gmKey,'{}')||'{}');}catch{}}
        let ls={}; try{ls=JSON.parse(localStorage.getItem(lsKey)||'{}');}catch{}
        let c={...gm,...ls};
        const flush=()=>{const j=JSON.stringify(c),e=!Object.keys(c).length;try{e?localStorage.removeItem(lsKey):localStorage.setItem(lsKey,j);}catch{}if(HAS_GM){try{GM_setValue(gmKey,e?'{}':j);}catch{}}};
        return {host,get:k=>k in c?c[k]:null,set(k,v){v===null?delete c[k]:(c[k]=v);flush();}};
    })();

    const state={...Store.load(),activeVideo:null,userClosed:false,globalUnmuted:false,_lsbr:null,_sptog:false};
    state.volume=1.0;
    if(Site.get('useOwn')===true)for(const k of ISOLATABLE){const v=Site.get(k);if(v!==null)state[k]=v;}

    const isDisabled=()=>Site.get('disable')===true;
    const isIsolated=()=>Site.get('useOwn')===true;
    const eff=k=>{if(isIsolated()){const v=Site.get(k);if(v!==null)return v;}return state[k];};

    function saveSetting(key){
        if(key==='volume')return;
        if(isIsolated()&&ISOLATABLE.has(key)){Site.set(key,state[key]);}
        else{if(isIsolated()){const gs=Store.load();if(key in DEFAULTS){gs[key]=state[key];Store.save(gs);}}else{Store.save(state);}}
    }

    let _lastInteract=0;
    document.addEventListener('click',  e=>{if(e.isTrusted)_lastInteract=Date.now();},true);
    document.addEventListener('keydown',e=>{if(e.isTrusted&&' Enter'.includes(e.key))_lastInteract=Date.now();},true);

    const isFeedFresh=()=>Date.now()-_lastInteract<100;
    const isFresh    =()=>Date.now()-_lastInteract<500;
    const isVeryFresh=()=>Date.now()-_lastInteract<300;

    let _selfPaused=null;
    const markSelfPaused =v=>{_selfPaused=v;_activelyApproved.delete(v);};
    const clearSelfPaused=()=>{_selfPaused=null;};

    const _approvedPlays=new WeakSet();
    // Persistent consent set — a video lives here while playing with user consent.
    // Cleared by markSelfPaused (block or user-pause) and on ended/emptied.
    // The strict scan exempts videos in this set so it never re-pauses the
    // video the user is actively watching.
    const _activelyApproved=new WeakSet();
    const approvePlay=v=>{if(v)_approvedPlays.add(v);};
    const _lastSpeedPerVideo=new WeakMap();
    const deb=(fn,ms)=>{let t;return(...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms);};};

    // FIX 4: lighter visibility check — only filter both-zero-dimension elements.
    // Full isActuallyVisible caused Reddit lazy-loaded videos to never activate.
    // The offsetWidth&&offsetHeight combined check still catches X's invisible
    // pre-loaded zero-size elements.
    function isActuallyVisible(v){
        if(!v||!v.isConnected)return false;
        if(!v.offsetWidth&&!v.offsetHeight)return false; // both zero = not rendered
        const r=v.getBoundingClientRect();
        if(r.width<2||r.height<2)return false;
        const left=Math.max(r.left,0),right=Math.min(r.right,innerWidth);
        const top =Math.max(r.top, 0),bottom=Math.min(r.bottom,innerHeight);
        return right>left && bottom>top; // any viewport overlap (removed 50px min)
    }

    function sealNoLoop(v){
        if(v._uvce_ls)return; v._uvce_ls=true; v.loop=false;
        try{v.removeAttribute('loop');}catch{}
        try{
            const GOPD=Object.getOwnPropertyDescriptor,orig=GOPD(HTMLMediaElement.prototype,'loop');
            Object.defineProperty(v,'loop',{configurable:true,enumerable:true,get:()=>false,
                set(){if(!state.noLoop){delete v._uvce_ls;try{if(orig)Object.defineProperty(v,'loop',orig);else delete v.loop;}catch{}}}});
        }catch{}
        try{const mo=new MutationObserver(()=>{if(state.noLoop&&v.hasAttribute('loop'))try{v.removeAttribute('loop');}catch{}});mo.observe(v,{attributes:true,attributeFilter:['loop']});v._uvce_lmo=mo;}catch{}
        if(!v._uvce_nl_el){
            v._uvce_nl_el=true;
            v.addEventListener('ended',()=>{if(eff('noLoop'))v._uvce_nl_ended=Date.now();});
            v.addEventListener('emptied',()=>{delete v._uvce_nl_ended;});
        }
    }
    function unsealNoLoop(v){
        if(!v._uvce_ls)return; delete v._uvce_ls;
        if(v._uvce_lmo){v._uvce_lmo.disconnect();delete v._uvce_lmo;}
        try{delete v.loop;}catch{}
    }

    function watchMute(v){
        if(v._uvce_mw)return; v._uvce_mw=true;
        let _guard=false;
        v.addEventListener('volumechange',()=>{
            if(_guard||!state.globalUnmuted)return;
            _guard=true;
            if(v.muted)v.muted=false;
            if(v.volume<0.01)try{v.volume=1;}catch{}
            _guard=false;
        });
    }

    function watchAutoplayAttr(v){
        if(v._uvce_aaw)return; v._uvce_aaw=true;
        const strip=()=>{
            if((eff('blockAutoplay')||eff('blockAutoplayStrict'))&&PLAT.autoStop&&v.hasAttribute('autoplay')){
                v.autoplay=false; try{v.removeAttribute('autoplay');}catch{}
            }
        };
        try{const mo=new MutationObserver(strip);mo.observe(v,{attributes:true,attributeFilter:['autoplay']});v._uvce_aamo=mo;}catch{}
    }

    function positionHost(host,id){
        host.style.setProperty('position','fixed','important');
        host.style.setProperty('z-index',String(Z_TOP),'important');
        host.style.setProperty('pointer-events','none','important');
        for(const s of['top','right','bottom','left'])host.style.setProperty(s,'auto','important');
        const safe='max(10px,env(safe-area-inset-bottom,10px))';
        if(id==='TL'){host.style.setProperty('top','10px','important');host.style.setProperty('left','10px','important');}
        else if(id==='TR'){host.style.setProperty('top','10px','important');host.style.setProperty('right','10px','important');}
        else if(id==='BL'){host.style.setProperty('bottom',safe,'important');host.style.setProperty('left','10px','important');}
        else            {host.style.setProperty('bottom',safe,'important');host.style.setProperty('right','10px','important');}
    }

    function antiInvert(host){
        const chk=deb(()=>{try{const hf=getComputedStyle(document.documentElement).filter||'';const bf=document.body?(getComputedStyle(document.body).filter||''):'';host.style.setProperty('filter',(hf+bf).includes('invert')?'invert(1)':'none','important');}catch{}},300);
        chk();
        try{new MutationObserver(chk).observe(document.documentElement,{attributes:true,attributeFilter:['style','class']});}catch{}
        try{if(document.body)new MutationObserver(chk).observe(document.body,{attributes:true,attributeFilter:['style','class']});}catch{}
    }

    const BlockAuto=(() => {
        const orig=UW.HTMLMediaElement.prototype.play; let on=false;
        return {
            patch(){
                if(on)return; on=true;
                UW.HTMLMediaElement.prototype.play=function(...a){
                    if(eff('noLoop')&&PLAT.autoStop&&this._uvce_nl_ended&&Date.now()-this._uvce_nl_ended<3000&&!this._ok){
                        this._uvce_mp=true; return Promise.resolve();
                    }
                    const blocking=eff('blockAutoplay')||eff('blockAutoplayStrict');
                    if(!blocking){delete this._ok;return orig.apply(this,a);}
                    const fresh=PLAT.autoStop
                        ?(!eff('blockAutoplayStrict')&&isFeedFresh())
                        :(isVeryFresh()||(!eff('blockAutoplayStrict')&&isFresh()));
                    const gif=this.loop&&isFinite(this.duration)&&this.duration<=4;
                    if(!this._ok&&!fresh&&!gif){this._uvce_mp=true;return Promise.resolve();}
                    delete this._ok; return orig.apply(this,a);
                };
            },
            ok:v=>{if(v){v._ok=true;approvePlay(v);}},
        };
    })();

    const Cleaner=(() => {
        let mo=null,sel='';
        const build=()=>[...(eff('cleanCookies')?CLEAN.cookies:[]),...(eff('cleanModals')?CLEAN.modals:[]),...(eff('cleanAds')?CLEAN.ads:[])].join(',');
        const run=()=>{if(!sel)return;try{document.querySelectorAll(sel).forEach(e=>e.style.setProperty('display','none','important'));}catch{}if(eff('cleanModals')||eff('cleanCookies')){try{document.body?.style.removeProperty('overflow');document.documentElement.style.removeProperty('overflow');}catch{}}};
        return{apply(){sel=build();if(!sel){mo?.disconnect();mo=null;return;}run();if(!mo){mo=new MutationObserver(run);mo.observe(document.documentElement,{childList:true,subtree:true});}}};
    })();

    const Boost=(() => {
        let ctx=null,gain=null; const srcs=new WeakMap();
        const init=()=>{if(ctx)return true;try{ctx=new(UW.AudioContext||UW.webkitAudioContext)();gain=ctx.createGain();gain.connect(ctx.destination);return true;}catch{return false;}};
        return {
            ensureResumed(){if(ctx&&ctx.state==='suspended')ctx.resume().catch(()=>{});},
            // FIX 1: no 'ended' listener — source stays alive for replays so gain is
            // always managed. 'emptied' (src change) disconnects and resets properly.
            connect(v){
                if(getMaxVol()===1.0||srcs.has(v)||v._uvce_bc||!init())return;
                try{
                    if(v.src&&!v.src.startsWith(location.origin)&&v.readyState===0)v.crossOrigin='anonymous';
                    const s=ctx.createMediaElementSource(v);
                    s.connect(gain);
                    srcs.set(v,s);
                    v._uvce_bc=true;
                    // When the element is recycled for a new src, tear down cleanly
                    v.addEventListener('emptied',()=>{
                        const src=srcs.get(v);
                        if(src){try{src.disconnect();}catch{}srcs.delete(v);}
                        delete v._uvce_bc;
                    },{once:true});
                }catch{}
            },
            setGain:g=>{if(getMaxVol()===1.0)return;if(gain)gain.gain.value=Math.max(0,Math.min(g,getMaxVol()));},
            has:v=>srcs.has(v),
        };
    })();

    const Eng=(() => {
        const lockC=new WeakMap();
        const clampSafe=v=>Math.min(.6,v);
        const isGif=v=>eff('skipGifs')&&v.loop&&isFinite(v.duration)&&v.duration<=4&&!(v.audioTracks?.length>0&&!v.muted);
        const isLive=v=>!isFinite(v.duration);
        const isLocked=v=>lockC.has(v);

        function applyVol(v,gainOverride){
            if(!v)return;
            if(!state.globalUnmuted&&gainOverride==null)return;
            if(state.globalUnmuted&&v.muted)v.muted=false;
            const target=gainOverride!=null?gainOverride:(eff('safeMode')?clampSafe(state.volume):state.volume);
            const mx=getMaxVol(); const t=Math.min(target,mx);
            if(Boost.has(v)){if(v.volume<.99)v.volume=1;Boost.setGain(t);}
            else if(t>1){Boost.connect(v);if(Boost.has(v)){if(v.volume<.99)v.volume=1;Boost.setGain(t);}else{if(v.volume<.99)v.volume=1;}}
            else{if(Math.abs(v.volume-t)>.005)v.volume=t;}
        }

        function lockSpd(v){
            if(!v||isGif(v)||isLive(v)){unlockSpd(v);return;}
            unlockSpd(v);
            const ctrl=new AbortController(),sig=ctrl.signal; lockC.set(v,ctrl);
            const apply=()=>{
                if(!v.isConnected||!isFinite(v.duration))return;
                const spd=eff('speed');
                if(Math.abs(v.playbackRate-spd)>.01)try{v.defaultPlaybackRate=spd;v.playbackRate=spd;}catch{}
            };
            apply();
            let debT=null;
            const enf=()=>{clearTimeout(debT);debT=setTimeout(apply,30);};
            SPD_EV.forEach(e=>v.addEventListener(e,enf,{signal:sig}));
        }

        function unlockSpd(v){const c=lockC.get(v);if(c){c.abort();lockC.delete(v);}}

        function consider(v){
            if(isDisabled())return;
            if(!v.isConnected)return;
            const w=VM.best(state.threshold);
            if(!w||w!==v)return;
            activate(v);
        }

        function activate(v){
            if(isDisabled())return;
            const isNewVideo=state.activeVideo!==v;
            if(isNewVideo)state.userClosed=false;
            if(state.activeVideo)unlockSpd(state.activeVideo);
            state.activeVideo=v; state.globalUnmuted=false; clearSelfPaused();
            _activelyApproved.add(v); // user's current video is always consented
            if(_lastSpeedPerVideo.has(v))state.speed=_lastSpeedPerVideo.get(v);
            else if(isIsolated()){for(const k of ISOLATABLE){const val=Site.get(k);if(val!==null)state[k]=val;}}
            else{const gs=Store.load();state.speed=gs.speed??DEFAULTS.speed;for(const k of ISOLATABLE){if(k!=='speed'&&k in gs)state[k]=gs[k];}}
            UI.refreshSpd();UI.refreshVol();UI.refreshChips();
            if(state.noLoop)sealNoLoop(v);
            if(eff('autoUnmute')){
                state.globalUnmuted=true;
                const doUnmute=()=>{
                    if(state.activeVideo!==v||!eff('autoUnmute'))return;
                    if(v.muted)v.muted=false;
                    if(v.volume<0.01)try{v.volume=1;}catch{}
                };
                doUnmute();
                [50,150,300,600,1200].forEach(ms=>setTimeout(doUnmute,ms));
            }
            lockSpd(v); applyVol(v); UI.syncVis(); UI.kickRaf();
            // FIX 2: speed retries for YouTube AND autoStop (X, Reddit etc.)
            // Covers the case where duration=NaN at activation time
            if(PLAT.name==='youtube'){
                [500,1500,3000].forEach(ms=>setTimeout(()=>{if(state.activeVideo===v)lockSpd(v);},ms));
                [80,250,600].forEach(ms=>setTimeout(()=>UI.syncVis(),ms));
            } else if(PLAT.autoStop){
                [200,600].forEach(ms=>setTimeout(()=>{if(state.activeVideo===v&&!isLocked(v))lockSpd(v);},ms));
            }
            if(eff('autoplay')&&v.paused){
                if(!(PLAT.autoStop&&(eff('blockAutoplay')||eff('blockAutoplayStrict')))&&PLAT.name!=='youtube'){
                    v._uvce_mp=false; approvePlay(v); BlockAuto.ok(v); v.play().catch(()=>{});
                }
            }
        }
        return{applyVol,lockSpd,unlockSpd,consider,activate,isGif,isLive,isLocked,clampSafe};
    })();

    const VM=(() => {
        const obs=new WeakSet(),ratios=new Map(); let io=null;

        // FIX: X stores a reference to the native video.play() before our script
        // runs, bypassing the prototype patch. X's internal retry loop fires the
        // play event 30-60 times per second. Without debouncing, we called v.pause()
        // on every single event → rapid play/pause → audio glitch + CPU spike.
        // doPause() allows at most one actual pause() call per 300 ms per element.
        // All block-state flags (_uvce_mp, markSelfPaused) are still set on every
        // call so the blocking state is always current; only the expensive pause()
        // call is rate-limited.
        function doPause(v){
            v._uvce_mp=true;
            const now=Date.now();
            if(v._uvce_pausedAt&&now-v._uvce_pausedAt<300){return;} // debounce
            v._uvce_pausedAt=now;
            markSelfPaused(v);
            v.pause();
        }

        // FIX 3A: removed !v._uvce_mp — previously a blocked video was exempt from
        // future blocking. X re-plays via MSE bypassing the prototype patch, but the
        // play listener then skipped it because _uvce_mp was still true from the first
        // block. Now every unapproved play is evaluated regardless.
        function shouldBlock(v){
            return (eff('blockAutoplay')||eff('blockAutoplayStrict'))&&PLAT.autoStop&&!_approvedPlays.has(v);
        }

        function proc(v){
            if((eff('blockAutoplay')||eff('blockAutoplayStrict'))&&v.autoplay){v.autoplay=false;v.removeAttribute('autoplay');}
            if(PLAT.autoStop)watchAutoplayAttr(v);
            if(obs.has(v))return;
            obs.add(v); io.observe(v);
            if(state.noLoop)sealNoLoop(v);
            if(PLAT.aggMute)watchMute(v);
            if(shouldBlock(v)&&!v.paused&&!isFeedFresh()){doPause(v);}

            v.addEventListener('play',()=>{
                if(eff('noLoop')&&v._uvce_nl_ended&&Date.now()-v._uvce_nl_ended<3000){
                    doPause(v);return;
                }
                const wasApproved=_approvedPlays.has(v);
                if(wasApproved)_approvedPlays.delete(v);
                if(shouldBlock(v)&&!wasApproved){
                    const fresh=!eff('blockAutoplayStrict')&&isFeedFresh();
                    if(!fresh){doPause(v);return;}
                }
                // Play is allowed — mark as actively consented so the strict scan
                // doesn't pause this video on its next tick.
                _activelyApproved.add(v);
                if(v!==state.activeVideo)Eng.consider(v);
            });
            v.addEventListener('loadedmetadata',()=>{if(v===state.activeVideo)Eng.lockSpd(v);});
            // Clear active consent when the video ends or the element is recycled.
            if(!v._uvce_aa_el){
                v._uvce_aa_el=true;
                v.addEventListener('ended', ()=>{_activelyApproved.delete(v);});
                v.addEventListener('emptied',()=>{_activelyApproved.delete(v);});
            }
        }

        function scan(){document.querySelectorAll('video').forEach(proc);}

        function init(){
            io=new IntersectionObserver(entries=>{
                entries.forEach(e=>{
                    if(e.target.isConnected){ratios.set(e.target,e.intersectionRatio);if(e.intersectionRatio>=state.threshold)Eng.consider(e.target);}
                    else{ratios.delete(e.target);Eng.unlockSpd(e.target);}
                });
            },{threshold:IO_THR});
            scan(); setInterval(scan,8000);

            // General MO: microtask-batched (YouTube/Reddit safe)
            const pending=new Set();
            const flush=()=>{pending.forEach(v=>{if(v.isConnected)proc(v);});pending.clear();};
            new MutationObserver(mutations=>{
                let dirty=false;
                for(const m of mutations)
                    for(const nd of m.addedNodes)
                        if(nd.nodeType===1&&nd.isConnected){
                            const tag=nd.tagName;
                            if(tag==='SCRIPT'||tag==='STYLE'||tag==='LINK'||tag==='META')continue;
                            if(tag==='VIDEO'){pending.add(nd);dirty=true;}
                            else{const vs=nd.querySelectorAll?.('video');if(vs?.length){vs.forEach(v=>pending.add(v));dirty=true;}}
                        }
                if(dirty)queueMicrotask(flush);
            }).observe(document.documentElement,{childList:true,subtree:true});

            // Fast-block MO: synchronous, autoStop only
            new MutationObserver(mutations=>{
                if(!PLAT.autoStop)return;
                const blocking=(eff('blockAutoplay')||eff('blockAutoplayStrict'));
                if(!blocking&&!eff('noLoop'))return;
                for(const m of mutations)
                    for(const nd of m.addedNodes)
                        if(nd.nodeType===1&&nd.isConnected){
                            const vids=nd.tagName==='VIDEO'?[nd]:[...nd.querySelectorAll?.('video')||[]];
                            vids.forEach(v=>{
                                if(v.autoplay){v.autoplay=false;v.removeAttribute('autoplay');}
                                proc(v);
                                if(blocking&&!v.paused&&!_approvedPlays.has(v)&&!_activelyApproved.has(v)&&!isFeedFresh()){
                                    doPause(v);
                                }
                            });
                        }
            }).observe(document.documentElement,{childList:true,subtree:true});

            // Speed lock maintenance (5 s)
            setInterval(()=>{
                const v=state.activeVideo;
                if(v&&!Eng.isGif(v)&&!Eng.isLive(v)&&!Eng.isLocked(v))Eng.lockSpd(v);
            },5000);

            // FIX 3B: strict scan on its own 1.5 s interval.
            // _uvce_mp guard removed — a video that was blocked once but resumed
            // (via MSE or recycled element) is now caught on the next tick.
            setInterval(()=>{
                if(!eff('blockAutoplayStrict')||!PLAT.autoStop)return;
                document.querySelectorAll('video').forEach(vid=>{
                    if(vid.paused||_activelyApproved.has(vid))return;
                    vid._uvce_mp=true;markSelfPaused(vid);vid.pause();
                });
            },1500);
        }

        // FIX 4: replaced full isActuallyVisible with lighter check
        const best=t=>{
            let b=null,br=-1;
            ratios.forEach((r,v)=>{
                if(!v.isConnected)return;
                if(!v.offsetWidth&&!v.offsetHeight)return; // both-zero = truly invisible
                if(r>=t&&r>br){br=r;b=v;}
            });
            return b;
        };
        const ratio=v=>ratios.get(v)??0;
        return{init,scan,best,ratio};
    })();

    function pauseVideo(v){if(!v||v.paused)return;markSelfPaused(v);v._uvce_mp=true;v.pause();}
    function findTargetVideo(){
        if(state.activeVideo&&VM.ratio(state.activeVideo)>=0.1&&isActuallyVisible(state.activeVideo))return state.activeVideo;
        let bestV=null,bestArea=0;
        document.querySelectorAll('video').forEach(v=>{
            if(!isActuallyVisible(v))return;
            const r=v.getBoundingClientRect();
            const w=Math.min(r.right,innerWidth)-Math.max(r.left,0);
            const h=Math.min(r.bottom,innerHeight)-Math.max(r.top,0);
            if(w<=0||h<=0)return;
            const a=w*h;if(a>bestArea){bestArea=a;bestV=v;}
        });
        return bestV||VM.best(0)||state.activeVideo;
    }

    const Keys={init(){
        window.addEventListener('keydown',e=>{
            if(e.ctrlKey&&e.shiftKey&&e.key.toLowerCase()==='u'){e.preventDefault();e.stopImmediatePropagation();state.userClosed=false;UI.syncVis();return;}
            if(!eff('hotkeys'))return;
            const tag=document.activeElement?.tagName;
            if(tag==='INPUT'||tag==='TEXTAREA'||document.activeElement?.isContentEditable)return;
            if(e.ctrlKey||e.metaKey||e.altKey)return;
            if(e.key===' '&&eff('hkSpace')){
                if(!PLAT.autoStop)return;
                e.preventDefault();e.stopPropagation();e.stopImmediatePropagation();
                if(state._sptog)return;
                state._sptog=true;setTimeout(()=>state._sptog=false,120);
                const av=findTargetVideo();if(!av)return;
                if(av!==state.activeVideo)Eng.activate(av);
                if(av.paused){clearSelfPaused();av._uvce_mp=false;approvePlay(av);BlockAuto.ok(av);av.play().catch(()=>{});}
                else pauseVideo(av);
                return;
            }
            const v=state.activeVideo;if(!v)return;
            const setSpd=val=>{e.preventDefault();e.stopImmediatePropagation();state.speed=Math.min(eff('maxSpeed'),Math.max(.1,+val.toFixed(2)));if(v){v.playbackRate=state.speed;Eng.lockSpd(v);}UI.refreshSpd();saveSetting('speed');UI.toast(state.speed.toFixed(2)+'×','info',800);};
            switch(e.key){
                case 'r':case 'R':if(!eff('hkSpeed'))break;e.preventDefault();e.stopImmediatePropagation();if(Math.abs(state.speed-1.0)>0.01){state._lsbr=state.speed;setSpd(1.0);}else if(state._lsbr){setSpd(state._lsbr);state._lsbr=null;}break;
                case 's':case 'S':if(eff('hkSpeed'))setSpd(state.speed-.25);break;
                case 'd':case 'D':if(eff('hkSpeed'))setSpd(state.speed+.25);break;
                case '+':case '=':if(eff('hkSpeed'))setSpd(state.speed+.25);break;
                case '-':          if(eff('hkSpeed'))setSpd(state.speed-.25);break;
                case 'ArrowUp':   if(eff('hkVol')){e.preventDefault();e.stopImmediatePropagation();adjVol(.1);}break;
                case 'ArrowDown': if(eff('hkVol')){e.preventDefault();e.stopImmediatePropagation();adjVol(-.1);}break;
                case 'm':case 'M':if(eff('hkMute')){e.preventDefault();e.stopImmediatePropagation();togMute();}break;
                case 'ArrowRight':if(eff('hkSeek')&&!e.shiftKey){e.preventDefault();e.stopImmediatePropagation();v.currentTime=Math.min(v.duration||Infinity,v.currentTime+eff('seekStep'));}break;
                case 'ArrowLeft': if(eff('hkSeek')&&!e.shiftKey){e.preventDefault();e.stopImmediatePropagation();v.currentTime=Math.max(0,v.currentTime-eff('seekStep'));}break;
            }
        },true);
        function adjVol(d){
            Boost.ensureResumed();
            state.volume=Math.min(getMaxVol(),Math.max(0,+((state.volume+d).toFixed(2))));
            if(state.volume===0){state.globalUnmuted=false;if(state.activeVideo)state.activeVideo.muted=true;}
            else{state.globalUnmuted=true;if(state.activeVideo){state.activeVideo.muted=false;Eng.applyVol(state.activeVideo);}}
            UI.refreshVol();UI.toast(Math.round(state.volume*100)+'%','info',800);
        }
        function togMute(){
            const v=state.activeVideo;if(!v)return;
            if(v.muted||v.volume<0.01){v.muted=false;state.volume=Math.max(state.volume,1);state.globalUnmuted=true;Eng.applyVol(v);}
            else{v.muted=true;state.globalUnmuted=false;}
            UI.refreshVol();
        }
    }};

    // ── CSS ───────────────────────────────────────────────────────
    const CSS=`
:host{all:initial;pointer-events:none;color-scheme:dark;forced-color-adjust:none;
  --bg:#141820;--bg2:#212733;--bdr:#2D3545;--txt:#D9E0EE;--lbl:#6E738D;--ac:#22D3EE;
  --sf:#06B6D4;--ss:#2DD4BF;--se:#10B981;--vlo:#FFCC66;--vhs:#c26000;--vhe:#ef4444;
  --ear:#F9E2AF;--ply:#A6E3A1;--mut:#94E2D5;--def:#A6ADC8;--hov:#CDD6F4;
  --cbg:#212733;--chv:#2A3241;}
*,*::before,*::after{box-sizing:border-box}
.shell{pointer-events:none;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;line-height:1;user-select:none;display:inline-block}
.panel{pointer-events:auto;position:relative;color:var(--txt);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
  line-height:1;background:var(--bg);border:1px solid var(--bdr);border-radius:14px;
  padding:10px 10px 9px;width:min(calc(200px*var(--sw,1)),88vw);
  box-shadow:0 20px 52px rgba(0,0,0,.84);backdrop-filter:blur(28px) saturate(1.5);
  display:flex;flex-direction:column;overflow:visible}
.panel::before{content:'';position:absolute;top:0;left:15%;right:15%;height:1px;
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.13),transparent);border-radius:50%;pointer-events:none}
.panel.gone{display:none!important;pointer-events:none!important}
.main{display:flex;flex-direction:column;width:100%}
.cfg{display:none;position:absolute;top:0;min-width:min(260px,88vw);max-height:min(78vh,520px);
  overflow-y:auto;flex-direction:column;
  background:rgba(11,14,22,0.90);-webkit-backdrop-filter:blur(18px) saturate(2);
  backdrop-filter:blur(18px) saturate(2);border:1px solid rgba(255,255,255,.1);
  border-radius:14px;padding:10px 10px 12px;z-index:50;
  box-shadow:0 16px 48px rgba(0,0,0,.75);transition:opacity .2s ease}
.panel.sopen .cfg{display:flex}
.cfg.sizing{opacity:0!important;pointer-events:none!important;transition:none!important}
.mini{display:none;width:22px;height:22px;border-radius:50%;background:rgba(34,211,238,.7);
  border:1px solid rgba(34,211,238,.5);pointer-events:auto;cursor:pointer;
  align-items:center;justify-content:center;font-size:9px;font-weight:900;color:var(--bg);
  box-shadow:0 0 10px rgba(34,211,238,.3);margin-top:4px}
.mini.show{display:flex}.mini:hover{background:rgba(34,211,238,.9);transform:scale(1.15)}
.hdr{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;margin-bottom:8px}
.lw{display:flex;align-items:center;gap:4px}
.dot{width:5px;height:5px;border-radius:50%;background:var(--ac);box-shadow:0 0 6px var(--ac)}
.logo{font-size:7px;font-weight:900;color:var(--ac);text-transform:uppercase;letter-spacing:1.5px}
.plat{font-size:6.5px;font-weight:700;color:var(--lbl);background:rgba(255,255,255,.04);
  border-radius:4px;padding:2px 5px;text-transform:uppercase;text-align:center;justify-self:center}
.hbs{display:flex;gap:3px;justify-content:flex-end;align-items:center}
.hb{width:17px;height:17px;border-radius:50%;border:1px solid var(--bdr);background:var(--bg2);
  color:var(--def);font-size:9px;cursor:pointer;display:flex;align-items:center;
  justify-content:center;transition:all .15s;flex-shrink:0}
.hb:hover{background:var(--chv);color:var(--hov);border-color:var(--hov)}
.hb.x-btn{width:20px;height:20px;font-size:12px;font-weight:900;
  background:rgba(239,68,68,.1);border-color:rgba(239,68,68,.25);color:#f87171}
.hb.x-btn:hover{background:rgba(239,68,68,.3);border-color:rgba(239,68,68,.6);color:#fff}
.hb.rz-btn{font-size:9px;color:var(--ac);background:rgba(34,211,238,.08);border-color:rgba(34,211,238,.2)}
.hb.rz-btn:hover{background:rgba(34,211,238,.2);border-color:rgba(34,211,238,.4);color:#fff}
.badge{display:none;font-size:7px;font-weight:900;text-transform:uppercase;letter-spacing:.6px;border-radius:4px;padding:2px 6px;margin-bottom:6px;text-align:center}
.badge.on{display:block}.badge.info{color:#60a5fa;background:rgba(96,165,250,.09)}
.badge.ok{color:#34d399;background:rgba(52,211,153,.09)}.badge.warn{color:#fbbf24;background:rgba(251,191,36,.09)}
.isol-badge{display:none;font-size:7px;font-weight:900;color:var(--ac);background:rgba(34,211,238,.08);
  border:1px solid rgba(34,211,238,.2);border-radius:4px;padding:2px 6px;margin-bottom:7px;text-align:center}
.isol-badge.on{display:block}
.sticks-row{display:flex;flex-direction:column;gap:5px}
.stick-wrap{display:contents}
.stick-lbl{display:none;font-size:7px;font-weight:900;color:var(--lbl);text-transform:uppercase;text-align:center;margin-top:2px}
.tr{position:relative;width:100%;height:max(16px,calc(20px*var(--sh,1)));background:var(--bg2);
  border-radius:7px;border:1px solid var(--bdr);overflow:hidden;cursor:pointer;margin-bottom:0;transition:border-color .2s}
.tr:hover{border-color:var(--ac)}.tr.gone{display:none!important}
.bar{position:absolute;top:0;left:0;height:100%;pointer-events:none;border-radius:inherit}
.bs{background:linear-gradient(90deg,var(--ss),var(--se));opacity:.75}
.bsp{background:var(--sf);opacity:.75}
#vol_t{background:linear-gradient(90deg,rgba(255,204,102,.07) 50%,rgba(194,96,0,.12) 50%)}
.bv-lo{background:var(--vlo);opacity:.88}
.bv-hi{background:linear-gradient(90deg,var(--vhs),var(--vhe));opacity:.82}
.vol-mark{position:absolute;top:0;bottom:0;left:50%;width:2px;background:rgba(255,255,255,.5);pointer-events:none;z-index:4;border-radius:1px}
.vol-split{position:absolute;top:0;bottom:0;left:50%;width:1px;background:rgba(255,255,255,.2);pointer-events:none;z-index:3}
.lbl{position:absolute;left:7px;top:50%;transform:translateY(-50%);font-size:7px;font-weight:900;
  color:var(--lbl);text-transform:uppercase;letter-spacing:.5px;pointer-events:none;white-space:nowrap}
.lbl.vl{pointer-events:auto;cursor:pointer;transition:color .15s;padding:3px 0}
.lbl.vl:hover{color:var(--vlo)}.lbl.vl.bst{color:var(--vhs)}
.val{position:absolute;right:7px;top:50%;transform:translateY(-50%);font-size:9px;font-weight:700;color:var(--txt);pointer-events:none;white-space:nowrap}
.val.bst{color:var(--vhs);font-weight:900}
.val.dur{font-size:7.5px;color:var(--lbl)}
input[type=range]{position:absolute;width:100%;height:100%;opacity:0;cursor:pointer;z-index:5;margin:0;padding:0}
.sfb{display:none;position:relative;height:2px;border-radius:2px;
  background:linear-gradient(90deg,#10b981 40%,#f59e0b 70%,#ef4444 100%);margin-bottom:4px;opacity:.75}
.sfb.on{display:block}.sfn{position:absolute;top:-3px;width:3px;height:8px;background:#fff;border-radius:2px;transform:translateX(-50%);box-shadow:0 0 5px rgba(255,255,255,.5)}
.row{display:flex;gap:3px;margin-top:4px}
.btn{flex:1;min-width:0;height:max(24px,calc(29px*var(--sh,1)));background:var(--bg2);border:1px solid var(--bdr);border-radius:8px;color:var(--def);font-size:12px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s}
.btn:hover{background:var(--chv);color:var(--txt);border-color:var(--ac)}
.btn.on{background:rgba(34,211,238,.12);color:var(--ac);border-color:rgba(34,211,238,.28)}
.btn.safe-on{background:rgba(52,211,153,.1);color:#34d399;border-color:rgba(52,211,153,.22)}
.btn.mut{background:rgba(239,68,68,.1);color:#f87171;border-color:rgba(239,68,68,.22)}
.btn.gone{display:none!important}
#b_sf{color:var(--ear)}#b_sf:hover{background:rgba(249,226,175,.15);border-color:var(--ear)}
#b_pl{color:var(--ply)}#b_pl:hover{background:rgba(166,227,161,.15);border-color:var(--ply)}
#b_mu{color:var(--mut)}#b_mu:hover{background:rgba(148,226,213,.15);border-color:var(--mut)}
.sec{margin-bottom:10px}
.stitle{font-size:6.5px;font-weight:900;color:var(--lbl);text-transform:uppercase;letter-spacing:1.2px;margin-bottom:6px;padding-bottom:3px;border-bottom:1px solid var(--bdr)}
.stitle.iso{color:var(--ac);border-color:rgba(34,211,238,.25)}
.srow{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:5px;gap:5px}
.slbl{font-size:7px;font-weight:900;color:var(--lbl);text-transform:uppercase;white-space:nowrap;flex-shrink:0;padding-top:4px}
.chips{display:flex;gap:3px;flex-wrap:wrap;justify-content:flex-end}
.chip{padding:3px 6px;border-radius:5px;border:1px solid var(--bdr);background:var(--cbg);color:var(--lbl);font-size:7.5px;font-weight:800;cursor:pointer;transition:all .12s;white-space:nowrap}
.chip:hover{color:var(--txt);border-color:var(--ac);background:var(--chv)}
.chip.on{background:rgba(34,211,238,.12);color:var(--ac);border-color:rgba(34,211,238,.28)}
.chip.on-r{background:rgba(239,68,68,.1);color:#f87171;border-color:rgba(239,68,68,.25)}
.chip.soff{background:rgba(251,191,36,.1);color:#fbbf24;border-color:rgba(251,191,36,.25)}
.rs-row{display:flex;align-items:center;gap:4px;flex:1;min-width:0}
.rs-tr{height:16px!important;flex:1;min-width:40px;border-radius:6px!important;cursor:pointer}
.rs-tr input[type=range]{height:100%}
.rs-bar-w{background:var(--ac);opacity:.85}
.rs-bar-h{background:#8B5CF6;opacity:.85}
.panel.vert-mode{width:min(calc(110px*var(--sw,1)),28vw)!important;padding:8px 6px 8px}
.panel.vert-mode::before{display:none}
.vert-mode .main .hdr{display:flex;justify-content:center;flex-wrap:wrap;gap:2px;margin-bottom:4px}
.vert-mode .main .lw{gap:2px}
.vert-mode .main .plat{display:none}
.vert-mode .main .dot{width:4px;height:4px}
.vert-mode .main .logo{font-size:6px}
.vert-mode .main .hb{width:15px;height:15px;font-size:8px}
.vert-mode .main .hb.x-btn{width:18px!important;height:18px!important;font-size:11px!important}
.vert-mode .main .row{flex-direction:row;flex-wrap:wrap;gap:2px;margin-top:4px}
.vert-mode .main .btn{height:max(22px,calc(24px*var(--sh,1)));font-size:11px;border-radius:5px}
.vert-mode .sticks-row{flex-direction:row;gap:4px;height:max(90px,calc(140px*var(--sh,1)));align-items:stretch}
.vert-mode .stick-wrap{display:flex;flex-direction:column;align-items:center;flex:1;min-width:0;gap:2px}
.vert-mode .stick-lbl{display:block;font-size:6px}
.vert-mode .stick-wrap:has(.tr.gone){display:none!important}
.vert-mode .sticks-row .tr{flex:1;width:100%!important;height:auto!important;min-height:0;margin-bottom:0!important;border-radius:7px}
.vert-mode .sticks-row input[type=range]{writing-mode:vertical-lr;direction:rtl;-webkit-appearance:slider-vertical;appearance:slider-vertical;width:100%;height:100%;position:absolute;top:0;left:0;opacity:0;cursor:pointer;z-index:5;margin:0;padding:0}
.vert-mode .sticks-row .bar{width:100%!important;top:auto!important;bottom:0!important;left:0;right:0}
.vert-mode .sticks-row .bv-hi{bottom:50%!important}
.vert-mode #vol_t{background:linear-gradient(to top,rgba(255,204,102,.07) 50%,rgba(194,96,0,.12) 50%)}
.vert-mode .sticks-row .vol-mark{top:auto!important;bottom:50%!important;left:0!important;right:0!important;width:100%!important;height:2px!important}
.vert-mode .sticks-row .vol-split{top:auto!important;bottom:50%!important;left:0!important;right:0!important;width:100%!important;height:1px!important}
.vert-mode .sticks-row .lbl,.vert-mode .sticks-row .val{display:none}
.vert-mode .main .isol-badge{font-size:6px;padding:1px 4px;margin-bottom:4px}
@media(max-width:600px),(pointer:coarse){
  .panel{width:min(calc(180px*var(--sw,1)),88vw)!important}
  .panel.vert-mode{width:min(calc(90px*var(--sw,1)),26vw)!important}
  .tr{height:max(20px,calc(20px*var(--sh,1)))}
  .btn{height:max(28px,calc(29px*var(--sh,1)));font-size:13px}
  .hb.x-btn{width:26px!important;height:26px!important;font-size:14px!important}
  .vert-mode .sticks-row{height:max(110px,calc(140px*var(--sh,1)))}
  .mini{width:24px;height:24px;font-size:9px}
  .rs-tr{height:22px!important;border-radius:8px!important}
}`;

    function mkHTML(){
        const iso=isIsolated(),ev=k=>eff(k);
        const pos=volGainToPos(state.volume);
        const loPos=Math.min(pos,50),hiPos=Math.max(0,pos-50);
        const disPct=Math.round(state.volume*100)+'%',boost=state.volume>1;
        const spPct=((state.speed-.1)/(ev('maxSpeed')-.1)*100);
        const maxC=MAX_SPDS.map(v=>`<span class="chip${ev('maxSpeed')===v?' on':''}" data-max="${v}">${v}×</span>`).join('');
        const corC=CORNERS.map(o=>`<span class="chip${state.corner===o.id?' on':''}" data-cor="${o.id}">${o.ar}</span>`).join('');
        const vertOn=ev('verticalMode');
        const wFill=((state.uiScaleW-RS_MIN)/(RS_MAX-RS_MIN)*100).toFixed(1);
        const hFill=((state.uiScaleH-RS_MIN)/(RS_MAX-RS_MIN)*100).toFixed(1);
        const sk=ev('showSeek')?'':' gone',ss=ev('showSpeed')?'':' gone',sv=ev('showVol')?'':' gone';
        return `<div class="shell" id="shell"><div class="panel${state.userClosed?' gone':''}${vertOn?' vert-mode':''}" id="panel">
<div class="main">
  <div class="hdr">
    <div class="lw"><div class="dot"></div><span class="logo">UVCE</span></div>
    <span class="plat">${PLAT.label}</span>
    <div class="hbs">
      <div class="hb rz-btn" id="b_resetSize" title="Reset size">⟲</div>
      <div class="hb" id="bs" title="Settings">⚙</div>
      <div class="hb x-btn" id="bx" title="Close">✕</div>
    </div>
  </div>
  <div class="badge" id="badge"></div>
  <div class="isol-badge${iso?' on':''}" id="isol_badge">⚙ ${Site.host} — isolated</div>
  <div class="sticks-row">
    <div class="stick-wrap">
      <div class="tr${sk}" id="seek_t"><span class="lbl">Seek</span><input type="range" id="sld_seek" min="0" max="1000" step="1" value="0"><div class="bar bs" id="bar_s" style="width:0%"></div><div class="val dur" id="val_dur">─</div></div>
      <span class="stick-lbl">Seek</span>
    </div>
    <div class="stick-wrap">
      <div class="tr${ss}" id="speed_t"><span class="lbl">Speed</span><input type="range" id="sld_s" min=".1" max="${ev('maxSpeed')}" step=".05" value="${state.speed}"><div class="bar bsp" id="bar_sp" style="width:${spPct.toFixed(1)}%"></div><div class="val" id="val_s">${state.speed.toFixed(2)}×</div></div>
      <span class="stick-lbl">Spd</span>
    </div>
    <div class="stick-wrap">
      <div class="tr${sv}" id="vol_t"><span class="lbl vl${boost?' bst':''}" id="vol_lbl" title="Click: reset 100%">${boost?'⚡':'Vol'}</span><input type="range" id="sld_v" min="0" max="100" step="0.5" value="${pos.toFixed(1)}"><div class="bar bv-lo" id="bar_vlo" style="width:${loPos.toFixed(1)}%"></div><div class="bar bv-hi" id="bar_vhi" style="left:50%;width:${hiPos.toFixed(1)}%"></div><div class="vol-mark"></div><div class="vol-split"></div><div class="val${boost?' bst':''}" id="val_v">${disPct}</div></div>
      <span class="stick-lbl">Vol</span>
    </div>
  </div>
  <div class="sfb${ev('safeMode')?' on':''}" id="sfb"><div class="sfn" id="sfn" style="left:0%"></div></div>
  <div class="row">
    <button class="btn${ev('safeMode')?' safe-on':''}${ev('showSafe')?'':' gone'}" id="b_sf">👂</button>
    <button class="btn" id="b_pl">▶</button>
    <button class="btn${ev('showMute')?'':' gone'}" id="b_mu">🔊</button>
  </div>
</div>
<div class="cfg" id="cfg">
  <div class="hdr" style="margin-bottom:8px">
    <div class="lw"><span style="font-size:7px;font-weight:900;color:var(--ac);text-transform:uppercase;letter-spacing:1.5px">Settings</span></div>
    <span></span>
    <div class="hbs"><div class="hb x-btn" id="bsc">✕</div></div>
  </div>
  <div class="sec">
    <div class="stitle${iso?' iso':''}">${iso?`⚙ ${Site.host}`:'Global defaults'}</div>
    <div class="srow"><span class="slbl">Max speed</span><div class="chips">${maxC}</div></div>
    <div class="srow"><span class="slbl">Auto-play</span><div class="chips">
      <span class="chip${ev('autoplay')?' on':''}" id="c_ap">▶ Resume</span>
      <span class="chip${ev('blockAutoplay')?' on':''}" id="c_ba">⛔ Block</span>
      <span class="chip${ev('blockAutoplayStrict')?' on-r':''}" id="c_bas" title="Independent: zero-window + 1.5 s full scan">🚫 Strict</span>
      <span class="chip${ev('autoUnmute')?' on':''}" id="c_au">🔊 Unmute</span>
    </div></div>
    <div class="srow"><span class="slbl">Volume</span><div class="chips">
      <span class="chip${ev('safeMode')?' on':''}" id="c_sf2">👂 Cap 60%</span>
      <span class="chip${ev('volumeBoost')?' on':''}" id="c_vboost">⚡ Boost</span>
    </div></div>
    <div class="srow"><span class="slbl">No loop</span><div class="chips">
      <span class="chip${ev('noLoop')?' on':''}" id="c_nl">🔁 Loop</span>
    </div></div>
    <div class="srow"><span class="slbl">Sticks</span><div class="chips">
      <span class="chip${ev('showSeek')?' on':''}" id="c_seek">Seek</span>
      <span class="chip${ev('showSpeed')?' on':''}" id="c_spd">Speed</span>
      <span class="chip${ev('showVol')?' on':''}" id="c_vol">Volume</span>
    </div></div>
    <div class="srow"><span class="slbl">Buttons</span><div class="chips">
      <span class="chip${ev('showSafe')?' on':''}" id="c_shsf">👂</span>
      <span class="chip${ev('showMute')?' on':''}" id="c_shmu">🔊</span>
      <span class="chip${ev('showMini')?' on':''}" id="c_shmini" title="Show dot when panel is closed">● Dot</span>
    </div></div>
  </div>
  <div class="sec">
    <div class="stitle">Display</div>
    <div class="srow"><span class="slbl">Corner</span><div class="chips">${corC}</div></div>
    <div class="srow"><span class="slbl">Mode</span><div class="chips">
      <span class="chip${!vertOn?' on':''}" id="c_horiz">⬌ Horiz</span>
      <span class="chip${vertOn?' on':''}" id="c_vert">⬍ Vert</span>
    </div></div>
    <div class="srow"><span class="slbl">Auto-hide</span><div class="chips">
      <span class="chip${ev('autoHide')?' on':''}" id="c_ah" title="Panel hides after ${AUTO_HIDE_DELAY/1000} s with no mouse activity — click dot to restore">⏱ ${AUTO_HIDE_DELAY/1000} s</span>
    </div></div>
    <div class="srow" style="align-items:center">
      <span class="slbl" style="min-width:36px">⬌ W<br><span id="rs_w_pct" style="color:var(--ac);font-size:7px">${Math.round(state.uiScaleW*100)}%</span></span>
      <div class="rs-row"><div class="tr rs-tr"><div class="bar rs-bar-w" id="rs_w_fill" style="width:${wFill}%"></div><input type="range" id="sld_sw" min="${RS_MIN}" max="${RS_MAX}" step="0.05" value="${state.uiScaleW}"></div></div>
    </div>
    <div class="srow" style="align-items:center">
      <span class="slbl" style="min-width:36px">⬍ H<br><span id="rs_h_pct" style="color:#8B5CF6;font-size:7px">${Math.round(state.uiScaleH*100)}%</span></span>
      <div class="rs-row"><div class="tr rs-tr"><div class="bar rs-bar-h" id="rs_h_fill" style="width:${hFill}%"></div><input type="range" id="sld_sh" min="${RS_MIN}" max="${RS_MAX}" step="0.05" value="${state.uiScaleH}"></div></div>
    </div>
  </div>
  <div class="sec">
    <div class="stitle">Shortcuts</div>
    <div class="srow"><span class="slbl">Master</span><div class="chips"><span class="chip${ev('hotkeys')?' on':''}" id="c_hk">⌨ All</span></div></div>
    <div class="srow"><span class="slbl">Space</span><div class="chips"><span class="chip${ev('hkSpace')?' on':''}" id="c_hks">⏸ Play</span></div></div>
    <div class="srow"><span class="slbl">Speed</span><div class="chips"><span class="chip${ev('hkSpeed')?' on':''}" id="c_hkd">⚡ SDR</span></div></div>
    <div class="srow"><span class="slbl">Vol/Mute</span><div class="chips"><span class="chip${ev('hkVol')?' on':''}" id="c_hkv">↑↓</span><span class="chip${ev('hkMute')?' on':''}" id="c_hkm">M</span></div></div>
    <div class="srow"><span class="slbl">Seek</span><div class="chips"><span class="chip${ev('hkSeek')?' on':''}" id="c_hkl">←→</span></div></div>
  </div>
  <div class="sec">
    <div class="stitle">🌐 ${Site.host}</div>
    <div class="srow"><span class="slbl">Isolate</span><div class="chips">
      <span class="chip${iso?' on':''}" id="so_own">⚙ Isolated</span>
      <span class="chip${Site.get('disable')?' soff':''}" id="so_dis">🚫 Off</span>
    </div></div>
  </div>
  <div class="sec"><div class="stitle">Danger</div><div class="srow"><div class="chips"><span class="chip" id="so_reset">🗑 Reset all</span></div></div></div>
</div>
</div></div><button class="mini" id="mini" title="Re-open UVCE">●</button>`;
    }

    const UI=(() => {
        let rafId=null,lastFT=0,ui={};

        // AUTO-HIDE: starts a timer that sets userClosed after AUTO_HIDE_DELAY ms.
        // Cancelled while mouse is over the panel or settings is open.
        function startAutoHide(){
            clearTimeout(ui._ahTimer);
            if(!eff('autoHide')||state.userClosed)return;
            if(ui.panel?.classList.contains('sopen'))return;
            ui._ahTimer=setTimeout(()=>{
                if(!eff('autoHide'))return;
                if(ui.panel?.classList.contains('sopen'))return;
                state.userClosed=true;
                syncVis();
            },AUTO_HIDE_DELAY);
        }

        function setBarFill(bar,pct){
            if(!bar)return; pct=Math.max(0,Math.min(100,pct));
            if(eff('verticalMode')){bar.style.width='100%';bar.style.height=pct.toFixed(1)+'%';bar.style.top='auto';bar.style.bottom='0';}
            else{bar.style.width=pct.toFixed(1)+'%';bar.style.height='';bar.style.top='';bar.style.bottom='';}
        }
        function setVolBars(pos){
            const lo=Math.max(0,Math.min(50,pos)),hi=Math.max(0,pos-50);
            if(eff('verticalMode')){
                if(ui.bar_vlo)ui.bar_vlo.style.cssText=`width:100%;height:${lo.toFixed(1)}%;top:auto;bottom:0;left:0`;
                if(ui.bar_vhi)ui.bar_vhi.style.cssText=`width:100%;height:${hi.toFixed(1)}%;top:auto;bottom:50%;left:0`;
            }else{
                if(ui.bar_vlo)ui.bar_vlo.style.cssText=`width:${lo.toFixed(1)}%;height:100%;top:0;bottom:auto;left:0`;
                if(ui.bar_vhi)ui.bar_vhi.style.cssText=`width:${hi.toFixed(1)}%;height:100%;top:0;bottom:auto;left:50%`;
            }
        }

        function toast(msg,type='info',ms=2400){if(!ui.badge)return;clearTimeout(ui.badge._t);ui.badge.textContent=msg;ui.badge.className=`badge on ${type}`;ui.badge._t=setTimeout(()=>{ui.badge.className='badge';},ms);}

        function positionCfg(){
            const cfg=ui.cfg;if(!cfg)return;
            const c=state.corner;
            cfg.style.left  =(c==='TR'||c==='BR')?'auto':'0';
            cfg.style.right =(c==='TR'||c==='BR')?'0':'auto';
            cfg.style.top   =(c==='TL'||c==='TR')?'0':'auto';
            cfg.style.bottom=(c==='BL'||c==='BR')?'0':'auto';
        }

        function applyScaleW(w){
            const bW=state.verticalMode?110:200;
            w=Math.max(RS_MIN,Math.min(w,RS_MAX,(window.innerWidth-20)/bW));
            state.uiScaleW=w;
            if(ui.shell)ui.shell.style.setProperty('--sw',w.toFixed(3));
            const root=ui.panel?.getRootNode();
            if(root){
                const s=root.querySelector('#sld_sw');if(s)s.value=w;
                const p=root.querySelector('#rs_w_pct');if(p)p.textContent=Math.round(w*100)+'%';
                const f=root.querySelector('#rs_w_fill');if(f)f.style.width=((w-RS_MIN)/(RS_MAX-RS_MIN)*100).toFixed(1)+'%';
            }
            saveSetting('uiScaleW');
        }
        function applyScaleH(h){
            h=Math.max(RS_MIN,Math.min(h,RS_MAX));
            state.uiScaleH=h;
            if(ui.shell)ui.shell.style.setProperty('--sh',h.toFixed(3));
            const root=ui.panel?.getRootNode();
            if(root){
                const s=root.querySelector('#sld_sh');if(s)s.value=h;
                const p=root.querySelector('#rs_h_pct');if(p)p.textContent=Math.round(h*100)+'%';
                const f=root.querySelector('#rs_h_fill');if(f)f.style.width=((h-RS_MIN)/(RS_MAX-RS_MIN)*100).toFixed(1)+'%';
            }
            saveSetting('uiScaleH');
        }

        function attachResizeHide(sliderEl){
            if(!sliderEl)return;
            const hide=()=>{
                if(!ui.cfg)return;
                ui.cfg.classList.add('sizing');
                const restore=()=>ui.cfg?.classList.remove('sizing');
                window.addEventListener('pointerup',restore,{once:true,capture:true});
                window.addEventListener('touchend', restore,{once:true,capture:true});
            };
            sliderEl.addEventListener('pointerdown',hide,{passive:true});
            sliderEl.addEventListener('touchstart', hide,{passive:true});
        }

        function applyVerticalMode(on){
            if(!ui.panel)return;
            ui.panel.classList.toggle('vert-mode',on);
            const root=ui.panel.getRootNode();
            root.querySelector('#c_horiz')?.classList.toggle('on',!on);
            root.querySelector('#c_vert')?.classList.toggle('on',on);
            refreshSpd();refreshVol();
        }

        function applyBarVis(){
            if(!ui.panel)return;
            const root=ui.panel.getRootNode();
            root.querySelector('#seek_t')?.classList.toggle('gone',!eff('showSeek'));
            root.querySelector('#speed_t')?.classList.toggle('gone',!eff('showSpeed'));
            if(ui.vol_t)ui.vol_t.classList.toggle('gone',!eff('showVol'));
            if(ui.b_sf)ui.b_sf.classList.toggle('gone',!eff('showSafe'));
            if(ui.b_mu)ui.b_mu.classList.toggle('gone',!eff('showMute'));
        }

        function refreshSpd(){
            if(!ui.sld_s)return;
            ui.sld_s.value=state.speed;
            if(ui.val_s)ui.val_s.textContent=state.speed.toFixed(2)+'×';
            setBarFill(ui.bar_sp,(state.speed-.1)/(eff('maxSpeed')-.1)*100);
        }
        function refreshVol(){
            if(!ui.sld_v)return;
            const v=state.activeVideo;
            const isMuted=v?(v.muted||v.volume<0.01):false;
            const dg=isMuted?0:state.volume;
            const pos=volGainToPos(dg),pct=Math.round(dg*100)+'%',boost=dg>1;
            ui.sld_v.value=isMuted?'0':pos.toFixed(1);
            if(ui.val_v){ui.val_v.textContent=isMuted?'0%':pct;ui.val_v.classList.toggle('bst',boost);}
            setVolBars(isMuted?0:pos);
            if(ui.vol_lbl){ui.vol_lbl.textContent=(boost&&!isMuted)?'⚡':'Vol';ui.vol_lbl.classList.toggle('bst',boost&&!isMuted);}
        }
        function refreshChips(){
            if(!ui.panel)return;
            const root=ui.panel.getRootNode();
            const q=id=>root.querySelector('#'+id);
            const upd=(id,val,cls='on')=>{const el=q(id);if(el)el.classList.toggle(cls,!!val);};
            upd('c_ap',eff('autoplay'));upd('c_ba',eff('blockAutoplay'));upd('c_bas',eff('blockAutoplayStrict'),'on-r');
            upd('c_au',eff('autoUnmute'));upd('c_nl',eff('noLoop'));upd('c_sf2',eff('safeMode'));
            upd('c_vboost',eff('volumeBoost'));upd('c_seek',eff('showSeek'));upd('c_spd',eff('showSpeed'));
            upd('c_vol',eff('showVol'));upd('c_shsf',eff('showSafe'));upd('c_shmu',eff('showMute'));
            upd('c_shmini',eff('showMini'));upd('c_ah',eff('autoHide'));
            upd('c_hk',eff('hotkeys'));upd('c_hks',eff('hkSpace'));upd('c_hkd',eff('hkSpeed'));
            upd('c_hkv',eff('hkVol'));upd('c_hkm',eff('hkMute'));upd('c_hkl',eff('hkSeek'));
            upd('c_horiz',!eff('verticalMode'));upd('c_vert',eff('verticalMode'));
            const iso=isIsolated();upd('so_own',iso);upd('isol_badge',iso);
            applyBarVis();
        }
        function syncVis(){
            if(!ui.panel)return;
            const show=!isDisabled()&&!state.userClosed;
            ui.panel.classList.toggle('gone',!show);
            if(ui.mini)ui.mini.classList.toggle('show',!show&&eff('showMini'));
            if(!show){
                clearTimeout(ui._ahTimer);
                if(rafId!==null){cancelAnimationFrame(rafId);rafId=null;}
            }else{
                if(state.activeVideo&&rafId===null)kickRaf();
                startAutoHide(); // restart 3 s countdown whenever panel becomes visible
            }
        }
        function kickRaf(){if(rafId===null&&state.activeVideo)rafId=requestAnimationFrame(frame);}
        function frame(now){
            if(!state.activeVideo||(ui.panel&&ui.panel.classList.contains('gone'))){rafId=null;return;}
            if(now-lastFT<33){rafId=requestAnimationFrame(frame);return;}
            lastFT=now;
            const v=state.activeVideo;
            if(v){
                if(isFinite(v.duration)&&v.duration>0){
                    const pct=v.currentTime/v.duration;
                    if(ui.sld_seek)ui.sld_seek.value=Math.round(pct*1000);
                    setBarFill(ui.bar_s,pct*100);
                    if(ui.val_dur)ui.val_dur.textContent=fmtTime(v.currentTime)+' / '+fmtTime(v.duration);
                }else if(v.duration===Infinity){
                    if(ui.val_dur)ui.val_dur.textContent='LIVE';
                }else{
                    if(ui.val_dur)ui.val_dur.textContent='─';
                }
                if(ui.b_pl)ui.b_pl.textContent=v.paused?'▶':'⏸';
                if(ui.b_mu){const m=v.muted||v.volume<0.01;ui.b_mu.textContent=m?'🔇':'🔊';ui.b_mu.classList.toggle('mut',m);}
                if(Boost.has(v))Boost.setGain(eff('safeMode')?Eng.clampSafe(state.volume):state.volume);
                const ep=Math.round((eff('safeMode')?Eng.clampSafe(state.volume):Math.min(state.volume,1))*100);
                if(ui.sfn)ui.sfn.style.left=Math.min(100,ep)+'%';
            }
            rafId=requestAnimationFrame(frame);
        }

        function inject(){
            if(document.getElementById(RIG_ID))return;
            const host=document.createElement('div');host.id=RIG_ID;
            positionHost(host,state.corner);
            const sr=host.attachShadow({mode:'open'});
            sr.innerHTML=trustedHTML(`<style>${CSS}</style>${mkHTML()}`);
            const q=id=>sr.querySelector('#'+id);
            ui={
                shell:q('shell'),panel:q('panel'),cfg:q('cfg'),badge:q('badge'),mini:q('mini'),
                isol_badge:q('isol_badge'),
                bar_s:q('bar_s'),bar_sp:q('bar_sp'),bar_vlo:q('bar_vlo'),bar_vhi:q('bar_vhi'),
                sld_seek:q('sld_seek'),sld_s:q('sld_s'),sld_v:q('sld_v'),
                val_s:q('val_s'),val_v:q('val_v'),val_dur:q('val_dur'),
                vol_lbl:q('vol_lbl'),vol_t:q('vol_t'),
                sfb:q('sfb'),sfn:q('sfn'),seek_t:q('seek_t'),
                b_pl:q('b_pl'),b_mu:q('b_mu'),b_sf:q('b_sf'),
                b_resetSize:q('b_resetSize'),host,
            };
            bindEvents(sr);
            (document.body||document.documentElement).appendChild(host);
            antiInvert(host);
            ui.shell.style.setProperty('--sw',state.uiScaleW.toFixed(3));
            ui.shell.style.setProperty('--sh',state.uiScaleH.toFixed(3));
            applyVerticalMode(state.verticalMode);
            positionCfg();
            // Auto-hide: reset timer on mouse-over; restart on mouse-leave
            if(ui.panel){
                ui.panel.addEventListener('mouseenter',()=>{clearTimeout(ui._ahTimer);});
                ui.panel.addEventListener('mouseleave',()=>{startAutoHide();});
            }
            syncVis();refreshSpd();refreshVol();kickRaf();
            window.addEventListener('resize',deb(()=>applyScaleW(state.uiScaleW),200));
        }

        function bindEvents(sr){
            const q=id=>sr.querySelector('#'+id);
            const sc=id=>{const el=q(id);return el||{onclick:null,oninput:null};};

            sc('b_resetSize').onclick=()=>{applyScaleW(1.0);applyScaleH(1.0);toast('Size reset','ok',800);};
            const sw=q('sld_sw');if(sw){attachResizeHide(sw);sw.oninput=()=>applyScaleW(parseFloat(sw.value));}
            const sh=q('sld_sh');if(sh){attachResizeHide(sh);sh.oninput=()=>applyScaleH(parseFloat(sh.value));}

            sc('c_horiz').onclick=()=>{state.verticalMode=false;applyVerticalMode(false);saveSetting('verticalMode');};
            sc('c_vert').onclick=()=>{state.verticalMode=true;applyVerticalMode(true);saveSetting('verticalMode');};

            if(ui.sld_seek)ui.sld_seek.oninput=()=>{
                const v=state.activeVideo;if(!v||!isFinite(v.duration))return;
                const pct=parseFloat(ui.sld_seek.value)/1000;
                v.currentTime=pct*v.duration;setBarFill(ui.bar_s,pct*100);
                if(ui.val_dur)ui.val_dur.textContent=fmtTime(v.currentTime)+' / '+fmtTime(v.duration);
            };
            q('sld_s').oninput=()=>{
                state.speed=parseFloat(q('sld_s').value);refreshSpd();
                if(state.activeVideo){Eng.lockSpd(state.activeVideo);_lastSpeedPerVideo.set(state.activeVideo,state.speed);}
                saveSetting('speed');
            };
            q('sld_v').oninput=()=>{
                Boost.ensureResumed();
                state.volume=volPosToGain(parseFloat(q('sld_v').value)||0);
                const v=state.activeVideo;
                if(state.volume===0){state.globalUnmuted=false;if(v)v.muted=true;}
                else{state.globalUnmuted=true;if(v){if(v.muted)v.muted=false;Eng.applyVol(v);}}
                refreshVol();
            };
            q('vol_lbl').onclick=e=>{
                e.stopPropagation();state.volume=1.0;state.globalUnmuted=true;
                if(state.activeVideo){state.activeVideo.muted=false;Eng.applyVol(state.activeVideo);}
                refreshVol();toast('Volume reset','ok',1000);
            };
            sc('b_pl').onclick=()=>{
                const av=findTargetVideo();if(!av)return;
                if(av!==state.activeVideo)Eng.activate(av);
                if(av.paused){clearSelfPaused();av._uvce_mp=false;approvePlay(av);BlockAuto.ok(av);av.play().catch(()=>{});}
                else pauseVideo(av);
            };
            sc('b_mu').onclick=()=>{
                const v=state.activeVideo;if(!v)return;
                if(v.muted||v.volume<0.01){v.muted=false;state.volume=Math.max(state.volume,1);state.globalUnmuted=true;Eng.applyVol(v);}
                else{v.muted=true;state.globalUnmuted=false;}
                refreshVol();
            };
            sc('b_sf').onclick=()=>{state.safeMode=!state.safeMode;q('b_sf')?.classList.toggle('safe-on',state.safeMode);if(ui.sfb)ui.sfb.classList.toggle('on',state.safeMode);saveSetting('safeMode');};
            sc('bx').onclick=()=>{state.userClosed=true;syncVis();};
            sc('bs').onclick=()=>{refreshChips();ui.panel.classList.toggle('sopen');positionCfg();clearTimeout(ui._ahTimer);};
            sc('bsc').onclick=()=>{ui.panel.classList.remove('sopen');startAutoHide();};
            sc('mini').onclick=()=>{state.userClosed=false;syncVis();};

            sr.querySelectorAll('.chip[data-cor]').forEach(c=>c.onclick=()=>{
                state.corner=c.dataset.cor;positionHost(ui.host,state.corner);applyScaleW(state.uiScaleW);positionCfg();
                sr.querySelectorAll('.chip[data-cor]').forEach(x=>x.classList.toggle('on',x.dataset.cor===state.corner));
                saveSetting('corner');
            });
            sr.querySelectorAll('.chip[data-max]').forEach(c=>c.onclick=()=>{
                state.maxSpeed=parseFloat(c.dataset.max);q('sld_s').max=state.maxSpeed;
                if(state.speed>state.maxSpeed){state.speed=state.maxSpeed;Eng.lockSpd(state.activeVideo);}
                sr.querySelectorAll('.chip[data-max]').forEach(x=>x.classList.toggle('on',x.dataset.max==state.maxSpeed));
                refreshSpd();saveSetting('maxSpeed');
            });

            const tog=(id,key,cls,cb)=>{
                if(typeof cls==='function'){cb=cls;cls='on';}
                cls=cls||'on';
                const el=q(id);if(!el)return;
                el.onclick=()=>{state[key]=!state[key];el.classList.toggle(cls,state[key]);if(cb)cb(state[key]);saveSetting(key);};
            };
            tog('c_ap','autoplay');
            tog('c_ba','blockAutoplay',en=>{if(en)BlockAuto.patch();else{state.blockAutoplayStrict=false;q('c_bas')?.classList.remove('on-r');saveSetting('blockAutoplayStrict');}});
            tog('c_bas','blockAutoplayStrict','on-r',en=>{if(en)BlockAuto.patch();});
            tog('c_au','autoUnmute',en=>{if(en&&state.activeVideo){const v=state.activeVideo;state.globalUnmuted=true;if(v.muted)v.muted=false;if(v.volume<0.01)try{v.volume=1;}catch{}}});
            tog('c_nl','noLoop',en=>{document.querySelectorAll('video').forEach(en?sealNoLoop:unsealNoLoop);if(en)BlockAuto.patch();});
            tog('c_sf2','safeMode',()=>{if(ui.sfb)ui.sfb.classList.toggle('on',state.safeMode);q('b_sf')?.classList.toggle('safe-on',state.safeMode);});
            tog('c_vboost','volumeBoost',()=>{refreshVol();if(state.activeVideo)Eng.applyVol(state.activeVideo);});
            tog('c_seek','showSeek',v=>{const t=sr.querySelector('#seek_t');if(t)t.classList.toggle('gone',!v);});
            tog('c_spd','showSpeed',v=>{const t=sr.querySelector('#speed_t');if(t)t.classList.toggle('gone',!v);});
            tog('c_vol','showVol',v=>{if(ui.vol_t)ui.vol_t.classList.toggle('gone',!v);});
            tog('c_shsf','showSafe',v=>{if(ui.b_sf)ui.b_sf.classList.toggle('gone',!v);});
            tog('c_shmu','showMute',v=>{if(ui.b_mu)ui.b_mu.classList.toggle('gone',!v);});
            tog('c_shmini','showMini',()=>{syncVis();});
            tog('c_ah','autoHide',en=>{if(en)startAutoHide();else clearTimeout(ui._ahTimer);});
            tog('c_hk','hotkeys');tog('c_hks','hkSpace');tog('c_hkd','hkSpeed');
            tog('c_hkv','hkVol');tog('c_hkm','hkMute');tog('c_hkl','hkSeek');

            sc('so_own').onclick=()=>{
                if(isIsolated()){for(const k of ISOLATABLE)Site.set(k,null);Site.set('useOwn',false);Object.assign(state,Store.load(),{activeVideo:state.activeVideo,userClosed:state.userClosed});state.volume=1.0;}
                else{Site.set('useOwn',true);for(const k of ISOLATABLE)Site.set(k,state[k]);}
                refreshChips();refreshSpd();refreshVol();
            };
            sc('so_dis').onclick=()=>{
                const cur=Site.get('disable');Site.set('disable',cur?null:true);
                q('so_dis')?.classList.toggle('soff',!cur);
                if(!cur&&state.activeVideo){Eng.unlockSpd(state.activeVideo);state.activeVideo=null;}
                syncVis();
            };
            sc('so_reset').onclick=()=>{
                for(const k of ISOLATABLE)Site.set(k,null);Site.set('useOwn',false);Site.set('disable',null);
                Object.assign(state,DEFAULTS,{activeVideo:state.activeVideo,userClosed:false,globalUnmuted:false});
                state.volume=1.0;Store.save(state);
                applyScaleW(1.0);applyScaleH(1.0);applyVerticalMode(false);
                refreshSpd();refreshVol();refreshChips();syncVis();
                toast('Settings reset','ok',2000);
            };
        }
        return{inject,syncVis,kickRaf,refreshSpd,refreshVol,refreshChips,toast};
    })();

    if(eff('blockAutoplay')||eff('blockAutoplayStrict')||eff('noLoop'))BlockAuto.patch();
    VM.init();Keys.init();

    if(IS_TOP){
        Cleaner.apply();UI.inject();
        if(PLAT.name==='youtube'){
            window.addEventListener('yt-navigate-finish',()=>{
                if(!document.getElementById(RIG_ID))UI.inject();
                state.userClosed=false;UI.syncVis();
            });
        }
    }

    if(HAS_MC){
        GM_registerMenuCommand('▶ Show UVCE',()=>{state.userClosed=false;UI.syncVis();},'u');
        GM_registerMenuCommand('⛔ Block Auto',()=>{state.blockAutoplay=!state.blockAutoplay;if(state.blockAutoplay)BlockAuto.patch();else state.blockAutoplayStrict=false;saveSetting('blockAutoplay');saveSetting('blockAutoplayStrict');},'b');
        GM_registerMenuCommand('🚫 Strict',()=>{state.blockAutoplayStrict=!state.blockAutoplayStrict;if(state.blockAutoplayStrict)BlockAuto.patch();saveSetting('blockAutoplayStrict');},'t');
        GM_registerMenuCommand('🔁 No Loop',()=>{state.noLoop=!state.noLoop;document.querySelectorAll('video').forEach(state.noLoop?sealNoLoop:unsealNoLoop);if(state.noLoop)BlockAuto.patch();saveSetting('noLoop');},'l');
        GM_registerMenuCommand('⌨ Hotkeys',()=>{state.hotkeys=!state.hotkeys;saveSetting('hotkeys');},'k');
        GM_registerMenuCommand('🚫 Disable on '+Site.host,()=>{const cur=Site.get('disable');Site.set('disable',cur?null:true);if(!cur&&state.activeVideo){Eng.unlockSpd(state.activeVideo);state.activeVideo=null;}},'d');
    }
})();