/**
 * Visual overlay management for focus and delete indicators
 */
import { CSS_CLASSES, Z_INDEX, SELECTORS, MODES, COLORS, FEATURE_FLAGS, CLICKABLE_CATEGORY, KP_UI_FONT, SCROLL } from '../config/constants.js';
import {
  getAllInspectorHostClasses,
  getInspectorDef,
  getInspectorInstructionText
} from './inspector-mode.js';
import { MSG } from '../messaging/types.js';
import { HighlightManager } from './highlight-manager.js';
import { PopupManager } from './popup-manager.js';
import { DEFAULT_SETTINGS } from './settings-manager.js';
import { storageGetValue, storageSetValue } from '../utils/storage.js';
import { makePopoverResizable } from '../utils/popover-resize.js';
import { createPreviewOpenActionButtons } from '../ui/preview-open-actions.js';
import {
  createPopoverTitlebar,
  createTitlebarCloseHint
} from '../ui/popover-titlebar.js';
import { ensureOpenChromeShadow } from '../ui/kp-chrome-shadow.js';
import { createSegmentedControl } from '../ui/segmented-control.js';
import {
  NCT_DARK_UI_PANEL_BACKGROUND,
  NCT_DARK_UI_PANEL_BORDER,
  NCT_DARK_UI_PANEL_RADIUS,
  NCT_DARK_UI_PANEL_BOX_SHADOW
} from '../ui/nct-dark-ui.js';

/** Per-host Link Preview viewport mode: { [hostname]: 'mobile' }. Missing/default = desktop. */
const PREVIEW_VIEWPORT_BY_HOST_KEY = 'kp_link_preview_viewport_by_host';

/**
 * @param {string} url
 * @returns {string} normalized hostname (lowercase, no leading www.)
 */
function previewHostFromUrl(url) {
  try {
    let host = new URL(String(url || '')).hostname.toLowerCase();
    if (host.startsWith('www.')) host = host.slice(4);
    return host;
  } catch {
    return '';
  }
}

/**
 * Link Preview iframes generally require HTTPS. Prefer https when the URL is plain http.
 * @param {string} url
 * @returns {string}
 */
function preferHttpsForPreview(url) {
  const s = String(url || '').trim();
  if (!s) return s;
  if (/^http:\/\//i.test(s)) {
    return `https://${s.slice('http://'.length)}`;
  }
  return s;
}


export class OverlayManager {
  constructor() {
    // Rendering mode configuration: 'dom' | 'canvas' | 'css-custom-props'
    // Default: no full-viewport canvas. DOM-hover uses element styling; fixed
    // backends are opt-in via setRenderingMode('canvas'|'css-custom-props').
    this.renderingMode = 'dom';

    // Canvas rendering backend
    this.canvasOverlay = null;
    this.canvasContext = null;

    // CSS Custom Properties rendering backend
    this.cssCustomPropsOverlay = null;

    this.focusOverlay = null;
    /** Shared inspector pick outline (Delete, Cols, future kinds) */
    this.inspectorOverlay = null;
    /** @type {string|null} last applied inspector kind for restyle */
    this._inspectorOverlayKind = null;
    /** Top-right companion instruction (like Text Select mode indicator) */
    this.inspectorModeIndicator = null;
    /** @type {{ kind?: string|null, confirmKey?: string }|null} */
    this._inspectorIndicatorOpts = null;
    // Legacy aliases — kept so any lingering callers don't throw during transition
    this.deleteOverlay = null;
    this.colsOverlay = null;
    this.viewportModalFrame = null; // Viewport modal frame for text focus mode
    this.escExitLabelText = null; // ESC label for text fields
    this.escExitLabelHover = null; // ESC label for hovered elements
    this.hoverClickLabelText = 'F clicks'; // Hover label for click arming in text focus mode
    this.popoverContainer = null; // Container for popover iframe
    this.popoverIframeElement = null; // iframe element (for focus management)
    this.popoverIframeWindow = null; // contentWindow of the current popover iframe (for message validation)
    this.popoverMessageHandler = null; // message listener for iframe bridge
    this.popoverInitTimer = null; // timer for bridge init retries
    this.popoverBridgeReady = false; // whether iframe bridge has acked readiness
    this.popoverCloseButton = null; // close button element for keyboard activation (F)
    this._popoverLastMouse = { x: null, y: null }; // last known mouse position in top document
    this._popoverMouseTrackerInstalled = false;
    this._isPreviewPopover = false; // track if current popover is preview style (no backdrop)
    this._popoverArrowStyle = null; // style element for preview popover triangle
    this._popoverClickOutsideHandler = null; // click outside handler for preview popover
    this._previewPopoverDragCleanup = null; // teardown titlebar drag listeners
    this._popoverResizeDispose = null; // teardown generic resize handles
    this._popoverHybridFocusCleanup = null; // teardown chrome↔iframe focus routing
    this._previewMobileUaActive = false; // SW session rule: mobile UA for preview iframe
    /** @type {HTMLElement|null} */
    this._edgeJumpFadeEl = null;
    this._edgeJumpFadeToken = 0;

    // Central popup stack + blurred backdrop (kept below click overlays).
    // Note: Panel change callback will be set by KeyPilot after initialization
    this.popupManager = new PopupManager();
    this._popoverPopupId = 'kpv2-iframe-popover';

    // Initialize highlight manager
    this.highlightManager = new HighlightManager();
    
    // Intersection observer for overlay visibility optimization
    this.overlayObserver = null;
    this.resizeObserver = null; // ResizeObserver for viewport modal frame
    
    // Track overlay visibility state
    this.overlayVisibility = {
      focus: true,
      delete: true,
      escExitLabel: true
    };

    // Settings-driven UI customization (populated by KeyPilot from chrome.storage.sync).
    this._modeSettings = {
      clickMode: null,
      textMode: null
    };

    // Debug panel for performance metrics
    this.debugPanel = null;
    this.debugPanelUpdateInterval = null;

    /**
     * Shadow-root debug HUD (msn.com / archive.org paint experiments).
     * @type {HTMLElement|null}
     */
    this._shadowDebugHud = null;
    /** @type {boolean} */
    this._shadowDebugHudEnabled = !!(FEATURE_FLAGS && FEATURE_FLAGS.DEBUG_SHADOW_ROOT_HUD);
    /**
     * Forced paint strategy while HUD is open: 'A' | 'B' | 'C' | null (auto).
     * @type {'A'|'B'|'C'|null}
     */
    this._shadowDebugPaintOverride = null;
    /** @type {{ leaf: Element|null, focus: Element|null, paint: Element|null, auto: string, applied: string, inShadow: boolean }|null} */
    this._shadowDebugLastInfo = null;

    // When DOM-hover listener mode is enabled, render non-text focus rectangles in blue so it's
    // visually obvious we're using browser-native hover targeting (vs RBush-driven hit-testing).
    this._useDomHoverFocusColors = false;

    /**
     * When true, hover focus uses strategy C: body-level fixed DOM overlay.
     * Scroll must reposition it; `usesElementFocusStyling()` reports false.
     * Preference: A DOM outline → B in-target ring → C this path.
     * @type {boolean}
     */
    this._focusPaintUsesFixedOverlay = false;

    /**
     * When true, hover focus uses strategy B: absolute ring inside the host
     * (local max z-index + 1). Co-located with the element → scrolls free.
     * @type {boolean}
     */
    this._focusPaintUsesInTargetRing = false;

    /** @type {HTMLElement|null} singleton in-target ring node */
    this._inTargetRing = null;
    /** @type {Element|null} host currently holding the in-target ring */
    this._inTargetHost = null;
    /**
     * If we set position:relative on a static host, restore on detach.
     * @type {{ host: Element, prev: string }|null}
     */
    this._inTargetHostPosRestore = null;

    // Text focus styling (we style the focused input + nearby wrapper parents directly).
    this._textFocusCurrentElement = null;
    this._textFocusStyledElements = new Set();
    /** @type {'left_edge'|'background_tint'|null} */
    this._textFocusAppliedStyle = null;

    // Text input hover styling (SVG "Press F…" hint on hovered fields; outline is separate).
    this._textHoverCurrentElement = null;
    this._textHoverStyledElements = new Set();

    // Last known focus target (used for F-click scale-up pulse across all render modes).
    this._lastFocusElement = null;
    this._lastFocusRect = null;

    /**
     * Cache for `_findFocusClipContext` — avoids repeated getComputedStyle ancestor
     * walks on hover thrash. Keyed by element; invalidated on resize / scroll-end TTL.
     * @type {WeakMap<Element, { clippers: Element[], tightWrapper: Element|null, ts: number, left: number, top: number, width: number, height: number }>}
     */
    this._focusClipContextCache = new WeakMap();
    /** @type {number} ms */
    this._focusClipContextTtlMs = 500;
    /** @type {(() => void)|null} */
    this._focusClipInvalidateBound = null;
    this._installFocusClipCacheInvalidation();

    /**
     * Active temporary click/image effect overlays.
     * Fixed-position ghosts must be torn down when the source leaves view or the
     * page navigates — otherwise they animate alone over the next screen.
     * @type {Set<{ pulse: Element, sourceEl: Element|null, originRect: DOMRect|null, rafId: number, timeoutId: number, io: IntersectionObserver|null, teardown: () => void }>}
     */
    this._activeEphemeralEffects = new Set();
    this._ephemeralEffectLifecycleInstalled = false;
    /** @type {(() => void)|null} */
    this._ephemeralEffectLifecycleDispose = null;
    
    this.setupOverlayObserver();
    
    // Initialize highlight manager with observer
    this.highlightManager.initialize(this.overlayObserver);
  }

