🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

Greasy Fork is available in English.

📂 缓存分发状态(共享加速已生效)
🕒 页面同步时间:2025/12/25 02:49:55
🔄 下次更新时间:2025/12/25 03:49:55
手动刷新缓存

Neutralize Headlines

Tone down sensationalist titles and simplify article body text via OpenAI API. Auto-detect + manual selectors, exclusions, per-domain configs, domain allow/deny, caching, Android-safe storage.

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

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

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

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

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

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

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

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

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

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

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

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

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

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

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

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         Neutralize Headlines
// @namespace    https://fanis.dev/userscripts
// @author       Fanis Hatzidakis
// @license      PolyForm-Internal-Use-1.0.0; https://polyformproject.org/licenses/internal-use/1.0.0/
// @version      1.7.0
// @description  Tone down sensationalist titles and simplify article body text via OpenAI API. Auto-detect + manual selectors, exclusions, per-domain configs, domain allow/deny, caching, Android-safe storage.
// @match        *://*/*
// @exclude      about:*
// @exclude      moz-extension:*
// @run-at       document-end
// @grant        GM.xmlHttpRequest
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM_deleteValue
// @grant        GM.deleteValue
// @grant        GM_registerMenuCommand
// @connect      api.openai.com
// ==/UserScript==

// SPDX-License-Identifier: PolyForm-Internal-Use-1.0.0
// Copyright (c) 2025 Fanis Hatzidakis
// License: PolyForm Internal Use License 1.0.0
// Summary: Free for personal and internal business use. No redistribution, resale,
// or offering as a service without a separate commercial license from the author.
// Full text: https://polyformproject.org/licenses/internal-use/1.0.0/

