/**
 * Chameleon Accessibility Content Script
 * Applies user accessibility preferences to any website
 */

// Guard against double-load. Manifest content_scripts run on fresh page
// loads; chrome.scripting.executeScript may also inject this file (e.g. on
// extension start to wake orphaned tabs). Without this guard, every
// re-injection registers another onMessage listener and the cursor would
// be relocated multiple times per CURSOR_UPDATE.
if (window.__chameleonA11yLoaded) {
  console.log("[Chameleon A11y] Already loaded; skipping re-init");
} else {
  window.__chameleonA11yLoaded = true;
console.log("[Chameleon A11y] Script loaded");

let currentPrefs = null;
let chameleonEnabled = false;
let lastAppliedZoom = 1.0;
const BASE_FONT_SIZE_PX = 16;
const TEXT_SCALE_ATTR = "data-chameleon-text-scale";
const TEXT_SCALE_INLINE_ATTR = "data-chameleon-text-scale-inline";
let textScaleReapplyTimeout = null;

function isFeatureEnabled(flag) {
  return flag === true || flag === "true" || flag === 1;
}

const READABLE_TEXT_SELECTOR = [
  "body", "p", "li", "a", "label", "h1", "h2", "h3", "h4", "h5", "h6",
  "blockquote", "figcaption", "caption", "summary", "td", "th", "dt", "dd",
  "small", "strong", "em",
  "span:not([class*='icon']):not([class*='Icon']):not([class*='material-icons']):not([class*='material-symbols']):not([class~='fa']):not([class^='fa-']):not([class*=' fa-']):not([class*='glyph'])",
  "div:not([class*='icon']):not([class*='Icon']):not([class*='material-icons']):not([class*='material-symbols']):not([class~='fa']):not([class^='fa-']):not([class*=' fa-']):not([class*='glyph'])",
  "[role='button']", "[role='link']", "[role='menuitem']", "[role='tab']", "[role='option']",
  "button", "textarea", "select", "option", "optgroup",
  "input:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='color']):not([type='file']):not([type='image'])"
].join(",\n      ");

chrome.runtime.sendMessage({ type: "GET_ENABLED" }, enabledResponse => {
  chameleonEnabled = !!enabledResponse?.enabled;
  if (!chameleonEnabled) {
    currentPrefs = null;
    removeAllStyles();
    console.log("[Chameleon A11y] Disabled at startup");
    return;
  }

  chrome.runtime.sendMessage({ type: "GET_PREFERENCES" }, response => {
    if (response && response.preferences) {
      console.log("[Chameleon A11y] Received preferences:", response.preferences);
      applyPreferences(response.preferences);
    } else {
      console.log("[Chameleon A11y] No preferences available yet");
    }
  });
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === "PREFERENCES_UPDATED") {
    console.log("[Chameleon A11y] Preferences updated:", message.preferences);
    if (chameleonEnabled) {
      applyPreferences(message.preferences);
    } else {
      currentPrefs = message.preferences || currentPrefs;
      removeAllStyles();
    }
    sendResponse({ ok: true });
  }
  if (message.type === "CHAMELEON_ENABLED_CHANGED") {
    console.log("[Chameleon A11y] Enabled changed:", message.enabled);
    chameleonEnabled = !!message.enabled;
    if (!chameleonEnabled) {
      currentPrefs = null;
      removeAllStyles();
      removeHeadTrackCursor();
    } else if (currentPrefs) {
      applyPreferences(currentPrefs);
    } else {
      chrome.runtime.sendMessage({ type: "GET_PREFERENCES" }, response => {
        if (response && response.preferences) applyPreferences(response.preferences);
      });
    }
    sendResponse({ ok: true });
  }
  if (message.type === "HEADTRACK_CURSOR") {
    // One-time log so we can confirm the relay reached this tab.
    if (!window.__chameleonHeadTrackFirstFired) {
      window.__chameleonHeadTrackFirstFired = true;
      console.log("[Chameleon A11y] First HEADTRACK_CURSOR received:", message.x, message.y, "viewport:", window.innerWidth, "x", window.innerHeight);
    }
    // Mouse takes priority: skip head-tracking updates for 500ms after any mouse move.
    if (Date.now() - _lastMouseMoveTime > 500) {
      updateHeadTrackCursor(message.x, message.y);
    }
  }
  if (message.type === "MODE_CHANGED") {
    setHeadTrackMode(message.mode);
  }
  if (message.type === "HEADTRACK_STOP") {
    removeHeadTrackCursor();
  }
  if (message.type === "VOICE_CLICK") {
    // Voice "click" command. Fires a synthetic click at the virtual cursor's
    // current position. Reuses the same helper as click-redirect because the
    // semantics are identical: target whatever is under the head-tracked dot.
    const now = Date.now();
    if (now - _lastVoiceClickAt < 300) return true; // debounce
    _lastVoiceClickAt = now;
    if (!_headTrackCursorEl) {
      console.log("[Chameleon A11y] VOICE_CLICK ignored — no head-track cursor active");
      return true;
    }
    try {
      _clickAtVirtualPointer();
    } catch (err) {
      console.warn("[Chameleon A11y] Voice click failed:", err?.message || err);
    }
  }
  return true;
});

let _lastVoiceClickAt = 0;

// ── HEAD-TRACKING VIRTUAL CURSOR + DWELL CLICK ───────────────────────────────
// Receives normalized (0..1) cursor coords from background.js (which relays
// them from the offscreen Tracky Mouse head tracker). Renders a fixed-position
// dot inside the page (Chrome won't let extensions move the OS cursor) plus a
// circular progress ring around it that fills over DWELL_MS, then dispatches
// a synthetic click at that location.
//
// Dwell is driven solely by HEADTRACK_CURSOR messages — we do NOT use Tracky
// Mouse's dwell clicker because it listens to global pointermove events,
// which would also catch the user's real mouse movement and fire spurious
// clicks anywhere they pause their cursor.

const DWELL_MS = 600;            // industry default
const DWELL_JITTER_PX = 45;      // Euclidean tolerance — clmtrackr wobbles a lot
const POST_CLICK_LOCK_MS = 1200; // after click, block re-dwell unless cursor moves away
const POST_CLICK_ESCAPE_PX = 70; // pixels of separation that releases the post-click lock

let _headTrackCursorEl = null;
let _headTrackRingEl = null;
let _mouseTrackListener = null;
let _lastMouseMoveTime = 0;
let _dwellAnchor = null;     // { x, y, target, startedAt }
let _lastClick = { x: 0, y: 0, at: 0 };
let _dwellRafId = 0;

function installMouseTrack() {
  if (_mouseTrackListener) return;
  // Record real-mouse activity only; do NOT move the green cursor to follow
  // the mouse. The green cursor is the head-tracking pointer and should only
  // appear in response to head movement (HEADTRACK_CURSOR messages).
  _mouseTrackListener = () => { _lastMouseMoveTime = Date.now(); };
  document.addEventListener("mousemove", _mouseTrackListener, { passive: true });
}

function uninstallMouseTrack() {
  if (_mouseTrackListener) {
    document.removeEventListener("mousemove", _mouseTrackListener);
    _mouseTrackListener = null;
  }
}

