☰

javjb bypass

this is my first project, thanks for using it. if you have any question or want to bypass more website, dm to my telegram: @wizard_8080

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴、Greasemonkey 油猴子 或 Violentmonkey 暴力猴,才能安装此脚本。

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴 或 Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴 或 Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         javjb bypass
// @namespace    wi3_avjb_bypass
// @version      1.6.5
// @description  this is my first project, thanks for using it. if you have any question or want to bypass more website, dm to my telegram: @wizard_8080
// @author       wi3
// @match        https://avjb.com/*
// @match        https://avjb.cc/*
// @match        https://javhd.com/*
// @match        https://enter.javhd.com/*
// @match        https://caribbeancom.com/*
// @match        https://*.caribbeancom.com/*
// @match        https://caribbeancompr.com/*
// @match        https://*.caribbeancompr.com/*
// @match        https://naughtyamerica.com/*
// @match        https://*.naughtyamerica.com/*
// @match        https://brazzers.com/*
// @match        https://*.brazzers.com/*
// @match        https://pornhubpremium.com/*
// @match        https://*.pornhubpremium.com/*
// @match        https://blacked.com/*
// @match        https://*.blacked.com/*
// @match        https://vixen.com/*
// @match        https://*.vixen.com/*
// @match        https://tushy.com/*
// @match        https://*.tushy.com/*
// @match        https://tushyraw.com/*
// @match        https://*.tushyraw.com/*
// @match        https://blackedraw.com/*
// @match        https://*.blackedraw.com/*
// @match        https://evilangel.com/*
// @match        https://*.evilangel.com/*
// @match        https://wicked.com/*
// @match        https://*.wicked.com/*
// @match        https://digitalplayground.com/*
// @match        https://*.digitalplayground.com/*
// @match        https://puretaboo.com/*
// @match        https://*.puretaboo.com/*
// @match        https://teamskeet.com/*
// @match        https://*.teamskeet.com/*
// @match        https://mofos.com/*
// @match        https://*.mofos.com/*
// @match        https://realitykings.com/*
// @match        https://*.realitykings.com/*
// @match        https://bangbros.com/*
// @match        https://*.bangbros.com/*
// @connect      *.jb-aiwei.cc
// @connect      jb-aiwei.cc
// @connect      *.imgclh.com
// @connect      *.avstatic.com
// @connect      avjb.com
// @connect      *.cdnjhd.com
// @connect      *.cdnjav.com
// @connect      javhd.com
// @connect      static.javhd.com
// @connect      fms.caribbeancom.com
// @connect      dl21.caribbeancom.com
// @connect      *.naughtycdn.com
// @connect      *.project1content.com
// @connect      *.phncdn.com
// @connect      www.pornhub.com
// @connect      pornhub.com
// @connect      web.archive.org
// @match        https://adulttime.com/*
// @match        https://*.adulttime.com/*
// @match        https://badteenspunished.com/*
// @match        https://*.badteenspunished.com/*
// @match        https://bountyhunterporn.com/*
// @match        https://*.bountyhunterporn.com/*
// @match        https://caughtmycoach.com/*
// @match        https://*.caughtmycoach.com/*
// @match        https://cheatingsis.com/*
// @match        https://*.cheatingsis.com/*
// @match        https://cumswappingsis.com/*
// @match        https://*.cumswappingsis.com/*
// @match        https://daddyslilangel.com/*
// @match        https://*.daddyslilangel.com/*
// @match        https://detentiongirls.com/*
// @match        https://*.detentiongirls.com/*
// @match        https://driverxxx.com/*
// @match        https://*.driverxxx.com/*
// @match        https://momsteachsex.com/*
// @match        https://*.momsteachsex.com/*
// @match        https://myfamilypies.com/*
// @match        https://*.myfamilypies.com/*
// @match        https://nubiles-casting.com/*
// @match        https://*.nubiles-casting.com/*
// @match        https://nubileset.com/*
// @match        https://*.nubileset.com/*
// @match        https://nubiles-porn.com/*
// @match        https://*.nubiles-porn.com/*
// @match        https://nubilesunscripted.com/*
// @match        https://*.nubilesunscripted.com/*
// @match        https://petiteballerinasfucked.com/*
// @match        https://*.petiteballerinasfucked.com/*
// @match        https://petitehdporn.com/*
// @match        https://*.petitehdporn.com/*
// @match        https://princesscum.com/*
// @match        https://*.princesscum.com/*
// @match        https://realitysis.com/*
// @match        https://*.realitysis.com/*
// @match        https://shesbreedingmaterial.com/*
// @match        https://*.shesbreedingmaterial.com/*
// @match        https://shesinmybed.com/*
// @match        https://*.shesinmybed.com/*
// @match        https://stepsiblingscaught.com/*
// @match        https://*.stepsiblingscaught.com/*
// @match        https://teacherfucksteens.com/*
// @match        https://*.teacherfucksteens.com/*
// @match        https://youngermommy.com/*
// @match        https://*.youngermommy.com/*
// @match        https://nubiles.net/*
// @match        https://*.nubiles.net/*
// @match        https://familyswap.xxx/*
// @match        https://*.familyswap.xxx/*
// @match        https://smashed.xxx/*
// @match        https://*.smashed.xxx/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @license      MIT
// @require      https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.1.5/hls.min.js
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    // ---- site dispatch (this block must stay FIRST: the wi3 trial guard
    // ---- is injected immediately after the line above and `return`s out of
    // ---- this IIFE, which has to disable BOTH halves at once).
    const _wi3Site = (function () {
        const h = location.hostname;
        if (h === 'avjb.com' || h === 'avjb.cc' || /\.avjb\.(com|cc)$/.test(h)) return 'avjb';
        if (h === 'javhd.com' || /\.javhd\.com$/.test(h)) return 'javhd';
        if (/(^|\.)caribbeancom(pr)?\.com$/.test(h)) return 'carib';
        if (/(^|\.)naughtyamerica\.com$/.test(h)) return 'na';
        if (/(^|\.)brazzers\.com$/.test(h)) return 't2gate';
        if (/(^|\.)pornhubpremium\.com$/.test(h)) return 'php';
        if (/(^|\.)(blacked|vixen|tushy|tushyraw|blackedraw|evilangel|wicked|digitalplayground|puretaboo)\.com$/.test(h)) return 'trlt';
        if (/(^|\.)teamskeet\.com$/.test(h)) return 'tskeet';
        if (/(^|\.)mofos\.com$|(^|\.)realitykings\.com$|(^|\.)bangbros\.com$/.test(h)) return 't2gate';
        if (/(^|\.)adulttime\.com$/.test(h)) return 'trlt';
        if (/(^|\.)(nubiles-porn|nubiles-casting|nubileset|nubilesunscripted|nubiles|momsteachsex|stepsiblingscaught|badteenspunished|bountyhunterporn|caughtmycoach|cheatingsis|cumswappingsis|daddyslilangel|detentiongirls|driverxxx|myfamilypies|petiteballerinasfucked|petitehdporn|princesscum|realitysis|shesbreedingmaterial|shesinmybed|teacherfucksteens|youngermommy)\.(com|net)$|(^|\.)(familyswap|smashed)\.xxx$/.test(h)) return 'nubil';
        return '';
    })();

    if (_wi3Site === 'avjb') {
        // ==================== AVJB half ====================


    // ============================================================
    // KNOWN CDN MAP  (folder structure: id/1000-floor = folder)
    // v1.2.2: 88newline (id<=~121k) + newbox (id>=~122k) serve the FULL
    // video HLS with NO signature — only Referer:avjb.com required.
    // ============================================================
    const SERVERS = [
        'https://88newline.jb-aiwei.cc',
        'https://newbox.jb-aiwei.cc',
        'https://99newline.jb-aiwei.cc',
        'https://7senver.jb-aiwei.cc',
        'https://r22.jb-aiwei.cc',
        'https://bmc2.imgclh.com',
        'https://cdn1.bmc2.imgclh.com',
        'https://cdn2.bmc2.imgclh.com',
        'https://v.bmc2.imgclh.com',
        'https://hls.bmc2.imgclh.com',
        'https://stream.bmc2.imgclh.com',
        'https://back.bbmmic2.com',
        'https://lvb.rsso2.com',
    ];
    const MIRRORS = ['https://newbox.jb-aiwei.cc', 'https://88newline.jb-aiwei.cc'];

    // v1.3.0 — trang VIP không nhả token (reg-wall): token gắn SESSION chứ
    // không gắn video. Trick tay của LO: vô trang FREE lấy CSRF -> quay lại
    // trang paid -> spped.php trả file đầy đủ. Ở đây tự động hoá y chang.
    const FREE_TOKEN_PAGES = [
        '/vi/video/126278/fc2-ppv-4694056-2/',
        '/vi/video/5552/9fb255b544c3ac94285563dd04dbfa9f/',
        '/vi/video/132865/2fc227f5caa952eaedad49cd3801e7a7/'
    ];

    // ============================================================
    // GM REQUEST — CORS-free, gửi được Referer (server bắt buộc
    // Referer chứa avjb.com; fetch thường bị CORS chặn vì mirror
    // không trả Access-Control-Allow-Origin).
    // ============================================================
    const GM_XHR = (typeof GM_xmlhttpRequest === 'function') ? GM_xmlhttpRequest : null;

    function gmRequest(url, responseType, timeout) {
        return new Promise((resolve, reject) => {
            if (!GM_XHR) { reject(new Error('no GM_xmlhttpRequest')); return; }
            try {
                GM_XHR({
                    method: 'GET',
                    url: url,
                    responseType: responseType || 'text',
                    timeout: timeout || 20000,
                    headers: { 'Referer': location.origin + '/' },
                    onload: (r) => resolve(r),
                    onerror: () => reject(new Error('gm http error: ' + url)),
                    ontimeout: () => reject(new Error('gm timeout: ' + url)),
                    onabort: () => reject(new Error('gm abort: ' + url)),
                });
            } catch (e) { reject(e); }
        });
    }

    // hls.js loader qua GM — manifest + segment đọc được dù mirror không có ACAO
    function GmLoader() {
        this._gm = null;
        this._aborted = false;
        this.stats = { aborted: false, loaded: 0, retry: 0, total: 0, chunkCount: 0, bwEstimate: 0,
            loading: { start: 0, first: 0, end: 0 }, parsing: { start: 0, end: 0 }, buffering: { start: 0, end: 0 } };
    }
    GmLoader.prototype.load = function (context, config, callbacks) {
        const self = this;
        const url = context.url;
        const rt = context.responseType === 'arraybuffer' ? 'arraybuffer' : 'text';
        self._aborted = false;
        const t0 = performance.now();
        const s = self.stats;
        s.aborted = false; s.loaded = 0; s.retry = 0; s.total = 0; s.chunkCount = 0; s.bwEstimate = 0;
        s.loading.start = t0; s.loading.first = t0; s.loading.end = 0;
        s.parsing.start = 0; s.parsing.end = 0;
        s.buffering.start = 0; s.buffering.end = 0;
        const netStub = { getResponseHeader: () => null, getAllResponseHeaders: () => '' };
        if (!GM_XHR) { callbacks.onError({ text: 'no GM_xhr', code: 0 }, context, netStub); return; }
        try {
            GM_XHR({
                method: 'GET', url: url, responseType: rt, timeout: 25000,
                headers: { 'Referer': location.origin + '/' },
                onload: (r) => {
                    const t1 = performance.now();
                    if (self._aborted || self.stats.aborted) return;
                    if (r.status >= 200 && r.status < 300) {
                        const data = r.response;
                        const len = (data && (data.byteLength || data.length)) || 0;
                        self.stats.loading.end = t1;
                        self.stats.loaded = len;
                        self.stats.total = len;
                        // xhr-loader gọi onProgress với payload trước onSuccess —
                        // stream-controller cần nó để transmuxer.push(), thiếu là stall
                        if (typeof callbacks.onProgress === 'function') {
                            callbacks.onProgress(self.stats, context, data, netStub);
                        }
                        callbacks.onSuccess({ url: url, data: data }, self.stats, context, netStub);
                    } else {
                        callbacks.onError({ text: 'HTTP ' + r.status, code: r.status }, context, netStub);
                    }
                },
                onerror: () => { if (!self._aborted) callbacks.onError({ text: 'gm error', code: 0 }, context, netStub); },
                ontimeout: () => { if (!self._aborted) callbacks.onError({ text: 'gm timeout', code: 0 }, context, netStub); },
            });
        } catch (e) {
            callbacks.onError({ text: String(e && e.message || e), code: 0 }, context, netStub);
        }
    };
    GmLoader.prototype.abort = function () {
        this._aborted = true;
        this.stats.aborted = true;
    };
    GmLoader.prototype.destroy = function () {};
    const PROBE_DATES = ['20220101','20220301','20220530','20220615','20220801',
        '20221001','20221201','20230201','20230501','20230801','20240101',
        '20240601','20250101','20250501','20250901','20260101','20260501'];

    let capturedUrl = null;     // stolen from the site's own player request
    let videoLoaded = false;
    let isChecking = false;
    let loopTimer = null;
    let loopMs = 1500;
    let apiFails = 0;
    let loopN = 0;

    // ============================================================
    // STAGE 0 — steal the stream URL at document-start
    // ============================================================
    if (!window.__wi3_avjb_hooked) {
        window.__wi3_avjb_hooked = true;

        // site's Playerjs lib (newjp.min.js) loads LATER and would clobber a
        // plain assignment — so trap it: setter eats the lib's definition,
        // getter always hands back our hook. Poll covers `function Playerjs(){}`
        // declarations, which replace the accessor outright.
        let _nativeP = null;

        function _capture(u) {
            if (u && typeof u === 'string' && !capturedUrl && !videoLoaded) {
                if (/_limt/i.test(u)) {          // teaser — paywalled 30s cut, never full
                    console.log('[wi3-AVJB] skip teaser:', u);
                    return;
                }
                capturedUrl = u;
                console.log('[wi3-AVJB] captured from Playerjs:', u);
            }
        }

        function _wi3Hook(config) {
            try { _capture(config && config.file); } catch (e) {}
            // dummy api: site code calls api("time"/"file"/...) — capture source
            // switches too, and never let site code crash on a dead player
            return {
                api: function (cmd, val) {
                    try { if (cmd === 'file') _capture(val); } catch (e) {}
                    const v = document.getElementById('wi3Video');
                    if (cmd === 'time' && v) return v.currentTime || 0;
                    if (cmd === 'duration' && v) return v.duration || 0;
                    if (cmd === 'file') return null;
                    return 0;
                }
            };
        }

        const _trap = function () {
            Object.defineProperty(window, 'Playerjs', {
                configurable: true,
                get: function () { return _wi3Hook; },
                set: function (fn) {
                    if (typeof fn === 'function' && fn !== _wi3Hook) _nativeP = fn;
                }
            });
        };
        try { _trap(); } catch (e) {}
        setInterval(function () {
            try {
                const cur = window.Playerjs;
                if (cur !== _wi3Hook) {
                    if (typeof cur === 'function') _nativeP = cur;
                    _trap();
                }
            } catch (e) {}
        }, 50);
    }

    function isBasicProbe(url) {
        const onKnown = SERVERS.some(s => url.startsWith(s));
        const signed = /md5=|[a-zA-Z0-9_-]{30,}/.test(url);
        return onKnown && !signed;
    }

    function grabIfM3u8(url) {
        if (!url || capturedUrl || videoLoaded) return;
        if (!/\.m3u8(\?|$)/i.test(url)) return;
        if (isBasicProbe(url)) return;
        capturedUrl = url;
        console.log('[wi3-AVJB] captured from network:', url);
    }

    const _fetch = window.fetch;
    window.fetch = function (input, init) {
        try {
            const method = (init && init.method || 'GET').toUpperCase();
            if (method !== 'HEAD') {
                grabIfM3u8(typeof input === 'string' ? input : input && input.url);
            }
        } catch (e) {}
        return _fetch.apply(this, arguments);
    };

    const _open = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (method, url) {
        try {
            if (String(method).toUpperCase() !== 'HEAD') grabIfM3u8(url);
        } catch (e) {}
        return _open.apply(this, arguments);
    };

    // ============================================================
    // STAGE 0b — custom player DOM (built early, injected later)
    // ============================================================
    const playerDiv = document.createElement('div');
    playerDiv.id = 'wi3Player';
    playerDiv.innerHTML = `
        <div class="wi3-content">
            <video id="wi3Video" controls preload="none" playsinline webkit-playsinline></video>
            <div class="wi3-footer">
                <span id="wi3Tips">⌛ initializing...</span>
                <a id="wi3Dl" href="#" target="_blank" rel="noopener">⏬ download</a>
            </div>
        </div>`;
    const video = playerDiv.querySelector('#wi3Video');
    const tipsEl = playerDiv.querySelector('#wi3Tips');
    const dlEl = playerDiv.querySelector('#wi3Dl');
    let hls = null;

    function setTips(text, color) {
        tipsEl.innerText = text;
        if (color) tipsEl.style.color = color;
    }

    // double-click left/right = -10s / +10s
    let lastClick = 0;
    video.addEventListener('click', (e) => {
        const now = Date.now();
        const dt = now - lastClick;
        if (dt < 300 && dt > 30) {
            const r = video.getBoundingClientRect();
            const x = (e.clientX - r.left) / r.width;
            const y = (e.clientY - r.top) / r.height;
            if (y > 0.9) { lastClick = now; return; }
            e.preventDefault(); e.stopPropagation();
            if (x < 0.5) video.currentTime = Math.max(0, video.currentTime - 10);
            else video.currentTime = Math.min(video.duration || 0, video.currentTime + 10);
        }
        lastClick = now;
    });

    // ============================================================
    // STAGE 1 — load an m3u8 into our player
    // ============================================================
    function loadStream(url) {
        if (videoLoaded || !url) return false;
        videoLoaded = true;
        console.log('[wi3-AVJB] loading stream:', url);
        dlEl.dataset.u = url;
        dlEl.href = url;   // mp4 tay; m3u8 se bi click handler chop lai tai truc tiep

        if (/\.mp4(\?|#|\/|$)/i.test(url) && !/\.m3u8/i.test(url)) {
            // direct mp4 (currentVideoURL fallback) — hls.js can't parse it
            video.src = url;
            video.addEventListener('loadedmetadata', () => {
                setTips('✅ unlocked — click to play', '#2ed573');
                stopLoop();
            }, { once: true });
            return true;
        }

        // ============================================================
        // TAI TRUC TIEP (v1.6.5) — khong qua trang dich vu. HLS: doc
        // playlist -> tai het segment GM_xhr (CORS-free) -> noi thanh
        // 1 file (.ts / .mp4) -> luu bang a.download. Co progress.
        // ============================================================
        async function wi3SaveDirect() {
            const gmBuf = async (u2) => {
                const r = await gmRequest(u2, 'arraybuffer', 30000);
                return r.response;
            };
            const fail = (m) => { setTips('❌ ' + m, '#ff4757'); console.warn('[wi3-dl]', m); };
            try {
                setTips('⬇ đọc playlist...', '#ffa502');
                const u = dlEl.dataset.u || url;
                if (!/\.m3u8(\?|$)/i.test(u)) {   // khong phai HLS: link direct luu ngay
                    dlEl.href = u; return;          // de browser xu ly binh thuong
                }
                let text = await gmRequest(u, 'text', 20000).then(r => r.response);
                let listUrl = u;
                // master playlist -> chon variant chat nhat (bandwidth cao nhat)
                if (/^#EXT-X-STREAM-INF/m.test(text)) {
                    const lines = text.split(/\r?\n/);
                    let best = null, bestBw = -1;
                    for (let i = 0; i < lines.length - 1; i++) {
                        if (!/^#EXT-X-STREAM-INF/m.test(lines[i])) continue;
                        const bw = parseInt((lines[i].match(/BANDWIDTH=(\d+)/) || [])[1] || '0', 10);
                        const cand = lines[i + 1].trim();
                        if (cand && !cand.startsWith('#') && bw > bestBw) { bestBw = bw; best = cand; }
                    }
                    if (!best) throw new Error('master khong co variant');
                    listUrl = new URL(best, u).href;
                    text = await gmRequest(listUrl, 'text', 20000).then(r => r.response);
                }
                if (!/^#EXTM3U/m.test(text)) throw new Error('khong phai playlist hop le');
                // lay segment (bo comment + tag), giu thu tu
                const segs = [];
                let mapUri = null;
                text.split(/\r?\n/).forEach(ln => {
                    const s = ln.trim();
                    if (!s) return;
                    if (s.startsWith('#EXT-X-MAP')) {
                        const mm = s.match(/URI="([^"]+)"/); if (mm) mapUri = mm[1];
                    } else if (s && !s.startsWith('#')) {
                        segs.push(s);
                    }
                });
                if (!segs.length) throw new Error('playlist rong');
                const parts = [];
                let got = 0;
                if (mapUri) {
                    const mu = new URL(mapUri, listUrl).href;
                    parts.push(await gmBuf(mu));
                    got++;
                }
                for (const s of segs) {
                    const su = new URL(s, listUrl).href;
                    const buf = await gmBuf(su);
                    if (buf && buf.byteLength) { parts.push(buf); got++; }
                    setTips('⬇ ' + got + '/' + segs.length + ' segment (' +
                        Math.round(got / segs.length * 100) + '%) ' +
                        Math.round(parts.reduce((a, b) => a + (b.byteLength || b), 0) / 1048576) + ' MB', '#ffa502');
                }
                if (!parts.length) throw new Error('tai segment that bai');
                const isMp4 = !!mapUri;
                const blob = new Blob(parts, { type: isMp4 ? 'video/mp4' : 'video/mp2t' });
                const name = ((document.title || 'video').replace(/[\\/:*?"<>|]/g, '_').slice(0, 90)) + (isMp4 ? '.mp4' : '.ts');
                const a = document.createElement('a');
                a.href = URL.createObjectURL(blob);
                a.download = name;
                document.body.appendChild(a);
                a.click();
                setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 60000);
                setTips('✅ đã tải: ' + name + ' (' + Math.round(blob.size / 1048576) + ' MB) — mở bằng VLC/mpv', '#2ed573');
            } catch (e) { fail('tải lỗi: ' + (e && e.message || e)); }
        }
        dlEl.onclick = (ev) => {
            const cu = dlEl.dataset.u || '';
            if (!/\.m3u8(\?|$)/i.test(cu)) return true;   // mp4: link direct, browser luu
            ev.preventDefault();
            wi3SaveDirect();
            return false;
        };
        if (typeof Hls !== 'undefined' && Hls.isSupported()) {
            if (hls) hls.destroy();
            const crossOrigin = (() => { try { return new URL(url, location.href).hostname !== location.hostname; } catch (e) { return false; } })();
            hls = new Hls({
                manifestLoadingTimeOut: 20000,
                manifestLoadingMaxRetry: 4,
                levelLoadingTimeOut: 20000,
                levelLoadingMaxRetry: 4,
                xhrSetup: (xhr) => { xhr.withCredentials = false; },
                enableWorker: true,
                // mirror không có CORS header + chặn hotlink → đọc qua GM
                loader: (crossOrigin && GM_XHR) ? GmLoader : Hls.DefaultConfig.loader,
            });
            hls.on(Hls.Events.MANIFEST_LOADING, () => setTips('⌛ loading manifest...'));
            hls.on(Hls.Events.MANIFEST_PARSED, () => {
                setTips('✅ unlocked — click to play', '#2ed573');
                stopLoop();
            });
            hls.on(Hls.Events.ERROR, (_e, data) => {
                if (!data.fatal) return;
                if (data.type === Hls.ErrorTypes.MEDIA_ERROR) { hls.recoverMediaError(); return; }
                setTips('❌ ' + (data.details || data.type), '#ff4757');
                videoLoaded = false;   // let the waterfall continue with next source
                kick();
            });
            hls.loadSource(url);
            hls.attachMedia(video);
        } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
            video.src = url;
            video.addEventListener('loadedmetadata', () => {
                setTips('✅ unlocked — click to play', '#2ed573');
                stopLoop();
            }, { once: true });
        }
        return true;
    }

    // ============================================================
    // STAGE 2a — ACCOUNT: detect session -> auto-login -> unlock
    // (video mới = reg-wall: trang chỉ nhả full URL khi đã đăng nhập)
    // ============================================================
    const CFG = { USER: '', PASS: '' };   // ← muốn hardcode tk thì điền 2 dòng này
    const accState = { tried: false, ok: false, fpN: 0 };

    function looksMember(h) {
        return /\/(logout|my-profile|my-settings|my-account|dashboard)(\/|["'#?])/.test(h);
    }

    // chẩn đoán: cái gì trong HTML match là "member" + trạng thái tường
    function fingerprint(label, h) {
        if (accState.fpN >= 2) return;
        accState.fpN++;
        try {
            const mm = h.match(/\/(logout|my-profile|my-settings|my-account|dashboard)(\/|["'#?])/);
            const ctx = mm ? h.substr(Math.max(0, mm.index - 70), 160).replace(/\s+/g, ' ') : '-';
            const pwTitle = (h.match(/class="pw-title"[^>]*>([^<]{0,100})/) || [])[1] || '-';
            const files = (h.match(/file\s*[:=]\s*["'][^"']+["']/g) || []).slice(0, 4);
            const sm3u8 = (h.replace(/\\\//g, '/').match(/https?:[^"'\\\s]+index\.m3u8\?[^"'\\\s]+/) || [])[0];
            const loginLink = /href="[^"]*\/login\/"/.test(h);
            console.log('[wi3-AVJB] FP(' + label + ') len=' + h.length +
                ' member=' + looksMember(h) + ' loginLink=' + loginLink +
                ' paywall=' + (h.match(/paywall/g) || []).length +
                ' token=' + /CSRF/i.test(h) + ' curURL=' + /currentVideoURL/.test(h));
            console.log('[wi3-AVJB] FP memberMatch:', mm ? mm[0] : null, '| ctx:', ctx);
            console.log('[wi3-AVJB] FP pwTitle:', pwTitle, '| fileUrls:', files, '| signedM3u8:', sm3u8 || null);
            if (label === 'fresh' && pwTitle !== '-') setTips('⚠ tường đang giữ: ' + pwTitle, '#ffa502');
        } catch (e) { console.warn('[wi3-AVJB] fp error', e); }
    }

    function getCreds() {
        if (CFG.USER && CFG.PASS) return { u: CFG.USER, p: CFG.PASS };
        try {
            const u = GM_getValue('avjb_user', '');
            const p = GM_getValue('avjb_pass', '');
            if (u && p) return { u, p };
        } catch (e) {}
        return null;
    }

    function askCreds(force) {
        try {
            if (!force && sessionStorage.getItem('wi3_asked')) return null;
            sessionStorage.setItem('wi3_asked', '1');
            const u = prompt('AVJB — username hoặc email:');
            if (!u) return null;
            const p = prompt('AVJB — password:');
            if (!p) return null;
            try { GM_setValue('avjb_user', u); GM_setValue('avjb_pass', p); } catch (e) {}
            console.log('[wi3-AVJB] creds saved for', u);
            return { u, p };
        } catch (e) { return null; }
    }

    async function ensureLogin(html) {
        if (accState.tried) return accState.ok;
        accState.tried = true;
        fingerprint('live', html);

        if (looksMember(html)) {            // đã đăng nhập sẵn trong browser
            accState.ok = true;
            console.log('[wi3-AVJB] member session detected — skip login');
            setTips('✅ session detected — unlocking...', '#2ed573');
            return true;
        }

        let c = getCreds();
        if (!c) {
            setTips('⚠ cần tài khoản avjb — đang hỏi...', '#ffa502');
            c = askCreds(false);
        }
        if (!c) {
            setTips('⚠ nhập tk qua menu Tampermonkey 🍅', '#ffa502');
            return false;
        }

        setTips('⌛ logging in as ' + c.u + '...');
        try {
            const body = new URLSearchParams({
                username: c.u, pass: c.p, remember_me: '1',
                action: 'login', email_link: location.origin + '/email/'
            }).toString();
            const ctrl = new AbortController();
            const timer = setTimeout(() => ctrl.abort(), 10000);
            const res = await fetch('/login/', {
                method: 'POST', credentials: 'same-origin', cache: 'no-store',
                signal: ctrl.signal,
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                    'X-Requested-With': 'XMLHttpRequest'
                },
                body: body
            });
            clearTimeout(timer);
            const txt = await res.text();
            console.log('[wi3-AVJB] login HTTP', res.status, '|', txt.slice(0, 200));
            const m = txt.match(/<failure>[\s\S]*?type="([^"]+)"/) || txt.match(/"error"\s*:\s*"([^"]+)"/);
            if (m) {
                const err = m[1];
                if (err === 'antispam' || err === 'captcha' || err === 'code') {
                    setTips('❌ login cần captcha — login tay 1 lần trong tab, script tự nhận session', '#ff4757');
                } else if (err === 'not_found' || err === 'wrong_password' || err === 'invalid_password') {
                    setTips('❌ sai mật khẩu — sửa qua menu TM 🍅', '#ff4757');
                } else {
                    setTips('❌ login lỗi: ' + err, '#ff4757');
                }
                return false;
            }
            accState.ok = true;
            setTips('✅ login ok — unlocking...', '#2ed573');
            return true;
        } catch (e) {
            setTips('❌ login: ' + (e && e.message), '#ff4757');
            console.warn('[wi3-AVJB] login error:', e);
            return false;
        }
    }

    // ============================================================
    // STAGE 2 — API: fresh CSRF -> spped.php -> try EVERY source
    // (v2.11.0 only tried defaultKey; we walk the whole list)
    // ============================================================
    async function fetchText(url, ms) {
        console.log('[wi3-AVJB] fetchText:', url);
        const ctrl = new AbortController();
        const t = setTimeout(() => ctrl.abort(), ms);
        try {
            const res = await fetch(url, { credentials: 'same-origin', cache: 'no-store', signal: ctrl.signal });
            if (!res.ok) throw new Error('HTTP ' + res.status);
            return await res.text();
        } finally { clearTimeout(t); }
    }

    // bất kỳ *CSRF* var nào cũng được — prefer PLAYER_CSRF, else dài nhất
    function grabToken(h) {
        const ms = [...h.matchAll(/([A-Za-z_]\w*CSRF\w*)\s*=\s*["']([^"']{20,})["']/g)];
        if (!ms.length) return null;
        const pref = ms.find(m => m[1] === 'PLAYER_CSRF');
        ms.sort((a, b) => b[2].length - a[2].length);
        return (pref || ms[0])[2];
    }

    let stealLastTry = 0;
    async function stealToken() {
        if (Date.now() - stealLastTry < 12000) return null;   // throttle 12s
        stealLastTry = Date.now();
        setTips('⌛ trang paid thiếu token — lấy từ trang free...', '#ffa502');
        for (const path of FREE_TOKEN_PAGES) {
            try {
                const freeHtml = await fetchText(path, 7000);
                const tok = grabToken(freeHtml);
                console.log('[wi3-AVJB] steal free page', path, '->', tok ? 'token OK' : 'no token');
                if (!tok) continue;
                // "quay lại" trang paid — server set video hiện tại là trang này
                try {
                    console.log('[wi3-AVJB] steal DEBUG — back-nav fetch starting:', location.href);
                    const back = await fetchText(location.href, 7000);
                    console.log('[wi3-AVJB] steal DEBUG — back-nav done, len=' + (back || '').length);
                    const paidTok = grabToken(back);
                    if (paidTok) {
                        console.log('[wi3-AVJB] token appeared after back-nav — dùng token paid');
                        return paidTok;
                    }
                } catch (e) {
                    console.warn('[wi3-AVJB] back-nav refetch failed:', e && e.message);
                }
                return tok;
            } catch (e) {
                console.warn('[wi3-AVJB] steal fail:', path, e && e.message);
            }
        }
        return null;
    }

    // nguồn từ video khác (get_file /126000/126278/...) → không bấm nhầm
    function urlMatchesVideo(u, folder, vid) {
        if (u.includes('/' + vid + '/') || u.includes('/' + vid + '_') ||
            u.includes('/' + folder + '/') || u.includes(vid + 'video') ||
            u.includes(vid + '_')) return true;
        const m = u.match(/\/(\d{3,7})\/(\d{4,7})\//);
        if (m) return +m[1] === folder || +m[2] === vid;
        return true;
    }

    async function playSources(data, strict) {
        if (!data || !Array.isArray(data.sources) || !data.sources.length) return false;
        const vid = idFromPath();
        const folder = vid ? Math.floor(vid / 1000) * 1000 : 0;
        const ordered = [...data.sources].sort((a, b) =>
            (b.key === data.defaultKey) - (a.key === data.defaultKey));
        for (const src of ordered) {
            if (!src || !src.url) continue;
            if (strict && vid && !urlMatchesVideo(src.url, folder, vid)) {
                console.log('[wi3-AVJB] skip other-video source:', src.url);
                continue;
            }
            console.log('[wi3-AVJB] trying source key:', src.key);
            if (loadStream(src.url)) return true;
            await new Promise(r => setTimeout(r, 2500));
            if (videoLoaded) return true;
        }
        return false;
    }

    async function apiBypass() {
        setTips('⌛ extracting token...');
        // token lives in inline <script>s — read from live DOM, zero network.
        // (Refetching the full page used to hang behind streaming connections.)
        let raw = document.documentElement.innerHTML || '';
        let token = grabToken(raw);
        if (!token) {
            const texts = [];
            document.querySelectorAll('script').forEach(s => { if (s.textContent) texts.push(s.textContent); });
            token = grabToken(texts.join('\n'));
        }
        if (!token) {
            // v1.2 — trang không token = reg-wall: tự detect session / auto-login trước
            await ensureLogin(raw);
        }
        if (!token) {
            try {
                const ctrl = new AbortController();
                const timer = setTimeout(() => ctrl.abort(), 6000);
                const res = await fetch(location.href, {
                    cache: 'no-store', credentials: 'same-origin', signal: ctrl.signal
                });
                const fresh = await res.text();
                clearTimeout(timer);
                token = grabToken(fresh);
                raw = fresh;                // giữ HTML mới cho stage 2b/2c
                fingerprint('fresh', fresh);
                if (token) {
                    console.log('[wi3-AVJB] token found after login/refetch');
                } else if (accState.ok && !looksMember(fresh)) {
                    setTips('❌ login chưa có hiệu lực — login tay rồi F5', '#ff4757');
                }
            } catch (e) {
                console.warn('[wi3-AVJB] refetch failed:', e && e.message);
            }
        }

        // v1.3.0 — vẫn không token? ăn cắp từ trang free rồi quay lại (cách của LO)
        let stolen = false;
        if (!token) {
            token = await stealToken();
            stolen = !!token;
            if (token) console.log('[wi3-AVJB] stolen token from free page');
        }

        if (token) {
            console.log('[wi3-AVJB] CSRF ok — calling spped.php');
            setTips('⌛ requesting sources...');
            try {
                const ctrl = new AbortController();
                const timer = setTimeout(() => ctrl.abort(), 8000);
                const res = await fetch('/player/spped.php?csrf=' + encodeURIComponent(token),
                    { credentials: 'same-origin', cache: 'no-store', signal: ctrl.signal });
                clearTimeout(timer);
                if (!res.ok) throw new Error('HTTP ' + res.status);
                const data = await res.json();
                if (await playSources(data, stolen)) return true;
            } catch (e) {
                console.warn('[wi3-AVJB] api failed:', e);
            }
        } else {
            console.warn('[wi3-AVJB] no CSRF token anywhere (refetch + steal) — direct fallback');
            setTips('⚠ chưa có token — loop chậm chờ server cấp...', '#ffa502');
        }

        // STAGE 2b — the site's own no-CSRF path: direct file URL in page source
        const dm = (raw.match(/currentVideoURL\s*=\s*["']([^"']+)["']/) || [])[1];
        if (dm) {
            const clean = dm.replace(/^function\/0\//, '');
            if (/\.mp4|\.m3u8/.test(clean)) {
                console.log('[wi3-AVJB] direct URL fallback:', clean);
                if (loadStream(clean)) return true;
            }
        }
        // STAGE 2c — trang đăng nhập có thể nhúng file thật thẳng vào Playerjs
        const fmAll = raw.match(/file\s*[:=]\s*["']([^"']+)["']/g) || [];
        for (const f of fmAll) {
            const u = (f.match(/["']([^"']+)["']/) || [])[1];
            if (!u || !/\.(mp4|m3u8)/i.test(u)) continue;
            if (/_limt/i.test(u)) {
                console.log('[wi3-AVJB] inline file = teaser (skip):', u);
                continue;
            }
            console.log('[wi3-AVJB] inline file fallback:', u);
            if (loadStream(u)) return true;
        }
        // STAGE 2d — signed m3u8 anywhere in page source
        const sm = (raw.replace(/\\\//g, '/').match(/https?:[^"'\\\s]+index\.m3u8\?[^"'\\\s]+/) || [])[1];
        if (sm) {
            console.log('[wi3-AVJB] signed m3u8 in page:', sm);
            if (loadStream(sm)) return true;
        }
        return false;
    }

    // ============================================================
    // STAGE 2.5 — MIRROR FAST PATH (v1.2.2)
    // 88newline + newbox nhả full HLS unsigned (chỉ cần Referer avjb.com
    // — browser tự gửi khi cross-origin fetch). 2 URL song song, ~200ms.
    // ============================================================
    let mirrorLastTry = 0;

    function mirrorProbe(videoID) {
        if (!videoID || videoLoaded) return Promise.resolve(false);
        const now = Date.now();
        if (now - mirrorLastTry < 8000) return Promise.resolve(false);
        mirrorLastTry = now;
        const folder = Math.floor(videoID / 1000) * 1000;
        let done = false;
        console.log('[wi3-AVJB] mirror probe — id:', videoID, 'folder:', folder);
        const jobs = MIRRORS.map(base => {
            const url = `${base}/videos/${folder}/${videoID}/index.m3u8`;
            const grab = GM_XHR
                ? gmRequest(url, 'text', 12000).then(r => ({ ok: r.status >= 200 && r.status < 300, text: r.response || '' }))
                : fetch(url, { cache: 'no-store', credentials: 'omit' })
                    .then(r => r.ok ? r.text().then(t => ({ ok: true, text: t })) : ({ ok: false, text: '' }))
                    .catch(() => ({ ok: false, text: '' }));
            return grab
                .then(res => {
                    if (!res.ok || done || videoLoaded || !/^#EXTM3U/.test((res.text || '').trim())) return null;
                    done = true;
                    console.log('[wi3-AVJB] mirror hit:', url, '(' + res.text.length + ' bytes)');
                    loadStream(url);
                    return url;
                })
                .catch(() => null);
        });
        return Promise.all(jobs).then(() => done);
    }

    // ============================================================
    // STAGE 3 — legacy CDN probe (HEAD-scan folder structure)
    // ============================================================
    function idFromPath() {
        const m = location.pathname.match(/\/video\/(\d+)/);
        if (m) return parseInt(m[1], 10);
        const meta = document.querySelector('meta[property="og:image"]');
        const hay = meta ? meta.content : '';
        const p = hay.split('/');
        for (let i = 0; i < p.length - 1; i++) {
            if (/^\d+000$/.test(p[i]) && /^\d+$/.test(p[i + 1])) return parseInt(p[i + 1], 10);
        }
        return null;
    }

    function legacyProbe(videoID) {
        if (!videoID || videoLoaded) return;
        const folder = Math.floor(videoID / 1000) * 1000;
        const cacheKey = 'wi3_date_' + videoID;
        const cached = localStorage.getItem(cacheKey);
        const jobs = [];
        console.log('[wi3-AVJB] legacy probe — id:', videoID, 'folder:', folder);

        SERVERS.forEach(base => {
            const urls = [];
            if (base.includes('bbmmic2.com')) {
                if (cached) urls.push(`${base}/data/${cached}/${videoID}/hls/index.m3u8`);
                PROBE_DATES.forEach(d => { if (d !== cached) urls.push(`${base}/data/${d}/${videoID}/hls/index.m3u8`); });
            } else if (base.includes('imgclh.com')) {
                urls.push(
                    `${base}/contents/videos/${folder}/${videoID}/index.m3u8`,
                    `${base}/contents/videos/${folder}/${videoID}/master.m3u8`,
                    `${base}/contents/videos_hls/${folder}/${videoID}/index.m3u8`,
                    `${base}/contents/hls/${folder}/${videoID}/index.m3u8`,
                    `${base}/videos/${folder}/${videoID}/index.m3u8`,
                    `${base}/hls/${folder}/${videoID}/index.m3u8`);
            } else {
                urls.push(`${base}/videos/${folder}/${videoID}/index.m3u8`);
            }
            urls.forEach(url => jobs.push(
                fetch(url, { method: 'HEAD', cache: 'no-store' })
                    .then(r => {
                        if (r.ok && !videoLoaded) {
                            const d = url.match(/\/data\/(\d{8})\//);
                            if (d) { try { localStorage.setItem(cacheKey, d[1]); } catch (e) {} }
                            console.log('[wi3-AVJB] legacy hit:', url);
                            loadStream(url);
                        }
                    }).catch(() => {})
            ));
        });
        return Promise.all(jobs);
    }

    // ============================================================
    // PAGE GATE — chi trang video (/vi/video/<id>/) moi duoc che/phau thuat.
    // Trang listing (home/album/the loai...) chi dedupe + thumbnail.
    // ============================================================
    function isVideoPage() { return /\/video\/\d+/.test(location.pathname); }

    // ============================================================
    // PAYWALL NUKER
    // ============================================================
    function nuke(root) {
        root = root || document;
        if (!isVideoPage()) return;   // khong che/kill/ inject tren listing
        try {
            const kill = (sel) => root.querySelectorAll(sel).forEach(el => el.remove());
            if (root.getElementById) {
                const l2 = root.getElementById('layer2');
                if (l2) l2.remove();
            }
            kill('.paywall-guest, .paywall-v2, .no-player, .pw-headline, .pw-guest-actions, .pw-btn-primary, .pw-btn-secondary, .pw-title, .message');

            root.querySelectorAll('*').forEach(el => {
                if (el.id === 'wi3Player' || (el.closest && el.closest('#wi3Player'))) return;
                if (el.children.length) return;
                const t = (el.textContent || '').trim();
                if (['Sign Up to Watch', 'Register Free Now', 'Already a member? Log In', 'Full video']
                        .some(s => t === s) || t.includes('Get 5 Free Watches')) {
                    let n = el;
                    for (let i = 0; i < 6 && n.parentElement; i++) {
                        const c = typeof n.className === 'string' ? n.className : '';
                        if (/paywall|no-player|message/.test(c)) { n.remove(); break; }
                        n = n.parentElement;
                    }
                }
            });
        } catch (e) {}

        // an player site goc (re-inject sau khi wipe holder) — chi giu wi3 player
        try {
            root.querySelectorAll('video').forEach(v => {
                if (v === video || (v.closest && v.closest('#wi3Player'))) return;
                v.style.setProperty('display', 'none', 'important');
                try { v.pause(); } catch (e) {}
                let n = v, hide = v;
                for (let i = 0; i < 4 && n.parentElement; i++) {
                    n = n.parentElement;
                    if (n === document.body || n.contains(playerDiv)) break;
                    const cls = (typeof n.className === 'string' ? n.className : '') + ' ' + (n.id || '');
                    if (/player|video|stream|holder|html5|media/i.test(cls)) hide = n;
                }
                if (hide !== v && !hide.contains(playerDiv)) {
                    hide.style.setProperty('display', 'none', 'important');
                }
            });
        } catch (e) {}

        if (root === document) injectPlayer();
    }

    function injectPlayer() {
        if (!isVideoPage()) return;   // gate: chi video page moi chen
        playerDiv.className = 'wi3-inline';
        const holder = document.querySelector('.player-holder');
        if (holder) {
            // KHONG wipe holder (site js se wipe lai -> dau rang -> mat ca 2).
            // An holder di, chet player ra TRUOC holder. Reposition: luc DOM chua
            // render no da body-prepend len tren header -> holder xuat h thi doi xuong dung cho.
            holder.style.setProperty('display', 'none', 'important');
            if (playerDiv.nextElementSibling !== holder) {
                holder.parentNode.insertBefore(playerDiv, holder);
                if (!videoLoaded) setTips('⌛ waiting for stream...');
            }
            return;
        }
        // holder chua render: nao co anchor thi chen truoc anchor,
        // DOM con trong thi cho lan sau — KHONG prepend len body (loi player treo tren header)
        const anchor = document.querySelector('.video-info, .video-title, .entry-content, h1, main, article');
        if (anchor && anchor.parentNode) {
            if (playerDiv.nextElementSibling !== anchor) {
                anchor.parentNode.insertBefore(playerDiv, anchor);
                if (!videoLoaded) setTips('⌛ waiting for stream...');
            }
            return;
        }
        // parse xong ma khong holder/anchor (reg-wall hang le) → fallback cuoi
        if (!playerDiv.isConnected && document.readyState !== 'loading') {
            document.body.prepend(playerDiv);
            if (!videoLoaded) setTips('⌛ waiting for stream...');
        }
    }

    // ============================================================
    // DEDUPE — an video trung lap tren grid (ban goc + "Quang ba" cung id)
    // ============================================================
    function dedupeVideos() {
        try {
            const anchors = document.querySelectorAll('a[href*="/video/"]');
            const byId = new Map();     // videoId -> card giu lai
            const done = new Set();     // card da xu ly
            anchors.forEach(a => {
                const m = ((a.getAttribute('href') || '').split(/[?#]/)[0]).match(/\/video\/(\d+)/);
                if (!m) return;
                const key = m[1];
                // leo tim "card": node gan nhat chua dung 1 video rieng
                let n = a, card = null;
                for (let i = 0; i < 5 && n.parentElement && n.parentElement !== document.body; i++) {
                    const p = n.parentElement;
                    const ids = new Set();
                    p.querySelectorAll('a[href*="/video/"]').forEach(x => {
                        const mm = ((x.getAttribute('href') || '').split(/[?#]/)[0]).match(/\/video\/(\d+)/);
                        if (mm) ids.add(mm[1]);
                    });
                    if (ids.size > 1) break;
                    n = p;
                    if (ids.size === 1) card = p;
                }
                if (!card || done.has(card)) return;
                done.add(card);
                if (byId.has(key)) {
                    const keep = byId.get(key);
                    if (keep === card || keep.contains(card) || card.contains(keep)) return;
                    card.style.setProperty('display', 'none', 'important');
                } else {
                    byId.set(key, card);
                }
            });
        } catch (e) {}
    }

    function setupNuker() {
        if (window.__wi3Nuker) return;
        window.__wi3Nuker = true;
        if (!document.body) return;
        new MutationObserver(() => nuke()).observe(document.body, { childList: true, subtree: true });
        setInterval(() => nuke(), 500);
        document.addEventListener('click', (e) => {
            const t = e.target;
            if (t.closest && t.closest('.paywall-v2, .paywall-guest, .no-player, .pw-btn-primary, .pw-btn-secondary, .pw-guest-actions')) {
                e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            }
        }, true);
    }

    // ============================================================
    // THUMBNAILS
    // ============================================================
    function fixThumbs() {
        document.querySelectorAll('a[href^="/video/"] img').forEach(img => {
            if (img.src && !/placeholder|blank|lazy/.test(img.src)) return;
            const link = img.closest('a[href^="/video/"]');
            const id = link && link.href.split('/video/')[1].split('/')[0];
            if (!id) return;
            const folder = Math.floor(parseInt(id, 10) / 1000) * 1000;
            img.src = `https://bmc2.imgclh.com/contents/videos_screenshots/${folder}/${id}/preview.jpg`;
        });
        document.querySelectorAll('img').forEach(img => {
            img.style.opacity = '1';
            img.style.visibility = 'visible';
            img.loading = 'eager';
            img.classList.remove('lazy', 'lazyload', 'b-lazy');
        });
    }

    // ============================================================
    // MAIN WATERFALL — captured -> api -> legacy, retries until life
    // ============================================================
    async function attempt() {
        if (videoLoaded || isChecking) return;
        // chi trang video co id — reg-wall cung van la /vi/video/<id>/ (holder khong can thiet)
        if (!isVideoPage()) return;

        isChecking = true;
        setupNuker();
        injectPlayer();
        fixThumbs();

        try {
            if (capturedUrl) {
                setTips('⌛ using captured stream...');
                loadStream(capturedUrl);
                if (videoLoaded) { apiFails = 0; return; }
            }
            // v1.2.2 — mirror unsigned HLS trước, không cần login/CSRF
            if (!videoLoaded) {
                setTips('⌛ probing mirrors...');
                if (await mirrorProbe(idFromPath())) { apiFails = 0; return; }
            }
            if (!videoLoaded && await apiBypass()) { apiFails = 0; return; }
            if (!videoLoaded) {
                apiFails++;
                setTips('⚠ api failed ×' + apiFails + ' — chờ server cấp token rồi thử lại...', '#ff4757');
                // legacy: rare — CDNs 404 on new videos and send no CORS headers
                if (apiFails >= 2 && loopN % 20 === 0) await legacyProbe(idFromPath());
            }
        } finally {
            isChecking = false;
            loopN++;
            // v1.3.0 — fail nhiều lần = server cần thời gian ("chờ 1 lúc"):
            // chuyển sang loop chậm 5s thay vì spam mỗi 1.5s
            const want = (!videoLoaded && apiFails >= 3) ? 5000 : 1500;
            if (want !== loopMs) setLoopMs(want);
        }
    }

    function setLoopMs(ms) {
        loopMs = ms;
        if (loopTimer) clearInterval(loopTimer);
        loopTimer = setInterval(attempt, ms);
        console.log('[wi3-AVJB] loop interval ->', ms + 'ms');
    }

    function kick() {
        if (!loopTimer) loopTimer = setInterval(attempt, loopMs);
        attempt();
    }
    function stopLoop() {
        if (loopTimer) { clearInterval(loopTimer); loopTimer = null; }
    }

    // ============================================================
    // STYLE
    // ============================================================
    GM_addStyle(`
        img { opacity: 1 !important; visibility: visible !important; }
        .thumb, .video-thumb { background: none !important; }
        .paywall-guest, .paywall-v2, .no-player, .pw-headline, .pw-guest-actions,
        .pw-btn-primary, .pw-btn-secondary, .pw-title, .message {
            display: none !important; visibility: hidden !important; opacity: 0 !important;
            pointer-events: none !important; height: 0 !important; width: 0 !important;
            position: absolute !important; left: -99999px !important; top: -99999px !important;
        }
        #wi3Player.wi3-inline {
            position: relative !important; width: 100% !important; max-width: 100vw !important;
            background: #000; z-index: 999999; display: block !important;
            visibility: visible !important; opacity: 1 !important;
        }
        #wi3Player .wi3-content { display: flex; flex-direction: column; width: 100%; }
        #wi3Player #wi3Video { width: 100%; max-height: 56.25vw; aspect-ratio: 16/9; background: #000; display: block; }
        #wi3Player .wi3-footer { display: flex; justify-content: space-between; align-items: center;
            padding: 8px 12px; background: #1a1a1a; font-size: 13px; font-family: Arial, sans-serif; }
        #wi3Tips { color: #ccc; }
        #wi3Dl { color: #ff4757; text-decoration: none; font-weight: bold; font-size: 13px; white-space: nowrap; }
        #wi3Dl:hover { color: #ff6b81; text-decoration: underline; }
    `);

    // ============================================================
    // BOOT
    // ============================================================
    function boot() {
        if (!document.body) { setTimeout(boot, 10); return; }
        console.log('[wi3-AVJB] v1.3.4 active on', location.pathname);
        try {
            GM_registerMenuCommand('Nhập / sửa tài khoản avjb', function () {
                try { sessionStorage.removeItem('wi3_asked'); } catch (e) {}
                accState.tried = false; accState.ok = false;
                apiFails = 0;
                const c = askCreds(true);
                if (c) { alert('Đã lưu tài khoản: ' + c.u); attempt(); }
            });
            GM_registerMenuCommand('Xóa tài khoản đã lưu', function () {
                try { GM_setValue('avjb_user', ''); GM_setValue('avjb_pass', ''); } catch (e) {}
                alert('Đã xóa tài khoản đã lưu.');
            });
            GM_registerMenuCommand('Lưu HTML trang (debug) — drop vào D:\\ccac', function () {
                fetch(location.href, { cache: 'no-store', credentials: 'same-origin' })
                    .then(r => r.text())
                    .then(t => {
                        const blob = new Blob([t], { type: 'text/html' });
                        const a = document.createElement('a');
                        a.href = URL.createObjectURL(blob);
                        a.download = 'avjb_page.html';
                        document.body.appendChild(a);
                        a.click();
                        a.remove();
                        setTimeout(() => URL.revokeObjectURL(a.href), 5000);
                    })
                    .catch(e => alert('Lỗi tải HTML: ' + e.message));
            });
        } catch (e) {}
        setupNuker();
        fixThumbs();
        dedupeVideos();
        setInterval(dedupeVideos, 1500);
        kick();
        new MutationObserver(() => setTimeout(fixThumbs, 100))
            .observe(document.body, { childList: true, subtree: true });
        let st;
        window.addEventListener('scroll', () => {
            clearTimeout(st); st = setTimeout(fixThumbs, 150);
        }, { passive: true });
    }
    boot();

    } else if (_wi3Site === 'javhd') {
        // ==================== JAVHD half ====================


    // ============================================================
    // CDN MAP
    // v1.0.0: c1 + c4 (cdnjhd / cdnjav) serve the FULL mp4 with NO
    // signature prefix and NO Referer requirement. c3/c2 require the
    // `<token>,<epoch>` path prefix the site mints for members, c5
    // answers 200 on everything with Content-Length: 0 (junk), the
    // rest 404. Accept-Ranges: bytes + Access-Control-Allow-Origin: *
    // so the plain <video> seeks natively.
    // ============================================================
    const HOSTS = [
        'https://c1.cdnjhd.com',
        'https://c4.cdnjhd.com',
        'https://c1.cdnjav.com',
        'https://c4.cdnjav.com',
    ];

    // suffix -> label, ordered best-first
    const QUALITIES = [
        { q: '_sh',  label: '1080p', res: 1080 },
        { q: '_hq',  label: '720p',  res: 720 },
        { q: '_med', label: '480p',  res: 480 },
        { q: '_low', label: '240p',  res: 240 },
    ];

    const GM_XHR = (typeof GM_xmlhttpRequest === 'function') ? GM_xmlhttpRequest : null;

    function gmHead(url, timeout) {
        return new Promise((resolve) => {
            if (!GM_XHR) { resolve(null); return; }
            try {
                GM_XHR({
                    method: 'HEAD',
                    url: url,
                    timeout: timeout || 12000,
                    headers: { 'Referer': location.origin + '/' },
                    onload: (r) => resolve(r),
                    onerror: () => resolve(null),
                    ontimeout: () => resolve(null),
                    onabort: () => resolve(null),
                });
            } catch (e) { resolve(null); }
        });
    }

    function gmGet(url, responseType, timeout) {
        return new Promise((resolve, reject) => {
            if (!GM_XHR) { reject(new Error('no GM_xmlhttpRequest')); return; }
            try {
                GM_XHR({
                    method: 'GET',
                    url: url,
                    responseType: responseType || 'text',
                    timeout: timeout || 15000,
                    headers: { 'Referer': location.origin + '/' },
                    onload: (r) => resolve(r),
                    onerror: () => reject(new Error('gm http error: ' + url)),
                    ontimeout: () => reject(new Error('gm timeout: ' + url)),
                    onabort: () => reject(new Error('gm abort: ' + url)),
                });
            } catch (e) { reject(e); }
        });
    }

    // ============================================================
    // STATE
    // ============================================================
    let videoLoaded = false;
    let isChecking = false;
    let loopTimer = null;
    let loopMs = 2000;
    let failN = 0;
    let loopN = 0;
    let currentRes = 0;
    let lastVid = null;   // SPA navigation: detect "we're on a different video now"

    // SLOT LOCK (v1.4.3) — this JAVHD block ALSO ships as the standalone
    // javhd-bypass.user.js script. Both @match javhd.com with separate
    // sandboxes, so window.__wi3JavhdNuker does NOT stop the other copy:
    // both inject #wi3Player -> two players on screen, and each script's
    // MutationObserver re-inserts whatever the other removed (thrash).
    // Claim the slot through the PAGE window, which every instance shares;
    // the loser stands down instead of fighting.
    const PAGE_WIN = (typeof unsafeWindow !== 'undefined' && unsafeWindow) ? unsafeWindow : window;
    const MY_SLOT = {};

    function claimSlot() {
        try {
            const owner = PAGE_WIN.__wi3JavhdSlot;
            if (owner && owner.token !== MY_SLOT && owner.el && owner.el.isConnected) {
                // lost the slot — if we already injected (boot race), retract so
                // exactly one player survives; our observer will retry and stand
                // down here on every pass.
                if (playerDiv.isConnected) playerDiv.remove();
                return false;
            }
            PAGE_WIN.__wi3JavhdSlot = { token: MY_SLOT, el: playerDiv };
            return true;
        } catch (e) { return true; }   // no page window access (safe mode) -> old behavior
    }

    // ============================================================
    // PAGE HELPERS
    // ============================================================
    // Two live URL families: /en/id/<n>/<slug> and /en/studio/room/<x>/video/<slug>.
    // The studio slug carries no id — videoIdFromDom() reads content-id from the DOM.
    function isVideoPage() {
        return /\/id\/\d+/.test(location.pathname)
            || /\/studio\/room\/[^/]+\/video\//.test(location.pathname);
    }

    function videoIdFromDom() {
        const pc = document.querySelector('player-component');
        if (pc) {
            const cid = pc.getAttribute('content-id');
            if (cid && /^\d+$/.test(cid)) return cid;
            const cp = pc.getAttribute('content-path') || '';
            const m = cp.match(/(?:videoId=|\/player\/)(\d+)/);
            if (m) return m[1];
        }
        const m = location.pathname.match(/\/id\/(\d+)/);
        if (m) return m[1];
        // v1.2.0 — hydrate removes <player-component> and studio/room pages
        // carry no /id/<n> in the path — pull the id from the inline
        // globicaParams (pageid: '<n>') / the join link (cms_id=<n>).
        try {
            const H = document.documentElement.innerHTML;
            const g = H.match(/pageid:\s*'(\d+)'/) || H.match(/cms_id=(\d+)/);
            if (g) return g[1];
        } catch (e) {}
        return null;
    }

    function contentPathFromDom() {
        const pc = document.querySelector('player-component');
        return pc ? (pc.getAttribute('content-path') || '') : '';
    }

    // poster may live in the component's `:empty-player` binding (guest
    // pages) or only in the player JSON (free/premiumFree pages, which
    // ship no empty-player at all).
    // v1.2.0 — after hydration the site replaces <player-component> with a
    // plain div.player whose poster moved into the CSS var --playerPoster
    // (and the <img class="jplayer__poster-img">). Probe all three.
    function posterFromDom() {
        const pc = document.querySelector('player-component');
        if (pc) {
            const blob = pc.outerHTML || '';
            const m = blob.match(/poster:\s*['"]([^'"]+)['"]/);
            if (m) return m[1].replace(/\\\//g, '/');
            const m2 = blob.match(/poster=["']([^"']+)["']/);
            if (m2) return m2[1];
        }
        const cont = document.querySelector('.jplayer__player-container, .player__container, .player');
        if (cont) {
            const st = cont.getAttribute('style') || '';
            const m3 = st.match(/--playerPoster:\s*url\(([^)]+)\)/);
            if (m3) return m3[1].trim().replace(/^['"]|['"]$/g, '');
        }
        const img = document.querySelector('.jplayer__poster-img, .player__poster img, .jplayer__poster img');
        if (img && img.src) return img.src;
        return '';
    }

    function posterFromApi(vid) {
        const endpoints = [
            '/en/player_api?videoId=' + vid + '&is_trailer=1',
            '/en/player/' + vid + '?is_trailer=1',
        ];
        // content-path already tells us which family the page uses — try it first
        const cp = contentPathFromDom();
        if (cp.indexOf('player_api') >= 0) endpoints.reverse();

        return (async () => {
            for (let i = 0; i < endpoints.length; i++) {
                try {
                    const r = await gmGet(location.origin + endpoints[i], 'text', 10000);
                    const t = String(r && r.responseText || r && r.response || '');
                    const m = t.match(/"poster"\s*:\s*"([^"]+)"/);
                    if (m) return m[1].replace(/\\\//g, '/');
                } catch (e) {}
            }
            return '';
        })();
    }

    // poster forms:
    //   .../content-01/thumbs/<slug>-p/images/940x530/146s.jpg   -> strip -p
    //   .../content-01/thumbs/<slug>/thumbs/thumb1/.../t1.jpg     -> keep as-is
    // We probe BOTH the stripped and the raw segment, so an honest slug
    // that happens to end in "-p" still resolves.
    function slugCandidates(poster) {
        if (!poster) return [];
        const m = poster.match(/^(https?:\/\/[^/]+)\/([^/]+)\/thumbs\/([^/?#]+)/);
        if (!m) return [];
        const origin = m[1];
        const root = m[2];                       // content-01 (etc.)
        let seg = decodeURIComponent(m[3]);
        const out = [];
        if (seg.length > 2 && seg.slice(-2) === '-p') {
            out.push({ origin: origin, root: root, slug: seg.slice(0, -2) });
        }
        out.push({ origin: origin, root: root, slug: seg });
        return out;
    }

    // GM exposes response headers as a single blob (`responseHeaders` in
    // Tampermonkey/Violentmonkey; some builds call it `headers`).
    function hdrLen(r) {
        const blob = String((r && (r.responseHeaders || r.headers)) || '');
        const m = blob.match(/content-length:\s*(\d+)/i);
        return m ? parseInt(m[1], 10) : 0;
    }

    // ============================================================
    // PROBE — HEAD the first quality; if it answers with a real body,
    // HEAD the remaining qualities on that same host+slug.
    // ============================================================
    async function probeHostSlug(hint) {
        const base = `${hint.origin}/${hint.root}/contents/${hint.slug}/videos/${hint.slug}`;
        const first = await gmHead(base + '_hq.mp4');
        if (!first || first.status < 200 || first.status >= 300) return null;
        const cl = hdrLen(first);
        if (!cl) return null;   // c5-style empty 200

        const found = await Promise.all(QUALITIES.map(async (Q) => {
            if (Q.q === '_hq') return { label: Q.label, res: Q.res, size: cl, url: base + Q.q + '.mp4' };
            const r = await gmHead(base + Q.q + '.mp4');
            if (!r || r.status < 200 || r.status >= 300) return null;
            const len = hdrLen(r);
            if (!len) return null;
            return { label: Q.label, res: Q.res, size: len, url: base + Q.q + '.mp4' };
        }));
        const list = found.filter(Boolean);
        if (!list.length) return null;
        console.log('[wi3-JAVHD] unsigned CDN hit:', hint.origin, hint.slug, list.length + ' qualities');
        return list;
    }

    async function unsignedSources(poster) {
        const cands = slugCandidates(poster);
        for (let i = 0; i < cands.length; i++) {
            for (let h = 0; h < HOSTS.length; h++) {
                const hit = await probeHostSlug({ origin: HOSTS[h], root: cands[i].root, slug: cands[i].slug });
                if (hit) return hit;
                // same bucket, other host — keep origin fixed to the CDN the
                // poster itself came from only as a last try
            }
        }
        // poster origin last (it's the host the site already trusts)
        for (let i = 0; i < cands.length; i++) {
            const hit = await probeHostSlug(cands[i]);
            if (hit) return hit;
        }
        return null;
    }

    // ============================================================
    // FALLBACK 1 — /en/player_api?videoId=X (no trailer flag)
    // Full signed sources, but ONLY for free / premiumFree rows;
    // premium rows answer `[]`.
    // ============================================================
    async function playerApiSources(vid) {
        try {
            const r = await gmGet(location.origin + '/en/player_api?videoId=' + vid, 'text', 10000);
            const raw = String(r && r.responseText || r && r.response || '').replace(/\\\//g, '/');
            if (raw.indexOf('"src"') < 0) return null;
            const srcs = [];
            const re = /"label"\s*:\s*"([^"]+)"[^}]*?"res"\s*:\s*"?(\d+)"?[^}]*?"src"\s*:\s*"([^"]+)"/g;
            let m;
            while ((m = re.exec(raw)) !== null) {
                const u = m[3];
                if (/black_cap\.mp4$/i.test(u)) continue;   // guest placeholder
                srcs.push({ label: m[1], res: parseInt(m[2], 10), url: u });
            }
            if (!srcs.length) return null;
            srcs.sort((a, b) => b.res - a.res);
            console.log('[wi3-JAVHD] player_api returned', srcs.length, 'sources');
            return srcs;
        } catch (e) { return null; }
    }

    // ============================================================
    // FALLBACK 2 — 1-minute trailer (guest JSON from /en/player/<id>)
    // ============================================================
    async function trailerSources(vid) {
        try {
            const r = await gmGet(location.origin + '/en/player/' + vid + '?is_trailer=1', 'text', 10000);
            const raw = String(r && r.responseText || r && r.response || '').replace(/\\\//g, '/');
            if (raw.indexOf('black_cap.mp4') >= 0) return null;
            const srcs = [];
            const re = /"label"\s*:\s*"([^"]+)"[^}]*?"res"\s*:\s*"?(\d+)"?[^}]*?"src"\s*:\s*"([^"]+)"/g;
            let m;
            while ((m = re.exec(raw)) !== null) {
                const u = m[3];
                if (/black_cap\.mp4$/i.test(u)) continue;
                srcs.push({ label: m[1], res: parseInt(m[2], 10), url: u });
            }
            if (!srcs.length) return null;
            srcs.sort((a, b) => b.res - a.res);
            console.log('[wi3-JAVHD] trailer-only:', srcs[0].url);
            return srcs;
        } catch (e) { return null; }
    }

    // ============================================================
    // PLAYER DOM
    // ============================================================
    const playerDiv = document.createElement('div');
    playerDiv.id = 'wi3Player';
    playerDiv.innerHTML = `
        <div class="wi3-content">
            <video id="wi3Video" controls preload="metadata" playsinline webkit-playsinline></video>
            <div class="wi3-footer">
                <span id="wi3Tips">⌛ initializing...</span>
                <span id="wi3Qual"></span>
                <a id="wi3Dl" href="#" target="_blank" rel="noopener">⏬ download</a>
            </div>
        </div>`;
    const video = playerDiv.querySelector('#wi3Video');
    const tipsEl = playerDiv.querySelector('#wi3Tips');
    const qualEl = playerDiv.querySelector('#wi3Qual');
    const dlEl = playerDiv.querySelector('#wi3Dl');

    let sourceSet = [];        // [{label,res,url,size}]
    let currentIndex = 0;

    function setTips(text, color) {
        tipsEl.innerText = text;
        if (color) tipsEl.style.color = color;
    }

    function fmtSize(n) {
        if (!n) return '';
        if (n > 1e9) return (n / 1e9).toFixed(2) + ' GB';
        if (n > 1e6) return (n / 1e6).toFixed(0) + ' MB';
        return Math.round(n / 1e3) + ' KB';
    }

    function renderQual() {
        if (!sourceSet.length) { qualEl.innerHTML = ''; return; }
        qualEl.innerHTML = sourceSet.map((s, i) =>
            `<button class="wi3-q${i === currentIndex ? ' on' : ''}" data-i="${i}">${s.label}</button>`
        ).join('');
    }

    qualEl.addEventListener('click', (e) => {
        const b = e.target.closest('button[data-i]');
        if (!b) return;
        const i = parseInt(b.getAttribute('data-i'), 10);
        if (i === currentIndex) return;
        const t = video.currentTime;
        const wasPlaying = !video.paused;
        currentIndex = i;
        applySource(true, t, wasPlaying);
    });

    function applySource(keepTime, at, autoplay) {
        const s = sourceSet[currentIndex];
        if (!s) return;
        currentRes = s.res;
        video.src = s.url;
        dlEl.href = s.url;
        dlEl.download = '';
        renderQual();
        const onMeta = () => {
            if (keepTime && at && isFinite(at)) {
                try { video.currentTime = Math.min(at, (video.duration || at) - 0.5); } catch (e) {}
            }
            if (autoplay) video.play().catch(() => {});
            setTips('✅ unlocked — ' + s.label + (s.size ? ' · ' + fmtSize(s.size) : ''), '#2ed573');
        };
        video.addEventListener('loadedmetadata', onMeta, { once: true });
        video.addEventListener('error', () => {
            if (currentIndex < sourceSet.length - 1) {
                currentIndex++;
                setTips('⚠ ' + s.label + ' failed — falling back...', '#ff4757');
                applySource(false, 0, false);
            } else {
                setTips('⚠ all qualities failed', '#ff4757');
            }
        }, { once: true });
    }

    function loadSources(list) {
        if (!list || !list.length || videoLoaded) return false;
        videoLoaded = true;
        sourceSet = list;
        currentIndex = 0;
        applySource(false, 0, false);
        stopLoop();
        console.log('[wi3-JAVHD] player armed with', list.length, 'sources');
        return true;
    }

    // double-click left/right = -10s / +10s
    let lastClick = 0;
    video.addEventListener('click', (e) => {
        const now = Date.now();
        const dt = now - lastClick;
        if (dt < 300 && dt > 30) {
            const r = video.getBoundingClientRect();
            const x = (e.clientX - r.left) / r.width;
            const y = (e.clientY - r.top) / r.height;
            if (y > 0.9) { lastClick = now; return; }
            e.preventDefault(); e.stopPropagation();
            if (x < 0.5) video.currentTime = Math.max(0, video.currentTime - 10);
            else video.currentTime = Math.min(video.duration || 0, video.currentTime + 10);
        }
        lastClick = now;
    });

    // ============================================================
    // PAYWALL NUKER — guest pages blur the poster behind a
    // .player-join-cap ("Full video available only for members").
    // ============================================================
    // v1.4.5 — the join funnel: category cards on the home/listing pages link
    // to /en/join?category_dir=... (the site's paywall). Rewrite them to the
    // real listing URL so no click ever lands on a paywall.
    function fixJoinLinks(root) {
        // v1.4.6 — ANY internal join link (header Join, /en/join?cms_id= video
        // CTAs, /en/join?category_dir= category cards, locale /vi/join ...) is a
        // paywall hop: category ones go to the real listing, the rest bounce home.
        try {
            root.querySelectorAll('a[href*="/join"]').forEach(a => {
                const h = a.getAttribute('href') || '';
                if (!/^(\/|https?:\/\/[^\/]*javhd\.com\/)/.test(h) || h.indexOf('/join') < 0) return;
                const cm = h.match(/category_dir=([\w\-]+)/);
                a.setAttribute('href', cm
                    ? 'https://javhd.com/en/japanese-porn-videos/category/' + cm[1]
                    : 'https://javhd.com/en/');
            });
        } catch (e) {}
    }

    // v1.4.5/1.4.6 — already ON a paywall? Swap it for real content before
    // anything paints. Covers: javhd.com/en/join (rare — usually server-302s
    // first), enter.javhd.com/signup?...category_dir= and the /track/ hop.
    function joinRedirect() {
        try {
            if (!/(^|\.)javhd\.com$/.test(location.hostname)) return;
            const cm = location.search.match(/category_dir=([\w\-]+)/);
            if (cm) {
                location.replace('https://javhd.com/en/japanese-porn-videos/category/' + cm[1]);
            } else if (/\/join\/?$/.test(location.pathname) || /\/(signup|track)\//.test(location.pathname)) {
                location.replace('https://javhd.com/en/');
            }
        } catch (e) {}
    }

    function nuke(root) {
        root = root || document;
        // v1.4.5 — the viewing-limit overlay (.player-caption-limit: "YOU HAVE
        // REACHED YOUR VIEWING LIMITS" / "GET FULL VIDEO") and the join CTAs
        // live on ANY page (home, category, video): run this pass site-wide.
        // Only the site-player hiding below stays gated to video pages.
        try {
            const kill = (sel) => root.querySelectorAll(sel).forEach(el => el.remove());
            // v1.2.0 — site renamed its player classes (player__* -> jplayer__*)
            // so the old kill-list matched nothing; cover both generations.
            // v1.4.5 — + .player-caption-limit and .player-join-cap (paywalls).
            kill('.player-caption-limit, .player-join-cap, .player-join-cap__btn, .player__poster, .player__overlay-play,' +
                ' .jplayer__poster, .jplayer__overlay-play, .jplayer__overlay-button,' +
                ' .jplayer__big-play, .jplayer__ad, .player__overlay, .jplayer__notice');

            root.querySelectorAll('*').forEach(el => {
                if (el.id === 'wi3Player' || (el.closest && el.closest('#wi3Player'))) return;
                if (el.children.length) return;
                const t = (el.textContent || '').trim();
                if (t === 'Join JAVHD' || t === 'Full video available only for members'
                        || t === 'Sign up to watch' || t === 'Already a member? Log in'
                        || t.indexOf('available only for members') >= 0
                        || t.indexOf('YOU HAVE REACHED YOUR VIEWING LIMITS') >= 0) {
                    let n = el;
                    for (let i = 0; i < 6 && n.parentElement; i++) {
                        const c = typeof n.className === 'string' ? n.className : '';
                        if (/join-cap|caption-limit|poster|blurred|overlay|limit/i.test(c)) { n.remove(); break; }
                        n = n.parentElement;
                    }
                }
            });
        } catch (e) {}

        fixJoinLinks(root);

        if (!isVideoPage()) return;

        // hide the site's own player so ours is the only one on screen
        try {
            root.querySelectorAll('video').forEach(v => {
                if (v === video || (v.closest && v.closest('#wi3Player'))) return;
                // v1.4.3 — ONLY the site's main player box (and its ancestors)
                // may be hidden. The old walk matched ANY ancestor whose class
                // contained "video"/"container" — recommend cards live in
                // .related-videos / .video-grid style wrappers, so the whole
                // "Recommended for you" section got whitewashed along the way.
                const box = v.closest('.player, player-component, .content__general--player,' +
                    ' .jplayer__player-container, .player__container, .player__video');
                if (!box) return;   // card preview / ad / thumbnail video — leave it alone
                v.style.setProperty('display', 'none', 'important');
                try { v.pause(); } catch (e) {}
                if (box !== v && !box.contains(playerDiv)) {
                    box.style.setProperty('display', 'none', 'important');
                }
                let n = box, hide = box;
                for (let i = 0; i < 5 && n.parentElement; i++) {
                    n = n.parentElement;
                    if (n === document.body || n.contains(playerDiv)) break;
                    const cls = (typeof n.className === 'string' ? n.className : '') + ' ' + (n.id || '');
                    // player-specific only, and never a recommend/related/suggest wrapper
                    if (/player|stream|jplayer/i.test(cls) && !/recommend|related|suggest/i.test(cls)) hide = n;
                }
                // v1.2.0 — after hydrate the site's box sits INSIDE
                // .content__general--player which ALSO holds our injected player
                // (a sibling of .player). The old `hide.contains(playerDiv)`
                // check failed, so hide() whitewashed OUR player too — the
                // symptom "site player overlays our clone". Skip any ancestor
                // that contains wi3Player; kill only the real site box.
                if (hide !== v && !hide.contains(playerDiv)) {
                    hide.style.setProperty('display', 'none', 'important');
                }
            });
            // pre-hydrate the component still exists (no <video> inside yet)
            const pc = root.querySelector && root.querySelector('player-component');
            if (pc && !pc.contains(playerDiv)) {
                pc.style.setProperty('display', 'none', 'important');
            }
            // post-hydrate: plain div.player box has no <video> until click —
            // hide the whole box directly so its poster/overlay never sits
            // over our player.
            const sp = root.querySelector && root.querySelector('.player');
            if (sp && !sp.contains(playerDiv)) {
                sp.style.setProperty('display', 'none', 'important');
            }
        } catch (e) {}

        if (root === document) injectPlayer();
    }

    function injectPlayer() {
        if (!isVideoPage()) return;
        if (!claimSlot()) return;   // other script instance already owns the slot
        // v1.4.4 — sweep foreign copies: a stale TM update bundle can inject an
        // old slot-unaware build alongside this one (2 players). We own the
        // slot, so remove any #wi3Player that isn't ours; our nuke loop
        // re-runs this pass so a re-inserted foreign copy gets swept again.
        for (const el of document.querySelectorAll('#wi3Player')) {
            if (el !== playerDiv) el.remove();
        }
        playerDiv.className = 'wi3-inline';
        if (playerDiv.isConnected) {
            // first inject can fire before Vue mounts .player — once the real
            // box exists, walk our player into it (only before playback starts,
            // so a running video is never reparented).
            const box = document.querySelector('.content__general--player');
            if (!videoLoaded && box && playerDiv.parentElement !== box) {
                box.insertBefore(playerDiv, box.firstElementChild);
            }
            return;
        }
        // v1.2.0 — the player's real box lives inside .content__general--player;
        // anchoring on bare h1/main pushed our player below the whole
        // description block. Prefer the box, then the old anchors.
        const anchor = document.querySelector('player-component, .content__general--player, .player,' +
            ' .video-info, .video-title, h1, main, article');
        if (anchor && anchor.parentNode) {
            if (anchor.classList && anchor.classList.contains('content__general--player')) {
                anchor.insertBefore(playerDiv, anchor.firstElementChild);
            } else {
                anchor.parentNode.insertBefore(playerDiv, anchor);
            }
            if (!videoLoaded) setTips('⌛ waiting for sources...');
            return;
        }
        if (document.body && document.readyState !== 'loading') {
            document.body.prepend(playerDiv);
            if (!videoLoaded) setTips('⌛ waiting for sources...');
        }
    }

    function setupNuker() {
        if (window.__wi3JavhdNuker) return;
        window.__wi3JavhdNuker = true;
        if (!document.body) return;
        new MutationObserver(() => nuke()).observe(document.body, { childList: true, subtree: true });
        setInterval(() => nuke(), 600);
        document.addEventListener('click', (e) => {
            const t = e.target;
            // v1.2.0 — cover the new jplayer play overlay (the one the guest
            // clicks that would otherwise mount the site's black_cap player).
            if (t.closest && t.closest('.player-join-cap, .player-join-cap__btn,' +
                    ' .jplayer__overlay-play, .jplayer__overlay-button, .jplayer__big-play')) {
                e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            }
        }, true);
    }

    // ============================================================
    // MAIN WATERFALL
    // ============================================================
    async function attempt() {
        if (videoLoaded || isChecking) return;
        if (!isVideoPage()) return;

        const vid = videoIdFromDom();
        if (!vid) return;

        // SPA route change to a different video — drop the old state and start over
        if (lastVid !== null && lastVid !== vid) {
            videoLoaded = false; failN = 0; sourceSet = []; currentRes = 0;
            try { video.removeAttribute('src'); video.load(); } catch (e) {}
        }
        lastVid = vid;

        isChecking = true;
        setupNuker();
        injectPlayer();

        try {
            // 1. poster -> slug -> unsigned CDN (works for premium AND free)
            setTips('⌛ probing CDN...');
            let poster = posterFromDom();
            if (!poster) poster = await posterFromApi(vid);
            if (poster) {
                const unsigned = await unsignedSources(poster);
                if (unsigned && loadSources(unsigned)) { failN = 0; return; }
            }

            // 2. player_api (signed URLs, free/premiumFree rows only)
            setTips('⌛ trying player_api...');
            const api = await playerApiSources(vid);
            if (api && loadSources(api)) { failN = 0; return; }

            // 3. 1-minute trailer so the page is never dead
            setTips('⌛ trying trailer...');
            const tr = await trailerSources(vid);
            if (tr && loadSources(tr)) {
                setTips('⚠ trailer only — full source unavailable', '#ffa502');
                failN = 0;
                return;
            }

            failN++;
            setTips('⚠ no source yet ×' + failN + ' — retrying...', '#ff4757');
        } finally {
            isChecking = false;
            loopN++;
            const want = (!videoLoaded && failN >= 3) ? 6000 : 2000;
            if (want !== loopMs) setLoopMs(want);
        }
    }

    function setLoopMs(ms) {
        loopMs = ms;
        if (loopTimer) clearInterval(loopTimer);
        loopTimer = setInterval(attempt, ms);
        console.log('[wi3-JAVHD] loop interval ->', ms + 'ms');
    }

    function kick() {
        if (!loopTimer) loopTimer = setInterval(attempt, loopMs);
        attempt();
    }
    function stopLoop() {
        if (loopTimer) { clearInterval(loopTimer); loopTimer = null; }
    }

    // ============================================================
    // STYLE
    // ============================================================
    GM_addStyle(`
        .player-join-cap, .player-join-cap__btn, .player__poster, .player__overlay-play,
        .jplayer__poster, .jplayer__overlay-play, .jplayer__big-play, .jplayer__ad {
            display: none !important; visibility: hidden !important; opacity: 0 !important;
            pointer-events: none !important; height: 0 !important; width: 0 !important;
            position: absolute !important; left: -99999px !important; top: -99999px !important;
        }
        #wi3Player.wi3-inline {
            position: relative !important; width: 100% !important; max-width: 100vw !important;
            background: #000; z-index: 999999; display: block !important;
            visibility: visible !important; opacity: 1 !important;
        }
        #wi3Player .wi3-content { display: flex; flex-direction: column; width: 100%; }
        #wi3Player #wi3Video {
            width: 100%; max-height: 70vh; aspect-ratio: 16/9; background: #000; display: block;
        }
        #wi3Player .wi3-footer {
            display: flex; justify-content: space-between; align-items: center; gap: 12px;
            padding: 8px 12px; background: #1a1a1a; font-size: 13px; font-family: Arial, sans-serif;
        }
        #wi3Tips { color: #ccc; }
        #wi3Qual { display: flex; gap: 6px; }
        #wi3Qual .wi3-q {
            background: #2a2a2a; color: #ccc; border: 1px solid #444; border-radius: 3px;
            padding: 3px 9px; font-size: 12px; cursor: pointer;
        }
        #wi3Qual .wi3-q.on { background: #ff4757; color: #fff; border-color: #ff4757; }
        #wi3Qual .wi3-q:hover { border-color: #888; }
        #wi3Dl {
            color: #ff4757; text-decoration: none; font-weight: bold; font-size: 13px; white-space: nowrap;
        }
        #wi3Dl:hover { color: #ff6b81; text-decoration: underline; }
    `);

    // ============================================================
    // BOOT
    // ============================================================
    function boot() {
        // v1.4.5 — never show the join funnel (the site's paywall): a category
        // request lands on the real listing, a bare join bounces home.
        joinRedirect();
        if (!document.body) { setTimeout(boot, 10); return; }
        console.log('[wi3-JAVHD] v1.2.0 active on', location.pathname);
        try {
            GM_registerMenuCommand('JAVHD — force reload sources', function () {
                videoLoaded = false; failN = 0; sourceSet = [];
                try { video.removeAttribute('src'); video.load(); } catch (e) {}
                setLoopMs(2000);
                kick();
            });
            GM_registerMenuCommand('JAVHD — save page HTML (debug)', function () {
                fetch(location.href, { cache: 'no-store', credentials: 'same-origin' })
                    .then(r => r.text())
                    .then(t => {
                        const blob = new Blob([t], { type: 'text/html' });
                        const a = document.createElement('a');
                        a.href = URL.createObjectURL(blob);
                        a.download = 'javhd_page.html';
                        document.body.appendChild(a);
                        a.click();
                        a.remove();
                        setTimeout(() => URL.revokeObjectURL(a.href), 5000);
                    })
                    .catch(e => alert('HTML fetch failed: ' + e.message));
            });
        } catch (e) {}
        setupNuker();
        kick();
        // Vue re-renders can evict our player on SPA navigation — always watch
        new MutationObserver(() => { if (!playerDiv.isConnected) injectPlayer(); })
            .observe(document.body, { childList: true, subtree: true });
        // SPA route change (pushState) — kick immediately instead of waiting
        ['pushState', 'replaceState'].forEach(function (m) {
            const orig = history[m];
            history[m] = function () {
                const r = orig.apply(this, arguments);
                try { window.dispatchEvent(new Event('wi3-route')); } catch (e) {}
                return r;
            };
        });
        window.addEventListener('popstate', function () { window.dispatchEvent(new Event('wi3-route')); });
        window.addEventListener('wi3-route', function () { joinRedirect(); attempt(); });
    }
    boot();

    } else if (_wi3Site === 'carib') {
        // ============================================================
        // CARIBBEANCOM / CARIBBEANCOMPR half — v1.5.1
        // Verified live 07/10: /r/member playlists are public (any u=),
        // but /hls/ FRAGMENTS return 401 without a member session token
        // (u= comes from $.getScript(getauthjw7key) -> authjw7Key.u, i.e.
        // D2Pass member login; commercial tools confirm the same gate).
        // So the flow is gated:
        //  - member page (window.User set): wait for authjw7Key.u, take
        //    over the site videojs box with our player + quality switcher
        //    (real token -> full 240p..1080p ladder streams fine).
        //  - guest: probe ONE fragment once. 200 -> take over with the
        //    dummy token; 401/err -> leave the site player alone (trailer
        //    keeps working) and log the pending free-unlock status.
        // ============================================================
        const CARIB_BRAND = location.hostname.replace(/^(www|m|en|cn|de|fr|es|it|ru|ko|zh|tv)\./, '');
        const CARIB_FMS = 'https://fms.caribbeancom.com';
        const CARIB_LABELS = ['1080p', '720p', '480p', '360p', '240p'];
        const C_GM = (typeof GM_xmlhttpRequest === 'function') ? GM_xmlhttpRequest : null;
        // page world (window.User / authjw7Key live there, not in sandbox)
        const C_PW = (typeof unsafeWindow !== 'undefined' && unsafeWindow) ? unsafeWindow : window;

        function caribId() {
            const m = location.pathname.match(/\/moviepages\/([0-9]{6}-[0-9]{3})\//);
            return m ? m[1] : null;
        }
        function caribMaster(id) {
            return CARIB_FMS + '/r/member/' + CARIB_BRAND + '/' + id + '/mb.m3u8?u=' + encodeURIComponent(cToken);
        }

        let cDiv = null, cVideo = null, cHls = null, cQual = null, cTimer = null, cGmRetry = false;
        let cToken = 'wi3'; // swapped for authjw7Key.u on member pages

        // GM loader — same proven shape as the AVJB half. Fallback when the
        // plain XHR path dies (Origin not whitelisted, e.g. a subdomain).
        function GmLoader() {
            this._aborted = false;
            this.stats = { aborted: false, loaded: 0, retry: 0, total: 0, chunkCount: 0, bwEstimate: 0,
                loading: { start: 0, first: 0, end: 0 }, parsing: { start: 0, end: 0 }, buffering: { start: 0, end: 0 } };
        }
        GmLoader.prototype.load = function (context, config, callbacks) {
            const self = this;
            const url = context.url;
            const rt = context.responseType === 'arraybuffer' ? 'arraybuffer' : 'text';
            self._aborted = false;
            const t0 = performance.now();
            const s = self.stats;
            s.aborted = false; s.loaded = 0; s.retry = 0; s.total = 0; s.chunkCount = 0; s.bwEstimate = 0;
            s.loading.start = t0; s.loading.first = t0; s.loading.end = 0;
            s.parsing.start = 0; s.parsing.end = 0;
            s.buffering.start = 0; s.buffering.end = 0;
            const netStub = { getResponseHeader: () => null, getAllResponseHeaders: () => '' };
            if (!C_GM) { callbacks.onError({ text: 'no GM_xhr', code: 0 }, context, netStub); return; }
            try {
                C_GM({
                    method: 'GET', url: url, responseType: rt, timeout: 25000,
                    headers: { 'Referer': location.origin + '/' },
                    onload: (r) => {
                        const t1 = performance.now();
                        if (self._aborted || self.stats.aborted) return;
                        if (r.status >= 200 && r.status < 300) {
                            const data = r.response;
                            const len = (data && (data.byteLength || data.length)) || 0;
                            self.stats.loading.end = t1;
                            self.stats.loaded = len;
                            self.stats.total = len;
                            // xhr-loader calls onProgress before onSuccess —
                            // stream-controller needs it to transmux()
                            if (typeof callbacks.onProgress === 'function') {
                                callbacks.onProgress(self.stats, context, data, netStub);
                            }
                            callbacks.onSuccess({ url: url, data: data }, self.stats, context, netStub);
                        } else {
                            callbacks.onError({ text: 'HTTP ' + r.status, code: r.status }, context, netStub);
                        }
                    },
                    onerror: () => { if (!self._aborted) callbacks.onError({ text: 'gm error', code: 0 }, context, netStub); },
                    ontimeout: () => { if (!self._aborted) callbacks.onError({ text: 'gm timeout', code: 0 }, context, netStub); },
                });
            } catch (e) {
                callbacks.onError({ text: String(e && e.message || e), code: 0 }, context, netStub);
            }
        };
        GmLoader.prototype.abort = function () { this._aborted = true; this.stats.aborted = true; };
        GmLoader.prototype.destroy = function () {};

        function caribStatus(t) {
            const s = cDiv && cDiv.querySelector('.wi3-status');
            if (s) s.textContent = t;
        }

        // levels sorted bandwidth-desc -> fixed ladder labels (the master
        // playlist serves exactly 240/360/480/720/1080 when it has 5 entries)
        function caribQualBuild() {
            if (!cQual || !cHls) return;
            let lv = (cHls.levels || []).map((l, i) => ({ i: i, b: l.bandwidth || 0 }));
            if (!lv.length) return;
            lv.sort((a, b) => b.b - a.b);
            cQual.innerHTML = '';
            lv.forEach((e, rank) => {
                const label = (lv.length === CARIB_LABELS.length)
                    ? CARIB_LABELS[rank]
                    : Math.round(e.b / 1000) + 'k';
                const b = document.createElement('button');
                b.className = 'wi3-q' + (rank === 0 ? ' on' : '');
                b.textContent = label;
                b.onclick = function () {
                    try { cHls.currentLevel = e.i; } catch (err) {}
                    Array.prototype.forEach.call(cQual.children, function (x) { x.classList.remove('on'); });
                    b.classList.add('on');
                };
                cQual.appendChild(b);
            });
        }

        function caribCreate(id, useGm) {
            try { if (cHls) { cHls.destroy(); cHls = null; } } catch (e) {}
            if (typeof Hls === 'undefined' || !Hls.isSupported()) {
                // no MSE (or @require failed) -> native HLS (Safari) or nothing
                try { cVideo.src = caribMaster(id); caribStatus('native hls'); } catch (e) {}
                return;
            }
            cHls = new Hls(useGm ? { startLevel: 0, loader: GmLoader } : { startLevel: 0 });
            cHls.on(Hls.Events.MANIFEST_PARSED, function () {
                caribQualBuild();
                caribStatus('full video ready - ' + (cHls.levels ? cHls.levels.length : 0) + ' qualities');
            });
            cHls.on(Hls.Events.ERROR, function (ev, data) {
                if (!data || !data.fatal) return;
                if (data.type === Hls.ErrorTypes.NETWORK_ERROR && !cGmRetry && C_GM && !useGm) {
                    // Origin not whitelisted for this subdomain -> GM xhr retry
                    cGmRetry = true;
                    caribStatus('retry via GM xhr...');
                    caribCreate(id, true);
                } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
                    try { cHls.recoverMediaError(); } catch (e) {}
                } else {
                    caribStatus('error: ' + (data.details || 'fatal'));
                }
            });
            cHls.loadSource(caribMaster(id));
            cHls.attachMedia(cVideo);
        }

        // site player (videojs: #player_outer > #player > video#vjs-player)
        // is guest-gated — hide it entirely, ours is the only player.
        function caribNuke() {
            try {
                const box = document.getElementById('player_outer');
                if (box && box !== cDiv) box.style.setProperty('display', 'none', 'important');
                document.querySelectorAll('video.video-js').forEach(function (v) {
                    v.style.setProperty('display', 'none', 'important');
                });
            } catch (e) {}
        }

        function caribInject() {
            const id = caribId();
            if (!id) return;
            if (cDiv && document.contains(cDiv)) return;
            const anchor = document.getElementById('player_outer')
                || document.getElementById('player')
                || document.querySelector('video.video-js');
            if (!anchor || !anchor.parentNode) return;
            // sweep foreign copies (stale TM bundle) — never 2 players
            document.querySelectorAll('#wi3Player').forEach(function (el) { if (el !== cDiv) el.remove(); });

            cDiv = document.createElement('div');
            cDiv.id = 'wi3Player';
            cDiv.innerHTML =
                '<div class="wi3-top"><span class="wi3-badge">wi3 &middot; Caribbeancom unlocked</span>' +
                '<span class="wi3-status">connecting...</span></div>' +
                '<video id="wi3CaribVid" controls playsinline></video>' +
                '<div id="wi3Qual"></div>';
            anchor.parentNode.insertBefore(cDiv, anchor);
            cVideo = cDiv.querySelector('video');
            cQual = cDiv.querySelector('#wi3Qual');
            caribCreate(id, false);
        }

        function caribTakeover(id, how) {
            if (cDiv && document.contains(cDiv)) return;
            try {
                GM_addStyle(
                    '#wi3Player{position:relative;z-index:999999;background:#000;border:1px solid #333;' +
                    'border-radius:6px;overflow:hidden;margin:8px 0;}' +
                    '#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
                    'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
                    '#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
                    '#wi3Player .wi3-status{color:#aaa;}' +
                    '#wi3CaribVid{display:block;width:100%;background:#000;max-height:70vh;}' +
                    '#wi3Qual{display:flex;gap:6px;padding:8px 10px;background:#111;flex-wrap:wrap;}' +
                    '#wi3Qual .wi3-q{background:#222;color:#ccc;border:1px solid #444;border-radius:4px;' +
                    'padding:4px 10px;font:12px sans-serif;cursor:pointer;}' +
                    '#wi3Qual .wi3-q.on{background:#ff4757;color:#fff;border-color:#ff4757;}' +
                    '#wi3Qual .wi3-q:hover{border-color:#888;}'
                );
            } catch (e) {}
            caribInject();
            if (!cTimer) {
                cTimer = setInterval(function () {
                    try {
                        caribNuke();
                        if (!cDiv || !document.contains(cDiv)) caribInject();
                    } catch (e) {}
                }, 600);
            }
            console.log('[wi3-CARIB] v1.5.1 active on', location.pathname, '| brand', CARIB_BRAND, '|', how);
        }

        // Gate: fragments 401 for guests (member token required). Two paths:
        //   member page -> wait briefly for $.getScript(getauthjw7key) to set
        //                  window.authjw7Key.u (real token) -> takeover
        //   guest      -> probe ONE fragment with dummy u once; only if the
        //                  server actually serves it (200/206) do we take over
        function caribGate(id) {
            const t0 = Date.now();
            let probed = false, stops = 0;
            const tick = function () {
                if (cDiv && document.contains(cDiv)) return;
                let key = null, isMember = false;
                try {
                    if (C_PW) {
                        if (C_PW.authjw7Key && C_PW.authjw7Key.u) key = String(C_PW.authjw7Key.u);
                        if (C_PW.User) isMember = true;
                    }
                } catch (e) {}
                if (key) {
                    cToken = key;
                    caribTakeover(id, 'member token');
                    return;
                }
                const elapsed = Date.now() - t0;
                if (isMember) {
                    // member session: authjw7Key arrives via $.getScript - wait up to 6s
                    if (elapsed < 6000 && ++stops < 40) { setTimeout(tick, 200); }
                    else { console.log('[wi3-CARIB] member page but no authjw7Key - leaving site player'); }
                    return;
                }
                if (probed) return;
                if (elapsed < 1500) { setTimeout(tick, 200); return; } // grace: let inline scripts define User
                probed = true;
                if (!C_GM) {
                    console.log('[wi3-CARIB] guest, no GM xhr to probe - site player untouched');
                    return;
                }
                const probe = CARIB_FMS + '/hls/member/' + CARIB_BRAND + '/' + id +
                    '/1080p.mp4Frag1Num0.ts?u=wi3&d=.ts';
                C_GM({
                    method: 'GET',
                    url: probe,
                    headers: { 'Referer': location.href, 'Origin': location.origin },
                    timeout: 15000,
                    onload: function (r) {
                        if (r.status >= 200 && r.status < 300) {
                            caribTakeover(id, 'dummy u ok, status ' + r.status);
                        } else {
                            console.log('[wi3-CARIB] guest fragment status ' + r.status +
                                ' - full video needs member login; site player untouched');
                        }
                    },
                    onerror: function (r) {
                        console.log('[wi3-CARIB] guest fragment probe failed' +
                            (r && r.status ? ' (status ' + r.status + ')' : '') +
                            ' - site player untouched');
                    },
                    ontimeout: function () {
                        console.log('[wi3-CARIB] guest fragment probe timeout - site player untouched');
                    }
                });
            };
            tick();
        }

        function caribBoot() {
            if (!document.body) { setTimeout(caribBoot, 20); return; }
            const id = caribId();
            if (id) caribGate(id);
        }
        caribBoot();

    } else if (_wi3Site === 'na') {
        // ============================================================
        // NAUGHTYAMERICA half -- v1.5.3 (Tier 2, verified live 08/10)
        // Guest /scene/<slug> pages ship a PUBLIC trailer HLS on the
        // videojs box #triggerPlay:
        //   abr2.naughtycdn.com/hls/mshf/trailers_apt/<scene>/<name>_
        //   ,1080,720,480,360,r.mp4.urlset/master.m3u8
        // Verified 08/10: master manifest CORS-open (plain fetch returns
        // #EXTM3U), 4-rung ladder whose top rungs are HEVC (hvc1) ->
        // pick the first h264 rung so Chromium MSE plays it; segments
        // serve in-page (206 init-f3 + m4s seen while playing).
        // Member session: the same <video> carries the full scene ->
        // identical takeover, status flips to 'full stream ready'.
        // Guest full scene stays membership-gated -> honest note line.
        // ============================================================
        const NA_LABELS = ['1080p', '720p', '480p', '360p'];
        const NA_GM = (typeof GM_xmlhttpRequest === 'function') ? GM_xmlhttpRequest : null;

        let nDiv = null, nVideo = null, nHls = null, nQual = null, nTimer = null, nGmRetry = false;

        // GM loader -- same proven shape as the carib half; fallback when
        // the CDN answers manifests CORS-open but segments insist on a
        // real Referer (location.href is what the site player sends).
        function GmLoader() {
            this._aborted = false;
            this.stats = { aborted: false, loaded: 0, retry: 0, total: 0, chunkCount: 0, bwEstimate: 0,
                loading: { start: 0, first: 0, end: 0 }, parsing: { start: 0, first: 0, end: 0 },
                buffering: { start: 0, first: 0, end: 0 } };
        }
        GmLoader.prototype.load = function (context, config, callbacks) {
            const self = this;
            const url = context.url;
            const rt = context.responseType === 'arraybuffer' ? 'arraybuffer' : 'text';
            self._aborted = false;
            const t0 = performance.now();
            const s = self.stats;
            s.aborted = false; s.loaded = 0; s.retry = 0; s.total = 0; s.chunkCount = 0; s.bwEstimate = 0;
            s.loading.start = t0; s.loading.first = t0; s.loading.end = 0;
            s.parsing.start = 0; s.parsing.end = 0;
            s.buffering.start = 0; s.buffering.end = 0;
            const netStub = { getResponseHeader: () => null, getAllResponseHeaders: () => '' };
            if (!NA_GM) { callbacks.onError({ text: 'no GM_xhr', code: 0 }, context, netStub); return; }
            try {
                NA_GM({
                    method: 'GET', url: url, responseType: rt, timeout: 25000,
                    headers: { 'Referer': location.href },
                    onload: (r) => {
                        const t1 = performance.now();
                        if (self._aborted || self.stats.aborted) return;
                        if (r.status >= 200 && r.status < 300) {
                            const data = r.response;
                            const len = (data && (data.byteLength || data.length)) || 0;
                            self.stats.loading.end = t1;
                            self.stats.loaded = len;
                            self.stats.total = len;
                            if (typeof callbacks.onProgress === 'function') {
                                callbacks.onProgress(self.stats, context, data, netStub);
                            }
                            callbacks.onSuccess({ url: url, data: data }, self.stats, context, netStub);
                        } else {
                            callbacks.onError({ text: 'HTTP ' + r.status, code: r.status }, context, netStub);
                        }
                    },
                    onerror: () => { if (!self._aborted) callbacks.onError({ text: 'gm error', code: 0 }, context, netStub); },
                    ontimeout: () => { if (!self._aborted) callbacks.onError({ text: 'gm timeout', code: 0 }, context, netStub); },
                });
            } catch (e) {
                callbacks.onError({ text: String(e && e.message || e), code: 0 }, context, netStub);
            }
        };
        GmLoader.prototype.abort = function () { this._aborted = true; this.stats.aborted = true; };
        GmLoader.prototype.destroy = function () {};

        function naStatus(t) {
            const s = nDiv && nDiv.querySelector('.wi3-status');
            if (s) s.textContent = t;
        }

        function naUrl() {
            try {
                const v = document.getElementById('triggerPlay_html5_api')
                    || document.querySelector('video.vjs-tech, video[src*="m3u8"]');
                const s = v && (v.currentSrc || v.getAttribute('src') || '');
                if (s && /\.m3u8/i.test(s)) return s;
                const h = (document.documentElement.innerHTML || '').replace(/\\u002F/g, '/');
                const m = h.match(/https?:\/\/abr\d*\.naughtycdn\.com\/[^"'\\\s<>]+master\.m3u8/);
                if (m) return m[0];
            } catch (e) {}
            return null;
        }

        function naIsHevc(lv) {
            const c = String((lv && lv.attrs && lv.attrs.CODECS) || (lv && lv.videoCodec) || '');
            return /hvc1|hev1|h265|hevc/i.test(c);
        }
        // first rung Chromium MSE can actually play (ladder tops are HEVC)
        function naFirstPlayable(hls) {
            const ls = (hls && hls.levels) || [];
            for (let i = 0; i < ls.length; i++) if (!naIsHevc(ls[i])) return i;
            return -1;
        }

        function naQualBuild() {
            if (!nQual || !nHls) return;
            let lv = (nHls.levels || []).map((l, i) => ({ i: i, b: l.bandwidth || 0, hevc: naIsHevc(l) }));
            if (!lv.length) return;
            lv.sort((a, b) => b.b - a.b);
            nQual.innerHTML = '';
            lv.forEach((e, rank) => {
                const label = (lv.length === NA_LABELS.length) ? NA_LABELS[rank]
                    : (Math.round(e.b / 1000) + 'k');
                const b = document.createElement('button');
                b.className = 'wi3-q' + (e.hevc ? ' hevc' : '') + (rank === 0 ? ' on' : '');
                b.textContent = label + (e.hevc ? ' (hevc)' : '');
                b.onclick = function () {
                    try { nHls.currentLevel = e.i; } catch (err) {}
                    Array.prototype.forEach.call(nQual.children, function (x) { x.classList.remove('on'); });
                    b.classList.add('on');
                };
                nQual.appendChild(b);
            });
        }

        function naCreate(useGm) {
            const url = naUrl();
            if (!url || !nVideo) return;
            try { if (nHls) { nHls.destroy(); nHls = null; } } catch (e) {}
            const trailer = /trailers_apt|trailer/i.test(url);
            const note = nDiv && nDiv.querySelector('.wi3-note');
            if (note) note.textContent = trailer
                ? 'guest sees the public trailer - the full scene is NA membership-gated'
                : 'member session - full scene stream';
            if (typeof Hls === 'undefined' || !Hls.isSupported()) {
                // no MSE (or @require failed) -> native HLS (Safari) or nothing
                try { nVideo.src = url; naStatus('native hls'); } catch (e) {}
                return;
            }
            nHls = new Hls(useGm ? { startLevel: 0, loader: GmLoader } : { startLevel: 0 });
            nHls.on(Hls.Events.MANIFEST_PARSED, function () {
                const idx = naFirstPlayable(nHls);
                if (idx > 0) { try { nHls.startLevel = idx; } catch (e) {} }
                naQualBuild();
                naStatus((trailer ? 'public trailer ready' : 'full stream ready')
                    + ' - ' + (nHls.levels ? nHls.levels.length : 0) + ' qualities');
            });
            nHls.on(Hls.Events.ERROR, function (ev, data) {
                if (!data || !data.fatal) return;
                if (data.type === Hls.ErrorTypes.NETWORK_ERROR && !nGmRetry && NA_GM && !useGm) {
                    nGmRetry = true;
                    naStatus('retry via GM xhr...');
                    naCreate(true);
                } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR) {
                    // HEVC rung on a box without hw decode -> step to first h264 rung
                    const cur = (nHls.currentLevel >= 0 && nHls.levels) ? nHls.levels[nHls.currentLevel] : null;
                    if (cur && naIsHevc(cur)) {
                        const idx = naFirstPlayable(nHls);
                        if (idx >= 0) { try { nHls.currentLevel = idx; } catch (e) {} return; }
                    }
                    try { nHls.recoverMediaError(); } catch (e) {}
                } else {
                    naStatus('error: ' + (data.details || 'fatal'));
                }
            });
            nHls.loadSource(url);
            nHls.attachMedia(nVideo);
        }

        // site videojs box #triggerPlay holds the same stream behind the
        // start card -- hide it and pause its audio, ours is the only player
        function naNuke() {
            try {
                const box = document.getElementById('triggerPlay');
                if (box && box !== nDiv) {
                    box.style.setProperty('display', 'none', 'important');
                    const sv = box.querySelector('video');
                    if (sv) { try { sv.pause(); } catch (e) {} }
                }
                document.querySelectorAll('video.vjs-tech').forEach(function (v) {
                    if (nVideo && v === nVideo) return;
                    try { v.pause(); } catch (e) {}
                    v.style.setProperty('display', 'none', 'important');
                });
            } catch (e) {}
        }

        function naInject() {
            if (!naUrl()) return;
            if (nDiv && document.contains(nDiv)) return;
            const siteBox = document.getElementById('triggerPlay');
            const anchor = (siteBox && siteBox.parentNode)
                || document.querySelector('.contain-start-card')
                || document.querySelector('.left-scene')
                || (function () { const v = document.querySelector('video.vjs-tech'); return v && v.parentNode; })();
            if (!anchor) return;
            // sweep foreign copies (stale TM bundle) -- never 2 players
            document.querySelectorAll('#wi3Player').forEach(function (el) { if (el !== nDiv) el.remove(); });

            nDiv = document.createElement('div');
            nDiv.id = 'wi3Player';
            nDiv.innerHTML =
                '<div class="wi3-top"><span class="wi3-badge">wi3 &middot; NaughtyAmerica</span>' +
                '<span class="wi3-status">connecting...</span></div>' +
                '<video id="wi3NaVid" controls playsinline></video>' +
                '<div id="wi3Qual"></div>' +
                '<div class="wi3-note"></div>';
            anchor.insertBefore(nDiv, anchor.firstChild || null);
            nVideo = nDiv.querySelector('video');
            nQual = nDiv.querySelector('#wi3Qual');
            naCreate(false);
        }

        function naTakeover(how) {
            if (nDiv && document.contains(nDiv)) return;
            try {
                GM_addStyle(
                    '#wi3Player{position:relative;z-index:999999;background:#000;border:1px solid #333;' +
                    'border-radius:6px;overflow:hidden;margin:8px 0;}' +
                    '#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
                    'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
                    '#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
                    '#wi3Player .wi3-status{color:#aaa;}' +
                    '#wi3NaVid{display:block;width:100%;background:#000;max-height:70vh;}' +
                    '#wi3Qual{display:flex;gap:6px;padding:8px 10px;background:#111;flex-wrap:wrap;}' +
                    '#wi3Qual .wi3-q{background:#222;color:#ccc;border:1px solid #444;border-radius:4px;' +
                    'padding:4px 10px;font:12px sans-serif;cursor:pointer;}' +
                    '#wi3Qual .wi3-q.on{background:#ff4757;color:#fff;border-color:#ff4757;}' +
                    '#wi3Qual .wi3-q:hover{border-color:#888;}' +
                    '#wi3Player .wi3-note{padding:6px 10px;background:#0b0b0b;color:#8ab4f8;' +
                    'font:12px/1.5 sans-serif;border-top:1px solid #222;}'
                );
            } catch (e) {}
            naInject();
            if (!nTimer) {
                nTimer = setInterval(function () {
                    try {
                        naNuke();
                        if (!nDiv || !document.contains(nDiv)) naInject();
                    } catch (e) {}
                }, 600);
            }
            console.log('[wi3-NA] v1.5.3 active on', location.pathname, '|', how);
        }

        function naBoot() {
            if (!document.body) { setTimeout(naBoot, 20); return; }
            const t0 = Date.now();
            let done = false;
            const tick = function () {
                if (done) return;
                const u = naUrl();
                if (u) {
                    done = true;
                    naTakeover('src ' + (u.indexOf('trailers_apt') >= 0 ? 'trailer' : 'full'));
                    return;
                }
                if (Date.now() - t0 > 15000) {
                    console.log('[wi3-NA] no stream url after 15s (not a /scene/ page or still gated)' +
                        ' - page untouched');
                    done = true;
                    return;
                }
                setTimeout(tick, 400);
            };
            tick();
        }
        naBoot();

    } else if (_wi3Site === 'php') {
        // ============================================================
        // PORNHUB PREMIUM guest bypass -- v1.5.4
        // The premium domain serves guests a static login wall (zero
        // video API calls fire on view_video.php). The SAME viewkey on
        // the main site still ships guest flashvars: videoUrl = signed
        // phncdn HLS master (h/e/f tokens) + quality ladder
        // 180P_225K..720P_4000K + signed progressive ev.phncdn mp4
        // (validfrom/validto/ip/hash).  GM xhr runs in the extension
        // context, so cross-domain GET to www.pornhub.com needs no
        // CORS -- buyer networks reach .com fine; probe boxes with a
        // sinkholed .com degrade gracefully to the honest note.
        // Parse -> hls.js takeover above the wall. Premium-exclusive
        // viewkeys (no main-site copy / redirect) -> honest note,
        // page untouched -- same discipline as the CC/BC gates.
        // ============================================================
        (function phpBoot() {
            // Viewkey carriers, best-effort: (1) location.search if the server
            // ever renders the video doc directly; (2) document.referrer when
            // the initiating URL carried viewkey. Verified recon 08/10:
            // guest /view_video.php is server-302'd to
            // /premium/login?redirect=<opaque ~90-byte blob> (no client key,
            // login HTML/JS has zero viewkey traces), Chrome propagates the
            // INITIATOR referrer (never the viewkey hop), cookies /
            // localStorage / XHRs carry nothing, header+bot-UA+cookie matrices
            // all302 -> the dominant paste flow lands here WITHOUT a viewkey.
            // Then we show the paste bar: the user still has the link in
            // hand. Known-viewkey flow kicks off automatically above it.
            const KRE = /[?&]viewkey=([a-z0-9]+)/i;
            const vm = location.search.match(KRE) ||
                (document.referrer || '').match(KRE);
            let anchor = null;
            const want = ['main', '#videoPlayerClass', '.video_wrapper', '#root', 'body'];
            for (let i = 0; i < want.length; i++) {
                anchor = document.querySelector(want[i]);
                if (anchor) break;
            }
            if (!anchor) { setTimeout(phpBoot, 300); return; }

            const vk = vm ? vm[1] : '';
            let pDiv = null, pHls = null;

            function phpStyle() {
                try {
                    GM_addStyle(
                        '#wi3Player{position:relative;z-index:999999;background:#000;border:1px solid #333;' +
                        'border-radius:6px;overflow:hidden;margin:8px 0;}' +
                        '#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
                        'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
                        '#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
                        '#wi3Player .wi3-status{color:#aaa;}' +
                        '#wi3Player .wi3-row{display:flex;gap:6px;padding:6px 10px;background:#111;align-items:center;}' +
                        '#wi3Player #wi3In{flex:1;min-width:120px;background:#000;color:#ddd;border:1px solid #333;' +
                        'border-radius:4px;padding:5px 8px;font:12px/1.4 sans-serif;}' +
                        '#wi3Player button{background:#1a1a1a;color:#5eead4;border:1px solid #333;' +
                        'border-radius:4px;padding:5px 10px;font:12px/1.4 sans-serif;cursor:pointer;}' +
                        '#wi3Vid{display:block;width:100%;background:#000;max-height:70vh;}'
                    );
                } catch (e) {}
            }

            function phpNote(t) {
                const s = pDiv && pDiv.querySelector('.wi3-status');
                if (s) s.textContent = t;
            }

            function phpShell(badge, status) {
                phpStyle();
                document.querySelectorAll('#wi3Player').forEach(function (x) { x.remove(); });
                pDiv = document.createElement('div');
                pDiv.id = 'wi3Player';
                pDiv.innerHTML =
                    '<div class="wi3-top"><span class="wi3-badge">' + badge + '</span>' +
                    '<span class="wi3-status">' + status + '</span></div>';
                anchor.insertBefore(pDiv, anchor.firstChild || null);
            }

            function phpPlayer(src, key) {
                phpShell('wi3 &middot; premium guest stream', 'connecting...');
                const v = document.createElement('video');
                v.id = 'wi3Vid';
                v.setAttribute('controls', '');
                v.setAttribute('playsinline', '');
                pDiv.appendChild(v);
                let mode = '';
                let flips = 0;
                const startHls = function () {
                    if (mode === 'hls' || mode === 'native') return;
                    if (src.hls && typeof Hls !== 'undefined' && Hls.isSupported()) {
                        mode = 'hls';
                        pHls = new Hls({ startLevel: 0 });
                        pHls.on(Hls.Events.MANIFEST_PARSED, function () {
                            phpNote('stream ready - main-site flashvars');
                        });
                        pHls.on(Hls.Events.ERROR, function (e2, d) {
                            if (!d || !d.fatal) return;
                            console.log('[wi3-PHP] hls fatal detail=' + (d.details || '?') +
                                ' code=' + (d.response && d.response.code));
                            try { if (pHls) { pHls.destroy(); pHls = null; } } catch (x) {}
                            if (src.mp4 && flips < 3) {
                                flips++;
                                mode = 'mp4';
                                v.src = src.mp4;
                                phpNote('hls rejected - progressive mp4 fallback');
                            } else {
                                phpNote('stream error - free path pending');
                            }
                        });
                        pHls.loadSource(src.hls);
                        pHls.attachMedia(v);
                    } else if (src.hls) {
                        mode = 'native';
                        v.src = src.hls;
                        phpNote('playing natively');
                    } else {
                        phpNote('stream error - free path pending');
                    }
                };
                v.addEventListener('error', function () {
                    if (mode === 'mp4' && src.hls && flips < 3) {
                        // progressive source rejected (expired edge / blocked) ->
                        // recover through hls.js (same flashvars, second rendition)
                        console.log('[wi3-PHP] mp4 error code=' + (v.error && v.error.code) + ' -> hls fallback');
                        flips++;
                        mode = '';
                        startHls();
                    } else if (mode === 'native' && src.mp4 && flips < 3) {
                        // native m3u8 refused (Chrome has no native HLS) -> progressive
                        console.log('[wi3-PHP] native hls rejected -> mp4 fallback');
                        flips++;
                        mode = 'mp4';
                        v.src = src.mp4;
                        phpNote('native hls rejected - progressive mp4 fallback');
                    } else if (mode === 'mp4' || mode === 'native') {
                        phpNote('stream error - free path pending');
                    }
                });
                if (src.mp4 && (src.mp4Ladder || !src.hls)) {
                    // progressive mp4 first: <video src> needs NO CORS at all and
                    // does not depend on the @require hls.js bundle being reachable.
                    // v1.6.2 ladder-first: when mp4 is only the og:video shadow
                    // (240p ip-bound), flashvars mediaPriority=hls owns quality.
                    mode = 'mp4';
                    v.src = src.mp4;
                    phpNote('progressive mp4 ready - main-site flashvars');
                } else {
                    startHls();
                }
                console.log('[wi3-PHP] takeover viewkey=' + key +
                    ' mp4=' + String(src.mp4 || '-').slice(0, 70) +
                    ' hls=' + String(src.hls || '-').slice(0, 70));
            }

            function phpParse(html) {
                // returns { hls, mp4 } | null -- walks the flashvars media array
                // entries  {"group":1,"height":720,"format":"mp4|hls",
                // "videoUrl":"https:\/\/..."} and keeps the HIGHEST rendition of
                // each type (first-match naive regex would grab 240P). Order of
                // keys inside an entry does not matter: the whole entry object is
                // parsed with JSON.parse. Legacy raw-URL scans stay as fallback
                // for pages whose flashvars use the old single-videoUrl shape.
                let hls = null, hlsQ = -1, mp4 = null, mp4Q = -1;
                let mp4Ladder = false;
                let idx = 0;
                while ((idx = html.indexOf('{"group":', idx)) !== -1) {
                    let depth = 0, inStr = false, esc = false, end = -1;
                    for (let k = idx; k < html.length && k < idx + 3000; k++) {
                        const ch = html.charAt(k);
                        if (inStr) {
                            if (esc) esc = false;
                            else if (ch === '\\') esc = true;
                            else if (ch === '"') inStr = false;
                        } else if (ch === '"') inStr = true;
                        else if (ch === '{') depth++;
                        else if (ch === '}') {
                            depth--;
                            if (depth === 0) { end = k + 1; break; }
                        }
                    }
                    if (end < 0) { idx += 9; continue; }
                    const raw = html.slice(idx, end);
                    idx = end;
                    let ent = null;
                    try { ent = JSON.parse(raw); } catch (x) { continue; }
                    if (!ent || !ent.videoUrl || !/^https?:\/\//.test(String(ent.videoUrl))) continue;
                    const u = String(ent.videoUrl).replace(/&amp;/g, '&');
                    if (/preroll|\/ads?\//i.test(u)) continue;
                    const q = parseInt(ent.height, 10) || 0;
                    if (ent.format === 'mp4' && q > mp4Q) { mp4Q = q; mp4 = u; mp4Ladder = true; }
                    else if (ent.format === 'hls' && q > hlsQ) { hlsQ = q; hls = u; }
                }
                // legacy fallbacks (old flashvars shape / no {"group": array)
                if (!hls || !mp4) {
                    const flat = html.replace(/\\\//g, '/');
                    const qOf = function (s) {
                        const m2 = s.match(/_(\d{3,4})P_(\d+)K_/);
                        return m2 ? parseInt(m2[2], 10) : 0;
                    };
                    if (!hls) {
                        const all = flat.match(/https?:[^"'\\\s]+\.m3u8[^"'\\\s]*/gi) || [];
                        for (const cand of all) {
                            const u2 = cand.replace(/&amp;/g, '&');
                            const q2 = qOf(u2);
                            if (q2 > hlsQ) { hlsQ = q2; hls = u2; }
                        }
                        if (!hls && all.length) hls = all[0].replace(/&amp;/g, '&');
                    }
                    if (!mp4) {
                        const all = flat.match(/https?:\/\/[^"'\\\s]*phncdn[^"'\\\s]+\.mp4\?[^"'\\\s]*/gi) || [];
                        for (const cand of all) {
                            const u2 = cand.replace(/&amp;/g, '&');
                            if (/\/hls\//i.test(u2) || /preroll|\/ads?\//i.test(u2)) continue;
                            const q2 = qOf(u2);
                            if (q2 > mp4Q) { mp4Q = q2; mp4 = u2; }
                        }
                        if (!mp4 && all.length) mp4 = all[0].replace(/&amp;/g, '&');
                    }
                }
                return (hls || mp4) ? { hls: hls, mp4: mp4, mp4Ladder: mp4Ladder } : null;
            }

            function phpKick(key) {
                phpShell('wi3 &middot; premium bypass', 'fetching main-site copy of ' + key + '...');
                const MAIN = 'https://www.pornhub.com/view_video.php?viewkey=' + encodeURIComponent(key);
                const noteDead = function () {
                    phpNote('main site unreachable from this network - free path pending');
                };
                const adopt = function (body) {
                    const u = phpParse(body);
                    if (u) {
                        phpPlayer(u, key);
                    } else if (/has been removed|video private|error-code/i.test(body)) {
                        phpNote('no main-site copy of this viewkey - free path pending');
                    } else {
                        phpNote('no flashvars on main-site copy - free path pending');
                    }
                };
                // v1.6.2 blocked-ISP path: ISPs that sinkhole www.pornhub.com
                // (DNS -> 127.0.0.1, or SNI-reset on the TLS handshake) never
                // reach the direct kick. archive.org Save-Page-Now returns the
                // FRESH main-site page inline from an unblocked egress. SPN
                // rewrites videoUrl through web.archive.org/web/<ts>/ (unwrap
                // both the raw and the JSON-escaped form), and the modern h/e/f
                // signature carries NO ip= binding -- verified live 08/10: the
                // archive-fetched master.m3u8 streamed from a different egress
                // (mp4 validfrom/ip/hash IS ip-bound, hence ladder-first above).
                // One mirror per 5 min so buyers never trip SPN rate limits.
                const remote = function (why) {
                    let last = 0;
                    try { last = parseInt(sessionStorage.getItem('wi3PhRm') || '0', 10) || 0; } catch (x) {}
                    if (Date.now() - last < 300000) { noteDead(); return; }
                    try { sessionStorage.setItem('wi3PhRm', String(Date.now())); } catch (x) {}
                    console.log('[wi3-PHP] direct kick failed (' + why + ') -> archive.org mirror');
                    phpNote('isp mirror warming (archive.org) - about 15s...');
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: 'https://web.archive.org/save/' + MAIN,
                        headers: { Referer: MAIN },
                        timeout: 120000,
                        onload: function (r) {
                            let body = String(r.responseText || '');
                            body = body
                                .replace(/(?:https?:)\\\/\\\/web\.archive\.org\\\/web\\\/\d+(?:[a-z]{2}_)?\\\//gi, '')
                                .replace(/https?:\/\/web\.archive\.org\/web\/\d+(?:[a-z]{2}_)?\//gi, '');
                            if (body.length < 4000 || !/(flashvars|mediaDefinitions|og:video)/i.test(body)) {
                                noteDead();
                                return;
                            }
                            adopt(body);
                        },
                        onerror: noteDead,
                        ontimeout: noteDead
                    });
                };
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: MAIN,
                    headers: { Referer: 'https://www.pornhub.com/' },
                    timeout: 25000,
                    onload: function (r) {
                        const fin = String(r.finalUrl || r.responseURL || '');
                        const body = String(r.responseText || '');
                        if (fin && !/viewkey=/i.test(fin)) {
                            phpNote('main site redirected (no main-site copy) - premium-exclusive, free path pending');
                            return;
                        }
                        if (body.length < 4000 || !/(flashvars|mediaDefinitions|og:video)/i.test(body)) {
                            remote('unusable-body');
                            return;
                        }
                        adopt(body);
                    },
                    onerror: function () { remote('network'); },
                    ontimeout: function () { remote('timeout'); }
                });
            }

            function phpBar() {
                phpStyle();
                document.querySelectorAll('#wi3Player').forEach(function (x) { x.remove(); });
                pDiv = document.createElement('div');
                pDiv.id = 'wi3Player';
                pDiv.innerHTML =
                    '<div class="wi3-top"><span class="wi3-badge">wi3 &middot; premium bypass</span>' +
                    '<span class="wi3-status">paste a premium video link - dual-listed scenes play free</span></div>' +
                    '<div class="wi3-row">' +
                    '<input id="wi3In" type="text" spellcheck="false" ' +
                    'placeholder="https://www.pornhubpremium.com/view_video.php?viewkey=...">' +
                    '<button id="wi3Go" type="button">watch</button>' +
                    '<button id="wi3Clip" type="button">clip</button></div>';
                anchor.insertBefore(pDiv, anchor.firstChild || null);
                const inp = pDiv.querySelector('#wi3In');
                const go = function () {
                    const raw = String(inp.value || '').trim();
                    const m = raw.match(KRE) || raw.match(/\b(ph[0-9a-z]{8,}|[0-9]{6,})\b/i);
                    if (!m) {
                        const s = pDiv.querySelector('.wi3-status');
                        if (s) s.textContent = 'need a link containing viewkey= (or paste just the key)';
                        return;
                    }
                    phpKick(m[1]);
                };
                pDiv.querySelector('#wi3Go').addEventListener('click', go);
                inp.addEventListener('keydown', function (e) { if (e.key === 'Enter') go(); });
                pDiv.querySelector('#wi3Clip').addEventListener('click', function () {
                    try {
                        navigator.clipboard.readText().then(function (t) {
                            inp.value = t || '';
                            if (t) go();
                        }).catch(function () {
                            const s = pDiv.querySelector('.wi3-status');
                            if (s) s.textContent = 'clipboard blocked - paste into the box';
                            inp.focus();
                        });
                    } catch (e2) { inp.focus(); }
                });
                console.log('[wi3-PHP] paste bar mounted (no viewkey in carriers)');
            }

            if (vk) phpKick(vk);
            else phpBar();
        })();

    } else if (_wi3Site === 'trlt') {
        // ============================================================
        // VMG + ANGEL NETWORK guest trailer takeover -- v1.6.0
        // (verified live 08/10, headless, UA-bound):
        //   VMG scene pages (blacked/vixen/tushy/tushyraw/blackedraw)
        //   mount <video src="cdn.<site>.com/video/mp4_trailer/<id>/
        //   <SITE>_<id>_480P.mp4?ip=<viewer>&Policy=..&Signature=..">
        //   CloudFront-signed-per-IP: 60-72s @480p (854w), media
        //   error 0 verified on blacked + vixen scene pages. Home
        //   heroes use /previewvideos/ (10s loop) -> excluded by the
        //   src pattern; visible-but-tiny (<400px) nodes skipped as
        //   hover thumbs, 0x0 pre-layout nodes accepted (Angel mounts
        //   its trailer at 0x0 until the player sizes it -- verified
        //   08/10: no listing page mounts trailer-pattern srcs).
        //   Angel Network video pages (evilangel/wicked/
        //   digitalplayground/puretaboo) mount
        //   trailers-fame.gammacdn.com/.../tr_<id>_720p.mp4:
        //   69-81s @720p (1280w), error 0 verified on evilangel +
        //   wicked.
        // Takeover: adopt the signed trailer url into the wi3 box
        // (plain mp4 -- the page-context media fetch carries the same
        // IP the signature was minted for, no GM/hls needed), pause +
        // hide every site copy, honest membership note. The 600ms
        // watcher re-injects after SPA re-renders, refreshes our copy
        // on scene swaps, and drops a stale box after 5 misses.
        // ============================================================
        let tDiv = null, tVid = null, tTimer = null, tMiss = 0;
        const TRL_SRC = /mp4_trailer|\/trailers\/|trailers-fame/i;

        function trHost() { return location.hostname.replace(/^www\./, ''); }

        function trFind() {
            const vs = document.querySelectorAll('video');
            for (let i = 0; i < vs.length; i++) {
                const v = vs[i];
                const s = v.currentSrc || v.getAttribute('src') || '';
                if (!s || !TRL_SRC.test(s)) continue;
                // visible-but-tiny = hover thumbnail; 0x0 = pre-layout main
                // player (Angel Network mounts the trailer at 0x0 until its
                // player sizes it) -> accept, listings mount no trailer srcs
                const w = v.offsetWidth || 0;
                if (w > 0 && w < 400) continue;
                return v;
            }
            return null;
        }

        function trStatus(t) {
            const s = tDiv && tDiv.querySelector('.wi3-status');
            if (s) s.textContent = t;
        }

        function trSweep() {
            const vs = document.querySelectorAll('video');
            for (let i = 0; i < vs.length; i++) {
                const v = vs[i];
                if (tVid && v === tVid) continue;
                const s = v.currentSrc || v.getAttribute('src') || '';
                if (!s || !TRL_SRC.test(s)) continue;
                try { v.pause(); } catch (e) {}
                try { v.muted = true; } catch (e) {}
                try { v.style.setProperty('visibility', 'hidden', 'important'); } catch (e) {}
            }
        }

        function trInject() {
            const v = trFind();
            if (!v) return false;
            const src = v.currentSrc || v.getAttribute('src') || '';
            if (tDiv && document.contains(tDiv)) {
                if (tVid && tVid.getAttribute('src') !== src) {
                    try { tVid.src = src; } catch (e) {}
                    trStatus('new scene trailer ready');
                }
                trSweep();
                return true;
            }
            tDiv = null; tVid = null;
            document.querySelectorAll('#wi3Player').forEach(function (el) { el.remove(); });
            try {
                GM_addStyle(
                    '#wi3Player{position:relative;width:100%;box-sizing:border-box;z-index:999999;background:#000;border:1px solid #333;' +
                    'border-radius:6px;overflow:hidden;margin:8px 0;}' +
                    '#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
                    'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
                    '#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
                    '#wi3Player .wi3-status{color:#aaa;}' +
                    '#wi3TrVid{display:block;width:100%;background:#000;max-height:70vh;}' +
                    '#wi3Player .wi3-note{padding:6px 10px;background:#0b0b0b;color:#8ab4f8;' +
                    'font:12px/1.5 sans-serif;border-top:1px solid #222;}'
                );
            } catch (e) {}
            const box = v.parentElement;
            let anchor = (box && box.parentElement) || document.body;
            // AdultTime mounts its trailer deep inside a display:none player chain ->
            // walk up to the first visible ancestor so the wi3 box is not swallowed
            // (VMG/Angel pages stop at the first hop - identical to the old behavior)
            let ref = box;
            while (anchor && anchor !== document.body) {
                const acs = getComputedStyle(anchor);
                if (acs.display !== 'none' && acs.visibility !== 'hidden' && anchor.offsetWidth > 0) break;
                ref = anchor;
                anchor = anchor.parentElement || document.body;
            }
            while (ref && ref.parentElement && ref.parentElement !== anchor) ref = ref.parentElement;
            tDiv = document.createElement('div');
            tDiv.id = 'wi3Player';
            tDiv.innerHTML =
                '<div class="wi3-top"><span class="wi3-badge">wi3 &middot; ' + trHost() + '</span>' +
                '<span class="wi3-status">loading trailer...</span></div>' +
                '<video id="wi3TrVid" controls playsinline></video>' +
                '<div class="wi3-note">free guest trailer - the full scene is ' + trHost() +
                ' membership-gated (wi3 keeps the trailer playable)</div>';
            anchor.insertBefore(tDiv, (ref && ref.parentElement === anchor) ? ref : null);
            tVid = tDiv.querySelector('video');
            tVid.addEventListener('loadedmetadata', function () {
                trStatus('public trailer ready - ' + Math.round(tVid.duration || 0) + 's @' +
                    (tVid.videoWidth || '?') + 'p');
            });
            tVid.addEventListener('error', function () {
                trStatus('trailer url rejected (signature/ip) - reload the page');
            });
            tVid.src = src;
            trSweep();
            console.log('[wi3-TRLT] trailer adopted on', location.hostname, '|', src.slice(0, 90));
            return true;
        }

        (function trBoot() {
            if (!document.body) { setTimeout(trBoot, 20); return; }
            trInject();
            if (!tTimer) {
                tTimer = setInterval(function () {
                    try {
                        if (tDiv && !document.contains(tDiv)) { tDiv = null; tVid = null; }
                        if (trInject()) {
                            tMiss = 0;
                        } else {
                            tMiss++;
                            if (tDiv && tMiss >= 5) {
                                try { tDiv.remove(); } catch (e) {}
                                tDiv = null; tVid = null;
                            }
                        }
                    } catch (e) {}
                }, 600);
            }
        })();

    } else if (_wi3Site === 'tskeet') {
        // ============================================================
        // TEAMSKEET signed-trailer adopt -- v1.6.0 (verified 08/10):
        // scene pages (/movies/<slug>) embed the free guest trailer
        // as a SIGNED Cloudflare Stream iframe
        // (iframe.cloudflarestream.com/<jwt path>); the frame-internal
        // <video> attaches an MSE blob with duration 82s for guests --
        // no cookie, no join. We CLONE the iframe (same src -> JWT
        // reloads within validity) into the wi3 box and hide the site
        // host with display:none: the original node stays attached
        // (no React/DOM teardown errors) and is unreachable (no double
        // audio). The 700ms watcher re-syncs src on SPA scene swaps,
        // re-asserts the hidden host, and drops the box after 5
        // consecutive misses (navigated back to a listing). Listing
        // pages carry no iframe -> idle.
        // ============================================================
        let kDiv = null, kTimer = null, kMiss = 0;

        function ksFind() { return document.querySelector('iframe[src*="cloudflarestream"]'); }

        function ksStatus(t) {
            const s = kDiv && kDiv.querySelector('.wi3-status');
            if (s) s.textContent = t;
        }

        function ksBuild(f) {
            const host = f.parentElement;
            const outer = host && host.parentElement;
            try {
                GM_addStyle(
                    '#wi3Player{position:relative;z-index:999999;background:#000;border:1px solid #333;' +
                    'border-radius:6px;overflow:hidden;margin:8px 0;}' +
                    '#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
                    'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
                    '#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
                    '#wi3Player .wi3-status{color:#aaa;}' +
                    '#wi3Player .wi3-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;}' +
                    '#wi3Player .wi3-frame iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:0;}' +
                    '#wi3Player .wi3-note{padding:6px 10px;background:#0b0b0b;color:#8ab4f8;' +
                    'font:12px/1.5 sans-serif;border-top:1px solid #222;}'
                );
            } catch (e) {}
            document.querySelectorAll('#wi3Player').forEach(function (el) { el.remove(); });
            kDiv = document.createElement('div');
            kDiv.id = 'wi3Player';
            kDiv.innerHTML =
                '<div class="wi3-top"><span class="wi3-badge">wi3 &middot; TeamSkeet</span>' +
                '<span class="wi3-status">trailer embed ready</span></div>' +
                '<div class="wi3-frame"></div>' +
                '<div class="wi3-note">free guest trailer (~80s) - the full scene is TeamSkeet ' +
                'membership-gated (wi3 keeps the trailer playable)</div>';
            if (outer) outer.insertBefore(kDiv, host);
            else if (host) host.insertBefore(kDiv, host.firstChild);
            else document.body.insertBefore(kDiv, document.body.firstChild);
            const src = f.getAttribute('src') || '';
            const mine = document.createElement('iframe');
            mine.setAttribute('src', src);
            mine.setAttribute('data-site-src', src);
            mine.setAttribute('allow', 'autoplay; encrypted-media; picture-in-picture');
            mine.setAttribute('allowfullscreen', '');
            kDiv.querySelector('.wi3-frame').appendChild(mine);
            try { host.style.setProperty('display', 'none', 'important'); } catch (e) {}
            console.log('[wi3-TSKEET] trailer iframe adopted on', location.pathname.slice(0, 70));
        }

        (function ksBoot() {
            if (!document.body) { setTimeout(ksBoot, 20); return; }
            if (!kTimer) {
                kTimer = setInterval(function () {
                    try {
                        const f = ksFind();
                        if (!f) {
                            kMiss++;
                            if (kDiv && kMiss >= 5) { try { kDiv.remove(); } catch (e) {} kDiv = null; }
                            return;
                        }
                        kMiss = 0;
                        const src = f.getAttribute('src') || '';
                        if (kDiv && !document.contains(kDiv)) kDiv = null;
                        if (!kDiv) { ksBuild(f); return; }
                        const mine = kDiv.querySelector('iframe');
                        if (mine && src && mine.getAttribute('data-site-src') !== src) {
                            mine.setAttribute('data-site-src', src);
                            mine.setAttribute('src', src);
                            ksStatus('trailer embed ready');
                        }
                        const host = f.parentElement;
                        if (host) host.style.setProperty('display', 'none', 'important');
                    } catch (e) {}
                }, 700);
            }
        })();

    } else if (_wi3Site === 'nubil') {
        // ============================================================
        // NUBILES ENTERTAINMENT NETWORK (26 domains) guest preview
        // takeover + preview-quota reset -- v1.6.1, verified 08/10 on
        // nubiles-porn.com + momsteachsex.com + stepsiblingscaught.com:
        //   scene pages /video/watch/<id>/<slug> mount videojs with a
        //   signed trailer ladder images.<site>/videos/<slug>/videos/
        //   <site>_<slug>_trailer_{480,640,960,1280}.mp4?st=<sig>&
        //   e=<unix> (84s @960x540 default; media hotlinks with a
        //   Referer only, no cookie; each quality carries its own st).
        //   Guests get 5 previews per SERVER session (PHPSESSID +
        //   avs_id, both HttpOnly=no -> JS writable): at
        //   "0 Preview(s) Remaining" the site mounts NO <video> at all.
        //   Priority: live video sources (site picks 960/1280) -> this
        //   scene's signed trailer urls left in the HTML -> og:video
        //   meta (480p) -> rotate both cookies + reload (guarded: one
        //   rotation per 5 min via sessionStorage, no reload loops).
        //   Full scene = members-only (unsigned paths answer 403) ->
        //   honest membership note. Age modal: set the site's own
        //   18-plus-modal cookie and drop a live modal.
        // ============================================================
        let nDiv = null, nVid = null, nTimer = null, nMiss = 0;
        const NB_TRAIL = /_trailer_\d+\.mp4/i;

        function nbHost() { return location.hostname.replace(/^www\./, ''); }

        function nbRndHex(n) {
            let s = '';
            try {
                const a = new Uint8Array(n);
                (window.crypto || window.msCrypto).getRandomValues(a);
                for (let i = 0; i < n; i++) s += ('0' + a[i].toString(16)).slice(-2);
            } catch (e) {
                for (let i = 0; i < n; i++) s += ('0' + Math.floor(Math.random() * 256).toString(16)).slice(-2);
            }
            return s;
        }

        function nbSid() {
            const cs = 'abcdefghijklmnopqrstuvwxyz0123456789';
            const a = new Uint32Array(32);
            try { (window.crypto || window.msCrypto).getRandomValues(a); } catch (e) {
                for (let i = 0; i < 32; i++) a[i] = Math.floor(Math.random() * 4294967296);
            }
            let s = '';
            for (let i = 0; i < 32; i++) s += cs[a[i] % cs.length];
            return s;
        }

        function nbRot() {
            const dom = ';domain=.' + nbHost() + ';path=/;max-age=31536000';
            try { document.cookie = 'PHPSESSID=;path=/;max-age=0'; } catch (e) {}
            try { document.cookie = 'avs_id=;path=/;max-age=0'; } catch (e) {}
            try { document.cookie = 'PHPSESSID=' + nbSid() + dom; } catch (e) {}
            try { document.cookie = 'avs_id=' + nbRndHex(32) + dom; } catch (e) {}
            console.log('[wi3-NUBIL] preview quota rotated on', nbHost());
        }

        function nbReloadGuarded() {
            let last = 0;
            try { last = parseInt(sessionStorage.getItem('wi3NbRot') || '0', 10) || 0; } catch (e) {}
            if (Date.now() - last < 300000) return false;
            try { sessionStorage.setItem('wi3NbRot', String(Date.now())); } catch (e) {}
            nbRot();
            setTimeout(function () { location.reload(); }, 400);
            return true;
        }

        function nbBest() {
            let live = null;
            const vs = document.querySelectorAll('video');
            for (let i = 0; i < vs.length; i++) {
                const s = vs[i].currentSrc || vs[i].getAttribute('src') || '';
                if (s && NB_TRAIL.test(s)) { live = vs[i]; break; }
            }
            if (live) {
                const cands = [];
                const ss = live.querySelectorAll('source');
                for (let i = 0; i < ss.length; i++) {
                    const u = ss[i].getAttribute('src') || '';
                    if (u && NB_TRAIL.test(u)) cands.push(u);
                }
                const cu = live.currentSrc || live.getAttribute('src') || '';
                if (cu && NB_TRAIL.test(cu)) cands.push(cu);
                let bw = -1, bp = '';
                for (let i = 0; i < cands.length; i++) {
                    const wm = cands[i].match(/_(\d+)\.mp4/i);
                    const w = wm ? parseInt(wm[1], 10) : 0;
                    if (w >= bw) { bw = w; bp = cands[i]; }
                }
                if (bp) return { src: bp, node: live };
            }
            const slug = (location.pathname.split('/').pop() || '').replace(/-/g, '_').toLowerCase();
            const html = document.documentElement.innerHTML || '';
            const re = /https?:\/\/[^"'\s\\<>]+?_trailer_(\d+)\.mp4[^"'\s\\<>]*/g;
            let mm, hw = -1, hp = '';
            while ((mm = re.exec(html)) !== null) {
                const u = mm[0].replace(/&amp;/g, '&');
                if (slug.length > 4 && u.toLowerCase().indexOf(slug) < 0) continue;
                const w = parseInt(mm[1], 10);
                if (w >= hw) { hw = w; hp = u; }
            }
            if (hp) return { src: hp, node: live };
            const og = document.querySelector('meta[property="og:video:secure_url"],meta[property="og:video"]');
            const ou = og && (og.getAttribute('content') || '');
            if (ou && NB_TRAIL.test(ou)) return { src: ou, node: live };
            return null;
        }

        function nbStatus(t) {
            const s = nDiv && nDiv.querySelector('.wi3-status');
            if (s) s.textContent = t;
        }

        function nbSweep() {
            const vs = document.querySelectorAll('video');
            for (let i = 0; i < vs.length; i++) {
                const v = vs[i];
                if (nVid && v === nVid) continue;
                const s = v.currentSrc || v.getAttribute('src') || '';
                if (!s || !NB_TRAIL.test(s)) continue;
                try { v.pause(); } catch (e) {}
                try { v.muted = true; } catch (e) {}
                try { v.style.setProperty('visibility', 'hidden', 'important'); } catch (e) {}
            }
        }

        function nbAge() {
            try { document.cookie = '18-plus-modal=hidden;path=/;max-age=31536000'; } catch (e) {}
            const m = document.querySelector('.eighteen-plus-modal.show');
            if (m) { try { m.style.setProperty('display', 'none', 'important'); } catch (e) {} }
        }

        function nbInject() {
            const b = nbBest();
            if (!b) {
                const body = document.body ? document.body.innerText : '';
                const walled = /Preview\(s\)\s*Remaining/.test(body) || /0 Preview/.test(body);
                if (walled || !document.querySelector('video')) nbReloadGuarded();
                return false;
            }
            const src = b.src;
            if (nDiv && document.contains(nDiv)) {
                if (nVid && nVid.getAttribute('src') !== src) {
                    try { nVid.src = src; } catch (e) {}
                    nbStatus('new scene trailer ready');
                }
                nbSweep();
                return true;
            }
            nDiv = null; nVid = null;
            document.querySelectorAll('#wi3Player').forEach(function (el) { el.remove(); });
            try {
                GM_addStyle(
                    '#wi3Player{position:relative;width:100%;box-sizing:border-box;z-index:999999;background:#000;border:1px solid #333;' +
                    'border-radius:6px;overflow:hidden;margin:8px 0;}' +
                    '#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
                    'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
                    '#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
                    '#wi3Player .wi3-status{color:#aaa;}' +
                    '#wi3TrVid{display:block;width:100%;background:#000;max-height:70vh;}' +
                    '#wi3Player .wi3-note{padding:6px 10px;background:#0b0b0b;color:#8ab4f8;' +
                    'font:12px/1.5 sans-serif;border-top:1px solid #222;}'
                );
            } catch (e) {}
            const box = (b.node && b.node.parentElement) ||
                document.querySelector('.watch-page-video-wrapper,.watch-page-video-container,#player,.video-player') || null;
            const anchor = (box && box.parentElement) || document.body;
            nDiv = document.createElement('div');
            nDiv.id = 'wi3Player';
            nDiv.innerHTML =
                '<div class="wi3-top"><span class="wi3-badge">wi3 &middot; ' + nbHost() + '</span>' +
                '<span class="wi3-status">loading trailer...</span></div>' +
                '<video id="wi3TrVid" controls playsinline></video>' +
                '<div class="wi3-note">free guest trailer - preview quota auto-reset - the full scene is ' + nbHost() +
                ' membership-gated (wi3 keeps the trailer playable)</div>';
            anchor.insertBefore(nDiv, box || null);
            nVid = nDiv.querySelector('video');
            nVid.addEventListener('loadedmetadata', function () {
                nbStatus('public trailer ready - ' + Math.round(nVid.duration || 0) + 's @' +
                    (nVid.videoWidth || '?') + 'p');
            });
            nVid.addEventListener('error', function () {
                nbStatus('trailer url expired - rotating preview quota...');
                if (nbReloadGuarded()) return;
                const b2 = nbBest();
                if (b2 && b2.src !== nVid.getAttribute('src')) { try { nVid.src = b2.src; } catch (e) {} }
            });
            nVid.src = src;
            nbSweep();
            console.log('[wi3-NUBIL] trailer adopted on', location.hostname, '|', src.slice(0, 110));
            return true;
        }

        (function nbBoot() {
            if (!document.body) { setTimeout(nbBoot, 20); return; }
            nbAge();
            nbInject();
            if (!nTimer) {
                nTimer = setInterval(function () {
                    try {
                        if (nDiv && !document.contains(nDiv)) { nDiv = null; nVid = null; }
                        nbAge();
                        if (nbInject()) {
                            nMiss = 0;
                        } else {
                            nMiss++;
                            if (nDiv && nMiss >= 5) {
                                try { nDiv.remove(); } catch (e) {}
                                nDiv = null; nVid = null;
                            }
                        }
                    } catch (e) {}
                }, 600);
            }
        })();


    } else if (_wi3Site === 't2gate') {
        // ============================================================
        // AYLO P1 honest guest gate -- v1.6.0 covers the whole family:
        // brazzers + mofos + realitykings + bangbros (PH Premium sits
        // in the 'php' bypass branch).
        // Brazzers verdict (bz1-bz7, 08/10): P1 tour instance
        // ZZ_PC_Tour_Gateway_Consolidation_CTRL (id358751),
        // playback.scene=false, trailer.isEnabled=false (maxViews3),
        // getVideoTrailerSrc omits videos.full.files for guests
        // (contentUrl=null); Play button = fake play -> join modal
        // (2Day_Trial assets = card trial, PII).
        // Family verdict (08/10, mofos/rk/bangbros scene pages hit the
        // live PlayerBlock site-api releases/<id>?blockName=PlayerBlock
        // directly): every one answers "videos":[],"groups":[],
        // "canPlay":true with a type:"scene" metadata child plus a
        // mediabook slideshow mp4 only -- no m3u8, no scene mp4, no
        // trailer child for guests. canPlay:true + mounted chrome IS
        // the trap (join modal).
        // Honest gate: watch12s for any guest stream the page reveals
        // (future free path), take it over when it appears; otherwise
        // leave the page untouched and log the pending status -- same
        // discipline as the CC gate.
        // ============================================================
        const T2_ADS = /static-landing-assets|P1TOUR|Expired_MA|images-assets-ht|topads|player\.mp4|\/ads?\/|mediabook|preview/i;
        let tDiv = null, tVideo = null, tHls = null;

        function t2StreamEl() {
            const vs = document.querySelectorAll('video, source');
            for (let i = 0; i < vs.length; i++) {
                const s = vs[i].currentSrc || vs[i].getAttribute('src') || '';
                if (s && /\.(mp4|m3u8)/i.test(s) && !T2_ADS.test(s)) return vs[i];
            }
            return null;
        }

        function t2Status(t) {
            const s = tDiv && tDiv.querySelector('.wi3-status');
            if (s) s.textContent = t;
        }

        function t2Takeover(el) {
            try {
                GM_addStyle(
                    '#wi3Player{position:relative;z-index:999999;background:#000;border:1px solid #333;' +
                    'border-radius:6px;overflow:hidden;margin:8px 0;}' +
                    '#wi3Player .wi3-top{display:flex;justify-content:space-between;align-items:center;' +
                    'padding:6px 10px;background:#111;font:12px/1.4 sans-serif;}' +
                    '#wi3Player .wi3-badge{color:#5eead4;font-weight:bold;}' +
                    '#wi3Player .wi3-status{color:#aaa;}' +
                    '#wi3T2Vid{display:block;width:100%;background:#000;max-height:70vh;}'
                );
            } catch (e) {}
            const anchor = el.parentElement || document.body;
            document.querySelectorAll('#wi3Player').forEach(function (x) { if (x !== tDiv) x.remove(); });
            tDiv = document.createElement('div');
            tDiv.id = 'wi3Player';
            tDiv.innerHTML =
                '<div class="wi3-top"><span class="wi3-badge">wi3 &middot; guest stream</span>' +
                '<span class="wi3-status">connecting...</span></div>' +
                '<video id="wi3T2Vid" controls playsinline></video>';
            anchor.insertBefore(tDiv, anchor.firstChild || null);
            tVideo = tDiv.querySelector('video');
            const s = el.currentSrc || el.getAttribute('src') || '';
            // hide the site copy so only ours makes sound
            try { if (el.pause) el.pause(); el.style.setProperty('display', 'none', 'important'); } catch (e) {}
            if (/\.mp4/i.test(s)) {
                tVideo.src = s;
                t2Status('guest mp4 ready');
            } else if (/\.m3u8/i.test(s) && typeof Hls !== 'undefined' && Hls.isSupported()) {
                tHls = new Hls({ startLevel: 0 });
                tHls.on(Hls.Events.MANIFEST_PARSED, function () { t2Status('guest stream ready'); });
                tHls.on(Hls.Events.ERROR, function (e2, d) { if (d && d.fatal) t2Status('error: ' + (d.details || 'fatal')); });
                tHls.loadSource(s);
                tHls.attachMedia(tVideo);
            } else {
                try { tVideo.src = s; t2Status('playing natively'); } catch (e) {}
            }
            console.log('[wi3-T2] guest stream surfaced on', location.hostname, '- takeover');
        }

        function t2Boot() {
            if (!document.body) { setTimeout(t2Boot, 20); return; }
            const t0 = Date.now();
            let done = false;
            const tick = function () {
                if (done) return;
                const el = t2StreamEl();
                if (el) { done = true; t2Takeover(el); return; }
                if (Date.now() - t0 > 12000) {
                    done = true;
                    console.log('[wi3-T2] ' + location.hostname + ': guest wall confirmed (no free stream serves)' +
                        ' - honest gate, page untouched | free-unlock status pending');
                    return;
                }
                setTimeout(tick, 500);
            };
            tick();
        }
        t2Boot();

    }
})();