Raw Source
Nvirya / Nvirya AdGuard

// ==UserScript==
// @name         Nvirya AdGuard
// @license      MIT
// @namespace    https://nvirya.com/adguard
// @version      10.4.1.8
// @updateURL    https://raw.nvirya.com/adguard/nvirya-adguard.user.js
// @downloadURL  https://raw.nvirya.com/adguard/nvirya-adguard.user.js
// @description  A lightweight, mobile-first userscript designed to block intrusive ads, popups, redirects, and overlays with a native-style UI and custom element picker.
// @author       Nvirya
// @match        *://*/*
// @icon         https://raw.nvirya.com/assets/icon128x128.png
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      raw.nvirya.com
// @connect      raw.nvirya.com
// @connect      discord.com
// @connect      discordapp.com
// @run-at       document-start
// @all-frames   true
// ==/UserScript==
(function () {
'use strict';

const W = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const D = W.document;
if (W.__NVIRYA_ADGUARD_X__) return;
try { Object.defineProperty(W, '__NVIRYA_ADGUARD_X__', { value: true }); } catch (e) { W.__NVIRYA_ADGUARD_X__ = true; }
const VERSION = '10.4.1.8';
const CONFIG_VERSION = 4;
const K_CFG      = 'nvirya_x_config';
const K_WL       = 'nvirya_x_whitelist';
const K_SR       = 'nvirya_x_site_rules';
const K_ST       = 'nvirya_x_stats';
const K_SESS     = 'nvirya_x_session';
const K_LAST_REP = 'nvirya_x_last_report';
const UPDATE_URL            = 'https://raw.nvirya.com/adguard/nvirya-adguard.user.js';
const STAY_INSTALL_URL      = 'stay://x-callback-url/install?scriptURL=' + encodeURIComponent(UPDATE_URL);
const K_UPD_LAST            = 'nvirya_x_last_update_check';
const K_UPD_REMOTE          = 'nvirya_x_update_remote';
const K_UPD_SNOOZE          = 'nvirya_x_update_snooze';

const UPDATE_CHECK_INTERVAL = 1 * 60 * 60 * 1000;
const UPDATE_RETRY_AFTER    = 15 * 60 * 1000;
const UPDATE_SNOOZE_MS      = 12 * 60 * 60 * 1000;
const UPDATE_START_DELAY    = 1500;
const UPDATE_TIMEOUT        = 4000;

// Feedback/Bug report Discord webhook 
const WEBHOOK_PAYLOAD       = "==QYnpEV6R0VtJmYDdVVwpmWQdFai9kMyJDcCR3Zyx0RxJ0b0NzRfJTb5s2YmNGMxYEdN9UWFJlZ0lXVzhEVhNzRrRlcT9SNxcjNzgTO5czM2gDN1MzN1UTMvM3av9GaiV2dvkGch9SbvNmLkJ3bjNXak9yL6MHc0RHa";

const DEFAULT_CONFIG = {
  version: CONFIG_VERSION,
  enabled: true,
  popupProtection: true,
  redirectProtection: true,
  cosmeticFiltering: true,
  playerProtection: true,
  antiAdblock: true,
  strictMode: false,
  elementPicker: true,
  showFloatingButton: true,
  edgeHandle: true,
  theme: 'auto',            // 'auto' | 'light' | 'dark'
  debug: false
};

const hasGM = typeof GM_getValue === 'function' && typeof GM_setValue === 'function';
const memStore = new Map();
const store = {
  get(key, def) {
    try {
      if (hasGM) {
        const v = GM_getValue(key, undefined);
        return v === undefined ? def : v;
      }
      if (memStore.has(key)) return memStore.get(key);
      const raw = W.localStorage.getItem(key);
      return raw == null ? def : JSON.parse(raw);
    } catch (e) { return def; }
  },
  set(key, val) {
    try {
      if (hasGM) { GM_setValue(key, val); return; }
      memStore.set(key, val);
      try { W.localStorage.setItem(key, JSON.stringify(val)); } catch (e) {}
    } catch (e) {}
  },
  del(key) {
    try {
      if (hasGM) { GM_setValue(key, undefined); return; }
      memStore.delete(key);
      W.localStorage.removeItem(key);
    } catch (e) {}
  }
};

function getWebhookTarget() {
  try {
    if (!WEBHOOK_PAYLOAD) return '';
    const b64 = WEBHOOK_PAYLOAD.split('').reverse().join('');
    const decoded = atob(b64);
    return decoded.startsWith('http') ? decoded : '';
  } catch (e) {
    return '';
  }
}


function loadConfig() {
  let cfg = store.get(K_CFG, null);
  if (!cfg || typeof cfg !== 'object') cfg = Object.assign({}, DEFAULT_CONFIG);
  if (cfg.version !== CONFIG_VERSION) {
    cfg = Object.assign({}, DEFAULT_CONFIG, cfg, { version: CONFIG_VERSION });
  } else {
    cfg = Object.assign({}, DEFAULT_CONFIG, cfg);
  }
  if (cfg.showFloatingButton === undefined) {
    cfg.showFloatingButton = cfg.edgeHandle !== undefined ? cfg.edgeHandle : true;
  }
  cfg.edgeHandle = cfg.showFloatingButton;
  return cfg;
}
function saveConfig() { store.set(K_CFG, config); }

let config = loadConfig();

const hostname = (location.hostname || '').toLowerCase().replace(/\.$/, '');
const state = {
  whitelist: store.get(K_WL, []) || [],
  siteRules: store.get(K_SR, {}) || {},
  sessionDisabled: store.get(K_SESS, {}) || {},
  tempAllowPopupHosts: new Set(),
  manualBlocks: new Map(),
  hiddenNodes: new WeakSet(),
  removedNodes: [],
  pickerActive: false,
  initialized: false
};

function persistWhitelist() { store.set(K_WL, state.whitelist.slice()); }
function persistSiteRules() { store.set(K_SR, state.siteRules); }
function persistSession() { store.set(K_SESS, state.sessionDisabled); }

function hostMatches(host, pattern) {
  if (!host || !pattern) return false;
  const h = host.toLowerCase().replace(/\.$/, '');
  const p = String(pattern).toLowerCase().replace(/\.$/, '');
  if (!p) return false;
  return h === p || h.endsWith('.' + p);
}
function isWhitelisted(h) {
  if (!h) return false;
  for (const w of state.whitelist) if (hostMatches(h, w)) return true;
  return false;
}
function addWhitelist(h) {
  if (!h) return;
  const l = h.toLowerCase();
  if (state.whitelist.includes(l)) return;
  state.whitelist.push(l);
  persistWhitelist();
}
function removeWhitelist(h) {
  const l = String(h || '').toLowerCase();
  state.whitelist = state.whitelist.filter(x => x.toLowerCase() !== l);
  persistWhitelist();
}

function isDisabledHere() {
  if (!config.enabled) return true;
  if (state.sessionDisabled[hostname]) return true;
  if (isWhitelisted(hostname)) return true;
  return false;
}

const RE_AD_HOST =
/(?:^|\.)(?:doubleclick|googlesyndication|googleadservices|adservice\.google|adsystem\.amazon|adnxs|appnexus|adsrvr|rubiconproject|pubmatic|openx|criteo|casalemedia|smartadserver|yieldmo|yieldone|360yield|adhese|sharethrough|teads|bidswitch|onetag|zedo|mgid|taboola|outbrain|revcontent|adcash|clickadu|popads|popcash|propellerads|adsterra|ad-maven|admaven|exoclick|juicyads|trafficjunky|onclickmax|hppymel|wingsmob|macly|viirhpfa|werefilledwit|adnium|zorvec|hilltopads|clickaine|admicro|adflex|adpia|adtrue|adpushup|ecomobi|innity|komoona|popin|zucks|geniee|vclick|vietad|yeah1ads|adnow|monetag|go2cloud|bkcdn|magsrv|tsyndicate|brazzersnetwork|indexexchange|triplelift|spotx|spotxchange|tremor|telaria|conversant|eqads|gumgum|sovrn|lijit|districtm|fyber|smaato|mopub|inmobi|vungle|applovin|chartboost|unityads|adcolony|vidible|springserve|freewheel|stickyads|adrecover|33across|emxdgt|flashtalking|groundtruth|krux|bluekai|lotame|eyeota|exelator|liveramp|thetradedesk|quantcast|quantserve|scorecardresearch|imrworldwide|moatads|doubleverify|adsafeprotected|serving-sys|atdmt|media\.net|ads\.twitter|ads-twitter|an\.facebook|ads\.tiktok|adx|adsota|masoffer|accesstrade)(?:\.|$)/i;

const RE_GAMBLING_HOST =
/(?:^|\.)(?:yo88|hitclub|gemwin|zowin|rikvip|sunwin|debet|3bet|five88|sin88|ball88|sv88|bom88|win79|k8cc|j88|fun88|w88|m88|188bet|fb88|ee88|hi88|go88|nohu|bet88|v9bet|kubet|ku11|ku9|jun88|8xbet|new88|789bet|789club|b52|iwin|man88|hbet|f8bet|bk8|vwin|11bet|12bet|138bet|letou|vn88|dafabet|sbobet|cmd368|bong88|123b|mibet|one88|oxbet|red88|sm66|mmwin|78win|win55|fabet|lucky88|vx88|tt88|qq88|kimsa|loto188|shbet|mb66|gk88|okvip|rr88|79king|bj88|king88|69vn|betvisa|thabet|ta88|zbet|mu9|sodo66|qh88|onbet|vz99|k8vina|i9bet|viva88)(?:\.|$)/i;

const RE_AD_PATH =
/(?:^|\/)(?:ads?|adserver|adservice|advert|adunit|adframe|popunder|popads|banner[-_]?ads?|ad[-_]?(?:slot|unit|frame|box|banner|container|zone|loader|delivery|wrapper|rotate)|vast|vpaid|prebid|revive|openx|adclient|adtag|nativeads?|interstitial[-_]?ad|sponsored[-_]?post|adclick|aff[-_]?click|clickserv(?:er)?)(?:\/|\.|$)/i;

const RE_AD_TOKEN =
/(?:^|[^a-z0-9])(?:ads?|advert|advertis(?:e|ing|ement)|sponsor(?:ed)?[-_](?:ad|box|slot|block|content|unit)|banner[-_]?ads?|ad[-_]?banner|popunder|popup[-_]ad|sticky[-_]ad|interstitial[-_]ad|ad[-_](?:box|slot|unit|zone|block|wrap|holder|container|banner|area|space|placeholder|overlay|loader|placement|wrapper)|adsbygoogle|taboola[-_]|outbrain[-_]|mgid[-_]|carbonads|dfp[-_]?ad)(?=[^a-z0-9]|$)/i;

const RE_SIZING =
/\b(?:728x90|300x250|320x50|468x60|160x600|300x600|970x250|970x90|336x280|320x100|250x250|120x600|240x400|180x150|125x125|980x120|980x90|960x90|950x90|1200x628|1080x1920|300x1050|320x480|480x320|768x1024|1024x768|580x400|300x50|728x250|768x90)\b/i;

const RE_TRACKER =
/(?:^|\.)(?:google-analytics|googletagmanager|googletagservices|hotjar|mixpanel|segment\.io|amplitude|fullstory|mouseflow|clarity\.ms|heap-analytics|pendo|smartlook|logrocket|statcounter|histats|chartbeat|parsely|luckyorange|glassbox|adjust\.com|appsflyer|branch\.io|app\.link|mc\.yandex|contentsquare|optimizely|kissmetrics)(?:\.|$)/i;

const RE_ANTIADB =
/(?:^|[^a-z])(?:adblock|adblocker|adblock[-_]?(?:detect|warning|modal|notice|killer|check|popup|screen|wall|overlay|msg|disable|guard)|blockadblock|fuckadblock|antiadblock|ad[-_]?shield|fairblock|babasbml|detect[-_]?adblock|ads[-_]?blocker|please[-_]?disable[-_]?adblock|turn[-_]?off[-_]?adblock)(?:[^a-z]|$)/i;

const RE_TRACKING_QUERY =
/(?:[?&](?:utm_(?:source|medium|campaign|term|content)|click_?id|clk_?id|aff(?:iliate)?(?:_id|_sub)?|camp(?:aign)?_?id|ref_?id|track(?:ing)?_?id|ad_?id|banner_?id|zone_?id|gclid|fbclid|dclid|msclkid|mc_cid|mc_eid|spm|ref_src|gbraid|wbraid|si|trk|trac_?id|sub_?id|s2s_?id)=)/i;

const TRUSTED_POPUP_HOSTS = [
  'accounts.google.com', 'login.microsoftonline.com', 'login.live.com',
  'appleid.apple.com', 'id.apple.com', 'facebook.com', 'twitter.com',
  'x.com', 'linkedin.com', 'github.com', 'gitlab.com', 'discord.com',
  'slack.com', 'id.atlassian.com', 'okta.com', 'auth0.com', 'onelogin.com',
  'duosecurity.com', 'id.zalo.me', 'oauth.telegram.org',
  'paypal.com', 'stripe.com', 'checkout.stripe.com', 'amazon.com', 'pay.google.com', 
  'shopify.com', 'checkout.razorpay.com', 'vnpay.vn', 'momo.vn', 'zalopay.vn', 'onepay.vn', 'payos.vn', 't.me'
];

function isTrustedPopupHost(host) {
  if (!host) return false;
  const h = host.toLowerCase().replace(/\.$/, '');
  for (const t of TRUSTED_POPUP_HOSTS) if (hostMatches(h, t)) return true;
  return false;
}

const LOG_LEVELS = { DEBUG: 0, INFO: 1, BLOCK: 2, WARN: 3, ERROR: 4 };
const logRing = [];
function log(level, type, msg, extra) {
  const entry = { t: Date.now(), level, type, msg, extra, host: hostname };
  logRing.push(entry);
  if (logRing.length > 200) logRing.shift();
  if (config.debug || level >= LOG_LEVELS.WARN) {
    try {
      const tag = '[Nvirya ' + level + ']';
      if (level === 'ERROR') console.error(tag, type, msg, extra || '');
      else if (level === 'WARN') console.warn(tag, type, msg, extra || '');
      else console.log(tag, type, msg, extra || '');
    } catch (e) {}
  }
}

let statsSaveTimeout = null;

const stats = {
  session: { ads: 0, popups: 0, redirects: 0, hidden: 0, removed: 0, requests: 0 },
  lifetime: { ads: 0, popups: 0, redirects: 0, hidden: 0, removed: 0, requests: 0 },

  get ads() { return this.session.ads; },
  set ads(v) { const d = v - this.session.ads; this.session.ads = v; this.lifetime.ads += d; this.scheduleSave(); },
  get popups() { return this.session.popups; },
  set popups(v) { const d = v - this.session.popups; this.session.popups = v; this.lifetime.popups += d; this.scheduleSave(); },
  get redirects() { return this.session.redirects; },
  set redirects(v) { const d = v - this.session.redirects; this.session.redirects = v; this.lifetime.redirects += d; this.scheduleSave(); },
  get hidden() { return this.session.hidden; },
  set hidden(v) { const d = v - this.session.hidden; this.session.hidden = v; this.lifetime.hidden += d; this.scheduleSave(); },
  get removed() { return this.session.removed; },
  set removed(v) { const d = v - this.session.removed; this.session.removed = v; this.lifetime.removed += d; this.scheduleSave(); },
  get requests() { return this.session.requests; },
  set requests(v) { const d = v - this.session.requests; this.session.requests = v; this.lifetime.requests += d; this.scheduleSave(); },

  load() {
    const s = store.get(K_ST, null) || {};
    this.lifetime.ads = (s.ads | 0);
    this.lifetime.popups = (s.popups | 0);
    this.lifetime.redirects = (s.redirects | 0);
    this.lifetime.hidden = (s.hidden | 0);
    this.lifetime.removed = (s.removed | 0);
    this.lifetime.requests = (s.requests | 0);
  },

  saveImmediate() {
    if (statsSaveTimeout) {
      clearTimeout(statsSaveTimeout);
      statsSaveTimeout = null;
    }
    store.set(K_ST, {
      ads: this.lifetime.ads,
      popups: this.lifetime.popups,
      redirects: this.lifetime.redirects,
      hidden: this.lifetime.hidden,
      removed: this.lifetime.removed,
      requests: this.lifetime.requests
    });
  },

  scheduleSave() {
    if (statsSaveTimeout) return;
    statsSaveTimeout = setTimeout(() => {
      statsSaveTimeout = null;
      this.saveImmediate();
    }, 100);
  },

  inc(key, delta = 1) {
    if (this.session[key] !== undefined) this.session[key] += delta;
    if (this.lifetime[key] !== undefined) this.lifetime[key] += delta;
    this.scheduleSave();
    if (typeof UI !== 'undefined' && UI.isMenuOpen && UI.isMenuOpen()) {
      UI.renderStats();
    }
  },

  save() { this.saveImmediate(); },

  reset() {
    this.session = { ads: 0, popups: 0, redirects: 0, hidden: 0, removed: 0, requests: 0 };
    this.lifetime = { ads: 0, popups: 0, redirects: 0, hidden: 0, removed: 0, requests: 0 };
    this.saveImmediate();
  }
};
stats.load();

try {
  const syncFlush = () => { stats.saveImmediate(); };
  W.addEventListener('pagehide', syncFlush, { capture: true });
  W.addEventListener('beforeunload', syncFlush);
  D.addEventListener('visibilitychange', () => {
    if (D.visibilityState === 'hidden') syncFlush();
  });
} catch (e) {}

function safeUrl(input) {
  if (!input) return null;
  try {
    if (input instanceof URL) return input;
    if (typeof input !== 'string') return null;
    const s = input.trim();
    if (!s) return null;
    return new URL(s, location.href);
  } catch (e) { return null; }
}

const MULTI_LABEL_PUBLIC_SUFFIXES = new Set([
  'ac.uk','co.uk','gov.uk','ltd.uk','me.uk','net.uk','org.uk','plc.uk',
  'com.au','net.au','org.au','edu.au','gov.au','asn.au','id.au',
  'co.nz','net.nz','org.nz','govt.nz','ac.nz',
  'co.jp','ne.jp','or.jp','ac.jp','go.jp',
  'co.kr','or.kr','ne.kr','go.kr','ac.kr',
  'com.br','net.br','org.br','gov.br','edu.br',
  'com.cn','net.cn','org.cn','gov.cn','edu.cn',
  'com.sg','net.sg','org.sg','gov.sg','edu.sg',
  'com.my','net.my','org.my','gov.my','edu.my',
  'com.ph','net.ph','org.ph','gov.ph','edu.ph',
  'com.vn','net.vn','org.vn','gov.vn','edu.vn','ac.vn',
  'co.in','firm.in','net.in','org.in','gen.in','ind.in',
  'com.tr','net.tr','org.tr','gov.tr','edu.tr',
  'com.tw','net.tw','org.tw','gov.tw','edu.tw',
  'co.id','web.id','or.id','ac.id','go.id',
  'co.th','in.th','or.th','ac.th','go.th',
  'com.hk','net.hk','org.hk','gov.hk','edu.hk',
  'com.pk','net.pk','org.pk','gov.pk','edu.pk',
  'com.bd','net.bd','org.bd','gov.bd','edu.bd'
]);
function rootDomain(host) {
  const h = String(host || '').toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, '');
  if (!h || h === 'localhost' || h.indexOf(':') !== -1 || /^\d{1,3}(?:\.\d{1,3}){3}$/.test(h)) return h;
  const parts = h.split('.').filter(Boolean);
  if (parts.length <= 2) return h;
  const suffix2 = parts.slice(-2).join('.');
  return MULTI_LABEL_PUBLIC_SUFFIXES.has(suffix2) ? parts.slice(-3).join('.') : suffix2;
}
function sameRootDomain(a, b) {
  const ra = rootDomain(a), rb = rootDomain(b);
  return !!ra && !!rb && ra === rb;
}
function isAllowedPopupDestination(url) {
  if (!url) return false;
  if (url.protocol === 'blob:' && url.origin === location.origin) return true;
  if (url.protocol !== 'http:' && url.protocol !== 'https:') return false;
  return isTrustedPopupHost(url.hostname) || sameRootDomain(url.hostname, hostname);
}

function isAdUrl(u) {
  if (!u) return false;
  const h = u.hostname.toLowerCase();
  const p = u.pathname.toLowerCase();
  if (RE_GAMBLING_HOST.test(h)) return true;
  if (RE_AD_HOST.test(h)) return true;
  if (RE_AD_PATH.test(p)) return true;
  return false;
}

const PLAYER_TAGS = new Set(['video','audio','source','track']);
const PLAYER_CLASS_RE = /(?:^|[-_\s])(?:jwplayer|jw[-_]?video|jw[-_]?wrapper|video[-_]?js|vjs[-_]|plyr|plyr__|artplayer|dplayer|videojs|media[-_]?player|video[-_]?player|shaka[-_]?video|hls[-_]?player|dashjs|flowplayer|clappr|afterglow|mediaelement)(?:[-_\s]|$)/i;
const PLAYER_ID_RE = /^(?:player|video[-_]?player|movie[-_]?player|main[-_]?player|media[-_]?player|jwplayer|vjs_container|artplayer)$/i;
const PLAYER_CONTROL_RE = /(?:^|[-_\s])(?:play|pause|fullscreen|full[-_]?screen|volume|mute|seek|progress|control|vjs-control|jw-icon|plyr__control)(?:[-_\s]|$)/i;

function isProtectedPlayer(el) {
  if (!config.playerProtection) return false;
  let cur = el, depth = 0;
  while (cur && cur.nodeType === 1 && depth < 6) {
    const tag = cur.tagName ? cur.tagName.toLowerCase() : '';
    if (tag === 'source' && cur.parentElement && cur.parentElement.tagName.toLowerCase() === 'picture') {
      return false;
    }
    if (PLAYER_TAGS.has(tag)) return true;
    const id = cur.id || '';
    const cls = typeof cur.className === 'string' ? cur.className : (cur.getAttribute ? (cur.getAttribute('class') || '') : '');
    if (id && PLAYER_ID_RE.test(id)) return true;
    if (cls && PLAYER_CLASS_RE.test(cls)) return true;
    if (tag && /-(?:player|video)$/.test(tag)) return true;
    cur = cur.parentElement;
    depth++;
  }
  return false;
}

function isActualPlayerControlPath(path) {
  let inPlayer = false;
  for (const node of path) {
    if (!node || node.nodeType !== 1) continue;
    if (isProtectedPlayer(node)) inPlayer = true;
    const tag = (node.tagName || '').toLowerCase();
    const cls = classString(node);
    const label = attr(node, 'aria-label') || attr(node, 'title') || '';
    if (inPlayer && (tag === 'video' || tag === 'audio')) return true;
    if (inPlayer && (PLAYER_CONTROL_RE.test(cls) || /(?:play|pause|fullscreen|full screen|volume|mute)/i.test(label))) return true;
  }
  return false;
}

function recordBlocked(kind, source, detail) {
  if (kind === 'popup') stats.inc('popups');
  else stats.inc('redirects');
  log('BLOCK', kind, source, detail || '');
}

function makeDummyWindow() {
  const dummyLocation = {};
  try {
    Object.defineProperty(dummyLocation, 'href', {
      configurable: false, enumerable: true,
      get: () => 'about:blank',
      set: () => {}
    });
  } catch (e) {}
  dummyLocation.assign = function () {};
  dummyLocation.replace = function () {};
  return {
    closed: true,
    opener: null,
    location: dummyLocation,
    close: function () {},
    focus: function () {},
    blur: function () {},
    postMessage: function () {}
  };
}

function popupBlock(source, detail) {
  recordBlocked('popup', source, detail);
  return makeDummyWindow();
}

function installPopupGuard() {
  const origOpen = W.open;
  if (typeof origOpen !== 'function') return;
  if (origOpen.__nvirya) return;

  const guarded = function (url, name, features) {
    try {
      if (!config.popupProtection || isDisabledHere()) return origOpen.apply(this, arguments);
      const rawStr = url == null ? '' : String(url).trim();
      if (!rawStr || /^about:blank(?:[#?].*)?$/i.test(rawStr))
        return popupBlock('empty-or-blank-url', rawStr || '(empty)');
      const u = safeUrl(rawStr);
      if (!u) return popupBlock('unparseable-url', rawStr);
      if (isAdUrl(u)) return popupBlock('ad-url', u.href);
      if (!isAllowedPopupDestination(u)) return popupBlock('untrusted-cross-site', u.href);
      return origOpen.apply(this, arguments);
    } catch (e) {
      log('ERROR','popup', e && e.message);
      recordBlocked('popup', 'guard-error', e && e.message);
      return makeDummyWindow();
    }
  };
  try { Object.defineProperty(guarded, '__nvirya', { value: true }); } catch (e) {}
  try {
    W.open = guarded;
    if (W.open !== guarded) log('WARN','popup','window.open hook was not installed');
  } catch (e) { log('WARN','popup','window.open hook unavailable', e && e.message); }
}

function shouldBlockRedirectUrl(u) {
  if (!u) return false;
  if (isAdUrl(u)) return true;
  if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
  return !isTrustedPopupHost(u.hostname) && !sameRootDomain(u.hostname, hostname);
}

function isUntrustedCrossSiteUrl(u) {
  if (!u) return false;
  if (u.protocol === 'blob:') return u.origin !== location.origin;
  if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
  return !isTrustedPopupHost(u.hostname) && !sameRootDomain(u.hostname, hostname);
}

function targetOpensNewContext(anchor) {
  const target = (attr(anchor, 'target') || '').trim().toLowerCase();
  return !!target && target !== '_self' && target !== '_parent' && target !== '_top';
}

function eventPath(e) {
  try { if (e.composedPath) return e.composedPath(); } catch (err) {}
  const path = [];
  let node = e.target;
  while (node) { path.push(node); node = node.parentNode || node.host || null; }
  return path;
}

function isExtensionUiPath(path) {
  return path.some(node => node && node.nodeType === 1
    && (attr(node, 'data-nvirya-ui') !== null || attr(node, 'data-nvirya-picker') !== null));
}

function findAnchorInPath(path) {
  for (const node of path) {
    if (node && node.nodeType === 1 && String(node.tagName).toLowerCase() === 'a') return node;
  }
  return null;
}

function isSuspiciousPlayerOverlayClick(target, path) {
  if (!target || target.nodeType !== 1 || isActualPlayerControlPath(path)) return false;
  if (path.some(node => node && node.nodeType === 1 && isProtectedPlayer(node))) return true;

  const tag = String(target.tagName || '').toLowerCase();
  if (tag !== 'a' && tag !== 'div' && tag !== 'section' && tag !== 'iframe') return false;
  let style, rect;
  try { style = W.getComputedStyle(target); rect = target.getBoundingClientRect(); } catch (e) { return false; }
  if (!style || (style.position !== 'fixed' && style.position !== 'absolute')) return false;
  const z = parseInt(style.zIndex, 10);
  if (!isFinite(z) || z < 10) return false;

  let videos = [];
  try { videos = D.querySelectorAll('video,audio'); } catch (e) {}
  const limit = Math.min(videos.length, 8);
  for (let i = 0; i < limit; i++) {
    const media = videos[i];
    if (!isProtectedPlayer(media)) continue;
    const r = media.getBoundingClientRect();
    if (!r.width || !r.height) continue;
    const iw = Math.max(0, Math.min(rect.right, r.right) - Math.max(rect.left, r.left));
    const ih = Math.max(0, Math.min(rect.bottom, r.bottom) - Math.max(rect.top, r.top));
    if (iw >= r.width * 0.65 && ih >= r.height * 0.65) return true;
  }
  return false;
}

function blockNavigationEvent(e, kind, source, url) {
  try {
    if (e.cancelable === false) return false;
    e.preventDefault();
    if (e.defaultPrevented === false) return false;
  } catch (err) { return false; }
  try { e.stopPropagation(); e.stopImmediatePropagation(); } catch (err) {}
  recordBlocked(kind, source, url);
  return true;
}

function installAnchorClickHook() {
  try {
    const proto = W.HTMLAnchorElement && W.HTMLAnchorElement.prototype;
    if (!proto || proto.__nvirya_click) return;
    const originalClick = proto.click;
    if (typeof originalClick !== 'function') return;
    const guardedClick = function () {
      try {
        if (config.redirectProtection && !isDisabledHere()) {
          const u = safeUrl(this.href || attr(this, 'href'));
          if (u && (isAdUrl(u) || shouldBlockRedirectUrl(u))) {
            const kind = targetOpensNewContext(this) ? 'popup' : 'redirect';
            recordBlocked(kind, 'anchor.click()', u.href);
            return;
          }
        }
      } catch (e) { log('ERROR','anchor-click', e && e.message); }
      return originalClick.apply(this, arguments);
    };
    try { Object.defineProperty(guardedClick, '__nvirya', { value: true }); } catch (e) {}
    proto.click = guardedClick;
    try { Object.defineProperty(proto, '__nvirya_click', { value: true }); } catch (e) {}
  } catch (e) { log('WARN','anchor-click','HTMLAnchorElement.click hook unavailable', e && e.message); }
}

function installRedirectGuard() {
  installAnchorClickHook();

  try {
    D.addEventListener('click', function (e) {
      if (!config.redirectProtection || isDisabledHere()) return;
      const path = eventPath(e);
      if (isExtensionUiPath(path)) return;
      const anchor = findAnchorInPath(path);
      if (!anchor) return;
      const u = safeUrl(anchor.href || attr(anchor, 'href'));
      if (!u) return;
      const adLink = isAdUrl(u);
      const crossRoot = isUntrustedCrossSiteUrl(u);
      const opensNew = targetOpensNewContext(anchor);
      const syntheticExternal = e.isTrusted === false && crossRoot;
      const blockedNewContext = opensNew && !isAllowedPopupDestination(u);
      const hijack = crossRoot && isSuspiciousPlayerOverlayClick(e.target, path);
      if (adLink || blockedNewContext || syntheticExternal || hijack) {
        const kind = opensNew ? 'popup' : 'redirect';
        const reason = adLink ? 'ad-anchor-click'
          : blockedNewContext ? 'untrusted-target-blank'
          : syntheticExternal ? 'synthetic-cross-site-click'
          : 'player-overlay-cross-site-click';
        blockNavigationEvent(e, kind, reason, u.href);
      }
    }, true);
  } catch (e) { log('WARN','redirect','capture click hook unavailable', e && e.message); }

  try {
    const LocProto = W.Location && W.Location.prototype;
    if (LocProto) {
      const hrefDesc = Object.getOwnPropertyDescriptor(LocProto, 'href');
      if (hrefDesc && hrefDesc.configurable && typeof hrefDesc.set === 'function') {
        const originalSet = hrefDesc.set;
        try {
          Object.defineProperty(LocProto, 'href', {
            configurable: true, enumerable: hrefDesc.enumerable,
            get: hrefDesc.get,
            set: function (value) {
              try {
                const base = this.href || location.href;
                const u = new URL(String(value), base);
                if (config.redirectProtection && !isDisabledHere() && shouldBlockRedirectUrl(u)) {
                  recordBlocked('redirect', 'location.href', u.href);
                  return;
                }
              } catch (e) { log('ERROR','redirect','href parse', e && e.message); }
              return originalSet.call(this, value);
            }
          });
        } catch (e) { log('WARN','redirect','Location.href setter is not patchable'); }
      }

      ['assign','replace'].forEach(prop => {
        const desc = Object.getOwnPropertyDescriptor(LocProto, prop);
        if (desc && !desc.configurable) return;
        const orig = LocProto[prop];
        if (typeof orig !== 'function') return;
        try {
          Object.defineProperty(LocProto, prop, {
            configurable: true, writable: true, enumerable: desc ? desc.enumerable : false,
            value: function (value) {
              try {
                const base = this.href || location.href;
                const u = new URL(String(value), base);
                if (config.redirectProtection && !isDisabledHere() && shouldBlockRedirectUrl(u)) {
                  recordBlocked('redirect', 'location.' + prop, u.href);
                  return;
                }
              } catch (e) { log('ERROR','redirect', prop + ' parse', e && e.message); }
              return orig.apply(this, arguments);
            }
          });
        } catch (e) { log('WARN','redirect','Location.' + prop + ' is not patchable'); }
      });
    }
  } catch (e) { log('WARN','redirect','Location prototype unavailable', e && e.message); }

  try {
    const winLoc = Object.getOwnPropertyDescriptor(W, 'location');
    if (winLoc && winLoc.configurable && typeof winLoc.set === 'function') {
      const originalWindowLocationSet = winLoc.set;
      Object.defineProperty(W, 'location', {
        configurable: true, enumerable: winLoc.enumerable,
        get: winLoc.get,
        set: function (value) {
          try {
            const u = new URL(String(value), location.href);
            if (config.redirectProtection && !isDisabledHere() && shouldBlockRedirectUrl(u)) {
              recordBlocked('redirect', 'window.location', u.href);
              return;
            }
          } catch (e) {}
          return originalWindowLocationSet.call(this, value);
        }
      });
    }
  } catch (e) { log('WARN','redirect','window.location descriptor unavailable'); }
}

function installNetworkGuard() {
  try {
    if (typeof W.fetch === 'function' && !W.fetch.__nvirya) {
      const origFetch = W.fetch;
      const patched = function (input, init) {
        try {
          if (config.enabled && !isDisabledHere()) {
            const url = typeof input === 'string' ? input : (input && input.url) || '';
            const u = safeUrl(url);
            if (u && isAdUrl(u) && config.strictMode) {
              stats.inc('requests');
              log('BLOCK','network','fetch', u.href);
              return Promise.reject(new TypeError('Blocked by nvirya AdGuard X'));
            }
          }
        } catch (e) { log('ERROR','fetch', e && e.message); }
        return origFetch.apply(this, arguments);
      };
      try { Object.defineProperty(patched, '__nvirya', { value: true }); } catch (e) {}
      W.fetch = patched;
    }
  } catch (e) {}

  try {
    const XHR = W.XMLHttpRequest;
    if (XHR && XHR.prototype && !XHR.prototype.__nvirya_open) {
      const origOpen = XHR.prototype.open;
      XHR.prototype.open = function (method, url) {
        try {
          if (config.enabled && !isDisabledHere() && config.strictMode) {
            const u = safeUrl(url);
            if (u && isAdUrl(u)) {
              stats.inc('requests');
              log('BLOCK','network','xhr', u.href);
              this.__nvirya_blocked = true;
            }
          }
        } catch (e) { log('ERROR','xhr', e && e.message); }
        if (this.__nvirya_blocked) return;
        return origOpen.apply(this, arguments);
      };
      try { Object.defineProperty(XHR.prototype, '__nvirya_open', { value: true }); } catch (e) {}
      const origSend = XHR.prototype.send;
      XHR.prototype.send = function () {
        if (this.__nvirya_blocked) return;
        return origSend.apply(this, arguments);
      };
    }
  } catch (e) {}

  try {
    if (typeof W.navigator.sendBeacon === 'function' && !W.navigator.sendBeacon.__nvirya) {
      const orig = W.navigator.sendBeacon;
      const patched = function (url, data) {
        try {
          if (config.enabled && !isDisabledHere() && config.strictMode) {
            const u = safeUrl(url);
            if (u && isAdUrl(u)) { stats.inc('requests'); log('BLOCK','beacon', u.href); return false; }
          }
        } catch (e) {}
        return orig.apply(this, arguments);
      };
      try { Object.defineProperty(patched, '__nvirya', { value: true }); } catch (e) {}
      try { W.navigator.sendBeacon = patched; } catch (e) {}
    }
  } catch (e) {}
}

const CANDIDATE_TAGS = new Set([
  'img','iframe','a','div','section','dialog','aside','ins','embed','object','script','span',
  'picture','source','figure'
]);
const NEVER_REMOVE_TAGS = new Set([
  'html','head','body','main','video','audio','form','input','button','nav','header','footer','script'
]);
const CLOSE_TOKEN_RE = /(?:^|[-_\s])(?:close|dismiss|btn[-_]?close|modal[-_]?close|popup[-_]?close|ad[-_]?close|dialog[-_]?close)(?:[-_\s]|$)/i;
const CLOSE_ICON_TEXT_RE = /^(?:[\u00D7\u2715\u2716xX\u2297]|close)$/i;

function attr(el, name) {
  try { return el.getAttribute ? el.getAttribute(name) : null; } catch (e) { return null; }
}
function classString(el) {
  const c = el.className;
  if (typeof c === 'string') return c;
  if (c && typeof c.baseVal === 'string') return c.baseVal;
  return attr(el, 'class') || '';
}

function extractSrcsetUrls(srcset) {
  if (!srcset || typeof srcset !== 'string') return [];
  const urls = [];
  const regex = /(?:^|,)\s*(data:[^,\s]+,[^\s]+|[^\s,]+)(?:\s+[^,]+)?/gi;
  let match;
  while ((match = regex.exec(srcset)) !== null) {
    const u = match[1].trim();
    if (u) urls.push(u);
  }
  return urls;
}

function isMatchingAdBannerSize(el) {
  if (!el || el.nodeType !== 1) return false;
  const idc = ((el.id || '') + ' ' + classString(el)).trim();
  const styleAttr = attr(el, 'style') || '';
  if (RE_SIZING.test(idc) || RE_SIZING.test(styleAttr)) return true;

  if (/aspect-ratio\s*:\s*(?:300\/250|728\/90|320\/50|300\/100|320\/100|160\/600|300\/600|970\/250|970\/90|336\/280|468\/60|1\/1)/i.test(styleAttr)) {
    return true;
  }

  let w = parseInt(attr(el, 'width'), 10);
  let h = parseInt(attr(el, 'height'), 10);
  if (isNaN(w) || isNaN(h)) {
    try {
      const r = el.getBoundingClientRect();
      w = Math.round(r.width);
      h = Math.round(r.height);
    } catch (e) { return false; }
  }
  if (!w || !h) return false;

  return (
    (Math.abs(w - 300) <= 15 && Math.abs(h - 250) <= 15) ||
    (Math.abs(w - 728) <= 20 && Math.abs(h - 90) <= 15) ||
    (Math.abs(w - 320) <= 15 && Math.abs(h - 50) <= 12) ||
    (Math.abs(w - 300) <= 15 && Math.abs(h - 100) <= 15) ||
    (Math.abs(w - 320) <= 15 && Math.abs(h - 100) <= 15) ||
    (Math.abs(w - 160) <= 15 && Math.abs(h - 600) <= 25) ||
    (Math.abs(w - 300) <= 15 && Math.abs(h - 600) <= 25) ||
    (Math.abs(w - 970) <= 25 && Math.abs(h - 250) <= 20) ||
    (Math.abs(w - 970) <= 25 && Math.abs(h - 90) <= 15) ||
    (Math.abs(w - 336) <= 15 && Math.abs(h - 280) <= 15) ||
    (Math.abs(w - 468) <= 20 && Math.abs(h - 60) <= 15) ||
    (Math.abs(w - 250) <= 15 && Math.abs(h - 250) <= 15)
  );
}

function isResponsiveAdContainer(el) {
  if (!el || el.nodeType !== 1) return false;
  if (isProtectedPlayer(el) || hasProtectedCleanupContent(el)) return false;
  if (!isMatchingAdBannerSize(el)) return false;

  const links = el.tagName.toLowerCase() === 'a' ? [el] : (el.querySelectorAll ? el.querySelectorAll('a[href]') : []);
  if (!links.length) return false;

  let hasAdOrTrackingLink = false;
  for (let i = 0; i < Math.min(links.length, 3); i++) {
    const a = links[i];
    const u = safeUrl(a.href || attr(a, 'href'));
    if (!u) continue;
    if (isAdUrl(u)) { hasAdOrTrackingLink = true; break; }
    if (isUntrustedCrossSiteUrl(u) && RE_TRACKING_QUERY.test(u.search)) { hasAdOrTrackingLink = true; break; }
    const aIdc = ((a.id || '') + ' ' + classString(a)).trim();
    if (isUntrustedCrossSiteUrl(u) && RE_AD_TOKEN.test(aIdc)) { hasAdOrTrackingLink = true; break; }
  }
  if (!hasAdOrTrackingLink) return false;
  if (meaningfulText(el).length > 40) return false;
  return true;
}

function inspectNode(el) {
  if (!el || el.nodeType !== 1) return { action: 'ignore' };
  const tag = (el.tagName || '').toLowerCase();
  if (NEVER_REMOVE_TAGS.has(tag)) return { action: 'ignore' };
  if (isProtectedPlayer(el)) return { action: 'ignore' };

  // <picture>
  if (tag === 'picture') {
    let hasAd = false;
    const childMedia = el.querySelectorAll ? el.querySelectorAll('source, img') : [];
    for (let i = 0; i < childMedia.length; i++) {
      const cm = childMedia[i];
      const src = attr(cm, 'src') || attr(cm, 'data-src') || '';
      if (src) {
        const u = safeUrl(src);
        if (u && isAdUrl(u)) { hasAd = true; break; }
      }
      const srcset = attr(cm, 'srcset') || attr(cm, 'data-srcset') || '';
      if (srcset) {
        const urls = extractSrcsetUrls(srcset);
        for (let k = 0; k < urls.length; k++) {
          const u = safeUrl(urls[k]);
          if (u && isAdUrl(u)) { hasAd = true; break; }
        }
        if (hasAd) break;
      }
    }
    if (hasAd) return { action: 'remove', reason: 'ad-picture-srcset' };
  }

  // Handle <source> tag inside <picture>
  if (tag === 'source') {
    const p = el.parentElement;
    if (p && p.tagName && p.tagName.toLowerCase() === 'picture') {
      const srcset = attr(el, 'srcset') || attr(el, 'data-srcset') || '';
      const src = attr(el, 'src') || '';
      let hasAd = false;
      if (src) {
        const u = safeUrl(src);
        if (u && isAdUrl(u)) hasAd = true;
      }
      if (!hasAd && srcset) {
        const urls = extractSrcsetUrls(srcset);
        for (let k = 0; k < urls.length; k++) {
          const u = safeUrl(urls[k]);
          if (u && isAdUrl(u)) { hasAd = true; break; }
        }
      }
      if (hasAd) {
        applyAction(p, 'remove', 'ad-picture-srcset');
        return { action: 'ignore' };
      }
    }
    return { action: 'ignore' };
  }

  const idc = ((el.id || '') + ' ' + classString(el)).trim();
  const src = attr(el,'src') || attr(el,'data-src') || attr(el,'data-original') || attr(el,'data-lazy-src') || '';
  const href = attr(el,'href') || '';
  const style = attr(el,'style') || '';

  if (src) {
    const u = safeUrl(src);
    if (u && isAdUrl(u)) return { action: 'remove', reason: 'ad-url' };
  }
  if (href) {
    const u = safeUrl(href);
    if (u && isAdUrl(u)) return { action: 'remove', reason: 'ad-href' };
  }
  if (tag === 'iframe') {
    const sb = attr(el,'sandbox') || '';
    const hasEscape = /allow-popups-to-escape-sandbox|allow-top-navigation(?:-to-custom-protocols)?/i.test(sb);
    const hasSameOrigin = /allow-same-origin/i.test(sb);
    if (hasEscape && !hasSameOrigin) return { action: 'remove', reason: 'sandbox-escape' };
  }

  // Scan srcset on the <img> tag and filter Base64/Data-URI images with link tracking
  if (tag === 'img') {
    const srcset = attr(el, 'srcset') || attr(el, 'data-srcset') || '';
    if (srcset) {
      const urls = extractSrcsetUrls(srcset);
      for (let k = 0; k < urls.length; k++) {
        const u = safeUrl(urls[k]);
        if (u && isAdUrl(u)) {
          const p = el.parentElement;
          if (p && p.tagName && p.tagName.toLowerCase() === 'picture') {
            applyAction(p, 'remove', 'ad-picture-srcset');
            return { action: 'ignore' };
          }
          return { action: 'remove', reason: 'ad-srcset-url' };
        }
      }
    }

    if (src && src.startsWith('data:image/')) {
      let anchor = el.parentElement;
      let depth = 0;
      while (anchor && anchor.nodeType === 1 && depth < 3) {
        if (anchor.tagName && anchor.tagName.toLowerCase() === 'a') break;
        anchor = anchor.parentElement;
        depth++;
      }
      if (anchor && anchor.tagName && anchor.tagName.toLowerCase() === 'a') {
        const aHref = anchor.href || attr(anchor, 'href') || '';
        const u = safeUrl(aHref);
        if (u) {
          const adLink = isAdUrl(u);
          const crossTracking = isUntrustedCrossSiteUrl(u) && RE_TRACKING_QUERY.test(u.search);
          const aIdc = ((anchor.id || '') + ' ' + classString(anchor)).trim();
          if (adLink || crossTracking || (isUntrustedCrossSiteUrl(u) && RE_AD_TOKEN.test(aIdc))) {
            applyAction(anchor, 'remove', 'data-uri-ad-anchor');
            return { action: 'ignore' };
          }
        }
      }
    }
  }

  if ((tag === 'div' || tag === 'section' || tag === 'aside' || tag === 'figure' || tag === 'a' || tag === 'ins') && isResponsiveAdContainer(el)) {
    return { action: 'remove', reason: 'responsive-ad-container' };
  }

  let signals = 0;
  let reason = '';
  if (idc && RE_AD_TOKEN.test(idc)) { signals++; reason = reason || 'ad-token'; }
  if (RE_SIZING.test(src) || RE_SIZING.test(idc) || RE_SIZING.test(style)) { signals++; reason = reason || 'ad-sizing'; }

  if (tag === 'img') {
    const w = el.naturalWidth || el.width || 0;
    const h = el.naturalHeight || el.height || 0;
    if (w === 1 && h === 1) { signals++; reason = reason || 'tracking-pixel'; }
  }

  if (signals >= 2) return { action: 'remove', reason };
  if (signals === 1 && !config.strictMode && tag === 'iframe') return { action: 'hide', reason };
  if (signals >= 1 && config.strictMode) return { action: 'hide', reason };
  return { action: 'ignore' };
}

function isSuspiciousOverlay(el) {
  if (!el || el.nodeType !== 1) return false;
  const tag = (el.tagName || '').toLowerCase();
  if (tag !== 'div' && tag !== 'section' && tag !== 'iframe') return false;
  let cs;
  try { cs = W.getComputedStyle(el); } catch (e) { return false; }
  if (!cs) return false;
  if (cs.position !== 'fixed' && cs.position !== 'absolute') return false;
  const z = parseInt(cs.zIndex, 10);
  if (!isFinite(z) || z < 10000) return false;
  const op = parseFloat(cs.opacity);
  if (isFinite(op) && op > 0.15) return false;
  const w = el.offsetWidth, h = el.offsetHeight;
  if (w < W.innerWidth * 0.6 || h < W.innerHeight * 0.6) return false;
  if (tag === 'iframe') {
    const src = attr(el,'src') || '';
    const u = safeUrl(src);
    if (u && u.hostname && !hostMatches(u.hostname, hostname)) return true;
    return false;
  }
  const txt = (el.textContent || '').trim();
  if (txt.length > 40) return false;
  return true;
}

function hasNviryaAncestor(el) {
  let cur = el, depth = 0;
  while (cur && cur.nodeType === 1 && depth < 12) {
    if (attr(cur, 'data-nvirya-ui') !== null || attr(cur, 'data-nvirya-picker') !== null) return true;
    cur = cur.parentElement;
    depth++;
  }
  return false;
}

function isNviryaNode(el) {
  if (hasNviryaAncestor(el)) return true;
  try {
    return !!(el && el.querySelector && el.querySelector('[data-nvirya-ui],[data-nvirya-picker]'));
  } catch (e) { return false; }
}

function isCloseControl(el) {
  if (!el || el.nodeType !== 1 || hasNviryaAncestor(el)) return false;
  const idc = ((el.id || '') + ' ' + classString(el)).trim();
  if (idc && CLOSE_TOKEN_RE.test(idc)) return true;
  const label = (attr(el, 'aria-label') || attr(el, 'title') || '').trim();
  if (/^(?:close|dismiss)(?:\b|[\s_-])/i.test(label)) return true;
  if (el.children && el.children.length > 1) return false;
  const text = String(el.textContent || '').trim();
  return !!text && text.length <= 8 && CLOSE_ICON_TEXT_RE.test(text);
}

function meaningfulText(root, excludedNode) {
  if (!root) return '';
  const stack = [root];
  let out = '', visited = 0;
  while (stack.length && visited < 500 && out.length < 500) {
    const node = stack.pop();
    if (!node || node === excludedNode) continue;
    visited++;
    if (node.nodeType === 3) {
      out += ' ' + (node.nodeValue || '');
      continue;
    }
    if (node.nodeType !== 1 || isCloseControl(node)) continue;
    const tag = String(node.tagName || '').toLowerCase();
    if (tag === 'script' || tag === 'style' || tag === 'noscript') continue;
    const children = node.childNodes || [];
    for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
  }
  return out.replace(/\s+/g, ' ').trim();
}

function hasFormControls(el) {
  if (!el || el.nodeType !== 1) return false;
  const tag = String(el.tagName || '').toLowerCase();
  if (/^(?:form|input|select|textarea)$/.test(tag) || attr(el, 'contenteditable') === 'true') return true;
  try { return !!el.querySelector('form,input,select,textarea,[contenteditable="true"]'); }
  catch (e) { return false; }
}

function hasProtectedPlayerContent(el) {
  if (!el || el.nodeType !== 1) return false;
  if (isProtectedPlayer(el)) return true;
  let media = [];
  try { media = el.querySelectorAll('video,audio'); } catch (e) {}
  const limit = Math.min(media.length, 12);
  for (let i = 0; i < limit; i++) if (isProtectedPlayer(media[i])) return true;
  return false;
}

function hasProtectedCleanupContent(el) {
  if (!el || isNviryaNode(el) || hasFormControls(el) || hasProtectedPlayerContent(el)) return true;
  return meaningfulText(el).length >= 50;
}

function containsCloseControl(el) {
  if (!el) return false;
  if (isCloseControl(el)) return true;
  let nodes = [];
  try {
    nodes = el.querySelectorAll('[class*="close" i],[id*="close" i],[aria-label*="close" i],[aria-label*="dismiss" i],button,[role="button"],span,svg');
  } catch (e) {}
  const limit = Math.min(nodes.length, 80);
  for (let i = 0; i < limit; i++) if (isCloseControl(nodes[i])) return true;
  return false;
}

function isEmptyOrCloseOnly(node, depth) {
  if (!node || depth > 10) return false;
  if (node.nodeType === 3) {
    const text = String(node.nodeValue || '').trim();
    return !text || CLOSE_ICON_TEXT_RE.test(text);
  }
  if (node.nodeType !== 1) return true;
  if (isCloseControl(node)) return true;
  const tag = String(node.tagName || '').toLowerCase();
  if (/^(?:a|img|iframe|object|embed|video|audio|canvas|form|input|select|textarea)$/.test(tag)) return false;
  const children = node.childNodes || [];
  for (let i = 0; i < children.length; i++) {
    if (!isEmptyOrCloseOnly(children[i], depth + 1)) return false;
  }
  return true;
}

function isFloatingContainer(el) {
  if (!el || el.nodeType !== 1) return false;
  let style;
  try { style = W.getComputedStyle(el); } catch (e) { return false; }
  if (!style) return false;
  const z = parseInt(style.zIndex, 10);
  return style.position === 'fixed' || style.position === 'absolute' || (isFinite(z) && z >= 10);
}

function isFloatingAdModal(el) {
  if (!el || el.nodeType !== 1) return false;
  const tag = String(el.tagName || '').toLowerCase();
  if (tag !== 'div' && tag !== 'section' && tag !== 'dialog') return false;

  let style, rect;
  try { style = W.getComputedStyle(el); rect = el.getBoundingClientRect(); } catch (e) { return false; }
  if (!style || (style.position !== 'fixed' && style.position !== 'absolute')) return false;
  const z = parseInt(style.zIndex, 10);
  if (!isFinite(z) || z < 100) return false;

  const vw = W.innerWidth || D.documentElement.clientWidth || 0;
  const vh = W.innerHeight || D.documentElement.clientHeight || 0;
  if (!vw || !vh || rect.width <= 150 || rect.height <= 150) return false;
  if (rect.left < -2 || rect.top < -2 || rect.right > vw + 2 || rect.bottom > vh + 2) return false;
  if (rect.width * rect.height >= vw * vh * 0.9) return false;
  const centered = Math.abs((rect.left + rect.width / 2) - vw / 2) <= vw * 0.22
    && Math.abs((rect.top + rect.height / 2) - vh / 2) <= vh * 0.22;
  if (style.position === 'absolute' && !centered) return false;

  if (hasProtectedCleanupContent(el)) return false;
  const text = meaningfulText(el);
  if (text.length >= 50) return false;
  let links = [], media = [];
  try {
    links = el.querySelectorAll('a[href]');
    media = el.querySelectorAll('img,iframe,object,embed');
  } catch (e) {}
  if (links.length > 2 || media.length > 2 || links.length + media.length === 0) return false;

  let adEvidence = false, untrustedExternalLink = false, trustedIdentityLink = false;
  const marked = ((el.id || '') + ' ' + classString(el)).trim();
  if (RE_AD_TOKEN.test(marked)) adEvidence = true;
  for (let i = 0; i < Math.min(links.length, 3); i++) {
    const link = links[i];
    const u = safeUrl(link.href || attr(link, 'href'));
    const marker = ((link.id || '') + ' ' + classString(link)).trim();
    if (RE_AD_TOKEN.test(marker) || (u && isAdUrl(u))) adEvidence = true;
    if (u && isUntrustedCrossSiteUrl(u)) untrustedExternalLink = true;
    if (u && isTrustedPopupHost(u.hostname)) trustedIdentityLink = true;
  }
  for (let i = 0; i < Math.min(media.length, 3); i++) {
    const item = media[i];
    const u = safeUrl(attr(item, 'src') || attr(item, 'data-src') || '');
    const marker = ((item.id || '') + ' ' + classString(item)).trim();
    if (RE_AD_TOKEN.test(marker) || (u && isAdUrl(u))) adEvidence = true;
    if (u && isUntrustedCrossSiteUrl(u)) untrustedExternalLink = true;
    if (u && isTrustedPopupHost(u.hostname)) trustedIdentityLink = true;
  }

  const sparseImageDialog = media.length > 0 && text.length < 15 && containsCloseControl(el);
  if (trustedIdentityLink && !adEvidence) return false;
  return adEvidence || (media.length > 0 && untrustedExternalLink) || sparseImageDialog;
}

function isBackdropFor(backdrop, modal) {
  if (!backdrop || !modal || backdrop === modal || backdrop.nodeType !== 1) return false;
  const tag = String(backdrop.tagName || '').toLowerCase();
  if (tag !== 'div' && tag !== 'section') return false;
  if (hasProtectedCleanupContent(backdrop) || meaningfulText(backdrop).length > 2) return false;
  let bs, ms, br, mr;
  try {
    bs = W.getComputedStyle(backdrop); ms = W.getComputedStyle(modal);
    br = backdrop.getBoundingClientRect(); mr = modal.getBoundingClientRect();
  } catch (e) { return false; }
  if (!bs || !ms || (bs.position !== 'fixed' && bs.position !== 'absolute')) return false;
  const bz = parseInt(bs.zIndex, 10), mz = parseInt(ms.zIndex, 10);
  if (!isFinite(bz) || !isFinite(mz) || mz < 10 || bz > mz || mz - bz > 1) return false;
  const vw = W.innerWidth || D.documentElement.clientWidth || 0;
  const vh = W.innerHeight || D.documentElement.clientHeight || 0;
  return !!vw && !!vh && br.width >= vw * 0.95 && br.height >= vh * 0.95
    && br.width * br.height >= mr.width * mr.height;
}

function adjacentBackdropNodes(el) {
  if (!el || !el.parentElement) return [];
  const out = [];
  const prev = el.previousElementSibling, next = el.nextElementSibling;
  if (prev && isBackdropFor(prev, el)) out.push(prev);
  if (next && isBackdropFor(next, el)) out.push(next);
  return out;
}

function removeTrackedAdNode(el, reason) {
  if (!el || !el.parentNode) return false;
  try {
    if (state.removedNodes.length > 200) state.removedNodes.shift();
    const parent = el.parentNode, next = el.nextSibling;
    state.removedNodes.push({ el, parent, next });
    parent.removeChild(el);
    stats.inc('removed');
    stats.inc('ads');
    log('BLOCK','dom', reason || 'remove', el.tagName);
    return true;
  } catch (e) { log('ERROR','apply', e && e.message); return false; }
}

function removePairedBackdrops(el) {
  const backdrops = adjacentBackdropNodes(el);
  for (const backdrop of backdrops) removeTrackedAdNode(backdrop, 'backdrop-removed');
}

function pruneAdAncestors(start) {
  let current = start;
  for (let depth = 0; current && depth < 4; depth++) {
    const tag = String(current.tagName || '').toLowerCase();
    if (tag === 'body' || tag === 'html' || tag === 'main') break;
    const next = current.parentElement;
    if (!hasProtectedCleanupContent(current) && isFloatingContainer(current)
      && isEmptyOrCloseOnly(current, 0)) {
      removePairedBackdrops(current);
      if (removeTrackedAdNode(current, 'parent-pruned')) {
        current = next;
        continue;
      }
    }
    current = next;
  }
}

function applyAction(el, action, reason) {
  if (!el || !el.parentNode) return;
  try {
    if (action === 'remove') {
      const parent = el.parentNode;
      removePairedBackdrops(el);
      if (removeTrackedAdNode(el, reason || 'remove')) pruneAdAncestors(parent);
    } else if (action === 'hide') {
      if (state.hiddenNodes.has(el)) return;
      state.hiddenNodes.add(el);
      el.setAttribute('data-nvirya-hidden', reason || '1');
      stats.inc('hidden');
    }
  } catch (e) { log('ERROR','apply', e && e.message); }
}

function hasNonClosePayload(root, excludedNode) {
  if (!root) return false;
  const stack = [root];
  let visited = 0;
  while (stack.length && visited < 500) {
    const node = stack.pop();
    if (!node || node === excludedNode) continue;
    visited++;
    if (node.nodeType === 3) {
      const text = String(node.nodeValue || '').trim();
      if (text && !CLOSE_ICON_TEXT_RE.test(text)) return true;
      continue;
    }
    if (node.nodeType !== 1 || isCloseControl(node) || isNviryaNode(node)) continue;
    const tag = String(node.tagName || '').toLowerCase();
    if (/^(?:a|img|iframe|object|embed|video|audio|canvas|form|input|select|textarea)$/.test(tag)) return true;
    const children = node.childNodes || [];
    for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
  }
  return false;
}

function hasNearbyMeaningfulContent(closeEl, rect) {
  const parent = closeEl && closeEl.parentElement;
  if (!parent) return false;
  const parentTag = String(parent.tagName || '').toLowerCase();
  if (parentTag !== 'body' && parentTag !== 'html') {
    if (hasFormControls(parent) || hasProtectedPlayerContent(parent)) return true;
    if (meaningfulText(parent, closeEl).length > 20) return true;
    return hasNonClosePayload(parent, closeEl);
  }

  const siblings = parent.children || [];
  for (let i = 0; i < siblings.length; i++) {
    const sibling = siblings[i];
    if (sibling === closeEl || isCloseControl(sibling)) continue;
    let sr;
    try { sr = sibling.getBoundingClientRect(); } catch (e) { continue; }
    const dx = Math.max(0, rect.left - sr.right, sr.left - rect.right);
    const dy = Math.max(0, rect.top - sr.bottom, sr.top - rect.bottom);
    if (Math.sqrt(dx * dx + dy * dy) > 100) continue;
    if (meaningfulText(sibling).length > 10 || hasNonClosePayload(sibling, null)) return true;
  }
  return false;
}

function isOrphanCloseButton(el) {
  if (!el || !isCloseControl(el) || isNviryaNode(el) || isProtectedPlayer(el)) return false;
  const parent = el.parentElement;
  if (!parent || hasFormControls(parent) || hasProtectedPlayerContent(parent)) return false;
  let style, rect;
  try { style = W.getComputedStyle(el); rect = el.getBoundingClientRect(); } catch (e) { return false; }
  if (!style || (style.position !== 'fixed' && style.position !== 'absolute')) return false;
  const z = parseInt(style.zIndex, 10);
  if (!isFinite(z) || z < 50) return false;
  if (hasNearbyMeaningfulContent(el, rect)) return false;
  const parentTag = String(parent.tagName || '').toLowerCase();
  return parentTag === 'body' || parentTag === 'html' || !hasNonClosePayload(parent, el);
}

function cleanupOrphanCloseButtons(root) {
  if (isDisabledHere()) return;
  let nodes = [];
  const query = '[class*="close" i],[id*="close" i],[aria-label*="close" i],[aria-label*="dismiss" i],button,[role="button"],span,svg,a,div';
  try {
    if (root && root.nodeType === 1 && root.matches && root.matches(query)) nodes.push(root);
    const found = root && root.querySelectorAll ? root.querySelectorAll(query) : D.querySelectorAll(query);
    for (let i = 0; i < found.length; i++) nodes.push(found[i]);
  } catch (e) { return; }
  const limit = Math.min(nodes.length, 800);
  for (let i = 0; i < limit; i++) {
    const el = nodes[i];
    if (!el || !el.parentNode || !isOrphanCloseButton(el)) continue;
    const parent = el.parentElement;
    removePairedBackdrops(el);
    if (removeTrackedAdNode(el, 'orphan-close-removed')) pruneAdAncestors(parent);
  }
}

function cleanupFloatingAdModals(root) {
  if (isDisabledHere()) return;
  let nodes = [];
  const query = 'div,section,dialog';
  try {
    if (root && root.nodeType === 1 && root.matches && root.matches(query)) nodes.push(root);
    const found = root && root.querySelectorAll ? root.querySelectorAll(query) : D.querySelectorAll(query);
    for (let i = 0; i < found.length; i++) nodes.push(found[i]);
  } catch (e) { return; }
  const limit = Math.min(nodes.length, 1200);
  for (let i = 0; i < limit; i++) {
    const el = nodes[i];
    if (el && el.parentNode && isFloatingAdModal(el)) applyAction(el, 'remove', 'floating-ad-modal');
  }
}

let overlayCleanupScheduled = false;
let lastOverlayCleanupTs = 0;
const overlayCleanupRoots = new Set();
function scheduleOverlayCleanup(root) {
  if (root) {
    const candidate = root.nodeType === 1 ? root : root.parentElement;
    if (candidate) overlayCleanupRoots.add(candidate);
    if (overlayCleanupRoots.size > 100) {
      overlayCleanupRoots.clear();
      if (D.documentElement) overlayCleanupRoots.add(D.documentElement);
    }
  }
  if (overlayCleanupScheduled) return;
  overlayCleanupScheduled = true;
  const delay = Math.max(60, 350 - (Date.now() - lastOverlayCleanupTs));
  setTimeout(() => {
    overlayCleanupScheduled = false;
    lastOverlayCleanupTs = Date.now();
    if (isDisabledHere()) return;
    try {
      const roots = Array.from(overlayCleanupRoots);
      overlayCleanupRoots.clear();
      if (!roots.length && D.documentElement) roots.push(D.documentElement);
      for (const scope of roots) {
        cleanupFloatingAdModals(scope);
        cleanupOrphanCloseButtons(scope);
      }
    } catch (e) { log('ERROR','overlay-cleanup', e && e.message); }
  }, delay);
}

let cosmeticStyleEl = null;
function installCosmeticCSS() {
  if (!config.cosmeticFiltering) return;
  const css = `
    [id^="google_ads_"], [id^="aswift_"], [id^="div-gpt-ad"],
    ins.adsbygoogle,
    [data-ad-slot], [data-ad-client], [data-ad-unit],
    iframe[src*="googlesyndication"], iframe[src*="doubleclick"],
    iframe[src*="adservice.google"], iframe[src*="amazon-adsystem"],
    [data-nvirya-hidden] {
      display: none !important;
      visibility: hidden !important;
      pointer-events: none !important;
    }
  `;
  try {
    let style = D.querySelector('style[data-nvirya-cosmetic]');
    if (!style) {
      style = D.createElement('style');
      style.setAttribute('data-nvirya-cosmetic','');
      (D.head || D.documentElement).appendChild(style);
    }
    style.textContent = css;
    cosmeticStyleEl = style;
  } catch (e) {}
}

function installSiteRulesCSS() {
  const rules = state.siteRules[hostname];
  let style = D.querySelector('style[data-nvirya-site-rules]');
  if (!rules || !rules.length) {
    if (style && style.parentNode) style.parentNode.removeChild(style);
    return;
  }
  const css = rules.map(r => `${r.selector}{display:none!important;}`).join('\n');
  try {
    if (!style) {
      style = D.createElement('style');
      style.setAttribute('data-nvirya-site-rules','');
      (D.head || D.documentElement).appendChild(style);
    }
    style.textContent = css;
  } catch (e) {}
}

let observer = null;
const pendingQueue = [];
const pendingSet = new WeakSet();
let scheduled = false;
const MAX_QUEUE = 800;
const handledMetaRefresh = new WeakSet();

function removeMetaRefresh(root) {
  if (!config.redirectProtection || isDisabledHere() || !root) return;
  const candidates = [];
  try {
    if (root.nodeType === 1 && String(root.tagName).toLowerCase() === 'meta') candidates.push(root);
    if (root.querySelectorAll) {
      const found = root.querySelectorAll('meta[http-equiv]');
      for (let i = 0; i < found.length; i++) candidates.push(found[i]);
    }
  } catch (e) {}
  for (const meta of candidates) {
    if (handledMetaRefresh.has(meta)) continue;
    const equiv = (attr(meta, 'http-equiv') || '').trim().toLowerCase();
    if (equiv !== 'refresh') continue;
    handledMetaRefresh.add(meta);
    const content = attr(meta, 'content') || '';
    try {
      if (!meta.parentNode) continue;
      meta.parentNode.removeChild(meta);
      recordBlocked('redirect', 'meta-refresh', content);
    } catch (e) { log('ERROR','meta-refresh', e && e.message); }
  }
}

function scheduleProcess() {
  if (scheduled) return;
  scheduled = true;
  const run = () => {
    scheduled = false;
    processQueue();
  };
  if (typeof W.requestIdleCallback === 'function') {
    W.requestIdleCallback(run, { timeout: 250 });
  } else {
    setTimeout(run, 32);
  }
}

function processQueue() {
  if (pendingQueue.length === 0) return;
  const budget = Math.min(pendingQueue.length, 120);
  let count = 0;
  while (count < budget && pendingQueue.length) {
    const el = pendingQueue.shift();
    pendingSet.delete(el);
    try { inspectAndAct(el); } catch (e) { log('ERROR','scan', e && e.message); }
    count++;
  }
  if (pendingQueue.length) scheduleProcess();
}

function inspectAndAct(el) {
  if (!el || el.nodeType !== 1) return;
  if (isDisabledHere()) return;
  if (isNviryaNode(el)) return;
  if (el.hasAttribute && el.hasAttribute('data-nvirya-hidden')) return;

  const tag = (el.tagName || '').toLowerCase();
  if (!CANDIDATE_TAGS.has(tag)) return;

  if ((tag === 'div' || tag === 'section' || tag === 'dialog') && isFloatingAdModal(el)) {
    applyAction(el, 'remove', 'floating-ad-modal');
    return;
  }

  const decision = inspectNode(el);
  if (decision.action !== 'ignore') {
    applyAction(el, decision.action, decision.reason);
    return;
  }

  if (tag === 'div' || tag === 'section' || tag === 'iframe') {
    if (isSuspiciousOverlay(el)) applyAction(el, 'remove', 'suspicious-overlay');
  }
}

function enqueue(el) {
  if (!el || el.nodeType !== 1) return;
  if (pendingSet.has(el)) return;
  if (pendingQueue.length >= MAX_QUEUE) pendingQueue.shift();
  pendingSet.add(el);
  pendingQueue.push(el);
  scheduleProcess();
}

function startObserver() {
  if (observer) return;
  try {
    observer = new MutationObserver((mutations) => {
      for (let i = 0; i < mutations.length; i++) {
        const m = mutations[i];
        if (m.type === 'childList') {
          scheduleOverlayCleanup(m.target);
          for (const n of m.addedNodes) {
            if (n.nodeType === 1) {
              scheduleOverlayCleanup(n);
              removeMetaRefresh(n);
              if (String(n.tagName).toLowerCase() === 'meta'
                && (attr(n, 'http-equiv') || '').trim().toLowerCase() === 'refresh') continue;
              enqueue(n);
              if (n.children && n.children.length) {
                const lim = Math.min(n.children.length, 30);
                for (let k = 0; k < lim; k++) enqueue(n.children[k]);
              }
            }
          }
        } else if (m.type === 'attributes') {
          scheduleOverlayCleanup(m.target);
          removeMetaRefresh(m.target);
          enqueue(m.target);
        }
      }
      scheduleOverlayCleanup();
    });
  } catch (e) { return; }
  const target = D.documentElement || D;
  observer.observe(target, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ['src','href','style','class','id','data-src','srcset','data-srcset','http-equiv','aria-label','title']
  });
}

function initialScan() {
  const root = D.body || D.documentElement;
  if (!root) return;
  removeMetaRefresh(root);
  scheduleOverlayCleanup(root);
  const all = root.querySelectorAll ? root.querySelectorAll('*') : [];
  const CHUNK = 200;
  let i = 0;
  function step() {
    const end = Math.min(i + CHUNK, all.length);
    for (; i < end; i++) enqueue(all[i]);
    if (i < all.length) scheduleProcess(), setTimeout(step, 24);
    else scheduleProcess();
  }
  step();
}

function installSPAHooks() {
  try {
    const h = W.history;
    if (h && !h.__nvirya) {
      ['pushState','replaceState'].forEach(k => {
        const orig = h[k];
        if (typeof orig !== 'function') return;
        h[k] = function () {
          const ret = orig.apply(this, arguments);
          try { onLocationChange(); } catch (e) {}
          return ret;
        };
      });
      try { Object.defineProperty(h, '__nvirya', { value: true }); } catch (e) {}
    }
    W.addEventListener('popstate', onLocationChange, true);
  } catch (e) {}
}
let lastHref = location.href;
function onLocationChange() {
  if (location.href === lastHref) return;
  lastHref = location.href;
  if (isDisabledHere()) {
    if (observer) { try { observer.disconnect(); } catch (e) {} observer = null; }
  } else {
    if (!observer) startObserver();
    installSiteRulesCSS();
    initialScan();
  }
  updateUIStatus();
}

function handleAntiAdblock() {
  if (!config.antiAdblock) return;
  try {
    const all = D.querySelectorAll('[class*="adblock" i],[id*="adblock" i],[class*="adblocker" i]');
    const lim = Math.min(all.length, 30);
    for (let i = 0; i < lim; i++) {
      const el = all[i];
      const idc = ((el.id || '') + ' ' + classString(el));
      if (!RE_ANTIADB.test(idc)) continue;
      let cs;
      try { cs = W.getComputedStyle(el); } catch (e) { continue; }
      if (!cs) continue;
      const isOverlay = (cs.position === 'fixed' || cs.position === 'absolute')
        && parseInt(cs.zIndex || 0, 10) > 1000
        && el.offsetWidth >= W.innerWidth * 0.5
        && el.offsetHeight >= W.innerHeight * 0.5;
      if (isOverlay) {
        el.setAttribute('data-nvirya-hidden','anti-adblock');
        state.hiddenNodes.add(el);
        stats.inc('hidden');
        log('BLOCK','anti-adblock','removed overlay');
      }
    }
  } catch (e) {}
}

function getSanitizedUrl() {
  try {
    const u = new URL(location.href);
    const sensitive = /auth|token|session|key|pass|code|secret|credential|email|user/i;
    const cleanParams = new URLSearchParams();
    u.searchParams.forEach((v, k) => {
      if (!sensitive.test(k)) cleanParams.set(k, v);
      else cleanParams.set(k, '[REDACTED]');
    });
    u.search = cleanParams.toString() ? '?' + cleanParams.toString() : '';
    u.hash = '';
    return u.href;
  } catch (e) {
    return (location.origin || '') + (location.pathname || '');
  }
}

function compareVersions(a, b) {
  const parse = (v) => String(v == null ? '' : v).split('.').filter(s => s !== '').map(s => parseInt(s, 10) || 0);
  const pa = parse(a), pb = parse(b);
  const len = Math.max(pa.length, pb.length);
  for (let i = 0; i < len; i++) {
    const x = pa[i] || 0, y = pb[i] || 0;
    if (x > y) return 1;
    if (x < y) return -1;
  }
  return 0;
}

const Updater = (() => {
  let checking = false;
  let scheduled = false;

  function isTopFrame() {
    try { return W.top === W.self; } catch (e) { return false; }
  }

  function getAvailable() {
    const remote = store.get(K_UPD_REMOTE, null);
    if (!remote) return null;
    if (compareVersions(VERSION, remote) < 0) return String(remote);
    store.del(K_UPD_REMOTE);
    return null;
  }

  function isSnoozed() {
    const until = Number(store.get(K_UPD_SNOOZE, 0)) || 0;
    const now = Date.now();
    return until > now && until - now <= UPDATE_SNOOZE_MS + 1000;
  }

  function snooze() { store.set(K_UPD_SNOOZE, Date.now() + UPDATE_SNOOZE_MS); }

  function fetchRemoteVersion(done) {
    let finished = false;
    const finish = (err, ver) => { if (finished) return; finished = true; done(err, ver); };
    const parse = (text) => {
      const head = String(text || '').slice(0, 8192);
      const m = head.match(/\/\/\s*@version\s+([0-9.]+)/i);
      const ver = m && m[1].replace(/^\.+|\.+$/g, '');
      if (ver && /\d/.test(ver)) finish(null, ver);
      else finish(new Error('no @version in remote file'));
    };

    if (typeof GM_xmlhttpRequest === 'function') {
      try {
        GM_xmlhttpRequest({
          method: 'GET',
          url: UPDATE_URL,
          headers: { 'Range': 'bytes=0-4095', 'Cache-Control': 'no-cache' },
          nocache: true,
          timeout: UPDATE_TIMEOUT,
          onload: (res) => {
            if (res.status === 200 || res.status === 206) parse(res.responseText);
            else finish(new Error('HTTP ' + res.status));
          },
          onerror: () => finish(new Error('network error')),
          ontimeout: () => finish(new Error('timeout')),
          onabort: () => finish(new Error('aborted'))
        });
        return;
      } catch (e) { /* fetch */ }
    }

    try {
      const ctrl = typeof AbortController === 'function' ? new AbortController() : null;
      const timer = setTimeout(() => { try { if (ctrl) ctrl.abort(); } catch (e) {} finish(new Error('timeout')); }, UPDATE_TIMEOUT);
      fetch(UPDATE_URL, { cache: 'no-store', signal: ctrl ? ctrl.signal : undefined })
        .then(res => { if (!res.ok) throw new Error('HTTP ' + res.status); return res.text(); })
        .then(text => { clearTimeout(timer); parse(text); })
        .catch(err => { clearTimeout(timer); finish(err || new Error('network error')); });
    } catch (e) { finish(e); }
  }

  function check(manual) {
    if (checking) { if (manual) UI.toast('Checking...'); return; }
    const now = Date.now();
    const last = Number(store.get(K_UPD_LAST, 0)) || 0;
    if (!manual && last <= now && now - last < UPDATE_CHECK_INTERVAL) return;

    checking = true;
    store.set(K_UPD_LAST, now);
    UI.setUpdateChecking(true);
    if (manual) UI.toast('Checking...');

    fetchRemoteVersion((err, remote) => {
      checking = false;
      UI.setUpdateChecking(false);
      try {
        if (err) {
          log('WARN', 'update', 'check failed', err && err.message);
          store.set(K_UPD_LAST, Date.now() - UPDATE_CHECK_INTERVAL + UPDATE_RETRY_AFTER);
          if (manual) UI.toast('Không thể kiểm tra cập nhật');
          return;
        }
        if (compareVersions(VERSION, remote) < 0) {
          store.set(K_UPD_REMOTE, remote);
          if (manual) store.del(K_UPD_SNOOZE);
          UI.setUpdateAvailable(remote);
          if (manual || !isSnoozed()) UI.showUpdateBanner(remote);
          log('INFO', 'update', 'new version available', remote);
        } else {
          store.del(K_UPD_REMOTE);
          UI.setUpdateAvailable(null);
          UI.hideUpdateBanner();
          if (manual) UI.toast('Bạn đang dùng bản mới nhất!');
        }
      } catch (e) { log('ERROR', 'update', e && e.message); }
    });
  }

  function schedule() {
    if (scheduled || !isTopFrame()) return;
    scheduled = true;
    const run = () => {
      try {
        const avail = getAvailable();
        if (avail && !isSnoozed()) UI.showUpdateBanner(avail);
        check(false);
      } catch (e) { log('ERROR', 'update', e && e.message); }
    };
    setTimeout(() => {
      if (typeof W.requestIdleCallback === 'function') W.requestIdleCallback(run, { timeout: 3000 });
      else run();
    }, UPDATE_START_DELAY);
  }

  return { check, schedule, getAvailable, isSnoozed, snooze };
})();

const UI = (() => {
  const K_FAB = 'nvirya_x_fab';
  const THEMES = ['auto', 'light', 'dark'];
  const SVG_NS = 'http://www.w3.org/2000/svg';
  const FAB_SIZE = 46, FAB_MARGIN = 10;

  let host = null, root = null;
  let menuOpen = false;
  let handleEl = null, backdropEl = null, sheetEl = null, grabEl = null, bodyEl = null;
  let themeBtn = null, segBtns = [];
  let statusEl = null, statsEl = null, toastEl = null;
  let reportModalEl = null, reportReasonSelect = null, reportNoteInput = null;
  let pickerEl = null, pickerLabelEl = null, pickerActive = false;
  let pickerHover = null;
  let toastTimer = null;
  let lastToast = '';
  let statsTimer = null;
  let fab = { side: 'r', y: 0.7 };
  let statsMode = 'session'; // 'session' | 'lifetime'
  let segSessionBtn = null, segLifetimeBtn = null;
  let updateEl = null, ubTitleEl = null, newBadgeEl = null;
  let updateAvailable = null, updateChecking = false;
  let uiReady = false;

  function el(tag, props, ...children) {
    const e = document.createElement(tag);
    if (props) for (const k in props) {
      if (k === 'style' && typeof props[k] === 'object') Object.assign(e.style, props[k]);
      else if (k === 'class') e.className = props[k];
      else if (k.startsWith('on')) e.addEventListener(k.slice(2).toLowerCase(), props[k]);
      else if (k === 'text') e.textContent = props[k];
      else e.setAttribute(k, props[k]);
    }
    for (const c of children) if (c) e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
    return e;
  }
  function clear(n) { while (n && n.firstChild) n.removeChild(n.firstChild); }

  const ICONS = {
    shield:   ['M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z'],
    sun:      [['c', 12, 12, 4], 'M12 2v2', 'M12 20v2', 'M4.93 4.93l1.41 1.41', 'M17.66 17.66l1.41 1.41', 'M2 12h2', 'M20 12h2', 'M6.34 17.66l-1.41 1.41', 'M19.07 4.93l-1.41 1.41'],
    moon:     ['M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z'],
    close:    ['M18 6 6 18', 'M6 6l12 12'],
    chevron:  ['m9 18 6-6-6-6'],
    allow:    [['c', 12, 12, 10], 'm8 12 3 3 5-6'],
    pause:    [['c', 12, 12, 10], 'M10 9v6', 'M14 9v6'],
    target:   [['c', 12, 12, 10], 'M22 12h-4', 'M6 12H2', 'M12 6V2', 'M12 22v-4'],
    wand:     ['M15 4V2', 'M15 16v-2', 'M8 9h2', 'M20 9h2', 'M17.8 11.8 19 13', 'M15 9h.01', 'M17.8 6.2 19 5', 'M3 21l9-9', 'M12.2 6.2 11 5'],
    undo:     ['M3 7v6h6', 'M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13'],
    copy:     [['r', 9, 9, 13, 13, 2], 'M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1'],
    flag:     ['M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z', 'M4 22v-7'],
    download: ['M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4', 'm7 10 5 5 5-5', 'M12 15V3']
  };
  function icon(name, cls) {
    const s = document.createElementNS(SVG_NS, 'svg');
    s.setAttribute('viewBox', '0 0 24 24');
    s.setAttribute('fill', 'none');
    s.setAttribute('stroke', 'currentColor');
    s.setAttribute('stroke-width', '2');
    s.setAttribute('stroke-linecap', 'round');
    s.setAttribute('stroke-linejoin', 'round');
    s.setAttribute('aria-hidden', 'true');
    if (cls) s.setAttribute('class', cls);
    for (const part of (ICONS[name] || [])) {
      let n;
      if (typeof part === 'string') {
        n = document.createElementNS(SVG_NS, 'path');
        n.setAttribute('d', part);
      } else if (part[0] === 'c') {
        n = document.createElementNS(SVG_NS, 'circle');
        n.setAttribute('cx', part[1]); n.setAttribute('cy', part[2]); n.setAttribute('r', part[3]);
      } else {
        n = document.createElementNS(SVG_NS, 'rect');
        n.setAttribute('x', part[1]); n.setAttribute('y', part[2]);
        n.setAttribute('width', part[3]); n.setAttribute('height', part[4]); n.setAttribute('rx', part[5]);
      }
      s.appendChild(n);
    }
    return s;
  }
  function fmt(n) { try { return Number(n || 0).toLocaleString(); } catch (e) { return String(n); } }

  const LIGHT_VARS = `
      --cs: light;
      --sheet: rgba(255,255,255,.94);
      --card: #f4f4f6;
      --card-hover: #eaeaea;
      --text: #18181b;
      --muted: #71717a;
      --border: rgba(0,0,0,.08);
      --accent: #3b82f6;
      --danger: #dc2626;
      --pill: rgba(0,0,0,.18);
      --ring: #ffffff;
      --backdrop: rgba(20,20,26,.34);
      --switch-off: #d4d4d8;
      --seg-bg: rgba(0,0,0,.06);
      --seg-on: #ffffff;
      --toast-bg: #18181b;
      --toast-fg: #f4f4f5;
      --shadow: 0 -10px 40px rgba(0,0,0,.16);
      --fab-shadow: 0 4px 16px rgba(0,0,0,.2);
  `;
  const DARK_VARS = `
      --cs: dark;
      --sheet: rgba(18,18,21,.94);
      --card: #1a1a1e;
      --card-hover: #232328;
      --text: #f4f4f5;
      --muted: #a1a1aa;
      --border: rgba(255,255,255,.08);
      --accent: #3b82f6;
      --danger: #f87171;
      --pill: rgba(255,255,255,.22);
      --ring: #121215;
      --backdrop: rgba(0,0,0,.52);
      --switch-off: #3f3f46;
      --seg-bg: rgba(255,255,255,.08);
      --seg-on: #3a3a41;
      --toast-bg: #f4f4f5;
      --toast-fg: #18181b;
      --shadow: 0 -10px 40px rgba(0,0,0,.5);
      --fab-shadow: 0 4px 16px rgba(0,0,0,.5);
  `;

  const CSS = `
    :host { all: initial; }
    :host { ${LIGHT_VARS} }
    @media (prefers-color-scheme: dark) {
      :host([data-theme="auto"]) { ${DARK_VARS} }
    }
    :host([data-theme="dark"]) { ${DARK_VARS} }
    :host([data-theme="light"]) { ${LIGHT_VARS} }

    * {
      box-sizing: border-box;
      font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, system-ui, "Helvetica Neue", Arial, sans-serif;
      -webkit-tap-highlight-color: transparent;
    }
    button { font: inherit; color: inherit; margin: 0; }
    svg { display: block; }

    .handle {
      position: fixed; left: auto; top: auto;
      width: ${FAB_SIZE}px; height: ${FAB_SIZE}px;
      border-radius: 50%;
      display: flex; align-items: center; justify-content: center;
      background: var(--sheet);
      color: var(--accent);
      border: 1px solid var(--border);
      box-shadow: var(--fab-shadow);
      -webkit-backdrop-filter: blur(14px) saturate(1.4);
      backdrop-filter: blur(14px) saturate(1.4);
      opacity: .78;
      cursor: grab;
      user-select: none; -webkit-user-select: none;
      touch-action: none;
      z-index: 2147482999;
      transition: left .28s cubic-bezier(.32,.72,0,1), top .28s cubic-bezier(.32,.72,0,1), opacity .18s ease, transform .18s ease;
    }
    .handle svg { width: 22px; height: 22px; }
    .handle:hover, .handle:focus-visible { opacity: 1; }
    .handle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
    .handle.hidden { display: none !important; }
    .handle.dragging { cursor: grabbing; opacity: 1; transform: scale(1.08); transition: opacity .18s ease, transform .18s ease; }
    .handle.away { opacity: 0; pointer-events: none; transform: scale(.8); }
    .handle { --lc: 34,197,94; }
    .handle[data-state="warn"] { --lc: 245,158,11; }
    .handle[data-state="off"] { --lc: 113,113,122; }
    .fab-dot {
      position: absolute; top: 3px; right: 3px;
      width: 10px; height: 10px; border-radius: 50%;
      background: rgb(var(--lc));
      box-shadow: 0 0 0 2px var(--ring);
    }

    .backdrop {
      position: fixed; top: 0; left: 0; right: 0; bottom: 0;
      background: var(--backdrop);
      -webkit-backdrop-filter: blur(8px);
      backdrop-filter: blur(8px);
      opacity: 0; visibility: hidden;
      touch-action: none;
      transition: opacity .3s ease, visibility 0s linear .3s;
      z-index: 2147483000;
    }
    .backdrop.open { opacity: 1; visibility: visible; transition-delay: 0s; }

    .sheet {
      position: fixed; left: 0; right: 0; bottom: 0;
      max-height: 85vh; max-height: 85dvh;
      display: flex; flex-direction: column;
      background: var(--sheet);
      color: var(--text);
      color-scheme: var(--cs);
      font-size: 14px; line-height: 1.4;
      border: 1px solid var(--border); border-bottom: none;
      border-radius: 24px 24px 0 0;
      box-shadow: var(--shadow);
      -webkit-backdrop-filter: blur(28px) saturate(1.5);
      backdrop-filter: blur(28px) saturate(1.5);
      overflow: hidden;
      transform: translateY(105%);
      visibility: hidden;
      transition: transform .4s cubic-bezier(.32,.72,0,1), opacity .25s ease, visibility 0s linear .4s;
      will-change: transform;
      z-index: 2147483001;
    }
    .sheet.open { transform: translateY(0); visibility: visible; transition-delay: 0s; }
    .sheet:focus { outline: none; }

    @media (min-width: 641px) {
      .sheet {
        left: 50%; right: auto; top: 50%; bottom: auto;
        width: 390px; max-height: 85vh;
        border: 1px solid var(--border);
        border-radius: 20px;
        box-shadow: 0 24px 70px rgba(0,0,0,.35);
        opacity: 0;
        transform: translate(-50%, -46%) scale(.96);
      }
      .sheet.open { opacity: 1; transform: translate(-50%, -50%) scale(1); }
      .pill { display: none; }
    }

    .grab { flex: none; padding-top: 8px; touch-action: none; }
    .pill { width: 36px; height: 5px; border-radius: 3px; background: var(--pill); margin: 0 auto 8px; }
    .head {
      display: flex; align-items: center; gap: 10px;
      padding: 6px 16px 12px;
      border-bottom: 1px solid var(--border);
    }
    @media (min-width: 641px) { .head { padding-top: 16px; } }
    .titles { flex: 1; min-width: 0; }
    .titles h1 { margin: 0; font-size: 17px; font-weight: 700; letter-spacing: -.01em; color: var(--text); }
    .badge {
      display: inline-block; max-width: 100%;
      margin-top: 4px; padding: 2px 9px;
      border-radius: 999px;
      background: var(--card); border: 1px solid var(--border);
      color: var(--muted); font-size: 11.5px; line-height: 1.5;
      overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
      vertical-align: top;
    }
    .head-btns { flex: none; display: flex; gap: 8px; }
    .icon-btn {
      width: 32px; height: 32px; border-radius: 50%;
      display: grid; place-items: center;
      background: var(--card); color: var(--muted);
      border: none; cursor: pointer; padding: 0;
      transition: background .15s ease, color .15s ease;
    }
    .icon-btn svg { width: 16px; height: 16px; }
    .icon-btn:active { background: var(--card-hover); color: var(--text); }
    .icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
    @media (hover: hover) { .icon-btn:hover { background: var(--card-hover); color: var(--text); } }

    .body {
      flex: 1; min-height: 0;
      overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;
      padding: 14px 16px calc(20px + env(safe-area-inset-bottom, 0px));
    }
    .sec-title { margin: 20px 4px 8px; font-size: 13px; font-weight: 600; color: var(--muted); }

    .status {
      display: flex; align-items: center; gap: 12px;
      padding: 14px;
      background: var(--card);
      border: 1px solid var(--border);
      border-radius: 16px;
    }
    .led {
      --lc: 34,197,94;
      flex: none; width: 10px; height: 10px; border-radius: 50%;
      background: rgb(var(--lc));
      box-shadow: 0 0 8px rgba(var(--lc), .7);
      animation: pulse 2.2s ease-out infinite;
    }
    .led.warn { --lc: 245,158,11; }
    .led.off { --lc: 113,113,122; animation: none; box-shadow: none; }
    @keyframes pulse {
      0%   { box-shadow: 0 0 0 0 rgba(var(--lc), .55), 0 0 8px rgba(var(--lc), .6); }
      70%  { box-shadow: 0 0 0 10px rgba(var(--lc), 0), 0 0 8px rgba(var(--lc), .6); }
      100% { box-shadow: 0 0 0 0 rgba(var(--lc), 0), 0 0 8px rgba(var(--lc), .6); }
    }
    .status .t { font-size: 15px; font-weight: 600; }
    .status .s { font-size: 12px; color: var(--muted); margin-top: 1px; }

    .stats { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
    .stat { background: var(--card); border: 1px solid var(--border); border-radius: 14px; padding: 12px 14px; min-width: 0; }
    .stat:last-child:nth-child(odd) { grid-column: 1 / -1; }
    .stat b { display: block; font-size: 24px; font-weight: 700; letter-spacing: -.02em; line-height: 1.15; font-variant-numeric: tabular-nums; color: var(--text); }
    .stat span { display: block; margin-top: 2px; font-size: 12px; color: var(--muted); font-weight: 500; }
    .stat-sub { display: block; margin-top: 2px; font-size: 11px; color: var(--muted); opacity: .85; }

    .group { background: var(--card); border: 1px solid var(--border); border-radius: 14px; overflow: hidden; --sep: 14px; }
    .group.icons { --sep: 56px; }
    .group.pad { padding: 6px; }
    .group + .group { margin-top: 10px; }
    .item {
      position: relative;
      display: flex; align-items: center; gap: 12px;
      width: 100%; min-height: 50px;
      padding: 10px 14px;
      background: none; border: none;
      color: var(--text); text-align: left;
      font-size: 15px; cursor: pointer;
      transition: background .12s ease;
    }
    .item + .item::before { content: ''; position: absolute; top: 0; left: var(--sep); right: 0; height: 1px; background: var(--border); }
    .item:active { background: var(--card-hover); }
    .item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
    @media (hover: hover) { .item:hover { background: var(--card-hover); } }
    .item .txt { flex: 1; min-width: 0; display: block; }
    .item .lbl { display: block; font-weight: 500; }
    .item .sub { display: block; margin-top: 1px; font-size: 12px; color: var(--muted); }
    .item.danger .lbl { color: var(--danger); }
    .chev { flex: none; width: 16px; height: 16px; color: var(--muted); opacity: .6; }
    .tile { flex: none; width: 30px; height: 30px; border-radius: 8px; display: grid; place-items: center; color: #fff; }
    .tile svg { width: 17px; height: 17px; }
    .t-green  { background: #34c759; }
    .t-orange { background: #ff9f0a; }
    .t-blue   { background: #3b82f6; }
    .t-purple { background: #6366f1; }
    .t-gray   { background: #8e8e93; }

    .switch { position: relative; flex: none; width: 46px; height: 28px; }
    .switch input { position: absolute; top: 0; left: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; z-index: 1; }
    .track { position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 14px; background: var(--switch-off); transition: background .22s ease; }
    .track::after {
      content: ''; position: absolute; top: 2px; left: 2px;
      width: 24px; height: 24px; border-radius: 50%;
      background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.3);
      transition: transform .24s cubic-bezier(.32,.72,0,1);
    }
    .switch input:checked + .track { background: var(--accent); }
    .switch input:checked + .track::after { transform: translateX(18px); }
    .switch input:focus-visible + .track { outline: 2px solid var(--accent); outline-offset: 2px; }

    .seg { display: flex; padding: 3px; gap: 2px; border-radius: 10px; background: var(--seg-bg); }
    .seg button {
      flex: 1; padding: 7px 0; border: none; border-radius: 8px;
      background: none; color: var(--muted);
      font-size: 13px; font-weight: 600; cursor: pointer;
      transition: background .18s ease, color .18s ease, box-shadow .18s ease;
    }
    .seg button[aria-pressed="true"] { background: var(--seg-on); color: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.18); }
    .seg button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

    .footer { margin-top: 18px; text-align: center; font-size: 11.5px; color: var(--muted); opacity: .8; }

    .toast {
      position: fixed; left: 50%; bottom: calc(18px + env(safe-area-inset-bottom, 0px));
      transform: translate(-50%, 16px);
      background: var(--toast-bg); color: var(--toast-fg);
      padding: 9px 14px; border-radius: 999px;
      font-size: 13px; font-weight: 500;
      box-shadow: 0 6px 24px rgba(0,0,0,.25);
      opacity: 0; pointer-events: none;
      transition: opacity .2s ease, transform .2s ease;
      z-index: 2147483002; max-width: 90vw;
    }
    .toast.top { bottom: auto; top: calc(14px + env(safe-area-inset-top, 0px)); transform: translate(-50%, -16px); }
    .toast.show, .toast.top.show { opacity: 1; transform: translate(-50%, 0); }

    .new-badge {
      display: none;
      margin-left: 8px; padding: 2px 7px;
      border-radius: 999px;
      background: #ff3b30; color: #fff;
      font-size: 10px; font-weight: 700; letter-spacing: .04em; line-height: 1.4;
      vertical-align: middle; position: relative; top: -1px;
    }
    .new-badge.show { display: inline-block; }

    .update-banner {
      position: fixed; left: 50%; top: calc(12px + env(safe-area-inset-top, 0px));
      width: min(92vw, 380px);
      padding: 14px;
      background: var(--sheet); color: var(--text);
      border: 1px solid var(--border); border-radius: 16px;
      box-shadow: 0 12px 40px rgba(0,0,0,.22), 0 2px 8px rgba(0,0,0,.08);
      -webkit-backdrop-filter: blur(28px) saturate(1.5);
      backdrop-filter: blur(28px) saturate(1.5);
      font-size: 14px; line-height: 1.4;
      opacity: 0; visibility: hidden; pointer-events: none;
      transform: translate(-50%, calc(-100% - 24px));
      transition: transform .45s cubic-bezier(.32,.72,0,1), opacity .25s ease, visibility 0s linear .45s;
      will-change: transform;
      z-index: 2147483004;
    }
    .update-banner.show {
      opacity: 1; visibility: visible; pointer-events: auto;
      transform: translate(-50%, 0);
      transition-delay: 0s;
    }
    .ub-row { display: flex; align-items: flex-start; gap: 12px; }
    .ub-icon { width: 36px; height: 36px; border-radius: 10px; }
    .ub-icon svg { width: 19px; height: 19px; }
    .ub-txt { flex: 1; min-width: 0; }
    .ub-title { font-size: 15px; font-weight: 700; letter-spacing: -.01em; }
    .ub-desc { margin-top: 2px; font-size: 12.5px; color: var(--muted); }
    .ub-close { width: 28px; height: 28px; margin: -2px -2px 0 0; }
    .ub-actions { display: flex; gap: 8px; margin-top: 12px; }
    .ub-btn {
      flex: 1; display: flex; align-items: center; justify-content: center;
      min-height: 38px; padding: 0 12px;
      border: none; border-radius: 12px;
      font-size: 14px; font-weight: 600;
      cursor: pointer; text-decoration: none;
      transition: opacity .15s ease;
    }
    .ub-btn:active { opacity: .8; }
    .ub-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
    .ub-btn.later { background: var(--card); color: var(--text); border: 1px solid var(--border); }
    .ub-btn.go { background: var(--accent); color: #fff; }

    .report-modal {
      position: fixed; top: 0; left: 0; right: 0; bottom: 0;
      background: var(--backdrop);
      -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
      display: flex; align-items: center; justify-content: center;
      padding: 16px; opacity: 0; visibility: hidden;
      transition: opacity .25s ease, visibility 0s linear .25s;
      z-index: 2147483010;
    }
    .report-modal.open { opacity: 1; visibility: visible; transition-delay: 0s; }
    .report-card {
      background: var(--sheet); color: var(--text);
      width: 100%; max-width: 360px;
      border: 1px solid var(--border); border-radius: 20px;
      padding: 18px; box-shadow: var(--shadow);
      display: flex; flex-direction: column; gap: 10px;
      box-sizing: border-box;
    }
    .report-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
    .report-head h2 { margin: 0; font-size: 16px; font-weight: 700; }
    .report-lbl { font-size: 12.5px; font-weight: 600; color: var(--muted); margin-top: 4px; }
    .report-select, .report-textarea {
      width: 100%; box-sizing: border-box;
      background: var(--card); color: var(--text);
      border: 1px solid var(--border); border-radius: 10px;
      padding: 9px 12px; font-size: 13.5px; font-family: inherit;
    }
    .report-select:focus, .report-textarea:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
    .report-textarea { resize: vertical; min-height: 64px; }
    .report-actions { display: flex; gap: 8px; margin-top: 8px; }
    .report-btn {
      flex: 1; padding: 10px 0; border-radius: 10px; border: none;
      font-size: 13.5px; font-weight: 600; cursor: pointer;
      transition: opacity .15s ease;
    }
    .report-btn.cancel { background: var(--card); color: var(--text); border: 1px solid var(--border); }
    .report-btn.submit { background: var(--accent); color: #fff; }
    .report-btn:active { opacity: .8; }

    @media (prefers-reduced-motion: reduce) {
      .update-banner { transition: none !important; }
      .handle, .backdrop, .sheet, .toast, .track, .track::after, .item, .seg button, .report-modal { transition: none !important; }
      .led { animation: none !important; }
    }
  `;

  function themeSetting() { return THEMES.indexOf(config.theme) !== -1 ? config.theme : 'auto'; }
  function effectiveTheme() {
    const t = themeSetting();
    if (t !== 'auto') return t;
    try { return W.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } catch (e) { return 'light'; }
  }
  function updateThemeUI() {
    const eff = effectiveTheme();
    if (themeBtn) {
      clear(themeBtn);
      themeBtn.appendChild(icon(eff === 'dark' ? 'sun' : 'moon'));
      const lbl = eff === 'dark' ? 'Switch to light theme' : 'Switch to dark theme';
      themeBtn.setAttribute('aria-label', lbl);
      themeBtn.setAttribute('title', lbl);
    }
    const cur = themeSetting();
    for (const b of segBtns) b.setAttribute('aria-pressed', String(b.dataset.val === cur));
  }
  function applyTheme() {
    if (host) host.setAttribute('data-theme', themeSetting());
    updateThemeUI();
  }
  function setTheme(val) {
    config.theme = THEMES.indexOf(val) !== -1 ? val : 'auto';
    saveConfig();
    applyTheme();
  }

  function loadFab() {
    const s = store.get(K_FAB, null);
    if (s && (s.side === 'l' || s.side === 'r') && typeof s.y === 'number' && isFinite(s.y)) {
      fab = { side: s.side, y: Math.max(0, Math.min(1, s.y)) };
    }
  }
  function applyFab() {
    if (!handleEl) return;
    const vw = D.documentElement.clientWidth || W.innerWidth || 360;
    const vh = W.innerHeight || 640;
    const left = fab.side === 'l' ? FAB_MARGIN : Math.max(FAB_MARGIN, vw - FAB_SIZE - FAB_MARGIN);
    const top = Math.max(FAB_MARGIN, Math.min(vh - FAB_SIZE - FAB_MARGIN, Math.round(fab.y * vh)));
    handleEl.style.left = left + 'px';
    handleEl.style.top = top + 'px';
  }

  function build() {
    if (uiReady && host && host.isConnected) return;
    host = document.createElement('div');
    host.setAttribute('data-nvirya-ui', '');
    host.style.cssText = 'all:initial;';
    root = host.attachShadow({ mode: 'open' });
    const style = document.createElement('style');
    style.textContent = CSS;
    root.appendChild(style);

    handleEl = el('div', {
      class: 'handle', role: 'button', tabindex: '0',
      'aria-label': 'nvirya AdGuard', 'aria-haspopup': 'dialog', 'aria-expanded': 'false',
      'data-state': 'ok'
    }, icon('shield'), el('span', { class: 'fab-dot' }));
    root.appendChild(handleEl);

    backdropEl = el('div', { class: 'backdrop', onclick: () => setMenu(false) });
    root.appendChild(backdropEl);

    sheetEl = el('div', { class: 'sheet', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Nvirya AdGuard X', 'aria-hidden': 'true', tabindex: '-1' });
    themeBtn = el('button', { class: 'icon-btn', type: 'button', onclick: () => setTheme(effectiveTheme() === 'dark' ? 'light' : 'dark') });
    const closeBtn = el('button', { class: 'icon-btn', type: 'button', 'aria-label': 'Close', onclick: () => setMenu(false) }, icon('close'));
    const head = el('div', { class: 'head' },
      el('div', { class: 'titles' },
        el('h1', null, 'Nvirya AdGuard X',
          newBadgeEl = el('span', { class: 'new-badge', 'aria-label': 'There is a new update' }, 'NEW')),
        el('div', { class: 'badge' }, hostname || '(unknown)')
      ),
      el('div', { class: 'head-btns' }, themeBtn, closeBtn)
    );
    grabEl = el('div', { class: 'grab' }, el('div', { class: 'pill' }), head);
    bodyEl = el('div', { class: 'body' });
    sheetEl.appendChild(grabEl);
    sheetEl.appendChild(bodyEl);
    root.appendChild(sheetEl);

    toastEl = el('div', { class: 'toast', role: 'status' });
    root.appendChild(toastEl);

    updateEl = el('div', { class: 'update-banner', role: 'alert', 'aria-hidden': 'true' },
      el('div', { class: 'ub-row' },
        el('span', { class: 'tile t-blue ub-icon' }, icon('download')),
        el('div', { class: 'ub-txt' },
          ubTitleEl = el('div', { class: 'ub-title' }, 'There is a new update!'),
          el('div', { class: 'ub-desc' }, 'The update helps improve the ability to block ads.')
        ),
        el('button', { class: 'icon-btn ub-close', type: 'button', 'aria-label': 'Để sau', onclick: () => dismissUpdateBanner() }, icon('close'))
      ),
			el('div', { class: 'ub-actions' },
        el('button', { class: 'ub-btn later', type: 'button', onclick: () => dismissUpdateBanner() }, 'Để sau'),
        el('a', { class: 'ub-btn go', href: STAY_INSTALL_URL,
          onclick: () => { Updater.snooze(); hideUpdateBanner(); 
          } 
        }, 'Cập nhật ngay')
      )
    );
    root.appendChild(updateEl);

    (D.documentElement || D.body).appendChild(host);

    loadFab();
    updateAvailable = Updater.getAvailable();
    buildBody();
    applyTheme();
    applyFab();
    applyUpdateBadge();

    if (!config.showFloatingButton) handleEl.classList.add('hidden');

    wireHandle();
    wireSheet();

    try {
      const mq = W.matchMedia('(prefers-color-scheme: dark)');
      if (mq.addEventListener) mq.addEventListener('change', updateThemeUI);
      else if (mq.addListener) mq.addListener(updateThemeUI);
    } catch (e) {}
    try {
      W.addEventListener('resize', applyFab, { passive: true });
      W.addEventListener('orientationchange', () => setTimeout(applyFab, 200), { passive: true });
    } catch (e) {}

    uiReady = true;
  }

  function sectionTitle(t) { return el('div', { class: 'sec-title' }, t); }

  function item(o) {
    const b = el('button', { class: 'item' + (o.danger ? ' danger' : ''), type: 'button', onclick: o.onclick });
    if (o.tone) b.appendChild(el('span', { class: 'tile t-' + o.tone }, icon(o.icon)));
    const txt = el('span', { class: 'txt' }, el('span', { class: 'lbl' }, o.label || ''));
    txt.appendChild(el('span', { class: 'sub' }, o.sub || ''));
    b.appendChild(txt);
    if (o.chevron) b.appendChild(icon('chevron', 'chev'));
    if (o.role) b.dataset.role = o.role;
    return b;
  }

  function setStatsMode(mode) {
    statsMode = mode;
    if (segSessionBtn) segSessionBtn.setAttribute('aria-pressed', String(mode === 'session'));
    if (segLifetimeBtn) segLifetimeBtn.setAttribute('aria-pressed', String(mode === 'lifetime'));
    renderStats();
  }

  function buildBody() {
    clear(bodyEl);

    statusEl = el('div', { class: 'status' });
    bodyEl.appendChild(statusEl);

    bodyEl.appendChild(sectionTitle('Blocked Stats'));
    const statSeg = el('div', { class: 'seg', role: 'group', 'aria-label': 'Stats Mode', style: { marginBottom: '8px' } });
    segSessionBtn = el('button', { type: 'button', 'aria-pressed': 'true', onclick: () => setStatsMode('session') }, 'This Page');
    segLifetimeBtn = el('button', { type: 'button', 'aria-pressed': 'false', onclick: () => setStatsMode('lifetime') }, 'All Time');
    statSeg.appendChild(segSessionBtn);
    statSeg.appendChild(segLifetimeBtn);
    bodyEl.appendChild(statSeg);

    statsEl = el('div', { class: 'stats' });
    bodyEl.appendChild(statsEl);

    bodyEl.appendChild(sectionTitle('Actions'));
    const actions = el('div', { class: 'group icons' });

    actions.appendChild(item({ role: 'wl', tone: 'green', icon: 'allow', chevron: true, onclick: () => {
      if (isWhitelisted(hostname)) { removeWhitelist(hostname); toast('Removed from whitelist'); }
      else { addWhitelist(hostname); toast('Whitelisted for this site'); }
      renderStatus(); refreshProtection();
    }}));
    actions.appendChild(item({ role: 'td', tone: 'orange', icon: 'pause', chevron: true, onclick: () => {
      if (state.sessionDisabled[hostname]) { delete state.sessionDisabled[hostname]; persistSession(); toast('Protection enabled for this session'); }
      else { state.sessionDisabled[hostname] = true; persistSession(); toast('Disabled for this session'); }
      renderStatus(); refreshProtection();
    }}));
    actions.appendChild(item({ label: 'Block element', sub: 'Pick an element to hide on this site', tone: 'blue', icon: 'target', chevron: true,
      onclick: () => { setMenu(false); enterPicker(); } }));
    actions.appendChild(item({ label: 'Clean page', sub: 'Hide leftover ad frames now', tone: 'purple', icon: 'wand', chevron: true,
      onclick: () => { cleanPage(); toast('Page cleaned'); } }));
    actions.appendChild(item({ label: 'Report broken / Missed ad', sub: 'Report missed ads or page breakage', tone: 'orange', icon: 'flag', chevron: true,
      onclick: () => openReportModal() }));
    actions.appendChild(item({ label: 'Undo last block', sub: 'Restore the last removed element', tone: 'gray', icon: 'undo', chevron: true,
      onclick: () => undoLast() }));
    bodyEl.appendChild(actions);

    bodyEl.appendChild(sectionTitle('Settings'));
    const themeGroup = el('div', { class: 'group pad' });
    const seg = el('div', { class: 'seg', role: 'group', 'aria-label': 'Theme' });
    segBtns = [];
    for (const [val, label] of [['auto', 'Auto'], ['light', 'Light'], ['dark', 'Dark']]) {
      const b = el('button', { type: 'button', onclick: () => setTheme(val) }, label);
      b.dataset.val = val;
      segBtns.push(b);
      seg.appendChild(b);
    }
    themeGroup.appendChild(seg);
    bodyEl.appendChild(themeGroup);

    const toggles = [
      ['enabled', 'Protection'],
      ['popupProtection', 'Popup blocking'],
      ['redirectProtection', 'Redirect protection'],
      ['cosmeticFiltering', 'Cosmetic filtering'],
      ['playerProtection', 'Player protection'],
      ['antiAdblock', 'Anti-adblock cleanup'],
      ['strictMode', 'Strict mode'],
      ['showFloatingButton', 'Show floating button'],
      ['debug', 'Debug logging'],
    ];
    const swGroup = el('div', { class: 'group' });
    for (const [k, label] of toggles) {
      const row = el('label', { class: 'item' });
      row.appendChild(el('span', { class: 'txt' }, el('span', { class: 'lbl' }, label)));
      const inp = el('input', { type: 'checkbox', role: 'switch' });
      inp.checked = !!config[k];
      inp.addEventListener('change', () => {
        config[k] = inp.checked;
        if (k === 'showFloatingButton') {
          config.edgeHandle = inp.checked;
          if (handleEl) handleEl.classList.toggle('hidden', !inp.checked);
        }
        saveConfig();
        if (k === 'enabled' || k === 'cosmeticFiltering' || k === 'redirectProtection') refreshProtection();
        renderStatus(); renderStats();
      });
      row.appendChild(el('span', { class: 'switch' }, inp, el('span', { class: 'track' })));
      swGroup.appendChild(row);
    }
    bodyEl.appendChild(swGroup);

    bodyEl.appendChild(sectionTitle('Backup & Share Rules'));
    const shareGroup = el('div', { class: 'group icons' });
    shareGroup.appendChild(item({
      label: 'Export Rules',
      sub: 'Copy custom site rules to clipboard',
      tone: 'blue',
      icon: 'copy',
      chevron: true,
      onclick: () => exportSiteRules()
    }));
    shareGroup.appendChild(item({
      label: 'Import Rules',
      sub: 'Merge rules from JSON or Base64',
      tone: 'purple',
      icon: 'download',
      chevron: true,
      onclick: () => importSiteRules()
    }));
    bodyEl.appendChild(shareGroup);

    bodyEl.appendChild(sectionTitle('Data'));
    const data = el('div', { class: 'group' });
    data.appendChild(item({ role: 'upd', label: 'Check for updates', sub: 'Current version v' + VERSION,
      onclick: () => Updater.check(true) }));
    data.appendChild(item({ label: 'Reset settings', sub: 'Restore the default options', danger: true, onclick: () => {
      if (!confirm('Reset all settings to defaults?')) return;
      config = Object.assign({}, DEFAULT_CONFIG);
      saveConfig(); buildBody(); applyTheme();
      if (handleEl) handleEl.classList.toggle('hidden', !config.showFloatingButton);
      refreshProtection(); toast('Settings reset');
    }}));
    data.appendChild(item({ label: 'Clear statistics', sub: 'Set all counters back to zero', danger: true, onclick: () => {
      if (!confirm('Clear all lifetime and session statistics?')) return;
      stats.reset(); renderStats(); toast('Stats cleared');
    }}));
    data.appendChild(item({ label: 'Reset site rules', sub: 'Remove all blocked elements on this site', danger: true, onclick: () => {
      if (!confirm('Clear all manual rules for this site?')) return;
      delete state.siteRules[hostname]; persistSiteRules(); toast('Site rules cleared'); onLocationChange();
    }}));
    bodyEl.appendChild(data);

    bodyEl.appendChild(sectionTitle('Debug'));
    const dbg = el('div', { class: 'group icons' });
    dbg.appendChild(item({ label: 'Copy debug info', sub: 'Copy diagnostics to the clipboard', tone: 'gray', icon: 'copy', onclick: () => {
      const info = collectDebugInfo();
      try { navigator.clipboard.writeText(info); toast('Debug info copied'); } catch (e) { console.log(info); toast('Logged to console'); }
    }}));
    bodyEl.appendChild(dbg);

    bodyEl.appendChild(el('div', { class: 'footer' }, 'Nvirya AdGuard X v' + VERSION));

    updateThemeUI();
    renderStatus(); renderStats();
    renderUpdateItem();
  }

  function renderUpdateItem() {
    if (!bodyEl) return;
    const b = bodyEl.querySelector('[data-role="upd"]');
    if (!b) return;
    const s = b.querySelector('.sub');
    if (!s) return;
    if (updateChecking) s.textContent = 'Checking...';
    else if (updateAvailable) s.textContent = 'There is a new version v' + updateAvailable + ' (now v' + VERSION + ')';
    else s.textContent = 'Current version v' + VERSION;
  }
  function applyUpdateBadge() {
    if (newBadgeEl) newBadgeEl.classList.toggle('show', !!updateAvailable);
  }
  function setUpdateAvailable(ver) {
    updateAvailable = ver || null;
    applyUpdateBadge();
    renderUpdateItem();
  }
  function setUpdateChecking(on) {
    updateChecking = !!on;
    renderUpdateItem();
  }
  function showUpdateBanner(ver) {
    if (!updateEl) return;
    if (ver) setUpdateAvailable(ver);
    if (!updateAvailable) return;
    if (ubTitleEl) ubTitleEl.textContent = 'There is a new update! (v' + updateAvailable + ')';
    updateEl.classList.add('show');
    updateEl.setAttribute('aria-hidden', 'false');
  }
  function hideUpdateBanner() {
    if (!updateEl) return;
    updateEl.classList.remove('show');
    updateEl.setAttribute('aria-hidden', 'true');
  }
  function dismissUpdateBanner() {
    Updater.snooze();
    hideUpdateBanner();
  }

  function renderStatus() {
    let tone = 'ok', label = 'Protected', sub = 'Blocking ads, popups and redirects';
    if (!config.enabled) { tone = 'off'; label = 'Disabled (global)'; sub = 'Turn on Protection in Settings to resume'; }
    else if (state.sessionDisabled[hostname]) { tone = 'warn'; label = 'Disabled for this session'; sub = 'Blocking is paused on this site'; }
    else if (isWhitelisted(hostname)) { tone = 'warn'; label = 'Whitelisted'; sub = 'This site is allowed to show ads'; }

    if (handleEl) handleEl.setAttribute('data-state', tone);
    if (!statusEl || !bodyEl) return;

    clear(statusEl);
    statusEl.appendChild(el('span', { class: 'led' + (tone === 'ok' ? '' : ' ' + tone) }));
    statusEl.appendChild(el('div', null,
      el('div', { class: 't' }, label),
      el('div', { class: 's' }, sub)
    ));

    const setItem = (role, label2, sub2) => {
      const b = bodyEl.querySelector('[data-role="' + role + '"]');
      if (!b) return;
      const l = b.querySelector('.lbl'), s = b.querySelector('.sub');
      if (l) l.textContent = label2;
      if (s) s.textContent = sub2;
    };
    const wl = isWhitelisted(hostname);
    setItem('wl', wl ? 'Remove from whitelist' : 'Do not block this site',
      wl ? 'Resume blocking on this site' : 'Always allow this site to run unfiltered');
    const sd = !!state.sessionDisabled[hostname];
    setItem('td', sd ? 'Enable for this session' : 'Disable for this session',
      sd ? 'Turn blocking back on' : 'Pause blocking on this site');
  }

  function renderStats() {
    if (!statsEl) return;
    const currentData = statsMode === 'session' ? stats.session : stats.lifetime;
    const secondaryData = statsMode === 'session' ? stats.lifetime : stats.session;
    const secLabel = statsMode === 'session' ? 'All-time: ' : 'This page: ';

    const rows = [
      ['Ads removed', currentData.ads, secondaryData.ads],
      ['Elements hidden', currentData.hidden, secondaryData.hidden],
      ['Popups blocked', currentData.popups, secondaryData.popups],
      ['Redirects blocked', currentData.redirects, secondaryData.redirects],
      ['Requests blocked', currentData.requests, secondaryData.requests],
    ];

    if (statsEl.childNodes.length !== rows.length) {
      clear(statsEl);
      for (const r of rows) {
        const statCard = el('div', { class: 'stat' },
          el('b', null, fmt(r[1])),
          el('span', null, r[0]),
          el('small', { class: 'stat-sub' }, secLabel + fmt(r[2]))
        );
        statsEl.appendChild(statCard);
      }
    } else {
      rows.forEach((r, i) => {
        const card = statsEl.childNodes[i];
        const b = card.childNodes[0];
        const span = card.childNodes[1];
        const sub = card.childNodes[2];
        const valStr = fmt(r[1]);
        const subStr = secLabel + fmt(r[2]);
        if (b.textContent !== valStr) b.textContent = valStr;
        if (span.textContent !== r[0]) span.textContent = r[0];
        if (sub && sub.textContent !== subStr) sub.textContent = subStr;
      });
    }
  }

  function setMenu(open) {
    if (!sheetEl) return;
    menuOpen = !!open;
    sheetEl.classList.toggle('open', menuOpen);
    backdropEl.classList.toggle('open', menuOpen);
    if (handleEl) {
      handleEl.classList.toggle('away', menuOpen);
      handleEl.setAttribute('aria-expanded', String(menuOpen));
    }
    sheetEl.setAttribute('aria-hidden', String(!menuOpen));
    if (statsTimer) { clearInterval(statsTimer); statsTimer = null; }
    if (menuOpen) {
      renderStatus(); renderStats(); updateThemeUI();
      statsTimer = setInterval(renderStats, 1000);
      try { sheetEl.focus({ preventScroll: true }); } catch (e) {}
    }
  }

  function wireHandle() {
    let drag = null;
    handleEl.addEventListener('pointerdown', (e) => {
      if (e.pointerType === 'mouse' && e.button !== 0) return;
      const r = handleEl.getBoundingClientRect();
      drag = { sx: e.clientX, sy: e.clientY, ox: r.left, oy: r.top, moved: false };
      try { handleEl.setPointerCapture(e.pointerId); } catch (er) {}
    });
    handleEl.addEventListener('pointermove', (e) => {
      if (!drag) return;
      const dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;
      if (!drag.moved) {
        if (Math.hypot(dx, dy) < 6) return;
        drag.moved = true;
        handleEl.classList.add('dragging');
      }
      const vw = D.documentElement.clientWidth || W.innerWidth;
      const vh = W.innerHeight;
      handleEl.style.left = Math.max(4, Math.min(vw - FAB_SIZE - 4, drag.ox + dx)) + 'px';
      handleEl.style.top = Math.max(4, Math.min(vh - FAB_SIZE - 4, drag.oy + dy)) + 'px';
    });
    const endDrag = (e, cancelled) => {
      if (!drag) return;
      const d = drag; drag = null;
      handleEl.classList.remove('dragging');
      try { handleEl.releasePointerCapture(e.pointerId); } catch (er) {}
      if (d.moved) {
        const r = handleEl.getBoundingClientRect();
        const vw = D.documentElement.clientWidth || W.innerWidth;
        const vh = W.innerHeight || 1;
        fab = { side: (r.left + r.width / 2) < vw / 2 ? 'l' : 'r', y: Math.max(0, Math.min(1, r.top / vh)) };
        store.set(K_FAB, fab);
        applyFab();
      } else if (!cancelled) {
        setMenu(!menuOpen);
      }
    };
    handleEl.addEventListener('pointerup', (e) => endDrag(e, false));
    handleEl.addEventListener('pointercancel', (e) => endDrag(e, true));
    handleEl.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setMenu(!menuOpen); }
    });

    D.addEventListener('keydown', (e) => {
      if (e.key === 'Escape') {
        if (pickerActive) exitPicker(true);
        else if (reportModalEl && reportModalEl.classList.contains('open')) closeReportModal();
        else if (menuOpen) setMenu(false);
      }
    }, true);
  }

  function isSheetMode() {
    try { return !W.matchMedia('(min-width: 641px)').matches; } catch (e) { return (W.innerWidth || 0) <= 640; }
  }

  function wireSheet() {
    let tracking = false, dragging = false, fromGrab = false;
    let x0 = 0, y0 = 0, t0 = 0, dy = 0;

    const reset = () => {
      sheetEl.style.transition = '';
      sheetEl.style.transform = '';
      backdropEl.style.transition = '';
      backdropEl.style.opacity = '';
    };

    sheetEl.addEventListener('touchstart', (e) => {
      if (!menuOpen || !isSheetMode() || !e.touches || e.touches.length !== 1) { tracking = false; return; }
      tracking = true; dragging = false; dy = 0;
      x0 = e.touches[0].clientX; y0 = e.touches[0].clientY; t0 = Date.now();
      fromGrab = !!(grabEl && grabEl.contains(e.target));
    }, { passive: true });

    sheetEl.addEventListener('touchmove', (e) => {
      if (!tracking || !e.touches || !e.touches.length) return;
      const x = e.touches[0].clientX, y = e.touches[0].clientY;
      const rawY = y - y0, rawX = x - x0;
      if (!dragging) {
        if (Math.abs(rawX) > Math.abs(rawY) && Math.abs(rawX) > 8) { tracking = false; return; }
        if (rawY < -6) { tracking = false; return; }
        if (rawY <= 6) return;
        if (!fromGrab && bodyEl.scrollTop > 0) { tracking = false; return; }
        dragging = true;
        sheetEl.style.transition = 'none';
        backdropEl.style.transition = 'none';
      }
      dy = Math.max(0, rawY);
      sheetEl.style.transform = 'translateY(' + dy + 'px)';
      const h = sheetEl.offsetHeight || 1;
      backdropEl.style.opacity = String(Math.max(0, 1 - dy / h));
      if (e.cancelable) e.preventDefault();
    }, { passive: false });

    const finish = () => {
      if (!tracking) return;
      tracking = false;
      if (!dragging) return;
      dragging = false;
      const h = sheetEl.offsetHeight || 1;
      const velocity = dy / Math.max(1, Date.now() - t0);
      const close = dy > h * 0.25 || (velocity > 0.5 && dy > 30);
      reset();
      if (close) setMenu(false);
    };
    sheetEl.addEventListener('touchend', finish, { passive: true });
    sheetEl.addEventListener('touchcancel', finish, { passive: true });
  }

  function toast(msg) {
    if (!toastEl) return;
    if (msg === lastToast && toastTimer) return;
    lastToast = msg;
    toastEl.textContent = msg;
    toastEl.classList.toggle('top', menuOpen);
    toastEl.style.top = (menuOpen && updateEl && updateEl.classList.contains('show'))
      ? 'calc(' + (updateEl.offsetHeight + 22) + 'px + env(safe-area-inset-top, 0px))' : '';
    toastEl.classList.add('show');
    if (toastTimer) clearTimeout(toastTimer);
    toastTimer = setTimeout(() => { toastEl.classList.remove('show'); toastTimer = null; lastToast = ''; }, 2200);
  }

  function updateUIStatus() { renderStatus(); renderStats(); }

  function buildReportModal() {
    if (reportModalEl) return;

    const closeBtn = el('button', {
      class: 'icon-btn',
      type: 'button',
      'aria-label': 'Close',
      onclick: () => closeReportModal()
    }, icon('close'));

    const head = el('div', { class: 'report-head' },
      el('div', { class: 'titles' },
        el('h2', null, 'Report an incident'),
        el('div', { class: 'badge', style: { marginTop: '2px' } }, hostname || '(unknown)')
      ),
      closeBtn
    );

    reportReasonSelect = el('select', { class: 'report-select' },
      el('option', { value: 'Lọt quảng cáo (Missed ad)' }, 'Lọt quảng cáo (Missed ad)'),
      el('option', { value: 'Lỗi giao diện (Broken layout)' }, 'Lỗi giao diện (Broken layout)'),
      el('option', { value: 'Lỗi phát video (Player issue)' }, 'Lỗi phát video (Player issue)'),
      el('option', { value: 'Khác (Other)' }, 'Khác (Other)')
    );

    reportNoteInput = el('textarea', {
      class: 'report-textarea',
      placeholder: 'Note more about the location of advertising...',
      rows: '3'
    });

    const cancelBtn = el('button', {
      class: 'report-btn cancel',
      type: 'button',
      onclick: () => closeReportModal()
    }, 'Hủy');

    const submitBtn = el('button', {
      class: 'report-btn submit',
      type: 'button',
      onclick: () => submitReport()
    }, 'Send a report');

    const actions = el('div', { class: 'report-actions' }, cancelBtn, submitBtn);

    const card = el('div', { class: 'report-card' },
      head,
      el('div', { class: 'report-lbl' }, 'Reason:'),
      reportReasonSelect,
      el('div', { class: 'report-lbl' }, 'Notes (optional):'),
      reportNoteInput,
      actions
    );

    reportModalEl = el('div', { class: 'report-modal' }, card);
    root.appendChild(reportModalEl);
  }

  function openReportModal() {
    if (!reportModalEl) buildReportModal();
    if (reportNoteInput) reportNoteInput.value = '';
    reportModalEl.classList.add('open');
  }

  function closeReportModal() {
    if (reportModalEl) reportModalEl.classList.remove('open');
  }

  function submitReport() {
    const lastMap = store.get(K_LAST_REP, {}) || {};
    const lastTime = lastMap[hostname] || 0;
    const elapsed = Date.now() - lastTime;
    if (elapsed < 30000) {
      const waitSec = Math.ceil((30000 - elapsed) / 1000);
      toast(`Please wait ${waitSec}s before sending again`);
      return;
    }

    const webhookUrl = getWebhookTarget();
    if (!webhookUrl) {
      toast('Have not configured a valid webhook in the script');
      closeReportModal();
      return;
    }

    const reason = (reportReasonSelect && reportReasonSelect.value) || 'Missed ad';
    const note = (reportNoteInput && reportNoteInput.value.trim()) || '';
    const cleanUrl = getSanitizedUrl();
    const recentLogs = logRing.slice(-5).map(e => `[${e.level}] ${e.type}: ${e.msg} ${e.extra || ''}`).join('\n') || 'There is no log';

    const payload = {
      username: 'Nvirya AdGuard Reporter',
      avatar_url: 'https://raw.nvirya.com/assets/icon.png',
      embeds: [
        {
          title: 'Báo cáo sự cố: ' + reason,
          color: reason.includes('Lọt') ? 15158332 : 16107018,
          fields: [
            { name: 'Domain', value: hostname || 'unknown', inline: true },
            { name: 'Phiên bản', value: 'v' + VERSION, inline: true },
            { name: 'Lý do', value: reason, inline: true },
            { name: 'URL (Sanitized)', value: cleanUrl },
            { name: 'Ghi chú', value: note || '(Không có)' },
            {
              name: 'Thống kê chặn',
              value: `• **Phiên hiện tại (Session):** Chặn ${stats.session.ads} ads, Ẩn ${stats.session.hidden} el, Chặn ${stats.session.popups} popups\n• **Tổng tích lũy (Lifetime):** Chặn ${stats.lifetime.ads} ads, Ẩn ${stats.lifetime.hidden} el`
            },
            { name: 'Logs gần nhất', value: '```text\n' + recentLogs + '\n```' },
            { name: 'User Agent', value: '```\n' + (navigator.userAgent || 'unknown').slice(0, 220) + '\n```' }
          ],
          footer: { text: 'Nvirya AdGuard Security Report' },
          timestamp: new Date().toISOString()
        }
      ]
    };

    const onSuccess = () => {
      lastMap[hostname] = Date.now();
      store.set(K_LAST_REP, lastMap);
      toast('The report has been sent successfully! Thank you.');
      closeReportModal();
    };

    const onError = (msg) => {
      toast('Send failed: ' + (msg || 'Connection error'));
    };

    const bodyStr = JSON.stringify(payload);
    if (typeof GM_xmlhttpRequest === 'function') {
      GM_xmlhttpRequest({
        method: 'POST',
        url: webhookUrl,
        headers: { 'Content-Type': 'application/json' },
        data: bodyStr,
        onload: (res) => {
          if (res.status >= 200 && res.status < 300) onSuccess();
          else onError('HTTP ' + res.status);
        },
        onerror: (err) => onError((err && err.statusText) || 'CORS / Network error')
      });
    } else {
      fetch(webhookUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: bodyStr
      }).then(res => {
        if (res.ok) onSuccess();
        else onError('HTTP ' + res.status);
      }).catch(err => onError(err && err.message));
    }
  }

  function exportSiteRules() {
    const rules = state.siteRules || {};
    let totalRules = 0;
    for (const h in rules) if (Array.isArray(rules[h])) totalRules += rules[h].length;
    if (totalRules === 0) {
      toast('There are no optional rules to export yet');
      return;
    }
    const jsonStr = JSON.stringify(rules);
    let payload = jsonStr;
    try {
      payload = btoa(unescape(encodeURIComponent(jsonStr)));
    } catch (e) {
      payload = jsonStr;
    }

    const fallback = () => prompt('Copy rule code (Base64/JSON):', payload);
    try {
      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(payload).then(() => {
          toast(`Đã sao chép ${totalRules} Rules on Clipboard!`);
        }).catch(fallback);
      } else {
        fallback();
      }
    } catch (e) { fallback(); }
  }

  function importSiteRules() {
    const input = prompt('Paste the rule chain (JSON or Base64):');
    if (!input || !input.trim()) return;
    const raw = input.trim();
    let parsed = null;

    try {
      const decoded = decodeURIComponent(escape(atob(raw)));
      parsed = JSON.parse(decoded);
    } catch (e) {
      try { parsed = JSON.parse(raw); } catch (e2) {}
    }

    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
      toast('Invalid rule format');
      return;
    }

    let mergedCount = 0;
    if (!state.siteRules) state.siteRules = {};

    for (const host of Object.keys(parsed)) {
      const hostRules = parsed[host];
      if (!Array.isArray(hostRules)) continue;
      const normHost = host.toLowerCase().trim();
      if (!state.siteRules[normHost]) state.siteRules[normHost] = [];

      const existingSelectors = new Set(state.siteRules[normHost].map(r => r.selector));
      for (const rule of hostRules) {
        if (rule && rule.selector && !existingSelectors.has(rule.selector)) {
          state.siteRules[normHost].push({
            selector: rule.selector,
            action: rule.action || 'hide',
            created: rule.created || Date.now()
          });
          existingSelectors.add(rule.selector);
          mergedCount++;
        }
      }
    }

    persistSiteRules();
    installSiteRulesCSS();
    initialScan();
    toast(`Successfully merged ${mergedCount} new rule!`);
  }

  function buildPicker() {
    if (pickerEl) return;
    pickerEl = document.createElement('div');
    pickerEl.setAttribute('data-nvirya-picker','');
    pickerEl.style.cssText = 'position:fixed;z-index:2147483003;pointer-events:none;border:2px solid #3b82f6;background:rgba(59,130,246,.1);border-radius:3px;transition:all .05s linear;box-sizing:border-box;';
    pickerLabelEl = document.createElement('div');
    pickerLabelEl.style.cssText = 'position:fixed;z-index:2147483004;pointer-events:none;background:#0b0b0d;color:#f4f4f5;font:11px system-ui,-apple-system,sans-serif;padding:3px 7px;border-radius:4px;border:1px solid rgba(255,255,255,.1);white-space:nowrap;';
    (D.body || D.documentElement).appendChild(pickerEl);
    (D.body || D.documentElement).appendChild(pickerLabelEl);
  }

  function enterPicker() {
    if (!config.elementPicker) return;
    buildPicker();
    pickerActive = true;
    pickerEl.style.display = 'block';
    pickerLabelEl.style.display = 'block';
    D.addEventListener('mousemove', pickerMove, true);
    D.addEventListener('click', pickerClick, true);
    D.addEventListener('touchmove', pickerTouchMove, true);
    D.addEventListener('touchend', pickerTouchEnd, true);
    toast('Tap an element to block');
  }

  function exitPicker(silent) {
    pickerActive = false;
    if (pickerEl) pickerEl.style.display = 'none';
    if (pickerLabelEl) pickerLabelEl.style.display = 'none';
    D.removeEventListener('mousemove', pickerMove, true);
    D.removeEventListener('click', pickerClick, true);
    D.removeEventListener('touchmove', pickerTouchMove, true);
    D.removeEventListener('touchend', pickerTouchEnd, true);
    if (!silent) toast('Picker closed');
  }

  function describe(el) {
    if (!el) return '';
    const tag = (el.tagName || '').toLowerCase();
    const id = el.id ? '#' + el.id : '';
    const cls = classString(el);
    const firstCls = cls ? '.' + cls.split(/\s+/).filter(Boolean).slice(0, 2).join('.') : '';
    return tag + id + firstCls;
  }

  function pickerMove(e) {
    if (!pickerActive) return;
    if (e.composedPath && e.composedPath().indexOf(host) !== -1) return;
    const el = D.elementFromPoint(e.clientX, e.clientY);
    if (!el || el === pickerEl || el === pickerLabelEl) return;
    if (el === pickerHover) return;
    pickerHover = el;
    const r = el.getBoundingClientRect();
    pickerEl.style.left = r.left + 'px';
    pickerEl.style.top = r.top + 'px';
    pickerEl.style.width = r.width + 'px';
    pickerEl.style.height = r.height + 'px';
    pickerLabelEl.textContent = describe(el);
    pickerLabelEl.style.left = Math.max(4, r.left) + 'px';
    pickerLabelEl.style.top = Math.max(4, r.top - 22) + 'px';
  }

  function pickerTouchMove(e) {
    if (!e.touches || e.touches.length !== 1) return;
    pickerMove({ clientX: e.touches[0].clientX, clientY: e.touches[0].clientY });
  }

  function pickerTouchEnd(e) {
    if (!e.changedTouches || !e.changedTouches.length) return;
    const t = e.changedTouches[0];
    const el = D.elementFromPoint(t.clientX, t.clientY);
    if (el) selectElement(el);
  }

  function pickerClick(e) {
    if (!pickerActive) return;
    if (e.composedPath && e.composedPath().indexOf(host) !== -1) return;
    e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
    const el = D.elementFromPoint(e.clientX, e.clientY);
    if (el) selectElement(el);
  }

  function makeSelector(el) {
    if (!el || el.nodeType !== 1) return null;
    if (el.id && /^[a-zA-Z][\w-]*$/.test(el.id)) return '#' + el.id;
    for (const a of ['data-testid','data-qa','data-test','data-cy','data-id']) {
      const v = el.getAttribute(a);
      if (v) return `${el.tagName.toLowerCase()}[${a}="${CSS.escape(v)}"]`;
    }
    const cls = classString(el).split(/\s+/).filter(c => c && c.length < 40);
    if (cls.length) {
      const combo = cls.slice(0, 2).map(c => '.' + CSS.escape(c)).join('');
      try {
        const matches = D.querySelectorAll(el.tagName.toLowerCase() + combo);
        if (matches.length === 1) return el.tagName.toLowerCase() + combo;
      } catch (e) {}
      return el.tagName.toLowerCase() + combo;
    }
    const parent = el.parentElement;
    if (!parent) return el.tagName.toLowerCase();
    const idx = Array.prototype.indexOf.call(parent.children, el) + 1;
    return `${makeSelector(parent) || parent.tagName.toLowerCase()} > ${el.tagName.toLowerCase()}:nth-child(${idx})`;
  }

  function selectElement(el) {
    if (!el || el.nodeType !== 1) return;
    if (isProtectedPlayer(el) && !confirm('This element belongs to the media player. Blocking can damage video playback. Continue?')) {
      exitPicker(true); return;
    }
    const sel = makeSelector(el);
    if (!sel) { toast('Không thể tạo selector'); exitPicker(true); return; }
    if (!confirm('Chặn phần tử này trên trang?\n\n' + sel)) { exitPicker(true); return; }
    if (!state.siteRules[hostname]) state.siteRules[hostname] = [];
    state.siteRules[hostname].push({ selector: sel, action: 'hide', created: Date.now() });
    persistSiteRules();
    try {
      el.setAttribute('data-nvirya-hidden','manual');
      state.hiddenNodes.add(el);
      stats.inc('hidden');
    } catch (e) {}
    installSiteRulesCSS();
    toast('Đã chặn phần tử');
    exitPicker(true);
  }

  function collectDebugInfo() {
    const lines = [];
    lines.push('Nvirya AdGuard X v' + VERSION);
    lines.push('Host: ' + hostname);
    lines.push('URL: ' + location.href.split('?')[0]);
    lines.push('Enabled: ' + config.enabled);
    lines.push('Whitelisted: ' + isWhitelisted(hostname));
    lines.push('Session-disabled: ' + !!state.sessionDisabled[hostname]);
    lines.push('Strict mode: ' + config.strictMode);
    lines.push('Observer: ' + (observer ? 'active' : 'inactive'));
    lines.push('Queue length: ' + pendingQueue.length);
    lines.push('Stats (Session): ' + JSON.stringify(stats.session));
    lines.push('Stats (Lifetime): ' + JSON.stringify(stats.lifetime));
    lines.push('Recent events:');
    const tail = logRing.slice(-15);
    for (const e of tail) lines.push('  ' + e.level + ' ' + e.type + ' ' + e.msg + (e.extra ? ' ' + e.extra : ''));
    return lines.join('\n');
  }

  return {
    build,
    setMenu,
    setTheme,
    toast,
    enterPicker,
    exitPicker,
    updateUIStatus,
    renderStats,
    showUpdateBanner,
    hideUpdateBanner,
    setUpdateAvailable,
    setUpdateChecking,
    isMenuOpen: () => menuOpen,
    isPickerActive: () => pickerActive
  };
})();