function ensureHeadTrackCursorEl() {
  if (_headTrackCursorEl && document.body.contains(_headTrackCursorEl)) {
    return _headTrackCursorEl;
  }
  if (!document.getElementById("chameleon-headtrack-cursor-style")) {
    const style = document.createElement("style");
    style.id = "chameleon-headtrack-cursor-style";
    const blankCursor = "url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'/>\") 0 0, none";
    style.textContent = `
      html.chameleon-headtrack-active,
      html.chameleon-headtrack-active body,
      html.chameleon-headtrack-active *,
      html.chameleon-headtrack-active *::before,
      html.chameleon-headtrack-active *::after {
        cursor: ${blankCursor} !important;
      }
      html.chameleon-headtrack-active video,
      html.chameleon-headtrack-active video::-webkit-media-controls,
      html.chameleon-headtrack-active video::-webkit-media-controls-panel,
      html.chameleon-headtrack-active video::-webkit-media-controls-enclosure,
      html.chameleon-headtrack-active video::-webkit-media-controls-overlay-play-button,
      html.chameleon-headtrack-active video::-webkit-media-controls-play-button,
      html.chameleon-headtrack-active video::-webkit-media-controls-timeline,
      html.chameleon-headtrack-active video::-webkit-media-controls-current-time-display,
      html.chameleon-headtrack-active video::-webkit-media-controls-time-remaining-display,
      html.chameleon-headtrack-active video::-webkit-media-controls-mute-button,
      html.chameleon-headtrack-active video::-webkit-media-controls-volume-slider,
      html.chameleon-headtrack-active video::-webkit-media-controls-fullscreen-button {
        cursor: ${blankCursor} !important;
      }
      html.chameleon-headtrack-active #chameleon-headtrack-cursor {
        cursor: none !important;
      }`;
    (document.head || document.documentElement).appendChild(style);
  }
  document.documentElement.classList.add("chameleon-headtrack-active");
  installShadowCursorHide();
  // Suppress the legacy mouse-driven dwell click while head-tracking owns dwell.
  disableDwellClick();

  // Wrapper container holds the dot + the SVG progress ring.
  const wrap = document.createElement("div");
  wrap.id = "chameleon-headtrack-cursor";
  Object.assign(wrap.style, {
    position: "fixed",
    top: "0",
    left: "0",
    width: "44px",
    height: "44px",
    pointerEvents: "none",
    zIndex: "2147483647",
    transform: "translate3d(-50%, -50%, 0)",
    willChange: "transform",
  });

  // Center dot — visible "where am I pointing" indicator.
  const dot = document.createElement("div");
  Object.assign(dot.style, {
    position: "absolute",
    left: "50%",
    top: "50%",
    width: "16px",
    height: "16px",
    marginLeft: "-8px",
    marginTop: "-8px",
    borderRadius: "50%",
    background: "radial-gradient(circle, rgba(16,185,129,1) 0%, rgba(16,185,129,0.9) 70%, rgba(16,185,129,0.55) 100%)",
    border: "2px solid white",
    outline: "1px solid rgba(0,0,0,0.7)",
    boxSizing: "border-box",
  });
  wrap.appendChild(dot);

  // SVG progress ring. stroke-dashoffset animates from C (empty) to 0 (full)
  // as dwell progresses. Dwell radius is 18px, stroke 4px, so circumference
  // C = 2 * π * 18 ≈ 113.097.
  const SVG_NS = "http://www.w3.org/2000/svg";
  const svg = document.createElementNS(SVG_NS, "svg");
  svg.setAttribute("viewBox", "0 0 44 44");
  Object.assign(svg.style, {
    position: "absolute",
    inset: "0",
    width: "100%",
    height: "100%",
    overflow: "visible",
    transform: "rotate(-90deg)",
  });
  const ringBg = document.createElementNS(SVG_NS, "circle");
  ringBg.setAttribute("cx", "22");
  ringBg.setAttribute("cy", "22");
  ringBg.setAttribute("r", "18");
  ringBg.setAttribute("fill", "none");
  ringBg.setAttribute("stroke", "rgba(0,0,0,0.35)");
  ringBg.setAttribute("stroke-width", "4");
  svg.appendChild(ringBg);
  const ring = document.createElementNS(SVG_NS, "circle");
  ring.setAttribute("cx", "22");
  ring.setAttribute("cy", "22");
  ring.setAttribute("r", "18");
  ring.setAttribute("fill", "none");
  ring.setAttribute("stroke", "rgb(16,185,129)");
  ring.setAttribute("stroke-width", "4");
  ring.setAttribute("stroke-linecap", "round");
  ring.setAttribute("stroke-dasharray", "113.097");
  ring.setAttribute("stroke-dashoffset", "113.097");
  svg.appendChild(ring);
  wrap.appendChild(svg);

  (document.body || document.documentElement).appendChild(wrap);
  console.log("[Chameleon A11y] Head-track cursor mounted");
  _headTrackCursorEl = wrap;
  _headTrackRingEl = ring;
  installMouseTrack();
  return wrap;
}

function setRingProgress(p) {
  // p in [0, 1]. dashoffset 113.097 → 0 as p → 1.
  if (!_headTrackRingEl) return;
  const C = 113.097;
  _headTrackRingEl.setAttribute("stroke-dashoffset", String(C * (1 - Math.max(0, Math.min(1, p)))));
}

const FOCUSABLE_TEXT_INPUT_SELECTOR =
  "input:not([type='button']):not([type='submit']):not([type='reset']):not([type='checkbox']):not([type='radio']):not([type='hidden']):not([disabled])," +
  "textarea:not([disabled])," +
  "[contenteditable='true']," +
  "[contenteditable='']," +
  "[role='textbox']," +
  "[role='searchbox']," +
  "[role='combobox']";

function fireDwellClick(target, x, y) {
  if (!(target instanceof Element) || !target.isConnected) return;
  try {
    // Resolve the actual focusable element. document.elementFromPoint often
    // returns a wrapper (e.g. a div containing the input, or an icon span
    // inside a search bar). Climb up to the nearest text-entry control so
    // .focus() lands on something keyboard-receiving.
    const focusableInput = target.closest(FOCUSABLE_TEXT_INPUT_SELECTOR);
    const clickTarget = focusableInput || target;

    const opts = { view: window, bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 };
    const pointerOpts = Object.assign({ pointerType: "mouse", pointerId: 1, isPrimary: true }, opts);
    // Some sites listen for pointer* not mouse*; dispatch both.
    try { clickTarget.dispatchEvent(new PointerEvent("pointerdown", pointerOpts)); } catch (_) {}
    clickTarget.dispatchEvent(new MouseEvent("mousedown", opts));
    try { clickTarget.dispatchEvent(new PointerEvent("pointerup", pointerOpts)); } catch (_) {}
    clickTarget.dispatchEvent(new MouseEvent("mouseup", opts));
    clickTarget.dispatchEvent(new MouseEvent("click", opts));

    // Explicitly focus when there's a text-entry target. Synthetic mousedown
    // events have isTrusted=false; many browsers skip the implicit focus
    // shift those would normally do, so we have to do it ourselves.
    if (focusableInput) {
      try { focusableInput.focus({ preventScroll: false }); } catch (_) {}
      // For <input> and <textarea>, also place the caret at the end so the
      // user can start typing immediately.
      try {
        if (focusableInput.matches("input, textarea") && typeof focusableInput.value === "string") {
          const len = focusableInput.value.length;
          focusableInput.setSelectionRange?.(len, len);
        }
      } catch (_) {}
    } else if (typeof clickTarget.click === "function") {
      // Non-text targets: native click handles activation behavior (anchors,
      // form submits, native checkboxes).
      clickTarget.click();
    }
  } catch (err) {
    console.warn("[Chameleon A11y] Dwell click failed:", err?.message || err);
  }
}

function tickDwell() {
  _dwellRafId = 0;
  if (!_dwellAnchor || !_headTrackCursorEl) { setRingProgress(0); return; }
  const elapsed = performance.now() - _dwellAnchor.startedAt;
  const p = elapsed / DWELL_MS;
  setRingProgress(p);
  if (p >= 1) {
    const { x, y } = _dwellAnchor;
    _dwellAnchor = null;
    setRingProgress(0);
    // Resolve target at click time. Position-based dwell shouldn't lock to
    // an element captured at anchor — the cursor may have drifted to a
    // different element within tolerance.
    const target = document.elementFromPoint(x, y);
    if (target) fireDwellClick(target, x, y);
    _lastClick = { x, y, at: Date.now() };
    return;
  }
  _dwellRafId = requestAnimationFrame(tickDwell);
}

function updateDwell(px, py) {
  // Honor the side-panel Click Method selector. When the user picks "Voice
  // Click", `dwellClickSim` is false and the dwell ring should never appear.
  // (When click_method is "voice_dwell" the pref stays true, so dwell still
  // works in that mode.)
  if (!currentPrefs?.dwellClickSim) {
    if (_dwellAnchor) _dwellAnchor = null;
    setRingProgress(0);
    return;
  }
  // Real-mouse priority: pause dwell while the user has moved the mouse recently.
  if (Date.now() - _lastMouseMoveTime < 500) {
    _dwellAnchor = null;
    setRingProgress(0);
    return;
  }
  // Post-click lock: until cursor moves POST_CLICK_ESCAPE_PX away from the
  // last click, refuse to start a new dwell. This prevents the ring from
  // immediately filling again on the same spot.
  const sinceClick = Date.now() - _lastClick.at;
  if (sinceClick < POST_CLICK_LOCK_MS) {
    const cx = px - _lastClick.x, cy = py - _lastClick.y;
    if (Math.sqrt(cx * cx + cy * cy) < POST_CLICK_ESCAPE_PX) {
      _dwellAnchor = null;
      setRingProgress(0);
      return;
    }
  }
  // Position-only dwell: anchor a (x, y), allow jitter inside DWELL_JITTER_PX,
  // reset only when cursor moves clearly outside that radius. Don't track
  // element identity — clmtrackr wobble crosses element boundaries even when
  // the user holds their head still, which used to thrash the anchor.
  if (!_dwellAnchor) {
    _dwellAnchor = { x: px, y: py, startedAt: performance.now() };
    setRingProgress(0);
  } else {
    const dx = px - _dwellAnchor.x;
    const dy = py - _dwellAnchor.y;
    if (Math.sqrt(dx * dx + dy * dy) > DWELL_JITTER_PX) {
      _dwellAnchor = { x: px, y: py, startedAt: performance.now() };
      setRingProgress(0);
    }
  }
  if (!_dwellRafId) _dwellRafId = requestAnimationFrame(tickDwell);
}