  /**
   * Toggle alternate focus overlay colors (blue) for DOM-hover listener targeting mode.
   * @param {boolean} enabled
   */
  setDomHoverFocusColorsEnabled(enabled) {
    this._useDomHoverFocusColors = !!enabled;
    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] DOM hover focus colors enabled:', this._useDomHoverFocusColors);
    }
  }

  /**
   * True when focus chrome is painted on/with the target element (strategies
   * A or B) and therefore scrolls with the page. False when strategy C
   * (body-level fixed overlay) must be repositioned on scroll.
   * @returns {boolean}
   */
  usesElementFocusStyling() {
    // A (DOM outline) and B (in-target ring) co-locate with the element.
    // C (body fixed) needs scroll reposition — only that reports false.
    return !!this._useDomHoverFocusColors && !this._focusPaintUsesFixedOverlay;
  }

  /**
   * Focus ring palette from click-mode settings (blue | green).
   * Falls back to blue when DOM-hover element styling is active and no setting is loaded.
   * @param {'blue'|'green'|string|null|undefined} [focusColor]
   */
  _getNonTextFocusPalette(focusColor) {
    const color = focusColor === 'green' || focusColor === 'blue'
      ? focusColor
      : (this._getClickModeSettings().focusColor || 'blue');
    if (color === 'green') {
      return {
        borderColor: COLORS.FOCUS_GREEN,
        shadowColor: COLORS.GREEN_SHADOW,
        shadowBrightColor: COLORS.GREEN_SHADOW_BRIGHT,
        backgroundColor: COLORS.FOCUS_GREEN_BG_T2
      };
    }
    return {
      borderColor: COLORS.FOCUS_BLUE,
      shadowColor: COLORS.BLUE_SHADOW,
      shadowBrightColor: COLORS.BLUE_SHADOW_BRIGHT,
      backgroundColor: COLORS.FOCUS_BLUE_BG_T2
    };
  }

  /**
   * Resolve clickable category for focus chrome decisions.
   * @param {Element|null} element
   * @returns {string}
   */
  getFocusCategory(element) {
    try {
      const detector = window.keyPilot?.detector || window.keyPilot?.elementDetector;
      if (detector && typeof detector.getClickableCategory === 'function') {
        return detector.getClickableCategory(element);
      }
    } catch { /* ignore */ }
    return CLICKABLE_CATEGORY.NONE;
  }

  /**
   * Suppress the semi-transparent blue focus fill by category:
   * - slider: never (progress bars are not link-like)
   * - media with seek chrome in the player shell: no fill (players, not thumbnails)
   * - text: no blue wash
   *
   * Links / generic / media thumbnails keep the fill.
   *
   * @param {Element|null} element
   * @returns {boolean}
   */
  shouldSuppressFocusFill(element) {
    try {
      if (!element || !(element instanceof Element)) return false;
      const detector = window.keyPilot?.detector || window.keyPilot?.elementDetector;
      if (detector && typeof detector.shouldSuppressFocusFillForElement === 'function') {
        return detector.shouldSuppressFocusFillForElement(element);
      }
      const cat = this.getFocusCategory(element);
      return cat === CLICKABLE_CATEGORY.SLIDER || cat === CLICKABLE_CATEGORY.TEXT;
    } catch {
      return false;
    }
  }

  /**
   * Media / slider surfaces for F-key scale-pulse skip (not link-style).
   * @param {Element|null} element
   * @returns {boolean}
   */
  isVideoLikeElement(element) {
    try {
      if (!element || !(element instanceof Element)) return false;
      const cat = this.getFocusCategory(element);
      if (cat === CLICKABLE_CATEGORY.SLIDER || cat === CLICKABLE_CATEGORY.MEDIA) return true;
      // Fallback: raw video node if category unavailable
      if (element.tagName === 'VIDEO' || element.tagName === 'AUDIO') return true;
      return !!(element.querySelector && element.querySelector('video'));
    } catch {
      return false;
    }
  }

  // Canvas-based rendering backend
  initCanvasRenderer() {
    if (this.canvasOverlay) return;

    this.canvasOverlay = document.createElement('canvas');
    this.canvasOverlay.className = CSS_CLASSES.CANVAS_OVERLAY || 'kpv2-canvas-overlay';
    this.canvasOverlay.style.cssText = `
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;
      pointer-events: none;
      z-index: ${Z_INDEX.OVERLAYS};
      will-change: transform;
    `;

    // Set canvas size to viewport
    this.canvasOverlay.width = window.innerWidth;
    this.canvasOverlay.height = window.innerHeight;

    this.canvasContext = this.canvasOverlay.getContext('2d');
    document.body.appendChild(this.canvasOverlay);

    // Handle viewport resize
    this._canvasResizeHandler = () => {
      this.canvasOverlay.width = window.innerWidth;
      this.canvasOverlay.height = window.innerHeight;
    };
    window.addEventListener('resize', this._canvasResizeHandler);

    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] Canvas renderer initialized');
    }
  }

  cleanupCanvasRenderer() {
    if (this.canvasOverlay) {
      if (this._canvasResizeHandler) {
        window.removeEventListener('resize', this._canvasResizeHandler);
        this._canvasResizeHandler = null;
      }
      this.canvasOverlay.remove();
      this.canvasOverlay = null;
      this.canvasContext = null;
    }
  }

  updateFocusOverlayCanvas(element, mode = MODES.NONE, rectOverride = null) {
    if (!this.canvasContext || !element) {
      this.hideFocusOverlayCanvas();
      return;
    }

    // Don't outline modal/popover iframes
    try {
      if (element.tagName === 'IFRAME') {
        const isPopoverIframe = this.popoverIframeElement && element === this.popoverIframeElement;
        const isModalIframe = !!(element.classList && element.classList.contains('modal-iframe'));
        if (isPopoverIframe || isModalIframe) {
          this.hideFocusOverlayCanvas();
          return;
        }
      }
    } catch { /* ignore */ }

    const rect = (rectOverride && typeof rectOverride === 'object')
      ? rectOverride
      : this.getBestRect(element);
    if (!rect || rect.width <= 0 || rect.height <= 0) {
      this.hideFocusOverlayCanvas();
      return;
    }

    // Determine element type and colors
    const isTextInput = element.matches && element.matches(SELECTORS.FOCUSABLE_TEXT);
    const suppressFill = this.shouldSuppressFocusFill(element);
    const isVeryLarge = rect.width > 512 && rect.height > 512;

    const clickSettings = this._getClickModeSettings();
    let borderColor, shadowColor, backgroundColor;
    if (isTextInput) {
      borderColor = COLORS.ORANGE;
      shadowColor = COLORS.ORANGE_SHADOW;
      backgroundColor = 'transparent';
    } else {
      const p = this._getNonTextFocusPalette(clickSettings.focusColor);
      borderColor = p.borderColor;
      shadowColor = p.shadowColor;
      // Fill for thumbnails/links; suppress for scrubbers and players that have a seek bar.
      backgroundColor = (suppressFill || isVeryLarge) ? 'transparent' : p.backgroundColor;
    }

    // Settings-driven behavior
    const { rectangleThickness, overlayFillEnabled, overlayShadowEnabled } = clickSettings;
    if (!isTextInput && !suppressFill && !isVeryLarge && overlayFillEnabled === false) {
      backgroundColor = 'transparent';
    }

    // Clear canvas and draw rectangle
    this.canvasContext.clearRect(0, 0, this.canvasOverlay.width, this.canvasOverlay.height);

    // Draw background fill if enabled
    if (backgroundColor !== 'transparent') {
      this.canvasContext.fillStyle = backgroundColor;
      this.canvasContext.fillRect(rect.left, rect.top, rect.width, rect.height);
    }

    // Draw border
    this.canvasContext.strokeStyle = borderColor;
    this.canvasContext.lineWidth = rectangleThickness;
    this.canvasContext.strokeRect(rect.left, rect.top, rect.width, rect.height);

    // Optional soft outer glow
    if (overlayShadowEnabled !== false) {
      this.canvasContext.shadowColor = shadowColor;
      this.canvasContext.shadowBlur = 4;
      this.canvasContext.strokeRect(rect.left, rect.top, rect.width, rect.height);
      this.canvasContext.shadowBlur = 0;
    }

    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] Canvas focus overlay updated:', {
        rect: rect,
        borderColor: borderColor,
        backgroundColor: backgroundColor
      });
    }
  }

  hideFocusOverlayCanvas() {
    if (this.canvasContext) {
      this.canvasContext.clearRect(0, 0, this.canvasOverlay.width, this.canvasOverlay.height);
    }
  }

  // CSS Custom Properties rendering backend
  initCSSCustomPropsRenderer() {
    if (this.cssCustomPropsOverlay) return;

    this.cssCustomPropsOverlay = document.createElement('div');
    this.cssCustomPropsOverlay.className = CSS_CLASSES.CSS_PROPS_OVERLAY || 'kpv2-css-props-overlay';
    const p = this._getNonTextFocusPalette();
    this.cssCustomPropsOverlay.style.cssText = `
      --rect-x: 0px;
      --rect-y: 0px;
      --rect-width: 0px;
      --rect-height: 0px;
      --rect-border-color: ${p.borderColor};
      --rect-background: transparent;
      --rect-shadow-color: ${p.shadowColor};
      --rect-shadow-bright-color: ${p.shadowBrightColor};
      --rect-border-thickness: 2px;

      position: fixed;
      left: var(--rect-x);
      top: var(--rect-y);
      width: var(--rect-width);
      height: var(--rect-height);
      border: var(--rect-border-thickness) solid var(--rect-border-color);
      background: var(--rect-background);
      box-shadow: 0 0 0 2px var(--rect-shadow-color), 0 0 10px 2px var(--rect-shadow-bright-color);
      pointer-events: none;
      z-index: ${Z_INDEX.OVERLAYS};
      will-change: transform;
    `;

    document.body.appendChild(this.cssCustomPropsOverlay);

    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] CSS Custom Properties renderer initialized');
    }
  }

  cleanupCSSCustomPropsRenderer() {
    if (this.cssCustomPropsOverlay) {
      this.cssCustomPropsOverlay.remove();
      this.cssCustomPropsOverlay = null;
    }
  }

  updateFocusOverlayCSSCustomProps(element, mode = MODES.NONE, rectOverride = null) {
    if (!this.cssCustomPropsOverlay || !element) {
      this.hideFocusOverlayCSSCustomProps();
      return;
    }

    // Don't outline modal/popover iframes
    try {
      if (element.tagName === 'IFRAME') {
        const isPopoverIframe = this.popoverIframeElement && element === this.popoverIframeElement;
        const isModalIframe = !!(element.classList && element.classList.contains('modal-iframe'));
        if (isPopoverIframe || isModalIframe) {
          this.hideFocusOverlayCSSCustomProps();
          return;
        }
      }
    } catch { /* ignore */ }

    const rect = (rectOverride && typeof rectOverride === 'object')
      ? rectOverride
      : this.getBestRect(element);
    if (!rect || rect.width <= 0 || rect.height <= 0) {
      this.hideFocusOverlayCSSCustomProps();
      return;
    }

    // Determine element type and colors
    const isTextInput = element.matches && element.matches(SELECTORS.FOCUSABLE_TEXT);
    const suppressFill = this.shouldSuppressFocusFill(element);
    const isVeryLarge = rect.width > 512 && rect.height > 512;

    const clickSettings = this._getClickModeSettings();
    let borderColor, shadowColor, backgroundColor, shadowBrightColor;
    if (isTextInput) {
      borderColor = COLORS.ORANGE;
      shadowColor = COLORS.ORANGE_SHADOW;
      shadowBrightColor = COLORS.ORANGE_SHADOW;
      backgroundColor = 'transparent';
    } else {
      const p = this._getNonTextFocusPalette(clickSettings.focusColor);
      borderColor = p.borderColor;
      shadowColor = p.shadowColor;
      shadowBrightColor = p.shadowBrightColor;
      // Fill for thumbnails/links; suppress for scrubbers and players that have a seek bar.
      backgroundColor = (suppressFill || isVeryLarge) ? 'transparent' : p.backgroundColor;
    }

    // Settings-driven behavior
    const { rectangleThickness, overlayFillEnabled, overlayShadowEnabled } = clickSettings;
    if (!isTextInput && !suppressFill && !isVeryLarge && overlayFillEnabled === false) {
      backgroundColor = 'transparent';
    }

    // Update CSS custom properties
    const overlay = this.cssCustomPropsOverlay;
    overlay.style.setProperty('--rect-x', rect.left + 'px');
    overlay.style.setProperty('--rect-y', rect.top + 'px');
    overlay.style.setProperty('--rect-width', rect.width + 'px');
    overlay.style.setProperty('--rect-height', rect.height + 'px');
    overlay.style.setProperty('--rect-border-color', borderColor);
    overlay.style.setProperty('--rect-background', backgroundColor);
    overlay.style.setProperty('--rect-shadow-color', shadowColor);
    overlay.style.setProperty('--rect-shadow-bright-color', shadowBrightColor);
    overlay.style.setProperty('--rect-border-thickness', rectangleThickness + 'px');
    if (overlayShadowEnabled === false) {
      overlay.style.boxShadow = 'none';
    } else {
      overlay.style.boxShadow =
        '0 0 0 2px var(--rect-shadow-color), 0 0 10px 2px var(--rect-shadow-bright-color)';
    }

    try {
      const radius = this._resolveElementBorderRadius(element);
      overlay.style.borderRadius = radius || '0';
      overlay.style.boxSizing = 'border-box';
    } catch {
      overlay.style.borderRadius = '0';
    }

    overlay.style.display = 'block';
    overlay.style.visibility = 'visible';
    overlay.style.opacity = '1';

    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] CSS Custom Props focus overlay updated:', {
        rect: rect,
        borderColor: borderColor,
        backgroundColor: backgroundColor
      });
    }
  }

  hideFocusOverlayCSSCustomProps() {
    if (this.cssCustomPropsOverlay) {
      this.cssCustomPropsOverlay.style.setProperty('--rect-width', '0px');
      this.cssCustomPropsOverlay.style.setProperty('--rect-height', '0px');
      this.cssCustomPropsOverlay.style.display = 'none';
    }
  }

  /**
   * Apply click/text mode settings from chrome.storage (via KeyPilot).
   * @param {object|null|undefined} settings
   */
  setModeSettings(settings) {
    const s = settings && typeof settings === 'object' ? settings : {};
    this._modeSettings = {
      clickMode: s.clickMode && typeof s.clickMode === 'object' ? s.clickMode : null,
      textMode: s.textMode && typeof s.textMode === 'object' ? s.textMode : null
    };

    // If a text field is currently styled, re-apply so focusStyle changes take effect live.
    try {
      const focused = this._textFocusCurrentElement;
      if (focused && focused.isConnected) {
        this._textFocusCurrentElement = null;
        this._applyTextFocusElementStyling(focused);
      }
    } catch { /* ignore */ }
  }

  _getClickModeSettings() {
    const cm = this._modeSettings?.clickMode && typeof this._modeSettings.clickMode === 'object'
      ? this._modeSettings.clickMode
      : {};
    const def = DEFAULT_SETTINGS.clickMode || {};
    const rectangleThickness = Number(cm.rectangleThickness);
    const thickness = Number.isFinite(rectangleThickness)
      ? Math.min(Math.max(rectangleThickness, 1), 16)
      : (Number(def.rectangleThickness) || 3);
    // Defaults: outline only (no fill / no glow) unless explicitly enabled.
    const overlayFillEnabled = cm.overlayFillEnabled === true;
    const overlayShadowEnabled = cm.overlayShadowEnabled === true;
    const focusColor = cm.focusColor === 'green' ? 'green' : 'blue';
    const rawEffect = cm.clickEffect;
    const clickEffect =
      rawEffect === 'flash' ||
      rawEffect === 'dash' ||
      rawEffect === 'marquee' ||
      rawEffect === 'scale' ||
      rawEffect === 'none'
        ? rawEffect
        : (def.clickEffect || 'flash');
    return {
      rectangleThickness: thickness,
      overlayFillEnabled,
      overlayShadowEnabled,
      focusColor,
      clickEffect
    };
  }

  _getTextModeSettings() {
    const tm = this._modeSettings?.textMode && typeof this._modeSettings.textMode === 'object'
      ? { ...DEFAULT_SETTINGS.textMode, ...this._modeSettings.textMode }
      : DEFAULT_SETTINGS.textMode;
    const strokeThickness = Number(tm.strokeThickness);
    const thickness = Number.isFinite(strokeThickness) ? Math.min(Math.max(strokeThickness, 1), 16) : 3;
    const leftEdgeRaw = Number(tm.leftEdgeWidth);
    const leftEdgeWidth = Number.isFinite(leftEdgeRaw)
      ? Math.min(Math.max(leftEdgeRaw, 1), 24)
      : (Number(DEFAULT_SETTINGS.textMode.leftEdgeWidth) || 5);
    const focusStyle = tm.focusStyle === 'background_tint' ? 'background_tint' : 'left_edge';
    return {
      strokeThickness: thickness,
      labelsEnabled: tm.labelsEnabled,
      focusStyle,
      leftEdgeWidth
    };
  }

  setHoverClickLabelText(text) {
    this.hoverClickLabelText = String(text || 'F clicks');
    // In the new UI, the hover-click text is shown as a suffix on the "Active text field" label.
    if (this.escExitLabelHover) this.escExitLabelHover.innerHTML = this.formatActiveTextFieldLabel();
  }

  formatHoverLabelText(text) {
    // Handle countdown format: "5 F clicks" -> "<span class='countdown'>5</span> <kbd>F</kbd> clicks"
    // Handle regular format: "F clicks" -> "<kbd>F</kbd> clicks"
    const countdownMatch = text.match(/^(\d+)\s+(F clicks?)$/);
    if (countdownMatch) {
      const [_, number, rest] = countdownMatch;
      return `<span class="countdown-number">${number}</span> <kbd>F</kbd> clicks`;
    } else {
      return text.replace(/^F/, '<kbd>F</kbd>');
    }
  }

  formatActiveTextFieldLabel() {
    const suffix = this.hoverClickLabelText ? this.formatHoverLabelText(this.hoverClickLabelText) : '';
    return suffix ? `Active text field &nbsp;•&nbsp; ${suffix}` : 'Active text field';
  }

  ensureTextModeLabels() {
    if (!this.escExitLabelHover) {
      // Reuse the existing "hover" label slot for the Active Text label (so cleanup + observer stays simple).
      this.escExitLabelHover = this.createElement('div', {
        className: CSS_CLASSES.ESC_EXIT_LABEL,
        style: `
          position: fixed;
          pointer-events: none;
          z-index: ${Z_INDEX.OVERLAYS_ABOVE};
          will-change: transform, opacity;
        `
      });
      this.escExitLabelHover.innerHTML = this.formatActiveTextFieldLabel();
      document.body.appendChild(this.escExitLabelHover);
      if (this.overlayObserver) this.overlayObserver.observe(this.escExitLabelHover);
    } else {
      this.escExitLabelHover.innerHTML = this.formatActiveTextFieldLabel();
    }

    if (!this.escExitLabelText) {
      this.escExitLabelText = this.createElement('div', {
        className: CSS_CLASSES.ESC_EXIT_LABEL,
        style: `
          position: fixed;
          pointer-events: none;
          z-index: ${Z_INDEX.OVERLAYS_ABOVE};
          will-change: transform, opacity;
        `
      });
      this.escExitLabelText.innerHTML = 'Press <kbd>ESC</kbd> to close';
      document.body.appendChild(this.escExitLabelText);
      if (this.overlayObserver) this.overlayObserver.observe(this.escExitLabelText);
    } else {
      this.escExitLabelText.innerHTML = 'Press <kbd>ESC</kbd> to close';
    }
  }

  updateTextModeLabels(element) {
    if (!element) {
      this.hideTextModeLabels();
      return;
    }

    const { labelsEnabled } = this._getTextModeSettings();
    if (!labelsEnabled) {
      this.hideTextModeLabels();
      return;
    }

    this.ensureTextModeLabels();

    const rect = this.getBestRect(element);
    if (!rect || rect.width <= 0 || rect.height <= 0) {
      this.hideTextModeLabels();
      return;
    }

    // Measure labels after setting innerHTML.
    const active = this.escExitLabelHover;
    const esc = this.escExitLabelText;
    const activeH = active?.offsetHeight || 24;
    const escH = esc?.offsetHeight || 24;
    const activeW = active?.offsetWidth || 180;
    const escW = esc?.offsetWidth || 180;
    const pad = 8;

    // Preferred placement: active above, ESC below.
    let activeTop = rect.top - activeH - pad;
    let escTop = rect.top + rect.height + pad;

    // If offscreen, flip.
    if (activeTop < pad) activeTop = rect.top + rect.height + pad;
    if (escTop + escH > window.innerHeight - pad) escTop = rect.top - escH - pad;

    // If they collide, stack them.
    if (Math.abs(activeTop - escTop) < Math.min(activeH, escH) + 4) {
      // Keep active closer to the element; push ESC away.
      if (escTop >= rect.top + rect.height) {
        escTop = escTop + activeH + 6;
      } else {
        escTop = escTop - activeH - 6;
      }
    }

    const clampLeft = (w) => Math.min(Math.max(rect.left, pad), window.innerWidth - w - pad);
    const activeLeft = clampLeft(activeW);
    const escLeft = clampLeft(escW);

    if (active) {
      active.style.left = `${activeLeft}px`;
      active.style.top = `${Math.min(Math.max(activeTop, pad), window.innerHeight - activeH - pad)}px`;
      active.style.display = 'block';
      active.style.visibility = 'visible';
    }
    if (esc) {
      esc.style.left = `${escLeft}px`;
      esc.style.top = `${Math.min(Math.max(escTop, pad), window.innerHeight - escH - pad)}px`;
      esc.style.display = 'block';
      esc.style.visibility = 'visible';
    }
  }

  hideTextModeLabels() {
    if (this.escExitLabelText) this.escExitLabelText.style.display = 'none';
    if (this.escExitLabelHover) this.escExitLabelHover.style.display = 'none';
  }

  _clearTextFocusElementStyling() {
    if (!this._textFocusStyledElements || this._textFocusStyledElements.size === 0) {
      this._textFocusCurrentElement = null;
      this._textFocusAppliedStyle = null;
      return;
    }
    try {
      for (const el of this._textFocusStyledElements) {
        if (!el || el.nodeType !== 1) continue;
        try {
          el.classList.remove(CSS_CLASSES.TEXT_FOCUS_INPUT);
          el.classList.remove(CSS_CLASSES.TEXT_FOCUS_INPUT_PARENT);
          el.classList.remove(CSS_CLASSES.TEXT_FOCUS_LEFT_EDGE);
        } catch { /* ignore */ }
      }
    } finally {
      this._textFocusStyledElements.clear();
      this._textFocusCurrentElement = null;
      this._textFocusAppliedStyle = null;
    }
  }

  _getNearbyInputWrappers(inputEl) {
    const parents = [];
    if (!inputEl || inputEl.nodeType !== 1) return parents;

    // Only attempt wrapper styling when we have a meaningful rect.
    const inputRect = this.getBestRect(inputEl);
    if (!inputRect || inputRect.width <= 0 || inputRect.height <= 0) return parents;

    const maxDepth = 4;
    const maxPad = 28; // px of allowed expansion around the input (rounded containers, icons, padding)
    const maxAreaMultiple = 4.0; // avoid tinting large layout containers

    let p = inputEl.parentElement;
    let depth = 0;
    while (p && depth++ < maxDepth) {
      // Never tint the whole page.
      if (p === document.body || p === document.documentElement) break;

      let r;
      try {
        r = p.getBoundingClientRect();
      } catch {
        break;
      }

      if (!r || r.width <= 0 || r.height <= 0) {
        p = p.parentElement;
        continue;
      }

      const dx = Math.max(Math.abs(r.left - inputRect.left), Math.abs(r.right - inputRect.right));
      const dy = Math.max(Math.abs(r.top - inputRect.top), Math.abs(r.bottom - inputRect.bottom));

      const inputArea = inputRect.width * inputRect.height;
      const area = r.width * r.height;

      const closeEnough = dx <= maxPad && dy <= maxPad;
      const notTooBig =
        area <= inputArea * maxAreaMultiple &&
        r.width <= inputRect.width + maxPad * 2 &&
        r.height <= inputRect.height + maxPad * 2;

      if (!closeEnough || !notTooBig) break;

      parents.push(p);
      p = p.parentElement;
    }

    return parents;
  }

  /**
   * Lazy-inject KeyPilot CSS into the open ShadowRoot that owns `el` (if any).
   * No-op for light DOM. See StyleManager.ensureStylesForNode.
   * @param {Element|null|undefined} el
   */
  _ensureStylesForElement(el) {
    try {
      const sm = window.keyPilot?.styleManager;
      if (sm && typeof sm.ensureStylesForNode === 'function') {
        sm.ensureStylesForNode(el);
      }
    } catch { /* ignore */ }
  }

  _applyTextFocusElementStyling(inputEl) {
    if (!inputEl || inputEl.nodeType !== 1) {
      this._clearTextFocusElementStyling();
      return;
    }

    const { focusStyle } = this._getTextModeSettings();
    const useLeftEdge = focusStyle !== 'background_tint';

    // Avoid thrashing the DOM on RAF-driven overlay refreshes when style is unchanged.
    if (
      this._textFocusCurrentElement === inputEl &&
      this._textFocusStyledElements.size > 0 &&
      this._textFocusAppliedStyle === focusStyle
    ) {
      try {
        inputEl.classList.add(CSS_CLASSES.TEXT_FOCUS_INPUT);
        if (useLeftEdge) inputEl.classList.add(CSS_CLASSES.TEXT_FOCUS_LEFT_EDGE);
        else inputEl.classList.remove(CSS_CLASSES.TEXT_FOCUS_LEFT_EDGE);
      } catch { /* ignore */ }
      return;
    }

    this._clearTextFocusElementStyling();
    this._textFocusCurrentElement = inputEl;
    this._textFocusAppliedStyle = focusStyle;

    try {
      this._ensureStylesForElement(inputEl);
      inputEl.classList.add(CSS_CLASSES.TEXT_FOCUS_INPUT);
      if (useLeftEdge) inputEl.classList.add(CSS_CLASSES.TEXT_FOCUS_LEFT_EDGE);
      this._textFocusStyledElements.add(inputEl);
    } catch { /* ignore */ }

    // Background-tint style can wash nearby rounded wrappers; left-edge only paints the field.
    if (!useLeftEdge) {
      const parents = this._getNearbyInputWrappers(inputEl);
      for (const p of parents) {
        try {
          this._ensureStylesForElement(p);
          p.classList.add(CSS_CLASSES.TEXT_FOCUS_INPUT_PARENT);
          this._textFocusStyledElements.add(p);
        } catch { /* ignore */ }
      }
    }
  }

  _clearTextHoverElementStyling() {
    if (!this._textHoverStyledElements || this._textHoverStyledElements.size === 0) {
      this._textHoverCurrentElement = null;
      return;
    }
    try {
      for (const el of this._textHoverStyledElements) {
        if (!el || el.nodeType !== 1) continue;
        try {
          el.classList.remove(CSS_CLASSES.TEXT_HOVER_INPUT);
          el.classList.remove(CSS_CLASSES.TEXT_HOVER_INPUT_PARENT);
        } catch { /* ignore */ }
      }
    } finally {
      this._textHoverStyledElements.clear();
      this._textHoverCurrentElement = null;
    }
  }

  _applyTextHoverElementStyling(inputEl) {
    if (!inputEl || inputEl.nodeType !== 1) {
      this._clearTextHoverElementStyling();
      return;
    }

    // Avoid thrashing while mouse is steady.
    if (this._textHoverCurrentElement === inputEl && this._textHoverStyledElements.size > 0) {
      try { inputEl.classList.add(CSS_CLASSES.TEXT_HOVER_INPUT); } catch { /* ignore */ }
      return;
    }

    this._clearTextHoverElementStyling();
    this._textHoverCurrentElement = inputEl;

    try {
      this._ensureStylesForElement(inputEl);
      inputEl.classList.add(CSS_CLASSES.TEXT_HOVER_INPUT);
      this._textHoverStyledElements.add(inputEl);
    } catch { /* ignore */ }

    // Hover is outline + SVG hint only — do not tint wrapper parents.
  }

  setupOverlayObserver() {
    // Observer to optimize overlay rendering when out of view
    this.overlayObserver = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          const overlay = entry.target;
          const isVisible = entry.intersectionRatio > 0;
          
          // Optimize rendering by hiding completely out-of-view overlays
          if (overlay === this.focusOverlay) {
            this.overlayVisibility.focus = isVisible;
            // Don't hide focus overlay when DOM hover colors are enabled (it should always be visible when shown)
            if (!this._useDomHoverFocusColors) {
              overlay.style.visibility = isVisible ? 'visible' : 'hidden';
            } else {
              overlay.style.visibility = 'visible';
            }
          } else if (overlay === this.inspectorOverlay || overlay === this.deleteOverlay) {
            this.overlayVisibility.delete = isVisible;
            overlay.style.visibility = isVisible ? 'visible' : 'hidden';
          } else if (overlay === this.escExitLabelText) {
            this.overlayVisibility.escExitLabel = isVisible;
            overlay.style.visibility = isVisible ? 'visible' : 'hidden';
          } else if (overlay === this.escExitLabelHover) {
            this.overlayVisibility.escExitLabel = isVisible;
            overlay.style.visibility = isVisible ? 'visible' : 'hidden';
          }
        });
      },
      {
        rootMargin: '10px',
        threshold: [0, 1.0]
      }
    );
  }

  /**
   * @param {Element|null} focusEl
   * @param {Element|null} inspectorEl shared pick target (Delete/Cols/…)
   * @param {string} mode
   * @param {Element|null} [focusedTextElement]
   * @param {DOMRect|object|null} [focusRectOverride]
   * @param {string|null} [inspectorKind] INSPECTOR_KIND when mode is inspector
   */
  updateOverlays(focusEl, inspectorEl, mode, focusedTextElement = null, focusRectOverride = null, inspectorKind = null) {
    // Debug logging when debug mode is enabled
    if (window.KEYPILOT_DEBUG && focusEl) {
      console.log('[KeyPilot Debug] Updating overlays:', {
        focusElement: focusEl.tagName,
        mode: mode,
        willShowFocus: mode === 'none' || mode === 'text_focus' || mode === 'highlight' || mode === 'popover',
        focusedTextElement: focusedTextElement?.tagName
      });
    }
    
    // Show focus overlay in normal mode, text focus mode, highlight mode, AND popover mode.
    // Popovers are modal but still need the green rectangle so the user can F-click UI
    // affordances like the close (×) button.
    // Hide green focus rect while in shared inspector pick mode.
    if (mode === 'none' || mode === 'text_focus' || mode === 'highlight' || mode === 'popover') {
      this.updateFocusOverlay(focusEl, mode, focusRectOverride);
      
      if (mode === 'text_focus') {
        // Labels are attached to the focused text field, not the hovered element.
        if (focusedTextElement) this.updateTextModeLabels(focusedTextElement);
        else this.hideTextModeLabels();
      } else {
        this.hideTextModeLabels();
      }
    } else {
      this.hideFocusOverlay();
    }

    // If there's no focus target at all, ensure we remove any hover tint.
    if (!focusEl) {
      this._clearTextHoverElementStyling();
    }
    
    // Text focus mode styling: tint the actual focused input (and nearby wrapper parents)
    // instead of drawing an orange frame overlay.
    if (mode === 'text_focus' && focusedTextElement) {
      this._applyTextFocusElementStyling(focusedTextElement);
    } else {
      this._clearTextFocusElementStyling();
    }

    // Show viewport modal frame when in text focus mode (controlled by flag)
    this.updateViewportModalFrame(mode === 'text_focus' && FEATURE_FLAGS.SHOW_WINDOW_OUTLINE);
    
    // Shared inspector pick overlay (Delete, Cols, future kinds)
    const inInspector = mode === MODES.INSPECTOR || mode === 'delete' || mode === 'cols';
    if (inInspector) {
      const kind = inspectorKind
        || (mode === 'delete' ? 'delete' : mode === 'cols' ? 'cols' : null);
      this.updateInspectorOverlay(inspectorEl, kind);
      // Keep top-right instruction visible while pick is active
      this.showInspectorModeIndicator({ kind });
    } else {
      this.hideInspectorOverlay();
      this.hideInspectorModeIndicator();
      this.clearInspectorPickedOverlays();
    }
    
    // Show highlight chrome in highlight mode (instruction + optional focus ring)
    if (mode === 'highlight') {
      this.highlightManager.updateHighlightOverlay(focusEl);
      // finishKey is set explicitly in startHighlighting; keep indicator visible here
      this.highlightManager.showHighlightModeIndicator();
    } else {
      this.highlightManager.hideHighlightOverlay();
      this.highlightManager.hideHighlightModeIndicator();
      // Leaving highlight: drop any leftover dashed guide (cancel also clears this)
      this.highlightManager.hideHighlightRectangleOverlay();
    }
  }

  // Unified interface that switches between rendering modes
  updateFocusOverlay(element, mode = MODES.NONE, rectOverride = null) {
    // Remember target for activation pulse (F-click), independent of render backend.
    try {
      this._lastFocusElement = element || null;
      if (element) {
        const r = (rectOverride && typeof rectOverride === 'object')
          ? rectOverride
          : this.getBestRect(element);
        this._lastFocusRect = r && r.width > 0 && r.height > 0
          ? { left: r.left, top: r.top, width: r.width, height: r.height }
          : null;
      } else {
        this._lastFocusRect = null;
      }
    } catch {
      this._lastFocusElement = element || null;
      this._lastFocusRect = null;
    }

    // Text inputs: show orange outline AND paint the SVG "Press F to select…" hint.
    // (Do not return early — fall through so the focus rectangle still draws.)
    try {
      const isTextInput = element && element.matches && element.matches(SELECTORS.FOCUSABLE_TEXT);
      if (isTextInput) {
        this._applyTextHoverElementStyling(element);
      } else {
        // Non-text elements: ensure we remove any lingering hover hint styling.
        this._clearTextHoverElementStyling();
      }
    } catch {
      try { this._clearTextHoverElementStyling(); } catch { /* ignore */ }
    }

    // DOM-hover paint preference (see focus-ring-paint.md):
    //   A = DOM outline; B = in-target ring (maxZ+1); C = body fixed overlay.
    if (this._useDomHoverFocusColors) {
      try { this.hideFocusOverlayCanvas(); } catch { /* ignore */ }
      try { this.hideFocusOverlayCSSCustomProps(); } catch { /* ignore */ }

      if (!element) {
        this._focusPaintUsesFixedOverlay = false;
        this._focusPaintUsesInTargetRing = false;
        try { this.clearElementFocusStyling(); } catch { /* ignore */ }
        try { this.hideInTargetFocusRing(); } catch { /* ignore */ }
        try { this.hideFocusOverlayDOM(); } catch { /* ignore */ }
        try {
          this._updateShadowRootDebugHud(null, null, {
            inShadow: false,
            autoStrategy: '—',
            appliedStrategy: '—',
            override: this._shadowDebugPaintOverride
          });
        } catch { /* ignore */ }
        return;
      }

      // Resolve paint node once (pierces open shadow for collapsed hosts).
      let paintEl = element;
      try {
        paintEl = this._resolveElementForFocusStyling(element) || element;
      } catch {
        paintEl = element;
      }

      // Auto strategy (see focus-ring-paint.md):
      //   Light DOM: A → B → C (escape hatch only when A cannot show).
      //   Shadow targets (in a ShadowRoot, or open-shadow host): skip A,
      //   default B (in-target), else C. Debug HUD can still force A/B/C.
      let inShadow = false;
      try { inShadow = this._isInShadowTree(element); } catch { inShadow = false; }
      let isShadowHost = false;
      try { isShadowHost = !!this._getOpenShadowRoot(element); } catch { isShadowHost = false; }
      const shadowPaint = inShadow || isShadowHost;

      let needsEscapeHatch = false;
      try {
        needsEscapeHatch = this._shouldUseFixedFocusOverlay(element);
      } catch {
        needsEscapeHatch = false;
      }

      let autoStrategy = 'A';
      if (shadowPaint || needsEscapeHatch) {
        let canB = false;
        try {
          canB = !!(FEATURE_FLAGS && FEATURE_FLAGS.ENABLE_IN_TARGET_FOCUS_RING) &&
            !!this._resolveInTargetHost(element);
        } catch { canB = false; }
        autoStrategy = canB ? 'B' : 'C';
      }

      // Debug HUD can force A / B / C regardless of auto (for shadow experiments).
      const override = this._shadowDebugHudEnabled
        ? this._shadowDebugPaintOverride
        : null;
      const strategy = (override === 'A' || override === 'B' || override === 'C')
        ? override
        : autoStrategy;

      try {
        this._updateShadowRootDebugHud(element, paintEl, {
          inShadow,
          autoStrategy,
          appliedStrategy: strategy,
          override
        });
      } catch { /* ignore */ }

      if (strategy === 'A') {
        this._focusPaintUsesFixedOverlay = false;
        this._focusPaintUsesInTargetRing = false;
        try { this.hideInTargetFocusRing(); } catch { /* ignore */ }
        try { this.hideFocusOverlayDOM(); } catch { /* ignore */ }
        return this.updateFocusOverlayElementStyling(element, mode);
      }

      try { this.clearElementFocusStyling(); } catch { /* ignore */ }

      if (strategy === 'B') {
        let usedInTarget = false;
        try {
          usedInTarget = this.updateFocusOverlayInTarget(element, mode);
        } catch {
          usedInTarget = false;
        }
        if (usedInTarget) {
          this._focusPaintUsesInTargetRing = true;
          this._focusPaintUsesFixedOverlay = false;
          try { this.hideFocusOverlayDOM(); } catch { /* ignore */ }
          try {
            this._updateShadowRootDebugHud(element, paintEl, {
              inShadow,
              autoStrategy,
              appliedStrategy: 'B',
              override
            });
          } catch { /* ignore */ }
          return;
        }
        // B failed to mount — fall through to C.
      }

      // Strategy C: body fixed overlay.
      this._focusPaintUsesInTargetRing = false;
      try { this.hideInTargetFocusRing(); } catch { /* ignore */ }
      this._focusPaintUsesFixedOverlay = true;
      const paintForC = rectOverride ? element : paintEl;
      const cResult = this.updateFocusOverlayDOM(paintForC, mode, rectOverride);
      try {
        this._updateShadowRootDebugHud(element, paintEl, {
          inShadow,
          autoStrategy,
          appliedStrategy: 'C',
          override
        });
      } catch { /* ignore */ }
      return cResult;
    }

    switch (this.renderingMode) {
      case 'canvas':
        return this.updateFocusOverlayCanvas(element, mode, rectOverride);
      case 'css-custom-props':
        return this.updateFocusOverlayCSSCustomProps(element, mode, rectOverride);
      case 'dom':
      default:
        return this.updateFocusOverlayDOM(element, mode, rectOverride);
    }
  }

  _installFocusClipCacheInvalidation() {
    if (this._focusClipInvalidateBound) return;
    this._focusClipInvalidateBound = () => {
      try {
        // WeakMap cannot be cleared; replace instance so entries are GC'd with elements.
        this._focusClipContextCache = new WeakMap();
      } catch { /* ignore */ }
    };
    try {
      window.addEventListener('resize', this._focusClipInvalidateBound, { passive: true });
    } catch { /* ignore */ }
    try {
      document.addEventListener('keypilot:scroll-end', this._focusClipInvalidateBound, { passive: true });
    } catch { /* ignore */ }
  }

  /**
   * Find ancestors that would clip an outer focus ring around `element`.
   *
   * Used only when FEATURE_FLAGS.ENABLE_FOCUS_CLIP_INSET and/or
   * ENABLE_FOCUS_TIGHT_WRAPPER_PROMOTION are on (see constants.js for tentative
   * purpose). Never opens page overflow — that broke carousels (IMDb).
   *
   * Results are cached briefly (WeakMap + TTL + geometry fingerprint) so hover
   * thrash does not re-walk getComputedStyle ancestors every paint.
   *
   * @param {Element} element
   * @returns {{
   *   clippers: Element[],
   *   tightWrapper: Element|null
   * }}
   *   clippers — ancestors that would clip a positive outline-offset ring
   *   tightWrapper — nearest clipper nearly the same size as the target
   */
  _findFocusClipContext(element) {
    /** @type {Element[]} */
    const emptyClippers = [];
    /** @type {Element|null} */
    const emptyTight = null;

    if (!element || element.nodeType !== 1) {
      return { clippers: emptyClippers, tightWrapper: emptyTight };
    }

    let er;
    try {
      er = element.getBoundingClientRect();
    } catch {
      return { clippers: emptyClippers, tightWrapper: emptyTight };
    }
    if (!er || !(er.width > 0) || !(er.height > 0)) {
      return { clippers: emptyClippers, tightWrapper: emptyTight };
    }

    // Cache hit: same element, recent, geometry nearly unchanged.
    try {
      const cached = this._focusClipContextCache?.get?.(element);
      if (cached) {
        const age = Date.now() - (cached.ts || 0);
        const geoClose =
          Math.abs(cached.left - er.left) < 1 &&
          Math.abs(cached.top - er.top) < 1 &&
          Math.abs(cached.width - er.width) < 1 &&
          Math.abs(cached.height - er.height) < 1;
        if (age < (this._focusClipContextTtlMs || 500) && geoClose) {
          return { clippers: cached.clippers, tightWrapper: cached.tightWrapper };
        }
      }
    } catch { /* ignore */ }

    /** @type {Element[]} */
    const clippers = [];
    /** @type {Element|null} */
    let tightWrapper = null;

    // Outer ring needs a little space outside the border box.
    const pad = 8;
    const needLeft = er.left - pad;
    const needTop = er.top - pad;
    const needRight = er.right + pad;
    const needBottom = er.bottom + pad;

    // Walk composed ancestors (parentElement + open shadow host hops).
    // archive.org nests clickables inside media-button → … → NAV[overflow:hidden];
    // a parentElement-only walk stops at the shadow root and misses clippers.
    try {
      let n = this._composedParent(element);
      let depth = 0;
      // Nested open shadows (archive.org tiles/nav) often need more than 12 hops.
      while (n && n.nodeType === 1 && depth++ < 24) {
        if (n === document.documentElement || n === document.body) {
          n = this._composedParent(n);
          continue;
        }

        const cs = window.getComputedStyle(n);
        if (!cs) {
          n = this._composedParent(n);
          continue;
        }

        const ox = cs.overflowX;
        const oy = cs.overflowY;
        const clipsOverflow =
          (ox && ox !== 'visible') || (oy && oy !== 'visible');

        let cvClip = false;
        try {
          const cv = cs.contentVisibility || cs.getPropertyValue('content-visibility');
          cvClip = cv === 'auto' || cv === 'hidden';
        } catch { /* ignore */ }

        let containClip = false;
        try {
          const contain = String(cs.contain || '');
          containClip =
            contain.includes('paint') ||
            contain.includes('strict') ||
            contain === 'content' ||
            contain.split(/\s+/).includes('content');
        } catch { /* ignore */ }

        if (clipsOverflow || cvClip || containClip) {
          let ar;
          try {
            ar = n.getBoundingClientRect();
          } catch {
            clippers.push(n);
            n = this._composedParent(n);
            continue;
          }

          const clipsRing =
            ar.left > needLeft + 0.5 ||
            ar.top > needTop + 0.5 ||
            ar.right < needRight - 0.5 ||
            ar.bottom < needBottom - 0.5;

          const similarSize =
            Math.abs(ar.width - er.width) < 20 &&
            Math.abs(ar.height - er.height) < 20;

          if (clipsRing || (cvClip && similarSize)) {
            clippers.push(n);
            if (!tightWrapper && similarSize) {
              tightWrapper = n;
            }
          }
        }

        // Slotted shadow hosts (msn.com `cs-responsive-card`: shadow is just
        // `div.root > slot`) clip via an *internal* wrapper, not the host's own
        // light-DOM-facing style — getComputedStyle(n) above never sees it.
        // Check the shadow root's own top-level children too.
        try {
          const extra = this._shadowInternalClipWrappers(n, er, needLeft, needTop, needRight, needBottom);
          for (let wi = 0; wi < extra.clippers.length; wi++) {
            clippers.push(extra.clippers[wi]);
          }
          if (!tightWrapper && extra.tightWrapper) {
            tightWrapper = extra.tightWrapper;
          }
        } catch { /* ignore */ }

        n = this._composedParent(n);
      }
    } catch { /* ignore */ }

    try {
      this._focusClipContextCache?.set?.(element, {
        clippers,
        tightWrapper,
        ts: Date.now(),
        left: er.left,
        top: er.top,
        width: er.width,
        height: er.height
      });
    } catch { /* ignore */ }

    return { clippers, tightWrapper };
  }

  /**
   * True when an ancestor is likely to clip *outer* focus chrome.
   * @param {Element} element
   * @returns {boolean}
   */
  _isProbablyClippedByAncestorOverflow(element) {
    try {
      const ctx = this._findFocusClipContext(element);
      return !!(ctx.clippers && ctx.clippers.length);
    } catch {
      return false;
    }
  }

  /**
   * Rect-based check: would a positive outline-offset ring around the hover
   * target be clipped (or sit flush with zero room) inside a clipping parent?
   *
   * Compares `getBoundingClientRect()` of the paint target to each overflow/
   * contain clip ancestor. If the target box is inset-or-flush with a clipper
   * such that outline-offset > 0 has nowhere to paint, return true.
   *
   * This alone does **not** mean we should use a fixed overlay — inset element
   * outlines often still work (control strip segments, toolbar buttons).
   * See `_shouldUseFixedFocusOverlay`.
   *
   * @param {Element} element - hover focus target (clickable)
   * @returns {boolean}
   */
  _outerFocusRingWouldBeClipped(element) {
    if (!element || element.nodeType !== 1) return false;

    let paintEl = element;
    try {
      paintEl = this._resolveElementForFocusStyling(element) || element;
    } catch {
      paintEl = element;
    }

    let er = null;
    try {
      er = paintEl.getBoundingClientRect();
    } catch {
      return false;
    }
    if (!er || !(er.width > 0) || !(er.height > 0)) return false;

    // How much room we need outside the border box for a visible outer ring.
    // Matches _findFocusClipContext pad; outline-offset is typically 2px plus
    // a few px of border thickness from settings.
    const pad = 8;

    let ctx = { clippers: [], tightWrapper: null };
    try {
      ctx = this._findFocusClipContext(paintEl);
    } catch {
      ctx = { clippers: [], tightWrapper: null };
    }

    const clippers = (ctx && ctx.clippers) || [];
    if (!clippers.length) return false;

    // Ancestor rect check: any clipper whose box does not fully contain
    // (target ± pad) means the outer ring would be cut off.
    for (let i = 0; i < clippers.length; i++) {
      const c = clippers[i];
      if (!c || c.nodeType !== 1) continue;
      let ar = null;
      try { ar = c.getBoundingClientRect(); } catch { ar = null; }
      if (!ar) continue;

      const roomLeft = er.left - ar.left;
      const roomTop = er.top - ar.top;
      const roomRight = ar.right - er.right;
      const roomBottom = ar.bottom - er.bottom;

      // Flush or inset (room < pad on any side) → outer outline clipped.
      if (
        roomLeft < pad - 0.5 ||
        roomTop < pad - 0.5 ||
        roomRight < pad - 0.5 ||
        roomBottom < pad - 0.5
      ) {
        return true;
      }
    }

    return false;
  }

  /**
   * True when computed overflow would clip descendants / outer decorations.
   * @param {CSSStyleDeclaration|null|undefined} cs
   * @returns {boolean}
   */
  _styleClipsOverflow(cs) {
    return !!(cs && (
      (cs.overflow && cs.overflow !== 'visible') ||
      (cs.overflowX && cs.overflowX !== 'visible') ||
      (cs.overflowY && cs.overflowY !== 'visible')
    ));
  }

  /**
   * True when CSS containment clips paint/ink (including outer outlines) to the
   * padding edge. `contain: content` implies paint containment — common on
   * msn.com card shells (`div.root { contain: content }`).
   * @param {CSSStyleDeclaration|null|undefined} cs
   * @returns {boolean}
   */
  _styleClipsPaintContain(cs) {
    if (!cs) return false;
    try {
      const contain = String(cs.contain || '');
      if (!contain || contain === 'none') return false;
      return (
        contain.includes('paint') ||
        contain.includes('strict') ||
        contain === 'content' ||
        contain.split(/\s+/).includes('content')
      );
    } catch {
      return false;
    }
  }

  /**
   * Overflow or paint-containment clip (self / wrapper).
   * @param {CSSStyleDeclaration|null|undefined} cs
   * @returns {boolean}
   */
  _styleClipsSelf(cs) {
    return this._styleClipsOverflow(cs) || this._styleClipsPaintContain(cs);
  }

  /**
   * Parent across open shadow boundaries (parentElement, else ShadowRoot.host).
   * @param {Node|null|undefined} node
   * @returns {Element|null}
   */
  _composedParent(node) {
    if (!node || node.nodeType !== 1) return null;
    if (node.parentElement) return node.parentElement;
    try {
      const root = typeof node.getRootNode === 'function' ? node.getRootNode() : null;
      if (root && typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot) {
        return root.host || null;
      }
    } catch { /* ignore */ }
    return null;
  }

  /**
   * Detect clip ancestors hiding *inside* an open shadow host's own root —
   * e.g. msn.com `cs-responsive-card` shadow is `div.root{overflow:hidden} > slot`.
   * The slotted (light-DOM) content is a child of the *host* in the light tree,
   * so `getComputedStyle(host)` never sees the wrapper's `overflow`/`contain`,
   * even though it visually clips the slotted content in the composed tree.
   *
   * Only checks the shadow root's direct children (the common single-wrapper
   * pattern) — not a full recursive walk — to stay cheap on hover.
   *
   * @param {Element} host - composed ancestor being visited by _findFocusClipContext
   * @param {DOMRect} er - target element's rect
   * @param {number} needLeft
   * @param {number} needTop
   * @param {number} needRight
   * @param {number} needBottom
   * @returns {{ clippers: Element[], tightWrapper: Element|null }}
   */
  _shadowInternalClipWrappers(host, er, needLeft, needTop, needRight, needBottom) {
    const clippers = [];
    let tightWrapper = null;
    const sr = this._getOpenShadowRoot(host);
    if (!sr) return { clippers, tightWrapper };

    let kids;
    try {
      kids = sr.children;
    } catch {
      return { clippers, tightWrapper };
    }
    if (!kids) return { clippers, tightWrapper };

    for (let i = 0; i < kids.length && i < 6; i++) {
      const w = kids[i];
      if (!w || w.nodeType !== 1) continue;
      if ((w.tagName || '').toUpperCase() === 'STYLE') continue;

      let wcs = null;
      try { wcs = window.getComputedStyle(w); } catch { wcs = null; }
      // Overflow *or* paint containment (msn.com `div.root { contain: content }`).
      if (!this._styleClipsSelf(wcs)) continue;

      let war = null;
      try { war = w.getBoundingClientRect(); } catch { war = null; }
      if (!war) continue;

      const clipsRing =
        war.left > needLeft + 0.5 ||
        war.top > needTop + 0.5 ||
        war.right < needRight - 0.5 ||
        war.bottom < needBottom - 0.5;
      const similarSize =
        Math.abs(war.width - er.width) < 20 &&
        Math.abs(war.height - er.height) < 20;

      if (clipsRing) {
        clippers.push(w);
        if (!tightWrapper && similarSize) tightWrapper = w;
      }
    }

    return { clippers, tightWrapper };
  }

  /**
   * True when `el` is itself an open-shadow host whose top-level shadow
   * wrapper clips (`overflow`/`contain`), even though `el`'s own light-DOM-
   * facing computed style does not. Boolean sibling of
   * `_shadowInternalClipWrappers` for callers that only need presence, not
   * outline-room rect math (e.g. `_shouldUseFixedFocusOverlay`'s selfClips).
   * @param {Element} el
   * @returns {boolean}
   */
  _hostClipsViaInternalShadowWrapper(el) {
    const sr = this._getOpenShadowRoot(el);
    if (!sr) return false;
    let kids;
    try {
      kids = sr.children;
    } catch {
      return false;
    }
    if (!kids) return false;
    for (let i = 0; i < kids.length && i < 6; i++) {
      const w = kids[i];
      if (!w || w.nodeType !== 1) continue;
      if ((w.tagName || '').toUpperCase() === 'STYLE') continue;
      let wcs = null;
      try { wcs = window.getComputedStyle(w); } catch { wcs = null; }
      if (this._styleClipsSelf(wcs)) return true;
    }
    return false;
  }

  /**
   * True when `el` lives under a ShadowRoot (open or closed).
   * @param {Node|null|undefined} el
   * @returns {boolean}
   */
  _isInShadowTree(el) {
    if (!el || typeof el.getRootNode !== 'function') return false;
    try {
      const root = el.getRootNode();
      return !!(typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot);
    } catch {
      return false;
    }
  }

  /**
   * Open shadow root on `el`, if any.
   * @param {Element|null|undefined} el
   * @returns {ShadowRoot|null}
   */
  _getOpenShadowRoot(el) {
    if (!el || el.nodeType !== 1) return null;
    try {
      return el.shadowRoot || null;
    } catch {
      return null;
    }
  }

  /**
   * True when an open shadow has a default (unnamed) slot that can project
   * light-DOM children. Slotless Lit hosts (archive.org media-button /
   * collection-tile) render light children with 0×0 — strategy B must not
   * mount there.
   * @param {ShadowRoot|null|undefined} shadowRoot
   * @returns {boolean}
   */
  _shadowHasDefaultSlot(shadowRoot) {
    if (!shadowRoot || typeof shadowRoot.querySelectorAll !== 'function') return false;
    try {
      const slots = shadowRoot.querySelectorAll('slot');
      for (let i = 0; i < slots.length; i++) {
        const slot = slots[i];
        if (!slot) continue;
        const name = (slot.getAttribute && slot.getAttribute('name')) || '';
        if (!name) return true;
      }
    } catch { /* ignore */ }
    return false;
  }

  /**
   * True when appending a child to `host` will participate in layout/paint.
   * Light DOM elements: yes. Open shadow hosts: only with a default slot.
   * @param {Element|null|undefined} host
   * @returns {boolean}
   */
  _canMountVisibleChildOnHost(host) {
    if (!host || host.nodeType !== 1) return false;
    if (this._isReplacedOrVoidElement(host)) return false;
    if (typeof host.appendChild !== 'function') return false;
    const sr = this._getOpenShadowRoot(host);
    if (!sr) return true;
    return this._shadowHasDefaultSlot(sr);
  }

  /**
   * Push light children and (for open shadow hosts) top-level shadow children
   * onto `queue`, skipping KeyPilot injected style nodes.
   * @param {Element} el
   * @param {Element[]} queue
   */
  _enqueueShadowPiercingChildren(el, queue) {
    if (!el || el.nodeType !== 1 || !queue) return;
    try {
      const kids = el.children;
      if (kids) {
        for (let i = 0; i < kids.length; i++) {
          if (kids[i]?.nodeType === 1) queue.push(kids[i]);
        }
      }
    } catch { /* ignore */ }

    const sr = this._getOpenShadowRoot(el);
    if (!sr) return;
    try {
      const skids = sr.children;
      if (!skids) return;
      for (let i = 0; i < skids.length; i++) {
        const k = skids[i];
        if (!k || k.nodeType !== 1) continue;
        try {
          if (k.id === 'keypilot-shadow-styles' || k.id === 'kp-probe-style') continue;
          if ((k.tagName || '').toUpperCase() === 'STYLE' &&
              typeof k.id === 'string' && k.id.startsWith('keypilot')) {
            continue;
          }
        } catch { /* ignore */ }
        queue.push(k);
      }
    } catch { /* ignore */ }
  }

  /**
   * Inside an open-shadow host, find a non-replaced element that can hold the
   * in-target ring (strategy B). Prefers the largest visible box.
   * @param {Element} host
   * @returns {Element|null}
   */
  _findInShadowInTargetMount(host) {
    const sr = this._getOpenShadowRoot(host);
    if (!sr) return null;

    let best = null;
    let bestArea = 0;
    let visited = 0;
    const maxNodes = 48;
    /** @type {Element[]} */
    const queue = [];
    this._enqueueShadowPiercingChildren(host, queue);

    while (queue.length && visited < maxNodes) {
      const cur = queue.shift();
      if (!cur || cur.nodeType !== 1) continue;
      visited++;

      if (!this._isReplacedOrVoidElement(cur) && this._canMountVisibleChildOnHost(cur)) {
        let r = null;
        try { r = cur.getBoundingClientRect(); } catch { r = null; }
        if (r && r.width >= 8 && r.height >= 8) {
          const area = r.width * r.height;
          if (area > bestArea) {
            bestArea = area;
            best = cur;
          }
        }
      }

      this._enqueueShadowPiercingChildren(cur, queue);
    }

    return best;
  }

  /**
   * Preferred outer outline-offset (px) when clip ancestors leave enough room.
   * Matches the historical path-A default.
   */
  _preferredFocusOutlineOffsetPx() {
    return 2;
  }

  /**
   * Minimum free space outside the border box across clipping ancestors.
   * Infinity when there are no clippers (outer ring unconstrained).
   *
   * @param {Element} paintEl
   * @returns {number}
   */
  _minFocusOutlineRoomPx(paintEl) {
    if (!paintEl || paintEl.nodeType !== 1) return Infinity;

    let er = null;
    try {
      er = paintEl.getBoundingClientRect();
    } catch {
      return Infinity;
    }
    if (!er || !(er.width > 0) || !(er.height > 0)) return Infinity;

    let clippers = [];
    try {
      const ctx = this._findFocusClipContext(paintEl);
      clippers = (ctx && ctx.clippers) || [];
    } catch {
      clippers = [];
    }
    if (!clippers.length) {
      // Fallback: treat "probably clipped" ancestors as zero-room flush case
      // only when the dedicated walk found nothing but the heuristic disagrees.
      try {
        if (this._isProbablyClippedByAncestorOverflow(paintEl) && clippers.length === 0) {
          // Heuristic without enumerated clippers — don't invent a number;
          // graded path will still use preferred outer unless clippers exist.
        }
      } catch { /* ignore */ }
      return Infinity;
    }

    let minRoom = Infinity;
    for (let i = 0; i < clippers.length; i++) {
      const c = clippers[i];
      if (!c || c.nodeType !== 1) continue;
      let ar = null;
      try { ar = c.getBoundingClientRect(); } catch { ar = null; }
      if (!ar) continue;
      const roomLeft = er.left - ar.left;
      const roomTop = er.top - ar.top;
      const roomRight = ar.right - er.right;
      const roomBottom = ar.bottom - er.bottom;
      minRoom = Math.min(minRoom, roomLeft, roomTop, roomRight, roomBottom);
    }
    return Number.isFinite(minRoom) ? minRoom : Infinity;
  }

  /**
   * Graded outline-offset (px) for path A.
   *
   * Outer ring needs roughly (offset + stroke) outside the border box.
   * Given free room R outside the box to the nearest clip edge:
   *   offset = clamp(R - stroke, -stroke, preferredOuter)
   *
   * Examples (stroke 3, preferred outer 2):
   *   R ≥ 5  → +2 (full outer)
   *   R = 4  → +1
   *   R = 1  → -2 (mild inset; 1px bleed does not force full -3)
   *   R ≤ 0  → -3 (full inset)
   *
   * @param {Element} paintEl
   * @param {number} [ringWidthPx]
   * @returns {number}
   */
  _computeGradedFocusOutlineOffset(paintEl, ringWidthPx = 3) {
    const preferred = this._preferredFocusOutlineOffsetPx();
    const stroke = Math.min(Math.max(Number(ringWidthPx) || 3, 1), 16);

    if (!(FEATURE_FLAGS && FEATURE_FLAGS.ENABLE_FOCUS_CLIP_INSET)) {
      return preferred;
    }
    if (!paintEl || paintEl.nodeType !== 1) return preferred;

    const room = this._minFocusOutlineRoomPx(paintEl);
    if (!Number.isFinite(room) || room === Infinity) {
      return preferred;
    }

    // Subpixel slack so 0.4px room does not look like free outer space.
    const available = room - 0.5;
    let offset = available - stroke;
    if (offset > preferred) offset = preferred;
    if (offset < -stroke) offset = -stroke;

    // Snap near-integers for stable CSS (avoid 1.999999px thrash).
    const rounded = Math.round(offset * 100) / 100;
    return rounded;
  }

  /**
   * True when graded path-A offset paints **inside** the border box (negative
   * offset). Outer/zero offset is not covered by full-bleed children; negative
   * inset can be. Used to gate escape hatch B/C.
   *
   * @param {Element} paintEl
   * @param {number} [ringWidthPx]
   * @returns {boolean}
   */
  _wouldUseInsetFocusOutline(paintEl, ringWidthPx) {
    if (!paintEl || paintEl.nodeType !== 1) return false;
    if (!(FEATURE_FLAGS && FEATURE_FLAGS.ENABLE_FOCUS_CLIP_INSET)) {
      return false;
    }
    let stroke = ringWidthPx;
    if (stroke == null || !Number.isFinite(Number(stroke))) {
      try {
        stroke = this._getClickModeSettings().rectangleThickness;
      } catch {
        stroke = 3;
      }
    }
    const offset = this._computeGradedFocusOutlineOffset(paintEl, stroke);
    return offset < -0.25;
  }

  /**
   * True when **element-level** focus paint (strategy A) cannot show a visible
   * ring, so we must use an escape hatch (strategy B in-target, else C body fixed).
   *
   * Policy (outline-first = A preferred):
   * - Outer outline clipped by a parent alone → still use **graded element
   *   inset** on A (ENABLE_FOCUS_CLIP_INSET). Example: control-strip buttons
   *   inside `overflow:hidden` shells. Body fixed (C) would also sit under high
   *   z-index KP chrome (strip z > OVERLAYS) and stay invisible.
   * - Target clips itself **and** is covered by full-bleed media/pseudos →
   *   inset outline is under the cover; use B/C (TNW cards, etc.).
   * - Same when cover is only an **edge media strip** (msn.com card image on the
   *   top half): inset outline shows on the text half and vanishes under media.
   * - Target does not clip, but a full-size child stacking surface would cover
   *   an **inset** outline (e.g. newtab top-site tiles inside a scroller that
   *   forces inset). If outer outline still has room, stay on A — do not
   *   treat "has media child" alone as failure (ganjingworld thumbnails).
   *
   * @param {Element} element
   * @returns {boolean}
   */
  _shouldUseFixedFocusOverlay(element) {
    if (!element || element.nodeType !== 1) return false;

    let paintEl = element;
    try {
      paintEl = this._resolveElementForFocusStyling(element) || element;
    } catch {
      paintEl = element;
    }

    let er = null;
    try {
      er = paintEl.getBoundingClientRect();
    } catch {
      return false;
    }
    if (!er || !(er.width > 0) || !(er.height > 0)) return false;

    let selfClips = false;
    try {
      selfClips = this._styleClipsSelf(window.getComputedStyle(paintEl));
    } catch { /* ignore */ }
    // Shadow host whose *own* light-DOM style is non-clipping but whose
    // internal shadow wrapper clips (msn.com cs-responsive-card pattern:
    // overflow:hidden and/or contain:content on div.root).
    if (!selfClips) {
      try {
        selfClips = this._hostClipsViaInternalShadowWrapper(paintEl);
      } catch { /* ignore */ }
    }

    // Inset outline on the element is invisible when the element itself clips
    // and is painted over by full-bleed *or* edge-flush media (top-image cards).
    if (
      selfClips &&
      (this._hasFullBleedCoveringContent(paintEl, er) ||
        this._hasEdgeFlushMediaCover(paintEl, er))
    ) {
      return true;
    }

    // Full-bleed / edge-strip stacking child only defeats **inset** outline.
    // Outer outline paints outside the border box and remains visible
    // (ganjingworld, many video grids). Only escape when path A would inset.
    if (
      this._hasObscuringFullBleedChild(paintEl, er) ||
      this._hasEdgeFlushMediaCover(paintEl, er)
    ) {
      if (this._wouldUseInsetFocusOutline(paintEl)) {
        return true;
      }
    }

    // Shadow skip-A → B is decided in updateFocusOverlay (not here). This
    // helper only answers whether light-DOM geometry needs an escape hatch.

    return false;
  }

  /**
   * Media-like node that can paint over an inset focus outline.
   * @param {Element} el
   * @returns {boolean}
   */
  _isMediaLikeCoverElement(el) {
    if (!el || el.nodeType !== 1) return false;
    const tag = (el.tagName || '').toUpperCase();
    if (
      tag === 'IMG' ||
      tag === 'VIDEO' ||
      tag === 'PICTURE' ||
      tag === 'CANVAS' ||
      tag === 'SVG'
    ) {
      return true;
    }
    try {
      const cs = window.getComputedStyle(el);
      const bg = cs && cs.backgroundImage;
      if (bg && bg !== 'none') return true;
    } catch { /* ignore */ }
    return false;
  }

  /**
   * True when a media surface covers a substantial edge strip of `el` (e.g.
   * card hero image on the top ~half). Full-bleed (85%×85%) is handled by
   * `_hasFullBleedCoveringContent`; this catches the common "media header"
   * layout where strategy A inset outlines only survive on the text half.
   *
   * Walks direct shadow-piercing children plus one level into near-full
   * wrappers (card → `.root` → `img.media`). Does **not** alone force B/C —
   * callers still require self-clip or graded inset.
   *
   * @param {Element} el
   * @param {DOMRect|ClientRect} [er]
   * @returns {boolean}
   */
  _hasEdgeFlushMediaCover(el, er) {
    if (!el || el.nodeType !== 1) return false;
    let box = er;
    try {
      if (!box) box = el.getBoundingClientRect();
    } catch {
      return false;
    }
    if (!box || !(box.width > 0) || !(box.height > 0)) return false;

    /** @type {Element[]} */
    const candidates = [];
    try {
      this._enqueueShadowPiercingChildren(el, candidates);
      const nDirect = candidates.length;
      for (let i = 0; i < nDirect && i < 12; i++) {
        const wrap = candidates[i];
        if (!wrap || wrap.nodeType !== 1) continue;
        let wr = null;
        try { wr = wrap.getBoundingClientRect(); } catch { wr = null; }
        if (!wr || !(wr.width > 0) || !(wr.height > 0)) continue;
        // Near-full card shell — look one level deeper for the media strip.
        if (wr.width >= box.width * 0.85 && wr.height >= box.height * 0.85) {
          this._enqueueShadowPiercingChildren(wrap, candidates);
        }
      }
    } catch { /* ignore */ }

    const edgeTol = 3;
    for (let i = 0; i < candidates.length && i < 36; i++) {
      const child = candidates[i];
      if (!child || child.nodeType !== 1) continue;
      if (!this._isMediaLikeCoverElement(child)) continue;

      let cr = null;
      try { cr = child.getBoundingClientRect(); } catch { cr = null; }
      if (!cr || !(cr.width > 8) || !(cr.height > 8)) continue;

      const fracW = cr.width / box.width;
      const fracH = cr.height / box.height;
      // Major axis nearly spans the host; minor axis is a real strip (≥28%),
      // not a tiny icon — and not already handled as full-bleed (both ≥85%).
      const majorSpan = fracW >= 0.85 || fracH >= 0.85;
      const stripDepth = fracW >= 0.28 && fracH >= 0.28;
      if (!majorSpan || !stripDepth) continue;
      if (fracW >= 0.85 && fracH >= 0.85) continue; // full-bleed path owns this

      const flushTop = Math.abs(cr.top - box.top) <= edgeTol;
      const flushBottom = Math.abs(cr.bottom - box.bottom) <= edgeTol;
      const flushLeft = Math.abs(cr.left - box.left) <= edgeTol;
      const flushRight = Math.abs(cr.right - box.right) <= edgeTol;
      // Require a flush "cap": one end edge + both sides (top/bottom media bar)
      // or one side edge + both ends (left/right media rail).
      const topCap = flushTop && flushLeft && flushRight;
      const bottomCap = flushBottom && flushLeft && flushRight;
      const leftRail = flushLeft && flushTop && flushBottom;
      const rightRail = flushRight && flushTop && flushBottom;
      if (topCap || bottomCap || leftRail || rightRail) return true;
    }

    return false;
  }

  /**
   * True when absolute/fixed children or full-size pseudos cover most of `el`,
   * so an inset outline paints under (or is obscured by) that content.
   * @param {Element} el
   * @param {DOMRect|ClientRect} [er]
   * @returns {boolean}
   */
  _hasFullBleedCoveringContent(el, er) {
    if (!el || el.nodeType !== 1) return false;
    let box = er;
    try {
      if (!box) box = el.getBoundingClientRect();
    } catch {
      return false;
    }
    if (!box || !(box.width > 0) || !(box.height > 0)) return false;

    const nearlyFills = (w, h) =>
      w >= box.width * 0.85 && h >= box.height * 0.85;

    try {
      for (const pseudo of [':before', ':after']) {
        const pcs = window.getComputedStyle(el, pseudo);
        if (!pcs || !pcs.content || pcs.content === 'none') continue;
        if (pcs.position !== 'absolute' && pcs.position !== 'fixed') continue;
        const w = parseFloat(pcs.width);
        const h = parseFloat(pcs.height);
        // inset:-1px bleed (common on page thumbs) may not set usable width/height;
        // treat any absolute pseudo with non-none content as covering when the
        // host itself is the clipped media surface (caller checks overflow).
        if (Number.isFinite(w) && Number.isFinite(h) && nearlyFills(w, h)) {
          return true;
        }
        // Fallback: absolute pseudo with empty-string content and inset fill.
        if (pcs.content === '""' || pcs.content === "''") {
          const top = parseFloat(pcs.top);
          const left = parseFloat(pcs.left);
          const right = parseFloat(pcs.right);
          const bottom = parseFloat(pcs.bottom);
          const hasInset =
            (Number.isFinite(top) || Number.isFinite(bottom)) &&
            (Number.isFinite(left) || Number.isFinite(right));
          if (hasInset) return true;
        }
      }
    } catch { /* ignore */ }

    try {
      /** @type {Element[]} */
      const kids = [];
      this._enqueueShadowPiercingChildren(el, kids);
      // Also one level into near-full wrappers (card host → .root → img).
      const nDirect = kids.length;
      for (let i = 0; i < nDirect && i < 12; i++) {
        const wrap = kids[i];
        if (!wrap || wrap.nodeType !== 1) continue;
        let wr = null;
        try { wr = wrap.getBoundingClientRect(); } catch { wr = null; }
        if (wr && nearlyFills(wr.width, wr.height)) {
          this._enqueueShadowPiercingChildren(wrap, kids);
        }
      }
      for (let i = 0; i < kids.length && i < 36; i++) {
        const child = kids[i];
        if (!child || child.nodeType !== 1) continue;
        let cr = null;
        try { cr = child.getBoundingClientRect(); } catch { cr = null; }
        if (!cr || !nearlyFills(cr.width, cr.height)) continue;

        let cs = null;
        try { cs = window.getComputedStyle(child); } catch { cs = null; }
        if (!cs) continue;

        // Absolute/fixed full-bleed layer.
        if (cs.position === 'absolute' || cs.position === 'fixed') return true;
        // Static/relative media (msn.com hero img sized to the card).
        if (this._isMediaLikeCoverElement(child)) return true;
      }
    } catch { /* ignore */ }

    return false;
  }

  /**
   * True when a direct child fills most of `el` and would paint over an inset
   * outline applied to `el` (stacking-context / clipped media surface).
   *
   * Pattern: clickable wrapper (overflow:visible) > visual tile
   * (overflow:hidden, isolation:isolate, full-bleed ::before / img).
   *
   * Also considers top-level open-shadow children (archive.org / MSN hosts
   * often have empty light DOM with the visual tree only in shadow).
   *
   * @param {Element} el
   * @param {DOMRect|ClientRect} [er]
   * @returns {boolean}
   */
  _hasObscuringFullBleedChild(el, er) {
    if (!el || el.nodeType !== 1) return false;
    let box = er;
    try {
      if (!box) box = el.getBoundingClientRect();
    } catch {
      return false;
    }
    if (!box || !(box.width > 0) || !(box.height > 0)) return false;

    const nearlyFills = (w, h) =>
      w >= box.width * 0.85 && h >= box.height * 0.85;

    try {
      /** @type {Element[]} */
      const kids = [];
      this._enqueueShadowPiercingChildren(el, kids);
      if (!kids.length) return false;
      for (let i = 0; i < kids.length; i++) {
        const child = kids[i];
        if (!child || child.nodeType !== 1) continue;

        let cr = null;
        try { cr = child.getBoundingClientRect(); } catch { cr = null; }
        if (!cr || !nearlyFills(cr.width, cr.height)) continue;

        let cs = null;
        try { cs = window.getComputedStyle(child); } catch { cs = null; }
        if (!cs) continue;

        const childClips = this._styleClipsSelf(cs);
        const stacks =
          cs.isolation === 'isolate' ||
          (cs.transform && cs.transform !== 'none') ||
          (cs.filter && cs.filter !== 'none') ||
          (cs.opacity !== '' && Number(cs.opacity) < 1) ||
          (cs.position !== 'static' && cs.zIndex !== 'auto') ||
          (typeof cs.willChange === 'string' &&
            /(?:^|,\s*)(transform|opacity|filter|isolation)(?:\s*,|$)/i.test(cs.willChange));

        // Absolute/fixed full-bleed child always covers inset parent outline.
        if (cs.position === 'absolute' || cs.position === 'fixed') {
          return true;
        }

        // Clipped / paint-contained media tile hides inset rings.
        if (childClips && this._hasFullBleedCoveringContent(child, cr)) {
          return true;
        }

        // Near-full media child (static img sized to the tile).
        if (this._isMediaLikeCoverElement(child)) {
          return true;
        }

        // Stacking-context child that fills the host paints above parent outline
        // even without its own overflow clip (isolation + painted descendants).
        if (stacks && (childClips || this._hasFullBleedCoveringContent(child, cr))) {
          return true;
        }
      }
    } catch { /* ignore */ }

    return false;
  }

  /**
   * Tags that cannot accept element children (replaced / void). Strategy B
   * (in-target ring) cannot mount inside these — fall back to strategy C.
   * @param {Element|null|undefined} el
   * @returns {boolean}
   */
  _isReplacedOrVoidElement(el) {
    if (!el || el.nodeType !== 1) return true;
    const tag = (el.tagName || '').toUpperCase();
    return (
      tag === 'IMG' ||
      tag === 'VIDEO' ||
      tag === 'AUDIO' ||
      tag === 'CANVAS' ||
      tag === 'IFRAME' ||
      tag === 'EMBED' ||
      tag === 'OBJECT' ||
      tag === 'INPUT' ||
      tag === 'TEXTAREA' ||
      tag === 'SELECT' ||
      tag === 'OPTION' ||
      tag === 'BR' ||
      tag === 'HR' ||
      tag === 'SOURCE' ||
      tag === 'TRACK' ||
      tag === 'AREA' ||
      tag === 'COL' ||
      tag === 'SVG' || // prefer not to inject under SVG unless needed
      tag === 'MATH'
    );
  }

  /**
   * Dedicated full-bleed mount layer as last child of an open ShadowRoot.
   * Sized to the shadow host; ring paints above all shadow content.
   * @param {ShadowRoot} shadowRoot
   * @returns {Element|null}
   */
  _ensureShadowRootRingHost(shadowRoot) {
    if (!shadowRoot || typeof shadowRoot.appendChild !== 'function') return null;
    const ceHost = shadowRoot.host;
    if (!ceHost || ceHost.nodeType !== 1) return null;

    // Shadow host must be a containing block for absolute inset:0 layer.
    try {
      const cs = window.getComputedStyle(ceHost);
      if (cs && cs.position === 'static') {
        if (!this._inTargetHostPosRestore || this._inTargetHostPosRestore.host !== ceHost) {
          this._inTargetHostPosRestore = {
            host: ceHost,
            prev: ceHost.style.position || ''
          };
          ceHost.style.position = 'relative';
        }
      }
    } catch { /* ignore */ }

    let mount = null;
    try {
      const kids = shadowRoot.children;
      if (kids) {
        for (let i = 0; i < kids.length; i++) {
          const k = kids[i];
          if (k && k.nodeType === 1 && k.getAttribute?.('data-kp-shadow-b-host') === '1') {
            mount = k;
            break;
          }
        }
      }
    } catch { /* ignore */ }

    if (!mount) {
      try {
        mount = document.createElement('div');
        mount.setAttribute('data-kp-shadow-b-host', '1');
        mount.setAttribute('aria-hidden', 'true');
        mount.style.setProperty('position', 'absolute', 'important');
        mount.style.setProperty('inset', '0', 'important');
        mount.style.setProperty('box-sizing', 'border-box', 'important');
        mount.style.setProperty('pointer-events', 'none', 'important');
        mount.style.setProperty('margin', '0', 'important');
        mount.style.setProperty('padding', '0', 'important');
        mount.style.setProperty('border', '0', 'important');
        mount.style.setProperty('background', 'transparent', 'important');
        mount.style.setProperty('overflow', 'visible', 'important');
        mount.style.setProperty('z-index', '2147483000', 'important');
      } catch {
        return null;
      }
    }

    try {
      if (mount.parentNode !== shadowRoot || shadowRoot.lastElementChild !== mount) {
        shadowRoot.appendChild(mount);
      }
    } catch {
      return null;
    }

    return mount;
  }

  /**
   * True when candidate rect is not much larger than the focus target rect.
   * Prevents strategy B from mounting on a whole list/panel shadow host when
   * focusEl is a small row link (archive.org "Archive News").
   * @param {DOMRect|ClientRect|null|undefined} candidate
   * @param {DOMRect|ClientRect|null|undefined} focusRect
   * @returns {boolean}
   */
  _inTargetHostSizeOk(candidate, focusRect) {
    if (!candidate || !(candidate.width > 0) || !(candidate.height > 0)) return false;
    if (!focusRect || !(focusRect.width > 0) || !(focusRect.height > 0)) return true;
    const fArea = focusRect.width * focusRect.height;
    const cArea = candidate.width * candidate.height;
    if (fArea < 1) return true;
    // Allow modest padding around the focus box; reject container >> link.
    return (
      cArea <= fArea * 1.4 &&
      candidate.width <= focusRect.width * 1.3 + 8 &&
      candidate.height <= focusRect.height * 1.3 + 8
    );
  }

  /**
   * Host for an in-target absolute ring: paint-resolved clickable that can
   * accept a last-child ring (sibling above full-bleed media tiles).
   *
   * Ring box must track focusEl geometry. For open-shadow targets, mount on the
   * sized clickable when possible; only use a full ShadowRoot layer when the
   * shadow host itself is roughly the focus box (card tiles). Never fall back
   * to "largest node in the shadow" — that selects list containers.
   *
   * @param {Element} element - hover focusEl
   * @returns {Element|null}
   */
  _resolveInTargetHost(element) {
    if (!element || element.nodeType !== 1) return null;

    let paintEl = element;
    try {
      paintEl = this._resolveElementForFocusStyling(element) || element;
    } catch {
      paintEl = element;
    }
    if (!paintEl || paintEl.nodeType !== 1) return null;
    try {
      if (!paintEl.isConnected) return null;
    } catch {
      return null;
    }

    /** @type {DOMRect|ClientRect|null} */
    let focusRect = null;
    try { focusRect = element.getBoundingClientRect(); } catch { focusRect = null; }
    if (!focusRect || !(focusRect.width > 1) || !(focusRect.height > 1)) {
      try { focusRect = paintEl.getBoundingClientRect(); } catch { focusRect = null; }
    }

    // Open-shadow: size the ring to the clickable, not the owning custom element
    // (news lists share one large shadow host for many small links).
    try {
      const root = typeof paintEl.getRootNode === 'function' ? paintEl.getRootNode() : null;
      if (root && typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot) {
        if (this._canMountVisibleChildOnHost(paintEl)) {
          let pr = null;
          try { pr = paintEl.getBoundingClientRect(); } catch { pr = null; }
          if (pr && pr.width >= 8 && pr.height >= 8) {
            return paintEl;
          }
        }

        // Climb within this shadow only for a mountable ancestor ≈ focus size.
        let n = this._composedParent(paintEl);
        let depth = 0;
        while (n && n.nodeType === 1 && n !== root.host && depth++ < 8) {
          try {
            if (typeof n.getRootNode === 'function' && n.getRootNode() !== root) break;
          } catch { break; }
          if (this._canMountVisibleChildOnHost(n)) {
            let nr = null;
            try { nr = n.getBoundingClientRect(); } catch { nr = null; }
            if (nr && nr.width >= 8 && nr.height >= 8 && this._inTargetHostSizeOk(nr, focusRect)) {
              return n;
            }
          }
          n = this._composedParent(n);
        }

        // Full shadow-root layer only when the host box ≈ the focus target
        // (archive.org collection tiles: host/link same card rect). Collapsed
        // hosts (media-button ~0 height) also qualify — layer sizes to host.
        let hr = null;
        try { hr = root.host?.getBoundingClientRect?.(); } catch { hr = null; }
        const hostCollapsed = !!(hr && hr.width > 0 && hr.height < 2);
        if (hostCollapsed || this._inTargetHostSizeOk(hr, focusRect)) {
          const layer = this._ensureShadowRootRingHost(root);
          if (layer) return layer;
        }

        // Do not call _findInShadowInTargetMount(root.host) here — largest
        // visible box in a list shadow is the list container.
      }
    } catch { /* fall through */ }

    if (this._canMountVisibleChildOnHost(paintEl)) {
      let pr = null;
      try { pr = paintEl.getBoundingClientRect(); } catch { pr = null; }
      if (pr && pr.width >= 8 && pr.height >= 8) return paintEl;
    }

    // Slotless open-shadow host: mount inside, preferring a box ≈ focus size.
    if (this._getOpenShadowRoot(paintEl)) {
      const inner = this._findInShadowInTargetMountNear(paintEl, focusRect);
      if (inner) return inner;
    }

    // Replaced paint target (img/video): climb composed ancestors for a mount
    // that still matches focus geometry (never promote to a huge list shell).
    let n = this._composedParent(paintEl);
    let depth = 0;
    while (n && n.nodeType === 1 && depth++ < 8) {
      if (n === document.body || n === document.documentElement) break;
      if (this._canMountVisibleChildOnHost(n)) {
        let nr = null;
        try { nr = n.getBoundingClientRect(); } catch { nr = null; }
        if (nr && nr.width >= 8 && nr.height >= 8 && this._inTargetHostSizeOk(nr, focusRect)) {
          return n;
        }
      }
      if (this._getOpenShadowRoot(n)) {
        const inner = this._findInShadowInTargetMountNear(n, focusRect);
        if (inner) return inner;
      }
      n = this._composedParent(n);
    }

    return null;
  }

  /**
   * Like `_findInShadowInTargetMount`, but rejects nodes much larger than
   * `focusRect` so list/panel wrappers lose to the row link box.
   * @param {Element} host
   * @param {DOMRect|ClientRect|null|undefined} focusRect
   * @returns {Element|null}
   */
  _findInShadowInTargetMountNear(host, focusRect) {
    const sr = this._getOpenShadowRoot(host);
    if (!sr) return null;

    let best = null;
    let bestScore = -1;
    let visited = 0;
    const maxNodes = 48;
    /** @type {Element[]} */
    const queue = [];
    this._enqueueShadowPiercingChildren(host, queue);

    const focusArea = (focusRect && focusRect.width > 0 && focusRect.height > 0)
      ? (focusRect.width * focusRect.height)
      : 0;

    while (queue.length && visited < maxNodes) {
      const cur = queue.shift();
      if (!cur || cur.nodeType !== 1) continue;
      visited++;

      if (!this._isReplacedOrVoidElement(cur) && this._canMountVisibleChildOnHost(cur)) {
        let r = null;
        try { r = cur.getBoundingClientRect(); } catch { r = null; }
        if (r && r.width >= 8 && r.height >= 8 && this._inTargetHostSizeOk(r, focusRect)) {
          const area = r.width * r.height;
          // Prefer the box closest in area to the focus target (not merely largest).
          const score = focusArea > 0
            ? -Math.abs(area - focusArea)
            : area;
          if (score > bestScore) {
            bestScore = score;
            best = cur;
          }
        }
      }

      this._enqueueShadowPiercingChildren(cur, queue);
    }

    return best;
  }

  /**
   * Max numeric z-index among host pseudos and element children (excluding ring).
   * @param {Element} host
   * @returns {number}
   */
  _maxLocalZIndex(host) {
    let max = 0;
    const consider = (z) => {
      if (z == null || z === '' || z === 'auto') return;
      const n = parseInt(String(z), 10);
      if (Number.isFinite(n)) max = Math.max(max, n);
    };

    try {
      for (const pseudo of [':before', ':after']) {
        const ps = window.getComputedStyle(host, pseudo);
        if (ps) consider(ps.zIndex);
      }
    } catch { /* ignore */ }

    try {
      const kids = host.children;
      if (!kids) return max;
      const ringClass = CSS_CLASSES.FOCUS_RING_INTARGET || 'kpv2-focus-ring-intarget';
      for (let i = 0; i < kids.length; i++) {
        const child = kids[i];
        if (!child || child.nodeType !== 1) continue;
        if (child === this._inTargetRing) continue;
        try {
          if (child.classList && child.classList.contains(ringClass)) continue;
        } catch { /* ignore */ }
        let cs = null;
        try { cs = window.getComputedStyle(child); } catch { cs = null; }
        if (!cs) continue;
        // Only positioned / z-indexed children participate as explicit layers.
        if (cs.zIndex !== 'auto') consider(cs.zIndex);
      }
    } catch { /* ignore */ }

    return max;
  }

  /**
   * Ensure singleton in-target ring element exists.
   * @returns {HTMLElement}
   */
  _ensureInTargetRingEl() {
    if (this._inTargetRing && this._inTargetRing.nodeType === 1) {
      return this._inTargetRing;
    }
    const ringClass = CSS_CLASSES.FOCUS_RING_INTARGET || 'kpv2-focus-ring-intarget';
    this._inTargetRing = this.createElement('div', {
      className: ringClass,
      style: `
        position: absolute !important;
        inset: 0 !important;
        box-sizing: border-box !important;
        pointer-events: none !important;
        margin: 0 !important;
        padding: 0 !important;
        border-style: solid !important;
        background: transparent !important;
        display: none;
      `
    });
    try {
      this._inTargetRing.setAttribute('data-kp-focus-ring', '1');
      this._inTargetRing.setAttribute('aria-hidden', 'true');
    } catch { /* ignore */ }
    return this._inTargetRing;
  }

  /**
   * Restore position mutation on a previous in-target host, if any.
   */
  _restoreInTargetHostPosition() {
    const restore = this._inTargetHostPosRestore;
    this._inTargetHostPosRestore = null;
    if (!restore || !restore.host) return;
    try {
      if (restore.prev == null || restore.prev === '') {
        restore.host.style.removeProperty('position');
      } else {
        restore.host.style.position = restore.prev;
      }
    } catch { /* ignore */ }
  }

  /**
   * Detach / hide the in-target absolute focus ring and restore host position.
   */
  hideInTargetFocusRing() {
    this._focusPaintUsesInTargetRing = false;
    const ring = this._inTargetRing;
    if (ring) {
      try { ring.style.display = 'none'; } catch { /* ignore */ }
      try {
        if (ring.parentNode) ring.parentNode.removeChild(ring);
      } catch { /* ignore */ }
    }
    this._restoreInTargetHostPosition();
    this._inTargetHost = null;
  }

  /**
   * Multi-line / wrapped inline clickables fragment into several line boxes.
   * Absolute `inset:0` on an inline host sizes to one fragment (often first line
   * width only) — fall through to strategy C for the union rect instead.
   * @param {Element|null|undefined} el
   * @returns {boolean}
   */
  _isFragmentedInlineFocusTarget(el) {
    if (!el || el.nodeType !== 1) return false;
    try {
      const rects = el.getClientRects();
      if (rects && rects.length >= 2) return true;
    } catch { /* ignore */ }
    try {
      const display = String(window.getComputedStyle(el)?.display || '');
      // Bare `inline` abspos containing blocks are fragment-sized in Blink.
      if (display === 'inline') return true;
    } catch { /* ignore */ }
    return false;
  }

  /**
   * Strategy B: mount an absolute focus ring as last child of the host with
   * z-index = max(local siblings)+1 and border-radius from the visual host.
   *
   * @param {Element} element
   * @param {string} [mode]
   * @returns {boolean} true if ring was mounted
   */
  updateFocusOverlayInTarget(element, mode = MODES.NONE) {
    if (!(FEATURE_FLAGS && FEATURE_FLAGS.ENABLE_IN_TARGET_FOCUS_RING)) {
      return false;
    }
    if (!element || element.nodeType !== 1) return false;

    // Wrapped text links: B's inset:0 ring only covers one line fragment.
    // Let Auto fall through to C (body fixed union box) for full width.
    if (this._isFragmentedInlineFocusTarget(element)) {
      return false;
    }

    const host = this._resolveInTargetHost(element);
    if (!host) return false;
    if (this._isFragmentedInlineFocusTarget(host)) {
      return false;
    }

    // Modal/popover iframes: never decorate.
    try {
      if (host.tagName === 'IFRAME') {
        const isPopoverIframe = this.popoverIframeElement && host === this.popoverIframeElement;
        const isModalIframe = !!(host.classList && host.classList.contains('modal-iframe'));
        if (isPopoverIframe || isModalIframe) return false;
      }
    } catch { /* ignore */ }

    const ring = this._ensureInTargetRingEl();

    // Leaving a previous host — restore its position if we mutated it.
    if (this._inTargetHost && this._inTargetHost !== host) {
      this._restoreInTargetHostPosition();
    }

    // Containing block: static hosts need position:relative.
    try {
      const cs = window.getComputedStyle(host);
      if (cs && cs.position === 'static') {
        if (!this._inTargetHostPosRestore || this._inTargetHostPosRestore.host !== host) {
          this._inTargetHostPosRestore = {
            host,
            prev: host.style.position || ''
          };
          host.style.position = 'relative';
        }
      } else if (this._inTargetHostPosRestore && this._inTargetHostPosRestore.host === host) {
        // Host is no longer static (site style changed) — drop restore record carefully.
        // Keep relative we set only if we set it; if site now positions it, clear restore.
        if (cs && cs.position !== 'static' && host.style.position === 'relative') {
          // still our relative is fine
        }
      }
    } catch { /* ignore */ }

    // Ensure last child so we paint above earlier stacking siblings.
    try {
      if (ring.parentNode !== host || host.lastElementChild !== ring) {
        host.appendChild(ring);
      }
    } catch {
      this.hideInTargetFocusRing();
      return false;
    }

    this._inTargetHost = host;

    // Colors / thickness from click-mode settings (same as other backends).
    const isTextInput = element.matches && element.matches(SELECTORS.FOCUSABLE_TEXT);
    const suppressFill = this.shouldSuppressFocusFill(element);
    const {
      rectangleThickness,
      overlayFillEnabled,
      overlayShadowEnabled,
      focusColor
    } = this._getClickModeSettings();
    const ringWidthPx = Math.min(Math.max(Number(rectangleThickness) || 3, 1), 16);

    let borderColor;
    let shadowColor;
    let shadowBright;
    let backgroundColor = 'transparent';
    if (isTextInput) {
      borderColor = COLORS.ORANGE;
      shadowColor = COLORS.ORANGE_SHADOW;
      shadowBright = COLORS.ORANGE_SHADOW;
    } else {
      const p = this._getNonTextFocusPalette(focusColor);
      borderColor = p.borderColor;
      shadowColor = p.shadowColor;
      shadowBright = p.shadowBrightColor;
      // Fill is opt-in (same default as other backends).
      if (!suppressFill && overlayFillEnabled === true) {
        backgroundColor = p.backgroundColor || 'transparent';
      }
    }

    // Border radius: host first, else large rounded descendant (tile / media).
    let radius = '0';
    try {
      radius = this._resolveElementBorderRadius(host) ||
        this._resolveElementBorderRadius(element) ||
        '0';
    } catch {
      radius = '0';
    }

    const zLocal = this._maxLocalZIndex(host) + 1;
    // Shadow trees often nest stacking contexts (collection-tile inside tile-link).
    // Use a high floor so the ring stays above Lit/Fluent content.
    const z = this._isInShadowTree(host) ? Math.max(zLocal, 2147483000) : zLocal;

    try {
      ring.style.setProperty('position', 'absolute', 'important');
      ring.style.setProperty('inset', '0', 'important');
      ring.style.setProperty('box-sizing', 'border-box', 'important');
      ring.style.setProperty('pointer-events', 'none', 'important');
      ring.style.setProperty('margin', '0', 'important');
      ring.style.setProperty('padding', '0', 'important');
      ring.style.setProperty('width', 'auto', 'important');
      ring.style.setProperty('height', 'auto', 'important');
      ring.style.setProperty('z-index', String(z), 'important');
      ring.style.setProperty('border-width', `${ringWidthPx}px`, 'important');
      ring.style.setProperty('border-style', 'solid', 'important');
      ring.style.setProperty('border-color', borderColor, 'important');
      ring.style.setProperty('border-radius', radius, 'important');
      ring.style.setProperty('background', backgroundColor, 'important');
      if (overlayShadowEnabled === false) {
        ring.style.setProperty('box-shadow', 'none', 'important');
      } else {
        ring.style.setProperty(
          'box-shadow',
          `0 0 0 2px ${shadowColor}, 0 0 10px 2px ${shadowBright}`,
          'important'
        );
      }
      ring.style.setProperty('display', 'block', 'important');
      ring.style.setProperty('visibility', 'visible', 'important');
      ring.style.setProperty('opacity', '1', 'important');
    } catch {
      this.hideInTargetFocusRing();
      return false;
    }

    // Slotless shadow hosts accept appendChild but never layout the ring.
    // Require a positive box before claiming success — else fall through to C.
    // Also reject rings much narrower than the focus union (inline fragment CB).
    try {
      const rr = ring.getBoundingClientRect();
      if (!rr || !(rr.width > 1) || !(rr.height > 1)) {
        this.hideInTargetFocusRing();
        return false;
      }
      let fr = null;
      try { fr = element.getBoundingClientRect(); } catch { fr = null; }
      if (fr && fr.width > 16 && rr.width < fr.width * 0.85) {
        this.hideInTargetFocusRing();
        return false;
      }
    } catch {
      this.hideInTargetFocusRing();
      return false;
    }

    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] In-target focus ring mounted:', {
        host: host.tagName,
        hostClass: (host.className || '').toString().slice(0, 60),
        zIndex: z,
        borderRadius: radius,
        borderColor,
        mode
      });
    }

    return true;
  }

  /**
   * Update focus overlay using element styling (for DOM hover mode)
   * Styles the element directly instead of creating overlay elements
   */
  updateFocusOverlayElementStyling(element, mode = MODES.NONE) {
    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] updateFocusOverlayElementStyling called with:', {
        element: element?.tagName,
        mode: mode,
        _useDomHoverFocusColors: this._useDomHoverFocusColors
      });
    }

    // Clear any existing element styling first
    this.clearElementFocusStyling();

    if (!element) {
      return;
    }

    // In the wild (e.g. Engadget), an inline <a> sometimes wraps a block/flex child (<div>),
    // which causes the anchor to be split into multiple inline fragments. Outline/box-shadow
    // then renders as disjoint pieces (often thin strips). When we detect this, we style a
    // better single-rect descendant (usually the anchor's wrapper) instead of the anchor.
    let stylingTarget = this._resolveElementForFocusStyling(element) || element;

    // Optional clip-aware paint (flags in FEATURE_FLAGS — see constants.js).
    // Never mutate page overflow (broke IMDb carousels). Prefer painting on the
    // real hover target so data-kp-focus lands on the clickable <a>, not a parent.
    let clipCtx = { clippers: [], tightWrapper: null };
    const clipInsetOn = !!(FEATURE_FLAGS && FEATURE_FLAGS.ENABLE_FOCUS_CLIP_INSET);
    const tightPromoteOn = !!(FEATURE_FLAGS && FEATURE_FLAGS.ENABLE_FOCUS_TIGHT_WRAPPER_PROMOTION);
    if (clipInsetOn || tightPromoteOn) {
      try {
        clipCtx = this._findFocusClipContext(stylingTarget);
      } catch {
        clipCtx = { clippers: [], tightWrapper: null };
      }
      if (
        tightPromoteOn &&
        clipCtx.tightWrapper &&
        clipCtx.tightWrapper.nodeType === 1
      ) {
        stylingTarget = clipCtx.tightWrapper;
      }
    }

    // Don't style modal/popover iframes
    try {
      if (stylingTarget.tagName === 'IFRAME') {
        const isPopoverIframe = this.popoverIframeElement && stylingTarget === this.popoverIframeElement;
        const isModalIframe = !!(stylingTarget.classList && stylingTarget.classList.contains('modal-iframe'));
        if (isPopoverIframe || isModalIframe) {
          return;
        }
      }
    } catch { /* ignore */ }

    // Determine styling based on element type and mode
    // Category/fill decisions stay on the original hover target when we promoted
    // the ring to a tight wrapper.
    const categorySource = element;
    const isTextInput = categorySource.matches && categorySource.matches(SELECTORS.FOCUSABLE_TEXT);
    const suppressFill = this.shouldSuppressFocusFill(categorySource);

    // Settings-driven focus chrome (color / thickness / fill / shadow).
    const {
      rectangleThickness,
      overlayFillEnabled,
      overlayShadowEnabled,
      focusColor
    } = this._getClickModeSettings();
    const palette = this._getNonTextFocusPalette(focusColor);
    const ringColor = isTextInput ? COLORS.ORANGE : palette.borderColor;
    const ringWidthPx = Math.min(Math.max(Number(rectangleThickness) || 3, 1), 16);
    const ringWidth = `${ringWidthPx}px`;
    const shadowColor = isTextInput ? COLORS.ORANGE_SHADOW : palette.shadowColor;
    const shadowBright = isTextInput ? COLORS.ORANGE_SHADOW : palette.shadowBrightColor;
    // Translucent wash for thumbnails/links; none for scrubbers / when fill disabled.
    let ringBgColor = 'transparent';
    if (!suppressFill && overlayFillEnabled !== false) {
      ringBgColor = isTextInput
        ? 'rgba(255,140,0,0.2)'
        : (palette.backgroundColor || 'rgba(33,150,243,0.25)');
    }
    const boxShadow = overlayShadowEnabled === false
      ? 'none'
      : `0 0 0 2px ${shadowColor}, 0 0 10px 2px ${shadowBright}`;

    // Graded outline-offset from clip-ancestor room (not binary outer vs full inset).
    const outlineOffsetPx = clipInsetOn
      ? this._computeGradedFocusOutlineOffset(stylingTarget, ringWidthPx)
      : this._preferredFocusOutlineOffsetPx();
    const useInset = outlineOffsetPx < -0.25;

    // Shadow DOM: document CSS does not pierce; inject into this root on first use.
    this._ensureStylesForElement(stylingTarget);

    // Apply styling using CSS custom properties
    stylingTarget.style.setProperty('--keypilot-focus-ring-color', ringColor);
    stylingTarget.style.setProperty('--keypilot-focus-ring-width', ringWidth);
    stylingTarget.style.setProperty('--keypilot-focus-outline-offset', `${outlineOffsetPx}px`);
    stylingTarget.style.setProperty('--keypilot-focus-shadow-color', shadowColor);
    stylingTarget.style.setProperty('--keypilot-focus-ring-bg-color', ringBgColor);
    stylingTarget.style.setProperty('--keypilot-focus-box-shadow', boxShadow);

    // Outline follows the element's own border-radius. Many video thumbs put
    // radius on img / wrapper (12px) while the clickable <a> is square (0) —
    // without this the ring looks like a hard outer box around rounded media.
    // Only inject radius when the paint target itself has none.
    try {
      const ownRadius = this._readNonZeroBorderRadius(stylingTarget);
      if (!ownRadius) {
        const visualRadius = this._resolveElementBorderRadius(stylingTarget);
        if (visualRadius) {
          stylingTarget.style.setProperty('border-radius', visualRadius, 'important');
          stylingTarget.setAttribute('data-kp-focus-radius-set', '1');
        }
      }
    } catch { /* ignore */ }

    // Class + data attributes. Prefer data-kp-focus for paint (CSS): SPAs often
    // strip unknown classes on re-render but leave data-* alone.
    stylingTarget.classList.add('keypilot-focus-element');
    // Only wash the background when a fill color is actually set — never wipe the
    // element's own background with transparent on hover.
    if (ringBgColor !== 'transparent') {
      stylingTarget.classList.add('keypilot-focus-element--fill');
    } else {
      stylingTarget.classList.remove('keypilot-focus-element--fill');
    }
    try {
      stylingTarget.setAttribute('data-kp-focus', '1');
      if (useInset) stylingTarget.setAttribute('data-kp-focus-inset', '1');
      else stylingTarget.removeAttribute('data-kp-focus-inset');
    } catch { /* ignore */ }
    if (useInset) stylingTarget.classList.add('keypilot-focus-element--inset');
    else stylingTarget.classList.remove('keypilot-focus-element--inset');

    // Shadow trees: also paint outline inline. Injected <style> can be wiped by
    // Lit re-renders, and closed-shadow nodes from composedPath cannot receive
    // stylesheet injection — inline outline is the reliable source of truth.
    if (this._isInShadowTree(stylingTarget)) {
      try {
        stylingTarget.setAttribute('data-kp-focus-inline', '1');
        stylingTarget.style.setProperty(
          'outline',
          `${ringWidth} solid ${ringColor}`,
          'important'
        );
        stylingTarget.style.setProperty(
          'outline-offset',
          `${outlineOffsetPx}px`,
          'important'
        );
        stylingTarget.style.setProperty('box-shadow', boxShadow, 'important');
      } catch { /* ignore */ }
    }

    // Store reference for cleanup
    this._currentStyledElement = stylingTarget;

    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] Applied element styling:', {
        tagName: stylingTarget.tagName,
        originalTagName: element?.tagName,
        styledDifferentElement: stylingTarget !== element,
        outlineOffsetPx,
        useInset: useInset,
        clipInsetFlag: clipInsetOn,
        tightPromoteFlag: tightPromoteOn,
        tightWrapper: !!(clipCtx && clipCtx.tightWrapper),
        minRoom: this._minFocusOutlineRoomPx(stylingTarget),
        ringColor: ringColor,
        isTextInput: isTextInput
      });
    }
  }

  /**
   * Largest in-tree descendant with a positive box (for collapsed anchors that
   * only wrap position:absolute media — e.g. Breitbart video carousel thumbs).
   * Pierces open shadow roots (archive.org media-button / collection-tile hosts
   * often have empty light DOM with the visual tree only in shadow).
   * @param {HTMLElement} element
   * @returns {HTMLElement|null}
   */
  _findLargestVisibleDescendant(element) {
    if (!element || element.nodeType !== 1) return null;
    let best = null;
    let bestArea = 0;
    let visited = 0;
    const maxNodes = 48;
    /** @type {Element[]} */
    const queue = [];
    this._enqueueShadowPiercingChildren(element, queue);
    while (queue.length && visited < maxNodes) {
      const cur = queue.shift();
      if (!cur || cur.nodeType !== 1) continue;
      visited++;
      let r = null;
      try { r = cur.getBoundingClientRect(); } catch { r = null; }
      if (r && r.width >= 8 && r.height >= 8) {
        const area = r.width * r.height;
        if (area > bestArea) {
          bestArea = area;
          best = /** @type {HTMLElement} */ (cur);
        }
      }
      this._enqueueShadowPiercingChildren(cur, queue);
    }
    return best;
  }

  /**
   * Choose the best element to apply the focus ring styling to.
   *
   * Problems:
   * 1) Inline anchors that wrap only position:absolute media collapse to 0×0
   *    (Breitbart video thumbs) — outline on the <a> is invisible.
   * 2) Inline elements that contain block children can be split into multiple
   *    inline fragments, causing outline/box-shadow to render as disjoint pieces.
   * 3) Open-shadow custom-element hosts can be collapsed (archive.org
   *    media-button ~79×0) while the real clickable lives inside the shadow.
   *
   * Strategy:
   * - If the element has no usable box, style the largest visible descendant
   *   (piercing open shadows) or a sized composed parent that wraps abspos content.
   * - If `element.getClientRects()` indicates fragmentation (2+ rects) and
   *   element is inline-ish, find the largest single-rect descendant.
   * - Otherwise, return `element`.
   *
   * @param {HTMLElement} element
   * @returns {HTMLElement}
   */
  _resolveElementForFocusStyling(element) {
    if (!element || element.nodeType !== 1) return element;

    // Collapsed clickable (0×0) with visible abspos / shadow children — paint on media.
    let br = null;
    try { br = element.getBoundingClientRect(); } catch { br = null; }
    if (!br || br.width < 2 || br.height < 2) {
      const descendant = this._findLargestVisibleDescendant(element);
      if (descendant) return descendant;

      // No sized descendant: try composed parent that actually boxes the abspos content
      // (e.g. .video-image { position: relative } wrapping the collapsed <a>).
      try {
        let p = this._composedParent(element);
        let hops = 0;
        while (p && p.nodeType === 1 && hops++ < 4) {
          if (p === document.body || p === document.documentElement) break;
          let pr = null;
          try { pr = p.getBoundingClientRect(); } catch { pr = null; }
          if (pr && pr.width >= 8 && pr.height >= 8) {
            return /** @type {HTMLElement} */ (p);
          }
          p = this._composedParent(p);
        }
      } catch { /* ignore */ }
      return element;
    }

    let rects = null;
    try { rects = element.getClientRects(); } catch { rects = null; }
    if (!rects || rects.length < 2) return element;

    // Only apply fragmentation heuristic for inline-ish elements.
    let display = '';
    try { display = String(window.getComputedStyle(element)?.display || ''); } catch { display = ''; }
    const inlineish = display.startsWith('inline');
    if (!inlineish) return element;

    // Compute union rect (viewport coords) of the fragmented element.
    let uLeft = Infinity, uTop = Infinity, uRight = -Infinity, uBottom = -Infinity;
    try {
      for (const r of rects) {
        uLeft = Math.min(uLeft, r.left);
        uTop = Math.min(uTop, r.top);
        uRight = Math.max(uRight, r.right);
        uBottom = Math.max(uBottom, r.bottom);
      }
    } catch {
      return element;
    }
    if (!Number.isFinite(uLeft) || !Number.isFinite(uTop) || !Number.isFinite(uRight) || !Number.isFinite(uBottom)) return element;

    // Search a small subtree for a single-rect box that best matches the visible area.
    // Prefer the largest-area candidate to get the wrapper (e.g. a flex div) rather than an <img>.
    let best = null;
    let bestArea = 0;
    let nodesVisited = 0;
    const maxNodes = 40;
    const maxDepth = 4;

    const queue = [{ el: element, depth: 0 }];
    while (queue.length && nodesVisited < maxNodes) {
      const cur = queue.shift();
      const curEl = cur?.el;
      const d = cur?.depth ?? 0;
      if (!curEl || curEl.nodeType !== 1) continue;
      nodesVisited++;

      if (curEl !== element) {
        let cr = null;
        try { cr = curEl.getClientRects(); } catch { cr = null; }
        if (cr && cr.length === 1) {
          const r0 = cr[0];
          const w = Number(r0.width);
          const h = Number(r0.height);
          const area = (Number.isFinite(w) ? w : 0) * (Number.isFinite(h) ? h : 0);
          if (area > bestArea) {
            // Ensure candidate overlaps union rect and is plausibly the visual wrapper.
            const overlaps = !(r0.right <= uLeft || r0.left >= uRight || r0.bottom <= uTop || r0.top >= uBottom);
            if (overlaps) {
              bestArea = area;
              best = curEl;
            }
          }
        }
      }

      if (d >= maxDepth) continue;
      try {
        /** @type {Element[]} */
        const childList = [];
        this._enqueueShadowPiercingChildren(curEl, childList);
        for (let i = 0; i < childList.length; i++) {
          const k = childList[i];
          if (k && k.nodeType === 1) queue.push({ el: /** @type {HTMLElement} */ (k), depth: d + 1 });
          if (queue.length > maxNodes) break;
        }
      } catch { /* ignore */ }
    }

    return best || element;
  }

  /**
   * Strip KeyPilot focus ring classes/vars from a single element.
   * @param {Element|null|undefined} el
   */
  _stripFocusStylingFromElement(el) {
    if (!el || el.nodeType !== 1) return;
    try {
      el.classList.remove('keypilot-focus-element');
      el.classList.remove('keypilot-focus-element--inset');
      el.classList.remove('keypilot-focus-element--fill');
      try {
        el.removeAttribute('data-kp-focus');
        el.removeAttribute('data-kp-focus-inset');
        el.style.removeProperty('--keypilot-focus-outline-offset');
        el.style.removeProperty('--keypilot-focus-ring-color');
        el.style.removeProperty('--keypilot-focus-ring-width');
        el.style.removeProperty('--keypilot-focus-shadow-color');
        el.style.removeProperty('--keypilot-focus-ring-bg-color');
        el.style.removeProperty('--keypilot-focus-box-shadow');
        // Undo radius we injected so outline could match visual media corners.
        if (el.getAttribute('data-kp-focus-radius-set') === '1') {
          el.style.removeProperty('border-radius');
          el.removeAttribute('data-kp-focus-radius-set');
        }
        // Undo inline outline used for shadow-tree paint reliability.
        if (el.getAttribute('data-kp-focus-inline') === '1') {
          el.style.removeProperty('outline');
          el.style.removeProperty('outline-offset');
          el.style.removeProperty('box-shadow');
          el.removeAttribute('data-kp-focus-inline');
        }
      } catch { /* ignore */ }
      el.style.removeProperty('--keypilot-focus-ring-color');
      el.style.removeProperty('--keypilot-focus-ring-width');
      el.style.removeProperty('--keypilot-focus-shadow-color');
      el.style.removeProperty('--keypilot-focus-ring-bg-color');
      el.style.removeProperty('--keypilot-focus-box-shadow');
      el.style.removeProperty('filter');
    } catch { /* ignore */ }
  }

  /**
   * Clear focus styling from the currently styled element.
   * Also sweeps the owning open ShadowRoot (and document) for leftovers —
   * Lit re-renders / resolve-target swaps can strand the class on a node we
   * no longer hold in `_currentStyledElement`, which looks like "stuck" hover.
   *
   * @param {{ deep?: boolean }} [opts] - deep: walk all open shadow roots
   *   (used on full hide / mode switch to kill ghost rings on archive.org).
   */
  clearElementFocusStyling(opts = {}) {
    const deep = !!(opts && opts.deep);
    const primary = this._currentStyledElement;
    this._currentStyledElement = null;

    this._stripFocusStylingFromElement(primary);

    // Sweep the last styled tree scope for any stranded focus rings.
    let root = null;
    try {
      root = primary && typeof primary.getRootNode === 'function'
        ? primary.getRootNode()
        : null;
    } catch { root = null; }

    const roots = [];
    if (root && typeof root.querySelectorAll === 'function') roots.push(root);
    // Always also check the document — light-DOM leftovers.
    try { roots.push(document); } catch { /* ignore */ }

    for (const r of roots) {
      try {
        const stranded = r.querySelectorAll(
          '.keypilot-focus-element, .keypilot-focus-element--inset, [data-kp-focus], [data-kp-focus-inline]'
        );
        for (const el of stranded) {
          this._stripFocusStylingFromElement(el);
        }
      } catch { /* ignore */ }
    }

    if (deep) {
      try { this._deepStripFocusMarkers(document); } catch { /* ignore */ }
    }
  }

  /**
   * Walk open shadow trees and strip any leftover hover-ring markers.
   * @param {Document|ShadowRoot|Element} root
   * @param {number} [depth]
   */
  _deepStripFocusMarkers(root, depth = 0) {
    if (!root || depth > 24) return;

    try {
      if (typeof root.querySelectorAll === 'function') {
        const stranded = root.querySelectorAll(
          '.keypilot-focus-element, .keypilot-focus-element--inset, [data-kp-focus], [data-kp-focus-inline]'
        );
        for (const el of stranded) {
          this._stripFocusStylingFromElement(el);
        }
      }
    } catch { /* ignore */ }

    try {
      const base = (root.nodeType === 9) ? root.documentElement : root;
      if (!base) return;

      if (base.nodeType === 1 && base.shadowRoot) {
        this._deepStripFocusMarkers(base.shadowRoot, depth + 1);
      }

      const w = document.createTreeWalker(base, NodeFilter.SHOW_ELEMENT, null);
      let n;
      while ((n = w.nextNode())) {
        if (n.shadowRoot) {
          this._deepStripFocusMarkers(n.shadowRoot, depth + 1);
        }
      }
    } catch { /* ignore */ }
  }

  updateFocusOverlayDOM(element, mode = MODES.NONE, rectOverride = null) {
    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] updateFocusOverlayDOM called with:', {
        element: element?.tagName,
        mode: mode,
        rectOverride: rectOverride,
        _useDomHoverFocusColors: this._useDomHoverFocusColors
      });
    }

    if (!element) {
      this.hideFocusOverlay();
      return;
    }

    // Don't outline modal/popover iframes. The top document cannot see inside an iframe,
    // so outlining the iframe itself is usually distracting (especially for extension-page
    // iframes like Settings/Guide).
    try {
      if (element.tagName === 'IFRAME') {
        const isPopoverIframe = this.popoverIframeElement && element === this.popoverIframeElement;
        const isModalIframe = !!(element.classList && element.classList.contains('modal-iframe'));
        if (isPopoverIframe || isModalIframe) {
          this.hideFocusOverlay();
          return;
        }
      }
    } catch { /* ignore */ }

    // Determine if this is a text input element
    const isTextInput = element.matches && element.matches(SELECTORS.FOCUSABLE_TEXT);
    const suppressFill = this.shouldSuppressFocusFill(element);

    // We'll use this rect both for sizing/positioning and for deciding whether to render a fill.
    const rect = (rectOverride && typeof rectOverride === 'object')
      ? rectOverride
      : this.getBestRect(element);
    // If the hover target is extremely large, a filled overlay becomes distracting; keep just the frame.
    const isVeryLarge = rect && rect.width > 512 && rect.height > 512;
    
    // Determine overlay color based on element type
    let borderColor, shadowColor, backgroundColor;
    if (isTextInput) {
      // Orange color for text inputs in both normal mode and text focus mode
      borderColor = COLORS.ORANGE;
      shadowColor = COLORS.ORANGE_SHADOW;
      backgroundColor = 'transparent';
    } else {
      const p = this._getNonTextFocusPalette();
      borderColor = p.borderColor;
      shadowColor = p.shadowColor;
      // Fill for thumbnails/links; suppress for scrubbers and players that have a seek bar.
      backgroundColor = (suppressFill || isVeryLarge) ? 'transparent' : p.backgroundColor;
    }

    // Settings-driven behavior for Click Mode focus rectangle.
    const {
      rectangleThickness,
      overlayFillEnabled,
      overlayShadowEnabled,
      focusColor
    } = this._getClickModeSettings();
    if (!isTextInput && !suppressFill && !isVeryLarge && overlayFillEnabled === false) {
      backgroundColor = 'transparent';
    }
    // Re-resolve palette colors when settings focusColor is available (non-text).
    if (!isTextInput) {
      const p = this._getNonTextFocusPalette(focusColor);
      borderColor = p.borderColor;
      shadowColor = p.shadowColor;
      if (overlayFillEnabled !== false && !suppressFill && !isVeryLarge) {
        backgroundColor = p.backgroundColor;
      }
    }

    // Debug logging
    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] updateFocusOverlay called for:', {
        tagName: element.tagName,
        className: element.className,
        text: element.textContent?.substring(0, 30),
        mode: mode,
        isTextInput: isTextInput,
        suppressFill: suppressFill,
        isVeryLarge: isVeryLarge,
        borderColor: borderColor
      });
    }

    if (!this.focusOverlay) {
      this.focusOverlay = this.createElement('div', {
        className: CSS_CLASSES.FOCUS_OVERLAY,
        style: `
          position: fixed;
          left: 0;
          top: 0;
          pointer-events: none;
          z-index: ${Z_INDEX.OVERLAYS};
          background: ${backgroundColor};
          will-change: transform, width, height;
        `
      });
      document.body.appendChild(this.focusOverlay);
      
      // Debug logging for overlay creation
      if (window.KEYPILOT_DEBUG) {
        console.log('[KeyPilot Debug] Focus overlay created and added to DOM:', {
          element: this.focusOverlay,
          className: this.focusOverlay.className,
          style: this.focusOverlay.style.cssText,
          parent: this.focusOverlay.parentElement?.tagName
        });
      }
      
      // Start observing the overlay for visibility optimization
      if (this.overlayObserver) {
        this.overlayObserver.observe(this.focusOverlay);
      }
    }

    // Update overlay colors based on current context
    // Ensure any previous fade-out is cancelled when we re-show/update the overlay.
    this.focusOverlay.style.opacity = '1';
    this.focusOverlay.style.border = `${rectangleThickness}px solid ${borderColor}`;
    this.focusOverlay.style.background = backgroundColor;
    this.focusOverlay.style.boxSizing = 'border-box';
    // Strategy C: match target corner radius (A inherits it; fixed layer does not).
    // Prefer the element itself, else a large rounded descendant (card tile / media).
    try {
      const radius = this._resolveElementBorderRadius(element);
      this.focusOverlay.style.borderRadius = radius || '0';
    } catch {
      this.focusOverlay.style.borderRadius = '0';
    }
    if (overlayShadowEnabled === false) {
      this.focusOverlay.style.boxShadow = 'none';
    } else {
      const brightShadowColor = isTextInput
        ? COLORS.ORANGE_SHADOW
        : this._getNonTextFocusPalette(focusColor).shadowBrightColor;
      this.focusOverlay.style.boxShadow = `0 0 0 2px ${shadowColor}, 0 0 10px 2px ${brightShadowColor}`;
    }
    
    // Debug logging for positioning
    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] Focus overlay positioning:', {
        rect: rect,
        overlayExists: !!this.focusOverlay,
        overlayVisibility: this.overlayVisibility.focus
      });
    }
    
    if (rect.width > 0 && rect.height > 0) {
      // Position via transform to reduce layout work (fixed + translate3d).
      this.focusOverlay.style.transform = `translate3d(${rect.left}px, ${rect.top}px, 0)`;
      this.focusOverlay.style.width = `${rect.width}px`;
      this.focusOverlay.style.height = `${rect.height}px`;
      this.focusOverlay.style.display = 'block';
      this.focusOverlay.style.visibility = 'visible';
      
      if (window.KEYPILOT_DEBUG) {
        console.log('[KeyPilot Debug] Focus overlay positioned at:', {
          left: rect.left,
          top: rect.top,
          width: rect.width,
          height: rect.height
        });
      }
    } else {
      if (window.KEYPILOT_DEBUG) {
        console.log('[KeyPilot Debug] Focus overlay hidden - invalid rect:', rect);
      }
      this.hideFocusOverlay();
    }
  }

  // Unified hide interface that switches between rendering modes
  hideFocusOverlay() {
    // DOM-hover: clear A markers, B in-target ring, and C fixed overlay so
    // hybrid paint never leaves a ghost ring.
    if (this._useDomHoverFocusColors) {
      this._focusPaintUsesFixedOverlay = false;
      this._focusPaintUsesInTargetRing = false;
      this.clearElementFocusStyling({ deep: false });
      this.hideInTargetFocusRing();
      this.hideFocusOverlayDOM();
      return;
    }

    switch (this.renderingMode) {
      case 'canvas':
        return this.hideFocusOverlayCanvas();
      case 'css-custom-props':
        return this.hideFocusOverlayCSSCustomProps();
      case 'dom':
      default:
        return this.hideFocusOverlayDOM();
    }
  }

  hideFocusOverlayDOM() {
    if (this.focusOverlay) {
      this.focusOverlay.style.display = 'none';
    }
  }

  /**
   * Set the rendering mode for overlays
   * @param {string} mode - 'dom', 'canvas', or 'css-custom-props'
   */
  setRenderingMode(mode) {
    if (!['dom', 'canvas', 'css-custom-props'].includes(mode)) {
      console.warn('[KeyPilot] Invalid rendering mode:', mode);
      return;
    }

    // Cleanup current renderer
    this.cleanupRenderingMode();

    // Set new mode and initialize
    this.renderingMode = mode;
    this.initRenderingMode();

    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] Rendering mode changed to:', mode);
    }
  }

  initRenderingMode() {
    switch (this.renderingMode) {
      case 'canvas':
        this.initCanvasRenderer();
        break;
      case 'css-custom-props':
        this.initCSSCustomPropsRenderer();
        break;
      case 'dom':
      default:
        // DOM renderer doesn't need explicit initialization
        break;
    }
  }

  cleanupRenderingMode() {
    switch (this.renderingMode) {
      case 'canvas':
        this.cleanupCanvasRenderer();
        break;
      case 'css-custom-props':
        this.cleanupCSSCustomPropsRenderer();
        break;
      case 'dom':
      default:
        // DOM renderer cleanup happens in cleanup() method
        break;
    }
  }

  /**
   * Fade out the focus overlay rect, then hide it.
   * Useful after clicks that mutate the DOM (accordions, menus, etc.) so we don't leave a stale rect.
   */
  fadeOutFocusOverlay(durationMs = 120) {
    if (this.renderingMode === 'dom' && this.focusOverlay) {
      if (this.focusOverlay.style.display === 'none') return;

      // Avoid stacking transitions; we'll reset after the fade completes.
      this.focusOverlay.style.transition = `opacity ${durationMs}ms ease-out`;
      this.focusOverlay.style.opacity = '0';

      window.setTimeout(() => {
        if (!this.focusOverlay) return;
        // Only hide if we're still faded out (another update may have brought it back).
        if (this.focusOverlay.style.opacity === '0') {
          this.hideFocusOverlay();
        }
        // Clear transition so other overlay updates don't inherit this timing.
        if (this.focusOverlay) {
          this.focusOverlay.style.transition = '';
        }
      }, durationMs);
    } else {
      // For canvas and CSS custom props, just hide immediately
      this.hideFocusOverlay();
    }
  }

  /**
   * Shared inspector outline for any pick tool kind.
   * @param {Element|null|undefined} element
   * @param {string|null|undefined} kind
   */
  updateInspectorOverlay(element, kind = null) {
    if (!element) {
      this.hideInspectorOverlay();
      return;
    }

    const def = getInspectorDef(kind);
    const border = def?.borderColor || COLORS.DELETE_RED;
    const shadow = def?.shadowColor || COLORS.DELETE_SHADOW;
    const shadowBright = def?.shadowBrightColor || COLORS.DELETE_SHADOW_BRIGHT;

    if (!this.inspectorOverlay) {
      this.inspectorOverlay = this.createElement('div', {
        className: CSS_CLASSES.INSPECTOR_OVERLAY || CSS_CLASSES.DELETE_OVERLAY,
        style: `
          position: fixed;
          pointer-events: none;
          z-index: ${Z_INDEX.OVERLAYS};
          border: 3px solid ${border};
          box-shadow: 0 0 0 2px ${shadow}, 0 0 12px 2px ${shadowBright};
          background: transparent;
          will-change: transform;
        `
      });
      document.body.appendChild(this.inspectorOverlay);
      // Back-compat alias
      this.deleteOverlay = this.inspectorOverlay;
      if (this.overlayObserver) {
        this.overlayObserver.observe(this.inspectorOverlay);
      }
    }

    // Restyle when kind changes
    if (kind && kind !== this._inspectorOverlayKind) {
      this._inspectorOverlayKind = kind;
      this.inspectorOverlay.style.border = `3px solid ${border}`;
      this.inspectorOverlay.style.boxShadow =
        `0 0 0 2px ${shadow}, 0 0 12px 2px ${shadowBright}`;
      try {
        this.inspectorOverlay.setAttribute('data-kp-inspector-kind', kind);
      } catch { /* ignore */ }
    }

    const rect = this.getBestRect(element);
    if (rect.width > 0 && rect.height > 0) {
      this.inspectorOverlay.style.left = `${rect.left}px`;
      this.inspectorOverlay.style.top = `${rect.top}px`;
      this.inspectorOverlay.style.width = `${rect.width}px`;
      this.inspectorOverlay.style.height = `${rect.height}px`;
      this.inspectorOverlay.style.display = 'block';
      this.inspectorOverlay.style.visibility = 'visible';
    } else {
      this.hideInspectorOverlay();
    }
  }

  hideInspectorOverlay() {
    if (this.inspectorOverlay) {
      this.inspectorOverlay.style.display = 'none';
    }
  }

  /**
   * Paint persistent outlines for cumulatively picked elements + union rect.
   * @param {Element[]} elements
   * @param {{ left: number, top: number, width: number, height: number }|null} unionRect
   * @param {string|null|undefined} kind
   */
  updateInspectorPickedOverlays(elements, unionRect, kind = null) {
    const def = getInspectorDef(kind);
    const border = def?.borderColor || COLORS.HIGHLIGHT_BLUE;
    const fill = COLORS.HIGHLIGHT_SELECTION_BG || 'rgba(0,120,255,0.3)';

    if (!this._inspectorPickedOverlays) this._inspectorPickedOverlays = [];

    // Clear previous per-element overlays
    for (const ov of this._inspectorPickedOverlays) {
      try { ov.remove(); } catch { /* ignore */ }
    }
    this._inspectorPickedOverlays = [];

    const list = Array.isArray(elements) ? elements : [];
    for (const el of list) {
      if (!el || !el.isConnected) continue;
      const rect = this.getBestRect(el);
      if (rect.width <= 0 || rect.height <= 0) continue;
      const ov = this.createElement('div', {
        className: CSS_CLASSES.INSPECTOR_PICKED_OVERLAY || 'kpv2-inspector-picked-overlay',
        style: `
          position: fixed;
          pointer-events: none;
          z-index: ${Z_INDEX.OVERLAYS_BELOW};
          left: ${rect.left}px;
          top: ${rect.top}px;
          width: ${rect.width}px;
          height: ${rect.height}px;
          border: 2px solid ${border};
          background: ${fill};
          box-sizing: border-box;
        `
      });
      try { document.body.appendChild(ov); } catch { /* ignore */ }
      this._inspectorPickedOverlays.push(ov);
    }

    // Union rect overlay
    if (!this._inspectorUnionOverlay) {
      this._inspectorUnionOverlay = this.createElement('div', {
        className: CSS_CLASSES.INSPECTOR_UNION_OVERLAY || 'kpv2-inspector-union-overlay',
        style: `
          position: fixed;
          pointer-events: none;
          z-index: ${Z_INDEX.OVERLAYS_BELOW_2};
          border: 2px dashed ${border};
          background: transparent;
          box-sizing: border-box;
          display: none;
        `
      });
      try { document.body.appendChild(this._inspectorUnionOverlay); } catch { /* ignore */ }
    }

    if (unionRect && unionRect.width > 0 && unionRect.height > 0) {
      this._inspectorUnionOverlay.style.left = `${unionRect.left}px`;
      this._inspectorUnionOverlay.style.top = `${unionRect.top}px`;
      this._inspectorUnionOverlay.style.width = `${unionRect.width}px`;
      this._inspectorUnionOverlay.style.height = `${unionRect.height}px`;
      this._inspectorUnionOverlay.style.border = `2px dashed ${border}`;
      this._inspectorUnionOverlay.style.display = 'block';
    } else {
      this._inspectorUnionOverlay.style.display = 'none';
    }
  }

  clearInspectorPickedOverlays() {
    if (this._inspectorPickedOverlays) {
      for (const ov of this._inspectorPickedOverlays) {
        try { ov.remove(); } catch { /* ignore */ }
      }
      this._inspectorPickedOverlays = [];
    }
    if (this._inspectorUnionOverlay) {
      this._inspectorUnionOverlay.style.display = 'none';
    }
  }

  /**
   * Remember confirm-key / kind for the top-right instruction chip.
   * Call when entering inspector so updateOverlays can keep the chip visible.
   * @param {{ kind?: string|null, confirmKey?: string }} opts
   */
  setInspectorModeIndicatorOpts(opts = {}) {
    this._inspectorIndicatorOpts = {
      kind: opts.kind || null,
      confirmKey: opts.confirmKey || ''
    };
  }

  /**
   * Top-right companion instruction while inspector pick is active
   * (same pattern as Text Select "Press H again to finish selection").
   * @param {{ kind?: string|null, confirmKey?: string, message?: string }} [opts]
   */
  showInspectorModeIndicator(opts = {}) {
    const prev = this._inspectorIndicatorOpts || {};
    const kind = opts.kind || prev.kind || null;
    const confirmKey = opts.confirmKey || prev.confirmKey || '';
    if (opts.kind || opts.confirmKey) {
      this._inspectorIndicatorOpts = { kind, confirmKey };
    }

    const def = getInspectorDef(kind);
    const modeText = (opts.message && String(opts.message).trim())
      || getInspectorInstructionText(kind, confirmKey);

    const bg = def?.borderColor || COLORS.COLS_PURPLE;
    const shadow = def?.shadowColor || COLORS.COLS_SHADOW;

    if (this.inspectorModeIndicator) {
      this.inspectorModeIndicator.textContent = modeText;
      this.inspectorModeIndicator.style.display = 'block';
      this.inspectorModeIndicator.style.background = bg;
      this.inspectorModeIndicator.style.boxShadow = `0 2px 10px ${shadow}`;
      try {
        if (kind) this.inspectorModeIndicator.setAttribute('data-kp-inspector-kind', kind);
      } catch { /* ignore */ }
      return;
    }

    this.inspectorModeIndicator = this.createElement('div', {
      className: CSS_CLASSES.INSPECTOR_MODE_INDICATOR || 'kpv2-inspector-mode-indicator',
      style: `
        position: fixed;
        top: 20px;
        right: 20px;
        background: ${bg};
        color: white;
        padding: 10px 14px;
        font-size: 14px;
        font-weight: bold;
        font-family: ${KP_UI_FONT};
        border-radius: 6px;
        box-shadow: 0 2px 10px ${shadow};
        z-index: ${Z_INDEX.MESSAGE_BOX};
        pointer-events: none;
        max-width: min(360px, calc(100vw - 40px));
        line-height: 1.35;
        will-change: transform, opacity;
        animation: kpv2-pulse 1.5s ease-in-out infinite;
        letter-spacing: 0.01em;
      `
    });
    try {
      if (kind) this.inspectorModeIndicator.setAttribute('data-kp-inspector-kind', kind);
    } catch { /* ignore */ }
    this.inspectorModeIndicator.textContent = modeText;
    try {
      document.body.appendChild(this.inspectorModeIndicator);
    } catch {
      try { document.documentElement.appendChild(this.inspectorModeIndicator); } catch { /* ignore */ }
    }
  }

  hideInspectorModeIndicator() {
    if (this.inspectorModeIndicator) {
      try { this.inspectorModeIndicator.remove(); } catch { /* ignore */ }
      this.inspectorModeIndicator = null;
    }
    this._inspectorIndicatorOpts = null;
  }

  /** @deprecated use updateInspectorOverlay(el, kind) */
  updateDeleteOverlay(element) {
    this.updateInspectorOverlay(element, 'delete');
  }

  /** @deprecated use hideInspectorOverlay() */
  hideDeleteOverlay() {
    this.hideInspectorOverlay();
  }

  /** @deprecated use updateInspectorOverlay(el, 'cols') */
  updateColsOverlay(element) {
    this.updateInspectorOverlay(element, 'cols');
  }

  /** @deprecated use hideInspectorOverlay() */
  hideColsOverlay() {
    this.hideInspectorOverlay();
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  updateHighlightRectangleOverlay(startPosition, currentPosition) {
    return this.highlightManager.updateHighlightRectangleOverlay(startPosition, currentPosition);
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  hideHighlightRectangleOverlay() {
    return this.highlightManager.hideHighlightRectangleOverlay();
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  removeHighlightRectangleOverlay() {
    return this.highlightManager.removeHighlightRectangleOverlay();
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  updateHighlightSelectionOverlays(selection) {
    return this.highlightManager.updateHighlightSelectionOverlays(selection);
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  clearHighlightSelectionOverlays() {
    return this.highlightManager.clearHighlightSelectionOverlays();
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  setSelectionMode(mode) {
    return this.highlightManager.setSelectionMode(mode);
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  getSelectionMode() {
    return this.highlightManager.getSelectionMode();
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  startCharacterSelection(position, findTextNodeAtPosition, getTextOffsetAtPosition) {
    return this.highlightManager.startCharacterSelection(position, findTextNodeAtPosition, getTextOffsetAtPosition);
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  updateCharacterSelection(currentPosition, startPosition, findTextNodeAtPosition, getTextOffsetAtPosition) {
    return this.highlightManager.updateCharacterSelection(currentPosition, startPosition, findTextNodeAtPosition, getTextOffsetAtPosition);
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  updateRectangleSelectionFromCarets(startPosition, currentPosition, findTextNodeAtPosition, getTextOffsetAtPosition) {
    return this.highlightManager.updateRectangleSelectionFromCarets(
      startPosition,
      currentPosition,
      findTextNodeAtPosition,
      getTextOffsetAtPosition
    );
  }

  updateElementRectangleSelection(startPosition, currentPosition) {
    return this.highlightManager.updateElementRectangleSelection(startPosition, currentPosition);
  }

  getMatchedElements() {
    return this.highlightManager.getMatchedElements?.() || [];
  }

  clearElementSelection() {
    return this.highlightManager.clearElementSelection?.();
  }

  /**
   * Live-refresh highlight rectangle + selection during scroll (document-anchored origin).
   */
  syncHighlightSelectionToScroll(currentViewportMouse, findTextNodeAtPosition, getTextOffsetAtPosition) {
    return this.highlightManager.syncSelectionToScroll(
      currentViewportMouse,
      findTextNodeAtPosition,
      getTextOffsetAtPosition
    );
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  peekCharacterSelectedText() {
    return this.highlightManager.peekCharacterSelectedText();
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  completeCharacterSelection() {
    return this.highlightManager.completeCharacterSelection();
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  clearCharacterSelection() {
    return this.highlightManager.clearCharacterSelection();
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  /**
   * Create selection overlays for a specific range with shadow DOM support
   * @param {Range} range - DOM Range object
   */
  createSelectionOverlaysForRangeWithShadowSupport(range) {
    if (!range || range.collapsed) {
      return;
    }

    try {
      // Get all rectangles for the range (handles multi-line selections)
      const rects = this.getClientRectsWithShadowSupport(range);
      
      for (let i = 0; i < rects.length; i++) {
        const rect = rects[i];
        
        // Skip zero-width or zero-height rectangles
        if (rect.width <= 0 || rect.height <= 0) {
          continue;
        }

        // Create overlay element for this rectangle
        const overlay = this.createElement('div', {
          className: CSS_CLASSES.HIGHLIGHT_SELECTION,
          style: `
            position: fixed;
            left: ${rect.left}px;
            top: ${rect.top}px;
            width: ${rect.width}px;
            height: ${rect.height}px;
            background: ${COLORS.HIGHLIGHT_SELECTION_BG};
            border: 1px solid ${COLORS.HIGHLIGHT_SELECTION_BORDER};
            pointer-events: none;
            z-index: ${Z_INDEX.OVERLAYS_BELOW};
            will-change: transform;
          `
        });

        // Append to main document body (overlays should always be in main document)
        document.body.appendChild(overlay);
        this.highlightSelectionOverlays.push(overlay);

        // Start observing the overlay for visibility optimization
        if (this.overlayObserver) {
          this.overlayObserver.observe(overlay);
        }
      }

      if (window.KEYPILOT_DEBUG && rects.length > 0) {
        console.log('[KeyPilot Debug] Created selection overlays for range with shadow DOM support:', {
          rectCount: rects.length,
          firstRect: {
            left: rects[0].left,
            top: rects[0].top,
            width: rects[0].width,
            height: rects[0].height
          }
        });
      }
    } catch (error) {
      console.warn('[KeyPilot] Error creating selection overlays for range with shadow DOM support:', error);
    }
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  /**
   * Get client rectangles for a range with shadow DOM support
   * @param {Range} range - DOM Range object
   * @returns {DOMRectList|Array} - Client rectangles
   */
  getClientRectsWithShadowSupport(range) {
    try {
      // First try the standard method
      const rects = range.getClientRects();
      if (rects && rects.length > 0) {
        return rects;
      }

      // If no rectangles found, try alternative methods for shadow DOM
      return this.getAlternativeClientRects(range);
    } catch (error) {
      console.warn('[KeyPilot] Error getting client rects with shadow DOM support:', error);
      return [];
    }
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  /**
   * Get alternative client rectangles for shadow DOM ranges
   * @param {Range} range - DOM Range object
   * @returns {Array} - Array of rectangle objects
   */
  getAlternativeClientRects(range) {
    try {
      const rects = [];
      
      // Try to get bounding rect as fallback
      const boundingRect = range.getBoundingClientRect();
      if (boundingRect && boundingRect.width > 0 && boundingRect.height > 0) {
        rects.push(boundingRect);
      }
      
      // For shadow DOM, we might need to manually calculate rectangles
      // by walking through the range contents
      if (rects.length === 0) {
        const shadowRects = this.calculateShadowDOMRects(range);
        rects.push(...shadowRects);
      }
      
      return rects;
    } catch (error) {
      console.warn('[KeyPilot] Error getting alternative client rects:', error);
      return [];
    }
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  /**
   * Calculate rectangles for shadow DOM ranges manually
   * @param {Range} range - DOM Range object
   * @returns {Array} - Array of rectangle objects
   */
  calculateShadowDOMRects(range) {
    try {
      const rects = [];
      
      // Get start and end containers
      const startContainer = range.startContainer;
      const endContainer = range.endContainer;
      
      if (startContainer === endContainer && startContainer.nodeType === Node.TEXT_NODE) {
        // Single text node selection
        const textRect = this.getTextNodeRect(startContainer, range.startOffset, range.endOffset);
        if (textRect) {
          rects.push(textRect);
        }
      } else {
        // Multi-node selection - this is more complex for shadow DOM
        // For now, use bounding rect as approximation
        try {
          const boundingRect = range.getBoundingClientRect();
          if (boundingRect && boundingRect.width > 0 && boundingRect.height > 0) {
            rects.push(boundingRect);
          }
        } catch (error) {
          // Ignore errors in complex shadow DOM scenarios
        }
      }
      
      return rects;
    } catch (error) {
      console.warn('[KeyPilot] Error calculating shadow DOM rects:', error);
      return [];
    }
  }

  // SELECTION RECTANGLE FUNCTIONALITY ONLY
  /**
   * Get rectangle for a portion of a text node
   * @param {Text} textNode - Text node
   * @param {number} startOffset - Start character offset
   * @param {number} endOffset - End character offset
   * @returns {DOMRect|null} - Rectangle or null
   */
  getTextNodeRect(textNode, startOffset, endOffset) {
    try {
      const ownerDocument = textNode.ownerDocument || document;
      const tempRange = ownerDocument.createRange();
      tempRange.setStart(textNode, startOffset);
      tempRange.setEnd(textNode, endOffset);
      
      const rect = tempRange.getBoundingClientRect();
      return rect.width > 0 && rect.height > 0 ? rect : null;
    } catch (error) {
      console.warn('[KeyPilot] Error getting text node rect:', error);
      return null;
    }
  }

  /**
   * Create selection overlays for a specific range (legacy method for compatibility)
   * @param {Range} range - DOM Range object
   */
  createSelectionOverlaysForRange(range) {
    // Delegate to the shadow DOM-aware method
    this.createSelectionOverlaysForRangeWithShadowSupport(range);
  }

  calculateLabelPosition(elementRect, labelHeight) {
    const viewportHeight = window.innerHeight;
    const viewportWidth = window.innerWidth;
    
    // Try placing below the element first
    const belowPosition = {
      left: elementRect.left,
      top: elementRect.top + elementRect.height + 8,
      position: 'below'
    };
    
    // Check if below position is off-screen
    if (belowPosition.top + labelHeight > viewportHeight) {
      // Try above the element
      const abovePosition = {
        left: elementRect.left,
        top: elementRect.top - labelHeight - 8,
        position: 'above'
      };
      
      if (abovePosition.top < 0) {
        // Try right side if both above/below don't work
        return {
          left: elementRect.left + elementRect.width + 8,
          top: elementRect.top,
          position: 'right'
        };
      }
      return abovePosition;
    }
    return belowPosition;
  }

  updateEscExitLabelText(element) {
    if (!element) {
      this.hideEscExitLabelText();
      return;
    }

    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] updateEscExitLabelText called for:', {
        tagName: element.tagName,
        className: element.className,
        id: element.id
      });
    }

    if (!this.escExitLabelText) {
      this.escExitLabelText = this.createElement('div', {
        className: CSS_CLASSES.ESC_EXIT_LABEL,
        style: `
          position: fixed;
          pointer-events: none;
          z-index: ${Z_INDEX.OVERLAYS_ABOVE};
          will-change: transform, opacity;
          font-family: Arial, sans-serif;
          font-size: 14px;
          padding: 4px 8px;
          border-radius: 4px;
          white-space: nowrap;
          background-color: ${COLORS.ORANGE_BG};
          color: ${COLORS.ORANGE_TEXT};
          border: 1px solid ${COLORS.ORANGE_BORDER_SOLID};
        `
      });
      this.escExitLabelText.innerHTML = 'Press <kbd>ESC</kbd> to exit';
      document.body.appendChild(this.escExitLabelText);
      this.labelHeight = this.escExitLabelText.offsetHeight;
      if (this.overlayObserver) this.overlayObserver.observe(this.escExitLabelText);
    }

    const rect = this.getBestRect(element);
    if (rect.width > 0 && rect.height > 0) {
      const position = this.calculateLabelPosition(rect, this.labelHeight);
      this.escExitLabelText.style.left = `${position.left}px`;
      this.escExitLabelText.style.top = `${position.top}px`;
      this.escExitLabelText.style.display = 'block';
      this.escExitLabelText.style.visibility = 'visible';
    } else {
      this.hideEscExitLabelText();
    }
  }

  updateEscExitLabelHover(element) {
    if (!element) {
      this.hideEscExitLabelHover();
      return;
    }

    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] updateEscExitLabelHover called for:', {
        tagName: element.tagName,
        className: element.className,
        id: element.id
      });
    }

    if (!this.escExitLabelHover) {
      this.escExitLabelHover = this.createElement('div', {
        className: CSS_CLASSES.ESC_EXIT_LABEL,
        style: `
          position: fixed;
          pointer-events: none;
          z-index: ${Z_INDEX.OVERLAYS_ABOVE};
          will-change: transform, opacity;
          font-family: Arial, sans-serif;
          font-size: 14px;
          padding: 4px 8px;
          border-radius: 4px;
          white-space: nowrap;
          background-color: ${COLORS.FOCUS_GREEN_BG};
          color: ${COLORS.FOCUS_GREEN_TEXT};
          border: 1px solid ${COLORS.FOCUS_GREEN_SOLID};
        `
      });
      this.escExitLabelHover.innerHTML = this.formatHoverLabelText(this.hoverClickLabelText || 'F clicks');
      document.body.appendChild(this.escExitLabelHover);
      this.labelHeight = this.escExitLabelHover.offsetHeight;
      if (this.overlayObserver) this.overlayObserver.observe(this.escExitLabelHover);
    } else {
      // Keep label text fresh (e.g. countdown updates) even if element is reused.
      this.escExitLabelHover.innerHTML = this.formatHoverLabelText(this.hoverClickLabelText || 'F clicks');
    }

    const rect = this.getBestRect(element);
    if (rect.width > 0 && rect.height > 0) {
      const position = this.calculateLabelPosition(rect, this.labelHeight);
      this.escExitLabelHover.style.left = `${position.left}px`;
      this.escExitLabelHover.style.top = `${position.top}px`;
      this.escExitLabelHover.style.display = 'block';
      this.escExitLabelHover.style.visibility = 'visible';
    } else {
      this.hideEscExitLabelHover();
    }
  }

  hideEscExitLabelText() {
    if (this.escExitLabelText) this.escExitLabelText.style.display = 'none';
  }

  hideEscExitLabelHover() {
    if (this.escExitLabelHover) this.escExitLabelHover.style.display = 'none';
  }

  hideEscExitLabel() {
    this.hideEscExitLabelText();
    this.hideEscExitLabelHover();
  }

  /**
   * Host-element class paint for focus + inspector hover.
   * @param {Element|null} focusEl
   * @param {Element|null} inspectorEl
   * @param {Element|null} prevFocusEl
   * @param {Element|null} prevInspectorEl
   * @param {string|null} [inspectorKind]
   */
  updateElementClasses(focusEl, inspectorEl, prevFocusEl, prevInspectorEl, inspectorKind = null) {
    const hostClasses = getAllInspectorHostClasses();

    // Remove previous classes
    if (prevFocusEl && prevFocusEl !== focusEl) {
      prevFocusEl.classList.remove(CSS_CLASSES.FOCUS);
    }
    if (prevInspectorEl && prevInspectorEl !== inspectorEl) {
      for (const c of hostClasses) {
        try { prevInspectorEl.classList.remove(c); } catch { /* ignore */ }
      }
    }

    // Add new classes (ensure shadow styles first so brightness filter applies in open roots)
    if (focusEl) {
      this._ensureStylesForElement(focusEl);
      focusEl.classList.add(CSS_CLASSES.FOCUS);
    }
    if (inspectorEl) {
      this._ensureStylesForElement(inspectorEl);
      const def = getInspectorDef(inspectorKind);
      try { inspectorEl.classList.add(CSS_CLASSES.INSPECTOR); } catch { /* ignore */ }
      if (def?.hostClass) {
        try { inspectorEl.classList.add(def.hostClass); } catch { /* ignore */ }
      }
    }
  }

  getBestRect(element) {
    if (!element) return { left: 0, top: 0, width: 0, height: 0 };
    
    let rect = element.getBoundingClientRect();
    
    // Debug logging
    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] getBestRect for element:', {
        tagName: element.tagName,
        originalRect: {
          left: rect.left,
          top: rect.top,
          width: rect.width,
          height: rect.height
        }
      });
    }
    
    // If the element has no height (common with links containing other elements),
    // try to find a child element with height
    if (rect.height === 0 && element.children.length > 0) {
      for (const child of element.children) {
        const childRect = child.getBoundingClientRect();
        if (childRect.height > 0) {
          // Use the child's rect but keep the parent's left position if it's a link
          if (element.tagName.toLowerCase() === 'a') {
            const finalRect = {
              left: Math.min(rect.left, childRect.left),
              top: childRect.top,
              width: Math.max(rect.width, childRect.width),
              height: childRect.height
            };
            if (window.KEYPILOT_DEBUG) {
              console.log('[KeyPilot Debug] Using child rect for link:', finalRect);
            }
            return finalRect;
          }
          if (window.KEYPILOT_DEBUG) {
            console.log('[KeyPilot Debug] Using child rect:', childRect);
          }
          return childRect;
        }
      }
    }
    
    // If still no height, try to get text content dimensions
    if (rect.height === 0 && element.textContent && element.textContent.trim()) {
      // For text-only elements, use a minimum height
      const finalRect = {
        left: rect.left,
        top: rect.top,
        width: Math.max(rect.width, 20), // Minimum width
        height: Math.max(rect.height, 20) // Minimum height
      };
      if (window.KEYPILOT_DEBUG) {
        console.log('[KeyPilot Debug] Using minimum dimensions:', finalRect);
      }
      return finalRect;
    }
    
    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] Using original rect:', rect);
    }
    return rect;
  }

  /**
   * Read a non-zero computed border-radius from an element.
   * @param {Element|null|undefined} el
   * @returns {string|null}
   */
  _readNonZeroBorderRadius(el) {
    try {
      if (!el || el.nodeType !== 1) return null;
      const cs = window.getComputedStyle(el);
      if (!cs) return null;

      let radius = String(cs.borderRadius || '').trim();
      if (!radius) {
        const tl = cs.borderTopLeftRadius || '0';
        const tr = cs.borderTopRightRadius || '0';
        const br = cs.borderBottomRightRadius || '0';
        const bl = cs.borderBottomLeftRadius || '0';
        radius = `${tl} ${tr} ${br} ${bl}`.trim();
      }
      if (!radius) return null;

      // Collapse "0px 0px 0px 0px" / "0" / "0%" to null so callers can try children.
      const tokens = radius.replace(/,/g, ' ').split(/\s+/).filter(Boolean);
      if (
        tokens.length > 0 &&
        tokens.every((t) => /^(0|0px|0%)$/i.test(t))
      ) {
        return null;
      }
      return radius;
    } catch {
      return null;
    }
  }

  /**
   * Resolve a CSS border-radius that matches the visual shape of the activation target.
   * Prefer the element itself; if it is not rounded (common for <a> wrapping a card/image),
   * use a large rounded descendant that fills most of the host box.
   *
   * @param {Element|null|undefined} element
   * @returns {string|null} CSS border-radius value, or null to keep stylesheet default
   */
  _resolveElementBorderRadius(element) {
    if (!element || element.nodeType !== 1) return null;

    const own = this._readNonZeroBorderRadius(element);
    if (own) return own;

    try {
      let parentArea = 0;
      try {
        const pr = element.getBoundingClientRect();
        parentArea = Math.max(0, (pr.width || 0) * (pr.height || 0));
      } catch {
        parentArea = 0;
      }

      const candidates = [];
      try {
        if (element.children && element.children.length) {
          for (const child of element.children) candidates.push(child);
        }
      } catch { /* ignore */ }

      // Media often carries the visible corner radius on image links / cards.
      try {
        const media = element.querySelectorAll?.('img, svg, video, picture');
        if (media && media.length) {
          for (const m of media) candidates.push(m);
        }
      } catch { /* ignore */ }

      let best = null;
      let bestArea = 0;
      for (const c of candidates) {
        const radius = this._readNonZeroBorderRadius(c);
        if (!radius) continue;
        let area = 0;
        try {
          const cr = c.getBoundingClientRect();
          area = Math.max(0, (cr.width || 0) * (cr.height || 0));
        } catch {
          area = 0;
        }
        if (area >= bestArea) {
          bestArea = area;
          best = radius;
        }
      }

      // Avoid picking a tiny decorative rounded icon inside a larger link.
      if (best && (parentArea <= 0 || bestArea >= parentArea * 0.35)) {
        return best;
      }
    } catch { /* ignore */ }

    return null;
  }

  /**
   * Resolve the viewport rect of the current focus outline for activation feedback.
   * Works across DOM-hover element styling, DOM overlay, CSS-custom-props, and canvas modes.
   * @returns {{ left: number, top: number, width: number, height: number }|null}
   */
  _getFocusPulseRect() {
    // Prefer live geometry from active visuals.
    try {
      if (this.focusOverlay && this.focusOverlay.style.display !== 'none') {
        const r = this.focusOverlay.getBoundingClientRect();
        if (r && r.width > 0 && r.height > 0) {
          return { left: r.left, top: r.top, width: r.width, height: r.height };
        }
      }
    } catch { /* ignore */ }

    try {
      if (this.cssCustomPropsOverlay && this.cssCustomPropsOverlay.style.display !== 'none') {
        const r = this.cssCustomPropsOverlay.getBoundingClientRect();
        if (r && r.width > 0 && r.height > 0) {
          return { left: r.left, top: r.top, width: r.width, height: r.height };
        }
      }
    } catch { /* ignore */ }

    try {
      const el = this._currentStyledElement || this._lastFocusElement;
      if (el && el.nodeType === 1 && el.isConnected) {
        const r = this.getBestRect(el);
        if (r && r.width > 0 && r.height > 0) {
          return { left: r.left, top: r.top, width: r.width, height: r.height };
        }
      }
    } catch { /* ignore */ }

    // Last known rect from hover tracking (may be slightly stale after scroll).
    if (this._lastFocusRect && this._lastFocusRect.width > 0 && this._lastFocusRect.height > 0) {
      return { ...this._lastFocusRect };
    }
    return null;
  }

  /**
   * Install one-shot page lifecycle hooks that wipe fixed-position click effects
   * during navigation / hide (SPA soft-nav, full load, bfcache, tab background).
   */
  _installEphemeralEffectLifecycle() {
    if (this._ephemeralEffectLifecycleInstalled) return;
    this._ephemeralEffectLifecycleInstalled = true;

    const clear = () => {
      try { this.clearEphemeralEffects(); } catch { /* ignore */ }
    };

    /** @type {Array<[EventTarget, string, EventListenerOrEventListenerObject, boolean|AddEventListenerOptions|undefined]>} */
    const bindings = [];
    const bind = (target, type, handler, options) => {
      try {
        target.addEventListener(type, handler, options);
        bindings.push([target, type, handler, options]);
      } catch { /* ignore */ }
    };

    bind(window, 'pagehide', clear, true);
    bind(window, 'beforeunload', clear, true);
    bind(window, 'popstate', clear, true);
    bind(window, 'hashchange', clear, true);
    bind(document, 'visibilitychange', () => {
      if (document.visibilityState === 'hidden') clear();
    }, true);
    bind(window, 'pageshow', (e) => {
      // bfcache restore can leave stale fixed ghosts from the prior visit.
      if (e && e.persisted) clear();
    }, true);

    // Same-document SPA navigations: clear only after the history entry changes.
    // (Avoid Navigation API "navigate" — it fires at click time and would kill
    // the intentional flash before the user sees it.)
    // popstate/hashchange already bound; also poll while effects are live via rAF.
    try {
      this._ephemeralLastHref = String(location.href || '');
      this._ephemeralOnUrlMaybeChanged = () => {
        const next = String(location.href || '');
        if (next !== this._ephemeralLastHref) {
          this._ephemeralLastHref = next;
          clear();
        }
      };
    } catch { /* ignore */ }

    this._ephemeralEffectLifecycleDispose = () => {
      try {
        this._ephemeralLastHref = null;
        this._ephemeralOnUrlMaybeChanged = null;
      } catch { /* ignore */ }
      for (const [target, type, handler, options] of bindings) {
        try { target.removeEventListener(type, handler, options); } catch { /* ignore */ }
      }
      bindings.length = 0;
      this._ephemeralEffectLifecycleInstalled = false;
      this._ephemeralEffectLifecycleDispose = null;
    };
  }

  /**
   * Remove every temporary click / image-copy effect overlay immediately.
   */
  clearEphemeralEffects() {
    const entries = Array.from(this._activeEphemeralEffects || []);
    for (const entry of entries) {
      try { entry.teardown?.(); } catch { /* ignore */ }
    }
    this._activeEphemeralEffects?.clear?.();
  }

  /**
   * Track a temporary fixed overlay so it is removed when the source target is
   * gone/hidden or the page navigates — not only when the CSS animation ends.
   *
   * @param {Element} pulse
   * @param {Element|null|undefined} sourceEl
   * @param {{ left: number, top: number, width: number, height: number }|null|undefined} originRect
   * @param {number} cleanupMs
   */
  _trackEphemeralEffect(pulse, sourceEl, originRect, cleanupMs) {
    if (!pulse) return;
    this._installEphemeralEffectLifecycle();

    const entry = {
      pulse,
      sourceEl: sourceEl && sourceEl.nodeType === 1 ? sourceEl : null,
      originRect: originRect
        ? {
            left: originRect.left,
            top: originRect.top,
            width: originRect.width,
            height: originRect.height
          }
        : null,
      rafId: 0,
      timeoutId: 0,
      io: /** @type {IntersectionObserver|null} */ (null),
      teardown: () => {}
    };

    let tornDown = false;
    const teardown = () => {
      if (tornDown) return;
      tornDown = true;
      this._activeEphemeralEffects.delete(entry);
      if (entry.rafId) {
        try { cancelAnimationFrame(entry.rafId); } catch { /* ignore */ }
        entry.rafId = 0;
      }
      if (entry.timeoutId) {
        try { clearTimeout(entry.timeoutId); } catch { /* ignore */ }
        entry.timeoutId = 0;
      }
      if (entry.io) {
        try { entry.io.disconnect(); } catch { /* ignore */ }
        entry.io = null;
      }
      try {
        if (pulse.isConnected) pulse.remove();
      } catch { /* ignore */ }
    };
    entry.teardown = teardown;
    this._activeEphemeralEffects.add(entry);

    // Source left the viewport / was covered away → drop the ghost immediately.
    if (entry.sourceEl) {
      try {
        entry.io = new IntersectionObserver(
          (records) => {
            for (const r of records) {
              if (!r.isIntersecting || r.intersectionRatio <= 0) {
                teardown();
                return;
              }
            }
          },
          { root: null, threshold: 0 }
        );
        entry.io.observe(entry.sourceEl);
      } catch {
        entry.io = null;
      }
    }

    // Poll for disconnect / zero-size / large layout jump (SPA transition, scroll-away).
    const origin = entry.originRect;
    const tick = () => {
      if (tornDown) return;

      // SPA soft-nav often keeps the document alive but changes the URL.
      try {
        const onUrl = this._ephemeralOnUrlMaybeChanged;
        if (typeof onUrl === 'function') onUrl();
        if (tornDown) return;
      } catch { /* ignore */ }

      const src = entry.sourceEl;
      if (src) {
        if (!src.isConnected) {
          teardown();
          return;
        }
        try {
          const r = src.getBoundingClientRect();
          if (!(r.width > 0) || !(r.height > 0)) {
            teardown();
            return;
          }
          // Source moved far from the frozen ghost rect → page is transitioning.
          if (origin) {
            const dx = Math.abs(r.left - origin.left);
            const dy = Math.abs(r.top - origin.top);
            const dw = Math.abs(r.width - origin.width);
            const dh = Math.abs(r.height - origin.height);
            if (dx > 48 || dy > 48 || dw > 64 || dh > 64) {
              teardown();
              return;
            }
          }
          // Covered / opacity-0 during view transitions.
          try {
            const cs = window.getComputedStyle(src);
            if (cs) {
              const op = parseFloat(cs.opacity);
              if (cs.display === 'none' || cs.visibility === 'hidden' || (Number.isFinite(op) && op < 0.05)) {
                teardown();
                return;
              }
            }
          } catch { /* ignore */ }
        } catch {
          teardown();
          return;
        }
      }
      // Host document gone / pulse orphaned.
      if (!pulse.isConnected) {
        teardown();
        return;
      }
      entry.rafId = requestAnimationFrame(tick);
    };
    entry.rafId = requestAnimationFrame(tick);

    try {
      pulse.addEventListener('animationend', teardown, { once: true });
    } catch { /* ignore */ }

    const ms = Number.isFinite(cleanupMs) ? Math.max(200, cleanupMs) : 1000;
    entry.timeoutId = setTimeout(teardown, ms);
  }

  /**
   * Approximate border-radius in CSS px for SVG rx/ry from a CSS border-radius string.
   * @param {string|null|undefined} borderRadius
   * @param {number} width
   * @param {number} height
   * @returns {number}
   */
  _borderRadiusToSvgRx(borderRadius, width, height) {
    if (!borderRadius) return 3;
    const first = String(borderRadius).trim().split(/\s+/)[0] || '';
    if (!first) return 3;
    if (first.endsWith('%')) {
      const p = parseFloat(first);
      if (!Number.isFinite(p)) return 3;
      return Math.max(0, Math.min(width, height) * (p / 100));
    }
    const n = parseFloat(first);
    return Number.isFinite(n) ? Math.max(0, n) : 3;
  }

  /**
   * Map clickEffect id → CSS class + safety cleanup ms.
   * @param {string} clickEffect
   * @returns {{ className: string, cleanupMs: number } | null}
   */
  _clickEffectPresentation(clickEffect) {
    switch (clickEffect) {
      case 'flash':
        return { className: CSS_CLASSES.FOCUS_FLASH, cleanupMs: 500 };
      case 'scale':
        return { className: CSS_CLASSES.FOCUS_PULSE, cleanupMs: 800 };
      case 'marquee':
        return { className: CSS_CLASSES.FOCUS_MARQUEE, cleanupMs: 1200 };
      case 'dash':
        return { className: CSS_CLASSES.FOCUS_DASH, cleanupMs: 1100 };
      default:
        return null;
    }
  }

  /**
   * Build the dash-chase SVG overlay (dashed stroke travels the perimeter).
   * @param {{ left: number, top: number, width: number, height: number }} rect
   * @param {string|null|undefined} borderRadius
   * @returns {SVGSVGElement}
   */
  _createDashChasePulse(rect, borderRadius) {
    const w = Math.max(1, rect.width);
    const h = Math.max(1, rect.height);
    const stroke = 3;
    const inset = stroke / 2;
    const rw = Math.max(1, w - stroke);
    const rh = Math.max(1, h - stroke);
    const rx = Math.min(this._borderRadiusToSvgRx(borderRadius, w, h), rw / 2, rh / 2);

    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('class', CSS_CLASSES.FOCUS_DASH);
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('data-kp-ephemeral-effect', 'dash');
    svg.setAttribute('width', String(w));
    svg.setAttribute('height', String(h));
    svg.style.left = `${rect.left}px`;
    svg.style.top = `${rect.top}px`;
    svg.style.width = `${w}px`;
    svg.style.height = `${h}px`;

    const shape = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    shape.setAttribute('x', String(inset));
    shape.setAttribute('y', String(inset));
    shape.setAttribute('width', String(rw));
    shape.setAttribute('height', String(rh));
    shape.setAttribute('rx', String(rx));
    shape.setAttribute('ry', String(rx));
    shape.setAttribute('fill', 'none');
    shape.setAttribute('stroke', COLORS.FLASH_GREEN);
    shape.setAttribute('stroke-width', String(stroke));
    shape.setAttribute('stroke-linecap', 'round');
    shape.setAttribute('vector-effect', 'non-scaling-stroke');

    // Perimeter of rounded rect (good enough for dash patterning).
    const straight = 2 * (Math.max(0, rw - 2 * rx) + Math.max(0, rh - 2 * rx));
    const corners = 2 * Math.PI * rx;
    const peri = Math.max(24, straight + corners);
    const dash = Math.max(10, Math.min(36, peri * 0.09));
    const gap = Math.max(6, Math.min(22, peri * 0.055));
    shape.setAttribute('stroke-dasharray', `${dash} ${gap}`);
    shape.style.setProperty('--kp-dash-peri', String(peri));
    shape.classList.add(`${CSS_CLASSES.FOCUS_DASH}-stroke`);

    svg.appendChild(shape);
    return svg;
  }

  /**
   * F-key activation feedback for link-style targets.
   * Uses a temporary floating rectangle so it works in every render mode
   * (including DOM-hover element styling, where there is no focusOverlay div).
   *
   * Effect style comes from settings (clickMode.clickEffect):
   *   - flash (default): hard strobe on the outline
   *   - dash: dashed border chases around the perimeter
   *   - marquee: solid chaser light travels around the perimeter
   *   - scale: outline expands and fades out
   *   - none: no animation
   *
   * Whether any effect runs is decided solely by CLICKABLE_CATEGORY via
   * ElementDetector.isLinkStyleCategory (LINK + GENERIC only). Sliders, media,
   * buttons, text, and controls never get the pulse.
   *
   * @param {Element|null} [activationTarget] - Element that was F-activated.
   *   Prefer this over last-hover focus so category matches the real target.
   */
  flashFocusOverlay(activationTarget = null) {
    const el = (activationTarget && activationTarget.nodeType === 1)
      ? activationTarget
      : (this._currentStyledElement || this._lastFocusElement);

    try {
      const detector = window.keyPilot?.detector || window.keyPilot?.elementDetector;
      const cat = (detector && typeof detector.getClickableCategory === 'function')
        ? detector.getClickableCategory(el)
        : this.getFocusCategory(el);

      const showEffect = (detector && typeof detector.isLinkStyleCategory === 'function')
        ? detector.isLinkStyleCategory(cat)
        : (cat === CLICKABLE_CATEGORY.LINK || cat === CLICKABLE_CATEGORY.GENERIC);

      if (!showEffect) return;
    } catch { /* ignore */ }

    const { clickEffect } = this._getClickModeSettings();
    if (clickEffect === 'none') return;

    const presentation = this._clickEffectPresentation(clickEffect);
    if (!presentation) return;

    const rect = this._getFocusPulseRect();
    if (!rect) return;

    // Optional: briefly brighten the persistent DOM overlay if present.
    try {
      if (this.focusOverlay && this.focusOverlay.style.display !== 'none') {
        const originalBorder = this.focusOverlay.style.border;
        const originalBoxShadow = this.focusOverlay.style.boxShadow;
        this.focusOverlay.style.border = `3px solid ${COLORS.FLASH_GREEN}`;
        this.focusOverlay.style.boxShadow =
          `0 0 0 2px ${COLORS.FLASH_GREEN_SHADOW}, 0 0 20px 4px ${COLORS.FLASH_GREEN_GLOW}`;
        this.focusOverlay.style.transition = 'border 0.15s ease-out, box-shadow 0.15s ease-out';
        setTimeout(() => {
          if (!this.focusOverlay) return;
          this.focusOverlay.style.border = originalBorder;
          this.focusOverlay.style.boxShadow = originalBoxShadow;
          setTimeout(() => {
            if (this.focusOverlay) this.focusOverlay.style.transition = '';
          }, 150);
        }, 150);
      }
    } catch { /* ignore */ }

    // Click-effect ghost (link-style categories only).
    try {
      // Prefer a live rect from the activation target when available so tracking
      // compares against the same box we paint.
      let liveRect = rect;
      if (el && el.nodeType === 1) {
        try {
          const r = el.getBoundingClientRect();
          if (r && r.width > 0 && r.height > 0) {
            liveRect = {
              left: r.left,
              top: r.top,
              width: r.width,
              height: r.height
            };
          }
        } catch { /* keep last-focus rect */ }
      }

      // Don't start an effect if the target is already gone / not painted.
      if (el && el.nodeType === 1) {
        if (!el.isConnected) return;
        try {
          const cs = window.getComputedStyle(el);
          if (cs && (cs.display === 'none' || cs.visibility === 'hidden')) return;
        } catch { /* ignore */ }
      }

      const borderRadius = this._resolveElementBorderRadius(el);
      /** @type {HTMLElement|SVGSVGElement} */
      let pulse;
      if (clickEffect === 'dash') {
        pulse = this._createDashChasePulse(liveRect, borderRadius);
      } else {
        pulse = document.createElement('div');
        pulse.className = presentation.className;
        pulse.setAttribute('aria-hidden', 'true');
        pulse.setAttribute('data-kp-ephemeral-effect', clickEffect);
        // Position via left/top/width/height; CSS animation scales from transform-origin center.
        pulse.style.left = `${liveRect.left}px`;
        pulse.style.top = `${liveRect.top}px`;
        pulse.style.width = `${liveRect.width}px`;
        pulse.style.height = `${liveRect.height}px`;
        // Match corners to the clicked element (pill links, rounded cards, circular avatars).
        if (borderRadius) {
          pulse.style.borderRadius = borderRadius;
        }
      }
      document.body.appendChild(pulse);
      this._trackEphemeralEffect(pulse, el, liveRect, presentation.cleanupMs);
    } catch (e) {
      if (window.KEYPILOT_DEBUG) {
        console.warn('[KeyPilot] focus pulse failed:', e);
      }
    }
  }

  /**
   * Image-copy feedback: blue photo-frame over the source element that flashes,
   * pops slightly, then shrinks away (distinct from the green F-click expand pulse).
   *
   * @param {Element|null|undefined} element - Image or background-image host element
   */
  flashImageCopyPulse(element) {
    if (!element || element.nodeType !== 1) return;

    let rect = null;
    try {
      rect = element.getBoundingClientRect();
    } catch {
      return;
    }
    if (!rect) return;

    // Ignore degenerate / off-screen-ish rects.
    const w = Number(rect.width) || 0;
    const h = Number(rect.height) || 0;
    if (w < 2 || h < 2) return;

    // Cap extreme full-page backgrounds so the pulse stays readable.
    const maxW = Math.max(48, (typeof window !== 'undefined' ? window.innerWidth : 1200) * 0.92);
    const maxH = Math.max(48, (typeof window !== 'undefined' ? window.innerHeight : 800) * 0.92);
    let left = rect.left;
    let top = rect.top;
    let width = w;
    let height = h;
    if (width > maxW) {
      left += (width - maxW) / 2;
      width = maxW;
    }
    if (height > maxH) {
      top += (height - maxH) / 2;
      height = maxH;
    }

    try {
      if (!element.isConnected) return;
      const pulse = document.createElement('div');
      pulse.className = CSS_CLASSES.IMAGE_COPY_PULSE;
      pulse.setAttribute('aria-hidden', 'true');
      pulse.setAttribute('data-kp-ephemeral-effect', 'image-copy');
      pulse.style.left = `${left}px`;
      pulse.style.top = `${top}px`;
      pulse.style.width = `${width}px`;
      pulse.style.height = `${height}px`;
      // Match corners to the source image/host when it has a non-default radius.
      const borderRadius = this._resolveElementBorderRadius(element);
      if (borderRadius) {
        pulse.style.borderRadius = borderRadius;
      }
      document.body.appendChild(pulse);
      this._trackEphemeralEffect(
        pulse,
        element,
        { left, top, width, height },
        900
      );
    } catch (e) {
      if (window.KEYPILOT_DEBUG) {
        console.warn('[KeyPilot] image-copy pulse failed:', e);
      }
    }
  }

  createViewportModalFrame() {
    if (this.viewportModalFrame) {
      return this.viewportModalFrame;
    }

    this.viewportModalFrame = this.createElement('div', {
      className: CSS_CLASSES.VIEWPORT_MODAL_FRAME,
      style: `
        display: none;
      `
    });

    document.body.appendChild(this.viewportModalFrame);

    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] Viewport modal frame created and added to DOM:', {
        element: this.viewportModalFrame,
        className: this.viewportModalFrame.className,
        parent: this.viewportModalFrame.parentElement?.tagName
      });
    }

    return this.viewportModalFrame;
  }

  showViewportModalFrame() {
    if (!this.viewportModalFrame) {
      this.createViewportModalFrame();
    }

    this.viewportModalFrame.style.display = 'block';

    // Set up ResizeObserver to handle viewport changes with enhanced monitoring
    if (!this.resizeObserver && window.ResizeObserver) {
      this.resizeObserver = new ResizeObserver((entries) => {
        // Debounce resize updates to avoid excessive calls during continuous resizing
        if (this.resizeTimeout) {
          clearTimeout(this.resizeTimeout);
        }
        this.resizeTimeout = setTimeout(() => {
          this.updateViewportModalFrameSize();
          this.resizeTimeout = null;
        }, 16); // ~60fps for smooth updates
      });
      
      // Observe both document element and body for comprehensive viewport tracking
      this.resizeObserver.observe(document.documentElement);
      if (document.body) {
        this.resizeObserver.observe(document.body);
      }
    }

    // Enhanced fallback to window resize events if ResizeObserver is not available
    if (!window.ResizeObserver) {
      this.windowResizeHandler = this.debounce(() => {
        this.updateViewportModalFrameSize();
      }, 16);
      window.addEventListener('resize', this.windowResizeHandler);
      window.addEventListener('orientationchange', this.windowResizeHandler);
    }

    // Listen for fullscreen changes
    this.fullscreenHandler = () => {
      // Small delay to allow fullscreen transition to complete
      setTimeout(() => {
        this.updateViewportModalFrameSize();
      }, 100);
    };
    document.addEventListener('fullscreenchange', this.fullscreenHandler);
    document.addEventListener('webkitfullscreenchange', this.fullscreenHandler);
    document.addEventListener('mozfullscreenchange', this.fullscreenHandler);
    document.addEventListener('MSFullscreenChange', this.fullscreenHandler);

    // Listen for zoom changes (via visual viewport API if available)
    if (window.visualViewport) {
      this.visualViewportHandler = () => {
        this.updateViewportModalFrameSize();
      };
      window.visualViewport.addEventListener('resize', this.visualViewportHandler);
    }

    // Initial size update
    this.updateViewportModalFrameSize();

    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] Viewport modal frame shown with enhanced resize handling');
    }
  }

  hideViewportModalFrame() {
    if (this.viewportModalFrame) {
      this.viewportModalFrame.style.display = 'none';
    }

    // Clean up ResizeObserver
    if (this.resizeObserver) {
      this.resizeObserver.disconnect();
      this.resizeObserver = null;
    }

    // Clean up resize timeout
    if (this.resizeTimeout) {
      clearTimeout(this.resizeTimeout);
      this.resizeTimeout = null;
    }

    // Remove window resize listener fallback
    if (this.windowResizeHandler) {
      window.removeEventListener('resize', this.windowResizeHandler);
      window.removeEventListener('orientationchange', this.windowResizeHandler);
      this.windowResizeHandler = null;
    }

    // Remove fullscreen change listeners
    if (this.fullscreenHandler) {
      document.removeEventListener('fullscreenchange', this.fullscreenHandler);
      document.removeEventListener('webkitfullscreenchange', this.fullscreenHandler);
      document.removeEventListener('mozfullscreenchange', this.fullscreenHandler);
      document.removeEventListener('MSFullscreenChange', this.fullscreenHandler);
      this.fullscreenHandler = null;
    }

    // Remove visual viewport listener
    if (this.visualViewportHandler && window.visualViewport) {
      window.visualViewport.removeEventListener('resize', this.visualViewportHandler);
      this.visualViewportHandler = null;
    }

    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] Viewport modal frame hidden and all listeners cleaned up');
    }
  }

  updateViewportModalFrame(show) {
    if (show) {
      this.showViewportModalFrame();
    } else {
      this.hideViewportModalFrame();
    }
  }

  updateViewportModalFrameSize() {
    if (!this.viewportModalFrame || this.viewportModalFrame.style.display === 'none') {
      return;
    }

    // Get current viewport dimensions with fallbacks
    let viewportWidth, viewportHeight;

    // Use visual viewport API if available (handles zoom and mobile keyboards)
    if (window.visualViewport) {
      viewportWidth = window.visualViewport.width;
      viewportHeight = window.visualViewport.height;
    } else {
      // Fallback to standard viewport dimensions
      viewportWidth = window.innerWidth || document.documentElement.clientWidth;
      viewportHeight = window.innerHeight || document.documentElement.clientHeight;
    }

    // Handle fullscreen mode detection
    const isFullscreen = !!(
      document.fullscreenElement ||
      document.webkitFullscreenElement ||
      document.mozFullScreenElement ||
      document.msFullscreenElement
    );

    // Adjust for developer tools if not in fullscreen
    if (!isFullscreen) {
      // Check if developer tools might be open by comparing window dimensions
      const windowWidth = window.outerWidth;
      const windowHeight = window.outerHeight;
      
      // If there's a significant difference, dev tools might be open
      const widthDiff = Math.abs(windowWidth - viewportWidth);
      const heightDiff = Math.abs(windowHeight - viewportHeight);
      
      if (window.KEYPILOT_DEBUG) {
        console.log('[KeyPilot Debug] Viewport size analysis:', {
          viewportWidth,
          viewportHeight,
          windowWidth,
          windowHeight,
          widthDiff,
          heightDiff,
          isFullscreen,
          visualViewportAvailable: !!window.visualViewport
        });
      }
    }

    // Update frame dimensions using calculated viewport size
    this.viewportModalFrame.style.width = `${viewportWidth}px`;
    this.viewportModalFrame.style.height = `${viewportHeight}px`;

    // Ensure frame stays positioned at viewport origin
    this.viewportModalFrame.style.left = '0px';
    this.viewportModalFrame.style.top = '0px';

    // Handle zoom level changes by ensuring the frame covers the visible area
    if (window.visualViewport) {
      // Adjust position for visual viewport offset (mobile keyboards, etc.)
      this.viewportModalFrame.style.left = `${window.visualViewport.offsetLeft}px`;
      this.viewportModalFrame.style.top = `${window.visualViewport.offsetTop}px`;
    }

    if (window.KEYPILOT_DEBUG) {
      console.log('[KeyPilot Debug] Viewport modal frame size updated:', {
        width: `${viewportWidth}px`,
        height: `${viewportHeight}px`,
        left: this.viewportModalFrame.style.left,
        top: this.viewportModalFrame.style.top,
        isFullscreen,
        zoomLevel: window.devicePixelRatio || 1
      });
    }
  }

  cleanup() {
    // Close any open iframe/settings/guide/preview popovers and shared modal stack.
    try { this.hidePopover(); } catch { /* ignore */ }
    try { this.popupManager?.closeAll?.(); } catch { /* ignore */ }

    // Drop fixed click/image effect ghosts + their page lifecycle listeners.
    try { this.clearEphemeralEffects(); } catch { /* ignore */ }
    try { this._ephemeralEffectLifecycleDispose?.(); } catch { /* ignore */ }

    if (this.overlayObserver) {
      this.overlayObserver.disconnect();
      this.overlayObserver = null;
    }
    
    if (this.resizeObserver) {
      this.resizeObserver.disconnect();
      this.resizeObserver = null;
    }

    // Clean up resize timeout
    if (this.resizeTimeout) {
      clearTimeout(this.resizeTimeout);
      this.resizeTimeout = null;
    }

    // Clean up window resize handlers
    if (this.windowResizeHandler) {
      window.removeEventListener('resize', this.windowResizeHandler);
      window.removeEventListener('orientationchange', this.windowResizeHandler);
      this.windowResizeHandler = null;
    }

    // Clean up fullscreen handlers
    if (this.fullscreenHandler) {
      document.removeEventListener('fullscreenchange', this.fullscreenHandler);
      document.removeEventListener('webkitfullscreenchange', this.fullscreenHandler);
      document.removeEventListener('mozfullscreenchange', this.fullscreenHandler);
      document.removeEventListener('MSFullscreenChange', this.fullscreenHandler);
      this.fullscreenHandler = null;
    }

    // Clean up visual viewport handler
    if (this.visualViewportHandler && window.visualViewport) {
      window.visualViewport.removeEventListener('resize', this.visualViewportHandler);
      this.visualViewportHandler = null;
    }
    
    // Clean up all rendering backends
    this.cleanupRenderingMode();

    if (this.focusOverlay) {
      this.focusOverlay.remove();
      this.focusOverlay = null;
    }
    if (this.inspectorOverlay) {
      this.inspectorOverlay.remove();
      this.inspectorOverlay = null;
    }
    this.deleteOverlay = null;
    this.colsOverlay = null;
    this._inspectorOverlayKind = null;
    this.hideInspectorModeIndicator();
    // Clean up highlight manager
    if (this.highlightManager) {
      this.highlightManager.cleanup();
    }
    this._removeEdgeJumpFadeEl();

    if (this.viewportModalFrame) {
      this.viewportModalFrame.remove();
      this.viewportModalFrame = null;
    }
    if (this.escExitLabelText) {
      this.escExitLabelText.remove();
      this.escExitLabelText = null;
    }
    if (this.escExitLabelHover) {
      this.escExitLabelHover.remove();
      this.escExitLabelHover = null;
    }

    // Clean up debug panel / shadow HUD
    this.cleanupDebugPanel();
    this.cleanupShadowRootDebugHud();
  }

  createElement(tag, props = {}) {
    const element = document.createElement(tag);
    for (const [key, value] of Object.entries(props)) {
      if (key === 'className') {
        element.className = value;
      } else if (key === 'style') {
        element.style.cssText = value;
      } else {
        element.setAttribute(key, value);
      }
    }
    return element;
  }

  // Utility method for debouncing function calls
  debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout);
        func(...args);
      };
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
    };
  }

  /**
   * Focus the popover iframe so in-frame KeyPilot receives keys without a user click.
   * @param {HTMLIFrameElement|null|undefined} iframe
   */
  _focusPopoverIframe(iframe) {
    if (!iframe) return;
    try { iframe.focus(); } catch { /* ignore */ }
    try { iframe.contentWindow?.focus?.(); } catch { /* ignore */ }
  }

  /**
   * Hybrid focus: keys drive the iframe page when the pointer is over content;
   * when the pointer is over chrome (titlebar / actions / close), focus returns
   * to the parent so Esc/E/P and chrome controls stay reliable.
   *
   * @param {{
   *   iframe: HTMLIFrameElement,
   *   chromeEls?: Array<HTMLElement|null|undefined>,
   *   focusChromeEl?: HTMLElement|null
   * }} opts
   */
  _installPopoverHybridFocus({ iframe, chromeEls, focusChromeEl } = {}) {
    try { this._popoverHybridFocusCleanup?.(); } catch { /* ignore */ }
    this._popoverHybridFocusCleanup = null;

    if (!iframe) return;

    const chrome = (Array.isArray(chromeEls) ? chromeEls : []).filter(Boolean);
    /** @type {'iframe'|'chrome'|null} */
    let zone = null;

    const focusChrome = () => {
      const el = focusChromeEl || chrome[0] || this.popoverContainer;
      try { el?.focus?.(); } catch { /* ignore */ }
    };

    const focusIframe = () => this._focusPopoverIframe(iframe);

    const setZone = (next) => {
      if (next === zone) return;
      zone = next;
      if (next === 'iframe') focusIframe();
      else if (next === 'chrome') focusChrome();
    };

    const onChromeEnter = () => setZone('chrome');
    const onIframeEnter = () => setZone('iframe');

    for (const el of chrome) {
      try { el.addEventListener('pointerenter', onChromeEnter, true); } catch { /* ignore */ }
    }
    try { iframe.addEventListener('pointerenter', onIframeEnter, true); } catch { /* ignore */ }

    this._popoverHybridFocusCleanup = () => {
      for (const el of chrome) {
        try { el.removeEventListener('pointerenter', onChromeEnter, true); } catch { /* ignore */ }
      }
      try { iframe.removeEventListener('pointerenter', onIframeEnter, true); } catch { /* ignore */ }
    };
  }

  /**
   * Show popover with iframe containing the linked page.
   * Uses the shared {@link createPopoverTitlebar} chrome: single titlebar with
   * title, optional close-key hint, and uniform × close when enabled.
   *
   * @param {string} url - The URL to load in the popover
   * @param {object} [opts]
   * @param {string} [opts.title] - Optional title for the titlebar (defaults to url)
   * @param {string} [opts.hintKeyLabel] - Optional key label in the titlebar hint (defaults to 'P')
   * @param {boolean} [opts.showClose=true] - Whether to show the titlebar close button
   * @param {string|Node|null} [opts.titlebarHint] - Override titlebar hint (string or Node)
   * @param {string[]} [opts.closeKeys] - Keys forwarded from iframe that should request close (defaults to ['Escape','p','P'])
   * @param {string} [opts.width] - Optional fixed width (e.g., '920px', overrides default 80vw)
   * @param {string} [opts.height] - Optional fixed height (e.g., '600px', overrides default 80vh)
   */
  showPopover(url, opts = {}) {
    // Remove existing popover if any
    this.hidePopover();

    const titleText = (opts && typeof opts.title === 'string' && opts.title.trim()) ? opts.title.trim() : String(url || '');
    const hintKeyLabel = (opts && typeof opts.hintKeyLabel === 'string' && opts.hintKeyLabel.trim()) ? opts.hintKeyLabel.trim() : 'P';
    const closeKeys = Array.isArray(opts?.closeKeys) && opts.closeKeys.length
      ? opts.closeKeys.map(String)
      : ['Escape', 'p', 'P'];

    // Centralized close request:
    // Always prefer going through KeyPilot so state (mode/popoverOpen) is updated.
    // Fall back to direct DOM cleanup if KeyPilot isn't available for some reason.
    const requestClosePopover = () => {
      try {
        if (window.__KeyPilotInstance && typeof window.__KeyPilotInstance.handleClosePopover === 'function') {
          window.__KeyPilotInstance.handleClosePopover();
          return;
        }
      } catch (_e) {
        // Ignore and fall back to direct hide
      }
      this.hidePopover();
    };

    const ensureTopMouseTracking = () => {
      if (this._popoverMouseTrackerInstalled) return;
      this._popoverMouseTrackerInstalled = true;
      const update = (e) => {
        try {
          if (!e) return;
          if (typeof e.clientX === 'number') this._popoverLastMouse.x = e.clientX;
          if (typeof e.clientY === 'number') this._popoverLastMouse.y = e.clientY;
        } catch {
          // ignore
        }
      };
      try { document.addEventListener('mousemove', update, true); } catch { /* ignore */ }
      try { document.addEventListener('pointermove', update, true); } catch { /* ignore */ }
    };

    const clickCloseIfHovered = () => {
      try {
        const btn = this.popoverCloseButton;
        if (!btn) return false;
        const x = this._popoverLastMouse.x;
        const y = this._popoverLastMouse.y;
        if (typeof x !== 'number' || typeof y !== 'number') return false;
        const el = previewChromeHost?.shadowRoot?.elementFromPoint?.(x, y)
          || document.elementFromPoint(x, y);
        const shadowEl = btn.getRootNode?.()?.elementFromPoint?.(x, y);
        if (el === btn || btn.contains(el) || shadowEl === btn || btn.contains(shadowEl)) {
          try { btn.click(); } catch { /* ignore */ }
          return true;
        }
        return false;
      } catch {
        return false;
      }
    };

    // Create popover container (NOT using the native Popover API).
    // The Popover API uses the browser "top layer", which can sit above our cursor /
    // green click rectangle regardless of z-index, breaking F-to-click on popover UI.
    this.popoverContainer = this.createElement('div', {
      className: 'kpv2-popover-container',
      tabindex: '-1',
      role: 'dialog',
      'aria-modal': 'true',
      style: `
        position: fixed;
        inset: 0;                  /* top: 0; left: 0; bottom: 0; right: 0; */
        width: ${opts.width || '80vw'};
        height: ${opts.height || '80vh'};
        max-width: 100vw;          /* prevents overflow on very small screens */
        max-height: 100vh;
        margin: auto;              /* this is what centers it perfectly */
        background: ${NCT_DARK_UI_PANEL_BACKGROUND};
        border-radius: ${NCT_DARK_UI_PANEL_RADIUS};
        border: ${NCT_DARK_UI_PANEL_BORDER};
        box-shadow: ${NCT_DARK_UI_PANEL_BOX_SHADOW};
        display: flex;
        flex-direction: column;
        overflow: hidden;
        font-family: ${KP_UI_FONT};
        font-size: 14px;
        line-height: 1.3;
        letter-spacing: normal;
      `
    });

    // Keep the PopupManager panel and iframe in the light DOM: its focus and
    // resize paths operate on these host-owned nodes. Only KeyPilot-owned
    // titlebar/error chrome is isolated from page CSS in an open shadow root.
    const chromeHost = this.createElement('div', {
      className: 'kpv2-popover-chrome-host',
      style: `
        display: flex;
        flex: 0 0 auto;
        flex-direction: column;
        min-height: 0;
      `
    });
    const chromeShadow = ensureOpenChromeShadow(chromeHost, { id: 'iframe-popover' });
    const chromeMount = chromeShadow || chromeHost;


    // Store iframe reference for focus management
    let iframeRef = null;
    this.popoverBridgeReady = false;

    // Single standard titlebar: title + close hint + uniform × close (no second header bar).
    const showClose = opts?.showClose !== false;
    const titlebarHint = opts?.titlebarHint !== undefined
      ? opts.titlebarHint
      : createTitlebarCloseHint({
        keys: [hintKeyLabel, 'Esc'],
        suffix: 'Use the same keyboard navigation controls.'
      });
    const titlebarApi = createPopoverTitlebar({
      title: titleText,
      variant: 'modal',
      showClose,
      onClose: requestClosePopover,
      closeTitle: 'Close (Esc)',
      hint: titlebarHint,
      className: 'kpv2-popover-titlebar'
    });
    const header = titlebarApi.titlebar;
    const closeButton = titlebarApi.closeButton;
    this.popoverCloseButton = closeButton;
    ensureTopMouseTracking();

    // Create error message container (initially hidden)
    const errorContainer = this.createElement('div', {
      className: 'kpv2-popover-error',
      style: `
        flex: 1;
        display: none;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 40px;
        text-align: center;
        background: #f9f9f9;
      `
    });

    const errorIcon = this.createElement('div', {
      style: `
        font-size: 48px;
        margin-bottom: 16px;
        color: #999;
      `
    });
    errorIcon.textContent = '🚫';
    errorContainer.appendChild(errorIcon);

    const errorTitle = this.createElement('div', {
      style: `
        font-size: 18px;
        font-weight: 600;
        color: #333;
        margin-bottom: 8px;
      `
    });
    errorTitle.textContent = 'Cannot Display Page';
    errorContainer.appendChild(errorTitle);

    const errorMessage = this.createElement('div', {
      style: `
        font-size: 14px;
        color: #666;
        margin-bottom: 24px;
        max-width: 400px;
      `
    });
    errorMessage.textContent = 'This website prevents embedding in iframes for security reasons.';
    errorContainer.appendChild(errorMessage);

    const openInTabButton = this.createElement('button', {
      style: `
        background: #4CAF50;
        color: white;
        border: none;
        padding: 10px 20px;
        border-radius: 4px;
        cursor: pointer;
        font-size: 14px;
        font-weight: 500;
      `
    });
    openInTabButton.textContent = 'Open in New Tab';
    openInTabButton.onclick = () => {
      window.open(url, '_blank');
      requestClosePopover();
    };
    errorContainer.appendChild(openInTabButton);

    // Create iframe
    const iframe = this.createElement('iframe', {
      src: url,
      tabindex: '0',
      style: `
        flex: 1;
        border: none;
        width: 100%;
        height: 100%;
      `
    });
    iframeRef = iframe;
    this.popoverIframeElement = iframe;
    this.popoverIframeWindow = iframe.contentWindow || null;

    // Initialize the iframe bridge (content script running inside the iframe).
    // We retry a few times because content scripts in the frame may not be ready immediately,
    // and some pages navigate/redirect after initial load.
    // Pass closeKeys so Esc/P work inside the focused iframe without a host click.
    const sendBridgeInit = () => {
      try {
        iframe.contentWindow?.postMessage({
          type: 'KP_POPOVER_BRIDGE_INIT',
          closeKeys
        }, '*');
      } catch {
        // Ignore
      }
    };

    // Detect iframe load errors
    // Note: We can't reliably detect X-Frame-Options blocking for cross-origin iframes
    // due to same-origin policy. The declarativeNetRequest rules should handle most cases.
    // Only show error on actual load failure (onerror event).
    iframe.onerror = () => {
      console.log('[KeyPilot] Iframe load error detected');
      iframe.style.display = 'none';
      chromeHost.style.flex = '1 1 auto';
      errorContainer.style.display = 'flex';
    };

    // Optional: Very long timeout as last resort (30 seconds) for cases where
    // iframe never fires onload/onerror (shouldn't happen with declarativeNetRequest)
    const loadTimeout = setTimeout(() => {
      // Only show error if iframe hasn't loaded at all (no onload fired)
      // This is a fallback for edge cases
      console.log('[KeyPilot] Iframe load timeout - showing error as fallback');
      iframe.style.display = 'none';
      chromeHost.style.flex = '1 1 auto';
      errorContainer.style.display = 'flex';
    }, 30000);

    iframe.onload = () => {
      clearTimeout(loadTimeout);
      // Iframe loaded successfully - keep it visible
      // Note: We can't check contentDocument for cross-origin iframes,
      // but if onload fired, the iframe should be working
      console.log('[KeyPilot] Iframe loaded successfully');
      sendBridgeInit();
    };

    chromeMount.appendChild(header);
    chromeMount.appendChild(errorContainer);
    this.popoverContainer.appendChild(chromeHost);
    this.popoverContainer.appendChild(iframe);
    // Mount via PopupManager so the backdrop + stacking are consistent across popups.
    // This also keeps the popup in the normal DOM stacking context (no Popover API top-layer),
    // so KeyPilot overlays (green click rectangle) can sit above it by z-index.
    this.popupManager?.showModal?.({
      id: this._popoverPopupId,
      panel: this.popoverContainer,
      onRequestClose: requestClosePopover
    });
    sendBridgeInit();

    // Short retry window to cover slow frames / initial about:blank then navigation
    try {
      let attemptsLeft = 6; // ~1.5s total
      this.popoverInitTimer = setInterval(() => {
        if (!this.popoverContainer || attemptsLeft <= 0) {
          clearInterval(this.popoverInitTimer);
          this.popoverInitTimer = null;
          return;
        }
        attemptsLeft -= 1;
        sendBridgeInit();
      }, 250);
    } catch {
      // Ignore
    }

    // Prevent body scroll when popover is open
    document.body.style.overflow = 'hidden';

    // Add keyboard event listeners directly to catch Escape and F key
    // This ensures they work even when iframe has focus
    const handlePopoverKeyDown = (e) => {
      console.log('[KeyPilot] Popover key event:', e.key, 'Target:', e.target, 'Active element:', document.activeElement);
      
      // Escape key - close popover (always, regardless of where it's pressed)
      if (e.key === 'Escape') {
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
        requestClosePopover();
        return;
      }
      
      // NOTE: We intentionally do NOT repurpose "F" to close popovers.
      // "F" is KeyPilot's click key, and users expect it to click popover UI (e.g. ×)
      // once the green rectangle can sit above popovers via z-index stacking.
    };

    // Add listeners to document and popover container with capture phase
    // This ensures we catch events even when iframe or other elements have focus
    document.addEventListener('keydown', handlePopoverKeyDown, true);
    this.popoverContainer.addEventListener('keydown', handlePopoverKeyDown, true);
    
    // Store cleanup function and backdrop reference
    this.popoverKeyHandler = handlePopoverKeyDown;

    // Listen for key events forwarded from the iframe (content script bridge).
    // This enables closing the popover even after the user clicks into the iframe,
    // where the parent document can no longer observe keydown events directly.
    this.popoverMessageHandler = (event) => {
      const data = event?.data;
      if (!data || typeof data.type !== 'string') return;
      if (this.popoverIframeWindow && event.source !== this.popoverIframeWindow) return;

      if (data.type === 'KP_POPOVER_BRIDGE_READY') {
        this.popoverBridgeReady = true;
        // Auto-focus iframe so full KeyPilot works inside without a user click.
        // Close keys (Esc/P) are handled by the iframe bridge → parent.
        this._focusPopoverIframe(iframeRef);
        this._installPopoverHybridFocus({
          iframe: iframeRef,
          chromeEls: [header, closeButton].filter(Boolean),
          focusChromeEl: closeButton || header
        });
        return;
      }

      if (data.type === 'KP_POPOVER_REQUEST_CLOSE') {
        // Close on configured keys forwarded by the iframe bridge.
        if (closeKeys.includes(String(data.key))) requestClosePopover();
      }

      if (data.type === 'KP_POPOVER_LAUNCH_WALKTHROUGH') {
        // Guide "Launch Walkthrough": close this popover, then open tutorial from reset.
        requestClosePopover();
        try {
          const ob = window.__KeyPilotOnboarding;
          if (ob && typeof ob.resetTutorial === 'function') {
            void ob.resetTutorial();
          }
        } catch {
          // ignore
        }
        return;
      }

      if (data.type === 'KP_POPOVER_BRIDGE_KEYDOWN') {
        const k = String(data.key || '');
        if (k === 'f' || k === 'F') {
          // Prefer "click close button if hovered" so users can use F on the × affordance
          // even when focus is inside the iframe (keydown doesn't propagate to parent).
          clickCloseIfHovered();
        }
      }
    };
    window.addEventListener('message', this.popoverMessageHandler, true);

    // Until the bridge is ready, keep focus on chrome (then hybrid focus takes over).
    try {
      (closeButton || header)?.focus?.();
    } catch (_e) {
      try {
        this.popoverContainer.focus();
      } catch (_e2) {
        // Ignore
      }
    }
  }

  /**
   * Hide the popover
   */
  /**
   * Remembered viewport mode for a host. Default is desktop; only mobile is stored.
   * @param {string} hostname
   * @returns {Promise<'mobile'|'desktop'>}
   */
  async _getPreviewViewportModeForHost(hostname) {
    const host = String(hostname || '').toLowerCase();
    if (!host) return 'desktop';
    try {
      const map = await storageGetValue(PREVIEW_VIEWPORT_BY_HOST_KEY, {});
      if (map && typeof map === 'object' && map[host] === 'mobile') {
        return 'mobile';
      }
    } catch (e) {
      console.warn('[KeyPilot] Failed to read preview viewport prefs:', e?.message || e);
    }
    return 'desktop';
  }

  /**
   * Persist viewport mode for a host. Desktop clears the override (default).
   * @param {string} hostname
   * @param {'mobile'|'desktop'} mode
   * @returns {Promise<void>}
   */
  async _setPreviewViewportModeForHost(hostname, mode) {
    const host = String(hostname || '').toLowerCase();
    if (!host) return;
    try {
      const prev = await storageGetValue(PREVIEW_VIEWPORT_BY_HOST_KEY, {});
      const map = (prev && typeof prev === 'object' && !Array.isArray(prev))
        ? { ...prev }
        : {};
      if (mode === 'mobile') {
        map[host] = 'mobile';
      } else {
        delete map[host];
      }
      await storageSetValue(PREVIEW_VIEWPORT_BY_HOST_KEY, map);
    } catch (e) {
      console.warn('[KeyPilot] Failed to save preview viewport prefs:', e?.message || e);
    }
  }

  /**
   * Ask the service worker to enable/disable mobile User-Agent for this tab's
   * sub_frame requests (Link Preview Mobile mode).
   * @param {boolean} enabled
   * @returns {Promise<boolean>}
   */
  async _setPreviewMobileUa(enabled) {
    const next = !!enabled;
    try {
      if (typeof chrome === 'undefined' || !chrome.runtime?.sendMessage) {
        this._previewMobileUaActive = false;
        return false;
      }
      const res = await chrome.runtime.sendMessage({
        type: MSG.SET_PREVIEW_MOBILE_UA,
        enabled: next
      });
      if (res?.type === MSG.ERROR) {
        console.warn('[KeyPilot] Preview mobile UA not applied:', res.error || res);
        this._previewMobileUaActive = false;
        return false;
      }
      this._previewMobileUaActive = next;
      return true;
    } catch (e) {
      console.warn('[KeyPilot] Preview mobile UA message failed:', e?.message || e);
      this._previewMobileUaActive = false;
      return false;
    }
  }

  /**
   * Move focus out of the popover (esp. its iframe) before the node is removed.
   * Otherwise Chrome often hands focus to the browser omnibox, which steals keys
   * from the page (notably on New Tab after a second E closes Link Preview).
   */
  _restoreFocusFromPopover() {
    const iframe = this.popoverIframeElement;
    const container = this.popoverContainer;
    if (!iframe && !container) return;

    let active = null;
    try { active = document.activeElement; } catch { /* ignore */ }

    const focusInPopover =
      !!(iframe && active === iframe) ||
      !!(container && active instanceof Node && container.contains(active));

    // Always try to leave the iframe before removal; focus-in-iframe is the
    // main omnibox-steal case even when activeElement reporting is odd.
    try { iframe?.blur?.(); } catch { /* ignore */ }
    if (focusInPopover) {
      try { active?.blur?.(); } catch { /* ignore */ }
    }

    try { window.focus(); } catch { /* ignore */ }

    // Park focus on a surviving element. Prefer body (make it programmatically
    // focusable) so the page keeps keyboard ownership after teardown.
    try {
      const body = document.body;
      if (body) {
        if (!body.hasAttribute('tabindex')) {
          body.setAttribute('tabindex', '-1');
        }
        body.focus({ preventScroll: true });
      }
    } catch { /* ignore */ }
  }

  hidePopover() {
    // Drop mobile UA session rule so host-page iframes are not affected after close.
    if (this._previewMobileUaActive) {
      this._previewMobileUaActive = false;
      try {
        void this._setPreviewMobileUa(false);
      } catch { /* ignore */ }
    }

    // Capture focus back onto the page *before* removing a focused iframe.
    try { this._restoreFocusFromPopover(); } catch { /* ignore */ }

    // Stop bridge init retries
    if (this.popoverInitTimer) {
      try {
        clearInterval(this.popoverInitTimer);
      } catch {
        // Ignore
      }
      this.popoverInitTimer = null;
    }

    // Remove iframe bridge message listener
    if (this.popoverMessageHandler) {
      try {
        window.removeEventListener('message', this.popoverMessageHandler, true);
      } catch {
        // Ignore
      }
      this.popoverMessageHandler = null;
    }
    this.popoverIframeWindow = null;
    this.popoverIframeElement = null;
    this.popoverBridgeReady = false;
    this.popoverCloseButton = null;

    // Remove keyboard event listeners
    if (this.popoverKeyHandler) {
      document.removeEventListener('keydown', this.popoverKeyHandler, true);

      if (this.popoverContainer) {
        this.popoverContainer.removeEventListener('keydown', this.popoverKeyHandler, true);
      }

      this.popoverKeyHandler = null;
    }

    // Remove preview popover arrow style if exists
    if (this._popoverArrowStyle) {
      try {
        this._popoverArrowStyle.remove();
      } catch { /* ignore */ }
      this._popoverArrowStyle = null;
    }

    // Remove click-outside handler if exists
    if (this._popoverClickOutsideHandler) {
      try {
        document.removeEventListener('mousedown', this._popoverClickOutsideHandler, true);
      } catch { /* ignore */ }
      this._popoverClickOutsideHandler = null;
    }

    // Tear down preview titlebar drag handlers
    if (this._previewPopoverDragCleanup) {
      try {
        this._previewPopoverDragCleanup();
      } catch { /* ignore */ }
      this._previewPopoverDragCleanup = null;
    }

    // Tear down resize handles (preview mounts outside PopupManager)
    if (this._popoverResizeDispose) {
      try {
        this._popoverResizeDispose();
      } catch { /* ignore */ }
      this._popoverResizeDispose = null;
    }

    // Tear down hybrid chrome/iframe focus routing
    if (this._popoverHybridFocusCleanup) {
      try {
        this._popoverHybridFocusCleanup();
      } catch { /* ignore */ }
      this._popoverHybridFocusCleanup = null;
    }

    if (this.popoverContainer) {
      // For preview popover (direct mount), remove directly
      // For regular popover, unmount via PopupManager
      if (this._isPreviewPopover) {
        try {
          this.popoverContainer.remove();
        } catch { /* ignore */ }
      } else {
        // Unmount via PopupManager (removes panel + shared backdrop when last popup closes).
        try {
          this.popupManager?.hideModal?.(this._popoverPopupId);
        } catch {
          // Fallback: direct remove
          try { this.popoverContainer.remove(); } catch { /* ignore */ }
        }
      }
      this.popoverContainer = null;
    }

    this._isPreviewPopover = false;

    // After DOM teardown, re-assert page focus (container may have held it).
    try {
      window.focus();
      if (document.body && document.activeElement !== document.body) {
        document.body.focus({ preventScroll: true });
      }
    } catch { /* ignore */ }

    // Restore body scroll (only needed for regular popover, but doesn't hurt)
    document.body.style.overflow = '';
  }

  /**
   * Post a message to the popover iframe bridge (if present).
   * @param {any} message
   * @returns {boolean} Whether a postMessage was attempted successfully
   */
  postMessageToPopoverIframe(message) {
    const win = this.popoverIframeWindow;
    if (!win) return false;
    try {
      win.postMessage(message, '*');
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Cover the overflow box that will jump, run `onCovered` (instant scroll), then uncover.
   * Nested scrollers get their client box; the document root gets the viewport.
   * @param {() => void} onCovered
   * @param {{ durationMs?: number, coverEl?: Element|null, coverRect?: { left: number, top: number, width: number, height: number }|null }} [opts]
   * @returns {Promise<void>}
   */
  async runEdgeJumpFade(onCovered, opts = {}) {
    const durationMs = Number.isFinite(Number(opts.durationMs))
      ? Math.max(80, Number(opts.durationMs))
      : SCROLL.EDGE_JUMP_FADE_MS;

    let reduced = false;
    try {
      reduced = !!window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches;
    } catch { /* ignore */ }
    if (reduced) {
      try { onCovered?.(); } catch { /* ignore */ }
      return;
    }

    const token = ++this._edgeJumpFadeToken;
    const el = this._ensureEdgeJumpFadeEl();
    this._positionEdgeJumpFadeEl(el, opts.coverEl || null, opts.coverRect || null);
    el.style.background = this._resolveEdgeJumpFadeColor(opts.coverEl || null);
    el.style.transition = `opacity ${durationMs}ms ease`;
    await this._fadeEdgeJumpEl(el, 1, durationMs);
    try { onCovered?.(); } catch { /* ignore */ }
    if (token !== this._edgeJumpFadeToken) return;
    await this._fadeEdgeJumpEl(el, 0, durationMs);
    if (token === this._edgeJumpFadeToken) this._removeEdgeJumpFadeEl();
  }

  /**
   * @returns {HTMLElement}
   */
  _ensureEdgeJumpFadeEl() {
    if (this._edgeJumpFadeEl && this._edgeJumpFadeEl.isConnected) return this._edgeJumpFadeEl;
    const el = document.createElement('div');
    el.className = CSS_CLASSES.EDGE_JUMP_FADE;
    el.setAttribute('aria-hidden', 'true');
    const host = document.documentElement || document.body;
    try { host.appendChild(el); } catch { document.body?.appendChild(el); }
    this._edgeJumpFadeEl = el;
    return el;
  }

  /**
   * Pin the veil to the visible overflow box (or the viewport for the document).
   * @param {HTMLElement} veil
   * @param {Element|null} coverEl
   * @param {{ left: number, top: number, width: number, height: number }|null} coverRect
   */
  _positionEdgeJumpFadeEl(veil, coverEl, coverRect) {
    let left = 0;
    let top = 0;
    let width = window.innerWidth || 0;
    let height = window.innerHeight || 0;
    if (coverRect && Number.isFinite(coverRect.width) && Number.isFinite(coverRect.height)) {
      left = coverRect.left;
      top = coverRect.top;
      width = coverRect.width;
      height = coverRect.height;
    } else if (coverEl) {
      try {
        const doc = coverEl.ownerDocument || document;
        const se = doc.scrollingElement;
        const isRoot = coverEl === se || coverEl === doc.documentElement || coverEl === doc.body;
        if (isRoot) {
          left = 0;
          top = 0;
          width = window.innerWidth || 0;
          height = window.innerHeight || 0;
        } else {
          const r = coverEl.getBoundingClientRect();
          left = r.left;
          top = r.top;
          width = r.width;
          height = r.height;
        }
      } catch { /* keep viewport */ }
    }
    veil.style.left = `${Math.round(left)}px`;
    veil.style.top = `${Math.round(top)}px`;
    veil.style.width = `${Math.max(0, Math.round(width))}px`;
    veil.style.height = `${Math.max(0, Math.round(height))}px`;
  }

  /**
   * @param {Element|null} coverEl
   * @returns {string}
   */
  _resolveEdgeJumpFadeColor(coverEl) {
    const isTransparent = (c) => {
      const s = String(c || '').trim().toLowerCase();
      if (!s || s === 'transparent') return true;
      const m = s.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/);
      if (m && m[4] !== undefined && Number(m[4]) === 0) return true;
      return false;
    };
    try {
      let node = coverEl;
      let hops = 0;
      while (node && hops++ < 12) {
        const c = getComputedStyle(node).backgroundColor;
        if (!isTransparent(c)) return c;
        node = node.parentElement;
      }
      const html = getComputedStyle(document.documentElement);
      const body = document.body ? getComputedStyle(document.body) : html;
      for (const c of [body.backgroundColor, html.backgroundColor]) {
        if (!isTransparent(c)) return c;
      }
    } catch { /* ignore */ }
    try {
      if (window.matchMedia?.('(prefers-color-scheme: dark)')?.matches) return '#111';
    } catch { /* ignore */ }
    return '#fff';
  }

  /**
   * @param {HTMLElement} el
   * @param {number} opacity
   * @param {number} ms
   * @returns {Promise<void>}
   */
  _fadeEdgeJumpEl(el, opacity, ms) {
    return new Promise((resolve) => {
      let settled = false;
      const done = () => {
        if (settled) return;
        settled = true;
        el.removeEventListener('transitionend', onEnd);
        resolve();
      };
      const onEnd = (e) => {
        if (e.target !== el || (e.propertyName && e.propertyName !== 'opacity')) return;
        done();
      };
      el.addEventListener('transitionend', onEnd);
      const apply = () => { el.style.opacity = String(opacity); };
      requestAnimationFrame(() => requestAnimationFrame(apply));
      setTimeout(done, ms + 80);
    });
  }

  _removeEdgeJumpFadeEl() {
    this._edgeJumpFadeToken += 1;
    const el = this._edgeJumpFadeEl;
    this._edgeJumpFadeEl = null;
    if (!el) return;
    try { el.remove(); } catch { /* ignore */ }
  }

  /**
   * Scroll the popover iframe by a delta (in pixels).
   * @param {number} deltaY
   * @param {'smooth'|'auto'} behavior
   */
  scrollPopoverBy(deltaY, behavior = 'smooth') {
    return this.postMessageToPopoverIframe({
      type: 'KP_POPOVER_SCROLL',
      command: 'scrollBy',
      delta: deltaY,
      behavior
    });
  }

  scrollPopoverToTop(behavior = 'smooth') {
    return this.postMessageToPopoverIframe({
      type: 'KP_POPOVER_SCROLL',
      command: 'scrollToTop',
      behavior
    });
  }

  scrollPopoverToBottom(behavior = 'smooth') {
    return this.postMessageToPopoverIframe({
      type: 'KP_POPOVER_SCROLL',
      command: 'scrollToBottom',
      behavior
    });
  }

  /**
   * Check if popover is currently open
   * @returns {boolean}
   */
  isPopoverOpen() {
    return this.popoverContainer !== null;
  }

  /**
   * Show a preview popover near the cursor (picture-in-picture style)
   * @param {string} url - URL to load in iframe
   * @param {Object} opts - Options including mouseX, mouseY for positioning
   * @param {'mobile'|'desktop'} [opts.viewportMode] - Override mode; otherwise host preference or desktop default
   */
  async showPreviewPopover(url, opts = {}) {
    // Remove existing popover if any
    this.hidePopover();

    // Prefer HTTPS for preview embeds (many sites refuse insecure framing / mixed content).
    url = preferHttpsForPreview(url);

    const mouseX = opts.mouseX ?? window.innerWidth / 2;
    const popoverWidth = 600;
    const arrowSize = 10; // Kept for drag cleanup / style vars if user resizes later
    const margin = 20; // Margin from viewport edges
    const previewHost = previewHostFromUrl(url);

    // Full viewport height with the existing edge margins (top + bottom).
    const popoverHeight = Math.max(200, window.innerHeight - margin * 2);
    const top = margin;

    // Center horizontally under cursor, but clamp to viewport
    let left = mouseX - popoverWidth / 2;
    left = Math.max(margin, Math.min(left, window.innerWidth - popoverWidth - margin));

    // No top/bottom caret on a full-height panel (would sit outside the viewport).
    const arrowLeft = Math.max(20, Math.min(mouseX - left, popoverWidth - 20));

    const titleText = 'Link Preview';
    const closeKeys = Array.isArray(opts?.closeKeys) && opts.closeKeys.length
      ? opts.closeKeys.map(String)
      : ['Escape', 'e', 'E'];

    // Default desktop; restore per-host Mobile if the user chose it before.
    /** @type {'mobile'|'desktop'} */
    let viewportMode = 'desktop';
    if (opts.viewportMode === 'mobile' || opts.viewportMode === 'desktop') {
      viewportMode = opts.viewportMode;
    } else {
      try {
        viewportMode = await this._getPreviewViewportModeForHost(previewHost);
      } catch {
        viewportMode = 'desktop';
      }
    }

    // Centralized close request
    const requestClosePopover = () => {
      try {
        if (window.__KeyPilotInstance && typeof window.__KeyPilotInstance.handleClosePopover === 'function') {
          window.__KeyPilotInstance.handleClosePopover();
          return;
        }
      } catch (_e) {
        // Ignore and fall back to direct hide
      }
      this.hidePopover();
    };

    const ensureTopMouseTracking = () => {
      if (this._popoverMouseTrackerInstalled) return;
      this._popoverMouseTrackerInstalled = true;
      const update = (e) => {
        try {
          if (!e) return;
          if (typeof e.clientX === 'number') this._popoverLastMouse.x = e.clientX;
          if (typeof e.clientY === 'number') this._popoverLastMouse.y = e.clientY;
        } catch {
          // ignore
        }
      };
      try { document.addEventListener('mousemove', update, true); } catch { /* ignore */ }
      try { document.addEventListener('pointermove', update, true); } catch { /* ignore */ }
    };

    const clickCloseIfHovered = () => {
      try {
        const btn = this.popoverCloseButton;
        if (!btn) return false;
        const x = this._popoverLastMouse.x;
        const y = this._popoverLastMouse.y;
        if (typeof x !== 'number' || typeof y !== 'number') return false;
        const el = document.elementFromPoint(x, y);
        if (!el) return false;
        if (el === btn || btn.contains(el)) {
          try { btn.click(); } catch { /* ignore */ }
          return true;
        }
        return false;
      } catch {
        return false;
      }
    };

    // Create popover container (full viewport height; no top/bottom caret).
    this.popoverContainer = this.createElement('div', {
      className: 'kpv2-preview-popover-container',
      tabindex: '-1',
      role: 'dialog',
      'aria-modal': 'true',
      style: `
        position: fixed;
        top: ${top}px;
        left: ${left}px;
        width: ${popoverWidth}px;
        height: ${popoverHeight}px;
        background: ${NCT_DARK_UI_PANEL_BACKGROUND};
        border-radius: ${NCT_DARK_UI_PANEL_RADIUS};
        border: ${NCT_DARK_UI_PANEL_BORDER};
        box-shadow: ${NCT_DARK_UI_PANEL_BOX_SHADOW};
        display: flex;
        flex-direction: column;
        overflow: hidden;
        --arrow-left: ${arrowLeft}px;
        z-index: ${Z_INDEX.POPUP_PANEL_BASE};
        font-family: ${KP_UI_FONT};
        font-size: 12px;
        line-height: 1.3;
        letter-spacing: normal;
      `
    });
    // Intentionally hybrid:
    // - The light host owns fixed geometry, the caret pseudo-elements, the
    //   iframe viewport, and resize handles. The iframe bridge/focus flow
    //   depends on this host relationship, and the resize utility pins this
    //   host's box while temporarily disabling iframe pointer events.
    // - Only KeyPilot-owned interactive chrome is placed in the open shadow
    //   root, preventing page CSS from restyling the titlebar and controls.
    // Do not move the iframe or resize handles into this shadow root without
    // updating their focus, hit-testing, and geometry contracts together.
    const previewChromeHost = this.createElement('div', {
      className: 'kpv2-preview-popover-chrome-host',
      style: 'display:flex; flex:0 0 auto; flex-direction:column; min-height:0;'
    });
    const previewChromeShadow = ensureOpenChromeShadow(previewChromeHost, { id: 'link-preview-chrome' });
    const previewChromeMount = previewChromeShadow || previewChromeHost;

    // Inject triangle arrow CSS
    const arrowStyle = this.createElement('style');
    arrowStyle.textContent = `
      .kpv2-preview-popover-container::before {
        content: "";
        position: absolute;
        width: 0;
        height: 0;
        left: var(--arrow-left, 50%);
        margin-left: -${arrowSize}px;
        border: ${arrowSize}px solid transparent;
        z-index: 1;
      }
      .kpv2-preview-popover-container::after {
        content: "";
        position: absolute;
        width: 0;
        height: 0;
        left: var(--arrow-left, 50%);
        margin-left: -${arrowSize - 1}px;
        border: ${arrowSize - 1}px solid transparent;
        z-index: 2;
      }
      .kpv2-preview-popover-container[data-placement="bottom"]::before {
        top: -${arrowSize * 2}px;
        border-bottom-color: rgb(43, 43, 43);
      }
      .kpv2-preview-popover-container[data-placement="bottom"]::after {
        top: -${arrowSize * 2 - 1}px;
        border-bottom-color: rgb(18, 18, 18);
      }
      .kpv2-preview-popover-container[data-placement="top"]::before {
        bottom: -${arrowSize * 2}px;
        border-top-color: rgb(43, 43, 43);
      }
      .kpv2-preview-popover-container[data-placement="top"]::after {
        bottom: -${arrowSize * 2 - 1}px;
        border-top-color: rgb(11, 11, 11);
      }
    `;
    document.head.appendChild(arrowStyle);
    this._popoverArrowStyle = arrowStyle;

    // Store iframe reference for focus management
    let iframeRef = null;
    this.popoverBridgeReady = false;

    // Full-bleed viewport shell (mobile and desktop both fill the popover width).
    const iframeViewport = this.createElement('div', {
      className: 'kpv2-preview-popover-viewport',
      style: `
        flex: 1;
        min-height: 0;
        display: flex;
        align-items: stretch;
        justify-content: stretch;
        overflow: hidden;
        background: #0a0a0a;
      `
    });

    // Shared outline Open / Open in New Tab controls (also used by Launcher preview).
    const { actions: previewOpenActions } = createPreviewOpenActionButtons({
      getUrl: () => url,
      afterOpen: () => requestClosePopover(),
      afterOpenNewTab: () => requestClosePopover()
    });

    /**
     * Mobile = mobile User-Agent + client hints (true mobile pages).
     * Desktop = normal desktop UA.
     * Both modes fill the popover width; reload is required for UA to take effect.
     * @param {'mobile'|'desktop'} mode
     * @param {{ reload?: boolean }} [opts]
     */
    const applyViewportMode = async (mode, opts = {}) => {
      const next = mode === 'desktop' ? 'desktop' : 'mobile';
      const prev = viewportMode;
      const shouldReload = opts.reload !== false && prev !== next && !!iframeRef;
      viewportMode = next;
      try {
        this.popoverContainer?.setAttribute('data-kp-preview-viewport', viewportMode);
      } catch { /* ignore */ }

      // Full-width frame in both modes (no side letterboxing).
      if (iframeRef) {
        iframeRef.style.width = '100%';
        iframeRef.style.maxWidth = 'none';
        iframeRef.style.flex = '1 1 auto';
        iframeRef.style.alignSelf = 'stretch';
        iframeRef.style.height = '100%';
      }
      iframeViewport.style.justifyContent = 'stretch';

      // Install / clear mobile UA before any navigation so the document request sees it.
      if (viewportMode === 'mobile') {
        await this._setPreviewMobileUa(true);
      } else {
        await this._setPreviewMobileUa(false);
      }

      if (shouldReload && iframeRef) {
        try {
          // Reset load error UI if we were showing it.
          iframeViewport.style.display = '';
          errorContainer.style.display = 'none';
        } catch { /* ignore */ }
        try {
          armLoadTimeout();
        } catch { /* ignore — armLoadTimeout defined later; first apply never reloads */ }
        try {
          // Force a real navigation with the new UA (same-url assignment can be a no-op).
          iframeRef.src = 'about:blank';
          iframeRef.src = url;
          this.popoverIframeWindow = iframeRef.contentWindow || null;
        } catch {
          try {
            iframeRef.src = url;
            this.popoverIframeWindow = iframeRef.contentWindow || null;
          } catch { /* ignore */ }
        }
      }
    };

    const viewportModeControl = createSegmentedControl({
      value: viewportMode,
      ariaLabel: 'Preview viewport mode',
      className: 'kpv2-preview-viewport-mode',
      options: [
        {
          value: 'mobile',
          label: 'Mobile',
          title: 'Mobile site (mobile User-Agent). Remembered for this website.',
          ariaLabel: 'Mobile preview'
        },
        {
          value: 'desktop',
          label: 'Desktop',
          title: 'Desktop site (default). Remembered for this website.',
          ariaLabel: 'Desktop preview'
        }
      ],
      onChange: (value) => {
        void (async () => {
          await applyViewportMode(value, { reload: true });
          // Persist per website so future Link Previews open in the same mode.
          try {
            await this._setPreviewViewportModeForHost(previewHost, value === 'mobile' ? 'mobile' : 'desktop');
          } catch { /* ignore */ }
        })();
      }
    });

    // Single standard titlebar (drag handle): title + hint + mode + actions + uniform × close.
    const titlebarApi = createPopoverTitlebar({
      title: titleText,
      variant: 'preview',
      draggable: true,
      titleAttr: 'Drag to move',
      showClose: true,
      onClose: requestClosePopover,
      closeTitle: 'Close (Esc)',
      hint: 'Press Esc / E to hide',
      actions: [viewportModeControl.root, previewOpenActions],
      className: 'kpv2-preview-popover-titlebar'
    });
    const header = titlebarApi.titlebar;
    const closeButton = titlebarApi.closeButton;
    this.popoverCloseButton = closeButton;
    ensureTopMouseTracking();

    // Titlebar drag: move the preview popover; clamp to viewport; hide caret once moved.
    let dragState = null;
    const DRAG_MOVE_THRESHOLD_PX = 3;

    const clampPopoverPosition = (leftPx, topPx) => {
      const el = this.popoverContainer;
      if (!el) return { left: leftPx, top: topPx };
      const w = el.offsetWidth || popoverWidth;
      const h = el.offsetHeight || popoverHeight;
      const maxLeft = Math.max(margin, window.innerWidth - w - margin);
      const maxTop = Math.max(margin, window.innerHeight - h - margin);
      return {
        left: Math.max(margin, Math.min(leftPx, maxLeft)),
        top: Math.max(margin, Math.min(topPx, maxTop))
      };
    };

    const hidePreviewArrow = () => {
      const el = this.popoverContainer;
      if (!el || el.dataset.kpDragged === '1') return;
      el.dataset.kpDragged = '1';
      el.removeAttribute('data-placement');
    };

    const onDragPointerMove = (e) => {
      if (!dragState || !this.popoverContainer) return;
      const dx = e.clientX - dragState.startX;
      const dy = e.clientY - dragState.startY;
      if (!dragState.moved) {
        if (Math.abs(dx) < DRAG_MOVE_THRESHOLD_PX && Math.abs(dy) < DRAG_MOVE_THRESHOLD_PX) {
          return;
        }
        dragState.moved = true;
        hidePreviewArrow();
        header.style.cursor = 'grabbing';
        // Prevent iframe from swallowing pointer events mid-drag
        if (this.popoverIframeElement) {
          this.popoverIframeElement.style.pointerEvents = 'none';
        }
      }
      const next = clampPopoverPosition(dragState.originLeft + dx, dragState.originTop + dy);
      this.popoverContainer.style.left = `${next.left}px`;
      this.popoverContainer.style.top = `${next.top}px`;
    };

    const endDrag = (e) => {
      if (!dragState) return;
      const pointerId = dragState.pointerId;
      dragState = null;
      header.style.cursor = 'grab';
      if (this.popoverIframeElement) {
        this.popoverIframeElement.style.pointerEvents = '';
      }
      try {
        if (e && typeof e.pointerId === 'number') {
          header.releasePointerCapture(e.pointerId);
        } else if (typeof pointerId === 'number') {
          header.releasePointerCapture(pointerId);
        }
      } catch { /* ignore */ }
      document.removeEventListener('pointermove', onDragPointerMove, true);
      document.removeEventListener('pointerup', endDrag, true);
      document.removeEventListener('pointercancel', endDrag, true);
    };

    const onTitlebarPointerDown = (e) => {
      // Action / close buttons keep their own click behavior (don't start a drag)
      const interactive = titlebarApi.getInteractiveElements();
      for (const btn of interactive) {
        if (e.target === btn || (btn.contains && btn.contains(e.target))) {
          return;
        }
      }
      // Primary button only for mouse
      if (e.pointerType === 'mouse' && e.button !== 0) return;
      if (!this.popoverContainer) return;

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

      const rect = this.popoverContainer.getBoundingClientRect();
      dragState = {
        startX: e.clientX,
        startY: e.clientY,
        originLeft: rect.left,
        originTop: rect.top,
        pointerId: e.pointerId,
        moved: false
      };

      try {
        header.setPointerCapture(e.pointerId);
      } catch { /* ignore */ }

      document.addEventListener('pointermove', onDragPointerMove, true);
      document.addEventListener('pointerup', endDrag, true);
      document.addEventListener('pointercancel', endDrag, true);
    };

    header.addEventListener('pointerdown', onTitlebarPointerDown);

    this._previewPopoverDragCleanup = () => {
      endDrag();
      try {
        header.removeEventListener('pointerdown', onTitlebarPointerDown);
      } catch { /* ignore */ }
    };

    // Generic resize handles (edges + SE grip). Shared util used by all popovers.
    try {
      this._popoverResizeDispose?.();
    } catch { /* ignore */ }
    try {
      const resizeApi = makePopoverResizable(this.popoverContainer, {
        minWidth: 280,
        minHeight: 200,
        margin,
        onResizeStart: () => {
          hidePreviewArrow();
        }
      });
      this._popoverResizeDispose = resizeApi?.dispose || null;
    } catch (e) {
      console.warn('[KeyPilot] Failed to make preview popover resizable:', e?.message || e);
      this._popoverResizeDispose = null;
    }

    // Create error message container (initially hidden)
    const errorContainer = this.createElement('div', {
      className: 'kpv2-popover-error',
      style: `
        flex: 1;
        display: none;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        padding: 20px;
        text-align: center;
        background: #f9f9f9;
      `
    });
    // `display:contents` preserves the original flex fallback layout while
    // still giving KeyPilot's error UI its own isolated shadow tree.
    const previewErrorHost = this.createElement('div', {
      className: 'kpv2-preview-popover-error-host',
      style: 'display:contents;'
    });
    const previewErrorShadow = ensureOpenChromeShadow(previewErrorHost, { id: 'link-preview-error' });
    const previewErrorMount = previewErrorShadow || previewErrorHost;

    const errorIcon = this.createElement('div', {
      style: `
        font-size: 32px;
        margin-bottom: 12px;
        color: #999;
      `
    });
    errorIcon.textContent = '🚫';
    errorContainer.appendChild(errorIcon);

    const errorTitle = this.createElement('div', {
      style: `
        font-size: 14px;
        font-weight: 600;
        color: #333;
        margin-bottom: 6px;
      `
    });
    errorTitle.textContent = 'Cannot Display Page';
    errorContainer.appendChild(errorTitle);

    const errorMessage = this.createElement('div', {
      style: `
        font-size: 12px;
        color: #666;
        margin-bottom: 16px;
        max-width: 300px;
      `
    });
    errorMessage.textContent = 'This website prevents embedding in iframes.';
    errorContainer.appendChild(errorMessage);

    const openInTabButton = this.createElement('button', {
      style: `
        background: #4CAF50;
        color: white;
        border: none;
        padding: 8px 16px;
        border-radius: 4px;
        cursor: pointer;
        font-size: 12px;
        font-weight: 500;
      `
    });
    openInTabButton.textContent = 'Open in New Tab';
    openInTabButton.onclick = () => {
      window.open(url, '_blank');
      requestClosePopover();
    };
    errorContainer.appendChild(openInTabButton);

    // Create iframe full-bleed; src is set only after mobile UA is installed (when needed).
    const iframe = this.createElement('iframe', {
      tabindex: '0',
      style: `
        border: none;
        width: 100%;
        max-width: none;
        height: 100%;
        flex: 1 1 auto;
        align-self: stretch;
        background: #fff;
      `
    });
    iframeRef = iframe;
    this.popoverIframeElement = iframe;
    this.popoverIframeWindow = iframe.contentWindow || null;

    // Initialize the iframe bridge; pass closeKeys so Esc/E close from inside the frame.
    const sendBridgeInit = () => {
      try {
        iframe.contentWindow?.postMessage({
          type: 'KP_POPOVER_BRIDGE_INIT',
          closeKeys
        }, '*');
      } catch {
        // Ignore
      }
    };

    // Detect iframe load errors / hangs
    /** @type {ReturnType<typeof setTimeout>|null} */
    let loadTimeout = null;
    const armLoadTimeout = () => {
      if (loadTimeout) {
        try { clearTimeout(loadTimeout); } catch { /* ignore */ }
      }
      loadTimeout = setTimeout(() => {
        console.log('[KeyPilot] Iframe load timeout - showing error as fallback');
        iframeViewport.style.display = 'none';
        errorContainer.style.display = 'flex';
      }, 30000);
    };
    const clearLoadTimeout = () => {
      if (!loadTimeout) return;
      try { clearTimeout(loadTimeout); } catch { /* ignore */ }
      loadTimeout = null;
    };

    iframe.onerror = () => {
      console.log('[KeyPilot] Iframe load error detected');
      clearLoadTimeout();
      iframeViewport.style.display = 'none';
      errorContainer.style.display = 'flex';
    };

    iframe.onload = () => {
      // Ignore the intermediate about:blank used when switching UA modes.
      try {
        const srcAttr = iframe.getAttribute('src') || '';
        if (srcAttr === 'about:blank' || iframe.src === 'about:blank') {
          return;
        }
      } catch { /* ignore */ }
      clearLoadTimeout();
      console.log('[KeyPilot] Iframe loaded successfully');
      sendBridgeInit();
    };

    iframeViewport.appendChild(iframe);
    previewChromeMount.appendChild(header);
    previewErrorMount.appendChild(errorContainer);
    this.popoverContainer.appendChild(previewChromeHost);
    this.popoverContainer.appendChild(iframeViewport);
    this.popoverContainer.appendChild(previewErrorHost);

    // Mark this as a preview popover (for cleanup logic)
    this._isPreviewPopover = true;

    // Mount directly to document.body (no backdrop/blur for preview popover)
    try {
      document.body.appendChild(this.popoverContainer);
    } catch (e) {
      console.error('[KeyPilot] Failed to mount preview popover:', e);
      this._isPreviewPopover = false;
      return;
    }

    // Apply UA for remembered/default mode before first navigation (desktop = no spoof).
    void (async () => {
      try {
        // First apply: set UA without reload; then assign src once.
        await applyViewportMode(viewportMode, { reload: false });
      } catch (e) {
        console.warn('[KeyPilot] Failed to prepare preview viewport mode:', e?.message || e);
      }
      try {
        armLoadTimeout();
        iframe.src = url;
        this.popoverIframeWindow = iframe.contentWindow || null;
      } catch (e) {
        console.error('[KeyPilot] Failed to load preview URL:', e?.message || e);
        clearLoadTimeout();
        iframeViewport.style.display = 'none';
        errorContainer.style.display = 'flex';
      }
    })();

    // Add click-outside-to-close handler for preview popover
    this._popoverClickOutsideHandler = (e) => {
      // Check if click is outside the popover container
      if (this.popoverContainer && !this.popoverContainer.contains(e.target)) {
        console.log('[KeyPilot] Click outside preview popover, closing');
        requestClosePopover();
      }
    };
    // Use a small delay to avoid immediately closing if the preview key click triggered this
    setTimeout(() => {
      if (this.popoverContainer) {
        document.addEventListener('mousedown', this._popoverClickOutsideHandler, true);
      }
    }, 100);

    sendBridgeInit();

    // Retry window for iframe bridge init
    try {
      let attemptsLeft = 6;
      this.popoverInitTimer = setInterval(() => {
        if (!this.popoverContainer || attemptsLeft <= 0) {
          clearInterval(this.popoverInitTimer);
          this.popoverInitTimer = null;
          return;
        }
        attemptsLeft -= 1;
        sendBridgeInit();
      }, 250);
    } catch {
      // Ignore
    }

    // Don't prevent body scroll for preview popover - page should remain interactive

    // Parent-side close while focus is on chrome (titlebar / buttons).
    // When focus is in the iframe, Esc/E are handled by the bridge → REQUEST_CLOSE.
    const handlePopoverKeyDown = (e) => {
      console.log('[KeyPilot] Preview popover key event:', e.key);

      if (e.key === 'Escape' || closeKeys.includes(String(e.key))) {
        // Only handle here if focus is still in the parent document.
        // (If focus is in the iframe, the bridge already owns these keys.)
        const ae = document.activeElement;
        const focusInIframe = ae === iframeRef || ae === iframe;
        if (focusInIframe) return;

        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
        requestClosePopover();
        return;
      }
    };

    document.addEventListener('keydown', handlePopoverKeyDown, true);
    this.popoverContainer.addEventListener('keydown', handlePopoverKeyDown, true);

    this.popoverKeyHandler = handlePopoverKeyDown;

    // Listen for messages from iframe bridge
    this.popoverMessageHandler = (event) => {
      const data = event?.data;
      if (!data || typeof data.type !== 'string') return;
      if (this.popoverIframeWindow && event.source !== this.popoverIframeWindow) return;

      if (data.type === 'KP_POPOVER_BRIDGE_READY') {
        this.popoverBridgeReady = true;
        // Auto-focus iframe so full KeyPilot works on the preview page without a click.
        // Esc/E close via bridge → parent; hybrid focus returns to chrome on titlebar hover.
        this._focusPopoverIframe(iframeRef);
        this._installPopoverHybridFocus({
          iframe: iframeRef,
          chromeEls: [
            header,
            closeButton,
            viewportModeControl.root,
            previewOpenActions
          ].filter(Boolean),
          focusChromeEl: closeButton || header
        });
        return;
      }

      if (data.type === 'KP_POPOVER_REQUEST_CLOSE') {
        if (closeKeys.includes(String(data.key))) requestClosePopover();
      }

      if (data.type === 'KP_POPOVER_LAUNCH_WALKTHROUGH') {
        requestClosePopover();
        try {
          const ob = window.__KeyPilotOnboarding;
          if (ob && typeof ob.resetTutorial === 'function') {
            void ob.resetTutorial();
          }
        } catch {
          // ignore
        }
        return;
      }

      if (data.type === 'KP_POPOVER_BRIDGE_KEYDOWN') {
        const k = String(data.key || '');
        if (k === 'f' || k === 'F') {
          clickCloseIfHovered();
        }
      }
    };
    window.addEventListener('message', this.popoverMessageHandler, true);

    // Until bridge ready, park focus on chrome.
    try {
      (closeButton || header)?.focus?.();
    } catch (_e) {
      try {
        this.popoverContainer.focus();
      } catch (_e2) {
        // Ignore
      }
    }
  }

  // =============================================================================
  // DEBUG PANEL - Performance metrics display in upper-right corner
  // =============================================================================

  /**
   * Initialize debug panel if enabled
   */
  initDebugPanel() {
    if (!FEATURE_FLAGS.ENABLE_DEBUG_PANEL) return;

    this.createDebugPanel();
    this.startDebugPanelUpdates();
  }

  /**
   * Create the debug panel element
   */
  createDebugPanel() {
    if (this.debugPanel) return;

    this.debugPanel = document.createElement('div');
    this.debugPanel.id = 'kpv2-debug-panel';
    this.debugPanel.style.cssText = `
      position: fixed;
      top: 10px;
      right: 10px;
      background: rgba(0, 0, 0, 0.9);
      color: #00ff00;
      font-family: monospace;
      font-size: 11px;
      padding: 8px 12px;
      border-radius: 4px;
      border: 1px solid #333;
      z-index: ${Z_INDEX.DEBUG_HUD};
      pointer-events: none;
      max-width: 300px;
      white-space: pre-wrap;
      line-height: 1.4;
    `;

    document.body.appendChild(this.debugPanel);
  }

  /**
   * Start periodic updates of debug panel data
   */
  startDebugPanelUpdates() {
    if (!this.debugPanel) return;

    // Update immediately
    this.updateDebugPanel();

    // Update every 2 seconds
    this.debugPanelUpdateInterval = setInterval(() => {
      this.updateDebugPanel();
    }, 2000);
  }

  /**
   * Update debug panel with current metrics
   */
  updateDebugPanel() {
    if (!this.debugPanel || !FEATURE_FLAGS.ENABLE_DEBUG_PANEL) return;

    const data = this.collectDebugData();
    const content = this.formatDebugContent(data);

    this.debugPanel.textContent = content;
  }

  /**
   * Collect debug data from various sources
   */
  collectDebugData() {
    const intersectionManager = window.keyPilot?.intersectionManager;
    const complexPageDetector = intersectionManager?.complexPageDetector;
    const currentState = window.keyPilot?.state?.getState?.() || {};

    // Get hover element information
    const hoverElement = currentState.focusEl;
    let hoverInfo = 'None';
    let clickableReasons = [];
    if (hoverElement) {
      const tagName = hoverElement.tagName?.toLowerCase() || 'unknown';
      const className = hoverElement.className ? `.${hoverElement.className.split(' ')[0]}` : '';
      const id = hoverElement.id ? `#${hoverElement.id}` : '';
      const href = hoverElement.href ? ` → ${hoverElement.href.substring(0, 30)}...` : '';
      const text = hoverElement.textContent?.trim().substring(0, 20) || '';
      hoverInfo = `${tagName}${id}${className}${href ? href : text ? ` "${text}..."` : ''}`;

      // Analyze why this element is clickable
      clickableReasons = this.analyzeClickableReasons(hoverElement);
    }

    const isComplexPage = complexPageDetector ? complexPageDetector.complexityLevel !== 'low' : false;

    return {
      // Page complexity
      complexityLevel: complexPageDetector?.complexityLevel || 'unknown',
      isComplexPage: isComplexPage,

      // Hover information
      hoverElement: hoverInfo,
      clickableReasons: clickableReasons,

      // Element counts
      totalElements: document.querySelectorAll('*').length,
      interactiveElements: document.querySelectorAll(
        'a[href], button, input, select, textarea, [role="button"], [role="link"], [contenteditable="true"], [onclick]'
      ).length,

      // IO metrics
      ioObservations: intersectionManager?.observedInteractiveElements?.size || 0,
      visibleElements: intersectionManager?.visibleInteractiveElements?.size || 0,

      // RBush metrics
      rbushEnabled: intersectionManager?._rtreeEnabled?.() || false,
      rbushItems: intersectionManager?._rtreeItemsByElement?.size || 0,
      rbushQueries: intersectionManager?.metrics?.rtreeQueries || 0,
      rbushHits: intersectionManager?.metrics?.rtreeHits || 0,

      // Performance metrics
      cacheHits: intersectionManager?.metrics?.cacheHits || 0,
      cacheMisses: intersectionManager?.metrics?.cacheMisses || 0,
      observerUpdates: intersectionManager?.metrics?.observerUpdates || 0,

      // Culling stats (only for medium/high complexity sites)
      culledCount: isComplexPage ? (intersectionManager?.metrics?.culledCount || 0) : 0,
      totalCulled: isComplexPage ? (intersectionManager?.metrics?.totalCulled || 0) : 0,

      // Rendering mode
      renderingMode: this.renderingMode || 'unknown',

      // Memory estimate
      estimatedMemory: this.estimateMemoryUsage()
    };
  }

  /**
   * Estimate memory usage of various components
   */
  estimateMemoryUsage() {
    const intersectionManager = window.keyPilot?.intersectionManager;
    let memoryKB = 0;

    // Estimate RBush memory (rough approximation)
    if (intersectionManager?._rtreeItemsByElement) {
      memoryKB += intersectionManager._rtreeItemsByElement.size * 0.5; // ~0.5KB per item
    }

    // Estimate element cache memory
    if (intersectionManager?.elementPositionCache) {
      memoryKB += intersectionManager.elementPositionCache.size * 1; // ~1KB per cached element
    }

    return Math.round(memoryKB);
  }

  /**
   * Analyze why an element is considered clickable
   */
  analyzeClickableReasons(element) {
    if (!element || element.nodeType !== 1) return [];

    const reasons = [];

    // Check selector matches
    const focusableSel = 'a[href], button, input, select, textarea, video, audio, [contenteditable="true"], [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="tab"], [data-action], [data-toggle], [data-click], [data-href], [data-link], [vue-click], [ng-click]';
    const matchesSelector = element.matches && element.matches(focusableSel);
    if (matchesSelector) {
      reasons.push('CSS Selector Match');
    }

    // Check role attribute
    const role = (element.getAttribute && (element.getAttribute('role') || '').trim().toLowerCase()) || '';
    const clickableRoles = ['link', 'button', 'slider', 'checkbox', 'radio', 'tab', 'menuitem', 'option', 'switch', 'treeitem', 'combobox', 'spinbutton'];
    const hasRole = role && clickableRoles.includes(role);
    if (hasRole) {
      reasons.push(`ARIA Role: "${role}"`);
    }

    // Check click handlers
    const hasOnClick = element.onclick || element.getAttribute('onclick');
    const elementDetector = window.keyPilot?.elementDetector;
    const hasTrackedClickHandler = elementDetector?.hasTrackedClickHandler?.(element);
    if (hasOnClick) {
      reasons.push('onclick Attribute');
    }
    if (hasTrackedClickHandler) {
      reasons.push('Event Listener (click)');
    }

    // Check cursor style (always check, not just when no other reasons found).
    // Use ElementDetector so CUSTOM_CURSORS mode still reports real page pointers.
    try {
      const hasCursor = elementDetector && typeof elementDetector.hasExplicitCursorPointer === 'function'
        ? elementDetector.hasExplicitCursorPointer(element)
        : !!(window.getComputedStyle && window.getComputedStyle(element).cursor === 'pointer');
      if (hasCursor) {
        reasons.push('CSS cursor: pointer');
      }
    } catch {
      // Ignore getComputedStyle errors
    }

    // If element is determined to be clickable by isLikelyInteractive but we haven't found any reasons,
    // add a generic reason to ensure we show something
    if (reasons.length === 0) {
      try {
        const elementDetector = window.keyPilot?.elementDetector;
        if (elementDetector && typeof elementDetector.isLikelyInteractive === 'function') {
          const isClickable = elementDetector.isLikelyInteractive(element);
          if (isClickable) {
            // If it's clickable but we didn't find a specific reason, check more thoroughly
            // This handles cases where the tracked click handler might not be detected yet
            // or where cursor pointer wasn't checked due to other conditions
            reasons.push('Likely Interactive (detected by element detector)');
          }
        }
      } catch (e) {
        // If isLikelyInteractive check fails, still try to provide a reason
        // Check if element is in the intersection observer's observed elements
        const intersectionManager = window.keyPilot?.intersectionManager;
        if (intersectionManager?.observedInteractiveElements?.has?.(element)) {
          reasons.push('Observed as Interactive Element');
        }
      }
    }

    return reasons;
  }

  /**
   * Format debug data into readable display content
   */
  formatDebugContent(data) {
    const cacheHitRate = data.cacheHits + data.cacheMisses > 0
      ? Math.round((data.cacheHits / (data.cacheHits + data.cacheMisses)) * 100)
      : 0;

    const rbushHitRate = data.rbushQueries > 0
      ? Math.round((data.rbushHits / data.rbushQueries) * 100)
      : 0;

    const cullingSection = data.isComplexPage ? `

🗑️  Spatial Culling
Last Cull: ${data.culledCount.toLocaleString()}
Total Culled: ${data.totalCulled.toLocaleString()}` : '';

    const clickableReasonsSection = data.clickableReasons.length > 0
      ? `\n🔍 Clickable Because:\n${data.clickableReasons.map(reason => `  • ${reason}`).join('\n')}`
      : '';

    return `KeyPilot Debug Panel
───────────────────
Complexity: ${data.complexityLevel.toUpperCase()} ${data.isComplexPage ? '🔴' : '🟢'}
Renderer: ${data.renderingMode.toUpperCase()}

🎯 Hover: ${data.hoverElement}${clickableReasonsSection}

📊 Elements
Total: ${data.totalElements.toLocaleString()}
Interactive: ${data.interactiveElements.toLocaleString()}

👁️  IO Observer
Observed: ${data.ioObservations.toLocaleString()}
Visible: ${data.visibleElements.toLocaleString()}

🌳 RBush Index
Enabled: ${data.rbushEnabled ? '✅' : '❌'}
Items: ${data.rbushItems.toLocaleString()}
Queries: ${data.rbushQueries.toLocaleString()}
Hit Rate: ${rbushHitRate}%${cullingSection}

⚡ Performance
Cache Hits: ${data.cacheHits.toLocaleString()} (${cacheHitRate}%)
Observer Updates: ${data.observerUpdates.toLocaleString()}

💾 Memory: ~${data.estimatedMemory}KB`;
  }

  /**
   * Clean up debug panel
   */
  cleanupDebugPanel() {
    if (this.debugPanelUpdateInterval) {
      clearInterval(this.debugPanelUpdateInterval);
      this.debugPanelUpdateInterval = null;
    }

    if (this.debugPanel) {
      this.debugPanel.remove();
      this.debugPanel = null;
    }
  }

  // =============================================================================
  // SHADOW ROOT DEBUG HUD — leaf / focus / paint + force A|B|C
  // =============================================================================

  /**
   * Enable or disable the interactive shadow-root paint debug HUD.
   * Runtime: `keyPilot.setShadowRootDebugHud(true)`
   * @param {boolean} enabled
   */
  setShadowRootDebugHud(enabled) {
    this._shadowDebugHudEnabled = !!enabled;
    try {
      if (FEATURE_FLAGS) FEATURE_FLAGS.DEBUG_SHADOW_ROOT_HUD = !!enabled;
    } catch { /* ignore */ }

    if (this._shadowDebugHudEnabled) {
      this._ensureShadowRootDebugHud();
      // Re-paint current focus with current override (if any).
      try {
        const focusEl = window.keyPilot?.state?.getState?.()?.focusEl ||
          window.keyPilot?.intersectionManager?.getDomHoveredElement?.() ||
          null;
        if (focusEl) this.updateFocusOverlay(focusEl);
        else this._updateShadowRootDebugHud(null, null, {
          inShadow: false,
          autoStrategy: '—',
          appliedStrategy: '—',
          override: this._shadowDebugPaintOverride
        });
      } catch { /* ignore */ }
    } else {
      this._shadowDebugPaintOverride = null;
      this.cleanupShadowRootDebugHud();
      // Restore auto paint for current focus.
      try {
        const focusEl = window.keyPilot?.state?.getState?.()?.focusEl || null;
        if (focusEl) this.updateFocusOverlay(focusEl);
      } catch { /* ignore */ }
    }
  }

  /**
   * Force paint strategy while the HUD is open.
   * @param {'A'|'B'|'C'|null|string} strategy - null / 'auto' clears override
   */
  setShadowDebugPaintStrategy(strategy) {
    const s = strategy == null ? null : String(strategy).toUpperCase();
    if (s === 'A' || s === 'B' || s === 'C') {
      this._shadowDebugPaintOverride = s;
    } else {
      this._shadowDebugPaintOverride = null;
    }
    this._refreshShadowDebugHudButtons();
    try {
      const focusEl = window.keyPilot?.state?.getState?.()?.focusEl ||
        window.keyPilot?.intersectionManager?.getDomHoveredElement?.() ||
        null;
      if (focusEl) this.updateFocusOverlay(focusEl);
    } catch { /* ignore */ }
  }

  /** @returns {boolean} */
  isShadowRootDebugHudEnabled() {
    return !!this._shadowDebugHudEnabled;
  }

  cleanupShadowRootDebugHud() {
    if (this._shadowDebugHud) {
      try { this._shadowDebugHud.remove(); } catch { /* ignore */ }
      this._shadowDebugHud = null;
    }
  }

  /**
   * Compact element description for the HUD.
   * @param {Element|null|undefined} el
   * @returns {{ line: string, detail: string }}
   */
  _describeElForShadowDebug(el) {
    if (!el || el.nodeType !== 1) {
      return { line: '(none)', detail: '' };
    }
    const tag = el.tagName || '?';
    const id = el.id ? `#${el.id}` : '';
    let cls = '';
    try {
      const cn = typeof el.className === 'string'
        ? el.className
        : (el.className && el.className.baseVal) || '';
      const parts = String(cn).trim().split(/\s+/).filter(Boolean).slice(0, 2);
      cls = parts.length ? '.' + parts.join('.') : '';
    } catch { /* ignore */ }

    let wh = '?×?';
    try {
      const r = el.getBoundingClientRect();
      wh = `${Math.round(r.width)}×${Math.round(r.height)}`;
    } catch { /* ignore */ }

    let inShadow = false;
    const hosts = [];
    try {
      inShadow = this._isInShadowTree(el);
      let n = el;
      let depth = 0;
      while (n && depth++ < 8) {
        const root = typeof n.getRootNode === 'function' ? n.getRootNode() : null;
        if (root && typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot) {
          hosts.push(root.host?.tagName || 'HOST');
          n = root.host;
        } else {
          break;
        }
      }
    } catch { /* ignore */ }

    let href = '';
    try {
      if (el.href) href = String(el.href).slice(0, 56);
    } catch { /* ignore */ }

    const line = `${tag}${id}${cls}`.slice(0, 72);
    const detailParts = [
      wh,
      inShadow ? `shadow:${hosts.join('←') || '?'}` : 'light',
      href ? href : null
    ].filter(Boolean);
    return { line, detail: detailParts.join(' · ') };
  }

  _ensureShadowRootDebugHud() {
    if (this._shadowDebugHud && this._shadowDebugHud.isConnected) {
      this._shadowDebugHud.style.display = 'block';
      this._refreshShadowDebugHudButtons();
      return this._shadowDebugHud;
    }

    const hud = document.createElement('div');
    hud.id = 'kpv2-shadow-debug-hud';
    hud.className = 'kpv2-shadow-debug-hud';
    hud.setAttribute('data-kp-ui', '1');
    hud.setAttribute('aria-label', 'KeyPilot shadow root debug HUD');
    hud.style.cssText = `
      position: fixed;
      bottom: 12px;
      left: 12px;
      z-index: ${Z_INDEX.DEBUG_HUD};
      width: min(420px, calc(100vw - 24px));
      max-height: min(50vh, 420px);
      overflow: auto;
      background: rgba(12, 16, 22, 0.94);
      color: #d7ffe8;
      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
      font-size: 11px;
      line-height: 1.35;
      padding: 10px 12px 12px;
      border-radius: 8px;
      border: 1px solid #3d8f6a;
      box-shadow: 0 8px 28px rgba(0,0,0,0.45);
      pointer-events: auto;
      user-select: text;
    `;

    hud.innerHTML = `
      <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;">
        <strong style="color:#7CFFB2;letter-spacing:0.02em;">Shadow Root Debug</strong>
        <button type="button" data-kp-shadow-dbg="close"
          style="background:#243028;color:#cfe;border:1px solid #3d8f6a;border-radius:4px;padding:2px 8px;cursor:pointer;font:inherit;">×</button>
      </div>
      <div data-kp-shadow-dbg-body style="white-space:pre-wrap;margin-bottom:10px;color:#b8eccf;"></div>
      <div style="display:flex;flex-wrap:wrap;gap:6px;align-items:center;">
        <span style="color:#8ab89a;">Paint:</span>
        <button type="button" data-kp-shadow-dbg-strategy="auto">Auto</button>
        <button type="button" data-kp-shadow-dbg-strategy="A">A outline</button>
        <button type="button" data-kp-shadow-dbg-strategy="B">B in-target</button>
        <button type="button" data-kp-shadow-dbg-strategy="C">C fixed</button>
      </div>
      <div style="margin-top:8px;color:#6f9a80;font-size:10px;">
        A = element outline · B = absolute ring in host · C = body fixed overlay
      </div>
    `;

    // Shared button chrome; active state set in _refreshShadowDebugHudButtons.
    try {
      hud.querySelectorAll('button[data-kp-shadow-dbg-strategy]').forEach((btn) => {
        btn.style.cssText = `
          background:#1a2820;color:#cfe;border:1px solid #3d5a48;border-radius:4px;
          padding:4px 8px;cursor:pointer;font:inherit;
        `;
      });
    } catch { /* ignore */ }

    const stop = (e) => {
      try { e.stopPropagation(); } catch { /* ignore */ }
      try { e.stopImmediatePropagation?.(); } catch { /* ignore */ }
    };
    hud.addEventListener('pointerdown', stop, true);
    hud.addEventListener('pointerup', stop, true);
    hud.addEventListener('click', (e) => {
      stop(e);
      const t = e.target;
      if (!(t instanceof Element)) return;
      if (t.getAttribute('data-kp-shadow-dbg') === 'close') {
        this.setShadowRootDebugHud(false);
        return;
      }
      const strat = t.getAttribute('data-kp-shadow-dbg-strategy');
      if (strat === 'auto') this.setShadowDebugPaintStrategy(null);
      else if (strat === 'A' || strat === 'B' || strat === 'C') {
        this.setShadowDebugPaintStrategy(strat);
      }
    }, true);

    document.body.appendChild(hud);
    this._shadowDebugHud = hud;
    this._refreshShadowDebugHudButtons();
    return hud;
  }

  _refreshShadowDebugHudButtons() {
    const hud = this._shadowDebugHud;
    if (!hud) return;
    const active = this._shadowDebugPaintOverride; // null = auto
    try {
      hud.querySelectorAll('button[data-kp-shadow-dbg-strategy]').forEach((btn) => {
        const key = btn.getAttribute('data-kp-shadow-dbg-strategy');
        const isOn =
          (key === 'auto' && !active) ||
          (key === active);
        btn.style.background = isOn ? '#2f6b4a' : '#1a2820';
        btn.style.borderColor = isOn ? '#7CFFB2' : '#3d5a48';
        btn.style.color = isOn ? '#eafff2' : '#cfe';
        btn.style.fontWeight = isOn ? '700' : '400';
      });
    } catch { /* ignore */ }
  }

  /**
   * Refresh leaf line after sticky-host pointer moves (focusEl/paint unchanged).
   * Called from IntersectionObserverManager — does not re-run A/B/C paint.
   */
  refreshShadowRootDebugHudLeaf() {
    if (!this._shadowDebugHudEnabled) return;
    const last = this._shadowDebugLastInfo;
    let focusEl = last?.focus || null;
    try {
      if (!focusEl || !focusEl.isConnected) {
        focusEl = window.keyPilot?.state?.getState?.()?.focusEl ||
          window.keyPilot?.intersectionManager?.getDomHoveredElement?.() ||
          null;
      }
    } catch { focusEl = last?.focus || null; }

    let paintEl = last?.paint || null;
    if (focusEl && (!paintEl || !paintEl.isConnected)) {
      try {
        paintEl = this._resolveElementForFocusStyling(focusEl) || focusEl;
      } catch {
        paintEl = focusEl;
      }
    }

    let inShadow = !!last?.inShadow;
    try {
      if (focusEl) inShadow = this._isInShadowTree(focusEl);
    } catch { /* ignore */ }

    this._updateShadowRootDebugHud(focusEl, paintEl, {
      inShadow,
      autoStrategy: last?.auto || '—',
      appliedStrategy: last?.applied || '—',
      override: this._shadowDebugPaintOverride
    });
  }

  /**
   * Refresh HUD text from current hover / paint decision.
   * @param {Element|null} focusEl
   * @param {Element|null} paintEl
   * @param {{ inShadow: boolean, autoStrategy: string, appliedStrategy: string, override: string|null }} meta
   */
  _updateShadowRootDebugHud(focusEl, paintEl, meta) {
    if (!this._shadowDebugHudEnabled) return;
    const hud = this._ensureShadowRootDebugHud();
    if (!hud) return;

    let leaf = null;
    try {
      leaf = window.keyPilot?.intersectionManager?.getDomHoverLeaf?.() ||
        window.__KP_HOVER_LEAF ||
        null;
    } catch { leaf = null; }

    const leafDesc = this._describeElForShadowDebug(leaf);
    const focusDesc = this._describeElForShadowDebug(focusEl);
    const paintDesc = this._describeElForShadowDebug(paintEl);

    const override = meta?.override || null;
    const autoStrategy = meta?.autoStrategy || '—';
    const applied = meta?.appliedStrategy || '—';
    const inShadow = !!meta?.inShadow;

    this._shadowDebugLastInfo = {
      leaf,
      focus: focusEl,
      paint: paintEl,
      auto: autoStrategy,
      applied,
      inShadow
    };

    const body = hud.querySelector('[data-kp-shadow-dbg-body]');
    if (!body) return;

    const paintSame =
      focusEl && paintEl && focusEl === paintEl
        ? 'same as focus'
        : (paintEl ? 'resolved (may pierce shadow)' : '—');

    body.textContent =
`Leaf (under pointer)
  ${leafDesc.line}
  ${leafDesc.detail || '—'}

Hover target (focusEl / F-activate)
  ${focusDesc.line}
  ${focusDesc.detail || '—'}

Paint target (outline box)
  ${paintDesc.line}
  ${paintDesc.detail || '—'}
  (${paintSame})

Strategy
  focus in shadow: ${inShadow ? 'YES' : 'no'}
  auto would use:  ${autoStrategy}
  applied now:     ${applied}${override ? `  (forced ${override})` : '  (auto)'}`;

    this._refreshShadowDebugHudButtons();
  }
}