(async () => {
  'use strict';

  // ───────────────────────── CONFIG ─────────────────────────
  const CFG = {
    model: 'gpt-4o-mini',
    temperature: 0.2,
    maxBatch: 24,
    DEBUG: false,

    highlight: true,
    highlightMs: 900,
    highlightColor: '#fff4a3',

    visibleOnly: true,
    rootMargin: '1000px 0px',
    threshold: 0,
    flushDelayMs: 180,

    autoDetect: true,
    minLen: 8,
    maxLen: 180,
    sanityCheckLen: 500, // Warn user if manual selector matches text > 500 chars

    // smarter headline scoring
    minWords: 3,
    maxWords: 35,
    scoreThreshold: 75,
    topKPerCard: 1,
    kickerFilterStrict: true,

    DEBUG_SCORES: false,
    showOriginalOnHover: true,

    // cache controls
    cacheLimit: 1500,       // max entries
    cacheTrimTo: 1100,      // when trimming, keep last N
  };

  const UI_ATTR = 'data-neutralizer-ui';
  const HOST = location.hostname;

  const STATS = { total: 0, live: 0, cache: 0, batches: 0 };
  const LOG_PREFIX = '[neutralizer-ai]';
  function log(...args) { if (!CFG.DEBUG) return; console.log(LOG_PREFIX, ...args); }

  // Prevent multiple API key dialogs from showing
  let apiKeyDialogShown = false;

  // API token usage tracking (persistent, not cleared with stats)
  let API_TOKENS = {
    headlines: { input: 0, output: 0, calls: 0 },
    body: { input: 0, output: 0, calls: 0 }
  };

  // API Pricing configuration (user-editable)
  // Default: gpt-4o-mini pricing as of January 2025
  // Source: https://openai.com/api/pricing/
  let PRICING = {
    model: 'gpt-4o-mini',
    inputPer1M: 0.15,    // USD per 1M input tokens
    outputPer1M: 0.60,   // USD per 1M output tokens
    lastUpdated: '2025-01-25',
    source: 'https://openai.com/api/pricing/'
  };

  // ───────────────────────── STORAGE (GM → LS → memory) ─────────────────────────
  const MEM = new Map();
  const LS_KEY_NS = '__neutralizer__';

  const storage = {
    async get(key, def = '') {
      try { if (typeof GM?.getValue === 'function') { const v = await GM.getValue(key); if (v != null) return v; } } catch {}
      try { if (typeof GM_getValue === 'function') { const v = GM_getValue(key); if (v != null) return v; } } catch {}
      try { const bag = JSON.parse(localStorage.getItem(LS_KEY_NS) || '{}'); if (key in bag) return bag[key]; } catch {}
      if (MEM.has(key)) return MEM.get(key);
      return def;
    },
    async set(key, val) {
      let ok = false;
      try { if (typeof GM?.setValue === 'function') { await GM.setValue(key, val); ok = true; } } catch {}
      if (!ok) { try { if (typeof GM_setValue === 'function') { GM_setValue(key, val); ok = true; } } catch {} }
      if (!ok) { try { const bag = JSON.parse(localStorage.getItem(LS_KEY_NS) || '{}'); bag[key] = val; localStorage.setItem(LS_KEY_NS, JSON.stringify(bag)); ok = true; } catch {} }
      if (!ok) MEM.set(key, val);
      return ok;
    },
    async del(key) {
      let ok = false;
      try { if (typeof GM?.deleteValue === 'function') { await GM.deleteValue(key); ok = true; } } catch {}
      try { if (typeof GM_deleteValue === 'function') { GM_deleteValue(key); ok = true; } } catch {}
      try { const bag = JSON.parse(localStorage.getItem(LS_KEY_NS) || '{}'); if (key in bag) { delete bag[key]; localStorage.setItem(LS_KEY_NS, JSON.stringify(bag)); ok = true; } } catch {}
      MEM.delete(key);
      return ok;
    },
  };

  // ───────────────────────── PERSISTED LISTS ─────────────────────────
  const SELECTORS_KEY = 'neutralizer_selectors_v1'; // global defaults
  const DEFAULT_SELECTORS = ['h1','h2','h3','.lead','[itemprop="headline"]','[role="heading"]','.title','.title a','.summary','.hn__title-container h2 a','.article-title'];
  let SELECTORS_GLOBAL = DEFAULT_SELECTORS.slice();
  let SELECTORS_DOMAIN = []; // domain-specific additions
  let SELECTORS = []; // merged result

  const EXCLUDES_KEY = 'neutralizer_excludes_v1'; // global defaults
  const DEFAULT_EXCLUDES = { self: [], ancestors: ['footer','nav','aside','[role="navigation"]','.breadcrumbs','[aria-label*="breadcrumb" i]'] };
  let EXCLUDE_GLOBAL = { ...DEFAULT_EXCLUDES, ancestors: [...DEFAULT_EXCLUDES.ancestors] };
  let EXCLUDE_DOMAIN = { self: [], ancestors: [] }; // domain-specific additions
  let EXCLUDE = { self: [], ancestors: [] }; // merged result

  // Per-domain configuration (additive)
  const DOMAIN_SELECTORS_KEY = 'neutralizer_domain_selectors_v2'; // { 'hostname': [...] }
  const DOMAIN_EXCLUDES_KEY = 'neutralizer_domain_excludes_v2'; // { 'hostname': { self: [...], ancestors: [...] } }
  const LONG_HEADLINE_EXCEPTIONS_KEY = 'neutralizer_long_exceptions_v1'; // { 'hostname': true } - domains allowed to process 500+ char headlines
  let DOMAIN_SELECTORS = {};
  let DOMAIN_EXCLUDES = {};
  let LONG_HEADLINE_EXCEPTIONS = {};

  const DOMAINS_MODE_KEY    = 'neutralizer_domains_mode_v1';     // 'deny' | 'allow'
  const DOMAINS_DENY_KEY    = 'neutralizer_domains_excluded_v1'; // array of patterns
  const DOMAINS_ALLOW_KEY   = 'neutralizer_domains_enabled_v1';  // array of patterns
  const DEBUG_KEY           = 'neutralizer_debug_v1';
  const AUTO_DETECT_KEY     = 'neutralizer_autodetect_v1';
  const SHOW_ORIG_KEY       = 'neutralizer_showorig_v1';
  const SHOW_BADGE_KEY      = 'neutralizer_showbadge_v1';
  const BADGE_COLLAPSED_KEY = 'neutralizer_badge_collapsed_v1';
  const BADGE_POS_KEY       = 'neutralizer_badge_pos_v1';
  const TEMPERATURE_KEY     = 'neutralizer_temperature_v1';
  const SIMPLIFY_BODY_KEY   = 'neutralizer_simplifybody_v1';
  const SIMPLIFICATION_STRENGTH_KEY = 'neutralizer_simplification_v1';
  const FIRST_INSTALL_KEY   = 'neutralizer_installed_v1';
  const API_TOKENS_KEY      = 'neutralizer_api_tokens_v1';
  const PRICING_KEY         = 'neutralizer_pricing_v1';

  // Temperature levels mapping
  const TEMPERATURE_LEVELS = {
    'Minimal': 0.0,
    'Light': 0.1,
    'Moderate': 0.2,
    'Strong': 0.35,
    'Maximum': 0.5
  };
  const TEMPERATURE_ORDER = ['Minimal', 'Light', 'Moderate', 'Strong', 'Maximum'];
  let TEMPERATURE_LEVEL = 'Moderate'; // default level name

  // Simplification strength levels mapping
  const SIMPLIFICATION_LEVELS = {
    'Minimal': 0.0,
    'Light': 0.1,
    'Moderate': 0.2,
    'Strong': 0.3,
    'Maximum': 0.4
  };
  const SIMPLIFICATION_ORDER = ['Minimal', 'Light', 'Moderate', 'Strong', 'Maximum'];
  let SIMPLIFICATION_LEVEL = 'Moderate'; // default level name
  let SIMPLIFICATION_TEMPERATURE = SIMPLIFICATION_LEVELS['Moderate'];

  // load toggles
  try { const v = await storage.get(DEBUG_KEY, ''); if (v !== '') CFG.DEBUG = (v === true || v === 'true'); } catch {}
  try { const v = await storage.get(AUTO_DETECT_KEY, ''); if (v !== '') CFG.autoDetect = (v === true || v === 'true'); } catch {}
  try { const v = await storage.get(SHOW_ORIG_KEY, ''); if (v !== '') CFG.showOriginalOnHover = (v === true || v === 'true'); } catch {}

  let SHOW_BADGE = true; // default
  try { const v = await storage.get(SHOW_BADGE_KEY, ''); if (v !== '') SHOW_BADGE = (v === true || v === 'true'); } catch {}

  let BADGE_COLLAPSED = false; // default expanded
  try { const v = await storage.get(BADGE_COLLAPSED_KEY, ''); if (v !== '') BADGE_COLLAPSED = (v === true || v === 'true'); } catch {}

  let BADGE_POS = { x: window.innerWidth - 220, y: window.innerHeight - 200 }; // default near bottom-right
  try { const v = await storage.get(BADGE_POS_KEY, ''); if (v) BADGE_POS = JSON.parse(v); } catch {}

  let SIMPLIFY_BODY = false; // default off
  try { const v = await storage.get(SIMPLIFY_BODY_KEY, ''); if (v !== '') SIMPLIFY_BODY = (v === true || v === 'true'); } catch {}

  // load temperature setting
  try {
    const v = await storage.get(TEMPERATURE_KEY, '');
    if (v !== '' && TEMPERATURE_LEVELS[v] !== undefined) {
      TEMPERATURE_LEVEL = v;
      CFG.temperature = TEMPERATURE_LEVELS[v];
    }
  } catch {}

  // load simplification strength setting
  try {
    const v = await storage.get(SIMPLIFICATION_STRENGTH_KEY, '');
    if (v !== '' && SIMPLIFICATION_LEVELS[v] !== undefined) {
      SIMPLIFICATION_LEVEL = v;
      SIMPLIFICATION_TEMPERATURE = SIMPLIFICATION_LEVELS[v];
    }
  } catch {}

  async function setDebug(on)         { CFG.DEBUG = !!on; await storage.set(DEBUG_KEY, String(CFG.DEBUG)); location.reload(); }
  async function setAutoDetect(on)    { CFG.autoDetect = !!on; await storage.set(AUTO_DETECT_KEY, String(CFG.autoDetect)); location.reload(); }
  async function setShowBadge(on)     { SHOW_BADGE = !!on; await storage.set(SHOW_BADGE_KEY, String(SHOW_BADGE)); location.reload(); }
  async function setSimplifyBody(on)  { SIMPLIFY_BODY = !!on; await storage.set(SIMPLIFY_BODY_KEY, String(SIMPLIFY_BODY)); location.reload(); }

  async function setTemperature(level) {
    if (TEMPERATURE_LEVELS[level] === undefined) return;
    TEMPERATURE_LEVEL = level;
    CFG.temperature = TEMPERATURE_LEVELS[level];
    await storage.set(TEMPERATURE_KEY, level);
    location.reload();
  }

  async function setSimplificationStrength(level) {
    if (SIMPLIFICATION_LEVELS[level] === undefined) return;
    SIMPLIFICATION_LEVEL = level;
    SIMPLIFICATION_TEMPERATURE = SIMPLIFICATION_LEVELS[level];
    await storage.set(SIMPLIFICATION_STRENGTH_KEY, level);
    location.reload();
  }

  // domain mode + lists
  let DOMAINS_MODE   = 'deny'; // default
  let DOMAIN_DENY    = [];
  let DOMAIN_ALLOW   = [];

  const CACHE_KEY = 'neutralizer_cache_v1';
  const BODY_CACHE_KEY = 'neutralizer_body_cache_v1';
  let CACHE = {};
  let BODY_CACHE = {};
  let cacheDirty = false;
  let bodyCacheDirty = false;

  // Body cache config - store fewer articles but more data per article
  const BODY_CACHE_LIMIT = 30;      // max articles
  const BODY_CACHE_TRIM_TO = 20;    // trim to this many articles

  // Load persisted data
  // Load global settings
  try { SELECTORS_GLOBAL = JSON.parse(await storage.get(SELECTORS_KEY, JSON.stringify(DEFAULT_SELECTORS))); } catch {}
  try { EXCLUDE_GLOBAL = JSON.parse(await storage.get(EXCLUDES_KEY, JSON.stringify(DEFAULT_EXCLUDES))); } catch {}

  // Load per-domain additions
  try { DOMAIN_SELECTORS = JSON.parse(await storage.get(DOMAIN_SELECTORS_KEY, '{}')); } catch {}
  try { DOMAIN_EXCLUDES = JSON.parse(await storage.get(DOMAIN_EXCLUDES_KEY, '{}')); } catch {}
  try { LONG_HEADLINE_EXCEPTIONS = JSON.parse(await storage.get(LONG_HEADLINE_EXCEPTIONS_KEY, '{}')); } catch {}

  // Load domain-specific settings for current host
  if (DOMAIN_SELECTORS[HOST]) {
    SELECTORS_DOMAIN = DOMAIN_SELECTORS[HOST];
  }
  if (DOMAIN_EXCLUDES[HOST]) {
    EXCLUDE_DOMAIN = DOMAIN_EXCLUDES[HOST];
  }

  // Merge global + domain-specific
  SELECTORS = [...new Set([...SELECTORS_GLOBAL, ...SELECTORS_DOMAIN])]; // deduplicate
  EXCLUDE = {
    self: [...new Set([...EXCLUDE_GLOBAL.self, ...EXCLUDE_DOMAIN.self])],
    ancestors: [...new Set([...EXCLUDE_GLOBAL.ancestors, ...EXCLUDE_DOMAIN.ancestors])]
  };

  if (SELECTORS_DOMAIN.length > 0 || EXCLUDE_DOMAIN.self.length > 0 || EXCLUDE_DOMAIN.ancestors.length > 0) {
    log('domain-specific additions for', HOST, ':', {
      selectors: SELECTORS_DOMAIN,
      excludes: EXCLUDE_DOMAIN
    });
  }

  try { DOMAINS_MODE = await storage.get(DOMAINS_MODE_KEY, 'deny'); } catch {}
  try { DOMAIN_DENY  = JSON.parse(await storage.get(DOMAINS_DENY_KEY, JSON.stringify(DOMAIN_DENY))); } catch {}
  try { DOMAIN_ALLOW = JSON.parse(await storage.get(DOMAINS_ALLOW_KEY, JSON.stringify(DOMAIN_ALLOW))); } catch {}
  try { CACHE = JSON.parse(await storage.get(CACHE_KEY, '{}')); } catch {}
  try { BODY_CACHE = JSON.parse(await storage.get(BODY_CACHE_KEY, '{}')); } catch {}
  try {
    const stored = await storage.get(API_TOKENS_KEY, '');
    if (stored) API_TOKENS = JSON.parse(stored);
  } catch {}
  try {
    const stored = await storage.get(PRICING_KEY, '');
    if (stored) PRICING = JSON.parse(stored);
  } catch {}

  const compiledSelectors = () => SELECTORS.join(',');

  // ───────────────────────── PUBLISHER OPT-OUT ─────────────────────────
  function publisherOptOut() {
    const m1 = document.querySelector('meta[name="neutralizer"][content="no-transform" i]');
    const m2 = document.querySelector('meta[http-equiv="X-Content-Transform"][content="none" i]');
    return !!(m1 || m2);
  }
  const OPTED_OUT = publisherOptOut();
  if (OPTED_OUT) log('publisher opt-out detected; disabling.');

  // ───────────────────────── DOMAIN MATCHING ─────────────────────────
  function globToRegExp(glob) {
    const esc = s => s.replace(/[.+^${}()|[\]\\]/g, '\\$&');
    const g = esc(glob).replace(/\\\*/g,'.*').replace(/\\\?/g,'.');
    return new RegExp(`^${g}$`, 'i');
  }
  function domainPatternToRegex(p) {
    p = p.trim();
    if (!p) return null;
    if (p.startsWith('/') && p.endsWith('/')) {
      try { return new RegExp(p.slice(1,-1), 'i'); } catch { return null; }
    }
    if (p.includes('*') || p.includes('?')) return globToRegExp(p.replace(/^\.*\*?\./,'*.'));
    const esc = p.replace(/[.+^${}()|[\]\\]/g,'\\$&');
    return new RegExp(`(^|\\.)${esc}$`, 'i');
  }
  function listMatchesHost(list, host) {
    for (const pat of list) { const rx = domainPatternToRegex(pat); if (rx && rx.test(host)) return true; }
    return false;
  }
  function computeDomainDisabled(host) {
    if (DOMAINS_MODE === 'allow') return !listMatchesHost(DOMAIN_ALLOW, host);
    return listMatchesHost(DOMAIN_DENY, host);
  }
  let DOMAIN_DISABLED = computeDomainDisabled(HOST);
  if (DOMAIN_DISABLED) log('domain disabled by list:', HOST, 'mode=', DOMAINS_MODE);

  // ───────────────────────── HEURISTICS ─────────────────────────
  const CARD_SELECTOR = 'article, [itemtype*="NewsArticle"], .card, .post, .entry, .teaser, .tile, .story, [data-testid*="card" i]';

  const KICKER_CLASS = /(kicker|eyebrow|label|badge|chip|pill|tag|topic|category|section|watch|brief|update|live|breaking)/i;
  const KICKER_ID    = /(kicker|eyebrow|label|badge|chip|pill|tag|topic|category|section|watch|brief|update|live|breaking)/i;

  const UI_LABELS = /\b(comments?|repl(?:y|ies)|share|watch|play|read(?:\s*more)?|more|menu|subscribe|login|sign ?in|sign ?up|search|next|previous|prev|back|trending|latest|live|open|close|expand|collapse|video|audio|podcast|gallery|photos?)\b/i;
  const UI_CONTAINERS = '.meta, .metadata, .byline, .tools, .actions, .card__meta, .card__footer, .post__meta, [data-testid*="tools" i], [role="toolbar"]';

  const normalizeSpace = (s) => s.replace(/\s+/g, ' ').trim();
  const textTrim = (n) => normalizeSpace(n.textContent || '');
  const words = (s) => normalizeSpace(s).split(' ').filter(Boolean);
  const withinLen = (t) => { const L = t.length; return L >= CFG.minLen && L <= CFG.maxLen; };
  const hasPunct = (s) => /[.?!:;—–-]/.test(s);
  const hasDigit = (s) => /\d/.test(s);
  const isVisible = (el) => el.offsetParent !== null;
  const isEditable = (el) => el.closest('input, textarea, [contenteditable=""], [contenteditable="true"]');

  const lowerRatio = (s) => {
    const letters = s.match(/[A-Za-zΑ-Ωα-ωΆ-Ώά-ώ]/g) || [];
    if (!letters.length) return 0;
    const lowers = s.match(/[a-zα-ωά-ώ]/g) || [];
    return lowers.length / letters.length;
  };
  const isAllCapsish = (s) => {
    const ls = s.replace(/[^A-Za-zΑ-Ωα-ωΆ-Ώ]/g,'');
    if (ls.length < 2) return false;
    const uppers = s.match(/[A-ZΑ-ΩΆ-Ώ]/g) || [];
    return uppers.length / ls.length >= 0.85;
  };

  function isLikelyKicker(el, t) {
    const w = words(t);
    const fewWords = w.length <= 4;
    const noEndPunct = !/[.?!]$/.test(t);
    const capsy = isAllCapsish(t);
    const looksLikeLabel = (el.className && KICKER_CLASS.test(el.className)) || (el.id && KICKER_ID.test(el.id));
    return ((CFG.kickerFilterStrict && (capsy && fewWords && noEndPunct)) || looksLikeLabel);
  }

  function tagScore(el) {
    const tag = el.tagName.toLowerCase();
    if (tag === 'h1') return 100;
    if (tag === 'h2') return 90;
    if (tag === 'h3') return 80;
    if (tag === 'h4') return 65;
    if (tag === 'a')  return 60;
    if (el.hasAttribute('role') && el.getAttribute('role') === 'heading') return 75;
    if (el.hasAttribute('itemprop') && /headline/i.test(el.getAttribute('itemprop'))) return 85;
    return 50;
  }
  function cssScore(el) {
    const cs = getComputedStyle(el);
    const fs = parseFloat(cs.fontSize) || 0;
    const fw = parseInt(cs.fontWeight, 10) || 400;
    let s = 0;
    if (fs) {
      s += Math.min(40, (fs - 12) * 2);
      if (fs < 14) s -= 30;
    }
    if (fw >= 700) s += 12;
    else if (fw >= 600) s += 8;
    else if (fw >= 500) s += 4;
    return s;
  }
  function contentScore(t) {
    const w = words(t);
    if (w.length < CFG.minWords) return -40;
    if (w.length > CFG.maxWords) return -20;
    let s = 0;
    if (hasPunct(t)) s += 8;
    if (hasDigit(t)) s += 4;
    const lr = lowerRatio(t);
    if (lr < 0.2) s -= 25;
    if (/[“”"«»]/.test(t)) s += 2;
    return s;
  }
  function computeCandidateScore(el, t) {
    let s = 0;
    s += tagScore(el);
    s += cssScore(el);
    s += contentScore(t);
    if (el.closest(CARD_SELECTOR)) s += 10;
    if (isLikelyKicker(el, t)) s -= 50;
    if (el.tagName.toLowerCase() === 'a' && words(t).length <= 3 && !hasPunct(t)) s -= 24;
    return s;
  }
  function isHardRejectText(el, t) {
    const w = words(t).length;
    if (el.closest?.(UI_CONTAINERS)) return true;
    if (UI_LABELS.test(t)) return true;
    const href = el.tagName?.toLowerCase?.() === 'a' ? (el.getAttribute('href') || '') : '';
    if (/#/.test(href) || /comment/i.test(href)) return true;
    if (w <= 2 && !hasPunct(t) && t.length < 18) return true;
    if (isAllCapsish(t) && w <= 4 && !hasPunct(t)) return true;
    return false;
  }

  function findTextHost(el) {
    const p = el.querySelector(':scope > p'); if (p && withinLen(textTrim(p))) return p;
    const a = el.querySelector(':scope > a'); if (a && withinLen(textTrim(a))) return a;
    const kids = Array.from(el.children);
    if (kids.length === 1 && textTrim(kids[0]) === textTrim(el)) return kids[0];
    let best=null, bestLen=0; for (const c of kids){ const L=textTrim(c).length; if (L>bestLen){best=c;bestLen=L;} }
    return best && bestLen ? best : el;
  }

  function ensureHighlightCSS() {
    if (document.getElementById('neutralizer-ai-style')) return;
    const style = document.createElement('style');
    style.id = 'neutralizer-ai-style';
    style.textContent = `
      .neutralizer-ai-flash { animation: neutralizerFlash var(--neutralizer-duration, 900ms) ease-out; }
      @keyframes neutralizerFlash {
        0% { background-color: var(--neutralizer-color, #fff4a3);
             box-shadow: 0 0 0 2px rgba(0,0,0,.03), 0 0 12px rgba(0,0,0,.12); }
        100% { background-color: transparent; box-shadow: none; }
      }
      .neutralizer-badge { position: fixed !important; z-index: 2147483646;
        font: 12px/1.4 system-ui, sans-serif !important; color: #0b3d2c !important;
        background: rgba(255,255,255,0.95) !important;
        border: 1px solid #79d4b0 !important; padding: 0 !important; border-radius: 10px !important;
        box-shadow: 0 6px 22px rgba(0,0,0,.18) !important;
        display: flex !important; flex-direction: column !important; gap: 0 !important;
        box-sizing: border-box !important; width: max-content !important; min-width: 120px !important;
        margin: 0 !important; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
        user-select: none !important; }
      .neutralizer-badge.collapsed { right: 0 !important; left: auto !important;
        transform: translateX(calc(100% - 1px)) !important; border-right: none !important;
        border-radius: 10px 0 0 10px !important; box-shadow: -4px 0 22px rgba(0,0,0,.18) !important; }
      .neutralizer-badge.dragging { transition: none !important; cursor: grabbing !important; }
      .neutralizer-badge .badge-handle { position: absolute !important; left: -28px !important; top: 50% !important;
        transform: translateY(-50%) !important; width: 28px !important; height: 56px !important;
        background: linear-gradient(90deg, #d4f8e8 0%, #c9f6e1 100%) !important; border: 1px solid #79d4b0 !important; border-right: none !important;
        border-radius: 8px 0 0 8px !important; cursor: pointer !important;
        display: flex !important; align-items: center !important; justify-content: center !important;
        font-size: 14px !important; color: #0b3d2c !important; user-select: none !important;
        box-shadow: -3px 0 12px rgba(0,0,0,.12) !important; transition: all 0.2s ease !important; }
      .neutralizer-badge .badge-handle:hover { left: -30px !important; box-shadow: -4px 0 16px rgba(0,0,0,.18) !important; }
      .neutralizer-badge .badge-header { background: linear-gradient(135deg, #2d6a54 0%, #0b3d2c 100%) !important;
        color: #fff !important; padding: 6px 10px !important; font-size: 10px !important;
        font-weight: 600 !important; text-align: center !important; cursor: grab !important;
        user-select: none !important; border-radius: 9px 9px 0 0 !important;
        letter-spacing: 0.3px !important; }
      .neutralizer-badge .badge-header:active { cursor: grabbing !important; }
      .neutralizer-badge .badge-content { display: flex !important; flex-direction: column !important;
        gap: 6px !important; padding: 8px 10px !important; }
      .neutralizer-badge * { box-sizing: border-box !important; }
      .neutralizer-badge .row { display: flex !important; gap: 8px !important; align-items: center !important;
        justify-content: center !important; margin: 0 !important; padding: 0 !important; width: 100%; }
      .neutralizer-badge .btn { cursor: pointer !important; padding: 6px 10px !important; border-radius: 8px !important;
        border: 1px solid #79d4b0 !important; background: #fff !important; box-sizing: border-box !important;
        white-space: nowrap !important; font-family: system-ui, sans-serif !important;
        font-size: 12px !important; line-height: 1.2 !important; color: #0b3d2c !important;
        margin: 0 !important; min-width: 0 !important; width: auto !important; }
      .neutralizer-badge .btn.primary { background: #0b3d2c !important; color: #fff !important;
        border-color: #0b3d2c !important; }
      .neutralizer-badge .btn:disabled { opacity: 0.6 !important; cursor: not-allowed !important; }
      .neutralizer-badge .small { font-size: 11px !important; opacity: .9 !important; }
    `;
    document.head.appendChild(style);
  }
  ensureHighlightCSS();

  function isExcluded(el) {
    if (el.closest?.(`[${UI_ATTR}]`)) return true;
    if (isEditable(el)) return true;
    if (EXCLUDE.self.length && el.matches?.(EXCLUDE.self.join(','))) return true;
    if (EXCLUDE.ancestors.length && el.closest?.(EXCLUDE.ancestors.join(','))) return true;
    return false;
  }

  // ───────────────────────── DATA STRUCTURES ─────────────────────────
  let seenEl = new WeakSet();
  const textToElements = new Map(); // text -> Set<host elements>
  const CHANGES = [];               // {from,to,count,source,mode:'auto'|'manual'}

  const cacheKey = (t) => HOST + '|' + t;
  const cacheGet = (t) => (CACHE[cacheKey(t)]?.r) ?? null;
  function cacheSet(t, r) {
    CACHE[cacheKey(t)] = { r, t: Date.now() };
    cacheDirty = true;
    const size = Object.keys(CACHE).length;
    if (size > CFG.cacheLimit) {
      // lazy trim
      queueMicrotask(() => {
        const keys = Object.keys(CACHE);
        if (keys.length <= CFG.cacheLimit) return;
        keys.sort((a,b)=>CACHE[a].t - CACHE[b].t);
        const toDrop = Math.max(0, keys.length - CFG.cacheTrimTo);
        for (let i=0; i<toDrop; i++) delete CACHE[keys[i]];
        storage.set(CACHE_KEY, JSON.stringify(CACHE));
        cacheDirty = false;
        log('cache trimmed:', keys.length, '→', Object.keys(CACHE).length);
      });
    } else if (cacheDirty) {
      clearTimeout(cacheSet._t);
      cacheSet._t = setTimeout(() => { storage.set(CACHE_KEY, JSON.stringify(CACHE)); cacheDirty = false; }, 250);
    }
  }
  async function cacheClear() { CACHE = {}; await storage.set(CACHE_KEY, JSON.stringify(CACHE)); }

  // ───────────────────────── BODY CACHE ─────────────────────────
  function simpleHash(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash; // Convert to 32bit integer
    }
    return Math.abs(hash).toString(36);
  }

  function getBodyCacheKey(url, paragraphTexts) {
    // Create a cache key from URL + hash of concatenated paragraph texts
    const contentHash = simpleHash(paragraphTexts.join('|||'));
    return `${url}::${contentHash}`;
  }

  function bodyCacheGet(url, paragraphTexts) {
    const key = getBodyCacheKey(url, paragraphTexts);
    const cached = BODY_CACHE[key];
    if (!cached) return null;

    // Update timestamp for LRU
    cached.t = Date.now();
    bodyCacheDirty = true;
    return cached.simplified;
  }

  function bodyCacheSet(url, paragraphTexts, simplified) {
    const key = getBodyCacheKey(url, paragraphTexts);
    BODY_CACHE[key] = {
      simplified,
      t: Date.now(),
      url,
      count: paragraphTexts.length
    };
    bodyCacheDirty = true;

    const size = Object.keys(BODY_CACHE).length;
    if (size > BODY_CACHE_LIMIT) {
      // Trim cache
      queueMicrotask(() => {
        const keys = Object.keys(BODY_CACHE);
        if (keys.length <= BODY_CACHE_LIMIT) return;
        keys.sort((a,b) => BODY_CACHE[a].t - BODY_CACHE[b].t);
        const toDrop = Math.max(0, keys.length - BODY_CACHE_TRIM_TO);
        for (let i = 0; i < toDrop; i++) delete BODY_CACHE[keys[i]];
        storage.set(BODY_CACHE_KEY, JSON.stringify(BODY_CACHE));
        bodyCacheDirty = false;
        log('body cache trimmed:', keys.length, '→', Object.keys(BODY_CACHE).length);
      });
    } else if (bodyCacheDirty) {
      clearTimeout(bodyCacheSet._t);
      bodyCacheSet._t = setTimeout(() => {
        storage.set(BODY_CACHE_KEY, JSON.stringify(BODY_CACHE));
        bodyCacheDirty = false;
      }, 500);
    }
  }

  async function bodyCacheClear() {
    BODY_CACHE = {};
    await storage.set(BODY_CACHE_KEY, JSON.stringify(BODY_CACHE));
    log('body cache cleared');
  }

  // ───────────────────────── API TOKEN TRACKING ─────────────────────────
  function updateApiTokens(type, usage) {
    if (!usage) return;

    // OpenAI API uses 'input_tokens' and 'output_tokens' (not prompt_tokens/completion_tokens)
    const inputTokens = usage.input_tokens || usage.prompt_tokens || 0;
    const outputTokens = usage.output_tokens || usage.completion_tokens || 0;

    if (inputTokens === 0 && outputTokens === 0) {
      log('WARNING: No token data found in usage object:', usage);
      return;
    }

    API_TOKENS[type].input += inputTokens;
    API_TOKENS[type].output += outputTokens;
    API_TOKENS[type].calls += 1;

    log(`${type} tokens: +${inputTokens} input, +${outputTokens} output (total: ${API_TOKENS[type].input + API_TOKENS[type].output})`);

    // Debounced save to avoid excessive writes
    clearTimeout(updateApiTokens._timer);
    updateApiTokens._timer = setTimeout(() => {
      storage.set(API_TOKENS_KEY, JSON.stringify(API_TOKENS));
      log('API tokens updated and saved:', API_TOKENS);
    }, 1000);
  }

  async function resetApiTokens() {
    API_TOKENS = {
      headlines: { input: 0, output: 0, calls: 0 },
      body: { input: 0, output: 0, calls: 0 }
    };
    await storage.set(API_TOKENS_KEY, JSON.stringify(API_TOKENS));
    log('API token stats reset');
  }

  function calculateApiCost() {
    const inputCost = (API_TOKENS.headlines.input + API_TOKENS.body.input) * PRICING.inputPer1M / 1_000_000;
    const outputCost = (API_TOKENS.headlines.output + API_TOKENS.body.output) * PRICING.outputPer1M / 1_000_000;
    return inputCost + outputCost;
  }

  async function updatePricing(newPricing) {
    PRICING = {
      ...PRICING,
      ...newPricing,
      lastUpdated: new Date().toISOString().split('T')[0] // YYYY-MM-DD
    };
    await storage.set(PRICING_KEY, JSON.stringify(PRICING));
    log('Pricing updated:', PRICING);
  }

  async function resetPricingToDefaults() {
    PRICING = {
      model: 'gpt-4o-mini',
      inputPer1M: 0.15,
      outputPer1M: 0.60,
      lastUpdated: '2025-01-25',
      source: 'https://openai.com/api/pricing/'
    };
    await storage.set(PRICING_KEY, JSON.stringify(PRICING));
    log('Pricing reset to defaults');
  }

  // ───────────────────────── DISCOVERY ─────────────────────────
  function getManualMatches(root){
    if (!SELECTORS.length) return [];
    try {
      return [...root.querySelectorAll(compiledSelectors())];
    } catch (e) {
      log('manual selector error:', e.message || e, 'selectors=', SELECTORS);
      return [];
    }
  }

  function discoverCandidates(root){
    if (!CFG.autoDetect) return [];
    const seedSets = [
      root.querySelectorAll('h1, h2, h3, h4, [role="heading"], [aria-level], [itemprop="headline"]'),
      root.querySelectorAll('.lead, .deck, .standfirst, .subhead, .kicker, .teaser, .title, .headline'),
      root.querySelectorAll(`${CARD_SELECTOR} h1, ${CARD_SELECTOR} h2, ${CARD_SELECTOR} h3, ${CARD_SELECTOR} a`)
    ];
    const candEls = [];
    const seen = new Set();
    for (const list of seedSets) {
      for (const el of list) {
        if (!el || seen.has(el) || !isVisible(el) || isExcluded(el)) continue;
        seen.add(el);
        candEls.push(el);
      }
    }

    const scored = [];
    for (const el of candEls) {
      const host = findTextHost(el);
      if (!host || isExcluded(host)) continue;
      const t = normalizeSpace(host.textContent || '');
      if (!withinLen(t)) continue;
      if (isHardRejectText(host, t)) continue;
      const score = computeCandidateScore(host, t);
      if (score >= CFG.scoreThreshold) {
        scored.push({ host, text: t, score, card: host.closest(CARD_SELECTOR) });
      }
    }

    const byCard = new Map();
    for (const row of scored) {
      const key = row.card || document;
      if (!byCard.has(key)) byCard.set(key, []);
      byCard.get(key).push(row);
    }
    const winners = [];
    for (const [, arr] of byCard) {
      arr.sort((a,b) => b.score - a.score);
      winners.push(...arr.slice(0, CFG.topKPerCard));
    }

    const bestByText = new Map();
    for (const w of winners) {
      const k = w.text;
      if (!bestByText.has(k) || w.score > bestByText.get(k).score) bestByText.set(k, w);
    }

    if (CFG.DEBUG_SCORES) {
      const top = [...bestByText.values()].sort((a,b)=>b.score-a.score).slice(0, 20);
      log('top candidates:', top.map(x => ({score:x.score, text:x.text.slice(0,120)})));
    }

    return [...bestByText.values()].map(x => x.host);
  }

  function getCandidateElements(root){
    const manual = getManualMatches(root).map(el => ({el: findTextHost(el), mode: 'manual'}));
    const auto   = discoverCandidates(root).map(el => ({el, mode: 'auto'}));
    const uniq = [];
    const seen = new Set();
    for (const rec of [...manual, ...auto]) {
      const el = rec.el;
      if (!el || isExcluded(el)) continue;
      if (seen.has(el)) continue;
      seen.add(el);
      uniq.push(rec);
    }
    return uniq;
  }

  // ───────────────────────── ATTACH & OBSERVE ─────────────────────────
  let longHeadlineCheckPending = false;

  async function attachTargets(root = document) {
    const candidates = getCandidateElements(root)
      .map(({el,mode}) => ({el: findTextHost(el), mode}))
      .filter(({el, mode}) => {
        if (!el || seenEl.has(el) || isExcluded(el)) return false;
        // Only apply length check to auto-detected elements, not manual selectors
        if (mode === 'auto' && !withinLen(textTrim(el))) return false;
        return true;
      });

    // Check for excessively long manual selections
    const excessivelyLong = [];
    const hostsToAttach = [];

    for (const {el: host, mode} of candidates) {
      const text = textTrim(host);

      // Sanity check: manual selectors with text > 500 chars
      if (mode === 'manual' && text.length > CFG.sanityCheckLen && !LONG_HEADLINE_EXCEPTIONS[HOST]) {
        excessivelyLong.push({host, text, length: text.length});
      } else {
        hostsToAttach.push({host, mode, text});
      }
    }

    // If we found excessively long manual selections, ask user once
    if (excessivelyLong.length > 0 && !longHeadlineCheckPending) {
      longHeadlineCheckPending = true;
      const result = await showLongHeadlineDialog(excessivelyLong);
      longHeadlineCheckPending = false;

      if (result) {
        // User approved processing
        for (const {host, text} of excessivelyLong) {
          hostsToAttach.push({host, mode: 'manual', text});
        }

        // If user chose "remember", save exception for this domain
        if (result === true) {
          LONG_HEADLINE_EXCEPTIONS[HOST] = true;
          await storage.set(LONG_HEADLINE_EXCEPTIONS_KEY, JSON.stringify(LONG_HEADLINE_EXCEPTIONS));
        }
      }
      // else: skip these elements
    }

    // Attach the approved hosts
    for (const {host, mode, text} of hostsToAttach) {
      host.setAttribute('data-neutralizer-mode', mode);
      let set = textToElements.get(text);
      if (!set) { set = new Set(); textToElements.set(text, set); }
      set.add(host);
      observerObserve(host);
    }
  }

  let IO = null;
  function ensureObserver() {
    if (IO || !CFG.visibleOnly) return;
    IO = new IntersectionObserver(onIntersect, { root: null, rootMargin: CFG.rootMargin, threshold: CFG.threshold });
  }
  function observerObserve(el) { if (CFG.visibleOnly) { ensureObserver(); IO.observe(el); } }

  const pending = new Set();
  let flushTimer = null;

  function onIntersect(entries) {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue;
      const el = entry.target;
      IO.unobserve(el);
      if (isExcluded(el)) continue;
      const text = textTrim(el);

      const cached = cacheGet(text);
      if (cached) { applyRewrites(buildMap([text]), [text], [cached], 'cache'); continue; }
      if (!pending.has(text)) pending.add(text);
    }
    scheduleFlush();
  }

  function scheduleFlush() { if (!flushTimer) flushTimer = setTimeout(flushPending, CFG.flushDelayMs); }

  function buildMap(texts) {
    const map = new Map();
    for (const t of texts) { const set = textToElements.get(t); if (set && set.size) map.set(t, [...set]); }
    return map;
  }

  function quoteProtect(original, rewritten) {
    const quotes = [];
    const rx = /[“”"«»](.*?)[“”"«»]/g;
    let m;
    while ((m = rx.exec(original)) !== null) {
      quotes.push(m[0]);
    }
    let out = rewritten;
    for (const q of quotes) {
      out = out.replace(/([“”"«»]).*?([“”"«»])/g, q);
    }
    return out;
  }

  function applyRewrites(map, originals, rewrites, source = 'live') {
    let changedBatch = 0;
    const localChanges = [];
    for (let i = 0; i < originals.length; i++) {
      const from = originals[i];
      let to = (rewrites[i] ?? '').trim();
      if (!to || to === from) continue;
      to = quoteProtect(from, to);

      const els = map.get(from) || [];
      let changedCount = 0;

      for (const host of els) {
        if (isExcluded(host) || seenEl.has(host)) continue;
        const before = textTrim(host);
        if (!before) continue;

        if (!host.hasAttribute('data-neutralizer-original')) {
          host.setAttribute('data-neutralizer-original', before);
        }
        if (CFG.showOriginalOnHover && !host.hasAttribute('title')) {
          host.setAttribute('title', before);
        }

        host.textContent = to;
        host.setAttribute('data-neutralizer-changed','1');
        seenEl.add(host);
        changedCount++;
        changedBatch++;

        if (CFG.highlight) {
          host.style.setProperty('--neutralizer-color', CFG.highlightColor);
          host.style.setProperty('--neutralizer-duration', `${CFG.highlightMs}ms`);
          host.classList.add('neutralizer-ai-flash');
          setTimeout(() => host.classList.remove('neutralizer-ai-flash'), CFG.highlightMs + 120);
        }
      }
      if (changedCount) {
        const mode = (els[0]?.getAttribute?.('data-neutralizer-mode')) || 'auto';
        log(`[${source}] (${mode}) "${from}" → "${to}" on ${changedCount} element(s)`);
        localChanges.push({ from, to, count: changedCount, source, mode });
      }
    }
    if (changedBatch) {
      if (source === 'live') STATS.live += changedBatch;
      else if (source === 'cache') STATS.cache += changedBatch;
      STATS.total = STATS.live + STATS.cache;
      CHANGES.push(...localChanges);
      updateBadgeCounts();
    }
    return changedBatch;
  }

  async function flushPending() {
    const toSend = [];
    for (const t of pending) { if (!cacheGet(t)) toSend.push(t); pending.delete(t); if (toSend.length === CFG.maxBatch) break; }
    flushTimer = null;
    if (!toSend.length) return;

    try {
      log('calling OpenAI for visible batch size', toSend.length);
      const rewrites = await rewriteBatch(toSend);
      for (let i = 0; i < toSend.length; i++) cacheSet(toSend[i], rewrites[i] ?? toSend[i]);
      applyRewrites(buildMap(toSend), toSend, rewrites, 'live');
      STATS.batches++;
      log(`[stats] batches=${STATS.batches} total=${STATS.total} (live=${STATS.live}, cache=${STATS.cache})`);
    } catch (e) {
      console.error('error:', e);
      friendlyApiError(e);
    }
    if (pending.size) scheduleFlush();
  }

  // ───────────────────────── OPENAI ─────────────────────────
  function apiHeaders(key) {
    return {
      Authorization: `Bearer ${key}`,
      'Content-Type': 'application/json',
      Accept: 'application/json'
    };
  }

  function xhrPost(url, data, headers = {}) {
    return new Promise((resolve, reject) => {
      const api = (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : GM_xmlhttpRequest;
      api({
        method: 'POST',
        url,
        data,
        headers,
        onload: (r) => {
          if (r.status >= 200 && r.status < 300) return resolve(r.responseText);
          const err = new Error(`HTTP ${r.status}`);
          err.status = r.status;
          err.body = r.responseText || '';
          reject(err);
        },
        onerror: (e) => { const err = new Error((e && e.error) || 'Network error'); err.status = 0; reject(err); },
        timeout: 30000,
        ontimeout: () => { const err = new Error('Request timeout'); err.status = 0; },
      });
    });
  }

  function xhrGet(url, headers = {}) {
    return new Promise((resolve, reject) => {
      const api = (typeof GM !== 'undefined' && GM.xmlHttpRequest) ? GM.xmlHttpRequest : GM_xmlHttpRequest;
      api({
        method: 'GET',
        url,
        headers,
        onload: (r) => (r.status >= 200 && r.status < 300) ? resolve(r.responseText) : reject(Object.assign(new Error(`HTTP ${r.status}`),{status:r.status,body:r.responseText})),
        onerror: (e) => reject(Object.assign(new Error((e && e.error) || 'Network error'),{status:0})),
        timeout: 20000,
        ontimeout: () => reject(Object.assign(new Error('Request timeout'),{status:0})),
      });
    });
  }

  function extractOutputText(data) {
    if (typeof data.output_text === 'string') return data.output_text;
    if (Array.isArray(data.output)) {
      const parts = [];
      for (const msg of data.output) {
        if (Array.isArray(msg.content)) {
          for (const c of msg.content) {
            if (typeof c.text === 'string') parts.push(c.text);
            else if (c.type === 'output_text' && typeof c.text === 'string') parts.push(c.text);
          }
        }
      }
      if (parts.length) return parts.join('');
    }
    if (Array.isArray(data.choices)) return data.choices.map((ch)=>ch.message?.content||'').join('\n');
    return '';
  }

  async function rewriteBatch(texts) {
    const KEY = await storage.get('OPENAI_KEY', '');
    if (!KEY) { openKeyDialog('OpenAI API key missing.'); throw Object.assign(new Error('API key missing'), {status:401}); }

    const safeInputs = texts.map(t => t.replace(/[\u2028\u2029]/g, ' '));
    const instructions =
      'You will receive INPUT as a JSON array of headlines.' +
      ' Rewrite each headline neutrally in the SAME language as input.' +
      ' Preserve factual meaning and named entities. Remove sensationalism and excess punctuation.' +
      ' If the headline contains a direct quote inside quotation marks (English "…", Greek «…»), keep that quoted text verbatim.' +
      ' Aim ≤ 110 characters when possible. Return ONLY a JSON array of strings, same order as input.';

    const body = JSON.stringify({
      model: CFG.model,
      temperature: CFG.temperature,
      max_output_tokens: 1000,
      instructions,
      input: JSON.stringify(safeInputs)
    });

    const resText = await xhrPost('https://api.openai.com/v1/responses', body, apiHeaders(KEY));
    const payload = JSON.parse(resText);

    // Track token usage for headlines
    if (payload.usage) {
      updateApiTokens('headlines', payload.usage);
    }

    const outStr = extractOutputText(payload);
    if (!outStr) throw Object.assign(new Error('No output_text/content from API'), {status:400});
    const cleaned = outStr.replace(/^```json\s*|\s*```$/g, '');
    const arr = JSON.parse(cleaned);
    if (!Array.isArray(arr)) throw Object.assign(new Error('API did not return a JSON array'), {status:400});
    return arr;
  }

  function friendlyApiError(err) {
    const s = err?.status || 0;
    if (s === 401) { openKeyDialog('Unauthorized (401). Please enter a valid OpenAI key.'); return; }
    if (s === 429) { openInfo('Rate limited by API (429). Try again in a minute. You can also lower maxBatch or enable visible-only to reduce burst.'); return; }
    if (s === 400) { openInfo('Bad request (400). The page may contain text the API could not parse. Try again, or disable auto-detect for this site and use narrower selectors.'); return; }
    openInfo(`Unknown error${s ? ' ('+s+')' : ''}. Check your network or try again.`);
  }

  // ───────────────────────── BODY SIMPLIFICATION ─────────────────────────
  let articleBodyParagraphs = null;
  let bodySimplified = false;

  function isArticlePage() {
    // Check for strong article indicators first (high confidence)
    const strongArticleSelectors = [
      '[itemprop="articleBody"]',
      '.article-body',
      '.post-content',
      '.entry-content',
      '[class*="article-content"]',
      '[class*="post-body"]'
    ];

    for (const selector of strongArticleSelectors) {
      if (document.querySelector(selector)) return true;
    }

    // Exclude homepage/listing pages
    const path = location.pathname;
    const isHomepage = path === '/' || path === '' || path === '/index.html' || path === '/index.php';

    if (isHomepage) {
      return false;
    }

    // Check for listing-specific classes
    const listingIndicators = [
      '.post-list', '.article-list', '.news-list',
      '.category-list', '.archive-list'
    ];
    for (const selector of listingIndicators) {
      if (document.querySelector(selector)) return false;
    }

    // Check for article element with semantic markup
    const articles = document.querySelectorAll('article');
    if (articles.length === 1) {
      const article = articles[0];
      if (article.getAttribute('role') === 'article' ||
          article.getAttribute('itemtype')?.includes('Article')) {
        return true;
      }
    }

    // URL pattern suggests article (has numbers suggesting specific content)
    const hasArticleUrlPattern = /\/\d{4,}\/|\/\d{4}-\d{2}-\d{2}\//.test(path);

    // Check for long-form content
    const mainContent = document.querySelector('article, main, [role="main"]');
    if (mainContent) {
      const paragraphs = mainContent.querySelectorAll('p');
      let substantialParagraphs = 0;
      for (const p of paragraphs) {
        if (p.textContent.trim().length > 100) substantialParagraphs++;
        if (substantialParagraphs >= 4) return true;
      }
    }

    // If URL pattern suggests article, require more evidence
    if (hasArticleUrlPattern) {
      const allParagraphs = document.querySelectorAll('article p, main p, [role="main"] p');
      let totalLength = 0;
      for (const p of allParagraphs) {
        totalLength += p.textContent.trim().length;
        if (totalLength > 800) return true;
      }
    }

    return false;
  }

  function extractArticleBody() {
    // Try multiple strategies to find article body paragraphs
    const strategies = [
      // Strategy 1: Look for semantic article body markers
      () => document.querySelector('[itemprop="articleBody"]'),
      () => document.querySelector('article[itemtype*="Article"]'),
      () => document.querySelector('.article-body'),
      () => document.querySelector('.post-content'),
      () => document.querySelector('.entry-content'),
      () => document.querySelector('[class*="article-content"]'),
      () => document.querySelector('[class*="post-body"]'),
      // Strategy 2: Look for article or main tags
      () => document.querySelector('article'),
      () => document.querySelector('main'),
      () => document.querySelector('[role="main"]')
    ];

    let container = null;
    for (const strategy of strategies) {
      container = strategy();
      if (container) break;
    }

    if (!container) return [];

    // Extract paragraphs, excluding UI elements
    const paragraphs = Array.from(container.querySelectorAll('p'));
    return paragraphs.filter(p => {
      // Filter out short paragraphs (likely UI text)
      if (p.textContent.trim().length < 50) return false;

      // Filter out paragraphs that are inside excluded containers
      if (p.closest('[data-neutralizer-ui]')) return false;
      if (p.closest('.comment, .comments, .sidebar, .navigation, .menu, .footer, .header')) return false;

      // Filter out paragraphs with attributes suggesting they're UI elements
      if (p.hasAttribute(UI_ATTR)) return false;

      return true;
    });
  }

  async function simplifyBodyText(paragraphs) {
    const KEY = await storage.get('OPENAI_KEY', '');
    if (!KEY) { openKeyDialog('OpenAI API key missing.'); throw Object.assign(new Error('API key missing'), {status:401}); }

    const texts = paragraphs.map(p => p.textContent.trim());
    const safeInputs = texts.map(t => t.replace(/[\u2028\u2029]/g, ' '));

    const instructions =
      'You will receive INPUT as a JSON array of article paragraphs.' +
      ' Simplify each paragraph by removing convoluted phrasing, excessive jargon, and overly complex or epistemic language.' +
      ' Make the language clearer and more direct while staying in the SAME language as input.' +
      ' CRITICAL: Do NOT change facts, numbers, names, quotes, or the actual meaning/details of the content.' +
      ' If a paragraph contains a direct quote inside quotation marks (English "…", Greek «…», etc.), keep that quoted text VERBATIM word-for-word.' +
      ' Only simplify the wording and sentence structure to make it easier to read.' +
      ' Preserve all factual information, statistics, proper nouns, and direct quotes exactly as they appear.' +
      ' Return ONLY a JSON array of simplified strings, same order and count as input.';

    const body = JSON.stringify({
      model: CFG.model,
      temperature: SIMPLIFICATION_TEMPERATURE,
      max_output_tokens: 4000,
      instructions,
      input: JSON.stringify(safeInputs)
    });

    const resText = await xhrPost('https://api.openai.com/v1/responses', body, apiHeaders(KEY));
    const payload = JSON.parse(resText);

    // Track token usage for body simplification
    if (payload.usage) {
      updateApiTokens('body', payload.usage);
    }

    const outStr = extractOutputText(payload);
    if (!outStr) throw Object.assign(new Error('No output_text/content from API'), {status:400});
    const cleaned = outStr.replace(/^```json\s*|\s*```$/g, '');
    const arr = JSON.parse(cleaned);
    if (!Array.isArray(arr)) throw Object.assign(new Error('API did not return a JSON array'), {status:400});
    return arr;
  }

  async function applyBodySimplification(forceApply = false) {
    if (!forceApply && !SIMPLIFY_BODY) return;
    if (bodySimplified) return; // Already simplified
    if (!isArticlePage()) {
      log('Not an article page, skipping body simplification');
      return;
    }

    try {
      const paragraphs = extractArticleBody();
      if (!paragraphs.length) {
        log('No article body paragraphs found');
        return;
      }

      log(`Found ${paragraphs.length} article body paragraphs to simplify`);

      // Store original text
      articleBodyParagraphs = paragraphs;

      // Mark originals before simplifying
      paragraphs.forEach(p => {
        if (!p.hasAttribute('data-neutralizer-body-original')) {
          p.setAttribute('data-neutralizer-body-original', p.textContent);
        }
      });

      const paragraphTexts = paragraphs.map(p => p.textContent.trim());
      const url = location.href;

      // Check cache first
      const cached = bodyCacheGet(url, paragraphTexts);
      if (cached && cached.length === paragraphs.length) {
        log(`Using cached simplification for ${paragraphs.length} paragraphs`);
        paragraphs.forEach((p, idx) => {
          if (cached[idx]) {
            p.textContent = cached[idx];
            p.setAttribute('data-neutralizer-body-simplified', '1');
          }
        });
        bodySimplified = true;
        syncBodyBadgeState();
        log('Body simplification complete (from cache)');
        return;
      }

      // Not in cache - simplify via API in parallel batches
      const batchSize = 10;
      const maxConcurrent = 5;
      const allSimplified = [];

      // Create all batches
      const batches = [];
      for (let i = 0; i < paragraphs.length; i += batchSize) {
        batches.push({
          paragraphs: paragraphs.slice(i, i + batchSize),
          startIdx: i
        });
      }

      log(`Processing ${batches.length} batches with max ${maxConcurrent} concurrent requests`);

      // Update badge to show progress
      const updateProgressBadge = (current, total) => {
        const btn = badge?.querySelector('.body-action');
        if (btn) {
          btn.textContent = `B: simplifying ${current}/${total}...`;
        }
      };

      // Process batches in chunks of maxConcurrent
      for (let i = 0; i < batches.length; i += maxConcurrent) {
        const chunk = batches.slice(i, i + maxConcurrent);
        const currentChunk = Math.floor(i / maxConcurrent) + 1;
        const totalChunks = Math.ceil(batches.length / maxConcurrent);

        updateProgressBadge(currentChunk, totalChunks);

        const chunkPromises = chunk.map(batch =>
          simplifyBodyText(batch.paragraphs).then(simplified => ({
            simplified,
            batch
          }))
        );

        // Wait for all batches in this chunk to complete
        const results = await Promise.all(chunkPromises);

        // Apply results to DOM in order
        results.forEach(({ simplified, batch }) => {
          allSimplified.push(...simplified);
          batch.paragraphs.forEach((p, idx) => {
            if (simplified[idx]) {
              p.textContent = simplified[idx];
              p.setAttribute('data-neutralizer-body-simplified', '1');
            }
          });
        });

        log(`Completed chunk ${currentChunk}/${totalChunks} (${chunk.length} batches)`);
      }

      // Store in cache
      bodyCacheSet(url, paragraphTexts, allSimplified);

      bodySimplified = true;
      syncBodyBadgeState();
      log('Body simplification complete (via API, cached)');
    } catch (err) {
      log('Body simplification error:', err);
      friendlyApiError(err);
      syncBodyBadgeState(); // Reset badge state on error
    }
  }

  function restoreBodyOriginals() {
    if (!bodySimplified) return;

    const paragraphs = document.querySelectorAll('[data-neutralizer-body-simplified="1"][data-neutralizer-body-original]');
    let n = 0;
    paragraphs.forEach(p => {
      const orig = p.getAttribute('data-neutralizer-body-original');
      if (typeof orig === 'string') {
        p.textContent = orig;
        n++;
      }
    });

    bodySimplified = false;
    log('Restored original body text on', n, 'paragraphs');
  }

  function reapplyBodySimplification() {
    if (!articleBodyParagraphs) return;

    // Try to get from cache
    const paragraphTexts = articleBodyParagraphs.map(p => p.getAttribute('data-neutralizer-body-original') || p.textContent.trim());
    const url = location.href;
    const cached = bodyCacheGet(url, paragraphTexts);

    if (cached && cached.length === articleBodyParagraphs.length) {
      log('Reapplying body simplification from cache');
      articleBodyParagraphs.forEach((p, idx) => {
        if (cached[idx]) {
          p.textContent = cached[idx];
          p.setAttribute('data-neutralizer-body-simplified', '1');
        }
      });
      bodySimplified = true;
      syncBodyBadgeState();
    } else {
      // Not in cache, need to re-simplify
      log('Cache miss, re-simplifying body text');
      bodySimplified = false;
      applyBodySimplification(true);
    }
  }

  // ───────────────────────── POLYMORPHIC EDITOR (list/secret/domain) ─────────────────────────
  function parseLines(s) { return s.split(/[\n,;]+/).map(x => x.trim()).filter(Boolean); }

  function openEditor({ title, hint = 'One item per line', mode = 'list', initial = [], globalItems = [], onSave, onValidate }) {
    const host = document.createElement('div'); host.setAttribute(UI_ATTR, '');
    const shadow = host.attachShadow({ mode: 'open' });
    const style = document.createElement('style');
    style.textContent = `
      .wrap{position:fixed;inset:0;z-index:2147483647;background:rgba(0,0,0,.45);
            display:flex;align-items:center;justify-content:center}
      .modal{background:#fff;max-width:680px;width:92%;border-radius:10px;
             box-shadow:0 10px 40px rgba(0,0,0,.35);padding:16px 16px 12px;box-sizing:border-box}
      .modal h3{margin:0 0 8px;font:600 16px/1.2 system-ui,sans-serif}
      .section-label{font:600 13px/1.2 system-ui,sans-serif;margin:8px 0 4px;color:#444}
      textarea{width:100%;height:220px;resize:vertical;padding:10px;box-sizing:border-box;
               font:13px/1.4 ui-monospace,Consolas,monospace;border:1px solid #ccc;border-radius:4px}
      textarea.readonly{background:#f5f5f5;color:#666;height:120px}
      textarea.editable{height:180px}
      .row{display:flex;gap:8px;align-items:center}
      input[type=password],input[type=text]{flex:1;padding:10px;border-radius:8px;border:1px solid #ccc;
               font:14px/1.3 ui-monospace,Consolas,monospace;box-sizing:border-box}
      .actions{display:flex;gap:8px;justify-content:flex-end;margin-top:10px}
      .actions button{padding:8px 12px;border-radius:8px;border:1px solid #d0d0d0;background:#f6f6f6;cursor:pointer}
      .actions .save{background:#1a73e8;color:#fff;border-color:#1a73e8}
      .actions .test{background:#34a853;color:#fff;border-color:#34a853}
      .hint{margin:8px 0 0;color:#666;font:12px/1.2 system-ui,sans-serif}
    `;
    const wrap = document.createElement('div'); wrap.className = 'wrap';
    const bodyList = `<textarea spellcheck="false" autocomplete="off" autocapitalize="off" autocorrect="off">${
      Array.isArray(initial) ? initial.join('\n') : ''
    }</textarea>`;
    const bodyDomain = `
      <div class="section-label">Global settings (read-only):</div>
      <textarea class="readonly" readonly spellcheck="false">${Array.isArray(globalItems) ? globalItems.join('\n') : ''}</textarea>
      <div class="section-label">Domain-specific additions (editable):</div>
      <textarea class="editable" spellcheck="false" autocomplete="off" autocapitalize="off" autocorrect="off">${Array.isArray(initial) ? initial.join('\n') : ''}</textarea>
    `;
    const bodySecret = `
      <div class="row">
        <input id="sec" type="password" placeholder="sk-..." autocomplete="off" />
        <button id="toggle" title="Show/Hide">👁</button>
      </div>`;
    const bodyInfo = `<textarea class="readonly" readonly spellcheck="false" style="height:auto;min-height:60px;max-height:300px;">${
      Array.isArray(initial) ? initial.join('\n') : String(initial)
    }</textarea>`;

    let bodyContent, actionsContent;
    if (mode === 'info') {
      bodyContent = bodyInfo;
      actionsContent = '<button class="cancel">Close</button>';
    } else if (mode === 'secret') {
      bodyContent = bodySecret;
      actionsContent = (onValidate ? '<button class="test">Validate</button>' : '') + '<button class="save">Save</button><button class="cancel">Cancel</button>';
    } else if (mode === 'domain') {
      bodyContent = bodyDomain;
      actionsContent = '<button class="save">Save</button><button class="cancel">Cancel</button>';
    } else {
      bodyContent = bodyList;
      actionsContent = '<button class="save">Save</button><button class="cancel">Cancel</button>';
    }

    wrap.innerHTML = `
      <div class="modal" role="dialog" aria-modal="true" aria-label="${title}">
        <h3>${title}</h3>
        ${bodyContent}
        <div class="actions">
          ${actionsContent}
        </div>
        <p class="hint">${hint}</p>
      </div>`;
    shadow.append(style, wrap);
    document.body.appendChild(host);
    const close = () => host.remove();

    if (mode === 'info') {
      const btnClose = shadow.querySelector('.cancel');
      btnClose.addEventListener('click', close);
      wrap.addEventListener('click', e => { if (e.target === wrap) close(); });
      shadow.addEventListener('keydown', e => {
        if (e.key === 'Escape' || e.key === 'Enter') { e.preventDefault(); close(); }
      });
      // Focus the wrapper to enable keyboard events
      wrap.setAttribute('tabindex', '-1');
      wrap.focus();
    } else if (mode === 'secret') {
      const inp = shadow.querySelector('#sec');
      const btnSave = shadow.querySelector('.save');
      const btnCancel = shadow.querySelector('.cancel');
      const btnToggle = shadow.querySelector('#toggle');
      const btnTest = shadow.querySelector('.test');
      if (typeof initial === 'string' && initial) inp.value = initial;
      btnToggle.addEventListener('click', () => { inp.type = (inp.type === 'password') ? 'text' : 'password'; inp.focus(); });
      btnSave.addEventListener('click', async () => {
        const v = inp.value.trim();
        if (!v) return;
        await onSave?.(v);
        btnSave.textContent = 'Saved';
        btnSave.style.background = '#34a853';
        btnSave.style.borderColor = '#34a853';
        setTimeout(close, 1000);
      });
      btnCancel.addEventListener('click', close);
      btnTest?.addEventListener('click', async () => { await onValidate?.(inp.value.trim()); });
      wrap.addEventListener('click', e => { if (e.target === wrap) close(); });
      shadow.addEventListener('keydown', e => { if (e.key === 'Escape') { e.preventDefault(); close(); } if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); btnSave.click(); } });
      inp.focus();
    } else if (mode === 'domain') {
      const ta = shadow.querySelector('textarea.editable');
      const btnSave = shadow.querySelector('.save');
      const btnCancel = shadow.querySelector('.cancel');
      btnSave.addEventListener('click', async () => { const lines = parseLines(ta.value); await onSave?.(lines); close(); });
      btnCancel.addEventListener('click', close);
      wrap.addEventListener('click', e => { if (e.target === wrap) close(); });
      shadow.addEventListener('keydown', e => { if (e.key === 'Escape') { e.preventDefault(); close(); } if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); btnSave.click(); } });
      ta.focus(); ta.selectionStart = ta.selectionEnd = ta.value.length;
    } else {
      const ta = shadow.querySelector('textarea');
      const btnSave = shadow.querySelector('.save');
      const btnCancel = shadow.querySelector('.cancel');
      btnSave.addEventListener('click', async () => { const lines = parseLines(ta.value); await onSave?.(lines); close(); });
      btnCancel.addEventListener('click', close);
      wrap.addEventListener('click', e => { if (e.target === wrap) close(); });
      shadow.addEventListener('keydown', e => { if (e.key === 'Escape') { e.preventDefault(); close(); } if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); btnSave.click(); } });
      ta.focus(); ta.selectionStart = ta.selectionEnd = ta.value.length;
    }
  }

  function openInfo(message) {
    openEditor({ title: 'Neutralizer', mode: 'info', initial: message, hint: 'Press Enter or Escape to close.' });
  }

  function openKeyDialog(extra) {
    if (apiKeyDialogShown) {
      log('API key dialog already shown, not showing again');
      return;
    }
    apiKeyDialogShown = true;

    openEditor({
      title: 'OpenAI API key',
      mode: 'secret',
      initial: '',
      hint: 'Stored locally (GM → localStorage → memory). Validate sends GET /v1/models.',
      onSave: async (val) => {
        const ok = await storage.set('OPENAI_KEY', val);
        const verify = await storage.get('OPENAI_KEY', '');
        log('API key saved:', ok, verify ? `••••${verify.slice(-4)}` : '(empty)');
        apiKeyDialogShown = false; // Reset so it can be shown again if needed
      },
      onValidate: async (val) => {
        const key = val || await storage.get('OPENAI_KEY', '');
        if (!key) { openInfo('No key to test'); return; }
        try { await xhrGet('https://api.openai.com/v1/models', { Authorization: `Bearer ${key}` }); log('key validation: OK'); openInfo('Validation OK (HTTP 200)'); }
        catch (e) { log('key validation error:', e.message || e); openInfo(`Validation failed: ${e.message || e}`); }
      }
    });
  }

  function openWelcomeDialog() {
    const host = document.createElement('div'); host.setAttribute(UI_ATTR, '');
    const shadow = host.attachShadow({ mode: 'open' });
    const style = document.createElement('style');
    style.textContent = `
      .wrap{position:fixed;inset:0;z-index:2147483647;background:rgba(0,0,0,.55);
            display:flex;align-items:center;justify-content:center}
      .modal{background:#fff;max-width:580px;width:94%;border-radius:12px;
             box-shadow:0 10px 40px rgba(0,0,0,.4);padding:24px;box-sizing:border-box}
      .modal h2{margin:0 0 16px;font:700 20px/1.3 system-ui,sans-serif;color:#1a1a1a}
      .modal p{margin:0 0 12px;font:14px/1.6 system-ui,sans-serif;color:#444}
      .modal .steps{background:#f8f9fa;padding:16px;border-radius:8px;margin:16px 0;
                     font:13px/1.5 system-ui,sans-serif}
      .modal .steps ol{margin:8px 0 0;padding-left:20px}
      .modal .steps li{margin:6px 0}
      .modal .steps a{color:#1a73e8;text-decoration:none}
      .modal .steps a:hover{text-decoration:underline}
      .actions{display:flex;gap:12px;justify-content:flex-end;margin-top:20px}
      .btn{padding:10px 20px;border-radius:8px;border:none;font:600 14px system-ui,sans-serif;
           cursor:pointer;transition:all 0.15s ease}
      .btn.primary{background:#1a73e8;color:#fff}
      .btn.primary:hover{background:#1557b0}
      .btn.secondary{background:#e8eaed;color:#1a1a1a}
      .btn.secondary:hover{background:#dadce0}
    `;
    const wrap = document.createElement('div'); wrap.className = 'wrap';

    wrap.innerHTML = `
      <div class="modal" role="dialog" aria-modal="true" aria-label="Welcome">
        <h2>Welcome to Neutralize Headlines!</h2>
        <p>This userscript helps you browse the web with calmer, more informative headlines by neutralizing sensationalist language.</p>
        <p>To get started, you'll need an OpenAI API key:</p>
        <div class="steps">
          <ol>
            <li>Visit <a href="https://platform.openai.com/api-keys" target="_blank">OpenAI API Keys</a></li>
            <li>Sign in or create an account</li>
            <li>Click "Create new secret key"</li>
            <li>Copy the key and paste it in the next dialog</li>
          </ol>
        </div>
        <p style="font-size:13px;color:#666;margin-top:16px"><strong>Domain control:</strong> By default, all websites are disabled. After setup, you can enable websites one by one via the menu, or toggle to "All domains with Denylist" mode to enable everywhere.</p>
        <p style="font-size:13px;color:#666">The script uses gpt-4o-mini (cost-effective). Your key is stored locally and never shared.</p>
        <div class="actions">
          <button class="btn secondary cancel">Maybe Later</button>
          <button class="btn primary continue">Set Up API Key</button>
        </div>
      </div>`;

    shadow.append(style, wrap);
    document.body.appendChild(host);

    const btnContinue = shadow.querySelector('.continue');
    const btnCancel = shadow.querySelector('.cancel');

    btnContinue.addEventListener('click', async () => {
      host.remove();
      // Open the API key editor
      openEditor({
        title: 'OpenAI API key',
        mode: 'secret',
        initial: '',
        hint: 'Paste your API key here. Click Validate to test it, then Save.',
        onSave: async (val) => {
          await storage.set('OPENAI_KEY', val);
          // Switch to deny mode (enable everywhere) now that we have a key
          await storage.set(DOMAINS_MODE_KEY, 'deny');
          await storage.set(FIRST_INSTALL_KEY, 'true');
          openInfo('API key saved! The script will now work on all websites. Reload any page to see it in action.');
        },
        onValidate: async (val) => {
          const key = val || await storage.get('OPENAI_KEY', '');
          if (!key) { openInfo('Please enter your API key first'); return; }
          try {
            await xhrGet('https://api.openai.com/v1/models', { Authorization: `Bearer ${key}` });
            openInfo('Validation OK! Click Save to continue.');
          }
          catch (e) { openInfo(`Validation failed: ${e.message || e}`); }
        }
      });
    });

    btnCancel.addEventListener('click', async () => {
      host.remove();
      await storage.set(FIRST_INSTALL_KEY, 'true');
      openInfo('You can set up your API key anytime via the userscript menu:\n"Set / Validate OpenAI API key"');
    });

    wrap.addEventListener('click', (e) => { if (e.target === wrap) btnCancel.click(); });
    shadow.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); btnCancel.click(); } });
    // Focus the wrapper to enable keyboard events
    wrap.setAttribute('tabindex', '-1');
    wrap.focus();
  }

  function openTemperatureDialog() {
    const host = document.createElement('div'); host.setAttribute(UI_ATTR, '');
    const shadow = host.attachShadow({ mode: 'open' });
    const style = document.createElement('style');
    style.textContent = `
      .wrap{position:fixed;inset:0;z-index:2147483647;background:rgba(0,0,0,.45);
            display:flex;align-items:center;justify-content:center}
      .modal{background:#fff;max-width:520px;width:92%;border-radius:10px;
             box-shadow:0 10px 40px rgba(0,0,0,.35);padding:20px;box-sizing:border-box}
      .modal h3{margin:0 0 16px;font:600 16px/1.2 system-ui,sans-serif}
      .options{display:flex;flex-direction:column;gap:10px}
      .option-btn{padding:14px 16px;border-radius:8px;border:2px solid #d0d0d0;background:#fff;
                  cursor:pointer;text-align:left;font:14px/1.4 system-ui,sans-serif;
                  transition:all 0.15s ease;display:flex;justify-content:space-between;align-items:center}
      .option-btn:hover{background:#f8f9fa;border-color:#1a73e8}
      .option-btn.selected{background:#e8f0fe;border-color:#1a73e8;font-weight:600}
      .option-btn .label{flex:1}
      .option-btn .value{color:#666;font-size:12px;margin-left:8px}
      .option-btn .checkmark{color:#1a73e8;margin-left:8px;font-weight:bold}
      .hint{margin:16px 0 0;color:#666;font:12px/1.4 system-ui,sans-serif;text-align:center}
    `;
    const wrap = document.createElement('div'); wrap.className = 'wrap';

    const optionsHTML = TEMPERATURE_ORDER.map(level => {
      const isSelected = level === TEMPERATURE_LEVEL;
      const value = TEMPERATURE_LEVELS[level];
      return `<button class="option-btn ${isSelected ? 'selected' : ''}" data-level="${level}">
        <span class="label">${level}</span>
        <span class="value">${value}</span>
        ${isSelected ? '<span class="checkmark">✓</span>' : ''}
      </button>`;
    }).join('');

    wrap.innerHTML = `
      <div class="modal" role="dialog" aria-modal="true" aria-label="Neutralization Strength">
        <h3>Neutralization Strength</h3>
        <div class="options">
          ${optionsHTML}
        </div>
        <p class="hint">Select how aggressively to neutralize headlines. Lower values preserve more of the original meaning.</p>
      </div>`;

    shadow.append(style, wrap);
    document.body.appendChild(host);
    const close = () => host.remove();

    // Add click handlers to all option buttons
    shadow.querySelectorAll('.option-btn').forEach(btn => {
      btn.addEventListener('click', async () => {
        const level = btn.getAttribute('data-level');
        await setTemperature(level);
      });
    });

    wrap.addEventListener('click', e => { if (e.target === wrap) close(); });
    shadow.addEventListener('keydown', e => { if (e.key === 'Escape') { e.preventDefault(); close(); } });
    // Focus the wrapper to enable keyboard events
    wrap.setAttribute('tabindex', '-1');
    wrap.focus();
  }

  function openSimplificationStrengthDialog() {
    const host = document.createElement('div'); host.setAttribute(UI_ATTR, '');
    const shadow = host.attachShadow({ mode: 'open' });
    const style = document.createElement('style');
    style.textContent = `
      .wrap{position:fixed;inset:0;z-index:2147483647;background:rgba(0,0,0,.45);
            display:flex;align-items:center;justify-content:center}
      .modal{background:#fff;max-width:520px;width:92%;border-radius:10px;
             box-shadow:0 10px 40px rgba(0,0,0,.35);padding:20px;box-sizing:border-box}
      .modal h3{margin:0 0 16px;font:600 16px/1.2 system-ui,sans-serif}
      .options{display:flex;flex-direction:column;gap:10px}
      .option-btn{padding:14px 16px;border-radius:8px;border:2px solid #d0d0d0;background:#fff;
                  cursor:pointer;text-align:left;font:14px/1.4 system-ui,sans-serif;
                  transition:all 0.15s ease;display:flex;justify-content:space-between;align-items:center}
      .option-btn:hover{background:#f8f9fa;border-color:#1a73e8}
      .option-btn.selected{background:#e8f0fe;border-color:#1a73e8;font-weight:600}
      .option-btn .label{flex:1}
      .option-btn .value{color:#666;font-size:12px;margin-left:8px}
      .option-btn .checkmark{color:#1a73e8;margin-left:8px;font-weight:bold}
      .hint{margin:16px 0 0;color:#666;font:12px/1.4 system-ui,sans-serif;text-align:center}
    `;
    const wrap = document.createElement('div'); wrap.className = 'wrap';

    const optionsHTML = SIMPLIFICATION_ORDER.map(level => {
      const isSelected = level === SIMPLIFICATION_LEVEL;
      const value = SIMPLIFICATION_LEVELS[level];
      return `<button class="option-btn ${isSelected ? 'selected' : ''}" data-level="${level}">
        <span class="label">${level}</span>
        <span class="value">${value}</span>
        ${isSelected ? '<span class="checkmark">✓</span>' : ''}
      </button>`;
    }).join('');

    wrap.innerHTML = `
      <div class="modal" role="dialog" aria-modal="true" aria-label="Simplification Strength">
        <h3>Simplification Strength</h3>
        <div class="options">
          ${optionsHTML}
        </div>
        <p class="hint">Select how aggressively to simplify article body text. Lower values preserve more of the original style.</p>
      </div>`;

    shadow.append(style, wrap);
    document.body.appendChild(host);
    const close = () => host.remove();

    // Add click handlers to all option buttons
    shadow.querySelectorAll('.option-btn').forEach(btn => {
      btn.addEventListener('click', async () => {
        const level = btn.getAttribute('data-level');
        await setSimplificationStrength(level);
      });
    });

    wrap.addEventListener('click', e => { if (e.target === wrap) close(); });
    shadow.addEventListener('keydown', e => { if (e.key === 'Escape') { e.preventDefault(); close(); } });
    // Focus the wrapper to enable keyboard events
    wrap.setAttribute('tabindex', '-1');
    wrap.focus();
  }

  function openPricingDialog() {
    const host = document.createElement('div'); host.setAttribute(UI_ATTR, '');
    const shadow = host.attachShadow({ mode: 'open' });
    const style = document.createElement('style');
    style.textContent = `
      .wrap{position:fixed;inset:0;z-index:2147483647;background:rgba(0,0,0,.55);
            display:flex;align-items:center;justify-content:center}
      .modal{background:#fff;max-width:520px;width:92%;border-radius:10px;
             box-shadow:0 10px 40px rgba(0,0,0,.35);padding:20px;box-sizing:border-box}
      .modal h3{margin:0 0 16px;font:600 16px/1.2 system-ui,sans-serif}
      .modal p{margin:0 0 12px;font:13px/1.5 system-ui,sans-serif;color:#666}
      .modal .info{background:#f8f9fa;padding:12px;border-radius:6px;margin:12px 0;font-size:12px;color:#444}
      .modal .info a{color:#1a73e8;text-decoration:none}
      .modal .info a:hover{text-decoration:underline}
      .form-group{margin:16px 0}
      .form-group label{display:block;margin-bottom:6px;font:600 13px system-ui,sans-serif;color:#333}
      .form-group input{width:100%;padding:10px;border:2px solid #d0d0d0;border-radius:6px;
                        font:14px system-ui,sans-serif;box-sizing:border-box}
      .form-group input:focus{outline:none;border-color:#1a73e8}
      .form-group .hint{margin-top:4px;font-size:11px;color:#666}
      .actions{display:flex;gap:8px;justify-content:flex-end;margin-top:20px}
      .btn{padding:10px 16px;border-radius:6px;border:none;font:600 13px system-ui,sans-serif;
           cursor:pointer;transition:all 0.15s ease}
      .btn.primary{background:#1a73e8;color:#fff}
      .btn.primary:hover{background:#1557b0}
      .btn.secondary{background:#e8eaed;color:#1a1a1a}
      .btn.secondary:hover{background:#dadce0}
      .btn:disabled{opacity:0.5;cursor:not-allowed}
    `;
    const wrap = document.createElement('div'); wrap.className = 'wrap';

    wrap.innerHTML = `
      <div class="modal" role="dialog" aria-modal="true" aria-label="API Pricing Configuration">
        <h3>API Pricing Configuration</h3>
        <p>Update pricing if OpenAI changes their rates. Current model: ${PRICING.model}</p>
        <div class="info">
          Last updated: ${PRICING.lastUpdated}<br>
          Source: <a href="${PRICING.source}" target="_blank">OpenAI Pricing Page</a>
        </div>
        <div class="form-group">
          <label for="inputPrice">Input tokens (per 1M tokens)</label>
          <input type="number" id="inputPrice" step="0.01" min="0" value="${PRICING.inputPer1M}">
          <div class="hint">USD per 1 million input tokens</div>
        </div>
        <div class="form-group">
          <label for="outputPrice">Output tokens (per 1M tokens)</label>
          <input type="number" id="outputPrice" step="0.01" min="0" value="${PRICING.outputPer1M}">
          <div class="hint">USD per 1 million output tokens</div>
        </div>
        <div class="actions">
          <button class="btn secondary reset">Reset to Defaults</button>
          <button class="btn secondary cancel">Cancel</button>
          <button class="btn primary save">Save</button>
        </div>
      </div>`;

    shadow.append(style, wrap);
    document.body.appendChild(host);
    const close = () => host.remove();

    const inputEl = shadow.querySelector('#inputPrice');
    const outputEl = shadow.querySelector('#outputPrice');
    const btnSave = shadow.querySelector('.save');
    const btnCancel = shadow.querySelector('.cancel');
    const btnReset = shadow.querySelector('.reset');

    btnSave.addEventListener('click', async () => {
      const inputPrice = parseFloat(inputEl.value);
      const outputPrice = parseFloat(outputEl.value);

      if (isNaN(inputPrice) || inputPrice < 0 || isNaN(outputPrice) || outputPrice < 0) {
        alert('Please enter valid positive numbers');
        return;
      }

      await updatePricing({
        inputPer1M: inputPrice,
        outputPer1M: outputPrice
      });

      openInfo(`Pricing updated!\nInput: $${inputPrice}/1M tokens\nOutput: $${outputPrice}/1M tokens`);
      close();
    });

    btnReset.addEventListener('click', async () => {
      if (confirm('Reset pricing to gpt-4o-mini defaults ($0.15 input, $0.60 output per 1M tokens)?')) {
        await resetPricingToDefaults();
        openInfo('Pricing reset to defaults (gpt-4o-mini: $0.15 input, $0.60 output per 1M tokens)');
        close();
      }
    });

    btnCancel.addEventListener('click', close);
    wrap.addEventListener('click', e => { if (e.target === wrap) close(); });
    shadow.addEventListener('keydown', e => { if (e.key === 'Escape') { e.preventDefault(); close(); } });
    // Focus the wrapper to enable keyboard events
    wrap.setAttribute('tabindex', '-1');
    wrap.focus();
  }

  function showLongHeadlineDialog(elements) {
    return new Promise((resolve) => {
      const host = document.createElement('div');
      host.setAttribute(UI_ATTR, '');
      const shadow = host.attachShadow({ mode: 'open' });

      const style = document.createElement('style');
      style.textContent = `
        .wrap { position: fixed; inset: 0; z-index: 2147483647; background: rgba(0,0,0,.55);
                display: flex; align-items: center; justify-content: center; }
        .modal { background: #fff; max-width: 600px; width: 92%; border-radius: 12px;
                 box-shadow: 0 10px 40px rgba(0,0,0,.4); padding: 20px; box-sizing: border-box; }
        .modal h3 { margin: 0 0 16px; font: 700 18px/1.3 system-ui, sans-serif; color: #1a1a1a; }
        .modal p { margin: 0 0 12px; font: 14px/1.6 system-ui, sans-serif; color: #444; }
        .warning { background: #fff3cd; border-left: 4px solid #ffc107; padding: 12px;
                   margin: 16px 0; border-radius: 4px; }
        .warning-icon { font-size: 20px; margin-right: 8px; }
        .element-list { background: #f8f9fa; padding: 12px; border-radius: 6px; margin: 12px 0;
                        max-height: 200px; overflow-y: auto; }
        .element-item { font: 12px/1.5 ui-monospace, Consolas, monospace; color: #666;
                        margin: 6px 0; padding: 6px; background: #fff; border-radius: 4px; }
        .element-length { color: #d32f2f; font-weight: 600; }
        .actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; }
        .btn { padding: 10px 20px; border-radius: 8px; border: none;
               font: 600 14px system-ui, sans-serif; cursor: pointer; transition: all 0.15s ease; }
        .btn.primary { background: #1a73e8; color: #fff; }
        .btn.primary:hover { background: #1557b0; }
        .btn.success { background: #34a853; color: #fff; }
        .btn.success:hover { background: #2d8e47; }
        .btn.secondary { background: #e8eaed; color: #1a1a1a; }
        .btn.secondary:hover { background: #dadce0; }
      `;

      const wrap = document.createElement('div');
      wrap.className = 'wrap';

      const elementListHTML = elements.slice(0, 5).map(({text, length}) => `
        <div class="element-item">
          <span class="element-length">${length} chars:</span> ${escapeHtml(text.substring(0, 80))}${text.length > 80 ? '...' : ''}
        </div>
      `).join('');

      const moreText = elements.length > 5 ? `<p style="text-align:center; font-size:12px; color:#666; margin-top:8px;">...and ${elements.length - 5} more</p>` : '';

      wrap.innerHTML = `
        <div class="modal" role="dialog" aria-modal="true" aria-label="Long Headlines Detected">
          <h3>⚠️ Excessively Long Headlines Detected</h3>

          <div class="warning">
            <span class="warning-icon">⚠️</span>
            <strong>Your manual selectors matched ${elements.length} element(s) with text longer than ${CFG.sanityCheckLen} characters.</strong>
          </div>

          <p>These might be entire paragraphs, navigation menus, or article bodies rather than headlines. Processing them will consume unnecessary API tokens.</p>

          <div class="element-list">
            ${elementListHTML}
            ${moreText}
          </div>

          <p><strong>Would you like to process these anyway?</strong></p>

          <div class="actions">
            <button class="btn secondary skip">Skip These</button>
            <button class="btn primary process-once">Process Once</button>
            <button class="btn success remember">Process & Remember for ${HOST}</button>
          </div>
        </div>
      `;

      shadow.append(style, wrap);
      document.body.appendChild(host);

      const close = (result) => {
        host.remove();
        resolve(result);
      };

      shadow.querySelector('.skip').addEventListener('click', () => close(false));
      shadow.querySelector('.process-once').addEventListener('click', () => close('once'));
      shadow.querySelector('.remember').addEventListener('click', () => close(true));

      wrap.addEventListener('click', e => { if (e.target === wrap) close(false); });
      shadow.addEventListener('keydown', e => { if (e.key === 'Escape') { e.preventDefault(); close(false); } });

      wrap.setAttribute('tabindex', '-1');
      wrap.focus();
    });
  }

  // ───────────────────────── MENUS ─────────────────────────
  // Configuration
  GM_registerMenuCommand?.('--- Configuration ---', () => {});
  GM_registerMenuCommand?.('Set / Validate OpenAI API key', async () => {
    const current = await storage.get('OPENAI_KEY', '');
    openEditor({
      title: 'OpenAI API key',
      mode: 'secret',
      initial: current,
      hint: 'Stored locally (GM → localStorage → memory). Validate sends GET /v1/models.',
      onSave: async (val) => { await storage.set('OPENAI_KEY', val); },
      onValidate: async (val) => {
        const key = val || await storage.get('OPENAI_KEY', '');
        if (!key) { openInfo('No key to test'); return; }
        try { await xhrGet('https://api.openai.com/v1/models', { Authorization: `Bearer ${key}` }); openInfo('Validation OK (HTTP 200)'); }
        catch (e) { openInfo(`Validation failed: ${e.message || e}`); }
      }
    });
  });
  GM_registerMenuCommand?.('Configure API pricing', openPricingDialog);

  // Global settings
  GM_registerMenuCommand?.('Edit GLOBAL target selectors', () => {
    openEditor({
      title: 'Global target selectors (all domains)',
      mode: 'list',
      initial: SELECTORS_GLOBAL,
      hint: 'One CSS selector per line (e.g., h1, h2, h3, .lead). Applied to all domains.',
      onSave: async (lines) => {
        const clean = lines.filter(Boolean).map(s => s.trim()).filter(Boolean);
        SELECTORS_GLOBAL = clean.length ? clean : DEFAULT_SELECTORS.slice();
        await storage.set(SELECTORS_KEY, JSON.stringify(SELECTORS_GLOBAL));
        location.reload();
      }
    });
  });

  GM_registerMenuCommand?.('Edit GLOBAL excludes: elements (self)', () => {
    openEditor({
      title: 'Global excluded elements (all domains)',
      mode: 'list',
      initial: EXCLUDE_GLOBAL.self || [],
      hint: 'One CSS selector per line (e.g., .sponsored, .ad-title). Applied to all domains.',
      onSave: async (lines) => {
        EXCLUDE_GLOBAL.self = lines;
        await storage.set(EXCLUDES_KEY, JSON.stringify(EXCLUDE_GLOBAL));
        location.reload();
      }
    });
  });

  GM_registerMenuCommand?.('Edit GLOBAL excludes: containers (ancestors)', () => {
    openEditor({
      title: 'Global excluded containers (all domains)',
      mode: 'list',
      initial: EXCLUDE_GLOBAL.ancestors || [],
      hint: 'One per line (e.g., header, footer, nav, aside). Applied to all domains.',
      onSave: async (lines) => {
        EXCLUDE_GLOBAL.ancestors = lines;
        await storage.set(EXCLUDES_KEY, JSON.stringify(EXCLUDE_GLOBAL));
        location.reload();
      }
    });
  });

  // Domain-specific settings (additions)
  GM_registerMenuCommand?.(`Edit DOMAIN additions: target selectors (${HOST})`, () => {
    openEditor({
      title: `Domain-specific target selectors for ${HOST}`,
      mode: 'domain',
      initial: SELECTORS_DOMAIN,
      globalItems: SELECTORS_GLOBAL,
      hint: 'Domain-specific selectors are added to global ones. Edit only the bottom section.',
      onSave: async (lines) => {
        DOMAIN_SELECTORS[HOST] = lines;
        await storage.set(DOMAIN_SELECTORS_KEY, JSON.stringify(DOMAIN_SELECTORS));
        location.reload();
      }
    });
  });

  GM_registerMenuCommand?.(`Edit DOMAIN additions: excludes elements (${HOST})`, () => {
    openEditor({
      title: `Domain-specific excluded elements for ${HOST}`,
      mode: 'domain',
      initial: EXCLUDE_DOMAIN.self || [],
      globalItems: EXCLUDE_GLOBAL.self || [],
      hint: 'Domain-specific excludes are added to global ones. Edit only the bottom section.',
      onSave: async (lines) => {
        if (!DOMAIN_EXCLUDES[HOST]) DOMAIN_EXCLUDES[HOST] = { self: [], ancestors: [] };
        DOMAIN_EXCLUDES[HOST].self = lines;
        await storage.set(DOMAIN_EXCLUDES_KEY, JSON.stringify(DOMAIN_EXCLUDES));
        location.reload();
      }
    });
  });

  GM_registerMenuCommand?.(`Edit DOMAIN additions: excludes containers (${HOST})`, () => {
    openEditor({
      title: `Domain-specific excluded containers for ${HOST}`,
      mode: 'domain',
      initial: EXCLUDE_DOMAIN.ancestors || [],
      globalItems: EXCLUDE_GLOBAL.ancestors || [],
      hint: 'Domain-specific excludes are added to global ones. Edit only the bottom section.',
      onSave: async (lines) => {
        if (!DOMAIN_EXCLUDES[HOST]) DOMAIN_EXCLUDES[HOST] = { self: [], ancestors: [] };
        DOMAIN_EXCLUDES[HOST].ancestors = lines;
        await storage.set(DOMAIN_EXCLUDES_KEY, JSON.stringify(DOMAIN_EXCLUDES));
        location.reload();
      }
    });
  });

  // Domain Controls
  GM_registerMenuCommand?.('--- Domain Controls ---', () => {});
  GM_registerMenuCommand?.(
    DOMAINS_MODE === 'allow'
      ? 'Domain mode: Allowlist only'
      : 'Domain mode: All domains with Denylist',
    async () => {
      DOMAINS_MODE = (DOMAINS_MODE === 'allow') ? 'deny' : 'allow';
      await storage.set(DOMAINS_MODE_KEY, DOMAINS_MODE);
      location.reload();
    }
  );
  GM_registerMenuCommand?.(
    computeDomainDisabled(HOST) ? `Current page: DISABLED (click to enable)` : `Current page: ENABLED (click to disable)`,
    async () => {
      if (DOMAINS_MODE === 'allow') {
        // In allowlist mode: toggle presence in allowlist
        if (listMatchesHost(DOMAIN_ALLOW, HOST)) {
          DOMAIN_ALLOW = DOMAIN_ALLOW.filter(p => !domainPatternToRegex(p)?.test(HOST));
        } else {
          DOMAIN_ALLOW.push(HOST);
        }
        await storage.set(DOMAINS_ALLOW_KEY, JSON.stringify(DOMAIN_ALLOW));
      } else {
        // In denylist mode: toggle presence in denylist
        if (computeDomainDisabled(HOST)) {
          DOMAIN_DENY = DOMAIN_DENY.filter(p => !domainPatternToRegex(p)?.test(HOST));
        } else {
          if (!DOMAIN_DENY.includes(HOST)) DOMAIN_DENY.push(HOST);
        }
        await storage.set(DOMAINS_DENY_KEY, JSON.stringify(DOMAIN_DENY));
      }
      location.reload();
    }
  );

  // Toggles
  GM_registerMenuCommand?.('--- Toggles ---', () => {});
  GM_registerMenuCommand?.(`Neutralization strength (${TEMPERATURE_LEVEL})`, openTemperatureDialog);
  GM_registerMenuCommand?.(`Simplification strength (${SIMPLIFICATION_LEVEL})`, openSimplificationStrengthDialog);
  GM_registerMenuCommand?.(`Toggle auto-detect (${CFG.autoDetect ? 'ON' : 'OFF'})`, async () => { await setAutoDetect(!CFG.autoDetect); });
  GM_registerMenuCommand?.(`Toggle body simplification (${SIMPLIFY_BODY ? 'ON' : 'OFF'})`, async () => { await setSimplifyBody(!SIMPLIFY_BODY); });
  GM_registerMenuCommand?.(`Toggle DEBUG logs (${CFG.DEBUG ? 'ON' : 'OFF'})`, async () => { await setDebug(!CFG.DEBUG); });
  GM_registerMenuCommand?.(`Toggle badge (${SHOW_BADGE ? 'ON' : 'OFF'})`, async () => { await setShowBadge(!SHOW_BADGE); });

  // Actions
  GM_registerMenuCommand?.('--- Actions ---', () => {});
  GM_registerMenuCommand?.('Show stats & changes (diff audit)', showDiffAudit);
  GM_registerMenuCommand?.('Process visible now', () => { processVisibleNow(); });
  GM_registerMenuCommand?.('Flush headline cache & rerun', async () => { await cacheClear(); resetAndReindex(); processVisibleNow(); });
  GM_registerMenuCommand?.('Flush body simplification cache', async () => { await bodyCacheClear(); location.reload(); });
  if (LONG_HEADLINE_EXCEPTIONS[HOST]) {
    GM_registerMenuCommand?.(`Clear long headline exception (${HOST})`, async () => {
      if (confirm(`Clear the long headline exception for ${HOST}?\n\nYou'll be prompted again if selectors match text longer than ${CFG.sanityCheckLen} characters.`)) {
        delete LONG_HEADLINE_EXCEPTIONS[HOST];
        await storage.set(LONG_HEADLINE_EXCEPTIONS_KEY, JSON.stringify(LONG_HEADLINE_EXCEPTIONS));
        openInfo(`Cleared long headline exception for ${HOST}.\n\nReload the page to see changes.`);
      }
    });
  }
  GM_registerMenuCommand?.('Reset stats counters', () => { STATS.total = STATS.live = STATS.cache = STATS.batches = 0; CHANGES.length = 0; updateBadgeCounts(); });
  GM_registerMenuCommand?.('Reset API usage stats', async () => { await resetApiTokens(); openInfo('API usage stats reset. Token counters and cost tracking cleared.'); });

  // ───────────────────────── BADGE (Calmed / Restore) + Per-site toggle ─────────────────────────
  let badge, badgeState = 'calmed'; // 'calmed' or 'originals'
  let bodyBadgeState = 'original'; // 'original' or 'simplified'

  function ensureBadge() {
    if ((DOMAIN_DISABLED || OPTED_OUT) || !SHOW_BADGE) return;

    // Check if badge exists and is still in the DOM
    if (badge && badge.isConnected) return;

    // Badge was removed or doesn't exist, recreate it
    badge = document.createElement('div');
    badge.className = 'neutralizer-badge';
    if (BADGE_COLLAPSED) badge.classList.add('collapsed');
    badge.setAttribute(UI_ATTR,'');

    // Set initial position
    const maxX = window.innerWidth - 220;
    const maxY = window.innerHeight - 200;
    BADGE_POS.x = Math.max(0, Math.min(BADGE_POS.x, maxX));
    BADGE_POS.y = Math.max(0, Math.min(BADGE_POS.y, maxY));

    // Always set top position
    badge.style.top = `${BADGE_POS.y}px`;

    // Set horizontal position based on collapse state
    if (BADGE_COLLAPSED) {
      badge.style.right = '0px';
    } else {
      badge.style.left = `${BADGE_POS.x}px`;
    }

    const isArticle = SIMPLIFY_BODY && isArticlePage();
    const bodyRow = isArticle ? `
      <div class="row">
        <button class="btn body-action">B: original</button>
      </div>
    ` : '';

    badge.innerHTML = `
      <div class="badge-handle" title="${BADGE_COLLAPSED ? 'Open' : 'Close'}">${BADGE_COLLAPSED ? '◀' : '▶'}</div>
      <div class="badge-header">NEUTRALIZE HEADLINES</div>
      <div class="badge-content">
        <div class="row">
          <button class="btn primary action">H: neutral</button>
        </div>
        ${bodyRow}
        <div class="row">
          <button class="btn inspect" title="Click to inspect any element on the page">🔍 Inspect</button>
        </div>
      </div>
    `;
    document.body.appendChild(badge);
    badge.querySelector('.action').addEventListener('click', onBadgeAction);
    badge.querySelector('.badge-handle').addEventListener('click', toggleBadgeCollapse);
    badge.querySelector('.badge-header').addEventListener('mousedown', startBadgeDrag);
    badge.querySelector('.inspect').addEventListener('click', enterInspectionMode);
    if (isArticle) {
      badge.querySelector('.body-action')?.addEventListener('click', onBodyBadgeAction);
    }
  }

  // ───────────────────────── INSPECTION MODE ─────────────────────────
  let inspectionOverlay = null;
  let inspectedElement = null;

  function enterInspectionMode() {
    if (inspectionOverlay) return; // Already in inspection mode

    // Create overlay
    const overlay = document.createElement('div');
    overlay.setAttribute(UI_ATTR, '');
    overlay.style.cssText = `
      position: fixed; inset: 0; z-index: 2147483645;
      background: rgba(0, 0, 0, 0.3);
      font-family: system-ui, sans-serif;
      pointer-events: none;
    `;

    // Add instruction message
    const message = document.createElement('div');
    message.setAttribute(UI_ATTR, '');
    message.style.cssText = `
      position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
      background: #1a73e8; color: white; padding: 12px 24px;
      border-radius: 8px; font-size: 14px; font-weight: 600;
      box-shadow: 0 4px 20px rgba(0,0,0,0.3); z-index: 2147483646;
      pointer-events: none;
    `;
    message.textContent = '🔍 Inspection Mode - Click any element to analyze. ESC to exit.';

    // Set crosshair cursor on body
    const originalCursor = document.body.style.cursor;
    document.body.style.cursor = 'crosshair';

    document.body.appendChild(overlay);
    document.body.appendChild(message);
    inspectionOverlay = { overlay, message, originalCursor };

    // Hover highlight
    let currentHighlight = null;
    const onMouseMove = (e) => {
      // Find element under cursor (overlay is pointer-events:none so this works)
      const target = document.elementFromPoint(e.clientX, e.clientY);
      if (!target || target.closest(`[${UI_ATTR}]`)) return; // Skip our own UI

      // Remove previous highlight
      if (currentHighlight && currentHighlight !== target) {
        currentHighlight.style.outline = currentHighlight._origOutline || '';
        currentHighlight.style.outlineOffset = currentHighlight._origOutlineOffset || '';
        delete currentHighlight._origOutline;
        delete currentHighlight._origOutlineOffset;
      }

      // Add new highlight
      if (currentHighlight !== target) {
        currentHighlight = target;
        currentHighlight._origOutline = currentHighlight.style.outline;
        currentHighlight._origOutlineOffset = currentHighlight.style.outlineOffset;
        currentHighlight.style.outline = '2px dashed #1a73e8';
        currentHighlight.style.outlineOffset = '2px';
      }
    };

    // Click handler
    const onClick = (e) => {
      // Find element under cursor
      const target = document.elementFromPoint(e.clientX, e.clientY);
      if (!target || target.closest(`[${UI_ATTR}]`)) return; // Skip our own UI

      e.preventDefault();
      e.stopPropagation();

      inspectedElement = target;

      // Permanent highlight
      if (currentHighlight) {
        currentHighlight.style.outline = currentHighlight._origOutline || '';
        currentHighlight.style.outlineOffset = currentHighlight._origOutlineOffset || '';
        delete currentHighlight._origOutline;
        delete currentHighlight._origOutlineOffset;
      }

      inspectedElement._origOutline = inspectedElement.style.outline;
      inspectedElement._origOutlineOffset = inspectedElement.style.outlineOffset;
      inspectedElement.style.outline = '3px solid #ea4335';
      inspectedElement.style.outlineOffset = '2px';

      exitInspectionMode();
      showDiagnosticDialog(inspectedElement);
    };

    // ESC handler
    const onKeyDown = (e) => {
      if (e.key === 'Escape') {
        e.preventDefault();
        exitInspectionMode();
      }
    };

    document.body.addEventListener('mousemove', onMouseMove, true);
    document.addEventListener('click', onClick, true);
    document.addEventListener('keydown', onKeyDown);

    // Store handlers for cleanup
    inspectionOverlay.onMouseMove = onMouseMove;
    inspectionOverlay.onClick = onClick;
    inspectionOverlay.onKeyDown = onKeyDown;
    inspectionOverlay.currentHighlight = () => currentHighlight;
  }

  function exitInspectionMode() {
    if (!inspectionOverlay) return;

    const { overlay, message, onMouseMove, onClick, onKeyDown, currentHighlight, originalCursor } = inspectionOverlay;

    // Remove highlight
    const el = currentHighlight?.();
    if (el) {
      el.style.outline = el._origOutline || '';
      el.style.outlineOffset = el._origOutlineOffset || '';
      delete el._origOutline;
      delete el._origOutlineOffset;
    }

    // Restore original cursor
    document.body.style.cursor = originalCursor;

    // Remove event listeners
    document.body.removeEventListener('mousemove', onMouseMove, true);
    document.removeEventListener('click', onClick, true);
    document.removeEventListener('keydown', onKeyDown);

    // Remove DOM elements
    overlay.remove();
    message.remove();

    inspectionOverlay = null;
  }

  function diagnoseElement(el) {
    const text = textTrim(el);
    const selector = generateCSSSelector(el);

    // Check auto-detection
    const autoDetect = {
      matched: false,
      reasons: []
    };

    if (el.closest(`[${UI_ATTR}]`)) {
      autoDetect.reasons.push('Part of script\'s own UI');
    } else if (isEditable(el)) {
      autoDetect.reasons.push('Editable element (input/textarea)');
    } else {
      // Check headline heuristics
      const cardParent = el.closest(CARD_SELECTOR);
      if (cardParent) autoDetect.reasons.push('✓ Inside card/article container');

      const tag = el.tagName.toLowerCase();
      if (['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a'].includes(tag)) {
        autoDetect.reasons.push(`✓ Headline tag: <${tag}>`);
      }

      if (withinLen(text)) {
        autoDetect.reasons.push(`✓ Length OK (${text.length} chars)`);
      } else {
        autoDetect.reasons.push(`✗ Length ${text.length} chars (need ${CFG.minLen}-${CFG.maxLen})`);
      }

      if (isLikelyKicker(el, text)) {
        autoDetect.reasons.push('✗ Detected as kicker/label (excluded)');
      }

      if (el.closest(UI_CONTAINERS)) {
        autoDetect.reasons.push('✗ Inside UI container (meta/byline/tools)');
      }

      // Simplified auto-detection check
      autoDetect.matched = cardParent && ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'a'].includes(tag) &&
                          withinLen(text) && !isLikelyKicker(el, text) && !el.closest(UI_CONTAINERS);
    }

    // Check global selectors
    const globalSelectors = findMatchingSelectors(el, SELECTORS_GLOBAL);

    // Check domain selectors
    const domainSelectors = findMatchingSelectors(el, SELECTORS_DOMAIN);

    // Check global exclusions
    const globalExclusions = findMatchingExclusions(el, EXCLUDE_GLOBAL);

    // Check domain exclusions
    const domainExclusions = findMatchingExclusions(el, EXCLUDE_DOMAIN);

    // Publisher opt-out
    const hasOptOut = document.querySelector('meta[name="neutralizer"][content="opt-out"]') !== null;

    // Final determination
    const isProcessed = !isExcluded(el) && (autoDetect.matched || globalSelectors.length > 0 || domainSelectors.length > 0);

    return {
      element: el,
      selector,
      text: text.substring(0, 100) + (text.length > 100 ? '...' : ''),
      tag: el.tagName.toLowerCase(),
      classes: Array.from(el.classList).join(' '),
      id: el.id,
      autoDetect,
      globalSelectors,
      domainSelectors,
      globalExclusions,
      domainExclusions,
      hasOptOut,
      isExcluded: isExcluded(el),
      isProcessed
    };
  }

  function findMatchingSelectors(el, selectorList) {
    const matches = [];
    for (const sel of selectorList) {
      try {
        if (el.matches(sel)) {
          matches.push(sel);
        }
      } catch (e) {
        // Invalid selector, skip
      }
    }
    return matches;
  }

  function findMatchingExclusions(el, excludeObj) {
    const matches = { self: [], ancestors: [] };

    if (excludeObj.self) {
      for (const sel of excludeObj.self) {
        try {
          if (el.matches(sel)) {
            matches.self.push(sel);
          }
        } catch (e) {
          // Invalid selector, skip
        }
      }
    }

    if (excludeObj.ancestors) {
      for (const sel of excludeObj.ancestors) {
        try {
          if (el.closest(sel)) {
            matches.ancestors.push(sel);
          }
        } catch (e) {
          // Invalid selector, skip
        }
      }
    }

    return matches;
  }

  function generateCSSSelector(el) {
    // Try ID first
    if (el.id) {
      return `#${CSS.escape(el.id)}`;
    }

    // Try classes
    if (el.classList.length > 0) {
      const classes = Array.from(el.classList).map(c => `.${CSS.escape(c)}`).join('');
      return `${el.tagName.toLowerCase()}${classes}`;
    }

    // Fallback to tag + nth-child
    let path = [];
    let current = el;
    while (current && current !== document.body) {
      let selector = current.tagName.toLowerCase();

      if (current.id) {
        selector = `#${CSS.escape(current.id)}`;
        path.unshift(selector);
        break;
      }

      let sibling = current;
      let nth = 1;
      while (sibling.previousElementSibling) {
        sibling = sibling.previousElementSibling;
        if (sibling.tagName === current.tagName) nth++;
      }

      if (nth > 1 || current.nextElementSibling) {
        selector += `:nth-child(${nth})`;
      }

      path.unshift(selector);
      current = current.parentElement;

      if (path.length > 3) break; // Keep it reasonably short
    }

    return path.join(' > ');
  }

  function showDiagnosticDialog(el) {
    const diag = diagnoseElement(el);

    const host = document.createElement('div');
    host.setAttribute(UI_ATTR, '');
    const shadow = host.attachShadow({ mode: 'open' });

    const style = document.createElement('style');
    style.textContent = `
      .wrap { position: fixed; inset: 0; z-index: 2147483647; background: rgba(0,0,0,.55);
              display: flex; align-items: center; justify-content: center; }
      .modal { background: #fff; max-width: 700px; width: 94%; max-height: 90vh;
               overflow-y: auto; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,.4);
               padding: 20px; box-sizing: border-box; }
      .modal h3 { margin: 0 0 16px; font: 700 18px/1.3 system-ui, sans-serif; color: #1a1a1a; }
      .section { margin: 16px 0; padding: 12px; background: #f8f9fa; border-radius: 8px; }
      .section-title { font: 600 14px system-ui, sans-serif; margin: 0 0 8px; color: #444; }
      .info-row { margin: 4px 0; font: 13px/1.5 ui-monospace, Consolas, monospace; color: #666; }
      .info-label { font-weight: 600; color: #333; }
      .status { display: inline-block; padding: 4px 12px; border-radius: 6px; font: 600 13px system-ui, sans-serif;
                margin: 8px 0; }
      .status.processed { background: #d4edda; color: #155724; }
      .status.not-processed { background: #f8d7da; color: #721c24; }
      .status.excluded { background: #fff3cd; color: #856404; }
      .list { margin: 8px 0; padding-left: 20px; }
      .list li { margin: 4px 0; font: 13px/1.5 system-ui, sans-serif; }
      .list li.match { color: #155724; }
      .list li.no-match { color: #666; }
      .list li.problem { color: #721c24; font-weight: 600; }
      .code { background: #f1f3f4; padding: 2px 6px; border-radius: 4px;
              font: 12px ui-monospace, Consolas, monospace; color: #333; }
      .actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 20px; }
      .btn { padding: 10px 16px; border-radius: 8px; border: none;
             font: 600 13px system-ui, sans-serif; cursor: pointer; transition: all 0.15s ease; }
      .btn.primary { background: #1a73e8; color: #fff; }
      .btn.primary:hover { background: #1557b0; }
      .btn.secondary { background: #e8eaed; color: #1a1a1a; }
      .btn.secondary:hover { background: #dadce0; }
      .btn.success { background: #34a853; color: #fff; }
      .btn.success:hover { background: #2d8e47; }
      .btn.danger { background: #ea4335; color: #fff; }
      .btn.danger:hover { background: #d33426; }
    `;

    const wrap = document.createElement('div');
    wrap.className = 'wrap';

    // Build status message
    let statusClass, statusText;
    if (diag.isExcluded) {
      statusClass = 'excluded';
      statusText = '⚠️ EXCLUDED - Not being processed';
    } else if (diag.isProcessed) {
      statusClass = 'processed';
      statusText = '✅ MATCHED - Being processed';
    } else {
      statusClass = 'not-processed';
      statusText = '❌ NOT MATCHED - Not being processed';
    }

    // Build auto-detection section
    const autoDetectHTML = diag.autoDetect.reasons.length > 0 ?
      `<ul class="list">${diag.autoDetect.reasons.map(r => `<li class="${r.startsWith('✓') ? 'match' : 'no-match'}">${r}</li>`).join('')}</ul>` :
      '<p class="info-row">No auto-detection analysis available.</p>';

    // Build selectors section
    const globalSelectorsHTML = diag.globalSelectors.length > 0 ?
      `<ul class="list">${diag.globalSelectors.map(s => `<li class="match">✓ <span class="code">${escapeHtml(s)}</span></li>`).join('')}</ul>` :
      '<p class="info-row no-match">No global selectors match this element.</p>';

    const domainSelectorsHTML = diag.domainSelectors.length > 0 ?
      `<ul class="list">${diag.domainSelectors.map(s => `<li class="match">✓ <span class="code">${escapeHtml(s)}</span></li>`).join('')}</ul>` :
      '<p class="info-row no-match">No domain selectors configured or matched.</p>';

    // Build exclusions section
    const globalExclusionsHTML = (diag.globalExclusions.self.length > 0 || diag.globalExclusions.ancestors.length > 0) ?
      `<ul class="list">
        ${diag.globalExclusions.self.map(s => `<li class="problem">✗ Element matches: <span class="code">${escapeHtml(s)}</span></li>`).join('')}
        ${diag.globalExclusions.ancestors.map(s => `<li class="problem">✗ Ancestor matches: <span class="code">${escapeHtml(s)}</span></li>`).join('')}
      </ul>` :
      '<p class="info-row no-match">No global exclusions affect this element.</p>';

    const domainExclusionsHTML = (diag.domainExclusions.self?.length > 0 || diag.domainExclusions.ancestors?.length > 0) ?
      `<ul class="list">
        ${(diag.domainExclusions.self || []).map(s => `<li class="problem">✗ Element matches: <span class="code">${escapeHtml(s)}</span></li>`).join('')}
        ${(diag.domainExclusions.ancestors || []).map(s => `<li class="problem">✗ Ancestor matches: <span class="code">${escapeHtml(s)}</span></li>`).join('')}
      </ul>` :
      '<p class="info-row no-match">No domain exclusions configured or affect this element.</p>';

    wrap.innerHTML = `
      <div class="modal" role="dialog" aria-modal="true" aria-label="Element Inspection">
        <h3>🔍 Element Inspection</h3>

        <div class="section">
          <div class="section-title">Element Information</div>
          <div class="info-row"><span class="info-label">Tag:</span> &lt;${diag.tag}&gt;</div>
          <div class="info-row"><span class="info-label">ID:</span> ${diag.id || '(none)'}</div>
          <div class="info-row"><span class="info-label">Classes:</span> ${diag.classes || '(none)'}</div>
          <div class="info-row"><span class="info-label">Text:</span> "${escapeHtml(diag.text)}"</div>
          <div class="info-row"><span class="info-label">CSS Selector:</span> <span class="code">${escapeHtml(diag.selector)}</span></div>
        </div>

        <div class="status ${statusClass}">${statusText}</div>

        <div class="section">
          <div class="section-title">Auto-Detection Analysis</div>
          ${autoDetectHTML}
        </div>

        <div class="section">
          <div class="section-title">Global Selectors</div>
          ${globalSelectorsHTML}
        </div>

        <div class="section">
          <div class="section-title">Domain Selectors (${HOST})</div>
          ${domainSelectorsHTML}
        </div>

        <div class="section">
          <div class="section-title">Global Exclusions</div>
          ${globalExclusionsHTML}
        </div>

        <div class="section">
          <div class="section-title">Domain Exclusions (${HOST})</div>
          ${domainExclusionsHTML}
        </div>

        ${diag.hasOptOut ? '<div class="section"><div class="section-title">⚠️ Publisher Opt-Out Detected</div><p class="info-row">This page has requested to opt-out from neutralization.</p></div>' : ''}

        <div class="actions">
          ${buildActionButtons(diag)}
          <button class="btn secondary copy-selector">📋 Copy Selector</button>
          <button class="btn secondary close">Close</button>
        </div>
      </div>
    `;

    shadow.append(style, wrap);
    document.body.appendChild(host);

    const close = () => {
      // Remove element highlight
      if (inspectedElement) {
        inspectedElement.style.outline = inspectedElement._origOutline || '';
        inspectedElement.style.outlineOffset = inspectedElement._origOutlineOffset || '';
        delete inspectedElement._origOutline;
        delete inspectedElement._origOutlineOffset;
        inspectedElement = null;
      }
      host.remove();
    };

    // Close button
    shadow.querySelector('.close').addEventListener('click', close);
    wrap.addEventListener('click', e => { if (e.target === wrap) close(); });
    shadow.addEventListener('keydown', e => { if (e.key === 'Escape') { e.preventDefault(); close(); } });

    // Copy selector button
    shadow.querySelector('.copy-selector').addEventListener('click', () => {
      navigator.clipboard.writeText(diag.selector).then(() => {
        const btn = shadow.querySelector('.copy-selector');
        const orig = btn.textContent;
        btn.textContent = '✓ Copied!';
        setTimeout(() => btn.textContent = orig, 2000);
      });
    });

    // Action buttons
    attachActionHandlers(shadow, diag, close);

    // Focus
    wrap.setAttribute('tabindex', '-1');
    wrap.focus();
  }

  function buildActionButtons(diag) {
    const buttons = [];

    // If excluded by global exclusions, offer to remove them
    if (diag.globalExclusions.self.length > 0) {
      diag.globalExclusions.self.forEach(sel => {
        buttons.push(`<button class="btn danger remove-global-excl-self" data-selector="${escapeHtml(sel)}">Remove Global Exclusion: ${escapeHtml(sel)}</button>`);
      });
    }
    if (diag.globalExclusions.ancestors.length > 0) {
      diag.globalExclusions.ancestors.forEach(sel => {
        buttons.push(`<button class="btn danger remove-global-excl-anc" data-selector="${escapeHtml(sel)}">Remove Global Ancestor Exclusion: ${escapeHtml(sel)}</button>`);
      });
    }

    // If excluded by domain exclusions, offer to remove them
    if (diag.domainExclusions.self?.length > 0) {
      diag.domainExclusions.self.forEach(sel => {
        buttons.push(`<button class="btn danger remove-domain-excl-self" data-selector="${escapeHtml(sel)}">Remove Domain Exclusion: ${escapeHtml(sel)}</button>`);
      });
    }
    if (diag.domainExclusions.ancestors?.length > 0) {
      diag.domainExclusions.ancestors.forEach(sel => {
        buttons.push(`<button class="btn danger remove-domain-excl-anc" data-selector="${escapeHtml(sel)}">Remove Domain Ancestor Exclusion: ${escapeHtml(sel)}</button>`);
      });
    }

    // If not matched by any selectors and not auto-detected, offer to add as selector
    if (!diag.isProcessed && !diag.isExcluded) {
      buttons.push(`<button class="btn success add-global-sel">Add as Global Selector</button>`);
      buttons.push(`<button class="btn success add-domain-sel">Add as Domain Selector</button>`);
    }

    return buttons.join('');
  }

  function attachActionHandlers(shadow, diag, closeDialog) {
    // Remove global exclusions
    shadow.querySelectorAll('.remove-global-excl-self').forEach(btn => {
      btn.addEventListener('click', async () => {
        const sel = btn.getAttribute('data-selector');
        EXCLUDE_GLOBAL.self = EXCLUDE_GLOBAL.self.filter(s => s !== sel);
        await storage.set(EXCLUDES_KEY, JSON.stringify(EXCLUDE_GLOBAL));
        EXCLUDE.self = [...EXCLUDE_GLOBAL.self, ...(EXCLUDE_DOMAIN.self || [])];
        openInfo(`Removed global exclusion: ${sel}\nReload the page to see changes.`);
        closeDialog();
      });
    });

    shadow.querySelectorAll('.remove-global-excl-anc').forEach(btn => {
      btn.addEventListener('click', async () => {
        const sel = btn.getAttribute('data-selector');
        EXCLUDE_GLOBAL.ancestors = EXCLUDE_GLOBAL.ancestors.filter(s => s !== sel);
        await storage.set(EXCLUDES_KEY, JSON.stringify(EXCLUDE_GLOBAL));
        EXCLUDE.ancestors = [...EXCLUDE_GLOBAL.ancestors, ...(EXCLUDE_DOMAIN.ancestors || [])];
        openInfo(`Removed global ancestor exclusion: ${sel}\nReload the page to see changes.`);
        closeDialog();
      });
    });

    // Remove domain exclusions
    shadow.querySelectorAll('.remove-domain-excl-self').forEach(btn => {
      btn.addEventListener('click', async () => {
        const sel = btn.getAttribute('data-selector');
        if (DOMAIN_EXCLUDES[HOST]) {
          DOMAIN_EXCLUDES[HOST].self = (DOMAIN_EXCLUDES[HOST].self || []).filter(s => s !== sel);
          await storage.set(DOMAIN_EXCLUDES_KEY, JSON.stringify(DOMAIN_EXCLUDES));
          EXCLUDE_DOMAIN.self = DOMAIN_EXCLUDES[HOST].self || [];
          EXCLUDE.self = [...EXCLUDE_GLOBAL.self, ...EXCLUDE_DOMAIN.self];
        }
        openInfo(`Removed domain exclusion: ${sel}\nReload the page to see changes.`);
        closeDialog();
      });
    });

    shadow.querySelectorAll('.remove-domain-excl-anc').forEach(btn => {
      btn.addEventListener('click', async () => {
        const sel = btn.getAttribute('data-selector');
        if (DOMAIN_EXCLUDES[HOST]) {
          DOMAIN_EXCLUDES[HOST].ancestors = (DOMAIN_EXCLUDES[HOST].ancestors || []).filter(s => s !== sel);
          await storage.set(DOMAIN_EXCLUDES_KEY, JSON.stringify(DOMAIN_EXCLUDES));
          EXCLUDE_DOMAIN.ancestors = DOMAIN_EXCLUDES[HOST].ancestors || [];
          EXCLUDE.ancestors = [...EXCLUDE_GLOBAL.ancestors, ...EXCLUDE_DOMAIN.ancestors];
        }
        openInfo(`Removed domain ancestor exclusion: ${sel}\nReload the page to see changes.`);
        closeDialog();
      });
    });

    // Add as global selector
    shadow.querySelectorAll('.add-global-sel').forEach(btn => {
      btn.addEventListener('click', async () => {
        SELECTORS_GLOBAL.push(diag.selector);
        await storage.set(SELECTORS_KEY, JSON.stringify(SELECTORS_GLOBAL));
        SELECTORS = [...SELECTORS_GLOBAL, ...SELECTORS_DOMAIN];
        openInfo(`Added global selector: ${diag.selector}\nReload the page to see changes.`);
        closeDialog();
      });
    });

    // Add as domain selector
    shadow.querySelectorAll('.add-domain-sel').forEach(btn => {
      btn.addEventListener('click', async () => {
        if (!DOMAIN_SELECTORS[HOST]) DOMAIN_SELECTORS[HOST] = [];
        DOMAIN_SELECTORS[HOST].push(diag.selector);
        await storage.set(DOMAIN_SELECTORS_KEY, JSON.stringify(DOMAIN_SELECTORS));
        SELECTORS_DOMAIN = DOMAIN_SELECTORS[HOST] || [];
        SELECTORS = [...SELECTORS_GLOBAL, ...SELECTORS_DOMAIN];
        openInfo(`Added domain selector: ${diag.selector}\nReload the page to see changes.`);
        closeDialog();
      });
    });
  }

  // ───────────────────────── BADGE DRAGGING ─────────────────────────
  let isBadgeDragging = false;
  let badgeDragOffset = { x: 0, y: 0 };

  function startBadgeDrag(e) {
    // Don't drag if collapsed
    if (BADGE_COLLAPSED) return;

    isBadgeDragging = true;
    badge.classList.add('dragging');

    const rect = badge.getBoundingClientRect();
    badgeDragOffset.x = e.clientX - rect.left;
    badgeDragOffset.y = e.clientY - rect.top;

    document.addEventListener('mousemove', onBadgeDrag);
    document.addEventListener('mouseup', stopBadgeDrag);

    e.preventDefault();
  }

  function onBadgeDrag(e) {
    if (!isBadgeDragging) return;

    let newX = e.clientX - badgeDragOffset.x;
    let newY = e.clientY - badgeDragOffset.y;

    // Constrain to viewport
    const maxX = window.innerWidth - badge.offsetWidth;
    const maxY = window.innerHeight - badge.offsetHeight;
    newX = Math.max(0, Math.min(newX, maxX));
    newY = Math.max(0, Math.min(newY, maxY));

    badge.style.left = `${newX}px`;
    badge.style.top = `${newY}px`;

    BADGE_POS = { x: newX, y: newY };
  }

  function stopBadgeDrag() {
    if (!isBadgeDragging) return;

    isBadgeDragging = false;
    badge.classList.remove('dragging');

    document.removeEventListener('mousemove', onBadgeDrag);
    document.removeEventListener('mouseup', stopBadgeDrag);

    storage.set(BADGE_POS_KEY, JSON.stringify(BADGE_POS));
  }
  async function toggleBadgeCollapse() {
    BADGE_COLLAPSED = !BADGE_COLLAPSED;
    await storage.set(BADGE_COLLAPSED_KEY, String(BADGE_COLLAPSED));

    const currentY = parseInt(badge.style.top) || BADGE_POS.y;

    if (BADGE_COLLAPSED) {
      badge.classList.add('collapsed');
      // Position on right edge for collapse animation
      badge.style.left = '';
      badge.style.right = '0px';
      badge.style.top = `${currentY}px`;
    } else {
      badge.classList.remove('collapsed');
      // Restore to saved position or default near right edge
      const rightEdgeX = BADGE_POS.x || (window.innerWidth - 220);

      badge.style.right = '';
      badge.style.left = `${rightEdgeX}px`;
      badge.style.top = `${currentY}px`;

      // Update saved position
      BADGE_POS = { x: rightEdgeX, y: currentY };
      storage.set(BADGE_POS_KEY, JSON.stringify(BADGE_POS));
    }

    const handle = badge.querySelector('.badge-handle');
    if (handle) {
      handle.title = BADGE_COLLAPSED ? 'Open' : 'Close';
      handle.textContent = BADGE_COLLAPSED ? '◀' : '▶';
    }
  }

  function onBadgeAction() {
    if (badgeState === 'calmed') {
      restoreOriginals();
      badgeState = 'originals';
      badge.querySelector('.action').textContent = 'H: original';
    } else {
      reapplyFromCache();
      badgeState = 'calmed';
      badge.querySelector('.action').textContent = 'H: neutral';
    }
  }

  function onBodyBadgeAction() {
    const btn = badge.querySelector('.body-action');
    if (bodyBadgeState === 'original') {
      // Simplify body
      btn.textContent = 'B: starting...';
      btn.disabled = true;
      applyBodySimplification(true).then(() => {
        // Badge state is synced inside applyBodySimplification
        btn.disabled = false;
      }).catch(err => {
        log('Body simplification error:', err);
        btn.textContent = 'B: original';
        btn.disabled = false;
      });
    } else {
      // Restore original
      restoreBodyOriginals();
      syncBodyBadgeState();
    }
  }

  function updateBadgeCounts() {
    // Counts display removed from badge
  }

  function syncBodyBadgeState() {
    if (!badge) return;
    const btn = badge.querySelector('.body-action');
    if (!btn) return;

    if (bodySimplified) {
      bodyBadgeState = 'simplified';
      btn.textContent = 'B: simplified';
    } else {
      bodyBadgeState = 'original';
      btn.textContent = 'B: original';
    }
  }

  function restoreOriginals() {
    const els = document.querySelectorAll('[data-neutralizer-changed="1"][data-neutralizer-original]');
    let n = 0;
    els.forEach(el => {
      const orig = el.getAttribute('data-neutralizer-original');
      if (typeof orig === 'string') { el.textContent = orig; n++; }
    });
    log('restored originals on', n, 'elements');
  }
  function reapplyFromCache() {
    seenEl = new WeakSet();
    // Apply cached rewrites to all elements
    for (const [text, set] of textToElements.entries()) {
      const cached = cacheGet(text);
      if (cached) {
        applyRewrites(buildMap([text]), [text], [cached], 'cache');
      }
    }
  }

  // ───────────────────────── UTILITIES ─────────────────────────
  function parseRootMarginPxY() { const m=(CFG.rootMargin||'0px').trim().split(/\s+/); const top=m[0]||'0px'; const val=parseFloat(top); return isNaN(val)?0:val; }
  function isInViewportWithMargin(el) { const rect=el.getBoundingClientRect(); const vh=window.innerHeight||document.documentElement.clientHeight; const m=parseRootMarginPxY(); return rect.bottom>=-m && rect.top<=vh+m; }

  function processVisibleNow() {
    for (const [text, set] of textToElements.entries()) {
      if (cacheGet(text)) continue;
      const el = set.values().next().value;
      if (!el) continue;
      if (CFG.visibleOnly ? isInViewportWithMargin(el) : true) pending.add(text);
    }
    scheduleFlush();
  }

  function resetAndReindex() {
    pending.clear(); if (flushTimer) clearTimeout(flushTimer); flushTimer = null;
    textToElements.clear(); seenEl = new WeakSet();
    if (IO) { IO.disconnect(); IO = null; }
    if (!(DOMAIN_DISABLED || OPTED_OUT)) { ensureObserver(); attachTargets(document); }
  }

  // ───────────────────────── DIFF AUDIT ─────────────────────────
  function showDiffAudit() {
    const host = document.createElement('div'); host.setAttribute(UI_ATTR,'');
    const shadow = host.attachShadow({ mode: 'open' });
    const style = document.createElement('style');
    style.textContent = `
      .wrap { position:fixed; inset:0; z-index:2147483647; background:rgba(0,0,0,.45);
              display:flex; align-items:center; justify-content:center; }
      .modal { background:#fff; max-width:900px; width:94%; border-radius:10px;
               box-shadow:0 10px 40px rgba(0,0,0,.4); padding:14px; box-sizing:border-box; }
      h3, h4 { margin:0 0 8px; font:600 16px/1.2 system-ui,sans-serif; }
      h4 { font-size:14px; }
      .list { max-height:70vh; overflow:auto; }
      .row { border-top:1px solid #eee; padding:8px 2px; }
      .from { color:#666; }
      .to { color:#111; font-weight:600; }
      .meta { color:#888; font-size:11px; }
      .btn { padding:10px 16px; border-radius:6px; border:none;
             font:600 13px system-ui,sans-serif; cursor:pointer;
             background:#e8eaed; color:#1a1a1a; }
      .btn:hover { background:#dadce0; }
    `;
    const wrap = document.createElement('div'); wrap.className = 'wrap';
    const modal = document.createElement('div'); modal.className = 'modal';
    const list = document.createElement('div'); list.className = 'list';

    // Add cache stats section
    const headlineCacheSize = Object.keys(CACHE).length;
    const bodyCacheSize = Object.keys(BODY_CACHE).length;
    const bodyCacheArticles = Object.values(BODY_CACHE).map(c => `${c.url} (${c.count}p)`).join('<br>');

    // Calculate API usage stats
    const totalInput = API_TOKENS.headlines.input + API_TOKENS.body.input;
    const totalOutput = API_TOKENS.headlines.output + API_TOKENS.body.output;
    const totalTokens = totalInput + totalOutput;
    const totalCalls = API_TOKENS.headlines.calls + API_TOKENS.body.calls;
    const estimatedCost = calculateApiCost();

    modal.innerHTML = `
      <h3>Stats & Changes (this page)</h3>
      <div style="background:#f5f5f5;padding:10px;border-radius:6px;margin-bottom:12px;font-size:13px;">
        <strong>Cache Stats:</strong><br>
        Headlines cached: ${headlineCacheSize} entries<br>
        Body simplifications cached: ${bodyCacheSize} articles<br>
        ${bodyCacheSize > 0 ? `<details style="margin-top:6px"><summary style="cursor:pointer">Cached articles</summary><div style="font-size:11px;margin-top:4px;max-height:100px;overflow-y:auto">${bodyCacheArticles}</div></details>` : ''}
      </div>
      <div style="background:#e8f4fd;padding:10px;border-radius:6px;margin-bottom:12px;font-size:13px;border-left:3px solid #1a73e8;">
        <strong>API Usage (since install):</strong><br>
        Total API calls: ${totalCalls.toLocaleString()}<br>
        Total tokens: ${totalTokens.toLocaleString()} (${totalInput.toLocaleString()} input, ${totalOutput.toLocaleString()} output)<br>
        <details style="margin-top:6px">
          <summary style="cursor:pointer">Breakdown by feature</summary>
          <div style="font-size:12px;margin-top:4px">
            <strong>Headlines:</strong> ${API_TOKENS.headlines.calls.toLocaleString()} calls, ${(API_TOKENS.headlines.input + API_TOKENS.headlines.output).toLocaleString()} tokens<br>
            <strong>Body simplification:</strong> ${API_TOKENS.body.calls.toLocaleString()} calls, ${(API_TOKENS.body.input + API_TOKENS.body.output).toLocaleString()} tokens
          </div>
        </details>
        <div style="margin-top:8px;padding-top:8px;border-top:1px solid #ccc;color:#1557b0;font-weight:600">
          Estimated cost: ~$${estimatedCost.toFixed(4)}
        </div>
        <div style="margin-top:6px;font-size:11px;color:#666">
          Pricing: $${PRICING.inputPer1M} input / $${PRICING.outputPer1M} output per 1M tokens (${PRICING.model}, updated ${PRICING.lastUpdated})
        </div>
      </div>
      <h4 style="margin:0 0 8px">Headlines Changed</h4>
    `;

    if (!CHANGES.length) {
      const p = document.createElement('p'); p.textContent = 'No recorded changes yet.'; modal.appendChild(p);
    } else {
      CHANGES.forEach((ch, idx) => {
        const row = document.createElement('div'); row.className = 'row';
        row.innerHTML = `
          <div class="meta">#${idx+1} • ${ch.source} • ${ch.mode} • on ${ch.count} element(s)</div>
          <div class="from">– ${escapeHtml(ch.from)}</div>
          <div class="to">+ ${escapeHtml(ch.to)}</div>`;
        list.appendChild(row);
      });
      modal.appendChild(list);
    }
    const actions = document.createElement('div');
    actions.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;margin-top:8px;';
    const btnClose = document.createElement('button'); btnClose.textContent = 'Close'; btnClose.className = 'btn';
    actions.appendChild(btnClose);
    modal.appendChild(actions);
    wrap.appendChild(modal);
    shadow.append(style, wrap);
    document.body.appendChild(host);
    const close = () => host.remove();
    wrap.addEventListener('click', (e) => { if (e.target === wrap) close(); });
    btnClose.addEventListener('click', () => close());
    shadow.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.preventDefault(); close(); } });
    // Focus the wrapper to enable keyboard events
    wrap.setAttribute('tabindex', '-1');
    wrap.focus();
  }
  function escapeHtml(s){return String(s).replace(/[&<>"']/g, m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[m]));}

  // ───────────────────────── BOOTSTRAP ─────────────────────────

  // Check if this is first install
  const isFirstInstall = await storage.get(FIRST_INSTALL_KEY, '') === '';
  const hasApiKey = (await storage.get('OPENAI_KEY', '')) !== '';

  if (isFirstInstall) {
    log('First install detected');
    // Set domain mode to allowlist (disabled everywhere) by default
    if (DOMAINS_MODE === 'deny') {
      await storage.set(DOMAINS_MODE_KEY, 'allow');
      DOMAINS_MODE = 'allow';
      log('Set domain mode to allowlist (disabled by default)');
    }

    // Show welcome dialog after a brief delay to let page settle
    setTimeout(() => {
      openWelcomeDialog();
    }, 500);
    return;
  }

  // If no API key, don't run the script
  if (!hasApiKey) {
    log('No API key configured. Script inactive. Set API key via menu.');
    return;
  }

  if (DOMAIN_DISABLED || OPTED_OUT) {
    log('inactive:', OPTED_OUT ? 'publisher opt-out' : 'domain disabled');
    return;
  }

  ensureBadge();
  attachTargets(document);
  ensureObserver();

  const mo = new MutationObserver((muts) => {
    ensureBadge(); // Recreate badge if it was removed
    for (const m of muts) { if (m.addedNodes && m.addedNodes.length) m.addedNodes.forEach(n => { if (n.nodeType === 1) attachTargets(n); }); }
  });
  mo.observe(document.body, { childList: true, subtree: true });

  processVisibleNow();

  // Body simplification is now only triggered manually via badge button click
  // (removed automatic application on page load)

})();