// Smoothed cursor position. EMA filter slows the cursor and damps clmtrackr
// wobble. Higher alpha = more responsive but jittery; lower alpha = smoother
// but laggier. 0.18 is a good balance for ~60Hz updates.
const CURSOR_SMOOTHING_ALPHA = 0.18;
let _smoothedX = null, _smoothedY = null;

function updateHeadTrackCursor(nx, ny) {
  if (typeof nx !== "number" || typeof ny !== "number") return;
  const el = ensureHeadTrackCursorEl();
  const targetX = nx * window.innerWidth;
  const targetY = ny * window.innerHeight;
  if (_smoothedX === null) {
    _smoothedX = targetX;
    _smoothedY = targetY;
  } else {
    _smoothedX += (targetX - _smoothedX) * CURSOR_SMOOTHING_ALPHA;
    _smoothedY += (targetY - _smoothedY) * CURSOR_SMOOTHING_ALPHA;
  }
  const px = Math.round(_smoothedX);
  const py = Math.round(_smoothedY);
  el.style.transform = `translate3d(${px}px, ${py}px, 0) translate(-50%, -50%)`;
  updateDwell(px, py);
}

function removeHeadTrackCursor() {
  if (_dwellRafId) { cancelAnimationFrame(_dwellRafId); _dwellRafId = 0; }
  _dwellAnchor = null;
  _smoothedX = null;
  _smoothedY = null;
  if (_headTrackCursorEl) {
    _headTrackCursorEl.remove();
    _headTrackCursorEl = null;
  }
  _headTrackRingEl = null;
  uninstallMouseTrack();
  document.documentElement.classList.remove("chameleon-headtrack-active");
  uninstallShadowCursorHide();
  // Restore OS pointer dwell now that the virtual cursor is gone.
  if (currentPrefs?.dwellClickSim) enableDwellClick();
  _headTrackMode = "CURSOR";
  _joystickAnchor = null;
}

function setHeadTrackMode(mode) {
  const nextMode = String(mode || "").toUpperCase() === "SCROLL" ? "SCROLL" : "CURSOR";
  _headTrackMode = nextMode;
  if (nextMode === "SCROLL") {
    const rect = _headTrackCursorEl?.getBoundingClientRect();
    _joystickAnchor = rect
      ? { x: Math.round(rect.left + rect.width / 2), y: Math.round(rect.top + rect.height / 2) }
      : { x: Math.round(window.innerWidth / 2), y: Math.round(window.innerHeight / 2) };
    return;
  }
  _joystickAnchor = null;
}

function handleJoystickScrolling(currentX, currentY) {
  if (!_joystickAnchor) {
    _joystickAnchor = { x: currentX, y: currentY };
  }
  // Calculate distance from the anchor point
  const deltaY = currentY - _joystickAnchor.y;

  // Create a "deadzone" so it doesn't drift if they hold relatively still
  if (Math.abs(deltaY) < 30) return;

  // Calculate speed (the further they look, the faster it goes)
  const speed = deltaY * 0.1;

  // Tell the window to scroll
  window.scrollBy({
    top: speed,
    behavior: "instant",
  });
}

// ── CLICK REDIRECT ────────────────────────────────────────────────────────────
// Physical clicks are used only as a trigger. We intercept every click,
// cancel it, then re-fire a full synthetic event sequence at wherever the
// virtual cursor is sitting.

let _redirectingClick = false;
let _clickRedirectInstalled = false;

function _dispatchVirtualMouseEvent(target, type, x, y) {
  const event = new MouseEvent(type, {
    bubbles: true,
    cancelable: true,
    view: window,
    clientX: x,
    clientY: y,
    screenX: window.screenX + x,
    screenY: window.screenY + y,
    button: 0,
    buttons: type === "mousedown" ? 1 : 0,
  });
  Object.defineProperty(event, "isVirtualCursorClick", { value: true });
  target.dispatchEvent(event);
}

function _clickAtVirtualPointer() {
  if (!_headTrackCursorEl) return;
  const rect = _headTrackCursorEl.getBoundingClientRect();
  const x = Math.round(rect.left + rect.width / 2);
  const y = Math.round(rect.top + rect.height / 2);

  _headTrackCursorEl.style.display = "none";
  const target = document.elementFromPoint(x, y);
  _headTrackCursorEl.style.display = "";
  if (!target) return;

  _redirectingClick = true;
  _dispatchVirtualMouseEvent(target, "mousemove", x, y);
  _dispatchVirtualMouseEvent(target, "mousedown", x, y);
  _dispatchVirtualMouseEvent(target, "mouseup", x, y);
  _dispatchVirtualMouseEvent(target, "click", x, y);
  _redirectingClick = false;

  // Browsers won't focus inputs from untrusted events — do it explicitly.
  const focusable =
    target.closest("input, textarea, select, [contenteditable], [tabindex]") ||
    target.querySelector("input, textarea, select, [contenteditable]");
  if (focusable && typeof focusable.focus === "function") focusable.focus();
}

function installClickRedirect() {
  if (_clickRedirectInstalled) return;
  _clickRedirectInstalled = true;

  document.addEventListener("click", (e) => {
    if (e.isVirtualCursorClick || _redirectingClick) return;
    if (!_headTrackCursorEl) return;
    e.preventDefault();
    e.stopImmediatePropagation();
    _clickAtVirtualPointer();
  }, true);

  document.addEventListener("mouseup", (e) => {
    if (e.isVirtualCursorClick || _redirectingClick) return;
    if (!_headTrackCursorEl) return;
    e.preventDefault();
    e.stopImmediatePropagation();
  }, true);
}

// Adopt a constructable stylesheet into every open shadow root so the cursor
// is hidden inside web components (the top-level CSS doesn't reach them).
let _shadowSheet = null;
let _shadowMutObs = null;
function installShadowCursorHide() {
  if (_shadowMutObs) return;
  const css = `:host, :host *, * { cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'/>") 0 0, none !important; }`;
  try {
    if (!_shadowSheet && typeof CSSStyleSheet === "function") {
      _shadowSheet = new CSSStyleSheet();
      _shadowSheet.replaceSync(css);
    }
  } catch (_) { _shadowSheet = null; }

  function adopt(root) {
    if (!root || root.__chameleonAdopted) return;
    root.__chameleonAdopted = true;
    if (_shadowSheet && "adoptedStyleSheets" in root) {
      try {
        root.adoptedStyleSheets = [..._shadowSheet ? root.adoptedStyleSheets : [], _shadowSheet];
        return;
      } catch (_) {}
    }
    const s = document.createElement("style");
    s.textContent = css;
    root.appendChild(s);
  }
  function walk(node) {
    if (!node) return;
    if (node.shadowRoot) adopt(node.shadowRoot);
    if (node.querySelectorAll) {
      for (const el of node.querySelectorAll("*")) {
        if (el.shadowRoot) adopt(el.shadowRoot);
      }
    }
  }
  walk(document);
  _shadowMutObs = new MutationObserver(muts => {
    for (const m of muts) for (const n of m.addedNodes) if (n.nodeType === 1) walk(n);
  });
  _shadowMutObs.observe(document.documentElement, { childList: true, subtree: true });
}
function uninstallShadowCursorHide() {
  if (_shadowMutObs) { _shadowMutObs.disconnect(); _shadowMutObs = null; }
}

function removeAllStyles() {
  ["chameleon-a11y-styles", "chameleon-dyslexic-font", "chameleon-reduce-motion",
   "chameleon-global-zoom", "chameleon-hide-distractions", "chameleon-tint-overlay"].forEach(id => {
    const el = document.getElementById(id);
    if (el) el.remove();
  });
  removeBionicReading();
  clearTextSizeScaling();
  disableDwellClick();
  disableDynamicFocusRing();
  disableFocusRuler();
  lastAppliedZoom = 1.0;
  console.log("[Chameleon A11y] All styles removed (paused)");
}