function cleanPage() {
  try {
    const sel = '[id^="google_ads_"], ins.adsbygoogle, iframe[src*="googlesyndication"], iframe[src*="doubleclick"]';
    const list = D.querySelectorAll(sel);
    const lim = Math.min(list.length, 200);
    for (let i = 0; i < lim; i++) {
      const el = list[i];
      if (!isProtectedPlayer(el)) {
        el.setAttribute('data-nvirya-hidden','clean');
        state.hiddenNodes.add(el);
        stats.inc('hidden');
      }
    }
    handleAntiAdblock();
  } catch (e) { log('ERROR','clean', e && e.message); }
}

function undoLast() {
  const last = state.removedNodes.pop();
  if (!last || !last.parent) { UI.toast('Nothing to undo'); return; }
  try {
    if (last.next && last.next.parentNode === last.parent) last.parent.insertBefore(last.el, last.next);
    else last.parent.appendChild(last.el);
    UI.toast('Restored');
  } catch (e) { UI.toast('Could not restore'); }
}

function refreshProtection() {
  if (isDisabledHere()) {
    if (observer) { try { observer.disconnect(); } catch (e) {} observer = null; }
    if (cosmeticStyleEl) cosmeticStyleEl.disabled = true;
  } else {
    if (cosmeticStyleEl) cosmeticStyleEl.disabled = false;
    if (!observer) startObserver();
    initialScan();
  }
  UI.updateUIStatus();
}

