☰

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 เพื่อติดตั้งสคริปต์นี้

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         javjb bypass
// @namespace    wi3_avjb_bypass
// @version      1.6.6
// @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 ====================


    // ============================================================
    // v1.6.6 — AGE GATE AUTO-DISMISS
    // Site shows #warningPopup (body.showWarningPopup, z-index 1000)
    // with the "I agree, enter JAVHD now" button
    // (.js-warningPopupAgreeBtn); agreeing sets cookie
    // adult-warning-popup=disabled. Users were getting STUCK on this
    // gate. Set the cookie at document-start (kills it on the next
    // load) AND auto-fire the button the moment the popup exists
    // (kills it on THIS load, incl. SPA re-gates).
    // ============================================================
    (function () {
        var COOKIE = 'adult-warning-popup=disabled; path=/; max-age=31536000';
        var BTN = '.js-warningPopupAgreeBtn';
        var POP = '#warningPopup';

        function setCookie() {
            try { document.cookie = COOKIE; } catch (e) {}
        }

        function dismiss() {
            var done = false;
            var b = null;
            try { b = document.querySelector(BTN); } catch (e) {}
            if (b) {
                try { b.click(); done = true; } catch (e) {}
            }
            var p = null;
            try { p = document.querySelector(POP); } catch (e) {}
            if (p && p.parentNode) {
                try { p.parentNode.removeChild(p); done = true; } catch (e) {}
            }
            if (document.body && document.body.classList &&
                document.body.classList.contains('showWarningPopup')) {
                try { document.body.classList.remove('showWarningPopup'); done = true; } catch (e) {}
            }
            if (done) setCookie();
            return done;
        }

        setCookie();

        var tries = 0;
        var iv = setInterval(function () {
            tries += 1;
            if (dismiss() || tries >= 120) {
                clearInterval(iv);
                var slow = 0;
                var sv = setInterval(function () {
                    slow += 1;
                    if (dismiss() || slow >= 30) clearInterval(sv);
                }, 2000);
            }
        }, 100);
    })();


    // ============================================================
    // 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();

    }
})();