function applyPreferences(prefs) {
  if (!prefs || !chameleonEnabled) return;
  currentPrefs = prefs;

  const existingStyle = document.getElementById("chameleon-a11y-styles");
  if (existingStyle) existingStyle.remove();

  const style = document.createElement("style");
  style.id = "chameleon-a11y-styles";
  style.textContent = generateStyles(prefs);
  document.head.appendChild(style);
  applyTextSizeScaling(Number(prefs.fontSize) || BASE_FONT_SIZE_PX);

  prefs.dyslexiaFriendly ? injectDyslexiaFont() : document.getElementById("chameleon-dyslexic-font")?.remove();
  prefs.reduceMotion     ? applyReducedMotion() : document.getElementById("chameleon-reduce-motion")?.remove();
  prefs.hideDistractingUI ? hideDistractingElements() : document.getElementById("chameleon-hide-distractions")?.remove();

  const nextZoom = Number(prefs.globalZoom) || 1.0;
  if (nextZoom !== 1.0) {
    applyGlobalZoom(nextZoom);
  } else {
    const existingZoomStyle = document.getElementById("chameleon-global-zoom");
    if (existingZoomStyle) existingZoomStyle.remove();
    lastAppliedZoom = 1.0;
  }

  // Head-tracking owns dwell while its cursor is mounted; don't let the
  // legacy mouse-driven dwell run alongside it (double-fires).
  if (_headTrackCursorEl) {
    disableDwellClick();
  } else {
    prefs.dwellClickSim ? enableDwellClick() : disableDwellClick();
  }
  prefs.focusHighlight ? enableFocusRuler(prefs) : disableFocusRuler();

  // Dynamic focus ring
  if (prefs.focusHighlight || prefs.highVisibilityFocusRing) {
    enableDynamicFocusRing();
  } else {
    disableDynamicFocusRing();
  }

  // Tinted Screen Overlay
  if (isFeatureEnabled(prefs.tintedOverlay)) {
    applyTintedOverlay(prefs.overlayColor);
  } else {
    document.getElementById("chameleon-tint-overlay")?.remove();
  }

  // Bionic Reading
  if (prefs.bionicReading) {
    applyBionicReading();
  } else {
    removeBionicReading();
  }

  console.log("[Chameleon A11y] Preferences applied successfully");
}

function generateStyles(prefs) {
  const styles = [];

  if (prefs.letterSpacing === "wide") {
    styles.push(`${READABLE_TEXT_SELECTOR} { letter-spacing: 0.04em !important; }`);
  }

  if (prefs.lineHeight === "relaxed") {
    styles.push(`${READABLE_TEXT_SELECTOR} { line-height: 1.65 !important; }`);
  } else if (prefs.lineHeight === "loose") {
    styles.push(`${READABLE_TEXT_SELECTOR} { line-height: 1.9 !important; }`);
  }

  if (prefs.underlineLinks) {
    styles.push(`
      a {
        text-decoration: none !important;
        background: linear-gradient(transparent 62%, rgba(57,255,20,0.45) 62%) !important;
        border-radius: 2px !important;
        box-shadow: inset 0 -2px 0 rgba(57,255,20,0.75) !important;
      }
    `);
  }

  if (prefs.dyslexiaFriendly) {
    styles.push(`
      ${READABLE_TEXT_SELECTOR} {
        font-family: 'OpenDyslexic', 'Comic Sans MS', cursive, sans-serif !important;
        -webkit-font-smoothing: antialiased !important;
        text-rendering: optimizeLegibility !important;
      }
    `);
  }

  if (prefs.contrast === "high") {
    styles.push(`body { filter: contrast(1.2) !important; }`);
  } else if (prefs.contrast === "maximum") {
    styles.push(`
      body { filter: contrast(1.5) grayscale(0.2) !important; }
      body * { border-color: currentColor !important; }
    `);
  }

  if (prefs.focusHighlight || prefs.highVisibilityFocusRing) {
    styles.push(`
      *:focus, *:focus-visible {
        outline:        4px solid #1e3a8a !important;
        outline-offset: 3px !important;
        box-shadow:     0 0 0 3px rgba(30, 58, 138, 0.25) !important;
      }
    `);
  }

  if (prefs.oversizedCursor || prefs.adhdOversizedCursor) {
    const cColor = prefs.oversizedCursor ? prefs.cursorColor : prefs.adhdCursorColor;
    const fill   = cColor === "white" ? "%23ffffff" : "%23000000";
    const stroke = cColor === "white" ? "%23000000" : "%23ffffff";
    const cursorSVG = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64' viewBox='0 0 64 64'%3E%3Cpath d='M8 6L8 48L20 37L29 57L37 53L28 33L43 33Z' fill='${fill}' stroke='${stroke}' stroke-width='4' stroke-linejoin='round'/%3E%3C/svg%3E") 8 6`;
    styles.push(`
      html, body, body *, body *::before, body *::after {
        cursor: ${cursorSVG}, auto !important;
      }
      a, button, [role="button"], [role="link"], label, summary,
      input[type="button"], input[type="submit"], input[type="checkbox"], input[type="radio"] {
        cursor: ${cursorSVG}, pointer !important;
      }
      input:not([type="button"]):not([type="submit"]):not([type="checkbox"]):not([type="radio"]),
      textarea, [contenteditable="true"] { cursor: ${cursorSVG}, text !important; }
      [draggable="true"], [data-draggable="true"] { cursor: ${cursorSVG}, grab !important; }
      [draggable="true"]:active, [data-draggable="true"]:active { cursor: ${cursorSVG}, grabbing !important; }
      [style*="cursor: pointer"], [style*="cursor:pointer"] { cursor: ${cursorSVG}, pointer !important; }
      [style*="cursor: text"], [style*="cursor:text"] { cursor: ${cursorSVG}, text !important; }
      [style*="cursor: grab"], [style*="cursor:grab"] { cursor: ${cursorSVG}, grab !important; }
      [style*="cursor: grabbing"], [style*="cursor:grabbing"] { cursor: ${cursorSVG}, grabbing !important; }
      [style*="cursor: move"], [style*="cursor:move"] { cursor: ${cursorSVG}, move !important; }
      [style*="cursor: ew-resize"], [style*="cursor:ew-resize"],
      [style*="cursor: ns-resize"], [style*="cursor:ns-resize"],
      [style*="cursor: col-resize"], [style*="cursor:col-resize"],
      [style*="cursor: row-resize"], [style*="cursor:row-resize"] {
        cursor: ${cursorSVG}, move !important;
      }
    `);
  }

  if (prefs.extraButtonGap) {
    styles.push(`
      button, a, input[type="button"], input[type="submit"], [role="button"] {
        margin: 8px !important;
      }
    `);
  }

  return styles.join("\n");
}

function parsePx(value) {
  if (typeof value !== "string") return NaN;
  const parsed = Number.parseFloat(value);
  return Number.isFinite(parsed) ? parsed : NaN;
}

function isIconLikeElement(el) {
  const cls =
    typeof el.className === "string"
      ? el.className.toLowerCase()
      : "";
  return (
    cls.includes("icon") ||
    cls.includes("material-icons") ||
    cls.includes("material-symbols") ||
    cls.includes("glyph") ||
    cls === "fa" ||
    cls.startsWith("fa-") ||
    cls.includes(" fa-")
  );
}

function shouldScaleElement(el) {
  if (!(el instanceof HTMLElement)) return false;
  if (!el.isConnected) return false;
  if (el.closest("svg, math, script, style, noscript")) return false;
  if (isIconLikeElement(el)) return false;

  const tag = el.tagName.toUpperCase();
  if (tag === "INPUT") {
    const type = (el.getAttribute("type") || "").toLowerCase();
    return !["checkbox", "radio", "range", "color", "file", "image", "hidden"].includes(type);
  }
  if (tag === "TEXTAREA" || tag === "SELECT" || tag === "OPTION" || tag === "OPTGROUP" || tag === "BUTTON") {
    return true;
  }
  if (el.isContentEditable || el.getAttribute("contenteditable") === "true" || el.getAttribute("contenteditable") === "") {
    return true;
  }

  return (el.textContent || "").trim().length > 0;
}

function clearTextSizeScaling() {
  document.querySelectorAll(`[${TEXT_SCALE_ATTR}]`).forEach(node => {
    if (!(node instanceof HTMLElement)) return;
    const inlineFontSize = node.getAttribute(TEXT_SCALE_INLINE_ATTR);
    if (inlineFontSize && inlineFontSize.length > 0) {
      node.style.fontSize = inlineFontSize;
    } else {
      node.style.removeProperty("font-size");
    }
    node.removeAttribute(TEXT_SCALE_ATTR);
    node.removeAttribute(TEXT_SCALE_INLINE_ATTR);
  });
}