function registerMenuCommands() {
  if (typeof GM_registerMenuCommand !== 'function') return;
  try {
    GM_registerMenuCommand('Open Nvirya AdGuard X', () => UI.setMenu(true));
    GM_registerMenuCommand('Disable on this site (session)', () => {
      state.sessionDisabled[hostname] = true; persistSession(); refreshProtection(); UI.toast('Disabled for session');
    });
    GM_registerMenuCommand('Enable on this site', () => {
      delete state.sessionDisabled[hostname]; persistSession(); refreshProtection(); UI.toast('Enabled');
    });
    GM_registerMenuCommand('Block element on this page', () => UI.enterPicker());
    GM_registerMenuCommand('Check for updates', () => Updater.check(true));
    GM_registerMenuCommand('Toggle debug', () => {
      config.debug = !config.debug; saveConfig(); UI.toast('Debug ' + (config.debug ? 'on' : 'off'));
    });
  } catch (e) {}
}

function init() {
  if (!D.documentElement) {
    if (D.readyState === 'loading') {
      D.addEventListener('DOMContentLoaded', init, { once: true });
    } else {
      setTimeout(init, 16);
    }
    return;
  }

  if (state.initialized) return;
  state.initialized = true;

  installPopupGuard();
  installRedirectGuard();
  installNetworkGuard();

  const isTop = (function () {
    try { return W.self === W.top; } catch (e) { return false; }
  })();

  const start = () => {
    installCosmeticCSS();
    installSiteRulesCSS();
    startObserver();
    initialScan();

    setTimeout(handleAntiAdblock, 1500);
    setTimeout(handleAntiAdblock, 4000);

    installSPAHooks();

    if (isTop) {
      try { UI.build(); } catch (e) { log('ERROR', 'ui', e && e.message); }
      
      if (!config.showFloatingButton) {
        const h = D.querySelector('[data-nvirya-ui]');
        if (h && h.shadowRoot) {
          const handle = h.shadowRoot.querySelector('.handle');
          if (handle) handle.classList.add('hidden');
        }
      }

      registerMenuCommands();

      try { Updater.schedule(); } catch (e) { log('ERROR', 'update', e && e.message); }

      try {
        D.addEventListener('fullscreenchange', () => {
          const fsEl = D.fullscreenElement;
          const h = D.querySelector('[data-nvirya-ui]');
          if (!h || !h.shadowRoot) return;
          const handle = h.shadowRoot.querySelector('.handle');
          if (!handle) return;
          handle.style.display = fsEl ? 'none' : '';
        });
      } catch (e) {}
    }

    log('INFO', 'init', 'ready v' + VERSION);
  };

  if (D.body) {
    start();
  } else {
    let started = false;
    const triggerStart = () => {
      if (started) return;
      started = true;
      if (bodyObserver) {
        try { bodyObserver.disconnect(); } catch (e) {}
      }
      start();
    };

    let bodyObserver = null;
    try {
      bodyObserver = new MutationObserver(() => {
        if (D.body) triggerStart();
      });
      bodyObserver.observe(D.documentElement, { childList: true });
    } catch (e) {}

    D.addEventListener('DOMContentLoaded', triggerStart, { once: true });
    setTimeout(triggerStart, 1500);
  }
}

try {
  W.addEventListener('pageshow', (event) => {
    if (event.persisted) {
      if (!observer) startObserver();
      installCosmeticCSS();
      installSiteRulesCSS();
    }
  });
} catch (e) {}

try {
  init();
} catch (e) {
  try { console.error('[Nvirya AdGuard X] init failed', e); } catch (e2) {}
}
})();