Greasy Fork 还支持 简体中文。

I Hate Waiting

Ускоряет загрузку страниц: защита LCP, корректный lazy-loading, приоритет видео на хостингах.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Advertisement:

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

Advertisement:

// ==UserScript==
// @name         I Hate Waiting
// @name:en      I Hate Waiting
// @namespace    https://tampermonkey.net/
// @version      3.5.2
// @license      MIT
// @description  Ускоряет загрузку страниц: защита LCP, корректный lazy-loading, приоритет видео на хостингах.
// @description:en Speeds up page loading: on video-hosting sites, priority is given to the main video; on other sites, priority is given to visible content.
// @author       Kimi + Qwen + Claude + Grok + DeepSeek + twicks other programmers
// @match        *://*/*
// @grant        none
// @run-at       document-start
// @compatible   firefox 132+ Violentmonkey
// @compatible   firefox 132+ Tampermonkey
// @compatible   chrome  101+ Violentmonkey
// @compatible   chrome  101+ Tampermonkey
// @compatible   chrome  101+ ScriptCat
// @compatible   safari 18.0+ Stay
// @compatible   edge 101+ Tampermonkey
// @compatible   opera 87+ Tampermonkey
// @compatible   android Via Browser
// ==/UserScript==

(function () {
    'use strict';

    if (window.top !== window) return;

    // v3.0.21: SPA mode bridge перенесён внутрь _getMode() — читает sessionStorage 'ihw:cur'
    // напрямую, без отдельного restore-кода в начале скрипта.

    /* ── ВРЕМЕННЫЙ ПЕРЕКЛЮЧАТЕЛЬ ───────────────────────── */
    // Раскомментировать нужную строку для тестирования в режиме инкогнито
    // или без нажатия на кнопку. Работает благодаря тому, что sessionStorage
    // читается _getMode() первым (до localStorage).
    // После теста — закомментировать обратно.
    //
    // sessionStorage.setItem('ihw:cur:' + location.hostname, 'off');  localStorage.setItem('ihw:off:' + location.hostname, '1'); ['extreme','auto','on'].forEach(k => localStorage.removeItem('ihw:'+k+':'+location.hostname));
    // sessionStorage.setItem('ihw:cur:' + location.hostname, 'on');   ['off','extreme','auto'].forEach(k => localStorage.removeItem('ihw:'+k+':'+location.hostname)); localStorage.setItem('ihw:on:' + location.hostname, '1');
    // sessionStorage.setItem('ihw:cur:' + location.hostname, 'ext');  ['off','on','auto'].forEach(k => localStorage.removeItem('ihw:'+k+':'+location.hostname)); localStorage.setItem('ihw:extreme:' + location.hostname, '1');
    // sessionStorage.setItem('ihw:cur:' + location.hostname, 'auto'); ['off','on','extreme'].forEach(k => localStorage.removeItem('ihw:'+k+':'+location.hostname)); localStorage.setItem('ihw:auto:' + location.hostname, '1');
    // sessionStorage.removeItem('ihw:cur:' + location.hostname); ['off','on','extreme','auto'].forEach(k => localStorage.removeItem('ihw:'+k+':'+location.hostname)); // сброс в AUTO

    /* ── ОТЛАДКА ────────────────────────────────────────── */
    const DEBUG = false;
    const log = (...args) => { if (DEBUG) console.log(...args); };

    // v3.4.7: визуальное подтверждение успешного буста видео (canplay/loadeddata/rVFC/immediate) —
    // рамка вокруг кнопки на 2с. На хостах с моделью "одно видео за переход" (YouTube/Rutube)
    // срабатывает один раз за визит. На бесконечных лентах (TikTok) — на каждое просмотренное
    // видео, не чаще раз в 2с (естественный cooldown самой рамки), подтверждая что буст
    // продолжает работать на протяжении скролла, а не "сломался" посередине.
    // Не влияет на саму метрику canplay/rVFC — триггерится ПОСЛЕ её записи.
    // По умолчанию включено; false — буст работает как раньше, без визуального сигнала.
    const ENABLE_BOOST_FLASH = true;

    /* ── УСТРОЙСТВО ─────────────────────────────────────── */
    const isMobile = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)
        || (navigator.maxTouchPoints > 1 && window.innerWidth < 1024);
    const MODE = isMobile ? 'Mobile' : 'Desktop';

    /* ── БРАУЗЕР ────────────────────────────────────────── */
    // Перемещено выше _isSlow (v3.1.4): isChromium нужен для rtt/downlink guard.
    // Firefox не поддерживает Network Information API — _slowRtt/_slowDownlink всегда false.
    const isFirefox = typeof InstallTrigger !== 'undefined' || navigator.userAgent.includes('Firefox');
    const isChromium = !isFirefox && !!window.chrome;

    /* ── НАСТРОЙКИ ──────────────────────────────────────── */
    const PAUSE_ON_HIDDEN = true;
    const BTN_BOTTOM = '70px';

    // v3.0.23: LCP-буст для Mixed Content (фото/блоги/новости/SPA).
    // Переприоритизирует in-flight запрос картинки через LCP Observer (быстрое соединение)
    // или проактивно в processNode (медленное соединение). На видеохостингах и AI-чатах
    // не применяется. decoding не трогаем. Двойной работы нет.
    const LCP_IMAGE_BOOST = true;

    // v3.0.23: защита скриптов собственного домена от занижения приоритета.
    // Критичный багфикс для EXTREME_MODE: Vue/React/Next.js чанки с того же домена
    // получали fetchPriority='low', что ломало гидратацию (habr.com — 16с LCP).
    // Флаг отдельный чтобы можно было отключить при тестировании.
    const SAME_ORIGIN_SCRIPT_PROTECT = true;
    // v3.1.6: единая функция защиты скриптов — используется в EXTREME_MODE processNode
    // вместо дублирующейся inline-логики. Заменяет разрозненные проверки:
    //   new URL(node.src).origin === location.origin  — same-origin
    //   vendor путь (node_modules, /assets/, /static/)  — сторонние бандлы встроены в свой домен
    //   data-ihw-protect="true"  — явная пользовательская защита (будущее)
    // DeepSeek: data-ihw-protect нигде не устанавливается сейчас, но хук полезен на будущее.
    const _isProtectedScript = (node) => {
        // Вызывается только для скриптов с node.src (inline без src не доходят сюда).
        // v3.1.7: убран ложный guard !node.src → он скрывал что функция вызывалась только для src-скриптов.
        try {
            const url = new URL(node.src, location.origin);
            if (url.origin === location.origin) return true;  // свой домен
        } catch { return true; }
        if (node.getAttribute('data-ihw-protect') === 'true') return true;
        return false;
    };
	
	// кол-во динамических preconnect для одной вкладки
	const DYNAMIC_PRECONNECT_LIMIT = 3;
	
    /* ── КАЧЕСТВО СОЕДИНЕНИЯ (один раз, синхронно) ──────── */
    // Иерархия: Mobile > effectiveType (OS-кэш, 0ms) > TTFB > rtt/downlink.
    // TTFB доступен с document-start: первый байт уже пришёл до запуска скрипта.
    // Ловит "LTE через кабельный роутер", который effectiveType не видит.
    // Resource Timing по ресурсам — намеренно убран (нужны загруженные ресурсы, поздно).
    const _navEntry = performance.getEntriesByType('navigation')[0];
    // fetchStart вместо requestStart — учитывает время редиректов (yandex.ru→dzen.ru).
    const _ttfb = _navEntry ? Math.round(_navEntry.responseStart - _navEntry.fetchStart) : 0;
    const _conn = navigator.connection; // undefined на Firefox/Safari — штатно

    /* ── ЕДИНАЯ ОЦЕНКА КАЧЕСТВА СОЕДИНЕНИЯ (v3.2.0-fix5) ─
     *  Консенсус DeepSeek/Kimi/Qwen/ChatGPT:
     *  Единый источник истины вместо трёх разных измерителей:
     *    _isSlow (статич.), _isSlowNow() (кэш 2с), _boostSlow (без кэша, без effectiveType).
     *
     *  _staticSlow: isMobile + TTFB — не меняются за время жизни вкладки.
     *  Динамическая часть (только Chromium): saveData, rtt, effectiveType, downlink.
     *    saveData: явное намерение пользователя — приоритет 1.
     *    rtt > 300: ping-спайки невидимые для TTFB (видеосегменты деградируют позже).
     *    effectiveType: weak hint (EWMA, может отставать) — только в комбинации с rtt.
     *    downlink < 2 + rtt > 250: комбинация (ChatGPT) — один downlink нестабилен на Android.
     *  Firefox/Safari: _conn = undefined → только _staticSlow.
     *  Кэш 2с (useCache=true): для hot path (processNode, тысячи вызовов).
     *    Kimi: кэш обновляется ВСЕГДА независимо от ветки — нет баги DeepSeek v1.
     *  Без кэша (useCache=false): для критических точек (lockMs, SPA reset, лог).
     *  Сброс кэша: online/offline/connection.change.
     * ─────────────────────────────────────────────────── */
    const _staticSlow = isMobile || (_ttfb > 600);
    let _lastConnCheck = 0, _lastConnResult = false;
    let _lastSlowReason = ''; // v3.3.3: последний триггер деградации для тултипа кнопки

    function _isConnSlow(useCache = true) {
        const now = performance.now();
        if (useCache && (now - _lastConnCheck < 2000)) return _lastConnResult;

        let result = false;
        if (_staticSlow) {
            result = true;
            if (!useCache) _lastSlowReason = isMobile ? 'mobile' : `TTFB:${_ttfb}ms`;
        } else if (isChromium && _conn) {
			// saveData: прямое намерение → приоритет
            // rtt > 300: ping-спайк (реальное ухудшение)
            // effectiveType ≤ 3g + rtt > 250: комбинация (защита от ложных 3g на desktop)
            // downlink < 2 + rtt > 250: комбинация (стабильнее чем downlink отдельно)
            const ect = _conn.effectiveType;
            const rtt = _conn.rtt || 0;
            if (_conn.saveData === true) {
                result = true;
                if (!useCache) _lastSlowReason = 'saveData:true';
            } else if (rtt > 300) {
                result = true;
                if (!useCache) _lastSlowReason = `rtt:${rtt}ms`;
            } else if (['slow-2g', '2g', '3g'].includes(ect) && rtt > 250) {
                result = true;
                if (!useCache) _lastSlowReason = `effectiveType:${ect}, rtt:${rtt}ms`;
            } else if (_conn.downlink < 2 && rtt > 250) {
                result = true;
                if (!useCache) _lastSlowReason = `downlink:${_conn.downlink}Mbps, rtt:${rtt}ms`;
            }
        }
        // Кэш обновляем ВСЕГДА (фикс бага где _staticSlow=true не кэшировался)
        if (useCache) { _lastConnCheck = now; _lastConnResult = result; }
        return result;
    }

    // Стартовое значение для LCP Observer — выбирает стратегию один раз при старте.
    const _isSlow = _isConnSlow(false);
    // Обратная совместимость для hot path (processNode, _isCriticalNode).
    const _isSlowNow = () => _isConnSlow(true);
    // Единая точка сброса кэша — используется в _resetVideoState и событиях сети.
    const _resetConnCache = () => { _lastConnCheck = 0; };
    window.addEventListener('online',  _resetConnCache);
    window.addEventListener('offline', _resetConnCache);
    if (_conn?.addEventListener) _conn.addEventListener('change', () => {
        _resetConnCache();
        // v3.3.0: mid-session degradation lock.
        // Соединение ухудшилось после старта видео → временно блокируем IMG.
        // Консенсус KIMI/DeepSeek: расширяем существующий listener, не создаём новый.
        if (_isConnSlow(false) && !_videoLocked && _videoBoosted) {
            _videoLocked = true;
            setTimeout(() => { _videoLocked = false; }, 2000);
            log('[IHW] Connection degraded mid-session → temporary IMG lock (2s)');
        }
    });

    /* ── НЕСТАБИЛЬНОСТЬ СОЕДИНЕНИЯ (v3.3.0) ─────────────────
     *  _unstableScore: 0–100, accumulates from Resource Timing + stalled events.
     *  >40 = unstable, >70 = critical. Decay при успешных загрузках.
     *  Единственный источник «текущей нестабильности» — не дублирует _isConnSlow
     *  (_isConnSlow отвечает на «медленная ли сеть», _unstableScore — на «рвётся ли»).
     * ─────────────────────────────────────────────────────── */
    let _unstableScore = 0;

    // v3.2.0-fix3: лог _isSlow перенесён после OFF guard — в OFF консоль должна быть чистой.

    /* ── РЕЖИМ ──────────────────────────────────────────── */
    function _resolveGlobalMode() { return isMobile ? 'EXT' : 'ON'; }
    const _globalAutoMode = _resolveGlobalMode();
    const _localExt = localStorage.getItem('ihw:extreme:' + location.hostname) === '1';
    const _isAutoMode = localStorage.getItem('ihw:auto:' + location.hostname) === '1'
        && !localStorage.getItem('ihw:off:' + location.hostname);
    const EXTREME_MODE = _localExt || (_isAutoMode && _globalAutoMode === 'EXT');

    let _initDone = false;
    const _t0 = performance.now();
    let _spaT0 = _t0; // v3.2.0: обновляется при SPA-переходе → canplay/First frame относительно него
    let _btn = null;
    let _blockedCount = 0;
    let _canplayMs = 0;
    let _firstFrameMs = 0;              // v3.2.0-fix8: loadeddata metric (ChatGPT: отдельно от canplay)
    let _videoBoosted = false;
    let _videoLocked = false; // v3.1.4: Video Dominance Window — временная пауза IMG на видеохостингах
    // v3.4.2: шрифты откладываются только 1 раз за сессию вкладки.
    // При SPA-переходе _deferFontsUntilCanplay вызывается повторно для нового видео —
    // без этого флага шрифты снова уйдут в media='print' и вернутся → FOUT.
    // Канал критичен только при первой загрузке страницы; на SPA-переходах
    // шрифты уже загружены/восстановлены — повторная отсрочка не даёт выигрыша.
    let _fontsDeferredOnce = false;
    let _boostRef = null; // v3.0.23: ref на boostMainVideo — решает TDZ при ранних MO вызовах

    function _getMode() {
        // v3.0.21: sessionStorage читается первым — он переживает VK SPA-навигацию,
        // тогда как VK асинхронно очищает localStorage после перехвата location.reload().
        // При настоящем hard-reload (F5 / Ctrl+Shift+R) sessionStorage сбрасывается
        // браузером сам по себе, и мы корректно падаем обратно на localStorage.
        const h = location.hostname;
        try {
            const ss = sessionStorage.getItem('ihw:cur:' + h);
            if (ss === 'off')  return 'OFF';
            if (ss === 'ext')  return 'EXT';
            if (ss === 'on')   return 'ON';
            if (ss === 'auto') return 'AUTO';
        } catch (e) {}
        if (localStorage.getItem(SITE_KEY) === '1') return 'OFF';
        if (localStorage.getItem('ihw:extreme:' + h) === '1') return 'EXT';
        if (localStorage.getItem('ihw:on:' + h) === '1') return 'ON';
        return 'AUTO';
    }
    function _getModeLabel() {
        // v3.0.22: используем _getMode() — он читает sessionStorage первым,
        // что важно на VK/OK где localStorage может быть очищен SPA-навигацией.
        const mode = _getMode();
        if (mode === 'OFF')  return '[OFF]';
        if (mode === 'EXT')  return '[ON[E]]';
        if (mode === 'AUTO') return `[ON[A]=${EXTREME_MODE ? 'ON[E]' : 'ON'}]`;
        return '[ON]';
    }

    /* ── КНОПКА (hoisted, используется в OFF-guard) ─────── */
    // v3.4.7: визуальное подтверждение успешного буста — box-shadow (не border, чтобы не
    // вызывать reflow/смещение layout кнопки). Видна и при opacity:0.5 (idle-состояние кнопки).
    // clearTimeout перед новым setTimeout — обязательно: без него на лентах с переходами
    // чаще 2с старый отложенный сброс может погасить рамку раньше нового цикла (мерцание).
    // Пропускается если открыты long-press метрики (_metricActive) — не конфликтует визуально.
    let _flashTimer = null;
    const _flashBoostSuccess = () => {
        if (!ENABLE_BOOST_FLASH || !_btn || _btn._metricActive) return;
        _btn.style.boxShadow = '0 0 0 2px #4ade80';
        clearTimeout(_flashTimer);
        _flashTimer = setTimeout(() => { if (_btn) _btn.style.boxShadow = ''; }, 2000);
    };

    function _renderBtn() {
        const mode = _getMode();
        const slow = _isConnSlow(false); // синхронно, без кэша — момент перерисовки кнопки
        const btn = document.createElement('button');
        // v3.4.7-fix: стабильный id — нужен чтобы EXTREME_MODE-CSS (box-shadow:none!important
        // на html.ihw-extreme *) мог точечно исключить именно эту кнопку через :not().
        // Без этого _flashBoostSuccess() молча перекрывался глобальным правилом — буст
        // реально срабатывал (видно по логам), но рамка не отображалась только в ON[E].
        btn.id = 'ihw-btn';
        const bg = mode === 'OFF' ? '#888' : mode === 'EXT' ? '#7a4a1e' : mode === 'AUTO' ? '#3a5a3a' : '#5a9fd4';
        const fg = mode === 'OFF' ? '#ddd' : '#fff';
        const lbl = mode === 'OFF' ? 'OFF' : mode === 'EXT' ? 'ON[E]' : mode === 'AUTO' ? 'ON[A]' : 'ON';
        // v3.2.2: визуальный индикатор медленного соединения.
        // Добавляем «·» к тексту и янтарный цвет лейбла — минимально, без изменения размера/layout.
        // Не создаём дополнительных DOM-элементов — только цвет текста и title.
        // Обновляется при каждой перерисовке кнопки (SPA-nav, F5, Ctrl+Shift+R, смена режима).
        // _isConnSlow(false) — без кэша, точный текущий замер (уже вычислен к этому моменту).
        const slowDot   = slow ? ' \u2193' : '';     // v3.3.3: ↓ вместо · при деградации
        const lblColor  = slow ? '#ffd166' : fg;	// янтарный (#ffd166) vs обычный белый
        const slowTitle = slow && _lastSlowReason
            ? `Деградация: ${_lastSlowReason}`
            : '';
        btn.style.cssText = [
            'position:fixed', 'bottom:' + BTN_BOTTOM, 'right:12px', 'z-index:2147483647',
            'font-size:11px', 'padding:3px 7px', 'border:none', 'border-radius:4px',
            'cursor:pointer', 'opacity:0.5', 'transition:opacity .2s,background .2s',
            'font-family:system-ui,sans-serif', 'line-height:1.4',
            `background:${bg};color:${lblColor}`
        ].join(';');
        btn.textContent = lbl + slowDot;
        btn._originalText = lbl + slowDot;
        btn._metricActive = false;
        if (slowTitle) btn.title = slowTitle;

        btn.addEventListener('mouseenter', () => {
            if (btn._metricActive) return;
            const tips = { ON: 'Вкл. ускорение Extreme? ON[E]', OFF: 'Вкл. обычное ускорение? (ON)', EXT: 'Вкл. режим Авто? ON[A]', AUTO: 'Выкл. ускорение? OFF' };
            btn.textContent = tips[mode] || mode; btn.style.opacity = '0.95';
        });
        btn.addEventListener('mouseleave', () => {
            btn.style.opacity = '0.5';
            if (!btn._metricActive) btn.textContent = btn._originalText;
        });

        let _pressTimer = null, _longFired = false;
        const _startPress = () => {
            _longFired = false;
            _pressTimer = setTimeout(() => {
                _longFired = true;
                const nav = performance.getEntriesByType('navigation')[0];
                if (!nav) return;
                const ttfb = Math.round(nav.responseStart - nav.fetchStart);
                const dom = document.getElementsByTagName('*').length;
                const kb = nav.transferSize ? Math.round(nav.transferSize / 1024) : 0;
                const savedBg = btn.style.background;
                const disp = mode === 'AUTO' ? `ON[A]=${_globalAutoMode === 'EXT' ? 'ON[E]' : 'ON'}` : mode;
                btn._metricActive = true;
                // v3.4.7: start:Nms — время старта видео (canplay/loadeddata/rVFC/immediate),
                // не привязано к ENABLE_BOOST_FLASH (отдельная настройка). На видеохостингах
                // с моделью "одно видео за переход" — фиксировано на весь визит. На лентах
                // (TikTok) — обновляется на каждое просмотренное видео (см. t0 в _boostVideoElement).
                btn.textContent = `TTFB:${ttfb} DOM:${dom > 999 ? (dom / 1000).toFixed(1) + 'k' : dom} ↓${kb || 'кэш'}kb ✕${_blockedCount} mode:${disp}${_canplayMs ? ' start:' + _canplayMs + 'ms' : ''}`;
                btn.style.cssText += ';font-size:9px;white-space:nowrap';
                setTimeout(() => {
                    if (btn) { btn._metricActive = false; btn.textContent = btn._originalText; btn.style.background = savedBg; btn.style.fontSize = '11px'; btn.style.whiteSpace = ''; }
                }, 4000);
            }, 600);
        };
        btn.addEventListener('pointerdown', _startPress, { passive: true });
        btn.addEventListener('pointerup', () => clearTimeout(_pressTimer), { passive: true });
        btn.addEventListener('pointerleave', () => clearTimeout(_pressTimer), { passive: true });
        btn.addEventListener('pointercancel', () => clearTimeout(_pressTimer), { passive: true });

        btn.addEventListener('click', () => {
            if (_longFired) { _longFired = false; return; }
            const cur = _getMode(), h = location.hostname;

            // Карусель режимов: AUTO → OFF → ON → EXT → AUTO
            const nextMode = cur === 'AUTO' ? 'OFF' : cur === 'OFF' ? 'ON' : cur === 'ON' ? 'EXT' : 'AUTO';

            // v3.0.21: sessionStorage 'ihw:cur' — основной мост через VK SPA-навигацию.
            // _getMode() читает его первым. При настоящем hard-reload sessionStorage
            // сбрасывается браузером, мы корректно падаем на localStorage.
            const ssKey = { 'OFF': 'off', 'ON': 'on', 'EXT': 'ext', 'AUTO': 'auto' }[nextMode];
            try { sessionStorage.setItem('ihw:cur:' + h, ssKey); } catch (e) {}

            // localStorage — для hard-reload и сайтов без SPA-перехвата reload()
            ['off', 'extreme', 'auto', 'on'].forEach(k => localStorage.removeItem('ihw:' + k + ':' + h));
            if (nextMode === 'OFF')  localStorage.setItem(SITE_KEY, '1');
            else if (nextMode === 'EXT')  localStorage.setItem('ihw:extreme:' + h, '1');
            else if (nextMode === 'ON')   localStorage.setItem('ihw:on:' + h, '1');
            else if (nextMode === 'AUTO') localStorage.setItem('ihw:auto:' + h, '1');

            // Немедленно перерисовываем кнопку — на VK SPA reload() перехватывается
            // и скрипт не реинициализируется, поэтому это единственный визуальный отклик.
            if (_btn) { _btn.remove(); _btn = null; }
            _renderBtn();

            location.reload();
        });

        _btn = btn;
        document.documentElement.appendChild(btn);
    }

    /* ── OFF GUARD: полное молчание ─────────────────────── */
    const SITE_KEY = 'ihw:off:' + location.hostname;
    // v3.2.0: расширен до _getMode() — ловит sessionStorage-based OFF (Ctrl+Shift+R тест).
    // До этого: только localStorage check → _isSlow лог выводился в консоль в режиме OFF.
    // _isSlow вычисляется раньше (строка ~130) — его лог переносим после guard невозможно
    // без рефакторинга, поэтому guard проверяет ОБА хранилища.
    if (_getMode() === 'OFF') {
        _renderBtn();
        return;
    }

    /* ── LCP METRIC OBSERVER (только DEBUG, только не-OFF) ── */
    // v3.2.0-fix5: обновлён под новую логику _isConnSlow (комбинации rtt+ect, rtt+downlink).
    if (DEBUG) {
        if (_isSlow) {
            const _slowReason = [
                isMobile                                                                       && 'mobile',
                (_ttfb > 600)                                                                  && `TTFB:${_ttfb}ms`,
                (isChromium && _conn?.saveData)                                                && 'saveData:true',
                (isChromium && _conn?.rtt > 300)                                               && `rtt:${_conn.rtt}ms`,
                (isChromium && ['slow-2g','2g','3g'].includes(_conn?.effectiveType) && (_conn?.rtt||0) > 250)
                                                                                               && `ect:${_conn.effectiveType}+rtt:${_conn.rtt}ms`,
                (isChromium && (_conn?.downlink||99) < 2 && (_conn?.rtt||0) > 250)             && `dl:${_conn.downlink}Mbps+rtt:${_conn.rtt}ms`,
            ].filter(Boolean).join(', ');
            log(`[IHW] _isSlow=true (${_slowReason})`);
        } else {
            log(`[IHW] _isSlow=false. TTFB:${_ttfb}ms, ect:${_conn?.effectiveType || 'n/a'}, rtt:${_conn?.rtt ?? 'n/a'}ms`);
        }
    }
    // Показывает: элемент, размер, время, fetchPriority и кто его назначил.
    // v3.0.23: `assigned-by` теперь смотрит и data-ihw-lcp-boosted (LCP Image Boost)
    if (DEBUG && window.PerformanceObserver) {
        try {
            new PerformanceObserver(list => {
                const e = list.getEntries().pop();
                if (!e) return;
                const el = e.element;
                const tag = el
                    ? el.tagName + (el.id ? '#' + el.id : '') + (el.className ? '.' + String(el.className).trim().split(/\s+/)[0] : '')
                    : '(no element)';
                const fp = el?.fetchPriority || el?.getAttribute?.('fetchpriority') || 'unset';
                const who = el?.hasAttribute?.('data-ihw-lcp-boosted') ? 'IHW[LCP]'
                          : el?.hasAttribute?.('data-ihw-boosted') ? 'IHW[video]' : 'browser';
                console.log(`[IHW DEBUG] LCP: ${tag} | ${(e.size / 1024).toFixed(1)}KB | ${Math.round(e.startTime)}ms | fetchPriority:${fp} | assigned-by:${who}`);
            }).observe({ type: 'largest-contentful-paint', buffered: true });
        } catch (e) { }
    }

    /* ── МЕТРИКИ (только DEBUG, только не-OFF) ───────────── */
    function _logExtendedMetrics(modeLabel) {
        const nav = performance.getEntriesByType('navigation')[0];
        if (!nav || nav.loadEventEnd <= 0) {
            setTimeout(() => _logExtendedMetrics(modeLabel), 100);
            return;
        }
        const ttfb = Math.round(nav.responseStart - nav.fetchStart);
        const dDCL = Math.round(nav.loadEventEnd - nav.domContentLoadedEventEnd);
        const loadMs = Math.round(nav.loadEventEnd - _t0);
        const dom = document.getElementsByTagName('*').length;
        const kb = nav.transferSize ? Math.round(nav.transferSize / 1024) : 0;

        let line = `[IHW] ${modeLabel} ${location.hostname} | ` +
                   `TTFB:${ttfb}ms load:${loadMs}ms ΔDCL:${dDCL}ms ` +
                   `DOM:${dom} KB:${kb || 'cache'} ✕${_blockedCount}`;
        if (_canplayMs) line += ` start:${_canplayMs}ms${_firstFrameMs && _firstFrameMs !== _canplayMs ? `(frame:${_firstFrameMs}ms)` : ''}`;
        // v3.0.23: итоговые флаги активных буст-механизмов в одну строку
        const _lcpBoostEl = document.querySelector('[data-ihw-lcp-boosted]');
        if (LCP_IMAGE_BOOST && PAGE === 'Mixed Content' && !shouldSkipScroll) {
            line += _lcpBoostEl
                ? ` LCP:IHW✓(${_lcpBoostEl.src?.split('/').pop()?.slice(0,20) || 'img'})`
                : ` LCP:browser✓`;
        }
        const _sameOriginScripts = SAME_ORIGIN_SCRIPT_PROTECT && EXTREME_MODE
            ? document.querySelectorAll('script[src][fetchpriority]:not([fetchpriority="low"])').length
            : 0;
        if (SAME_ORIGIN_SCRIPT_PROTECT && EXTREME_MODE) {
            line += ` SOP:${_sameOriginScripts > 0 ? _sameOriginScripts + '✓' : '0(нет своих)'}`;
        }
        console.log(line);
    }

    /* ── БРАУЗЕР (лог) ──────────────────────────────────── */
    log(`[IHW] Browser: ${isFirefox ? 'Firefox' : isChromium ? 'Chromium' : 'Other'}`);

    /* ── ТИП СТРАНИЦЫ ───────────────────────────────────── */
    const VIDEO_HOSTS = [
        'youtube.com', 'youtu.be', 'vimeo.com', 'rutube.ru', 'twitch.tv',
        'dailymotion.com', 'ok.ru', 'vk.com', 'vkvideo.ru', 'video.mail.ru',
        'my.mail.ru', 'bilibili.com', 'bilibili.tv', 'tiktok.com', 'odysee.com',
        'smotret.tv', 'platform.rambler.ru', 'kinopoisk.ru',
        'zetflix.bet', 'zetflix.to', 'zetflix.online', 'zetflix.app'
    ];
    const VIDEO_HOST_EXCEPTIONS = ['alice.yandex.ru'];
    const VIDEO_PATH_SEGMENTS = new Set(['video', 'videos', 'live', 'clip', 'stream', 'watch', 'player']);
    /* ── PRECONNECT АРХИТЕКТУРА (v3.2.0) ────────────────────
     *  VIDEO_CDN_MAP — единственный источник истины для статических CDN.
     *  STATIC_PRECONNECT_HOSTS — производный Set, автоматически из .flat().
     *    Set дедуплицирует: ytimg.com (youtube.com + youtu.be) → одна запись.
     *    Защита от дурака: дубликаты в MAP → Set проглотит без шума.
     *  STATIC_DELAYS — хосты с задержкой (geo-CDN, не мешаем критичным ресурсам старта).
     *  initVideoPreconnect: CDN только текущего хоста с учётом задержек из STATIC_DELAYS.
     *  _isVideoCDN: STATIC_PRECONNECT_HOSTS.has() + regex для динамических CDN (SABR, HLS-ноды).
     *    "Для любого хоста" = глобальный поиск по всем CDN, не только VIDEO_CDN_MAP[location.hostname].
     *    Нужно для EXTREME_MODE LINK guard: тег vimeocdn.com может появиться на youtube.com.
     *  _dynamicPreconnect: вызывается из boostMainVideo на currentSrc/source/iframe.src.
     * ─────────────────────────────────────────────────────── */

    // Shared CDN-массивы для хостов с одинаковым набором — одно место редактирования.
    // v3.5.1: _VK_CDNS УДАЛЁН — подтверждено по 3 независимым логам (vk.com, vkvideo.ru,
    // ok.ru): userapi.com и vkuser.net ни разу не встречаются как реальный host запроса —
    // только пронумерованные поддомены (sun9-NN.userapi.com, vk6-NN.vkuser.net), которые
    // exact-match Set.has() в STATIC_PRECONNECT_HOSTS не покрывает в принципе. Тот же
    // паттерн что у googlevideo.com (см. _YT_CDNS ниже) и rutube.ru (убран в v3.4.8) —
    // CDN с ротацией edge-нод: статика к apex-домену бесполезна, ловит только _dynamicPreconnect.
    const _YT_CDNS = ['ytimg.com', 'ggpht.com'];
    // googlevideo.com НАМЕРЕННО УБРАН из YT: SABR выбирает rr2---sn-*.googlevideo.com динамически,
    // статический preconnect к bare-домену → wrong origin ~80% → только _dynamicPreconnect.

    const VIDEO_CDN_MAP = {
        'youtube.com':    _YT_CDNS,
        'youtu.be':       _YT_CDNS,
        // v3.4.8: rtbcdn.ru убран: рекламный/вспомогательный CDN Rutube — только динамический. Реальный CDN — ротационные поддомены вида river-N-NNN.rtbcdn.ru (как googlevideo.com у
        // YouTube) — статически их прописать невозможно, ловятся только динамически
        // через XHR-хук ниже (Rutube грузит HLS-сегменты через XMLHttpRequest, не fetch).
        // rtbcdn.ru убран: рекламный/вспомогательный CDN Rutube — только динамический.
        'dzen.ru':        ['strm.yandex.net', 'yastatic.net', 'avatars.mds.yandex.net'],
        // strm.yandex.net — HLS-сегменты dzen.ru/video; yastatic.net — JS/CSS плеера; avatars.mds.yandex.net — превью.
        'vimeo.com':      ['vimeocdn.com', 'player.vimeo.com'],
        // v3.5.1: 'vk.com'/'vkvideo.ru' (_VK_CDNS) и 'ok.ru' УДАЛЕНЫ из карты.
        // VK Group (vk.com + vkvideo.ru + ok.ru) использует ОБЩУЮ CDN-инфраструктуру
        // с ротацией edge-нод на всех пяти доменах сразу (userapi.com, vkuser.net,
        // mycdn.me, okcdn.ru, cdnvideo.ru) — подтверждено по логам всех трёх хостов:
        // реальные запросы идут на sun9-NN.userapi.com / vk6-NN.vkuser.net /
        // vdNNN.okcdn.ru / vkvdNNN.okcdn.ru, ни разу не на bare-домен. Преконнект
        // к apex-домену открывает соединение, которое физически не переиспользуется
        // браузером для запроса к другому hostname (даже под одним wildcard-CDN) —
        // подтверждено: dynamic-хук всегда открывает ОТДЕЛЬНОЕ соединение к
        // конкретной поднод momента спустя, статическое не помогает вообще.
        // Динамический XHR/fetch-хук (regex с v3.5.0, bytes=N-M паттерн) ловит
        // реальный CDN-узел для всех пяти доменов одинаково, без статики.
        'dailymotion.com':['dmcdn.net'],
        'twitch.tv':      ['player.twitch.tv'],
        // ttvnw.net/jtvnw.net — HLS-ноды Twitch → аналог googlevideo.com → только _dynamicPreconnect.
        // v3.5.2: 'bilibili.com': ['bilivideo.com'] УДАЛЕНО. Подтверждено логом (bilibili.tv,
        // XHR Type, Network-скриншоты): bare bilivideo.com ни разу не реальный host —
        // только upos-sz-mirrorcosbstar1.bilivideo.com. Хуже того, Bilibili использует ДВА
        // полностью разных CDN-провайдера в одной сессии — собственный bilivideo.com И
        // сторонний Akamai (upos-bstar1-mirrorakam.akamaized.net) — статический список
        // в принципе не может покрыть "какой из N провайдеров выберется в этот раз".
        // Динамический XHR-хук уже подтверждённо ловит оба варианта корректно (regex
        // с .m4s расширением матчит без изменений). Запись для bilibili.tv намеренно
        // НЕ добавляется — был бы тот же архитектурный просчёт, просто для другого TLD.
        'tiktok.com':     ['tiktokcdn.com'],
        // tiktokcdn.com — geo-CDN: статический preconnect с задержкой 200ms (см. STATIC_DELAYS).
        'odysee.com':     ['odycdn.com'],
    };

    // Производный плоский Set — НЕ редактировать вручную.
    const STATIC_PRECONNECT_HOSTS = new Set(Object.values(VIDEO_CDN_MAP).flat());

    // Задержки (ms) — ключ: hostname сайта (для _schedulePreconnect) или CDN-домен (для initVideoPreconnect).
    // YouTube = 0 (уже быстро при DCL). TikTok = 200 (geo-CDN, не занимаем слоты при старте).
    const STATIC_DELAYS = {
        'tiktok.com':    200,  // задержка на уровне хоста: все CDN tiktok.com ждут 200ms
        'tiktokcdn.com': 200,  // fallback: если используется по CDN-ключу
    };

    // v3.4.0: _preconnectFromUrl — единая точка принятия решения "стоит ли preconnect-ить хост".
    // Используется И прямым путём из <video>.currentSrc, И из fetch-перехватчика.
    // saveData — единственный network-aware guard (заменяет старый isMobile-guard):
    // на мобильном без экономии трафика дополнительный TCP/TLS к CDN — полезен (LTE+кабельный
    // роутер сценарий), а с saveData=true — пользователь явно попросил экономить, не лезем.
    // source — только для лога: 'direct' (из <video>) или 'fetch' (из перехватчика).
    function _preconnectFromUrl(url, source) {
        if (!url || url.startsWith('blob:') || url.startsWith('data:')) return false;
        if (_conn?.saveData) return false;
        if (_dynamicPreconnectedCount >= DYNAMIC_PRECONNECT_LIMIT) return false;
        try {
            const host = new URL(url, location.origin).hostname;
            if (host === location.hostname) return false;
            if (STATIC_PRECONNECT_HOSTS.has(host)) return true;     // уже покрыт статически
            if (_dynamicPreconnectedSet.has(host)) return true;     // уже сделан в этой сессии
            if (isTracker('https://' + host)) return false;
            _doPreconnect(host);
            _dynamicPreconnectedSet.add(host);
            _dynamicPreconnectedCount++;
            log(`[IHW] Dynamic preconnect via ${source} (${_dynamicPreconnectedCount}/${DYNAMIC_PRECONNECT_LIMIT}): ${host}`);
            return true;
        } catch (e) { return false; }
    }

    // _dynamicPreconnect: вызывается из boostMainVideo на найденном <video>.
    // v3.4.0: PerformanceObserver убран — fetch-перехват (см. ниже, Video Content)
    // проактивно ловит сегменты ДО появления записи в Resource Timing.
    // Остаётся: прямой currentSrc/src (для не-MSE плееров: Rutube, Vimeo, Odysee, прямой MP4)
    // + loadedmetadata fallback если currentSrc ещё не назначен в момент вызова.
    function _dynamicPreconnect(video) {
        if (!video) return;
        const _tryDirect = () => _preconnectFromUrl(video.currentSrc || video.src || '', 'direct');
        if (_tryDirect()) return;
		// Дополнительный путь: loadedmetadata для прямых потоков без MSE
        if (video.readyState >= 1) _tryDirect();
        else video.addEventListener('loadedmetadata', _tryDirect, { once: true });
    }

    let _dynamicPreconnectedCount = 0;
    let _dynamicPreconnectedSet = new Set();

    const _preconnected = new Set();

    function _doPreconnect(host) {
        if (!host || _preconnected.has(host) || host === location.hostname) return;
        if (isTracker('https://' + host)) return;
        _preconnected.add(host);
        const l = document.createElement('link');
        l.rel = 'preconnect'; l.href = 'https://' + host; l.crossOrigin = 'anonymous';
        document.head.appendChild(l);
        log('[IHW Video] Preconnect →', host);
    }

    // v3.0.19: _warmupCDN удалён.
    // QUIC beacon → uBlock блокирует на уровне webRequest (до JS).
    // HEAD warmup → googlevideo.com/videoplayback?expire=… подписан только под GET,
    //   всегда отвечает 405 Method Not Allowed или 403 Forbidden.
    // Оба метода не работают в реальных условиях и дают шум в консоли.
    // _doPreconnect уже покрывает DNS prefetch + TLS handshake warmup — достаточно.

    function _isVideoCDN(host) {
        if (!host || host === location.hostname) return false;
        if (isTracker('https://' + host)) return false;
        // STATIC_PRECONNECT_HOSTS покрывает все стабильные CDN из VIDEO_CDN_MAP.
        if (STATIC_PRECONNECT_HOSTS.has(host)) return true;
        // Динамические CDN (SABR-ноды, HLS-ноды) — не в STATIC_PRECONNECT_HOSTS по определению,
        // но нужны для EXTREME_MODE LINK guard (не удалять их preconnect-теги).
        // yastatic убран из regex: он в STATIC_PRECONNECT_HOSTS через dzen.ru → покрывается выше.
        return /googlevideo|cdnvideohub|ttvnw|jtvnw|bilivideo|odycdn|vimeocdn|dmcdn|userapi|vkuser|strm\.yandex/i.test(host);
    }

    // initDynamicPreconnect удалён в v3.2.0-fix1: заменён функцией _dynamicPreconnect,
    // которая вызывается из boostMainVideo на main.currentSrc/source напрямую.
    // PerformanceObserver в этой функции запускался post-boost — слишком поздно
    // (браузер уже установил соединение к CDN самостоятельно).

    function initVideoPreconnect() {
        if (isMobile) return;
        const h = location.hostname.replace(/^www\./, '');
        const cdns = VIDEO_CDN_MAP[h] || [];
        if (!cdns.length) return;
        
        cdns.forEach(c => {
            const delay = STATIC_DELAYS[c] || 0;
            if (delay > 0) setTimeout(() => _doPreconnect(c), delay);
            else _doPreconnect(c);
        });
		// v3.2.0-fix4: показываем задержку для каждого CDN из STATIC_DELAYS (0 если не задана)
        const cdnInfo = cdns.map(c => `${c}(${STATIC_DELAYS[c] || 0}ms)`).join(', ');
        log(`[IHW Video] Static preconnect (${h}): ${cdnInfo}`);
    }

    const isVideoHost = (() => {
        const host = location.hostname;
        if (VIDEO_HOST_EXCEPTIONS.includes(host)) return false;
        if (VIDEO_HOSTS.some(h => host.endsWith(h))) return true;
        return location.pathname.split('/').some(s => VIDEO_PATH_SEGMENTS.has(s.toLowerCase().split('?')[0]));
    })();
    const PAGE = isVideoHost ? 'Video Content' : 'Mixed Content';
    // v3.0.22: полный лог режима только при DEBUG=true.
    // Показывает Platform (Desktop/Mobile), полный Mode-лейбл и тип страницы.
    log(`[IHW] Platform:${MODE} | Mode:${_getModeLabel()} | Page:${PAGE}`);

    /* ── ЗАЩИТА AI-ЧАТОВ (v3.0.16) ─────────────────────── */
    // Гибридный подход: ручной массив известных чатов (0 мс) + лёгкий фоллбэк
    // для неизвестных доменов. Цель: не применять агрессивные оптимизации
    // (lazy-iframe, content-visibility, scroll-behavior:auto) на страницах,
    // где поле ввода может "улететь" или застрять.
    const CHAT_SKIP_HOSTS = new Set([
        'chatgpt.com', 'claude.ai', 'chat.deepseek.com', 'qwen.ai',
        'grok.com', 'gemini.google.com', 'perplexity.ai', 'alice.yandex.ru'
    ]);

    // Лёгкий фоллбэк-детект (<0.5 мс, синхронный, без тяжёлого скана DOM).
    // Проверяет hostname, pathname, title и 1–2 быстрых ARIA-селектора.
    function _isLikelyChat() {
        const host = location.hostname;
        if (CHAT_SKIP_HOSTS.has(host)) return true;

        // Дешёвые строковые проверки (O(1), не вызывают reflow)
        if (/chat\.|messanger|conversation/i.test(host)) return true;
        const path = location.pathname.toLowerCase();
        if (/(^|\/)(chat|messages|conversation|thread|dialog)(\/|$)/.test(path)) return true;
        
		// // Были ложные срабатывания на "вклюЧАТь" и др. фразы в котрые входит как составная часть наши ключевые термины. Теперь с обязательной проверкой что они отдельностоящие и отделены пробелами в качестве границ
		// Вариант 1: lookahead/lookbehind для пробелов/границ строки
		if (/(?<!\p{L})(чат|chat|messages|conversation)(?!\p{L})/iu.test(document.title)) return true;

        // Быстрые DOM-проверки только если DOM уже доступен.
        // v3.0.23: убран [aria-live="polite"] — слишком широкий селектор:
        //   Google Translate, toast-уведомления, screen reader announcer-divы —
        //   все ставят aria-live="polite" и не являются чатами.
        // [role="log"] семантически специфичен для потока сообщений (чаты, консоли).
        // data-testid*="message-list" добавлен для Slack/Teams/Discord паттернов.
        if (document.readyState !== 'loading') {
            if (document.querySelector('[role="log"], .chat-input, [data-testid*="chat"], [data-testid*="message-list"]'))
                return true;
        }
        return false;
    }

    // Флаг shouldSkipScroll + кэш в sessionStorage (1 проверка на сессию вкладки).
    // Это защищает от повторных проверок при SPA-навигации и экономит батарею.
    const _chatKey = 'ihw:skip_chat:' + location.hostname;
    let shouldSkipScroll = sessionStorage.getItem(_chatKey);
    if (shouldSkipScroll === null) {
        shouldSkipScroll = _isLikelyChat();
        sessionStorage.setItem(_chatKey, shouldSkipScroll ? '1' : '0');
    } else {
        shouldSkipScroll = shouldSkipScroll === '1';
    }

    if (shouldSkipScroll) log('[IHW] Chat detected, aggressive opts skipped');

    /* ── LCP IMAGE BOOST (v3.0.23) ─────────────────────── */
    // Перенесён после shouldSkipScroll — иначе TDZ (Cannot access before initialization).
    // Реактивная стратегия (!_isSlow): LCP Observer после paint, переприоритизирует in-flight.
    // Проактивная стратегия (_isSlow): processNode сразу при появлении img в DOM.
    if (LCP_IMAGE_BOOST && !shouldSkipScroll && PAGE === 'Mixed Content'
            && !_isSlow && window.PerformanceObserver) {
        // v3.1.5-fix1: лог перенесён сюда — PAGE уже объявлен, контекст точнее (реактивный путь)
        log('[IHW] _isSlow=false → LCP boost: реактивный (Observer активен)');
        try {
            let _lcpBoosted = 0;
            const _lcpObs = new PerformanceObserver(list => {
                if (_lcpBoosted >= 2) return;
                for (const entry of list.getEntries()) {
                    const el = entry.element;
                    const t = Math.round(entry.startTime);
                    const kb = (entry.size / 1024).toFixed(1);
                    const tag = el ? el.tagName : '?';
                    const src = el?.src?.slice(0, 70) || el?.currentSrc?.slice(0, 70) || '(no src)';

                    // Placeholder/spinner — слишком рано, кандидат ещё не финальный
                    if (entry.startTime < 300) {
                        log(`[IHW] LCP candidate skip (placeholder, ${t}ms): ${tag} ${kb}KB`);
                        continue;
                    }
                    // Не img — браузер уже справляется (video poster, text)
                    if (!el || el.tagName !== 'IMG') {
                        log(`[IHW] LCP candidate agreed — браузер справится: ${tag} | ${kb}KB | ${t}ms | fetchPriority:${el?.fetchPriority || 'unset'}`);
                        continue;
                    }
                    // Уже загружена — переприоритизировать поздно
                    if (el.complete) {
                        log(`[IHW] LCP candidate agreed — IMG уже загружена: ${kb}KB | ${t}ms | src:${src}`);
                        continue;
                    }
                    // Браузер уже поставил high — не мешаем
                    if (el.fetchPriority === 'high') {
                        log(`[IHW] LCP candidate agreed — браузер уже high: ${kb}KB | ${t}ms | src:${src}`);
                        continue;
                    }
                    // Мы уже бустили (проактивно через processNode или повторный observer)
                    if (el.hasAttribute('data-ihw-lcp-boosted')) {
                        log(`[IHW] LCP candidate agreed — уже IHW-бустирован: ${kb}KB | ${t}ms | src:${src}`);
                        continue;
                    }
                    // Корректируем: браузер выставил не high, а мы видим что запрос ещё in-flight
                    const prevPriority = el.fetchPriority || el.getAttribute('fetchpriority') || 'unset';
                    el.fetchPriority = 'high';
                    if (el.loading === 'lazy') { el.loading = 'eager'; }
                    if (el.parentElement?.tagName === 'PICTURE') {
                        el.parentElement.querySelectorAll('source').forEach(s => {
                            if (!s.fetchPriority) s.fetchPriority = 'high';
                        });
                    }
                    el.setAttribute('data-ihw-lcp-boosted', 'true');
                    _lcpBoosted++;
                    log(`[IHW] LCP candidate OVERRIDDEN #${_lcpBoosted} — браузер: ${prevPriority} → IHW: high | ${kb}KB | ${t}ms | src:${src}`);
                }
            });
            _lcpObs.observe({ type: 'largest-contentful-paint', buffered: true });
            setTimeout(() => { try { _lcpObs.disconnect(); } catch (e) {} }, 8000);
        } catch (e) { log('[IHW] LCP boost error:', e); }
    }

    /* ── ТРЕКЕРЫ ────────────────────────────────────────── */
    // v3.1.7: гибридная структура TRACKERS.
    // Exact-match домены → Set (O(1) lookup). endsWith-суффиксы → короткий Array (O(N), N≤40).
    // Precompiled regex для шрифтовых CDN и preload-паттернов — создаётся 1 раз, не при каждом вызове.
    const RE_FONTS   = /fonts\.(googleapis|gstatic|bunny\.net)|use\.typekit\.net|fast\.fonts\.net/;
    const RE_PRELOAD = /preload|preconnect|modulepreload|dns-prefetch/;

    // Точные hostname (Set — O(1))
    const TRACKER_SET = new Set([
        'google-analytics.com', 'googletagmanager.com', 'doubleclick.net',
        'googlesyndication.com', 'googleadservices.com', 'googletagservices.com',
        'connect.facebook.net', 'fbcdn.net',
        'scorecardresearch.com', 'quantserve.com', 'outbrain.com', 'taboola.com',
        'moatads.com', 'adnxs.com', 'openx.net', 'adtelligent.com',
        'advertising.com', 'adsrvr.org', 'rubiconproject.com', 'pubmatic.com',
        'casalemedia.com', 'smartadserver.com', 'appnexus.com', 'criteo.com',
        'bidswitch.net', 'rlcdn.com', 'bluekai.com', 'demdex.net',
        'amazon-adsystem.com', 'adcolony.com', 'media.net', '33across.com',
        'clarity.ms', 'hotjar.com', 'mixpanel.com', 'segment.io', 'segment.com',
        'heap.io', 'heapanalytics.com', 'amplitude.com', 'fullstory.com',
        'logrocket.com', 'mouseflow.com', 'inspectlet.com', 'clicktale.net',
        'contentsquare.net', 'optimizely.com', 'crazy-egg.com',
        'deltarockme.com', 'vak345.com',
        'mc.yandex.ru', 'mc.yandex.net', 'top-fwz1.mail.ru',
        'redirect.appmetrica.yandex.com', 'counter.yadro.ru',
        'cnt.tbz.liveinternet.ru', 'open.rambler.ru',
        'ads.vk.com', 'target.my.com', 'rb.mail.ru', 'tbgcounter.com',
        'static.hotjar.com', 'vc.hotjar.io',
        'stats.g.doubleclick.net', 'bat.bing.com', 'ad.doubleclick.net', 'ad.atdmt.com',
        'adsymptotic.com', 'creativecdn.com', 'go.sonobi.com',
        'snigelweb.com', 'sharethrough.com', 'triplelift.com',
        'yieldmo.com', 'yieldlab.net', 'smartclip.net',
        'spoutable.com', 'undertone.com', 'indexexchange.com',
        'sovrn.com', 'lijit.com', 'contextweb.com', 'pulsepoint.com',
        'onesignal.com', 'pusher.com', 'pushcrew.com',
        'aimtell.com', 'subscribers.com', 'pushassist.com',
        'chartbeat.com', 'chartbeat.net', 'krxd.net',
        'clickagy.com', 'agkn.com', 'exelator.com', 'eyeota.net',
        'spotxchange.com', 'cxense.com', 'adobedtm.com', 'omtrdc.net',
        '2mdn.net', 'hlserve.com', 'cdn.syndication.twimg.com',
    ]);
    // endsWith-суффиксы для поддоменов (Array — только те что не покрываются точным Set)
    const TRACKER_SUFFIXES = [
        'google.com', 'facebook.com', 'fb.com',
        'vk.com', 'mail.ru',
    ];
    const TRACKER_EXCEPTIONS = new Set([
		'cloudflare.com',            // Cloudflare Challenge / Turnstile капча
        'challenges.cloudflare.com', // прямой домен капчи Cloudflare
        'recaptcha.net',          // Google reCAPTCHA — раскомментировать если сломается
		//'recaptcha.google.com',
        'hcaptcha.com',           // hCaptcha — раскомментировать если сломается
		]);

    // v3.1.7: кэш по hostname — isTracker вызывается на каждой ноде в hot path.
    // Без кэша: URL parsing + TRACKER_SET.has + TRACKER_SUFFIXES.some на каждый вызов.
    // С кэшем: 1 Map.get после первого вызова для данного hostname. Лимит 500 — защита от
    // рекламных поддоменов (px123.ads.example.com) бесконечно раздувающих Map.
    const _trackerCache = new Map();
    const isTracker = url => {
        try {
            const h = new URL(url, location.origin).hostname;
            if (_trackerCache.has(h)) return _trackerCache.get(h);
            const r = TRACKER_SET.has(h) || TRACKER_SUFFIXES.some(s => h.endsWith('.' + s) || h === s);
            if (_trackerCache.size > 500) _trackerCache.clear();
            _trackerCache.set(h, r);
            return r;
        } catch { return false; }
    };
    const isException = url => {
        try {
            const u = new URL(url, location.origin);
            const host = u.hostname;
            if (TRACKER_EXCEPTIONS.has(host)) return true;
            // v3.2.4: path-based исключение для Google reCAPTCHA на google.com.
            // Проблема: google.com в TRACKERS (реклама/аналитика), но
            // https://google.com/recaptcha/api2/anchor — легитимная капча.
            // Точечная проверка пути вместо добавления всего google.com в исключения.
            if (host === 'google.com' && u.pathname.startsWith('/recaptcha/')) return true;
            if (host === 'www.google.com' && u.pathname.startsWith('/recaptcha/')) return true;
            return false;
        } catch { return false; }
    };

    const _origBeacon = navigator.sendBeacon.bind(navigator);
    navigator.sendBeacon = (url, data) => isTracker(url) ? false : _origBeacon(url, data);

    /* ── font-display:swap (всегда, кроме OFF) ──────────── */
    const _fs = document.createElement('style');
    _fs.textContent = '@font-face{font-display:swap}';
    (document.head || document.documentElement).appendChild(_fs);

    /* ── EXTREME MODE ───────────────────────────────────── */
    if (EXTREME_MODE) {
        try {
            Object.defineProperty(navigator, 'connection', {
                value: { effectiveType: 'slow-2g', saveData: true, rtt: 2200, downlink: 0.05 },
                configurable: true
            });
            log('[IHW Extreme] Fake Save-Data + slow-2g');
        } catch (e) { }
        document.documentElement?.classList.add('ihw-extreme');
        const _xCss = document.createElement('style');
        _xCss.textContent =
            // v3.4.7-fix: :not(#ihw-btn) добавлен в селектор — иначе это же правило
            // (box-shadow:none!important) перекрывало _flashBoostSuccess() на самой
            // кнопке скрипта, делая визуальное подтверждение буста невидимым в ON[E].
            'html.ihw-extreme *:not(#ihw-btn),html.ihw-extreme :before,html.ihw-extreme :after{' +
            'background-image:none!important;filter:none!important;' +
            'backdrop-filter:none!important;box-shadow:none!important;' +
            'text-shadow:none!important;border-radius:0!important;' +
            'animation:none!important;transition:none!important}' +
            'html.ihw-extreme img,html.ihw-extreme video{image-rendering:crisp-edges!important}';
        (document.head || document.documentElement).appendChild(_xCss);
        document.querySelectorAll('video,audio').forEach(m => {
            if (!m.hasAttribute('data-ihw-boosted')) { m.preload = 'none'; m.autoplay = false; }
        });
        document.querySelectorAll('[autofocus]').forEach(el => el.removeAttribute('autofocus'));
        document.querySelectorAll('input,textarea,[contenteditable]').forEach(el => el.spellcheck = false);
    }

    /* ── Базовый CSS ────────────────────────────────────── */
    const _css = document.createElement('style');
    _css.textContent = 'html,body{visibility:visible!important;opacity:1!important}';
    (document.head || document.documentElement).appendChild(_css);

    /* ── VK / OK / vkvideo.ru: хост-специфичные оптимизации (v3.0.18) ──── */
    // Аналогично YouTube-блоку: только стабильные structural anchors и
    // product-level классы. Без webpack-патчинга, MutationObserver, DOM removal.
    // display:none для скелетонов — верный выбор (Kimi прав):
    //   скелетоны — чисто визуальные заглушки, не несут функционала.
    //   display:none полностью исключает из render tree → лучше для FCP чем
    //   visibility:hidden, который всё ещё резервирует место и требует paint.
    // #rightColumn для OK: product-level ID, стабилен годами. Если OK когда-
    //   либо переименует его в navigation+ads — просто перестанет работать,
    //   но сайт останется цел (только display:none, не DOM removal).
    // [class*="ad-overlay"] — НЕ берём: слишком широко, риск задеть subtitles/controls.
    if (/\b(vk\.com|vkvideo\.ru|ok\.ru)\b/.test(location.hostname)) {
        const _vkCss = document.createElement('style');
        _vkCss.textContent =
            // Скелетоны: убираем из render tree до гидратации React/Vue.
            // Именованные VK-классы стабильны (product-level, не CSS-modules).
            '#FeedPageSkeleton,.LeftMenuLegacySkeleton,.TopSearchRoot .SkeletonIso,' +
            '.ProfileMenuSkeletonRoot,.skeleton,.skeleton-loader,.placeholder-animation,' +
            '[class*="skeleton"],[class*="Skeleton"],.animated-background' +
            '{display:none!important}' +
            // Рекламные structural anchors: VK
            '#ads_left,.videoplayer_ads,.videoplayer_ads_actions,' +
            '.videoplayer_ads_media_el,.rb-adman-ad-actions' +
            '{display:none!important}' +
            // Рекламные structural anchors: OK
            '#rightColumn,#hook_Block_StickyBannerContainer' +
            '{display:none!important}' +
            // VK Video: blur restriction overlay
            // data-testid — stableнее чем hashed class, но следим:
            // если VK сменит testid — просто перестанет работать.
            '[data-testid="video_card_restriction_overlay"]{display:none!important}' +
            // Blur на превью (только img, не глобально — чтобы не задеть UI)
            'img[class*="Blur"],img[class*="blur"]{filter:none!important;-webkit-filter:none!important}' +
            // Изоляция плеера: уменьшаем объём перерисовок при обновлении UI вокруг
            '.videoplayer,.videoplayer_media,[class*="videoplayer"]:not([class*="ads"])' +
            '{contain:layout style!important}' +
            // Фиксированные элементы в отдельный compositor layer
            '#masthead,.vkuiFixedLayout,.TopNav,.TopSearchRoot,.LeftMenu' +
            '{will-change:transform!important}' +

            // v3.2.1: content-visibility для ленты новостей (консенсус всех 4 ИИ).
            // Лента VK — однонаправленный поток вниз, посты имеют стабильную структуру.
            // contain-intrinsic-size 500px 150px — двухмерная эвристика (Kimi) для точности.
            // content-visibility:auto запоминает реальную высоту после первого рендера.
            // НЕ для .im-mess (сообщения) — VKUI Messenger уже виртуализирован (Kimi/ChatGPT).
            // Без условия _isConnSlow: прирост есть и на быстрой сети (CPU/GPU) (решение пользователя).
			// v3.2.1-fix1: article[data-index] убран — слишком широкий.
            // На VK data-index используется в Stories, рекомендациях, событиях.
            // content-visibility:auto на них → scrollbar jump из-за вариативной высоты.
            // .feed_row + .post[data-post-id] достаточно точно покрывают посты ленты.
            '.feed_row,.post[data-post-id]' +
            '{content-visibility:auto;contain-intrinsic-size:500px 150px}' +

            // v3.2.1: contain для статичных зон (меню, футер, правые колонки).
            // Ограничиваем область перерисовки — изменение внутри блока не каскадит в страницу.
            // Не используем translate3d/will-change для плеера — Chromium 2026 уже layerized (ChatGPT).
            // .LeftMenu__root — product-level VKUI класс, не CSS Modules rotation.
            '.LeftMenu__root,.vkitFooter__root,.vkitRightMenu__container,#stl_left' +
            '{contain:paint layout!important}' +

            // v3.2.1: overscroll-behavior для внутренних скроллеров (UX фикс).
            // Предотвращает "тянучку" страницы при достижении границы чата/диалогов.
            // Риск нулевой — не ломает layout, не отменяет скролл.
            '._im_peer_history,._im_dialogs_list,.vkuiCustomScrollView__host' +
            '{overscroll-behavior:contain!important}';
        (document.head || document.documentElement).appendChild(_vkCss);
        log('[IHW] VK/OK: applied host-specific optimizations');
    }

    /* ── Bilibili: скрытие рекламы/промо CSS (v3.4.9) ───────────────────
     *  Источник: разбор донора "Скрыть рекламу и рекламные разделы на Bilibili"
     *  (Greasyfork). Взят только список селекторов — механизм донора (рекурсивный
     *  JS-обход + inline-стили на каждый узел + setInterval(1000)) НЕ копируется,
     *  заменён на единый <style> блок, как и для VK/OK/YouTube.
     *  Исключено из donor-списка:
     *    - весь .bili-danmaku-x-* (vote/score/guide/cmd-shrink/link/follow-to-electric):
     *      сам донор предупреждает что блокирует опцию интерактивных комментариев —
     *      это функциональный элемент (голосование/счёт в данму), не реклама.
     *      Исключена вся семья целиком, не выборочно (как .ytp-cued-thumbnail-overlay
     *      на YouTube ранее) — единичный подтверждённый риск в группе исключает группу.
     *    - .vcd: слишком общее/короткое имя класса, риск ложного срабатывания.
     *    - div[data-v-2ce37bb8].btn-ad: data-v-XXXXXXXX — автогенерируемый Vue.js
     *      scoped-CSS хеш, пересобирается на каждый билд фронтенда — классический
     *      хрупкий селектор, ломается при следующем деплое Bilibili.
     *    - a[data-loc-id="4331"]: числовой ID локации, может быть переназначен при
     *      редизайне без видимых изменений в разметке — менее заметный отказ чем
     *      переименование класса, консервативно исключён.
     *  Все оставшиеся — семантические, product-level имена (ad-report.*, activity-m-v1.*,
     *  video-card-ad-small, bpx-player-*, slide-ad-exp) — не build-хеши.
     * ──────────────────────────────────────────────────────────────────── */
    if (/\bbilibili\.(com|tv)\b/.test(location.hostname)) {
        const _biliCss = document.createElement('style');
        _biliCss.textContent =
            '.bpx-player-qoeFeedback,' +
            '.ad-report.strip-ad.left-banner,' +
            '.ad-report.ad-floor-exp.left-banner,' +
            '.ad-report.ad-floor-exp.right-bottom-banner,' +
            '.ad-report.video-card-ad-small,' +
            '.activity-m-v1.act-end,' +
            '.activity-m-v1.act-now,' +
            '.video-card-ad-small,' +
            '.video-page-game-card-small,' +
            '.slide-ad-exp,#slide-ad-exp,#slide_ad,' +
            '#right-bottom-banner,#bannerAd,#activity_vote,' +
            '.pop-live-small-mode.part-1,' +
            '.ad-floor-cover.b-img' +
            '{display:none!important}';
        (document.head || document.documentElement).appendChild(_biliCss);
        log('[IHW] Bilibili: applied host-specific optimizations');
    }

    /* ── Яндекс SERP: скрытие рекламных меток (v3.0.18) ─────────────── */
    // Только стабильные ARIA-атрибуты и named-классы.
    // НЕ используем :has(.serp-item) для скрытия целых карточек —
    //   риск ложных срабатываний + стоимость CSS engine на :has() высокого уровня.
    // НЕ используем MutationObserver/TreeWalker — антипаттерн для perf-скрипта.
    // Цель: убрать рекламные метки и баннеры до first paint → улучшение LCP
    //   (реклама часто занимает above-the-fold на поиске Яндекса).
    // Сетевые запросы Директа CSS не отменяет — только визуальный выигрыш.
	if ((location.hostname.includes('yandex.') || location.hostname.endsWith('dzen.ru'))&& PAGE !== 'Video Content') {
        const _yaCss = document.createElement('style');
        _yaCss.textContent =
            // ARIA-метки: семантически стабильны (часть a11y-контракта)
            '[aria-label="Реклама"],[aria-label="Промо"]' +
            '{display:none!important}' +
            // v3.1.0: расширенные селекторы из обновлённого Yandex SERP: Promo скрипта.
            // Named-классы и data-атрибуты Яндекса — product-level, не CSS-modules
            '.AdvLabel,.AdvLabel-Text,.AdvCaption,.PromoOffer,.direct-label,' +
            '.mg-adv-label,[data-baobab-name="adv"],.DistributionLinkBro,' +
            '[data-testid^="promo"],[data-fast-name="PromoOffer"]' +
            '{display:none!important}' +
            // Рекламные карточки: realty, finance — специфичные structural selectors
            '.RealtyListing-AdvItem,.OfferSnippet_highlight' +
            '{display:none!important}';
        (document.head || document.documentElement).appendChild(_yaCss);
        log('[IHW] Yandex: ad labels hidden');
    }

    /* ── Rutube: host-specific оптимизации (v3.0.26) ──────────────── */
    // Консенсус Kimi/Qwen/DeepSeek: только CSS-инъекция structural anchors.
    // Без fetch-перехвата, без MutationObserver, без postMessage-хаков.
    // display:none исключает из render tree до first paint → ускоряет LCP.
    // Graceful degradation: если Rutube сменит классы — сайт останется цел,
    // просто реклама перестанет скрываться.
    if (location.hostname.endsWith('rutube.ru')) {
        const _rtCss = document.createElement('style');
        _rtCss.textContent =
            // BEM-классы преролла — product-level, не CSS-modules, живут годами
            '.preroll,.player__preroll,.preroll__container,' +
            '.video-adv,.player__overlay-adv,' +
            // Семантические data-маркеры рекламного контракта сайта
            '[data-adv],[data-ad],' +
            // ID интеграций AdFox: меняются только при смене рекламного провайдера
            '#advertising,#ADFOX_FEED_BANNER' +
            '{display:none!important}' +
            // Изоляция основного видео: уменьшает перерисовки при обновлении UI.
            // data-testid="video" — стабильный e2e-контракт Rutube (SponsorBlock подтверждает)
            'video[data-testid="video"]' +
            '{contain:layout style!important}';
        (document.head || document.documentElement).appendChild(_rtCss);
        log('[IHW] Rutube: applied host-specific optimizations');
    }

    /* ── Универсальная защита скроллеров ────────────────── */
    function isInsideScroller(el) {
        let p = el.parentElement;
        while (p && p !== document.body) {
            const s = window.getComputedStyle(p);
            if (/auto|scroll/.test(s.overflowY) || /auto|scroll/.test(s.overflow)) return true;
            p = p.parentElement;
        }
        return false;
    }

    /* ── v3.0.15: упрощённый поиск видео (без VK/OK хаков) ── */
    function _findVideosDeep() {
        const found = new Set([...document.querySelectorAll('video')]);
        // Для плееров с известными обёртками — ищем внутри, но без VK/OK специфики
        const hosts = ['.videoplayer_media', '#video-poplayer-cnt', '.video-page-layout-module__player'];
        hosts.forEach(sel => {
            document.querySelectorAll(sel).forEach(host => {
                host.querySelectorAll('video').forEach(v => found.add(v));
            });
        });
        return [...found];
    }

    /* ── MutationObserver ───────────────────────────────── */
    const seen = new WeakSet();
    const processNode = node => {
        if (!(node instanceof HTMLElement) || seen.has(node)) return;
        seen.add(node);
        const tag = node.tagName;

        // v3.0.25: VIDEO на видеохостингах — полностью исключаем из processNode.
        // boostMainVideo — единственный owner VIDEO-логики на Video Content.
        // Устраняет race condition с SABR YouTube/Vimeo/VK без зональных эвристик.
        // Консенсус всех ИИ-анализов (Kimi, Grok, DeepSeek, ChatGPT, Claude).
        if (tag === 'VIDEO' && PAGE === 'Video Content') return;

        // v3.1.4: Video Dominance Window — искусственный bandwidth pressure.
        // На медленном соединении/прокси fetchPriority='high' реально давит на браузер:
        // узкий канал даёт видео приоритет. На быстром соединении fetchPriority игнорируется —
        // polymer добавляет 20-40 IMG рекомендаций, они конкурируют с SABR-сегментами.
        // Лок воспроизводит эффект прокси: новые IMG без LCP-буста откладываются в lazy
        // пока canplay не пришёл (или safety-timeout 1500ms). LCP-бустированные картинки
        // защищены data-ihw-lcp-boosted — они не затронуты (header/thumbnail в начальном HTML).
        // Безопасно: к моменту boost'а polymer уже отработал, LCP разрешён до этого.
        if (_videoLocked && PAGE === 'Video Content' && tag === 'IMG'
                && !node.hasAttribute('data-ihw-lcp-boosted')
                // v3.1.6-fix1: не трогаем 1×1 трекеры и иконки — они не потребляют bandwidth
                && !(node.width === 1 && node.height === 1)) {
            if (!node.loading) node.loading = 'lazy';
            return;
        }

        const src = node.src || node.href || '';

        if (src && isTracker(src) && !isException(src)) {
            // v3.2.4: DEBUG лог заблокированных трекеров.
            // Позволяет исключить домен если блокировка ломает сайт (добавить в TRACKER_EXCEPTIONS).
            log(`[IHW] Tracker blocked: ${new URL(src, location.origin).hostname} (<${tag}>)`);
            node.remove(); _blockedCount++; return;
        }

        if (tag === 'LINK' && node.rel === 'prefetch' && !isException(src)) {
            if (isVideoHost) {
                try { if (new URL(src, location.origin).hostname.endsWith(location.hostname)) return; } catch { }
            }
            log(`[IHW] Prefetch removed: ${src.slice(0, 80)}`);
            node.remove(); _blockedCount++; return;
        }
        // Шрифты откладываем ТОЛЬКО на Video Content
        if (tag === 'LINK' && RE_FONTS.test(src)) {
            if (PAGE !== 'Video Content') return;
            if (node.dataset.ihwFontDeferred) return;
            node.media = 'print';
            setTimeout(() => {
                if (node.parentNode && !node.dataset.ihwFontDeferred) node.media = 'all';
            }, 6000);
            return;
        }

        // v3.0.4: loading=lazy только для поздней динамики (>1000мс).
        // Preload scanner уже обработал начальный HTML.
        // Защита LCP на Pinterest/Unsplash и других фотобанках.
        if (tag === 'IMG') {
            if (!node.decoding) node.decoding = 'async';
            // v3.0.23 companion fix: не навешиваем lazy если LCP Observer уже забустил
            if (performance.now() > 1000 && !node.hasAttribute('loading')
                    && !node.hasAttribute('data-ihw-lcp-boosted')) {
                node.loading = 'lazy';
            }
            // v3.0.23: проактивный буст на медленных соединениях.
            // На быстрых — реактивный LCP Observer выше это сделает сам после paint.
            // v3.1.5: _isSlowNow() вместо _isSlow — ловим деградацию сети mid-page.
            // v3.1.5: rAF-retry при getBoundingClientRect()=0 (layout ещё не вычислен):
            //   IMG добавленная через MO на ранней стадии имеет width=height=0 до layout.
            //   Без retry: r.height > 80 не выполняется → boost пропускается даже при _isSlow.
            //   На prom.ua (TTFB:618ms, _isSlow=true) именно это было root cause пропуска.
            if (LCP_IMAGE_BOOST && _isSlowNow() && !shouldSkipScroll && PAGE === 'Mixed Content'
                    && !node.hasAttribute('data-ihw-lcp-boosted') && !node.complete) {
                const _tryBoostImg = (n, isRetry) => {
                    if (n.hasAttribute('data-ihw-lcp-boosted') || n.complete) return;
                    const r = n.getBoundingClientRect();
                    if (r.width === 0 && r.height === 0 && !isRetry) {
                        // Layout ещё не вычислен — ждём один кадр и повторяем
                        requestAnimationFrame(() => _tryBoostImg(n, true));
                        return;
                    }
                    if (r.top < window.innerHeight && r.height > 80 && r.width * r.height > 8000) {
                        n.fetchPriority = 'high';
                        if (n.loading === 'lazy') n.loading = 'eager';
                        n.setAttribute('data-ihw-lcp-boosted', 'true');
                        log('[IHW] Slow-conn LCP preemptive boost' + (isRetry ? ' (rAF retry)' : '') + ':', n.src?.slice(0, 80));
                    }
                };
                _tryBoostImg(node, false);
            }
        }

        if (node.hasAttribute('autofocus')) node.removeAttribute('autofocus');

        // v3.0.16: на AI-чатах не трогаем iframe (виджеты ввода, превью)
        if (shouldSkipScroll && tag === 'IFRAME') return;
        if (tag === 'IFRAME' && !node.fetchPriority && !isVideoHost) {
            if (node.offsetWidth < 200 || node.offsetHeight < 100) node.fetchPriority = 'low';
        }

        if (EXTREME_MODE) {
            if (tag === 'LINK') {
                const rel = node.getAttribute('rel') || '';
                if (RE_PRELOAD.test(rel)) {
                    try {
                        const h = new URL(node.href || '', location.origin).hostname;
                        // v3.1.6: защита Video CDN preconnect от EXTREME_MODE.
                        if (PAGE === 'Video Content' && _isVideoCDN(h)) return;
                        // v3.2.4: проверка исключений (капча) перед удалением.
                        // Без этого google.com/recaptcha/* удалялся → капча ломалась.
                        if (isException(node.href || '')) return;
                        if (!h.endsWith(location.hostname)) {
                            log(`[IHW] EXTREME: removed ${rel} → ${h} (cross-domain)`);
                            node.remove(); _blockedCount++; return;
                        }
                    } catch { }
                }
            }
            if (['VIDEO', 'AUDIO', 'SCRIPT'].includes(tag) && !node.fetchPriority) {
                // v3.1.6: _isProtectedScript() заменяет inline same-origin проверку
                if (tag === 'SCRIPT' && node.src && SAME_ORIGIN_SCRIPT_PROTECT) {
                    if (_isProtectedScript(node)) {
                        log(`[IHW] SAME_ORIGIN_PROTECT: скрипт сохранён (не занижен): ${node.src.slice(0, 80)}`);
                    } else {
                        node.fetchPriority = 'low';
                    }
                } else if (!(tag === 'VIDEO' && node.hasAttribute('data-ihw-boosted'))) {
                    node.fetchPriority = 'low';
                }
            }
            if (tag === 'INPUT' || tag === 'TEXTAREA' || node.hasAttribute('contenteditable')) node.spellcheck = false;
        }

        // v3.0.26: Единая логика медиа — все режимы (ON / ON[A] / ON[E]).
        // Вынесено из EXTREME_MODE: конкурирующие медиа приглушаются всегда.
        // VIDEO+VideoContent сюда не доходит — early return в начале processNode (v3.0.25).
        // v3.1.5: VIDEO на Mixed Content — только явно вне viewport.
        //   До этого: preload='none' ставился ВСЕМ видео на Mixed Content.
        //   Баг на yapx.ru: MP4-видео является главным контентом страницы — получало
        //   preload='none' и никогда не загружалось (OFF работал, ON — нет).
        //   Нельзя отличить "главное видео" от "фонового" на Mixed Content по URL/атрибутам.
        //   Безопасная эвристика: throttle только для видео у которых layout подтверждён
        //   (height>0) и они явно за пределами viewport (top > 2×innerHeight).
        //   Если layout ещё не вычислен (width=height=0) — консервативно не трогаем.
        if ((tag === 'VIDEO' || tag === 'AUDIO') && !node.hasAttribute('data-ihw-boosted')) {
            if (tag === 'AUDIO') {
                node.preload = 'none';
                node.autoplay = false;
            } else if (PAGE !== 'Video Content') {
                // Mixed Content: throttle только видео явно вне viewport
                const r = node.getBoundingClientRect();
                const isClearlyBelowFold = r.height > 0 && r.top > window.innerHeight * 2;
                if (isClearlyBelowFold) {
                    node.preload = 'none';
                    node.autoplay = false;
                }
                // width=height=0 (layout pending) или в/около viewport → не трогаем
            }
            // VIDEO + Video Content → boostMainVideo (early return выше)
        }
    };

    // v3.1.0: rAF-debounce для некритичных нод в MutationObserver.
    // Критичные (IMG при _isSlow+Mixed → LCP boost) — обрабатываем синхронно.
    // Остальные (LINK, SCRIPT, AUDIO, прочее) — батчим в rAF.
    // На бесконечных лентах (VK, Яндекс) это снижает количество microtask вызовов c N до 1.
    // Риск (DeepSeek): 16-32ms задержка для IMG при _isSlow → решено: такие IMG идут синхронно.
    let _rafPending = false;
    const _rafQueue = [];
    const _flushRafQueue = () => { _rafPending = false; while (_rafQueue.length) processNode(_rafQueue.shift()); };
    const _isCriticalNode = (n) => {
        if (!(n instanceof HTMLElement)) return false;
        const t = n.tagName;
        // IMG при медленном соединении на Mixed Content → проактивный LCP boost нельзя откладывать.
        // v3.1.5: _isSlowNow() вместо _isSlow — реагируем на ping-спайки возникшие после старта.
        if (t === 'IMG' && LCP_IMAGE_BOOST && _isSlowNow() && PAGE === 'Mixed Content') return true;
        return false;
    };
    const mo = new MutationObserver(muts => {
        const _now = performance.now(); // v3.1.7: один вызов на весь batch вместо N вызовов
        for (const m of muts) {
            for (const n of m.addedNodes) {
                // v3.1.2: <video> появился в DOM на видеохостинге → немедленный boost через rAF.
                // Root cause нестабильности v3.1.1: v3.0.25 правильно убрал VIDEO ownership
                // из processNode (early return), но заодно убрал быстрый триггер буста.
                // Следствие: MO видит <video>, делает ничего, следующая попытка tryBoost
                // через 1500ms — за это время браузер уже грузит картинки рекомендаций и скрипты.
                // Теперь: MO видит <video> → один rAF (16ms для layout) → _boostRef() напрямую.
                // Безопасно: processNode в v3.0.25 уже не устанавливает preload/autoplay для VIDEO
                // на Video Content, race condition с SABR исключён. tryBoost остаётся safety net.
                if (n instanceof HTMLElement && n.tagName === 'VIDEO'
                        && PAGE === 'Video Content' && !_videoBoosted && _boostRef) {
                    log('[IHW] Video found by MO → rAF boost in ~16ms');
                    requestAnimationFrame(() => {
                        if (!_videoBoosted) { const r = _boostRef(); if (r === true) _videoBoosted = true; }
                    });
                    continue; // в processNode всё равно early return — в очередь не добавляем
                }
                if (_isCriticalNode(n)) {
                    processNode(n);
                } else {
                    _rafQueue.push(n);
                    if (!_rafPending) { _rafPending = true; requestAnimationFrame(_flushRafQueue); }
                }
            }
        }
    });
    mo.observe(document.documentElement, { childList: true, subtree: true });

    /* ── runRenderOpts (v3.0.2) ─────────────────────────── */
	// Запускается после load. Браузер уже приоритизировал видимые изображения.
    // Оставшиеся без loading — безопасно перевести в lazy.
    const runRenderOpts = () => {
        // v3.0.16: на AI-чатах не рискуем — пропускаем batch-оптимизацию img,
        // чтобы не сломать динамическую подгрузку аватарок/превью сообщений.
        if (shouldSkipScroll) return;
        if (document.readyState !== 'complete') {
            window.addEventListener('load', runRenderOpts, { once: true });
            return;
        }
        requestIdleCallback((deadline) => {
            const images = document.querySelectorAll('img:not([loading])');
            let idx = 0;
            function processBatch() {
                while (idx < images.length && deadline.timeRemaining() > 0) {
                    images[idx++].loading = 'lazy';
                }
                if (idx < images.length) {
                    requestIdleCallback(processBatch, { timeout: 2000 });
                } else {
                    log('[IHW] runRenderOpts: lazy applied to', images.length, 'images');
                }
            }
            processBatch();
        }, { timeout: 2000 });
    };

    /* ── VIDEO CONTENT ──────────────────────────────────── */
    if (PAGE === 'Video Content') {
        /* ── Resource Timing Observer → _unstableScore (v3.3.0) ──────
         *  Консенсус KIMI+DeepSeek: реальная картина нестабильности сети
         *  по видео-сегментам (не effectiveType который отстаёт на 10–30с).
         *  DeepSeek уточнение: смотрим скорость KB/s, не только duration.
         *  <10 KB/s при duration>3s = признак стрессовой загрузки (не просто большой сегмент).
         *  transferSize=0 + duration>1s = вероятный failed/abort.
         *  Decay при успешных быстрых сегментах: −4 за каждый <800ms.
         *  Только Video Content: на Mixed нет видео-сегментов, PO бесполезен.
         * ──────────────────────────────────────────────────────────── */
        if (window.PerformanceObserver) {
            try {
                const _segmentPO = new PerformanceObserver(list => {
                    for (const e of list.getEntries()) {
                        if (!/\.(ts|m4s|mp4|webm)(\?|$)|videoplayback|cdnvideohub|mycdn\.me|okcdn/i.test(e.name)) continue;
                        const dur = e.duration;
                        const kb = e.transferSize / 1024;
                        if ((dur > 3000 && kb / (dur / 1000) < 10) || (e.transferSize === 0 && dur > 1000)) {
                            _unstableScore = Math.min(100, _unstableScore + 12);
                        } else if (dur < 800) {
                            _unstableScore = Math.max(0, _unstableScore - 4);
                        }
                    }
                });
                _segmentPO.observe({ type: 'resource', buffered: false });
                // Отключаем через 60s после canplay — нагрузка не оправдана для стабильных сессий
                setTimeout(() => { try { _segmentPO.disconnect(); } catch(e){} }, 120000);
            } catch(e) {}
        }

        /* ── Fetch/XHR-перехват: ранний _preconnectFromUrl для MSE-плееров (v3.4.0, расширено v3.4.8) ──
         *  YouTube SABR / OK.ru / Twitch грузят видеосегменты через fetch() из JS
         *  (MediaSource.appendBuffer). currentSrc на <video> в этот момент = blob:/пусто —
         *  _dynamicPreconnect(video) не может извлечь хост из элемента.
         *  Этот перехватчик видит URL сегмента ДО его выполнения → preconnect к реальной
         *  CDN-ноде (rr2---sn-*.googlevideo.com и т.п.) сразу, не ждёт Resource Timing entry.
         *  Дешёвая операция: regex.test() по строке URL — без сети/DOM, ~0.01ms.
         *  Безопасно на Mobile и при saveData: фактический preconnect внутри
         *  _preconnectFromUrl сам проверяет saveData и лимит — здесь не дублируем.
         *
         *  v3.4.8: добавлен симметричный XHR-хук — РЕГРЕССИЯ обнаружена и закрыта.
         *  Старое обоснование "XHR не перехватываем" (VK al_video.php, нужен
         *  response.clone().text() на тело) было верным для ОДНОГО конкретного
         *  сценария (URL сегмента внутри JSON-тела ответа), но ошибочно трактовалось
         *  как общий запрет на любой XHR-перехват. Diagnostика на Rutube показала:
         *  HLS.js там грузит .ts-сегменты через XMLHttpRequest (не fetch), и сам URL
         *  сегмента доступен синхронно в XMLHttpRequest.prototype.open(method, url) —
         *  БЕЗ обращения к телу ответа. По стоимости идентично fetch-хуку: один
         *  regex.test() на строку, ноль сетевых/DOM операций. Перехватывается только
         *  .open() — .send()/response не трогаем, тело ответа нигде не читается —
         *  оригинальное ограничение (не парсить XHR-тело) остаётся в силе нетронутым.
         *  Оба хука переиспользуют один _segmentUrlRe — нет дублирования паттерна.
         * ──────────────────────────────────────────────────────────────────────── */
        // v3.5.0: [?&]range= вместо \?range= (был узкий якорь — требовал range= ПЕРВЫМ
        // параметром сразу после ?, на практике редко так бывает). Добавлен [?&]bytes=\d+-\d+
        // — VK/OK (vkvideo.ru использует CDN okcdn.ru, подтверждено скриншотом Network)
        // отдаёt byte-range сегменты БЕЗ расширения файла в пути (просто "/?expires=...") и
        // с параметром bytes=1241433-2377309 вместо ожидаемого range= — другая, но
        // семантически идентичная конвенция именования. \d+-\d+ проверяет реальный
        // паттерн диапазона, не просто слово "bytes" где угодно в строке запроса.
        const _segmentUrlRe = /\.(ts|m4s|m4v|mp4|webm|m3u8|mpd)(\?|$)|\/seg-|\/segment-|\/chunk-|\/fragment-|[?&]range=|[?&]bytes=\d+-\d+|videoplayback|initplayback/i;
        if (typeof window.fetch === 'function') {
            try {
                const _origFetch = window.fetch;
                window.fetch = function(...args) {
                    const p = _origFetch.apply(this, args);
                    try {
                        const url = typeof args[0] === 'string' ? args[0] : (args[0]?.url || '');
                        if (url && _segmentUrlRe.test(url)) _preconnectFromUrl(url, 'fetch');
                    } catch (e) {}
                    return p;
                };
            } catch (e) { log('[IHW] fetch hook unavailable:', e.message); }
        }
        if (typeof XMLHttpRequest !== 'undefined') {
            try {
                const _origOpen = XMLHttpRequest.prototype.open;
                XMLHttpRequest.prototype.open = function(method, url, ...rest) {
                    try {
                        if (typeof url === 'string' && _segmentUrlRe.test(url)) _preconnectFromUrl(url, 'xhr');
                    } catch (e) {}
                    return _origOpen.call(this, method, url, ...rest);
                };
            } catch (e) { log('[IHW] XHR hook unavailable:', e.message); }
        }

        // v3.2.4: _preloadPoster — общая функция с CSS.escape дедупликацией.
        // Объявлена ДО boostMainVideo (и _ytBootstrap) чтобы обе могли вызвать её без TDZ.
        // CSS.escape: кавычки/спецсимволы в URL не сломают querySelector.
        // fetchPriority='high': ускоряет загрузку LCP-изображения (консенсус Kimi/DeepSeek).
        // Не грузим blob:/data: — они всегда локальные.
        const _preloadPoster = (url) => {
            if (!url || typeof url !== 'string') return;
            if (url.startsWith('blob:') || url.startsWith('data:')) return;
            if (!url.startsWith('http')) return;
            try {
                if (document.querySelector(`link[rel="preload"][as="image"][href="${CSS.escape(url)}"]`)) return;
                const l = document.createElement('link');
                l.rel = 'preload'; l.as = 'image'; l.href = url; l.fetchPriority = 'high';
                document.head.appendChild(l);
                log('[IHW] Poster preload:', url.slice(0, 80));
            } catch (e) {}
        };

        // v3.1.4: единообразный preconnect для всех видеохостингов.
        // YouTube делал синхронный preconnect в _ytBootstrap (DCL) — выигрыш 100-300ms.
        // Остальные хосты (Rutube, Vimeo, VK) шли через requestIdleCallback+500ms —
        // несоответствие без причины. Теперь: VIDEO_CDN_MAP вызывается синхронно
        // при DOMContentLoaded для всех хостов. PerformanceObserver (для динамических
        // CDN-нод) остаётся внутри initVideoPreconnect и запускается тоже при DCL.
        // YouTube дедуплицирует через _preconnected Set — двойных link нет.
        // v3.2.0-fix3: единственная точка вызова initVideoPreconnect — через _schedulePreconnect.
        // Задержка берётся из STATIC_DELAYS по hostname хоста (не CDN).
        // YouTube = 0 (немедленно при DCL), TikTok = 200ms, остальные = 0.
         // v3.2.0-fix4: для YouTube закомментировано — preconnect перенесён в _ytBootstrap
        // (атомарно с CSS-инъекцией, нет race между двумя DCL-листенерами).
        // Заскомментировать строки ниже чтобы вернуться к общему планировщику без исключения:
        const _h = location.hostname.replace(/^www\./, '');
        
        const _hostDelay = STATIC_DELAYS[_h] || 0;
        const _schedulePreconnect = () => {
			           if (_h === 'youtube.com' || _h === 'youtu.be') return; // v3.2.0-fix4: YouTube → _ytBootstrap эту строку закомментировать, чтобы планировщик был общим для всех без исключений (включая youtube, а в _ytBootstrap закоментировать вызов initVideoPreconnect)
            if (_hostDelay > 0) setTimeout(initVideoPreconnect, _hostDelay);
            else initVideoPreconnect();
        };
        if (document.readyState === 'loading')
            document.addEventListener('DOMContentLoaded', _schedulePreconnect, { once: true });
        else _schedulePreconnect();

        /* ── v3.0.15: шрифты — canplay + таймаут fallback ── */
        function _deferFontsUntilCanplay(mainVideo) {
            // v3.4.2: повторный вызов на SPA-переходе (тот же <video> переиспользуется,
            // _boostVideoElement выполняется снова) переводил уже восстановленные шрифты
            // обратно в media='print' и сразу возвращал → видимый FOUT без выигрыша
            // (канал критичен только при первой загрузке, не на SPA-переходе).
            if (_fontsDeferredOnce) return;
            const fontLinks = document.querySelectorAll(
                'link[href*="fonts.googleapis"], link[href*="fonts.gstatic"], ' +
                'link[href*="bunny.net"], link[href*="typekit.net"], link[href*="fast.fonts.net"], ' +
                'link[rel="preload"][as="font"]'
            );
            if (!fontLinks.length) return;
            _fontsDeferredOnce = true; // фиксируем факт отсрочки — повторов не будет
            let restored = false;
            const _restore = () => {
                if (restored) return;
                restored = true;
                clearTimeout(_timer);
                mainVideo.removeEventListener('error', _restore);
                fontLinks.forEach(lnk => {
                    if (!lnk.dataset.ihwFontDeferred) return;
                    delete lnk.dataset.ihwFontDeferred;
                    delete lnk.dataset.ihwOrigMedia;
                    lnk.media = lnk.dataset.ihwOrigMedia || 'all';
                    if (lnk.rel === 'preload' && lnk.as === 'font') lnk.fetchPriority = 'low';
                });
                log('[IHW Video] Шрифты восстановлены');
            };
            // v3.3.0: увеличенный таймаут шрифтов при нестабильной сети.
            // Консенсус KIMI: 15s вместо 8s при _unstableScore > 40.
            const _fontTimeout = _unstableScore > 40 ? 15000 : 8000;
            const _timer = setTimeout(_restore, _fontTimeout); // fallback
            mainVideo.addEventListener('canplay', _restore, { once: true });
            mainVideo.addEventListener('error', _restore, { once: true });
            fontLinks.forEach(lnk => {
                lnk.dataset.ihwOrigMedia = lnk.media || 'all';
                lnk.media = 'print';
                lnk.dataset.ihwFontDeferred = '1';
            });
        }

        if (location.hostname.endsWith('youtube.com') || location.hostname.endsWith('youtu.be')) {
            const noop = () => { };
            window.ytcsi = { tick: noop, span: noop, info: noop, setTick: noop, lastTick: noop };
            window.ytStats = noop;
            const _ytBootstrap = () => {
                try { if (window.yt?.config_) window.yt.config_.ENABLE_LOGGING = false; } catch (e) { }
                // v3.2.0-fix3: initVideoPreconnect() перенесён в _schedulePreconnect (единственная точка).
                // v3.2.0-fix4: ВОЗВРАЩЁН в _ytBootstrap как YouTube-исключение.
                // Причина: при TTFB>600ms MO ловит <video> ДО DCL — но _schedulePreconnect тоже DCL.
                // Разница: _ytBootstrap и _schedulePreconnect — два отдельных addEventListener(DCL).
                // Порядок их вызова не гарантирован. Внутри _ytBootstrap CSS-инъекция и preconnect
                // выполняются атомарно в одном обработчике — нет race между двумя листенерами.
                // _schedulePreconnect для YouTube закомментирован ниже (можно раскомментировать
                // для возврата к общему планировщику без ytBootstrap-исключения).
                initVideoPreconnect();
                log('[IHW] YT: early CDN preconnect initiated');
                // v3.2.4-fix1: ранний poster preload в _ytBootstrap УДАЛЁН.
                // YouTube не использует атрибут poster= на <video> — всё превью через CSS
                // background-image в .ytp-cued-thumbnail-overlay-image, который создаётся
                // polymer JS ПОСЛЕ DCL → элемент не существует в момент этого вызова.
                // #thumbnail img[src*="/vi/"] совпадает с рекомендательными превью боковой
                // панели (серверный рендер, есть в DOM при DCL), не с текущим видео →
                // риск preload рандомной рекомендации с fetchPriority='high'.
                // _preloadPoster() остаётся в boostMainVideo/loadstart для хостов
                // которые реально используют атрибут poster= (direct MP4, custom players).
                let c = 'ytd-masthead,#masthead-container{will-change:transform}' +
                    'ytd-rich-shelf-renderer[is-shorts],ytd-reel-shelf-renderer,#shorts-container{display:none!important}' +
                    // v3.1.0: CSS-изоляция рекомендаций и комментариев.
                    // Kimi: content-visibility:auto снижает raster/layout cost без влияния на сеть.
                    // contain-intrinsic-size — эвристика для сайдбара, предотвращает scrollbar jump.
                    // Не скрываем и не убираем из DOM — только откладываем рендер до viewport.
                    '#secondary,#comments,ytd-watch-next-secondary-results-renderer' +
                    '{content-visibility:auto;contain-intrinsic-size:300px 200px}' +
                    // v3.0.18: расширенный набор рекламных селекторов (из YouTube Ad-Bypass 2025-2026)
                    // .ad-showing>video / .ad-interrupting>video — НЕ скрываем:
                    //   YouTube переиспользует один <video> для рекламы и контента;
                    //   скрытие сломает boostMainVideo и canplay основного видео.
                    'ytd-ad-slot-renderer,ytd-promoted-sparkles-web-renderer,ytd-promoted-video-renderer,' +
                    '#player-ads,ytd-in-feed-ad-layout-renderer,' +
                    'ytd-rich-item-renderer:has(ytd-ad-slot-renderer),' +
                    '#masthead-ad,.ytp-ad-player-overlay,.ytp-ad-message-container,' +
                    '.yt-mealbar-promo-renderer,' +
                    'tp-yt-paper-dialog:has(#feedback.ytd-enforcement-message-view-model)' +
                    '{display:none!important}' +
                    // v3.2.1: YouTube Ads-Bypass — дополнительные безопасные рекламные элементы.
                    // 11 новых селекторов прошедших отбор DeepSeek (без активного скипа рекламы:
                    // это задача uBlock. Без .ad-showing>video / .ad-interrupting>video — один <video>
                    // используется и для рекламы и для контента, скрытие сломает boostMainVideo).
                    // Без .ytp-cued-thumbnail-overlay — превью при перемотке, легитимный UX.
                    // Без #root.yt-chips-search-renderer-header-v2 — слишком общий, ломает UI.
                    // Без div:has(> div#banner) — потенциальные ложные срабатывания.
                    'ytd-player-legacy-desktop-watch-ads-renderer,' +
                    '.ytp-ad-player-overlay-layout__player-card-container,' +
                    '.ytp-ad-player-overlay-layout__ad-info-container,' +
                    '.ytp-ad-player-overlay-layout__ad-disclosure-banner-container,' +
                    'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"],' +
                    'ytmusic-mealbar-promo-renderer,' +
                    '.ytd-video-masthead-ad-v3-renderer,' +
                    'ytd-ad-selection-preview-renderer,' +
                    '.ytp-ad-image-overlay,' +
                    '.ytp-ad-avatar,' +
                    '.ytp-ad-button-vm' +
                    '{display:none!important}' +
                    'yt-img-shadow{background-color:transparent!important}' +
                    '.ytp-ambient-light,.ytp-ambient-mode-enabled,ytd-watch-flexy[ambient-mode-enabled] .ytp-ambient-light{display:none!important}' +
                    'ytd-watch-flexy,#cinematics{backdrop-filter:none!important}' +
                    '#comments,#secondary,ytd-watch-next-secondary-results-renderer{contain:layout style paint}';
                if (isFirefox) c += 'html{scrollbar-width:thin}';
                // v3.0.16-fix7: скрываем hover-превью на шкале — только EXTREME.
                // В обычном режиме пользователи ориентируются по превью при перемотке.
                // v3.0.26: pause-overlay и endscreen — все режимы.
                // Консенсус Kimi/Grok/Qwen: эти оверлеи создают тяжёлый render tree
                // (сетка <img> с превью) и конкурируют за bandwidth с основным видео.
                // display:none до first paint → браузер не считает layout, не грузит превью.
                // .ytp-* — product-level классы HTML5-плеера, стабильны с 2015 года.
                // Рекомендации (ytd-watch-next-secondary-results-renderer) НЕ скрываем.
                c += '.ytp-pause-overlay,' +
                     '.ytp-fullscreen-grid-stills-container,.ytp-fullscreen-grid,' +
                     '.ytp-modern-videowall-still,.html5-endscreen,.videowall-endscreen' +
                     '{display:none!important}';
                if (EXTREME_MODE) c += '.ytp-inline-preview{display:none!important}';
                const s = document.createElement('style'); s.textContent = c; document.head.appendChild(s);
                try {
                    const f = window.yt?.config_?.EXPERIMENT_FLAGS;
                    if (f && typeof f === 'object') Object.assign(f, {
                        web_animated_actions: false, web_animated_like: false,
                        web_animated_like_lazy_load: false,
                        kevlar_watch_cinematics: false, web_cinematic_theater_mode: false,
                        web_cinematic_fullscreen: false, enable_cinematic_blur_desktop_loading: false,
                        kevlar_measure_ambient_mode_idle: false, smartimation_background: false,
                        kevlar_refresh_on_theme_change: false,
                    });
                } catch (e) { }
                const cm = document.querySelector('ytd-comments#comments');
                if (cm) { cm.style.contentVisibility = 'hidden'; new IntersectionObserver(e => { if (e[0].isIntersecting) cm.style.contentVisibility = ''; }, { rootMargin: '200px' }).observe(cm); }
            };
            if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', _ytBootstrap, { once: true });
            else _ytBootstrap();
        }

        /* ── _boostVideoElement: единая точка буста видеоэлемента (v3.4.0) ─────
         *  Консенсус KIMI/DeepSeek: вынести inline-буст из boostMainVideo в функцию
         *  чтобы её мог вызвать и boostMainVideo (через tryBoost) и prototype.play hook
         *  (для Shadow DOM видео которое querySelectorAll не видит).
         *  data-ihw-boosted — ренtrancy guard: ПЕРВОЙ строкой, до load()/play().
         *  v3.4.6: третий опциональный параметр t0 — точка отсчёта для метрики
         *  start:Nms. По умолчанию _spaT0 (старт скрипта/SPA-перехода) — поведение
         *  для обычных хостов (YouTube/Rutube/Odysee) не меняется.
         *  На бесконечных лентах (TikTok) без SPA-сброса _spaT0 не обновляется
         *  между видео в ленте — метрика монотонно росла бы (время с открытия
         *  вкладки), что бессмысленно. play-hook передаёт свой локальный t0 =
         *  момент перехвата нативного play() — самый ранний доступный сигнал
         *  «видео стало активным» (подтверждено диагностикой: play() опережает
         *  IntersectionObserver-кроссинг 0.9 на ~650мс) — без нового таймера/observer.
         * ──────────────────────────────────────────────────────────────────── */
        const _boostVideoElement = (main, source, t0 = _spaT0) => {
            if (!main || main.hasAttribute('data-ihw-boosted')) return false;
            main.setAttribute('data-ihw-boosted', 'true');  // замок — первой строкой
            // v3.4.3: страховка нулевой стоимости (KIMI/DeepSeek консенсус).
            // На переиспользуемых <video> (YouTube SPA) флаг может «залипнуть» от
            // предыдущего цикла раньше чем сюда дойдёт глобальная очистка в _resetVideoState.
            // Явный сброс здесь не вредит ни одному хосту и закрывает этот путь гонки.
            main._ihwHandled = false;
            log(`[IHW] Found main <video>: ${main.tagName} (via ${source})`);

            // v3.4.2: реордеринг — слушатели навешиваются ДО preload/load()/fetchPriority.
            // load() не вызывает события синхронно, реального риска не было, но порядок
            // "listeners → сетевая операция" безопаснее и единообразнее (DeepSeek).
            _videoLocked = true;
            const _lockSlow = _isConnSlow(false);
            const lockMs = (() => {
                const base = _lockSlow ? 2500 : 800;
                if (_unstableScore > 70) return Math.min(base * 3, 10000);
                if (_unstableScore > 40) return Math.min(base * 2, 6000);
                return base;
            })();
            log(`[IHW] Video Dominance Window: locked ${lockMs}ms (${_lockSlow ? 'slow' : 'fast'}, score:${_unstableScore})`);
            // v3.4.3: source-параметр (KIMI/DeepSeek консенсус) — явно показывает чем снят лок:
            // 'timeout' / 'canplay' / 'rVFC'. Раньше это приходилось угадывать по расстоянию
            // строк в логе (ненадёжная методология, источник долгой путаницы в диагностике Odysee).
            const _unlockVideo = (src) => { if (!_videoLocked) return; _videoLocked = false; log(`[IHW] Video Dominance Window: unlocked via ${src}`); };
            setTimeout(() => _unlockVideo('timeout'), lockMs);
            main.addEventListener('canplay', () => _unlockVideo('canplay'), { once: true });
            if (main.requestVideoFrameCallback) main.requestVideoFrameCallback(() => _unlockVideo('rVFC'));

            // v3.4.2: защита от listener leak при переиспользовании <video> (YouTube SPA).
            // stalled/waiting/loadstart — без {once:true} (должны жить весь срок жизни видео).
            // data-ihw-listeners — единообразно с data-ihw-boosted (DOM-атрибут, не JS-флаг):
            // видно в DevTools, не теряется при сериализации, симметрично сбрасывается
            // в _resetVideoState вместе с data-ihw-boosted.
			
			// v3.3.0: stalled/waiting → ре-активация Video Dominance Window.
            // Консенсус KIMI/DeepSeek: после снятия лока сеть может деградировать снова.
            // stalled = TCP «в тумане», данные не идут → жёсткий лок 3s + score++.
            // waiting = буфер кончился, сеть жива но медленная → только при slow, 1s.
            if (!main.hasAttribute('data-ihw-listeners')) {
                main.addEventListener('stalled', () => {
                    if (!_videoLocked) { _videoLocked = true; _unstableScore = Math.min(100, _unstableScore + 15); log('[IHW] Video stalled → IMG locked (3s)'); setTimeout(() => { _videoLocked = false; }, 3000); }
                });
                main.addEventListener('waiting', () => {
                    if (!_videoLocked && _isConnSlow(false)) { _videoLocked = true; setTimeout(() => { _videoLocked = false; }, 1000); log('[IHW] Video waiting + slow conn → IMG lock (1s)'); }
                });
				// v3.1.3 + v3.2.4: re-boost при SPA-переходе (loadstart).
                main.addEventListener('loadstart', () => {
                    // v3.4.4: сброс замка при переиспользовании узла (TikTok-подобные ленты).
                    // Условие && main._ihwHandled (не просто hasAttribute, консенсус DeepSeek
                    // против упрощения KIMI): _ihwHandled выставляется позже data-ihw-boosted
                    // (внутри _onVideoStart, асинхронно через canplay/loadeddata/rVFC). На
                    // быстром скролле readyState нередко 0 в момент play-hook буста — мы
                    // оказываемся в else-ветке _boostVideoElement, регистрируем слушатели,
                    // и loadstart СЛЕДУЮЩЕГО видео может сработать раньше чем ТЕКУЩИЙ буст
                    // успел подтвердить готовность. Безусловный сброс (просто hasAttribute)
                    // в этот момент снял бы замок с ещё не завершённого цикла — следующий
                    // play()-burst (тройной, по логу TikTok) повторно войдёт в синхронный
                    // путь _boostVideoElement (preconnect/шрифты/poster/querySelectorAll)
                    // для того же физического видео без необходимости. && _ihwHandled
                    // гарантирует: сбрасываем только когда предыдущий цикл буста реально
                    // завершился (метрика записана), не трогаем замок если буст "в полёте".
                    if (main.hasAttribute('data-ihw-boosted') && main._ihwHandled) {
                        main.removeAttribute('data-ihw-boosted');
                        main._ihwHandled = false;
                        log('[IHW] Reset boost lock on loadstart (node reuse)');
                    }
                    if (main.hasAttribute('data-ihw-boosted') && main.fetchPriority !== 'high') {
                        main.fetchPriority = 'high'; main.preload = 'auto';
                        log('[IHW] Re-boost fetchPriority on loadstart (SPA transition)');
						// v3.2.4: re-preload постера при SPA-переходе
                        const newPoster = main.poster || main.getAttribute('poster');
                        if (newPoster) _preloadPoster(newPoster);
                    }
                });
                main.setAttribute('data-ihw-listeners', '');
            }

            main.preload = 'auto';
            const _vSrc = main.currentSrc || main.src || '';
            if (!_vSrc.startsWith('blob:') && main.networkState <= 1) main.load();
            main.fetchPriority = 'high';

            _dynamicPreconnect(main);
            main.querySelectorAll('source').forEach(s => {
                if (s.src && !s.src.startsWith('blob:')) _preconnectFromUrl(s.src, 'source-el');
            });
            _deferFontsUntilCanplay(main);
			// v3.2.4: _preloadPoster с CSS.escape дедупликацией — без дублей при SPA.
            _preloadPoster(main.poster || main.getAttribute('poster') || '');
            main.querySelectorAll('source').forEach(s => { if (!s.fetchPriority) s.fetchPriority = 'high'; });
            const vs = main.src || main.currentSrc || '';
            if (vs.includes('.m3u8')) {
                try { const l = document.createElement('link'); l.rel = 'preload'; l.as = 'fetch'; l.fetchPriority = 'high'; l.href = vs; l.crossOrigin = 'anonymous'; document.head.appendChild(l); } catch (e) {}
            }
            main.setAttribute('playsinline', '');

            if (main.readyState >= 2) {
                // v3.4.5: main._ihwHandled = true добавлен сюда — критичный фикс.
                // Без этого узел, прошедший immediate-путь (readyState>=2 уже к моменту
                // буста — частый случай на TikTok при спокойном скролле, видео прогружено
                // заранее), никогда не получал _ihwHandled=true. Условие сброса в loadstart
                // (hasAttribute && _ihwHandled) для такого узла было вечно ложным →
                // data-ihw-boosted никогда не снимался → play-hook навсегда блокировался
                // на этом переиспользуемом узле (подтверждено логом: после нескольких
                // успешных циклов оба узла TikTok "залипали", дальнейший скролл молчал).
                main._ihwHandled = true;
                main.play().catch(e => { if (e.name !== 'NotAllowedError') log('[IHW] play() blocked:', e.message); });
                _canplayMs = Math.round(performance.now() - t0);
                log(`[IHW] Video ready immediately: ${_canplayMs}ms`);
                _flashBoostSuccess();
            } else {
					// v3.2.0-fix9: единый _onVideoStart — co-trigger для canplay, loadeddata и rVFC.
                    // Кто пришёл первым — тот снимает Video Dominance Window и логируется как start.
                    // canplay   = браузер готов воспроизводить (достаточно буфера)
                    // loadeddata = первый кадр декодирован (может прийти раньше на OK.ru)
                    // rVFC      = GPU нарисовал первый кадр 
                    // _ihwHandled атомарный флаг предотвращает двойной вызов play().
                const _onVideoStart = (ev) => {
                    if (main._ihwHandled) return; main._ihwHandled = true;
                    _canplayMs = Math.round(performance.now() - t0);
                    main.play().catch(e => { if (e.name !== 'NotAllowedError') log('[IHW] play() blocked:', e.message); });
                    log(`[IHW] Video ready via ${ev}: ${_canplayMs}ms`);
                    _flashBoostSuccess();
                };
				// 1. Навешиваем ДО проверки readyState
                main.addEventListener('canplay',    () => _onVideoStart('canplay'),    { once: true });
                main.addEventListener('loadeddata', () => _onVideoStart('loadeddata'), { once: true });
				// 2. rVFC: первый реально нарисованный GPU кадр (Chromium only, Firefox — нет rVFC).
                if (main.requestVideoFrameCallback) {
                    main.requestVideoFrameCallback(() => {
                        _firstFrameMs = Math.round(performance.now() - t0);
                        if (!main._ihwHandled) _onVideoStart('rVFC');
                        else log(`[IHW] First frame rendered: ${_firstFrameMs}ms`);
                    });
                }
				// v3.4.3: хвостовая проверка `if (readyState>=2 && currentSrc) _onVideoStart('immediate')`
				// УДАЛЕНА — недостижимый код. Между входом в этот else (readyState<2 подтверждён
				// строкой выше) и этим местом выполняется только синхронный JS без yield в event
				// loop — readyState физически не может измениться браузером в середине нашего
				// синхронного блока. Условие здесь гарантированно тождественно условию входа в else
				// (false), строка никогда не выполнялась ни на одном хосте. Сам внешний if/else
				// (readyState>=2 проверка на входе в функцию) — оставлен, он реален и нужен.
            }
            return true;
        };

        const boostMainVideo = () => {
            const videos = _findVideosDeep();
            const visible = videos.filter(v => {
                const rect = v.getBoundingClientRect();
                return rect.width > 0 && rect.height > 0 && rect.top < window.innerHeight * 1.5;
            });

            if (visible.length) {
                const main = visible.reduce((a, b) => {
                    const sa = a.getBoundingClientRect(), sb = b.getBoundingClientRect();
                    return (sb.width * sb.height || 1) > (sa.width * sa.height || 1) ? b : a;
                });
                return _boostVideoElement(main, 'tryBoost');
            }

            const iframes = [...document.querySelectorAll('iframe')].filter(fr => {
                if (fr.offsetWidth < 200 || fr.offsetHeight < 100) return false;
                const s = (fr.src || fr.name || fr.id || fr.className || '').toLowerCase();
                return /video|player|embed|rutube|vimeo|vk|ok\.ru|dzen|yandex|twitch|dailymotion|bilibili|tiktok/i.test(s);
            });
            if (iframes.length) {
                const main = iframes.reduce((a, b) => (b.offsetWidth * b.offsetHeight) > (a.offsetWidth * a.offsetHeight) ? b : a);
                log(`[IHW] Main video <iframe>: ${main.offsetWidth}x${main.offsetHeight}`);
                if (main.dataset.lazySrc) { main.src = main.dataset.lazySrc; delete main.dataset.lazySrc; }
                main.setAttribute('data-ihw-boosted', 'true'); main.loading = 'eager'; main.fetchPriority = 'high';
                // v3.2.0-fix3: iframe src — прямой URL, не MSE. Передаём как простой объект.
                if (main.src) {
                    const _iv = { currentSrc: main.src, src: main.src, readyState: 1, addEventListener: () => {} };
                    _dynamicPreconnect(_iv);
                }
                return true;
            }

            // v3.0.15: VK/OK — ускоряем контейнер, не ждём <video>
            const isVKOK = /vk\.com|vkvideo\.ru|ok\.ru/.test(location.hostname);
            const custom = document.querySelector('[class*="player"],[id*="player"],[class*="Player"],[id*="Player"],[data-video],[data-player],[data-src*="video"]');
            if (custom && custom.offsetWidth > 200) {
                log(`[IHW] Custom player wrapper: <${custom.tagName}>#${custom.id || '(no-id)'} .${(custom.className || '').split(' ')[0]}`);
                custom.setAttribute('data-ihw-boosted', 'true');
                if (isVKOK) {
                    custom.style.contentVisibility = 'visible';
                    log('[IHW] VK/OK container boost (no <video> in DOM)');
                    // v3.4.0: document-level play listener (fix6-fix9, OK.ru-специфичный,
                    // composedPath/event-retargeting workarounds) УДАЛЁН.
                    // Заменён HTMLMediaElement.prototype.play hook (см. ниже, после tryBoost) —
                    // `this` внутри hook'а = РЕАЛЬНЫЙ <video> элемент напрямую, без composedPath,
                    // без привязки к конкретному Shadow Root/контейнеру, без WeakSet.
                    // VK (closed shadow в cross-origin iframe vkvideo.ru) — известное
                    // ограничение: prototype-патч действует только в своём JS-realm.
                    // Если @match покрывает iframe-домен и нет @noframes — hook сработает и там
                    // (отдельный экземпляр скрипта в iframe). Если @noframes — VK остаётся
                    // нерешённым случаем, как и было до v3.4.0. OK.ru — same-document closed
                    // shadow, hook работает гарантированно.
                }
                return isVKOK ? 'custom-vk' : 'custom';
            }

            log('[IHW] No main video or player wrapper found');
            return false;
        };
        // v3.0.23 fix: присваиваем ref сразу после объявления функции,
        // чтобы processNode/MO мог вызвать её до load event без TDZ.
        _boostRef = boostMainVideo;

        /* ── HTMLMediaElement.prototype.play hook (v3.4.0, обобщён в v3.4.4) ──
         *  Изначально только для OK.ru (Shadow DOM). Диагностика TikTok (диалог
         *  с многоAI консенсусом) показала: тот же hook — единственный рабочий
         *  триггер для ЛЮБОГО видеохостинга с бесконечной лентой (TikTok,
         *  потенциально Reels/Shorts), где плеер переиспользует пул из N
         *  <video>-узлов, подменяя им src без пересоздания DOM-узла.
         *  MutationObserver принципиально не видит такую смену — он реагирует
         *  на добавление/удаление узлов, а не на смену атрибутов существующего.
         *  pushState/SPA-машинерия тоже не подходит — на TikTok при скролле
         *  нет ни одного вызова pushState/replaceState (подтверждено логом).
         *  play() же вызывается плеером САМ при каждой смене активной карточки
         *  (подтверждено: тройной burst за 2-5мс на каждом переходе) и
         *  опережает альтернативу (IntersectionObserver) на ~650мс по логу —
         *  IntersectionObserver отклонён не из экономии кода, а как объективно
         *  более медленный/неоднозначный триггер для этого UX-паттерна.
         *
         *  PAGE==='Video Content' guard — критичен: hook вызывает _boostVideoElement
         *  напрямую, минуя boostMainVideo (где раньше был единственный PAGE-чек).
         *  Без него лендинг с фоновым <video autoplay loop muted> на Mixed Content
         *  получил бы агрессивный буст, не предназначенный для таких страниц.
         *  Проверяется ПЕРВОЙ — до hasAttribute и до getBoundingClientRect.
         *
         *  Геометрия — абсолютные пиксели (200×150) + видимость во вьюпорте,
         *  НЕ проценты от innerWidth/innerHeight (консенсус DeepSeek/Qwen/KIMI):
         *  процентный порог либо слишком жёсткий для десктопа (YouTube-плеер
         *  часто занимает 60-70% ширины окна, не 90%+), либо слишком мягкий
         *  для отсева превью на разных разрешениях. Абсолютные 200×150 —
         *  тот же порог что уже используется для отсева мусора в коде, единый
         *  для mobile/desktop (isMobile-ветвление обсуждалось и отклонено как
         *  усложнение без доказанного выигрыша — ни один реальный кейс не
         *  показал разницу между 150×100 и 200×150 в пользу первого).
         *
         *  Reentrancy: data-ihw-boosted (первая строка _boostVideoElement) — замок.
         *  !_videoBoosted убран из условия — глобальный флаг создан под модель
         *  "одно видео за переход", на ленте он навечно блокирует повторный
         *  буст после первого успеха без единого события которое его сбросит.
         *  data-ihw-boosted на самом элементе — достаточная защита от реентерации.
         *
         *  VK (cross-origin iframe, closed shadow) — известное ограничение:
         *  prototype-патч действует только в своём JS-realm. Если @match
         *  покрывает iframe-домен и нет @noframes — сработает и там. OK.ru
         *  (same-document closed shadow) — работает гарантированно, как и раньше.
         * ──────────────────────────────────────────────────────────────────── */
        try {
            const _origPlay = HTMLMediaElement.prototype.play;
            HTMLMediaElement.prototype.play = function() {
                if (this.tagName === 'VIDEO' && PAGE === 'Video Content' && !this.hasAttribute('data-ihw-boosted')) {
                    const el = this;
                    // v3.4.6: _playT0 — момент перехвата нативного play(), отдельная точка
                    // отсчёта для метрики на бесконечных лентах (см. комментарий у t0
                    // параметра _boostVideoElement выше). Захватывается здесь, а не внутри
                    // rAF — play() и есть самый ранний сигнал «видео стало активным».
                    const _playT0 = performance.now();
                    requestAnimationFrame(() => {
                        if (el.hasAttribute('data-ihw-boosted')) return;
                        const r = el.getBoundingClientRect();
                        if (r.width > 200 && r.height > 150 && r.top < window.innerHeight && r.bottom > 0) {
                            const ok = _boostVideoElement(el, 'play-hook', _playT0);
                            // v3.4.5: _videoBoosted=true восстановлен — критичный фикс.
                            // Убран в v3.4.4 с обоснованием "на TikTok tryBoost уже исчерпан
                            // к моменту скролла, флаг ни на что не влияет" — верно для TikTok,
                            // но неверно как общее правило. На OK.ru play-hook может успешно
                            // забустить видео ВНУТРИ окна ретраев tryBoost (видео стартует на
                            // ~30с, что меньше суммарного окна ~8 попыток с экспоненциальной
                            // задержкой ~45-50с). Без этого флага tryBoost не получал сигнала
                            // об успехе и продолжал бесполезные попытки до 8/8 даже после
                            // того как видео уже было найдено и играло (подтверждено логом).
                            // На TikTok эта строка не влияет на способность hook'а бустить
                            // СЛЕДУЮЩИЕ видео в ленте — условие самого hook'а (выше) проверяет
                            // только per-элементный data-ihw-boosted, не этот глобальный флаг.
                            if (ok) {
                                _videoBoosted = true;
                                // v3.4.6: симметрично tryBoost'у ("Main video boosted successfully") —
                                // play-hook раньше не подтверждал завершение буста, только "Found".
                                log('[IHW] Main video boosted successfully (play-hook)');
                            }
                        }
                    });
                }
                return _origPlay.apply(this, arguments);
            };
            log('[IHW] HTMLMediaElement.prototype.play hook installed (universal)');
        } catch (e) { log('[IHW] prototype.play hook failed:', e.message); }

        let _boostAttempt = 0;
        // v3.4.0: динамический _maxAttempts через _isConnSlow(false) — без новой метрики,
        // используем уже существующий governor. На slow conn (rtt/downlink/3g/saveData/TTFB)
        // OK.ru/VK SPA-плеер может инициализироваться дольше 15с (старое окно: 8 попыток).
        // 12 попыток с тем же экспоненциальным расписанием (хардлимит 10000ms на попытку,
        // см. tryBoost) даёт суммарное окно ~50-60с — покрывает медленный SPA без хардкода
        // под конкретный хостинг. Пересчитывается в _resetVideoState на каждый SPA-переход
        // (качество сети могло измениться).
        let _maxAttempts = PAGE === 'Video Content' ? (_isConnSlow(false) ? 12 : 8) : 4;
        let _maxCustomAttempts = _maxAttempts - 1; // v3.0.25: соотношение 8→7 / 4→3 сохранено

        const tryBoost = () => {
            if (_videoBoosted) return;
            log(`[IHW] Boost attempt ${_boostAttempt + 1}/${_maxAttempts}...`);

            const result = boostMainVideo();
            if (result === true) {
                _videoBoosted = true;
                log('[IHW] Main video boosted successfully');
                return;
            }

            if (result === 'custom' || result === 'custom-vk') {
                log('[IHW] Custom player wrapper detected, waiting for native <video>...');
                if (_boostAttempt >= _maxCustomAttempts) {
                    log('[IHW] Custom player confirmed (no native <video> found after all attempts)');
                    return;
                }
            } else {
                log('[IHW] No video element found yet');
            }

            if (_boostAttempt >= _maxAttempts) {
                log('[IHW] Boost attempts exhausted (no video found)');
                return;
            }

            // v3.3.0: адаптивне розкладання retry.
            // slow+нестабильное: больше времени на инициализацию polymer/React при плохой сети.
            // Консенсус KIMI/DeepSeek: slow first=600ms, fast=300ms, хардлимит 10000ms.
            const _slow = _isConnSlow(true);
            const d = _boostAttempt === 0
                ? (_slow ? 600 : 300)
                : Math.min((_slow ? 2000 : 1500) * Math.pow(2, _boostAttempt - 1), 10000);
            _boostAttempt++;
            log(`[IHW] Retrying boost in ${d}ms`);
            setTimeout(tryBoost, d);
        };
        if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', tryBoost, { once: true });
        else tryBoost();

        // v3.3.0: online → перезапуск boost (внутри Video Content — доступ к tryBoost/_boostAttempt).
        // Если все 8 попыток исчерпались пока сеть была недоступна, скрипт молчал вечно.
        // Консенсус KIMI/DeepSeek: 300ms задержка (DNS/TCP успевает восстановиться), score −30.
        // Не создаём новый listener для _resetConnCache — он уже в outer scope.
        window.addEventListener('online', () => {
            if (!_videoBoosted) {
                _unstableScore = Math.max(0, _unstableScore - 30);
                setTimeout(() => {
                    if (!_videoBoosted) {
                        _boostAttempt = 0;
                        _resetConnCache();
                        log('[IHW] Connection restored → restarting boost');
                        tryBoost();
                    }
                }, 300);
            }
        });

        /* ── SPA-НАВИГАЦИЯ: СБРОС RUNTIME-СОСТОЯНИЯ (фикс Nav2/Nav3) ── */
        // v3.1.7: _videoBoosted=true оставался от предыдущей страницы при SPA-переходе →
        // tryBoost пропускал новую страницу. Три слоя: быстрый (YouTube-события) →
        // универсальный (History API) → надёжный (title MO).
        // Консенсус DeepSeek/Claude/Kimi: setTimeout(50) — не медленный fallback, а
        // необходимая синхронизация с polymer: yt-navigate-start срабатывает ДО удаления
        // старого DOM. Promise.resolve().then() (Qwen) выполняется в том же тике —
        // querySelectorAll('video') найдёт стale элемент → tryBoost исчерпает попытки.
        // replaceState НЕ патчим: YouTube вызывает его при обновлении таймкода (?t=120).
        // FLAGS (LCP_IMAGE_BOOST, EXTREME_MODE и др.) остаются статичными — контракт архитектуры.
        // TTFB при SPA = нерелевантен (нет нового HTTP-запроса) → _lastSlowTs сбрасываем
        // чтобы _isSlowNow() перечитал navigator.connection.rtt/downlink актуально.
        let _lastNavUrl = location.href;
        let _resetTimer = null;

        const _resetVideoState = (source) => {
            if (location.href === _lastNavUrl) return;  // не SPA (replaceState для таймкода)
            _lastNavUrl = location.href;
            _videoBoosted = false;
            _boostAttempt = 0;
            _videoLocked  = false;
            _resetConnCache();
            _canplayMs    = 0;
            _firstFrameMs = 0;      // v3.2.0-fix8: сброс метрики первого кадра
            _spaT0        = performance.now();
            // v3.4.2: глобальная очистка всех <video> на странице (DeepSeek + Qwen + KIMI).
            // Root cause бага "video already playing, but boost retries fail forever":
            // YouTube переиспользует тот же <video> между SPA-переходами. data-ihw-boosted
            // от предыдущего видео никогда не снимался → _boostVideoElement (guard на
            // 1-й строке) возвращал false → tryBoost логировал "No video element found yet"
            // все 8/12 попыток, хотя видео реально играло.
            // querySelectorAll('video') не пронизывает closed Shadow DOM (OK.ru) — но там
            // при SPA создаётся НОВЫЙ <video> без атрибутов, очистка для него не нужна (no-op).
            // data-ihw-listeners снимается синхронно с data-ihw-boosted — семантически связаны
            // (оба означают "видео обработано в текущей сессии"), иначе на повторном проходе
            // _boostVideoElement не навесит stalled/waiting/loadstart заново.
            // _ihwHandled (JS-флаг _onVideoStart) — сбрасывается чтобы canplay/loadeddata/rVFC
            // нового видео снова сработали (иначе метрики/play() не пишутся для второго видео).
            document.querySelectorAll('video').forEach(v => {
                v.removeAttribute('data-ihw-boosted');
                v.removeAttribute('data-ihw-listeners');
                v._ihwHandled = false;
            });
            _dynamicPreconnectedCount = 0;
            _dynamicPreconnectedSet.clear();
            // v3.2.2-fix1: сброс _logged флага для повторного статического preconnect.
            // На SPA флаг оставался true → повторный вызов initVideoPreconnect не логировал ничего,
            // но и link-теги дедуплицировались → видно 7+ пустых вызовов без логов в fix5-fix7.
            initVideoPreconnect._logged = false;
            // v3.4.0: OK.ru play-listener cleanup убран — заменён HTMLMediaElement.prototype.play
            // hook (глобальный, патчится один раз, не привязан к DOM-элементу/Shadow Root).
            // _okBoostedVideos/data-ihw-play-listener/data-ihw-ok-observer больше не существуют.
            // v3.2.0: логируем _isSlowNow() после сброса кэша — помогает понять
            // применяет ли governor slow/fast режим на новой SPA-странице
            const _nowSlow = _isConnSlow(false); // свежие данные без кэша после SPA-перехода
            // v3.4.0: пересчёт _maxAttempts на новой SPA-странице — качество сети могло
            // измениться (VPN переключил сервер, LTE→WiFi). Используем _nowSlow — то же
            // измерение, без повторного вызова _isConnSlow (нет лишнего сегмента в _netSegments).
            _maxAttempts = PAGE === 'Video Content' ? (_nowSlow ? 12 : 8) : 4;
            _maxCustomAttempts = _maxAttempts - 1;
            if (DEBUG) {
                const _r = [
                    isMobile                                                     && 'mobile',
                    _conn?.saveData                                               && 'saveData',
                    _conn && ['slow-2g','2g','3g'].includes(_conn.effectiveType)  && `ect:${_conn.effectiveType}`,
                    _conn && _conn.rtt > 300                                      && `rtt:${_conn.rtt}ms`,
                    _conn && _conn.downlink < 2                                   && `dl:${_conn.downlink}Mbps`,
                    (_ttfb > 600)                                                 && `TTFB:${_ttfb}ms`,
                ].filter(Boolean).join(', ');
                log(`[IHW] SPA reset (${source}) → _isSlow=${_nowSlow}${_r ? ' (' + _r + ')' : ''}`);
            } else {
                log(`[IHW] SPA reset (${source}) → state cleared`);
            }
            if (_resetTimer) return;
            _resetTimer = setTimeout(() => {
                _resetTimer = null;
                if (!_videoBoosted) tryBoost();
            }, 50);
        };

        // Tier 1: YouTube-события (быстрые, хрупкие — wrapped в try-catch)
        if (/youtube\.com|youtu\.be/.test(location.hostname)) {
            try {
                // start: ДО polymer-рендера — сброс максимально ранний
                // finish: ПОСЛЕ рендера — дополнительный шанс если start не сработал
                window.addEventListener('yt-navigate-start',  () => _resetVideoState('yt-start'),  { passive: true });
                window.addEventListener('yt-navigate-finish', () => _resetVideoState('yt-finish'), { passive: true });
            } catch (e) { log('[IHW] yt-navigate hooks unavailable (fallback active)'); }
        }

        // Tier 2: History API — универсально для любого SPA (VK, Vimeo, Rutube и др.)
        const _origPush = history.pushState;
        history.pushState = function() { _origPush.apply(this, arguments); _resetVideoState('pushState'); };
        window.addEventListener('popstate',   () => _resetVideoState('popstate'),   { passive: true });
        window.addEventListener('hashchange', () => _resetVideoState('hashchange'), { passive: true });

        // Tier 3: Title MO — надёжный fallback (YouTube меняет <title> при каждом SPA-переходе)
        // Один observer на один элемент (не subtree) — минимальная нагрузка.
        // Дешевле чем проверка location.href в основном MO на каждой из тысяч нод.
        const _titleEl = document.querySelector('title');
        if (_titleEl) new MutationObserver(() => {
            if (location.href !== _lastNavUrl) _resetVideoState('title-mo');
        }).observe(_titleEl, { childList: true });

        if (PAUSE_ON_HIDDEN) {
            document.addEventListener('visibilitychange', () => {
                const vs = [...document.querySelectorAll('video')].filter(v => v.offsetWidth > 0 && v.offsetHeight > 0);
                if (!vs.length) return;
                const main = vs.reduce((a, b) => b.offsetWidth * b.offsetHeight > a.offsetWidth * a.offsetHeight ? b : a);
                if (document.pictureInPictureElement) return;
                if (document.hidden) {
                    main._ihw_wasPlaying = !main.paused;
                    if (main._ihw_wasPlaying) main.pause();
                } else {
                    if (main._ihw_wasPlaying) { main.fetchPriority = 'high'; main.play().catch(() => { }); }
                    main._ihw_wasPlaying = undefined;
                }
            });
        }
    }

    /* ── MIXED CONTENT ──────────────────────────────────── */
    if (PAGE === 'Mixed Content') {
        // v3.1.7: boost fetchPriority для <link rel="preload" as="image"> в исходном HTML.
        // Браузер ставит medium на preload-image без явного fetchpriority.
        // Hero-картинки часто объявлены именно так. Один синхронный querySelectorAll при DCL —
        // до первого paint. Не дублирует LCP Observer: тот реагирует post-paint.
        const _boostPreloadImages = () => {
            document.querySelectorAll('link[rel="preload"][as="image"]').forEach(l => {
                if (!l.fetchPriority || l.fetchPriority === 'auto') {
                    l.fetchPriority = 'high';
                    log('[IHW] preload[as=image] → fetchPriority:high (IHW override):', l.href?.slice(0, 80));
                } else {
                    // браузер уже назначил приоритет — соглашаемся
                    log(`[IHW] preload[as=image] agreed with browser (${l.fetchPriority}):`, l.href?.slice(0, 80));
                }
            });
        };
        if (document.readyState === 'loading')
            document.addEventListener('DOMContentLoaded', _boostPreloadImages, { once: true });
        else _boostPreloadImages();
        const lazyIframeObserver = new IntersectionObserver(entries => {
            for (const e of entries) if (e.isIntersecting && e.target.dataset.lazySrc) {
                e.target.src = e.target.dataset.lazySrc; delete e.target.dataset.lazySrc; lazyIframeObserver.unobserve(e.target);
            }
        }, { rootMargin: '300px' });

        // v3.0.20: безопасная установка preload/autoplay для video.
        // НЕ трогаем video с пустым/about:blank src и без <source> детей:
        //   браузер попытается загрузить placeholder как медиа → ERR_UNKNOWN_URL_SCHEME
        //   → video переходит в ERROR state → сайт реактивно блокирует overlay-кнопку play.
        // Также пропускаем video помеченные как ihw-boosted (уже под нашим контролем).
        const _safeSetVideoMeta = (v) => {
            if (v.hasAttribute('data-ihw-boosted')) return;
            const src = v.currentSrc || v.getAttribute('src') || '';
            const hasSources = v.querySelector('source[src]') !== null;
            if (!src && !hasSources) return;                       // нет src вообще
            if (/^about:|^javascript:/i.test(src)) return;        // placeholder src
            v.autoplay = false;
            v.preload = 'metadata';
        };

        const initMixed = () => {
            // v3.0.16: на AI-чатах применяем только безопасное (async decoding,
            // autoplay off), но НЕ трогаем lazy-iframe и скроллеры.
            if (shouldSkipScroll) {
                document.querySelectorAll('img').forEach(img => { if (!img.decoding) img.decoding = 'async'; });
                document.querySelectorAll('video').forEach(v => { _safeSetVideoMeta(v); });
                return;
            }
            const vh = window.innerHeight;
            document.querySelectorAll('img').forEach(img => {
                if (!img.decoding) img.decoding = 'async';
            });
            [...document.querySelectorAll('iframe')].forEach(fr => {
                const top = fr.getBoundingClientRect().top;
                if (top > vh * 2 && fr.src) {
                    if (isInsideScroller(fr)) {
                        log('[IHW] lazy-iframe: пропущен (внутри скроллера)');
                        return;
                    }
                    const rect = fr.getBoundingClientRect();
                    const pb = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
                    if (fr.offsetHeight < 120 && rect.top > pb - vh * 1.5) return;
                    fr.dataset.lazySrc = fr.src; fr.removeAttribute('src'); lazyIframeObserver.observe(fr);
                }
            });
            document.querySelectorAll('video').forEach(v => { _safeSetVideoMeta(v); });
        };
        if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initMixed, { once: true });
        else initMixed();
    }

    /* ── MOBILE ─────────────────────────────────────────── */
    if (isMobile) {
        const _mh = document.createElement('style');
        _mh.textContent = '@media(hover:none){*{transition:none!important;animation:none!important}}' +
            'a,button,[role="button"],input,select,textarea,label,summary' +
            '{touch-action:manipulation;-webkit-tap-highlight-color:transparent}';
        (document.head || document.documentElement).appendChild(_mh);
        document.addEventListener('play', e => {
            const el = e.target;
            if ((el.tagName === 'VIDEO' || el.tagName === 'AUDIO') && el.autoplay) { el.pause(); el.autoplay = false; }
        }, { capture: true, passive: true });
        document.addEventListener('touchstart', () => { }, { passive: true });
        window.addEventListener('wheel', () => { }, { passive: true });
    }

    /* ── КНОПКА (не-OFF) ────────────────────────────────── */
    _renderBtn();

    /* ── DNS PREFETCH (v3.0.16-fix4: обёртка с гарантированным fallback) ── */
    let dnsPrefetchDone = false;
    
    // Надёжная обёртка: requestIdleCallback → setTimeout fallback
    const _runWhenIdle = (fn, timeout = 2000) => {
        if (window.requestIdleCallback) {
            const id = requestIdleCallback(fn, { timeout });
            // Двойная страховка: если idle не вызовется за timeout, force через setTimeout
            setTimeout(() => cancelIdleCallback(id), timeout + 100);
        } else {
            setTimeout(fn, timeout);
        }
    };

    const addDnsPrefetch = domains => {
        if (dnsPrefetchDone || !domains.length) return;
        const head = document.head || document.documentElement;
        domains.forEach(d => { 
            const l = document.createElement('link'); 
            l.rel = 'dns-prefetch'; 
            l.href = '//' + d; 
            head.appendChild(l); 
        });
        dnsPrefetchDone = true;
        log('[IHW] DNS prefetch added:', domains);
    };

    const createSecondScreenSentinel = () => {
        if (dnsPrefetchDone) { log('[IHW] Sentinel: already done'); return; }
        if (document.querySelector('.ihw-sentinel')) { log('[IHW] Sentinel: already exists'); return; }
        
        log('[IHW] Sentinel: creating...');
        const s = document.createElement('div');
        s.className = 'ihw-sentinel';
        s.style.cssText = 'position:absolute;top:2200px;left:0;width:1px;height:1px;pointer-events:none;visibility:hidden';
        document.documentElement.appendChild(s);

        let obs = null;
        let fallbackTimer = null;
        
        const cleanup = () => {
            if (obs) { try { obs.disconnect(); } catch(e) {} obs = null; }
            if (fallbackTimer) { clearTimeout(fallbackTimer); fallbackTimer = null; }
            if (s.parentNode) s.remove();
        };

        const doScan = (source) => {
            if (dnsPrefetchDone) return;
            cleanup();
            
            const ext = new Set();
            document.querySelectorAll('a[href^="http"], img[src^="http"], iframe[src^="http"]').forEach(el => {
                try { 
                    const h = new URL(el.href || el.src, location.origin).hostname; 
                    if (h && !h.endsWith(location.hostname)) ext.add(h); 
                } catch {}
            });
            
            const list = [...ext].slice(0, 10).filter(d => !isTracker('https://' + d));
            if (list.length) {
                addDnsPrefetch(list);
            } else {
                log('[IHW] Sentinel: no clean domains');
                sessionStorage.setItem('ihw:dns_done:' + location.hostname, '1');
            }
        };

        // Пробуем через IntersectionObserver (пользователь скроллит)
        try {
            obs = new IntersectionObserver(entries => {
                if (entries[0].isIntersecting) {
                    log('[IHW] Sentinel: viewport intersected');
                    doScan('observer');
                }
            }, { rootMargin: '500px 0px' });
            obs.observe(s);
        } catch(e) {
            log('[IHW] Sentinel: observer failed, force scan');
            doScan('observer-fail');
            return;
        }

        // Fallback 1: через 3с если страница короткая (sentinel уже в viewport)
        fallbackTimer = setTimeout(() => {
            const rect = s.getBoundingClientRect();
            if (rect.top < window.innerHeight + 500) {
                log('[IHW] Sentinel: fallback short-page');
                doScan('fallback-short');
            }
        }, 3000);

        // Fallback 2: через 8с в любом случае (force scan)
        setTimeout(() => {
            if (!dnsPrefetchDone) {
                log('[IHW] Sentinel: fallback force');
                doScan('fallback-force');
            }
        }, 8000);
    };

    /* ── ФИНАЛИЗАЦИЯ ────────────────────────────────────── */
    const onLoadHandler = () => {
        if (_initDone) return;
        _initDone = true;
        log('[IHW] Finalize: running');
        if (PAGE === 'Mixed Content') {
            _runWhenIdle(runRenderOpts, 500);
            log('[IHW] Finalize: launching sentinel');
            createSecondScreenSentinel();
        }
        setTimeout(() => mo.disconnect(), PAGE === 'Video Content' ? 30000 : 4000);
        document.querySelectorAll('noscript').forEach(n => n.remove());
        if (EXTREME_MODE) {
            document.querySelectorAll('video:not([data-ihw-boosted])').forEach(v => { try { v.disablePictureInPicture = true; } catch (e) { } });
        }
        if (DEBUG) _logExtendedMetrics(_getModeLabel());
    };

    // v3.0.16-fix5: гарантированный запуск — если уже complete, вызываем сразу
    if (document.readyState === 'complete') {
        log('[IHW] Finalize: document already complete, calling directly');
        onLoadHandler();
    } else {
        log('[IHW] Finalize: waiting for load event');
        window.addEventListener('load', onLoadHandler, { once: true });
        // v3.0.23: принудительный запуск через 8с если window.load не пришёл.
        // Сценарий: сторонний скрипт (Google Translate, Яндекс.Метрика) зависает
        // или получает ERR_TIMED_OUT (30с!) → load event задерживается на всё это время
        // → Finalize, sentinel и runRenderOpts не запускаются → страница выглядит «зависшей».
        // 8с — достаточно для нормальной загрузки, раньше Google Translate timeout (30с).
        setTimeout(() => {
            if (!_initDone) {
                log('[IHW] Finalize: hard timeout — load event не пришёл за 8с, запускаем принудительно');
                onLoadHandler();
            }
        }, 8000);
    }

})();