function applyTextSizeScaling(targetPx) {
  clearTextSizeScaling();
  if (!Number.isFinite(targetPx) || targetPx === BASE_FONT_SIZE_PX) return;

  const factor = targetPx / BASE_FONT_SIZE_PX;
  const matches = document.querySelectorAll(READABLE_TEXT_SELECTOR);
  const pending = [];

  matches.forEach(node => {
    if (!(node instanceof HTMLElement)) return;
    if (!shouldScaleElement(node)) return;
    const currentPx = parsePx(window.getComputedStyle(node).fontSize);
    if (!Number.isFinite(currentPx) || currentPx <= 0) return;
    pending.push({ node, currentPx, inlineFontSize: node.style.fontSize || "" });
  });

  pending.forEach(({ node, currentPx, inlineFontSize }) => {
    const scaledPx = Math.max(8, Math.round(currentPx * factor * 100) / 100);
    node.setAttribute(TEXT_SCALE_ATTR, "1");
    node.setAttribute(TEXT_SCALE_INLINE_ATTR, inlineFontSize);
    node.style.fontSize = `${scaledPx}px`;
  });
}

function isTextScalingActive() {
  const nextFontSize = Number(currentPrefs?.fontSize) || BASE_FONT_SIZE_PX;
  return Number.isFinite(nextFontSize) && nextFontSize !== BASE_FONT_SIZE_PX;
}

function scheduleTextScaleReapply() {
  clearTimeout(textScaleReapplyTimeout);
  textScaleReapplyTimeout = setTimeout(() => {
    if (!chameleonEnabled || !currentPrefs || !isTextScalingActive()) return;
    applyTextSizeScaling(Number(currentPrefs.fontSize) || BASE_FONT_SIZE_PX);
  }, 250);
}

function injectDyslexiaFont() {
  if (document.getElementById("chameleon-dyslexic-font")) return;
  const fontStyle = document.createElement("style");
  fontStyle.id = "chameleon-dyslexic-font";
  fontStyle.textContent = `
    @font-face {
      font-family: 'OpenDyslexic';
      src: url('https://cdn.jsdelivr.net/npm/open-dyslexic@1.0.3/woff/OpenDyslexic-Regular.woff') format('woff');
      font-weight: normal; font-style: normal; font-display: swap;
    }
    @font-face {
      font-family: 'OpenDyslexic';
      src: url('https://cdn.jsdelivr.net/npm/open-dyslexic@1.0.3/woff/OpenDyslexic-Bold.woff') format('woff');
      font-weight: bold; font-style: normal; font-display: swap;
    }
  `;
  document.head.appendChild(fontStyle);
}

function applyReducedMotion() {
  if (document.getElementById("chameleon-reduce-motion")) return;
  const style = document.createElement("style");
  style.id = "chameleon-reduce-motion";
  style.textContent = `
    *, *::before, *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  `;
  document.head.appendChild(style);
}

function applyGlobalZoom(zoom) {
  if (!Number.isFinite(zoom) || zoom <= 0) return;
  if (zoom === lastAppliedZoom) return;

  let style = document.getElementById("chameleon-global-zoom");
  if (!style) {
    style = document.createElement("style");
    style.id = "chameleon-global-zoom";
    document.head.appendChild(style);
  }
  style.textContent = `body { zoom: ${zoom} !important; }`;
  lastAppliedZoom = zoom;
}

function hideDistractingElements() {
  if (document.getElementById("chameleon-hide-distractions")) return;
  const style = document.createElement("style");
  style.id = "chameleon-hide-distractions";
  style.textContent = `
    ins.adsbygoogle, [data-ad-slot], [data-ad-client], [id^="google_ads_"],
    [id^="div-gpt-ad"], iframe[src*="doubleclick.net"], iframe[src*="googlesyndication"],
    [class*="advertisement-"], [class*="ad-container"], [class*="ad-wrapper"],
    [class*="ad-banner"], [class*="cookie-banner"], [class*="cookie-consent"],
    [id*="cookie-banner"], [id*="cookie-consent"] { display: none !important; }

    [class*="social-share"], [class*="share-buttons"],
    aside:not(nav aside):not(main aside),
    [role="complementary"]:not(main [role="complementary"]) {
      opacity: 0.3 !important; transition: opacity 0.2s !important;
    }
    [class*="social-share"]:hover, [class*="share-buttons"]:hover,
    aside:not(nav aside):not(main aside):hover,
    [role="complementary"]:not(main [role="complementary"]):hover { opacity: 1 !important; }
  `;
  document.head.appendChild(style);
}

// ── TINTED OVERLAY ────────────────────────────────────────────────────────────

function applyTintedOverlay(colorTheme) {
  let overlay = document.getElementById("chameleon-tint-overlay");
  
  if (!overlay) {
    overlay = document.createElement("div");
    overlay.id = "chameleon-tint-overlay";
    overlay.style.cssText = `
      position: fixed !important;
      top: 0 !important;
      left: 0 !important;
      width: 100vw !important;
      height: 100vh !important;
      pointer-events: none !important;
      z-index: 2147483647 !important;
      transition: background-color 0.3s ease !important;
    `;
    document.documentElement.appendChild(overlay);
  }

  const colors = {
    peach: "rgba(255, 149, 0, 0.12)",
    blue: "rgba(0, 122, 255, 0.12)",
    mint: "rgba(52, 199, 89, 0.12)",
    yellow: "rgba(255, 204, 0, 0.12)"
  };
  
  overlay.style.backgroundColor = colors[colorTheme] || colors.peach;
}

// ── BIONIC READING (Saccadic Support) ─────────────────────────────────────────

function applyBionicReading() {
  if (document.body.classList.contains("chameleon-bionic-enabled")) return;
  document.body.classList.add("chameleon-bionic-enabled");

  // Create a TreeWalker to find all raw text nodes on the page
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
    acceptNode: function(node) {
      const parent = node.parentNode;
      if (!parent) return NodeFilter.FILTER_REJECT;
      
      // Skip scripts, styles, and interactive inputs
      const tag = parent.nodeName.toUpperCase();
      if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT') return NodeFilter.FILTER_REJECT;
      if (parent.closest('[contenteditable="true"]') || parent.closest('textarea') || parent.closest('input')) return NodeFilter.FILTER_REJECT;
      
      // Skip empty whitespace nodes
      if (node.nodeValue.trim() === '') return NodeFilter.FILTER_SKIP;
      return NodeFilter.FILTER_ACCEPT;
    }
  }, false);

  const textNodes = [];
  let node;
  while (node = walker.nextNode()) textNodes.push(node);

  textNodes.forEach(textNode => {
    const text = textNode.nodeValue;
    const words = text.split(/(\s+)/); // Split by spaces but preserve the spaces
    
    if (words.length <= 1 && words[0].trim() === '') return;

    const fragment = document.createDocumentFragment();
    words.forEach(word => {
      if (word.trim().length === 0) {
        fragment.appendChild(document.createTextNode(word));
        return;
      }
      
      // The Bionic Math: Bold the first ~40% of the word
      const focalLength = word.length === 1 ? 1 : Math.ceil(word.length * 0.4);
      
      const b = document.createElement('b');
      b.className = 'chameleon-bionic-part';
      b.style.fontWeight = '900'; // Extra bold for high contrast
      b.textContent = word.slice(0, focalLength);

      const rest = document.createTextNode(word.slice(focalLength));
      fragment.appendChild(b);
      fragment.appendChild(rest);
    });

    // Wrap in a span so we can safely reverse it later without breaking React SPAs
    const wrapper = document.createElement('span');
    wrapper.className = 'chameleon-bionic-wrapper';
    wrapper.setAttribute('data-original-text', text);
    wrapper.appendChild(fragment);

    textNode.parentNode.replaceChild(wrapper, textNode);
  });
  
  console.log("[Chameleon A11y] Bionic Reading applied");
}

function removeBionicReading() {
  if (!document.body.classList.contains("chameleon-bionic-enabled")) return;
  document.body.classList.remove("chameleon-bionic-enabled");

  // Find all our custom wrappers and revert them back to raw text nodes
  document.querySelectorAll('.chameleon-bionic-wrapper').forEach(wrapper => {
    const originalText = wrapper.getAttribute('data-original-text') || '';
    const textNode = document.createTextNode(originalText);
    wrapper.parentNode.replaceChild(textNode, wrapper);
  });
  
  console.log("[Chameleon A11y] Bionic Reading removed");
}

// ── HEAD-TRACKING DWELL ───────────────────────────────────────────────────────
// When dwellClickSim is on while head-tracking is active, we drive dwell from
// the virtual cursor position rather than the hidden OS pointer.

let _htDwellTimeout = null;
let _htDwellCurrentEl = null;

function disableHeadTrackDwell() {
  if (_htDwellTimeout) { clearTimeout(_htDwellTimeout); _htDwellTimeout = null; }
  _htDwellCurrentEl = null;
}

function updateHeadTrackDwell(px, py) {
  if (!currentPrefs?.dwellClickSim || !_headTrackCursorEl) {
    disableHeadTrackDwell();
    return;
  }
  _headTrackCursorEl.style.display = "none";
  const under = document.elementFromPoint(px, py);
  _headTrackCursorEl.style.display = "";
  const candidate = under ? (under.closest(DWELL_CLICKABLE_SELECTOR) ?? null) : null;
  if (candidate === _htDwellCurrentEl) return;
  _htDwellCurrentEl = candidate;
  if (_htDwellTimeout) { clearTimeout(_htDwellTimeout); _htDwellTimeout = null; }
  if (candidate && isDwellCandidateClickable(candidate)) {
    _htDwellTimeout = setTimeout(() => {
      if (_htDwellCurrentEl?.isConnected) _clickAtVirtualPointer();
      _htDwellCurrentEl = null;
    }, DWELL_CLICK_DELAY_MS);
  }
}

// ── DWELL CLICK ───────────────────────────────────────────────────────────────

let dwellTimeout = null;
let dwellTarget = null;
let _dwellClickEnabled = false;
let _dwellPointerMoveHandler = null;
let _dwellPointerDownHandler = null;
let _dwellWheelHandler = null;
let _dwellKeyDownHandler = null;
let _lastDwellActivation = { target: null, at: 0 };

const DWELL_CLICK_DELAY_MS = 600;
const DWELL_CLICK_COOLDOWN_MS = 800;
const DWELL_CLICKABLE_SELECTOR = [
  "a[href]", "button", "[role='button']", "[role='link']", "summary", "label",
  "input[type='button']", "input[type='submit']", "input[type='checkbox']",
  "input[type='radio']", "input[type='file']"
].join(",");

function isDwellCandidateClickable(el) {
  if (!(el instanceof Element)) return false;
  if (!el.isConnected) return false;
  if (el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true") return false;
  const style = window.getComputedStyle(el);
  if (style.pointerEvents === "none" || style.visibility === "hidden" || style.display === "none") return false;
  return true;
}

function resolveDwellTarget(node) {
  if (!(node instanceof Element)) return null;
  const clickable = node.closest(DWELL_CLICKABLE_SELECTOR);
  return isDwellCandidateClickable(clickable) ? clickable : null;
}

function clearDwellTimer() {
  if (dwellTimeout) { clearTimeout(dwellTimeout); dwellTimeout = null; }
}

function setDwellTarget(nextTarget) {
  if (dwellTarget === nextTarget) return;
  dwellTarget = nextTarget;
  clearDwellTimer();
  if (!dwellTarget) return;
  dwellTimeout = setTimeout(() => {
    if (!dwellTarget || !dwellTarget.isConnected || !isDwellCandidateClickable(dwellTarget)) return;
    const now = Date.now();
    if (_lastDwellActivation.target === dwellTarget && now - _lastDwellActivation.at < DWELL_CLICK_COOLDOWN_MS) return;
    _lastDwellActivation = { target: dwellTarget, at: now };
    console.log("[Chameleon A11y] Dwell click activated on", dwellTarget.tagName);
    dwellTarget.click();
  }, DWELL_CLICK_DELAY_MS);
}

function enableDwellClick() {
  if (_dwellClickEnabled) return;
  _dwellClickEnabled = true;
  _dwellPointerMoveHandler = e => {
    setDwellTarget(resolveDwellTarget(document.elementFromPoint(e.clientX, e.clientY) || e.target));
  };
  _dwellPointerDownHandler = () => setDwellTarget(null);
  _dwellWheelHandler      = () => setDwellTarget(null);
  _dwellKeyDownHandler    = () => setDwellTarget(null);
  document.addEventListener("pointermove", _dwellPointerMoveHandler, true);
  document.addEventListener("pointerdown", _dwellPointerDownHandler, true);
  document.addEventListener("wheel",       _dwellWheelHandler,       true);
  document.addEventListener("keydown",     _dwellKeyDownHandler,     true);
  console.log("[Chameleon A11y] Dwell click enabled");
}

function disableDwellClick() {
  if (!_dwellClickEnabled) return;
  _dwellClickEnabled = false;
  document.removeEventListener("pointermove", _dwellPointerMoveHandler, true);
  document.removeEventListener("pointerdown", _dwellPointerDownHandler, true);
  document.removeEventListener("wheel",       _dwellWheelHandler,       true);
  document.removeEventListener("keydown",     _dwellKeyDownHandler,     true);
  _dwellPointerMoveHandler = _dwellPointerDownHandler = _dwellWheelHandler = _dwellKeyDownHandler = null;
  clearDwellTimer();
  dwellTarget = null;
  _lastDwellActivation = { target: null, at: 0 };
  console.log("[Chameleon A11y] Dwell click disabled");
}

// ── DYNAMIC FOCUS RING ────────────────────────────────────────────────────────

let _dynamicFocusHandler = null;

function getEffectiveBackgroundColor(el) {
  while (el && el.nodeType === 1) {
    const bg = window.getComputedStyle(el).backgroundColor;
    if (bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") return bg;
    el = el.parentElement;
  }
  return "rgb(255, 255, 255)";
}

function isDarkColor(rgbString) {
  const rgb = rgbString.match(/\d+/g);
  if (!rgb || rgb.length < 3) return false;
  const [r, g, b] = rgb.map(Number);
  return (r * 299 + g * 587 + b * 114) / 1000 < 128;
}

function enableDynamicFocusRing() {
  if (_dynamicFocusHandler) return;

  _dynamicFocusHandler = function (e) {
    const el = e.target;
    if (!(el instanceof Element)) return;

    const bg     = getEffectiveBackgroundColor(el);
    const isDark = isDarkColor(bg);
    const color  = isDark ? "#fbbf24"                 : "#1e3a8a";
    const shadow = isDark ? "rgba(251,191,36,0.3)"    : "rgba(30,58,138,0.25)";

    const prev = document.querySelector("[data-chameleon-focus]");
    if (prev && prev !== el) prev.removeAttribute("data-chameleon-focus");
    el.setAttribute("data-chameleon-focus", "true");

    let dynStyle = document.getElementById("chameleon-focus-ring-dynamic");
    if (!dynStyle) {
      dynStyle = document.createElement("style");
      dynStyle.id = "chameleon-focus-ring-dynamic";
      document.head.appendChild(dynStyle);
    }
    dynStyle.textContent = `
      [data-chameleon-focus]:focus,
      [data-chameleon-focus]:focus-visible {
        outline:        4px solid ${color} !important;
        outline-offset: 3px !important;
        box-shadow:     0 0 0 3px ${shadow} !important;
      }
    `;
  };

  document.addEventListener("focusin", _dynamicFocusHandler, true);
  console.log("[Chameleon A11y] Dynamic focus ring enabled");
}

function disableDynamicFocusRing() {
  if (!_dynamicFocusHandler) return;
  document.removeEventListener("focusin", _dynamicFocusHandler, true);
  _dynamicFocusHandler = null;
  document.getElementById("chameleon-focus-ring-dynamic")?.remove();
  document.querySelector("[data-chameleon-focus]")?.removeAttribute("data-chameleon-focus");
  console.log("[Chameleon A11y] Dynamic focus ring disabled");
}

// ── FOCUS RULER ──────────────────────────────────────────────────────────────

const RULER_SNAP_THRESHOLD = 80;
const RULER_TRANSITION     = "top 0.18s cubic-bezier(0.4,0,0.2,1), height 0.18s cubic-bezier(0.4,0,0.2,1)";

let _rulerWindowHeight = 60;
let _rulerDimOpacity   = 0.55;
let _rulerDimColor     = "#000000";

let _rulerTopEl        = null;
let _rulerBottomEl     = null;
let _rulerBarEl        = null;
let _rulerPopover      = null;
let _rulerEnabled        = false;
let _rulerMouseHandler   = null;
let _rulerWindowCentreY  = -1;
let _rulerFocusHandler   = null;
let _outsideClickHandler = null;

function saveRulerPrefs() {
  try {
    chrome.storage.local.set({
      chameleon_ruler_window_height: _rulerWindowHeight,
      chameleon_ruler_dim_opacity:   _rulerDimOpacity,
      chameleon_ruler_dim_color:     _rulerDimColor,
    });
  } catch(e) {}
}

function loadRulerPrefs(callback) {
  try {
    chrome.storage.local.get(
      ["chameleon_ruler_window_height","chameleon_ruler_dim_opacity","chameleon_ruler_dim_color"],
      (data) => {
        if (data.chameleon_ruler_window_height != null) _rulerWindowHeight = data.chameleon_ruler_window_height;
        if (data.chameleon_ruler_dim_opacity   != null) _rulerDimOpacity   = data.chameleon_ruler_dim_opacity;
        if (data.chameleon_ruler_dim_color     != null) _rulerDimColor     = data.chameleon_ruler_dim_color;
        if (callback) callback();
      }
    );
  } catch(e) { if (callback) callback(); }
}

function applyRulerPosition() {
  if (!_rulerTopEl) return;
  const centreY    = _rulerWindowCentreY;
  const halfWindow = _rulerWindowHeight / 2;
  const topH       = Math.max(0, centreY - halfWindow);
  const bottomTop  = centreY + halfWindow;

  _rulerTopEl.style.top    = "0";
  _rulerTopEl.style.height = topH + "px";

  _rulerBottomEl.style.top    = bottomTop + "px";
  _rulerBottomEl.style.bottom = "0";
  _rulerBottomEl.style.height = "";

  _rulerBarEl.style.top    = (centreY - halfWindow) + "px";
  _rulerBarEl.style.height = _rulerWindowHeight + "px";
}

function applyRulerStyles() {
  if (!_rulerTopEl) return;
  const dimBg = _rulerDimColor;
  const h = dimBg.replace("#","");
  const r = parseInt(h.slice(0,2),16);
  const g = parseInt(h.slice(2,4),16);
  const b = parseInt(h.slice(4,6),16);
  const bg = `rgba(${r},${g},${b},${_rulerDimOpacity})`;

  _rulerTopEl.style.background    = bg;
  _rulerBottomEl.style.background = bg;
}

function createRulerOverlays() {
  if (_rulerTopEl) return;

  function makeOverlay(id) {
    const el = document.createElement("div");
    el.id = id;
    Object.assign(el.style, {
      position:      "fixed",
      left:          "0",
      width:         "100%",
      zIndex:        "2147483630",
      pointerEvents: "none",
      userSelect:    "none",
    });
    return el;
  }

  _rulerTopEl           = makeOverlay("chameleon-ruler-top");
  _rulerTopEl.style.top = "0";

  _rulerBottomEl = makeOverlay("chameleon-ruler-bottom");

  _rulerBarEl = document.createElement("div");
  _rulerBarEl.id = "chameleon-ruler-bar";
  Object.assign(_rulerBarEl.style, {
    position:      "fixed",
    left:          "0",
    width:         "100%",
    zIndex:        "2147483640",
    pointerEvents: "none",
    userSelect:    "none",
    boxSizing:     "border-box",
    borderTop:     "2px solid rgba(255,255,255,0.25)",
    borderBottom:  "2px solid rgba(255,255,255,0.25)",
    transition:    RULER_TRANSITION,
  });

  const handle = document.createElement("div");
  handle.title = "Drag reading window";
  Object.assign(handle.style, {
    position:       "absolute",
    left:           "0", top: "0",
    width:          "28px", height: "100%",
    display:        "flex", flexDirection: "column",
    alignItems:     "center", justifyContent: "center",
    gap:            "4px",
    cursor:         "grab",
    pointerEvents:  "none",
    background:     "rgba(255,255,255,0.12)",
    borderRight:    "1px solid rgba(255,255,255,0.15)",
  });
  for (let i = 0; i < 3; i++) {
    const dot = document.createElement("div");
    Object.assign(dot.style, {
      width: "4px", height: "4px", borderRadius: "50%",
      background: "rgba(255,255,255,0.7)", flexShrink: "0", pointerEvents: "none",
    });
    handle.appendChild(dot);
  }
  _rulerBarEl.appendChild(handle);

  const gear = document.createElement("button");
  gear.title = "Reading window settings";
  gear.setAttribute("aria-label", "Reading window settings");

  const GEAR_SVG_PATH = `<circle cx="12" cy="12" r="3"/>
    <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06
      a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09
      A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83
      l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09
      A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83
      l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09
      a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83
      l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09
      a1.65 1.65 0 0 0-1.51 1z"/>`;

  function buildGearSVG(strokeColor) {
    return `<svg width="13" height="13" viewBox="0 0 24 24" fill="none"
      stroke="${strokeColor}" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
      ${GEAR_SVG_PATH}</svg>`;
  }

  function updateGearColors() {
    if (_rulerTopEl)    _rulerTopEl.style.visibility    = "hidden";
    if (_rulerBottomEl) _rulerBottomEl.style.visibility = "hidden";
    if (_rulerBarEl)    _rulerBarEl.style.visibility    = "hidden";

    let bg = "rgb(255,255,255)";
    try {
      const gearRect = gear.getBoundingClientRect();
      const sampleX  = Math.round(gearRect.left + gearRect.width / 2);
      const sampleY  = Math.round(gearRect.top  + gearRect.height / 2);
      const el = document.elementFromPoint(sampleX - 40, sampleY)
              || document.elementFromPoint(sampleX, sampleY);
      if (el && el !== gear && !gear.contains(el)) {
        bg = getEffectiveBackgroundColor(el);
      }
    } finally {
      if (_rulerTopEl)    _rulerTopEl.style.visibility    = "";
      if (_rulerBottomEl) _rulerBottomEl.style.visibility = "";
      if (_rulerBarEl)    _rulerBarEl.style.visibility    = "";
    }

    const dark = isDarkColor(bg);

    const iconColor  = dark ? "rgba(255,255,255,0.85)" : "rgba(0,0,0,0.7)";
    const bgIdle     = dark ? "rgba(255,255,255,0.13)" : "rgba(0,0,0,0.09)";
    const bgHover    = dark ? "rgba(255,255,255,0.28)" : "rgba(0,0,0,0.18)";
    const ringHover  = dark ? "rgba(255,255,255,0.45)" : "rgba(0,0,0,0.35)";

    gear.innerHTML = buildGearSVG(iconColor);
    gear._bgIdle   = bgIdle;
    gear._bgHover  = bgHover;
    gear._ring     = ringHover;
    gear.style.background = bgIdle;

    const borderColor = dark ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.20)";
    if (_rulerBarEl) {
      _rulerBarEl.style.borderTop    = `2px solid ${borderColor}`;
      _rulerBarEl.style.borderBottom = `2px solid ${borderColor}`;
    }
  }

  Object.assign(gear.style, {
    position:      "absolute", right: "6px", top: "50%",
    transform:     "translateY(-50%)",
    width:         "26px", height: "26px", borderRadius: "6px", border: "none",
    background:    "rgba(0,0,0,0.09)",
    cursor:        "pointer",
    display:       "flex", alignItems: "center", justifyContent: "center",
    pointerEvents: "auto", padding: "0", zIndex: "2",
    transition:    "background 0.15s, box-shadow 0.15s, transform 0.25s",
    boxShadow:     "0 0 0 1px rgba(128,128,128,0.25)",
  });

  gear.innerHTML = buildGearSVG("rgba(0,0,0,0.7)");

  gear.addEventListener("mousedown", e => { e.stopPropagation(); e.preventDefault(); });
  gear.addEventListener("click",     e => { e.stopPropagation(); openRulerPopover(); });
  gear.addEventListener("mouseenter", () => {
    gear.style.background = gear._bgHover || "rgba(0,0,0,0.18)";
    gear.style.boxShadow  = `0 0 0 2px ${gear._ring || "rgba(0,0,0,0.35)"}`;
    gear.style.transform  = "translateY(-50%) rotate(30deg)";
  });
  gear.addEventListener("mouseleave", () => {
    gear.style.background = gear._bgIdle || "rgba(0,0,0,0.09)";
    gear.style.boxShadow  = "0 0 0 1px rgba(128,128,128,0.25)";
    gear.style.transform  = "translateY(-50%)";
  });
  _rulerBarEl.appendChild(gear);

  _rulerBarEl.style.cursor = "default";

  _rulerMouseHandler = (e) => {
    if (_rulerPopover) return;
    _rulerWindowCentreY = e.clientY;
    _rulerTopEl.style.transition    = "none";
    _rulerBottomEl.style.transition = "none";
    _rulerBarEl.style.transition    = "none";
    applyRulerPosition();
  };
  document.addEventListener("mousemove", _rulerMouseHandler, { passive: true });

  document.addEventListener("touchstart", (e) => {
    if (_rulerPopover) return;
    if (e.target === gear || gear.contains(e.target)) return;
    _rulerWindowCentreY = e.touches[0].clientY;
    applyRulerPosition();
  }, { passive: true });

  window.addEventListener("resize", () => { if (_rulerEnabled) applyRulerPosition(); });

  _rulerWindowCentreY = Math.round(window.innerHeight / 2);
  applyRulerStyles();
  applyRulerPosition();

  document.body.appendChild(_rulerTopEl);
  document.body.appendChild(_rulerBottomEl);
  document.body.appendChild(_rulerBarEl);

  requestAnimationFrame(() => {
    applyRulerStyles();
    applyRulerPosition();
  });

  updateGearColors();
  setTimeout(updateGearColors, 50);
  setTimeout(updateGearColors, 200);

  let _gearColorTimer = null;
  document.addEventListener("mousemove", () => {
    if (!_rulerEnabled) return;
    clearTimeout(_gearColorTimer);
    _gearColorTimer = setTimeout(updateGearColors, 80);
  }, { passive: true });
}

function moveRulerToElement(el) {
  if (!_rulerTopEl || !el) return;
  const r = el.getBoundingClientRect();
  if (!r.height) return;
  _rulerWindowCentreY = r.top + r.height / 2;
  applyRulerPosition();
}

function openRulerPopover() {
  if (_rulerPopover) { closeRulerPopover(); return; }

  const barRect = _rulerBarEl.getBoundingClientRect();
  const popTop  = barRect.bottom + 8 < window.innerHeight - 220
    ? barRect.bottom + 8
    : barRect.top - 228;

  const pop = document.createElement("div");
  pop.id = "chameleon-ruler-popover";
  Object.assign(pop.style, {
    position: "fixed", right: "12px", top: popTop + "px",
    background: "#1c1c1c", border: "1px solid rgba(255,255,255,0.1)",
    borderRadius: "10px", padding: "14px 16px", zIndex: "2147483647",
    boxShadow: "0 8px 32px rgba(0,0,0,0.6)", minWidth: "210px",
    fontFamily: "system-ui,sans-serif", userSelect: "none",
  });

  const title = document.createElement("div");
  Object.assign(title.style, { fontSize:"11px", fontWeight:"600", color:"#71717a",
    letterSpacing:"0.07em", textTransform:"uppercase", marginBottom:"14px" });
  title.textContent = "Reading window";
  pop.appendChild(title);

  function makeSliderRow(labelText, min, max, step, value, unit, onInput, onChange) {
    const wrap = document.createElement("div");
    wrap.style.marginBottom = "12px";

    const labelRow = document.createElement("div");
    Object.assign(labelRow.style, { display:"flex", justifyContent:"space-between",
      alignItems:"center", marginBottom:"6px" });
    const lbl = document.createElement("span");
    Object.assign(lbl.style, { fontSize:"13px", fontWeight:"500", color:"#f4f4f5" });
    lbl.textContent = labelText;
    const val = document.createElement("span");
    Object.assign(val.style, { fontSize:"11px", fontFamily:"monospace", color:"#10b981",
      background:"rgba(16,185,129,0.12)", padding:"2px 8px", borderRadius:"4px" });
    val.textContent = value + unit;
    labelRow.appendChild(lbl);
    labelRow.appendChild(val);
    wrap.appendChild(labelRow);

    const sl = document.createElement("input");
    sl.type = "range"; sl.min = String(min); sl.max = String(max);
    sl.step = String(step); sl.value = String(value);
    Object.assign(sl.style, { width:"100%", accentColor:"#10b981", cursor:"pointer" });
    sl.addEventListener("mousedown", e => e.stopPropagation());
    sl.addEventListener("input", (e) => {
      e.stopPropagation();
      const v = parseFloat(e.target.value);
      val.textContent = v + unit;
      onInput(v);
    });
    sl.addEventListener("change", (e) => { e.stopPropagation(); onChange(parseFloat(e.target.value)); });
    wrap.appendChild(sl);
    return wrap;
  }

  pop.appendChild(makeSliderRow(
    "Window height", 20, 120, 1, _rulerWindowHeight, "px",
    (v) => { _rulerWindowHeight = v; applyRulerPosition(); },
    (v) => { _rulerWindowHeight = v; applyRulerPosition(); saveRulerPrefs(); }
  ));

  pop.appendChild(makeSliderRow(
    "Dim strength", 0, 1, 0.05, _rulerDimOpacity, "",
    (v) => { _rulerDimOpacity = v; applyRulerStyles(); },
    (v) => { _rulerDimOpacity = v; applyRulerStyles(); saveRulerPrefs(); }
  ));

  const colorRow = document.createElement("div");
  Object.assign(colorRow.style, { display:"flex", justifyContent:"space-between",
    alignItems:"center", marginBottom:"4px" });
  const colorLbl = document.createElement("span");
  Object.assign(colorLbl.style, { fontSize:"13px", fontWeight:"500", color:"#f4f4f5" });
  colorLbl.textContent = "Dim color";
  const colorPicker = document.createElement("input");
  colorPicker.type = "color";
  colorPicker.value = _rulerDimColor;
  Object.assign(colorPicker.style, { width:"32px", height:"24px", border:"1px solid rgba(255,255,255,0.15)",
    borderRadius:"5px", cursor:"pointer", padding:"2px", background:"transparent" });
  colorPicker.addEventListener("input",  (e) => { e.stopPropagation(); _rulerDimColor = e.target.value; applyRulerStyles(); });
  colorPicker.addEventListener("change", (e) => { e.stopPropagation(); saveRulerPrefs(); });
  colorRow.appendChild(colorLbl);
  colorRow.appendChild(colorPicker);
  pop.appendChild(colorRow);

  document.body.appendChild(pop);
  _rulerPopover = pop;

  setTimeout(() => {
    _outsideClickHandler = (e) => {
      if (_rulerPopover && !_rulerPopover.contains(e.target) &&
          _rulerBarEl   && !_rulerBarEl.contains(e.target)) {
        closeRulerPopover();
      }
    };
    document.addEventListener("mousedown", _outsideClickHandler, true);
  }, 50);
}

function closeRulerPopover() {
  if (_rulerPopover) { _rulerPopover.remove(); _rulerPopover = null; }
  if (_outsideClickHandler) {
    document.removeEventListener("mousedown", _outsideClickHandler, true);
    _outsideClickHandler = null;
  }
}

function enableFocusRuler(prefs) {
  if (prefs && prefs.focusRulerHeight != null) _rulerWindowHeight = prefs.focusRulerHeight;
  if (prefs && prefs.focusRulerColor)          _rulerDimColor     = prefs.focusRulerColor;
  if (prefs && prefs.focusRulerOpacity != null) _rulerDimOpacity  = prefs.focusRulerOpacity;

  loadRulerPrefs(() => {
    if (!_rulerEnabled) {
      _rulerEnabled = true;
      createRulerOverlays();
      _rulerFocusHandler = (e) => {
        if (e.target instanceof Element) moveRulerToElement(e.target);
      };
      document.addEventListener("focusin", _rulerFocusHandler, true);
      console.log("[Chameleon A11y] Focus ruler (reading mask) enabled");
    } else {
      applyRulerStyles();
      applyRulerPosition();
    }
  });
}

function disableFocusRuler() {
  if (!_rulerEnabled) return;
  _rulerEnabled = false;
  closeRulerPopover();
  if (_rulerMouseHandler) {
    document.removeEventListener("mousemove", _rulerMouseHandler);
    _rulerMouseHandler = null;
  }
  if (_rulerFocusHandler) {
    document.removeEventListener("focusin", _rulerFocusHandler, true);
    _rulerFocusHandler = null;
  }
  [_rulerTopEl, _rulerBottomEl, _rulerBarEl].forEach(el => { if (el) el.remove(); });
  _rulerTopEl = _rulerBottomEl = _rulerBarEl = null;
  _rulerWindowCentreY = -1;
  console.log("[Chameleon A11y] Focus ruler disabled");
}

// ── SPA RE-APPLY ──────────────────────────────────────────────────────────────

const observer = new MutationObserver(mutations => {
  const hasSignificantChanges = mutations.some(
    m => m.addedNodes.length > 5 || m.removedNodes.length > 5
  );
  if (hasSignificantChanges && chameleonEnabled && currentPrefs) {
    clearTimeout(observer.reapplyTimeout);
    observer.reapplyTimeout = setTimeout(() => applyPreferences(currentPrefs), 500);
    return;
  }

  const hasAddedNodes = mutations.some(m => m.addedNodes.length > 0);
  if (hasAddedNodes && chameleonEnabled && currentPrefs && isTextScalingActive()) {
    scheduleTextScaleReapply();
  }
});

observer.observe(document.documentElement, { childList: true, subtree: true });

} // end window.__chameleonA11yLoaded guard
