Web Inspector

页面元素检查与标注工具 - 支持框架组件源码定位、快捷键自定义、剪贴板复制

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Web Inspector
// @namespace    https://github.com/ibucon
// @version      1.0.0
// @description  页面元素检查与标注工具 - 支持框架组件源码定位、快捷键自定义、剪贴板复制
// @author       ibucon
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      127.0.0.1
// @connect      localhost
// @license      MIT
// @run-at       document-end
// ==/UserScript==

;(function() {
  'use strict';

  let menuCommandId = null;

  function isEnabledForSite() {
    const sites = GM_getValue('wi-enabledSites', {});
    return sites[location.host] === true;
  }

  function setEnabledForSite(enabled) {
    const sites = GM_getValue('wi-enabledSites', {});
    if (enabled) {
      sites[location.host] = true;
    } else {
      delete sites[location.host];
    }
    GM_setValue('wi-enabledSites', sites);
  }

  function updateMenuCommand() {
    if (menuCommandId !== null) GM_unregisterMenuCommand(menuCommandId);
    const enabled = isEnabledForSite();
    const label = enabled ? '关闭 Web Inspector' : '开启 Web Inspector';
    menuCommandId = GM_registerMenuCommand(label, () => {
      setEnabledForSite(!enabled);
      updateMenuCommand();
      location.reload();
    });
  }

  updateMenuCommand();
  if (!isEnabledForSite()) return;

  // ===== index.js content below =====
  if (document.getElementById('wi-toolbar')) return;

  // =========================================================================
  // Lucide Icons (inline SVG, 24x24 viewBox)
  // =========================================================================
  function lucide(w, paths) {
    return `<svg width="${w}" height="${w}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
  }
  const icons = {
    // Search Plus (logo)
    logo:     lucide(18, '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/><path d="M11 8v6"/><path d="M8 11h6"/>'),
    // Crosshair (inspect)
    inspect:  lucide(16, '<circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/>'),
    // Play
    play:     lucide(16, '<polygon points="6 3 20 12 6 21 6 3"/>'),
    // Pause
    pause:    lucide(16, '<rect x="14" y="4" width="4" height="16" rx="1"/><rect x="6" y="4" width="4" height="16" rx="1"/>'),
    // Pencil (for marker hover)
    pencil:   lucide(12, '<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/>'),
    // Plus (for new annotation marker)
    plus:     lucide(12, '<path d="M5 12h14"/><path d="M12 5v14"/>'),
    // Eye (show markers)
    eye:      lucide(16, '<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/>'),
    // Eye Off (hide markers)
    eyeOff:   lucide(16, '<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/>'),
    // Send
    copy:     lucide(16, '<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>'),
    // Trash
    trash:    lucide(16, '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/>'),
    // Trash small (for popup delete button)
    trashSm:  lucide(14, '<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>'),
    // Settings
    settings: lucide(16, '<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/>'),
    // X (close)
    close:    lucide(16, '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'),
    // Sun (light mode)
    sun:      lucide(16, '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>'),
    // Moon (dark mode)
    moon:     lucide(16, '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>'),
    // Check small (for checkbox)
    checkSm:  lucide(14, '<polyline points="20 6 9 17 4 12"/>'),
    // Chevron right (for nav link)
    chevronR: lucide(16, '<path d="M7.5 12.5L12 8L7.5 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'),
    // Chevron left (for back)
    chevronL: lucide(16, '<path d="M15 19l-7-7 7-7"/>'),
    // Terminal (console) — kept for future
    console:  lucide(16, '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>'),
    // Globe (network) — kept for future
    network:  lucide(16, '<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>'),
    // Database (storage) — kept for future
    storage:  lucide(16, '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14a9 3 0 0 0 18 0V5"/><path d="M3 12a9 3 0 0 0 18 0"/>'),
    // Keyboard (shortcuts)
    keyboard: lucide(16, '<rect width="20" height="16" x="2" y="4" rx="2" ry="2"/><path d="M6 8h.001"/><path d="M10 8h.001"/><path d="M14 8h.001"/><path d="M18 8h.001"/><path d="M8 12h.001"/><path d="M12 12h.001"/><path d="M16 12h.001"/><path d="M7 16h10"/>'),
  };

  // =========================================================================
  // Settings
  // =========================================================================
  const SETTINGS_KEY = 'wi-settings';
  const COLOR_OPTIONS = [
    { value: '#AF52DE', label: 'Purple' },
    { value: '#3c82f7', label: 'Blue' },
    { value: '#5AC8FA', label: 'Cyan' },
    { value: '#34C759', label: 'Green' },
    { value: '#FFD60A', label: 'Yellow' },
    { value: '#FF9500', label: 'Orange' },
    { value: '#FF3B30', label: 'Red' },
  ];
  const DEFAULT_SETTINGS = {
    annotationColor: '#3c82f7',
    autoClearAfterSend: false,
    blockInteractions: true,
    darkMode: true,
    webhookUrl: 'http://127.0.0.1:18765/inspect',
    webhooksEnabled: false,
    shortcuts: {
      inspect: { altKey: true, shiftKey: true, ctrlKey: false, metaKey: false, code: 'KeyI' },
      copy:    { altKey: true, shiftKey: true, ctrlKey: false, metaKey: false, code: 'KeyC' },
    },
  };

  const SHORTCUT_LABELS = { inspect: '切换检查', copy: '复制标注' };

  function loadSettings() {
    try {
      const stored = localStorage.getItem(SETTINGS_KEY);
      if (stored) {
        const parsed = JSON.parse(stored);
        if (!parsed.webhookUrl) delete parsed.webhookUrl;
        return { ...DEFAULT_SETTINGS, ...parsed };
      }
    } catch {}
    return { ...DEFAULT_SETTINGS };
  }
  function saveSettings(s) {
    try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(s)); } catch {}
  }

  let settings = loadSettings();

  // =========================================================================
  // Shortcut Utilities
  // =========================================================================
  function matchShortcut(e, shortcut) {
    return e.altKey === shortcut.altKey && e.shiftKey === shortcut.shiftKey &&
      e.ctrlKey === shortcut.ctrlKey && e.metaKey === shortcut.metaKey && e.code === shortcut.code;
  }

  function formatShortcut(shortcut) {
    const parts = [];
    if (shortcut.ctrlKey) parts.push('Ctrl');
    if (shortcut.altKey) parts.push('Alt');
    if (shortcut.shiftKey) parts.push('Shift');
    if (shortcut.metaKey) parts.push('⌘');
    const keyMap = { Backquote:'`', Minus:'-', Equal:'=', BracketLeft:'[', BracketRight:']', Backslash:'\\', Semicolon:';', Quote:"'", Comma:',', Period:'.', Slash:'/' };
    const code = shortcut.code;
    if (code.startsWith('Key')) parts.push(code.slice(3));
    else if (code.startsWith('Digit')) parts.push(code.slice(5));
    else parts.push(keyMap[code] || code.replace('Arrow', ''));
    return parts.join('+');
  }

  // =========================================================================
  // Styles
  // =========================================================================
  const style = document.createElement('style');
  style.textContent = `
    @keyframes wi-toolbar-enter {
      from { opacity: 0; transform: scale(0.5) rotate(90deg); }
      to   { opacity: 1; transform: scale(1) rotate(0deg); }
    }
    @keyframes wi-controls-in {
      from { opacity: 0; filter: blur(10px); transform: scale(0.4); }
      to   { opacity: 1; filter: blur(0);    transform: scale(1); }
    }
    @keyframes wi-controls-out {
      from { opacity: 1; filter: blur(0);    transform: scale(1); }
      to   { opacity: 0; filter: blur(10px); transform: scale(0.4); }
    }
    @keyframes wi-highlight-in {
      from { opacity: 0; transform: scale(0.98); }
      to   { opacity: 1; transform: scale(1); }
    }
    @keyframes wi-tooltip-in {
      from { opacity: 0; transform: scale(0.95) translateY(4px); }
      to   { opacity: 1; transform: scale(1) translateY(0); }
    }
    @keyframes wi-info-in {
      from { opacity: 0; transform: translateY(10px) scale(0.95); filter: blur(5px); }
      to   { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
    }
    @keyframes wi-info-out {
      from { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
      to   { opacity: 0; transform: translateY(10px) scale(0.95); filter: blur(5px); }
    }

    #wi-toolbar {
      position: fixed;
      bottom: 20px;
      right: 20px;
      z-index: 2147483647;
      font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      pointer-events: none;
      transition: left 0s, top 0s, right 0s, bottom 0s;
    }

    #wi-toolbar-container {
      user-select: none;
      display: flex;
      align-items: center;
      justify-content: center;
      background: #1a1a1a;
      color: #fff;
      border: none;
      box-shadow: 0 2px 8px rgba(0,0,0,0.2), 0 4px 16px rgba(0,0,0,0.1);
      pointer-events: auto;
      cursor: grab;
      transition: width 0.4s cubic-bezier(0.19,1,0.22,1),
                  height 0.4s cubic-bezier(0.19,1,0.22,1),
                  border-radius 0.4s cubic-bezier(0.19,1,0.22,1),
                  transform 0.4s cubic-bezier(0.19,1,0.22,1);
      animation: wi-toolbar-enter 0.5s cubic-bezier(0.34,1.2,0.64,1) forwards;
    }

    #wi-toolbar-container.wi-dragging {
      cursor: grabbing;
      transition: width 0.4s cubic-bezier(0.19,1,0.22,1);
    }

    /* Collapsed state */
    #wi-toolbar-container.wi-collapsed {
      width: 44px;
      height: 44px;
      border-radius: 22px;
      padding: 0;
      cursor: pointer;
    }
    #wi-toolbar-container.wi-collapsed:hover {
      background: #2a2a2a;
    }
    #wi-toolbar-container.wi-collapsed:active {
      transform: scale(0.95);
    }

    /* Expanded state */
    #wi-toolbar-container.wi-expanded {
      height: 44px;
      border-radius: 1.5rem;
      padding: 0 6px;
    }

    /* Toggle icon (logo in collapsed) */
    #wi-toggle-icon {
      position: absolute;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: opacity 0.15s ease;
    }
    #wi-toggle-icon.wi-visible { opacity: 1; pointer-events: auto; }
    #wi-toggle-icon.wi-hidden  { opacity: 0; pointer-events: none; }

    /* Controls row */
    #wi-controls {
      display: flex;
      align-items: center;
      gap: 4px;
      transform-origin: right center;
    }
    #wi-controls.wi-visible {
      animation: wi-controls-in 0.35s cubic-bezier(0.19,1,0.22,1) forwards;
      pointer-events: auto;
    }
    #wi-controls.wi-hidden {
      animation: wi-controls-out 0.2s ease forwards;
      pointer-events: none;
    }

    /* Control buttons */
    .wi-btn {
      position: relative;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      width: 34px;
      height: 34px;
      border-radius: 50%;
      border: none;
      background: transparent;
      color: rgba(255,255,255,0.85);
      transition: background-color 0.15s ease, color 0.15s ease, transform 0.1s ease;
      padding: 0;
    }
    .wi-btn:hover {
      background: rgba(255,255,255,0.12);
      color: #fff;
    }
    .wi-btn:active {
      transform: scale(0.92);
    }
    .wi-btn[data-active="true"] {
      color: #3c82f7;
      background: rgba(60,130,247,0.25);
    }
    .wi-btn[data-active="paused"] {
      color: #f59e0b;
      background: rgba(245,158,11,0.2);
    }
    .wi-btn[data-danger]:hover {
      background: rgba(255,59,48,0.25);
      color: #ff3b30;
    }
    .wi-btn:disabled {
      opacity: 0.3;
      cursor: not-allowed;
      pointer-events: none;
    }

    /* Divider */
    .wi-divider {
      width: 1px;
      height: 12px;
      background: rgba(255,255,255,0.15);
      margin: 0 2px;
      flex-shrink: 0;
    }

    /* Button tooltip */
    .wi-btn-wrap {
      position: relative;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .wi-tooltip {
      position: absolute;
      bottom: calc(100% + 14px);
      left: 50%;
      transform: translateX(-50%) scale(0.95);
      padding: 6px 10px;
      background: #1a1a1a;
      color: rgba(255,255,255,0.9);
      font-size: 12px;
      font-weight: 500;
      border-radius: 8px;
      white-space: nowrap;
      opacity: 0;
      visibility: hidden;
      pointer-events: none;
      z-index: 2147483647;
      box-shadow: 0 2px 8px rgba(0,0,0,0.3);
      transition: opacity 0.135s ease, transform 0.135s ease, visibility 0.135s ease;
    }
    .wi-tooltip::after {
      content: "";
      position: absolute;
      top: calc(100% - 4px);
      left: 50%;
      transform: translateX(-50%) rotate(45deg);
      width: 8px;
      height: 8px;
      background: #1a1a1a;
      border-radius: 0 0 2px 0;
    }
    .wi-btn-wrap:hover .wi-tooltip {
      opacity: 1;
      visibility: visible;
      transform: translateX(-50%) scale(1);
      transition-delay: 0.6s;
    }

    /* Tooltip below (when toolbar near top) */
    #wi-toolbar.wi-tooltip-below .wi-tooltip {
      bottom: auto;
      top: calc(100% + 14px);
      transform: translateX(-50%) scale(0.95);
    }
    #wi-toolbar.wi-tooltip-below .wi-tooltip::after {
      top: -4px;
      bottom: auto;
      border-radius: 2px 0 0 0;
    }
    #wi-toolbar.wi-tooltip-below .wi-btn-wrap:hover .wi-tooltip {
      transform: translateX(-50%) scale(1);
    }

    /* ================================================================ */
    /* Inspect Mode Overlay                                             */
    /* ================================================================ */
    #wi-overlay {
      position: fixed;
      inset: 0;
      z-index: 2147483640;
      pointer-events: none;
    }
    #wi-overlay > * { pointer-events: auto; }

    /* Hover highlight box */
    #wi-hover-highlight {
      position: fixed;
      border: 2px solid rgba(60,130,247,0.5);
      border-radius: 4px;
      background: rgba(60,130,247,0.04);
      pointer-events: none !important;
      box-sizing: border-box;
      will-change: opacity;
      animation: wi-highlight-in 0.12s ease-out forwards;
    }

    /* Hover element tooltip */
    #wi-hover-tooltip {
      position: fixed;
      font-size: 11px;
      font-weight: 500;
      color: #fff;
      background: rgba(0,0,0,0.85);
      padding: 5px 10px;
      border-radius: 6px;
      pointer-events: none !important;
      white-space: nowrap;
      max-width: 360px;
      overflow: hidden;
      text-overflow: ellipsis;
      z-index: 2147483645;
      animation: wi-tooltip-in 0.1s ease-out forwards;
      font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    }
    #wi-hover-tooltip .wi-hover-path {
      font-size: 10px;
      color: rgba(255,255,255,0.5);
      margin-bottom: 2px;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    #wi-hover-tooltip .wi-hover-name {
      overflow: hidden;
      text-overflow: ellipsis;
    }
    #wi-hover-tooltip .wi-hover-size {
      font-size: 10px;
      color: rgba(255,255,255,0.45);
      margin-top: 2px;
    }

    /* Annotation marker dot */
    .wi-marker {
      position: fixed;
      width: 22px;
      height: 22px;
      background: #3c82f7;
      color: white;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 11px;
      font-weight: 600;
      transform: translate(-50%, -50%) scale(1);
      cursor: pointer;
      box-shadow: 0 2px 6px rgba(0,0,0,0.2), inset 0 0 0 1px rgba(0,0,0,0.04);
      user-select: none;
      z-index: 2147483643;
      animation: wi-marker-in 0.25s cubic-bezier(0.22,1,0.36,1) both;
    }
    .wi-marker:hover { transform: translate(-50%,-50%) scale(1.1); }
    .wi-marker .wi-marker-num { display: flex; align-items: center; justify-content: center; }
    .wi-marker .wi-marker-edit { display: none; align-items: center; justify-content: center; }
    .wi-marker:hover .wi-marker-num { display: none; }
    .wi-marker:hover .wi-marker-edit { display: flex; }
    .wi-marker.wi-marker-exit {
      animation: wi-marker-out 0.2s ease-out both;
      pointer-events: none;
    }
    @keyframes wi-marker-in {
      from { opacity: 0; transform: translate(-50%,-50%) scale(0.3); }
      to   { opacity: 1; transform: translate(-50%,-50%) scale(1); }
    }
    @keyframes wi-marker-out {
      from { opacity: 1; transform: translate(-50%,-50%) scale(1); }
      to   { opacity: 0; transform: translate(-50%,-50%) scale(0.3); }
    }

    /* Annotation popup (matches agentation) */
    @keyframes wi-popup-enter {
      from { opacity: 0; transform: translateX(-50%) scale(0.95) translateY(4px); }
      to   { opacity: 1; transform: translateX(-50%) scale(1) translateY(0); }
    }
    @keyframes wi-popup-exit {
      from { opacity: 1; transform: translateX(-50%) scale(1) translateY(0); }
      to   { opacity: 0; transform: translateX(-50%) scale(0.95) translateY(4px); }
    }
    @keyframes wi-popup-shake {
      0%,100% { transform: translateX(-50%) scale(1) translateY(0) translateX(0); }
      20%     { transform: translateX(-50%) scale(1) translateY(0) translateX(-3px); }
      40%     { transform: translateX(-50%) scale(1) translateY(0) translateX(3px); }
      60%     { transform: translateX(-50%) scale(1) translateY(0) translateX(-2px); }
      80%     { transform: translateX(-50%) scale(1) translateY(0) translateX(2px); }
    }
    .wi-popup {
      position: fixed;
      transform: translateX(-50%);
      width: 280px;
      padding: 12px 16px 14px;
      background: #1a1a1a;
      border-radius: 16px;
      box-shadow: 0 4px 24px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08);
      cursor: default;
      z-index: 2147483645;
      font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
      will-change: transform, opacity;
      opacity: 0;
    }
    .wi-popup.wi-popup-enter {
      animation: wi-popup-enter 0.2s cubic-bezier(0.34,1.56,0.64,1) forwards;
    }
    .wi-popup.wi-popup-entered {
      opacity: 1;
      transform: translateX(-50%) scale(1) translateY(0);
    }
    .wi-popup.wi-popup-exit {
      animation: wi-popup-exit 0.15s ease-in forwards;
    }
    .wi-popup.wi-popup-shake {
      animation: wi-popup-shake 0.25s ease-out;
    }

    .wi-popup-header {
      display: flex;
      align-items: center;
      margin-bottom: 9px;
    }
    .wi-popup-header-toggle {
      display: flex;
      align-items: center;
      gap: 4px;
      background: none;
      border: none;
      padding: 0;
      cursor: pointer;
      flex: 1;
      min-width: 0;
      text-align: left;
    }
    .wi-popup-element {
      font-size: 12px;
      font-weight: 400;
      color: rgba(255,255,255,0.5);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      flex: 1;
    }
    .wi-popup-chevron {
      color: rgba(255,255,255,0.5);
      transition: transform 0.25s cubic-bezier(0.16,1,0.3,1);
      flex-shrink: 0;
    }
    .wi-popup-chevron.wi-chevron-expanded { transform: rotate(90deg); }

    /* Computed styles accordion */
    .wi-popup-styles-wrapper {
      display: grid;
      grid-template-rows: 0fr;
      transition: grid-template-rows 0.3s cubic-bezier(0.16,1,0.3,1);
    }
    .wi-popup-styles-wrapper.wi-styles-expanded {
      grid-template-rows: 1fr;
    }
    .wi-popup-styles-inner {
      overflow: hidden;
    }
    .wi-popup-styles-block {
      background: rgba(255,255,255,0.05);
      border-radius: 6px;
      padding: 8px 10px;
      margin-bottom: 8px;
      font-family: ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
      font-size: 11px;
      line-height: 1.5;
    }
    .wi-popup-style-line {
      color: rgba(255,255,255,0.85);
      word-break: break-word;
    }
    .wi-popup-style-prop { color: #c792ea; }
    .wi-popup-style-val  { color: rgba(255,255,255,0.85); }

    .wi-popup-textarea {
      width: 100%;
      padding: 8px 10px;
      font-size: 13px;
      font-family: inherit;
      background: rgba(255,255,255,0.05);
      color: #fff;
      border: 1px solid rgba(255,255,255,0.15);
      border-radius: 8px;
      resize: none;
      outline: none;
      box-sizing: border-box;
      transition: border-color 0.15s ease;
    }
    .wi-popup-textarea:focus { border-color: var(--wi-accent, #3c82f7); }
    .wi-popup-textarea::placeholder { color: rgba(255,255,255,0.35); }

    .wi-popup-actions {
      display: flex;
      justify-content: flex-end;
      gap: 6px;
      margin-top: 8px;
    }
    .wi-popup-delete-wrap { margin-right: auto; }
    .wi-popup-delete {
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      width: 28px;
      height: 28px;
      border-radius: 50%;
      border: none;
      background: transparent;
      color: rgba(255,255,255,0.4);
      transition: background-color 0.15s ease, color 0.15s ease, transform 0.1s ease;
      padding: 0;
    }
    .wi-popup-delete:hover {
      background: rgba(255,59,48,0.25);
      color: #ff3b30;
    }
    .wi-popup-delete:active { transform: scale(0.92); }
    .wi-popup-cancel, .wi-popup-submit {
      padding: 6px 14px;
      font-size: 12px;
      font-weight: 500;
      border-radius: 1rem;
      border: none;
      cursor: pointer;
      transition: background-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
      font-family: inherit;
    }
    .wi-popup-cancel {
      background: transparent;
      color: rgba(255,255,255,0.5);
    }
    .wi-popup-cancel:hover {
      background: rgba(255,255,255,0.1);
      color: rgba(255,255,255,0.8);
    }
    .wi-popup-submit {
      background: #3c82f7;
      color: white;
    }
    .wi-popup-submit:hover:not(:disabled) { filter: brightness(0.9); }
    .wi-popup-submit:disabled { cursor: not-allowed; opacity: 0.4; }

    /* Settings panel */
    @keyframes wi-settings-in {
      from { opacity: 0; transform: translateY(8px) scale(0.95); filter: blur(5px); }
      to   { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
    }
    @keyframes wi-settings-out {
      from { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
      to   { opacity: 0; transform: translateY(8px) scale(0.95); filter: blur(5px); }
    }
    .wi-settings {
      position: absolute;
      right: 5px;
      bottom: calc(100% + 0.5rem);
      z-index: 2147483647;
      overflow: hidden;
      background: #1a1a1a;
      border-radius: 1rem;
      padding: 13px 1rem 16px;
      min-width: 220px;
      cursor: default;
      box-shadow: 0 4px 20px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08);
      font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
      pointer-events: auto;
    }
    .wi-settings.wi-settings-enter {
      animation: wi-settings-in 0.2s ease forwards;
    }
    .wi-settings.wi-settings-exit {
      animation: wi-settings-out 0.1s ease forwards;
      pointer-events: none;
    }

    /* Settings: light mode */
    .wi-settings.wi-light {
      background: #fff;
      box-shadow: 0 2px 8px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.04);
    }

    /* Settings: tooltip below when toolbar near top */
    #wi-toolbar.wi-tooltip-below .wi-settings {
      bottom: auto;
      top: calc(100% + 0.5rem);
    }

    .wi-settings-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      min-height: 24px;
      margin-bottom: 8px;
      padding-bottom: 9px;
      border-bottom: 1px solid rgba(255,255,255,0.07);
    }
    .wi-light .wi-settings-header { border-bottom-color: rgba(0,0,0,0.08); }
    .wi-settings-brand {
      font-size: 13px;
      font-weight: 600;
      letter-spacing: -0.0094em;
      color: #fff;
    }
    .wi-light .wi-settings-brand { color: rgba(0,0,0,0.85); }
    .wi-settings-brand-slash { transition: color 0.2s ease; }
    .wi-settings-version {
      font-size: 11px;
      font-weight: 400;
      color: rgba(255,255,255,0.4);
      margin-left: auto;
    }
    .wi-light .wi-settings-version { color: rgba(0,0,0,0.4); }
    .wi-theme-toggle {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 22px;
      height: 22px;
      margin-left: 8px;
      border: none;
      border-radius: 6px;
      background: transparent;
      color: rgba(255,255,255,0.4);
      cursor: pointer;
      transition: background-color 0.15s ease, color 0.15s ease;
      padding: 0;
    }
    .wi-theme-toggle:hover {
      background: rgba(255,255,255,0.1);
      color: rgba(255,255,255,0.8);
    }
    .wi-light .wi-theme-toggle {
      color: rgba(0,0,0,0.4);
    }
    .wi-light .wi-theme-toggle:hover {
      background: rgba(0,0,0,0.06);
      color: rgba(0,0,0,0.7);
    }

    .wi-settings-section + .wi-settings-section {
      margin-top: 8px;
      padding-top: 8px;
      border-top: 1px solid rgba(255,255,255,0.07);
    }
    .wi-light .wi-settings-section + .wi-settings-section { border-top-color: rgba(0,0,0,0.08); }

    .wi-settings-row {
      display: flex;
      align-items: center;
      justify-content: space-between;
      min-height: 24px;
    }
    .wi-settings-label {
      font-size: 13px;
      font-weight: 400;
      letter-spacing: -0.0094em;
      color: rgba(255,255,255,0.5);
    }
    .wi-light .wi-settings-label { color: rgba(0,0,0,0.5); }
    .wi-settings-label-marker {
      padding-top: 3px;
      margin-bottom: 10px;
    }

    /* Color options */
    .wi-color-options {
      display: flex;
      gap: 8px;
      margin-top: 6px;
      margin-bottom: 1px;
    }
    .wi-color-ring {
      display: flex;
      width: 24px;
      height: 24px;
      border: 2px solid transparent;
      border-radius: 50%;
      transition: border-color 0.3s ease;
      cursor: pointer;
    }
    .wi-color-dot {
      display: block;
      width: 20px;
      height: 20px;
      border-radius: 50%;
      transition: transform 0.2s cubic-bezier(0.25,1,0.5,1);
    }
    .wi-color-ring:hover .wi-color-dot { transform: scale(1.15); }
    .wi-color-ring.wi-selected .wi-color-dot { transform: scale(0.83); }

    /* Custom checkbox toggle */
    .wi-settings-toggle {
      display: flex;
      align-items: center;
      gap: 8px;
      cursor: pointer;
    }
    .wi-settings-toggle + .wi-settings-toggle { margin-top: 14px; }
    .wi-settings-toggle input { position: absolute; opacity: 0; width: 0; height: 0; }
    .wi-checkbox {
      position: relative;
      width: 14px;
      height: 14px;
      border: 1px solid rgba(255,255,255,0.2);
      border-radius: 4px;
      background: rgba(255,255,255,0.05);
      display: flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
      transition: background 0.25s ease, border-color 0.25s ease;
    }
    .wi-checkbox svg { color: #1a1a1a; }
    .wi-checkbox.wi-checked {
      border-color: rgba(255,255,255,0.3);
      background: rgba(255,255,255,1);
    }
    .wi-light .wi-checkbox {
      border-color: rgba(0,0,0,0.15);
      background: #fff;
    }
    .wi-light .wi-checkbox.wi-checked {
      border-color: #1a1a1a;
      background: #1a1a1a;
    }
    .wi-light .wi-checkbox.wi-checked svg { color: #fff; }
    .wi-toggle-label {
      font-size: 13px;
      font-weight: 400;
      color: rgba(255,255,255,0.5);
      letter-spacing: -0.0094em;
    }
    .wi-light .wi-toggle-label { color: rgba(0,0,0,0.5); }

    /* Nav link (to webhook page) */
    .wi-settings-nav {
      display: flex;
      align-items: center;
      justify-content: space-between;
      width: 100%;
      padding: 0;
      border: none;
      background: transparent;
      font-family: inherit;
      font-size: 13px;
      font-weight: 400;
      color: rgba(255,255,255,0.5);
      cursor: pointer;
      transition: color 0.15s ease;
    }
    .wi-settings-nav:hover { color: rgba(255,255,255,0.9); }
    .wi-settings-nav svg { color: rgba(255,255,255,0.4); transition: color 0.15s ease; }
    .wi-settings-nav:hover svg { color: #fff; }
    .wi-light .wi-settings-nav { color: rgba(0,0,0,0.5); }
    .wi-light .wi-settings-nav:hover { color: rgba(0,0,0,0.8); }
    .wi-light .wi-settings-nav svg { color: rgba(0,0,0,0.25); }
    .wi-light .wi-settings-nav:hover svg { color: rgba(0,0,0,0.8); }

    /* Settings page sliding */
    .wi-settings-pages {
      overflow: visible;
      position: relative;
      display: flex;
    }
    .wi-settings-pages.wi-transitioning { overflow-x: clip; overflow-y: visible; }
    .wi-settings-page {
      min-width: 100%;
      flex-shrink: 0;
      transition: transform 0.35s cubic-bezier(0.32,0.72,0,1), opacity 0.2s ease-out;
      opacity: 1;
    }
    .wi-settings-page.wi-slide-left {
      transform: translateX(-100%);
      opacity: 0;
    }
    .wi-settings-page-webhooks {
      position: absolute;
      top: 0;
      left: 100%;
      width: 100%;
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
      transition: transform 0.35s cubic-bezier(0.32,0.72,0,1), opacity 0.25s ease-out 0.1s;
      opacity: 0;
    }
    .wi-settings-page-webhooks.wi-slide-in {
      transform: translateX(-100%);
      opacity: 1;
    }
    .wi-settings-page-shortcuts {
      position: absolute;
      top: 0;
      left: 100%;
      width: 100%;
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
      transition: transform 0.35s cubic-bezier(0.32,0.72,0,1), opacity 0.25s ease-out 0.1s;
      opacity: 0;
    }
    .wi-settings-page-shortcuts.wi-slide-in {
      transform: translateX(-100%);
      opacity: 1;
    }

    /* Webhook page */
    .wi-settings-back {
      display: flex;
      align-items: center;
      gap: 4px;
      padding: 0;
      margin-bottom: 10px;
      border: none;
      background: transparent;
      font-family: inherit;
      font-size: 13px;
      font-weight: 400;
      color: rgba(255,255,255,0.5);
      cursor: pointer;
      transition: color 0.15s ease;
    }
    .wi-settings-back:hover { color: rgba(255,255,255,0.9); }
    .wi-light .wi-settings-back { color: rgba(0,0,0,0.5); }
    .wi-light .wi-settings-back:hover { color: rgba(0,0,0,0.8); }

    /* iOS-style toggle switch */
    .wi-toggle-switch {
      position: relative;
      display: inline-block;
      width: 24px;
      height: 16px;
      flex-shrink: 0;
      cursor: pointer;
    }
    .wi-toggle-switch input { opacity: 0; width: 0; height: 0; }
    .wi-toggle-slider {
      position: absolute;
      cursor: pointer;
      inset: 0;
      border-radius: 16px;
      background: #484848;
      transition: background 0.2s ease;
    }
    .wi-light .wi-toggle-slider { background: #ddd; }
    .wi-toggle-slider::before {
      content: "";
      position: absolute;
      height: 12px;
      width: 12px;
      left: 2px;
      bottom: 2px;
      background: white;
      border-radius: 50%;
      transition: transform 0.2s cubic-bezier(0.4,0,0.2,1);
      box-shadow: 0 1px 2px rgba(0,0,0,0.2);
    }
    .wi-toggle-switch input:checked + .wi-toggle-slider { background: var(--wi-accent, #3c82f7); }
    .wi-toggle-switch input:checked + .wi-toggle-slider::before { transform: translateX(8px); }
    .wi-toggle-switch.wi-disabled { opacity: 0.4; pointer-events: none; }
    .wi-auto-send-row { display: flex; align-items: center; gap: 8px; }
    .wi-auto-send-label {
      font-size: 13px;
      font-weight: 400;
      color: rgba(255,255,255,0.5);
      transition: color 0.15s ease;
    }
    .wi-auto-send-label.wi-active { color: rgba(255,255,255,0.85); }
    .wi-light .wi-auto-send-label { color: rgba(0,0,0,0.4); }
    .wi-light .wi-auto-send-label.wi-active { color: rgba(0,0,0,0.7); }

    .wi-webhook-desc {
      font-size: 12px;
      color: rgba(255,255,255,0.4);
      margin-top: 4px;
      margin-bottom: 8px;
      line-height: 1.4;
    }
    .wi-light .wi-webhook-desc { color: rgba(0,0,0,0.4); }
    .wi-webhook-input {
      width: 100%;
      padding: 6px 8px;
      font-size: 12px;
      font-family: ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
      background: rgba(255,255,255,0.05);
      color: #fff;
      border: 1px solid rgba(255,255,255,0.12);
      border-radius: 6px;
      resize: none;
      outline: none;
      box-sizing: border-box;
      transition: border-color 0.15s ease;
    }
    .wi-webhook-input:focus { border-color: var(--wi-accent, #3c82f7); }
    .wi-webhook-input::placeholder { color: rgba(255,255,255,0.3); }
    .wi-light .wi-webhook-input {
      background: rgba(0,0,0,0.03);
      color: #1a1a1a;
      border-color: rgba(0,0,0,0.12);
    }
    .wi-light .wi-webhook-input::placeholder { color: rgba(0,0,0,0.35); }

    /* Selected element outline (persistent while popup open) */
    .wi-selected-outline {
      position: fixed;
      border: 2px solid rgba(60,130,247,0.6);
      border-radius: 4px;
      background: rgba(60,130,247,0.05);
      pointer-events: none !important;
      box-sizing: border-box;
      z-index: 2147483641;
    }
    .wi-selected-outline.wi-outline-enter { animation: wi-highlight-in 0.15s ease-out forwards; }
    .wi-selected-outline.wi-outline-exit  { animation: wi-highlight-out 0.15s ease-out forwards; }
    @keyframes wi-highlight-out {
      from { opacity: 1; }
      to   { opacity: 0; }
    }

    /* ================================================================ */
    /* Light Mode (toolbar, popup, markers)                             */
    /* ================================================================ */
    #wi-toolbar-container.wi-light-toolbar {
      background: #fff;
      color: rgba(0,0,0,0.85);
      box-shadow: 0 2px 8px rgba(0,0,0,0.08), 0 4px 16px rgba(0,0,0,0.06);
    }
    #wi-toolbar-container.wi-light-toolbar.wi-collapsed:hover { background: #f5f5f5; }
    .wi-light-toolbar .wi-btn {
      color: rgba(0,0,0,0.65);
    }
    .wi-light-toolbar .wi-btn:hover {
      background: rgba(0,0,0,0.06);
      color: rgba(0,0,0,0.85);
    }
    .wi-light-toolbar .wi-btn[data-active="true"] {
      color: #3c82f7;
      background: rgba(60,130,247,0.12);
    }
    .wi-light-toolbar .wi-btn[data-active="paused"] {
      color: #f59e0b;
      background: rgba(245,158,11,0.12);
    }
    .wi-light-toolbar .wi-btn[data-danger]:hover {
      background: rgba(255,59,48,0.1);
      color: #ff3b30;
    }
    .wi-light-toolbar .wi-divider { background: rgba(0,0,0,0.1); }
    .wi-light-toolbar .wi-tooltip {
      background: #fff;
      color: rgba(0,0,0,0.75);
      box-shadow: 0 2px 8px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.06);
    }
    .wi-light-toolbar .wi-tooltip::after { background: #fff; }

    /* Light popup */
    .wi-popup.wi-popup-light {
      background: #fff;
      box-shadow: 0 4px 24px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.06);
    }
    .wi-popup-light .wi-popup-element { color: rgba(0,0,0,0.6); }
    .wi-popup-light .wi-popup-chevron { color: rgba(0,0,0,0.4); }
    .wi-popup-light .wi-popup-styles-block { background: rgba(0,0,0,0.03); }
    .wi-popup-light .wi-popup-style-line { color: rgba(0,0,0,0.75); }
    .wi-popup-light .wi-popup-style-prop { color: #7c3aed; }
    .wi-popup-light .wi-popup-style-val { color: rgba(0,0,0,0.75); }
    .wi-popup-light .wi-popup-textarea {
      background: rgba(0,0,0,0.03);
      color: #1a1a1a;
      border-color: rgba(0,0,0,0.12);
    }
    .wi-popup-light .wi-popup-textarea::placeholder { color: rgba(0,0,0,0.4); }
    .wi-popup-light .wi-popup-cancel { color: rgba(0,0,0,0.5); }
    .wi-popup-light .wi-popup-cancel:hover { background: rgba(0,0,0,0.06); color: rgba(0,0,0,0.75); }
    .wi-popup-light .wi-popup-delete { color: rgba(0,0,0,0.4); }
    .wi-popup-light .wi-popup-delete:hover { background: rgba(255,59,48,0.15); color: #ff3b30; }

    /* Shortcut recording */
    .wi-shortcut-tabs {
      display: flex; gap: 0; margin-bottom: 12px;
      border: 1px solid rgba(255,255,255,0.12); border-radius: 8px; overflow: hidden;
    }
    .wi-light .wi-shortcut-tabs { border-color: rgba(0,0,0,0.1); }
    .wi-shortcut-tab {
      flex: 1; padding: 6px 8px; font-size: 12px; font-weight: 500;
      border: none; cursor: pointer; transition: all 0.2s;
      background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.5);
      font-family: inherit;
    }
    .wi-light .wi-shortcut-tab { background: #f5f5f5; color: rgba(0,0,0,0.5); }
    .wi-shortcut-tab + .wi-shortcut-tab { border-left: 1px solid rgba(255,255,255,0.12); }
    .wi-light .wi-shortcut-tab + .wi-shortcut-tab { border-left-color: rgba(0,0,0,0.1); }
    .wi-shortcut-tab.wi-tab-active { background: var(--wi-accent, #3c82f7); color: white; }
    .wi-shortcut-desc {
      font-size: 12px; color: rgba(255,255,255,0.4); margin-bottom: 8px;
    }
    .wi-light .wi-shortcut-desc { color: rgba(0,0,0,0.4); }
    .wi-shortcut-kbd {
      display: block; text-align: center; padding: 10px 16px;
      background: rgba(255,255,255,0.05); border: 1.5px dashed rgba(255,255,255,0.2);
      border-radius: 8px; font-size: 14px; font-weight: 600;
      color: var(--wi-accent, #3c82f7);
      font-family: ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
      transition: all 0.2s; margin-bottom: 10px;
    }
    .wi-light .wi-shortcut-kbd { background: rgba(0,0,0,0.03); border-color: rgba(0,0,0,0.15); }
    .wi-shortcut-kbd.wi-recording { border-color: var(--wi-accent, #3c82f7); background: rgba(79,70,229,0.05); }
    .wi-shortcut-actions {
      display: flex; gap: 6px; justify-content: flex-end;
    }
    .wi-shortcut-actions button {
      padding: 6px 14px; border-radius: 1rem; border: none;
      font-size: 12px; font-weight: 500; cursor: pointer;
      transition: all 0.15s; font-family: inherit;
    }
    .wi-shortcut-actions .wi-sc-cancel { background: transparent; color: rgba(255,255,255,0.5); }
    .wi-shortcut-actions .wi-sc-cancel:hover { background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.8); }
    .wi-light .wi-shortcut-actions .wi-sc-cancel { color: rgba(0,0,0,0.5); }
    .wi-light .wi-shortcut-actions .wi-sc-cancel:hover { background: rgba(0,0,0,0.06); color: rgba(0,0,0,0.75); }
    .wi-shortcut-actions .wi-sc-save { background: var(--wi-accent, #3c82f7); color: white; }
    .wi-shortcut-actions .wi-sc-save:disabled { opacity: 0.4; cursor: not-allowed; }

    /* Toast */
    .wi-toast {
      position: fixed; top: 24px; left: 50%;
      transform: translateX(-50%) translateY(-20px);
      background: rgba(31,41,55,0.95); backdrop-filter: blur(8px);
      color: white; padding: 10px 18px; border-radius: 10px;
      font-size: 13px; font-weight: 500; z-index: 2147483647;
      opacity: 0; pointer-events: none;
      box-shadow: 0 8px 24px rgba(0,0,0,0.2);
      transition: all 0.3s cubic-bezier(0.175,0.885,0.32,1.275);
      font-family: system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    }
    .wi-toast.wi-toast-show { opacity: 1; transform: translateX(-50%) translateY(0); }
  `;
  document.head.appendChild(style);

  // =========================================================================
  // DOM Structure
  // =========================================================================
  const toolbar = document.createElement('div');
  toolbar.id = 'wi-toolbar';

  const container = document.createElement('div');
  container.id = 'wi-toolbar-container';
  container.className = 'wi-collapsed';

  // Toggle icon (visible when collapsed)
  const toggleIcon = document.createElement('div');
  toggleIcon.id = 'wi-toggle-icon';
  toggleIcon.className = 'wi-visible';
  toggleIcon.innerHTML = icons.logo;

  // Controls (visible when expanded)
  const controls = document.createElement('div');
  controls.id = 'wi-controls';
  controls.className = 'wi-hidden';

  const buttons = [
    { id: 'inspect',  icon: icons.inspect,  tip: '元素检查' },
    { id: 'eye',      icon: icons.eye,      tip: '隐藏标注', disabled: true },
    { id: 'copy',     icon: icons.copy,     tip: '复制', disabled: true },
    { id: 'trash',    icon: icons.trash,    tip: '删除全部', danger: true, disabled: true },
    { divider: true },
    { id: 'settings', icon: icons.settings, tip: '设置' },
    { id: 'close',    icon: icons.close,    tip: '关闭', danger: true },
  ];

  buttons.forEach(btn => {
    if (btn.divider) {
      const d = document.createElement('div');
      d.className = 'wi-divider';
      controls.appendChild(d);
      return;
    }
    const wrap = document.createElement('div');
    wrap.className = 'wi-btn-wrap';

    const el = document.createElement('button');
    el.className = 'wi-btn';
    el.id = `wi-btn-${btn.id}`;
    el.innerHTML = btn.icon;
    if (btn.danger) el.setAttribute('data-danger', '');
    if (btn.disabled) el.disabled = true;

    const tip = document.createElement('div');
    tip.className = 'wi-tooltip';
    tip.textContent = btn.tip;

    wrap.appendChild(el);
    wrap.appendChild(tip);
    controls.appendChild(wrap);
  });

  container.appendChild(toggleIcon);
  container.appendChild(controls);
  toolbar.appendChild(container);
  document.body.appendChild(toolbar);

  // =========================================================================
  // Expand / Collapse
  // =========================================================================
  let expanded = false;

  function expand() {
    expanded = true;
    container.classList.remove('wi-collapsed');
    container.classList.add('wi-expanded');
    toggleIcon.classList.remove('wi-visible');
    toggleIcon.classList.add('wi-hidden');
    controls.classList.remove('wi-hidden');
    controls.classList.add('wi-visible');
  }

  function collapse() {
    expanded = false;
    container.classList.remove('wi-expanded');
    container.classList.add('wi-collapsed');
    toggleIcon.classList.remove('wi-hidden');
    toggleIcon.classList.add('wi-visible');
    controls.classList.remove('wi-visible');
    controls.classList.add('wi-hidden');
  }

  // Click collapsed → expand
  container.addEventListener('click', (e) => {
    if (!expanded && !isDragging) expand();
  });

  // Close button → collapse
  document.getElementById('wi-btn-close').addEventListener('click', (e) => {
    e.stopPropagation();
    collapse();
  });

  // =========================================================================
  // Drag
  // =========================================================================
  let isDragging = false;
  let dragStarted = false;
  let startX, startY, origX, origY;

  container.addEventListener('mousedown', (e) => {
    if (e.button !== 0) return;
    isDragging = false;
    dragStarted = true;
    startX = e.clientX;
    startY = e.clientY;
    const rect = toolbar.getBoundingClientRect();
    origX = rect.left;
    origY = rect.top;
    container.classList.add('wi-dragging');
    e.preventDefault();
  });

  document.addEventListener('mousemove', (e) => {
    if (!dragStarted) return;
    const dx = e.clientX - startX;
    const dy = e.clientY - startY;
    if (!isDragging && (Math.abs(dx) > 3 || Math.abs(dy) > 3)) {
      isDragging = true;
    }
    if (isDragging) {
      let nx = origX + dx;
      let ny = origY + dy;
      const rect = container.getBoundingClientRect();
      nx = Math.max(0, Math.min(window.innerWidth - rect.width, nx));
      ny = Math.max(0, Math.min(window.innerHeight - rect.height, ny));
      toolbar.style.right = (window.innerWidth - nx - rect.width) + 'px';
      toolbar.style.top = ny + 'px';
      toolbar.style.left = 'auto';
      toolbar.style.bottom = 'auto';

      // Tooltip direction
      if (ny < 80) {
        toolbar.classList.add('wi-tooltip-below');
      } else {
        toolbar.classList.remove('wi-tooltip-below');
      }
    }
  });

  document.addEventListener('mouseup', () => {
    if (dragStarted) {
      container.classList.remove('wi-dragging');
      dragStarted = false;
      // Prevent click from firing when we were dragging
      if (isDragging) {
        setTimeout(() => { isDragging = false; }, 0);
      }
    }
  });

  // =========================================================================
  // Element Identification (ported from agentation)
  // =========================================================================
  function deepElementFromPoint(x, y) {
    let el = document.elementFromPoint(x, y);
    if (!el) return null;
    while (el?.shadowRoot) {
      const deeper = el.shadowRoot.elementFromPoint(x, y);
      if (!deeper || deeper === el) break;
      el = deeper;
    }
    return el;
  }

  function isOwnElement(el) {
    let cur = el;
    while (cur) {
      if (cur.id === 'wi-toolbar' || cur.id === 'wi-overlay' ||
          cur.id === 'wi-hover-highlight' || cur.id === 'wi-hover-tooltip' ||
          cur.hasAttribute?.('data-wi-popup') || cur.hasAttribute?.('data-wi-marker') ||
          cur.classList?.contains('wi-selected-outline') ||
          cur.classList?.contains('wi-settings')) return true;
      cur = cur.parentElement;
    }
    return false;
  }

  function getElementPath(target) {
    const parts = [];
    let cur = target;
    while (cur && cur !== document.body && cur !== document.documentElement) {
      const tag = cur.tagName.toLowerCase();
      let selector = tag;
      if (cur.id) {
        selector += `#${cur.id}`;
        parts.unshift(selector);
        break;
      } else if (cur.className && typeof cur.className === 'string') {
        const cls = cur.className.trim().split(/\s+/).filter(c => c && !c.includes(':'));
        if (cls.length) selector += `.${cls[0]}`;
      }
      const parent = cur.parentElement;
      if (parent) {
        const siblings = Array.from(parent.children).filter(c => c.tagName === cur.tagName);
        if (siblings.length > 1) selector += `:nth-of-type(${siblings.indexOf(cur) + 1})`;
      }
      parts.unshift(selector);
      cur = parent;
    }
    return parts.join(' > ');
  }

  function identifyElement(target) {
    const path = getElementPath(target);
    if (target.dataset?.element) return { name: target.dataset.element, path };
    const tag = target.tagName.toLowerCase();

    if (['path','circle','rect','line','g'].includes(tag)) {
      const svg = target.closest('svg');
      if (svg?.parentElement) {
        const pn = identifyElement(svg.parentElement).name;
        return { name: `graphic in ${pn}`, path };
      }
      return { name: 'graphic element', path };
    }
    if (tag === 'svg') return { name: 'icon', path };
    if (tag === 'button') {
      const text = target.textContent?.trim();
      const aria = target.getAttribute('aria-label');
      if (aria) return { name: `button [${aria}]`, path };
      return { name: text ? `button "${text.slice(0,25)}"` : 'button', path };
    }
    if (tag === 'a') {
      const text = target.textContent?.trim();
      if (text) return { name: `link "${text.slice(0,25)}"`, path };
      return { name: 'link', path };
    }
    if (tag === 'input') {
      const type = target.getAttribute('type') || 'text';
      const ph = target.getAttribute('placeholder');
      const nm = target.getAttribute('name');
      if (ph) return { name: `input "${ph}"`, path };
      if (nm) return { name: `input [${nm}]`, path };
      return { name: `${type} input`, path };
    }
    if (['h1','h2','h3','h4','h5','h6'].includes(tag)) {
      const text = target.textContent?.trim();
      return { name: text ? `${tag} "${text.slice(0,35)}"` : tag, path };
    }
    if (tag === 'p') {
      const text = target.textContent?.trim();
      if (text) return { name: `paragraph: "${text.slice(0,40)}${text.length > 40 ? '...' : ''}"`, path };
      return { name: 'paragraph', path };
    }
    if (tag === 'span' || tag === 'label') {
      const text = target.textContent?.trim();
      if (text && text.length < 40) return { name: `"${text}"`, path };
      return { name: tag, path };
    }
    if (tag === 'img') {
      const alt = target.getAttribute('alt');
      return { name: alt ? `image "${alt.slice(0,30)}"` : 'image', path };
    }
    if (['div','section','article','nav','header','footer','aside','main'].includes(tag)) {
      const role = target.getAttribute('role');
      const aria = target.getAttribute('aria-label');
      if (aria) return { name: `${tag} [${aria}]`, path };
      if (role) return { name: role, path };
      if (typeof target.className === 'string' && target.className) {
        const words = target.className.split(/[\s_-]+/)
          .map(c => c.replace(/[A-Z0-9]{5,}.*$/, ''))
          .filter(c => c.length > 2 && !/^[a-z]{1,2}$/.test(c))
          .slice(0, 2);
        if (words.length > 0) return { name: words.join(' '), path };
      }
      return { name: tag === 'div' ? 'container' : tag, path };
    }
    return { name: tag, path };
  }

  function getComputedStylesSnapshot(target) {
    const s = window.getComputedStyle(target);
    const result = {};
    const tag = target.tagName.toLowerCase();
    const textTags = new Set(['p','span','h1','h2','h3','h4','h5','h6','label','li','a','code','pre','em','strong','b','i']);
    const containerTags = new Set(['div','section','article','nav','header','footer','aside','main','ul','ol','form']);
    const defaults = new Set(['none','normal','auto','0px','rgba(0, 0, 0, 0)','transparent','static','visible']);

    let props;
    if (textTags.has(tag)) {
      props = ['color','fontSize','fontWeight','fontFamily','lineHeight'];
    } else if (tag === 'button') {
      props = ['backgroundColor','color','padding','borderRadius','fontSize'];
    } else if (['input','textarea','select'].includes(tag)) {
      props = ['backgroundColor','color','padding','borderRadius','fontSize'];
    } else if (['img','video','canvas','svg'].includes(tag)) {
      props = ['width','height','objectFit','borderRadius'];
    } else if (containerTags.has(tag)) {
      props = ['display','padding','margin','gap','backgroundColor'];
    } else {
      props = ['color','fontSize','margin','padding','backgroundColor'];
    }

    for (const prop of props) {
      const cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
      const val = s.getPropertyValue(cssProp);
      if (val && !defaults.has(val)) result[prop] = val;
    }
    return result;
  }

  function getElementClasses(target) {
    if (typeof target.className !== 'string' || !target.className) return '';
    return target.className.split(/\s+/).filter(c => c.length > 0)
      .map(c => { const m = c.match(/^([a-zA-Z][a-zA-Z0-9_-]*?)(?:_[a-zA-Z0-9]{5,})?$/); return m ? m[1] : c; })
      .filter((c, i, a) => a.indexOf(c) === i).join(', ');
  }

  // =========================================================================
  // Framework Component Source Detection
  // =========================================================================
  const MAX_WALK_UP_DEPTH = 15;

  function parseVueInspectorString(str) {
    if (!str || typeof str !== 'string') return null;
    const match = str.match(/^(.+?):(\d+)(?::(\d+))?$/);
    if (match) {
      return { framework: 'vue', file: match[1], line: parseInt(match[2], 10), column: match[3] ? parseInt(match[3], 10) : undefined };
    }
    return { framework: 'vue', file: str };
  }

  function getSourceFromDOM(el) {
    if (!el || typeof el.getAttribute !== 'function') return null;
    const inspectorAttr = el.getAttribute('data-v-inspector');
    if (inspectorAttr) return parseVueInspectorString(inspectorAttr);
    const vnodePaths = [el.__vnode?.props?.__v_inspector, el.__vnode?.ctx?.vnode?.props?.__v_inspector, el.__vnode?.component?.vnode?.props?.__v_inspector];
    for (const data of vnodePaths) { if (data) return parseVueInspectorString(data); }
    const vueFile = el.__vueParentComponent?.type?.__file;
    if (vueFile) return { framework: 'vue', file: vueFile };
    const vue2File = el.__vue__?.$options?.__file;
    if (vue2File) return { framework: 'vue', file: vue2File };
    const fiberKey = Object.keys(el).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternalInstance'));
    if (fiberKey) {
      let fiber = el[fiberKey];
      while (fiber) {
        const source = fiber._debugSource || fiber._debugOwner?._debugSource;
        if (source) return { framework: 'react', file: source.fileName, line: source.lineNumber, column: source.columnNumber };
        fiber = fiber.return;
      }
    }
    return null;
  }

  function findSourceByWalkUp(el) {
    let cur = el;
    for (let i = 0; i < MAX_WALK_UP_DEPTH && cur; i++) {
      const source = getSourceFromDOM(cur);
      if (source) return source;
      cur = cur.parentElement;
    }
    return null;
  }

  // =========================================================================
  // Inspect Mode
  // =========================================================================
  let inspectState = 'off'; // 'off' | 'active' | 'paused'
  let hoverHighlight = null;
  let hoverTooltip = null;
  let overlay = null;

  // Annotation state
  let annotations = [];       // { text, element, path, x, y, styles, markerEl }
  let pendingPopup = null;    // { popup, marker, outline, enterTimer }
  let pendingTarget = null;
  let editingIndex = -1;      // index of annotation being edited, -1 if none

  const chevronSvg = `<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M5.5 10.25L9 7.25L5.75 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;

  function createOverlay() {
    overlay = document.createElement('div');
    overlay.id = 'wi-overlay';
    document.body.appendChild(overlay);
  }

  function removeOverlay() {
    overlay?.remove();
    overlay = null;
  }

  function showHoverHighlight(rect) {
    if (!hoverHighlight) {
      hoverHighlight = document.createElement('div');
      hoverHighlight.id = 'wi-hover-highlight';
      document.body.appendChild(hoverHighlight);
    }
    const c = settings.annotationColor;
    hoverHighlight.style.borderColor = c + '80';
    hoverHighlight.style.backgroundColor = c + '0a';
    hoverHighlight.style.left = rect.left + 'px';
    hoverHighlight.style.top = rect.top + 'px';
    hoverHighlight.style.width = rect.width + 'px';
    hoverHighlight.style.height = rect.height + 'px';
  }

  function hideHoverHighlight() {
    hoverHighlight?.remove();
    hoverHighlight = null;
  }

  function showHoverTooltip(x, y, info, rect) {
    if (!hoverTooltip) {
      hoverTooltip = document.createElement('div');
      hoverTooltip.id = 'wi-hover-tooltip';
      document.body.appendChild(hoverTooltip);
    }
    const w = Math.round(rect.width);
    const h = Math.round(rect.height);
    hoverTooltip.innerHTML =
      `<div class="wi-hover-path">${info.path}</div>` +
      `<div class="wi-hover-name">${info.name}</div>` +
      `<div class="wi-hover-size">${w} × ${h}</div>`;

    const tipX = Math.max(8, Math.min(x, window.innerWidth - 200));
    const tipY = Math.max(y - 52, 8);
    hoverTooltip.style.left = tipX + 'px';
    hoverTooltip.style.top = tipY + 'px';
  }

  function hideHoverTooltip() {
    hoverTooltip?.remove();
    hoverTooltip = null;
  }

  // ---- Selected element outline ----
  function showSelectedOutline(el) {
    hideSelectedOutline();
    const outline = document.createElement('div');
    outline.className = 'wi-selected-outline wi-outline-enter';
    const rect = el.getBoundingClientRect();
    const c = settings.annotationColor;
    outline.style.borderColor = c + '99';
    outline.style.backgroundColor = c + '0d';
    outline.style.left = rect.left + 'px';
    outline.style.top = rect.top + 'px';
    outline.style.width = rect.width + 'px';
    outline.style.height = rect.height + 'px';
    document.body.appendChild(outline);
    return outline;
  }

  function hideSelectedOutline() {
    document.querySelectorAll('.wi-selected-outline').forEach(el => {
      el.classList.remove('wi-outline-enter');
      el.classList.add('wi-outline-exit');
      setTimeout(() => el.remove(), 150);
    });
  }

  // ---- Annotation Popup ----
  function buildMarkerContent(num) {
    return `<span class="wi-marker-num">${num}</span><span class="wi-marker-edit">${icons.pencil}</span>`;
  }

  function createAnnotationPopup(el, clickX, clickY, editIdx) {
    cancelPendingPopup();
    pendingTarget = el;
    editingIndex = editIdx ?? -1;

    const isEdit = editingIndex >= 0;
    const annotation = isEdit ? annotations[editingIndex] : null;
    const info = identifyElement(el);
    const computedStyles = getComputedStylesSnapshot(el);
    const styleEntries = Object.entries(computedStyles);
    const hasStyles = styleEntries.length > 0;

    // Marker position (percentage X, fixed Y)
    const markerXPct = isEdit ? annotation.x : (clickX / window.innerWidth) * 100;
    const markerY = isEdit ? annotation.y : clickY;

    // Create marker dot (only for new annotations)
    let marker;
    if (isEdit) {
      marker = annotation.markerEl;
    } else {
      marker = document.createElement('div');
      marker.className = 'wi-marker';
      marker.setAttribute('data-wi-marker', '');
      marker.innerHTML = icons.plus;
      marker.style.left = markerXPct + '%';
      marker.style.top = markerY + 'px';
      marker.style.backgroundColor = settings.annotationColor;
      document.body.appendChild(marker);
    }

    // Create popup
    const popup = document.createElement('div');
    popup.className = 'wi-popup';
    if (!settings.darkMode) popup.classList.add('wi-popup-light');
    popup.setAttribute('data-wi-popup', '');
    popup.style.setProperty('--wi-accent', settings.annotationColor);
    popup.addEventListener('click', e => e.stopPropagation());

    // Header with chevron toggle (if computed styles exist)
    let headerHTML = '';
    if (hasStyles) {
      headerHTML = `<div class="wi-popup-header">
        <button class="wi-popup-header-toggle" type="button">
          <span class="wi-popup-chevron">${chevronSvg}</span>
          <span class="wi-popup-element">${info.name}</span>
        </button>
      </div>`;
    } else {
      headerHTML = `<div class="wi-popup-header">
        <span class="wi-popup-element">${info.name}</span>
      </div>`;
    }

    // Computed styles block (collapsed by default)
    let stylesHTML = '';
    if (hasStyles) {
      let linesHTML = '';
      for (const [prop, val] of styleEntries) {
        const cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
        linesHTML += `<div class="wi-popup-style-line"><span class="wi-popup-style-prop">${cssProp}</span>: <span class="wi-popup-style-val">${val}</span>;</div>`;
      }
      stylesHTML = `<div class="wi-popup-styles-wrapper">
        <div class="wi-popup-styles-inner">
          <div class="wi-popup-styles-block">${linesHTML}</div>
        </div>
      </div>`;
    }

    const initialValue = isEdit ? annotation.text : '';
    const submitLabel = isEdit ? 'Update' : 'Add';
    const hasInitText = initialValue.trim().length > 0;

    const deleteHTML = isEdit
      ? `<div class="wi-popup-delete-wrap"><button class="wi-popup-delete" type="button">${icons.trashSm}</button></div>`
      : '';

    popup.innerHTML = headerHTML + stylesHTML +
      `<textarea class="wi-popup-textarea" placeholder="What should change?" rows="2">${initialValue}</textarea>
       <div class="wi-popup-actions">
         ${deleteHTML}
         <button class="wi-popup-cancel">Cancel</button>
         <button class="wi-popup-submit" ${hasInitText ? '' : 'disabled'} style="background-color:${settings.annotationColor};opacity:${hasInitText ? '1' : '0.4'}">${submitLabel}</button>
       </div>`;

    // Position popup
    const popupLeft = Math.max(160, Math.min(window.innerWidth - 160, (markerXPct / 100) * window.innerWidth));
    popup.style.left = popupLeft + 'px';
    if (markerY > window.innerHeight - 290) {
      popup.style.bottom = (window.innerHeight - markerY + 20) + 'px';
    } else {
      popup.style.top = (markerY + 20) + 'px';
    }

    document.body.appendChild(popup);

    // Selected outline
    const outline = showSelectedOutline(el);

    // Animate in
    requestAnimationFrame(() => {
      popup.classList.add('wi-popup-enter');
    });
    const enterTimer = setTimeout(() => {
      popup.classList.remove('wi-popup-enter');
      popup.classList.add('wi-popup-entered');
    }, 200);

    // Focus textarea
    const textarea = popup.querySelector('.wi-popup-textarea');
    const submitBtn = popup.querySelector('.wi-popup-submit');
    const cancelBtn = popup.querySelector('.wi-popup-cancel');

    setTimeout(() => {
      textarea.focus();
      textarea.selectionStart = textarea.selectionEnd = textarea.value.length;
    }, 50);

    // Text change → enable/disable submit
    textarea.addEventListener('input', () => {
      const hasText = textarea.value.trim().length > 0;
      submitBtn.disabled = !hasText;
      submitBtn.style.opacity = hasText ? '1' : '0.4';
    });

    // Chevron toggle for computed styles
    if (hasStyles) {
      const toggleBtn = popup.querySelector('.wi-popup-header-toggle');
      const chevron = popup.querySelector('.wi-popup-chevron');
      const wrapper = popup.querySelector('.wi-popup-styles-wrapper');
      toggleBtn.addEventListener('click', () => {
        const isExpanded = wrapper.classList.contains('wi-styles-expanded');
        if (isExpanded) {
          wrapper.classList.remove('wi-styles-expanded');
          chevron.classList.remove('wi-chevron-expanded');
          setTimeout(() => textarea.focus(), 0);
        } else {
          wrapper.classList.add('wi-styles-expanded');
          chevron.classList.add('wi-chevron-expanded');
        }
      });
    }

    // Submit handler
    function handleSubmit() {
      const text = textarea.value.trim();
      if (!text) return;

      if (isEdit) {
        // Update existing annotation
        annotations[editingIndex].text = text;
        annotations[editingIndex].styles = computedStyles;
        console.log(`[WI] Annotation #${editingIndex + 1} updated: "${text}"`);
      } else {
        // Create new annotation
        const num = annotations.length + 1;
        // Convert marker to numbered with hover-pencil
        marker.innerHTML = buildMarkerContent(num);
        bindMarkerEvents(marker, annotations.length);
        annotations.push({
          text,
          element: info.name,
          path: info.path,
          x: markerXPct,
          y: markerY,
          styles: computedStyles,
          markerEl: marker,
          targetEl: el,
        });
        console.log(`[WI] Annotation #${num}: "${text}" on ${info.name}`);
      }

      // Close popup
      popup.classList.remove('wi-popup-entered');
      popup.classList.add('wi-popup-exit');
      hideSelectedOutline();
      setTimeout(() => popup.remove(), 150);
      pendingPopup = null;
      pendingTarget = null;
      editingIndex = -1;
      updateAnnotationButtons();
    }

    // Delete single annotation (edit mode only)
    function handleDelete() {
      const idx = editingIndex;
      const ann = annotations[idx];
      // Close popup
      popup.classList.remove('wi-popup-entered');
      popup.classList.add('wi-popup-exit');
      ann.markerEl.classList.add('wi-marker-exit');
      hideSelectedOutline();
      setTimeout(() => {
        popup.remove();
        ann.markerEl.remove();
      }, 200);
      // Remove from array and renumber remaining markers
      annotations.splice(idx, 1);
      annotations.forEach((a, i) => {
        a.markerEl.querySelector('.wi-marker-num').textContent = i + 1;
      });
      pendingPopup = null;
      pendingTarget = null;
      editingIndex = -1;
      updateAnnotationButtons();
      console.log(`[WI] Annotation #${idx + 1} deleted`);
    }

    submitBtn.addEventListener('click', handleSubmit);
    cancelBtn.addEventListener('click', () => cancelPendingPopup());
    if (isEdit) {
      const deleteBtn = popup.querySelector('.wi-popup-delete');
      if (deleteBtn) deleteBtn.addEventListener('click', handleDelete);
    }

    // Keyboard
    textarea.addEventListener('keydown', (e) => {
      if (e.isComposing) return;
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        handleSubmit();
      }
      if (e.key === 'Escape') {
        cancelPendingPopup();
      }
    });

    pendingPopup = { popup, marker, outline, enterTimer, isEdit };
  }

  function bindMarkerEvents(marker, index) {
    marker.addEventListener('click', (e) => {
      e.stopPropagation();
      e.preventDefault();
      const ann = annotations[index];
      if (!ann) return;
      // Open edit popup
      const el = ann.targetEl && document.contains(ann.targetEl) ? ann.targetEl : null;
      if (el) {
        createAnnotationPopup(el, (ann.x / 100) * window.innerWidth, ann.y, index);
      }
    });
  }

  function cancelPendingPopup() {
    if (!pendingPopup) return;
    const { popup, marker, outline, enterTimer, isEdit } = pendingPopup;
    clearTimeout(enterTimer);
    // Exit animation
    popup.classList.remove('wi-popup-enter', 'wi-popup-entered');
    popup.classList.add('wi-popup-exit');
    if (!isEdit) {
      marker.classList.add('wi-marker-exit');
    }
    hideSelectedOutline();
    setTimeout(() => {
      popup.remove();
      if (!isEdit) marker.remove();
    }, 200);
    pendingPopup = null;
    pendingTarget = null;
    editingIndex = -1;
  }

  function shakePendingPopup() {
    if (!pendingPopup) return;
    const { popup } = pendingPopup;
    popup.classList.add('wi-popup-shake');
    setTimeout(() => {
      popup.classList.remove('wi-popup-shake');
      popup.querySelector('.wi-popup-textarea')?.focus();
    }, 250);
  }

  // =========================================================================
  // Inspect event handlers
  // =========================================================================
  function onInspectMouseMove(e) {
    if (inspectState !== 'active' || pendingPopup) return;
    const target = (e.composedPath?.()?.[0] || e.target);
    if (!target || isOwnElement(target)) {
      hideHoverHighlight();
      hideHoverTooltip();
      return;
    }
    const el = deepElementFromPoint(e.clientX, e.clientY);
    if (!el || isOwnElement(el)) {
      hideHoverHighlight();
      hideHoverTooltip();
      return;
    }
    const rect = el.getBoundingClientRect();
    const info = identifyElement(el);
    showHoverHighlight(rect);
    showHoverTooltip(e.clientX, e.clientY, info, rect);
  }

  function onInspectClick(e) {
    const target = (e.composedPath?.()?.[0] || e.target);
    if (isOwnElement(target)) return;

    // Block page interactions
    if (settings.blockInteractions || inspectState === 'active') {
      e.preventDefault();
      e.stopPropagation();
      e.stopImmediatePropagation();
    }

    // Paused state: don't create annotations
    if (inspectState === 'paused') return;

    // If popup is already open, shake it
    if (pendingPopup) {
      shakePendingPopup();
      return;
    }

    const el = deepElementFromPoint(e.clientX, e.clientY);
    if (!el || isOwnElement(el)) return;

    hideHoverHighlight();
    hideHoverTooltip();
    createAnnotationPopup(el, e.clientX, e.clientY);
  }

  function onInspectKeydown(e) {
    if (e.key === 'Escape') {
      e.preventDefault();
      e.stopPropagation();
      stopInspect();
    }
  }

  function startInspect() {
    inspectState = 'active';
    createOverlay();
    document.addEventListener('mousemove', onInspectMouseMove, true);
    document.addEventListener('click', onInspectClick, true);
    document.addEventListener('keydown', onInspectKeydown, true);
    inspectBtn.innerHTML = icons.pause;
    inspectBtn.setAttribute('data-active', 'true');
    inspectBtn.style.color = settings.annotationColor;
    inspectBtn.style.backgroundColor = settings.annotationColor + '40';
    inspectTip.textContent = '暂停检查';
  }

  function pauseInspect() {
    inspectState = 'paused';
    hideHoverHighlight();
    hideHoverTooltip();
    cancelPendingPopup();
    inspectBtn.innerHTML = icons.play;
    inspectBtn.setAttribute('data-active', 'paused');
    inspectBtn.style.color = '';
    inspectBtn.style.backgroundColor = '';
    inspectTip.textContent = '继续检查';
  }

  function resumeInspect() {
    inspectState = 'active';
    inspectBtn.innerHTML = icons.pause;
    inspectBtn.setAttribute('data-active', 'true');
    inspectBtn.style.color = settings.annotationColor;
    inspectBtn.style.backgroundColor = settings.annotationColor + '40';
    inspectTip.textContent = '暂停检查';
  }

  function stopInspect() {
    if (inspectState === 'off') return;
    inspectState = 'off';
    removeOverlay();
    hideHoverHighlight();
    hideHoverTooltip();
    cancelPendingPopup();
    hideSelectedOutline();
    document.removeEventListener('mousemove', onInspectMouseMove, true);
    document.removeEventListener('click', onInspectClick, true);
    document.removeEventListener('keydown', onInspectKeydown, true);
    inspectBtn.innerHTML = icons.inspect;
    inspectBtn.setAttribute('data-active', 'false');
    inspectBtn.style.color = '';
    inspectBtn.style.backgroundColor = '';
    inspectTip.textContent = '元素检查';
  }

  // =========================================================================
  // Button bindings
  // =========================================================================
  const inspectBtn = document.getElementById('wi-btn-inspect');
  const inspectTip = inspectBtn.parentElement.querySelector('.wi-tooltip');
  const eyeBtn = document.getElementById('wi-btn-eye');
  const eyeTip = eyeBtn.parentElement.querySelector('.wi-tooltip');
  const copyBtn = document.getElementById('wi-btn-copy');
  const trashBtn = document.getElementById('wi-btn-trash');
  const settingsBtn = document.getElementById('wi-btn-settings');

  let markersVisible = true;

  function updateAnnotationButtons() {
    const hasAnnotations = annotations.length > 0;
    eyeBtn.disabled = !hasAnnotations;
    copyBtn.disabled = !hasAnnotations;
    trashBtn.disabled = !hasAnnotations;
  }

  // =========================================================================
  // Theme management
  // =========================================================================
  function applyTheme() {
    const dark = settings.darkMode;
    if (dark) {
      container.classList.remove('wi-light-toolbar');
    } else {
      container.classList.add('wi-light-toolbar');
    }
  }
  applyTheme();

  // Update all marker colors
  function applyAnnotationColor() {
    const color = settings.annotationColor;
    annotations.forEach(ann => {
      ann.markerEl.style.backgroundColor = color;
    });
  }

  // =========================================================================
  // Settings panel
  // =========================================================================
  let settingsPanelEl = null;
  let settingsVisible = false;
  let settingsPage = 'main'; // 'main' | 'webhooks' | 'shortcuts'

  function buildSettingsHTML() {
    const dark = settings.darkMode;
    const color = settings.annotationColor;

    // Color dots
    let colorDotsHTML = '';
    for (const c of COLOR_OPTIONS) {
      const selected = c.value === color;
      colorDotsHTML += `<div class="wi-color-ring ${selected ? 'wi-selected' : ''}" data-color="${c.value}" style="border-color:${selected ? c.value : 'transparent'}" title="${c.label}"><div class="wi-color-dot" style="background-color:${c.value}"></div></div>`;
    }

    const checkIcon = icons.checkSm;
    const clearChecked = settings.autoClearAfterSend;
    const blockChecked = settings.blockInteractions;

    return `
      <div class="wi-settings-header">
        <span class="wi-settings-brand"><span class="wi-settings-brand-slash" style="color:${color}">/</span>web-inspector</span>
        <span class="wi-settings-version">v1.0.0</span>
        <button class="wi-theme-toggle" type="button" title="${dark ? '浅色模式' : '深色模式'}">${dark ? icons.sun : icons.moon}</button>
      </div>
      <div class="wi-settings-pages">
        <div class="wi-settings-page wi-settings-page-main">
          <div class="wi-settings-section">
            <div class="wi-settings-label wi-settings-label-marker">标注颜色</div>
            <div class="wi-color-options">${colorDotsHTML}</div>
          </div>
          <div class="wi-settings-section">
            <label class="wi-settings-toggle">
              <input type="checkbox" data-setting="autoClearAfterSend" ${clearChecked ? 'checked' : ''}>
              <div class="wi-checkbox ${clearChecked ? 'wi-checked' : ''}">${clearChecked ? checkIcon : ''}</div>
              <span class="wi-toggle-label">复制后清空</span>
            </label>
            <label class="wi-settings-toggle">
              <input type="checkbox" data-setting="blockInteractions" ${blockChecked ? 'checked' : ''}>
              <div class="wi-checkbox ${blockChecked ? 'wi-checked' : ''}">${blockChecked ? checkIcon : ''}</div>
              <span class="wi-toggle-label">阻止页面交互</span>
            </label>
          </div>
          <div class="wi-settings-section" style="padding-top:12px">
            <button class="wi-settings-nav" type="button" data-nav="shortcuts">
              <span>快捷键设置</span>
              <span>${icons.chevronR}</span>
            </button>
          </div>
          <div class="wi-settings-section" style="padding-top:12px">
            <button class="wi-settings-nav" type="button" data-nav="webhooks">
              <span>Webhook 配置</span>
              <span>${icons.chevronR}</span>
            </button>
          </div>
        </div>
        <div class="wi-settings-page wi-settings-page-webhooks">
          <button class="wi-settings-back" type="button" data-nav="main">${icons.chevronL}<span>Webhook 配置</span></button>
          <div class="wi-settings-section">
            <div class="wi-settings-row">
              <span class="wi-settings-label">Webhooks</span>
              <div class="wi-auto-send-row">
                <span class="wi-auto-send-label ${settings.webhooksEnabled ? 'wi-active' : ''}">Auto-Send</span>
                <label class="wi-toggle-switch ${!settings.webhookUrl ? 'wi-disabled' : ''}">
                  <input type="checkbox" data-setting="webhooksEnabled" ${settings.webhooksEnabled ? 'checked' : ''} ${!settings.webhookUrl ? 'disabled' : ''}>
                  <span class="wi-toggle-slider"></span>
                </label>
              </div>
            </div>
            <div class="wi-webhook-desc">标注数据将发送到此 URL 端点。</div>
            <textarea class="wi-webhook-input" placeholder="Webhook URL" rows="2">${settings.webhookUrl}</textarea>
          </div>
        </div>
        <div class="wi-settings-page wi-settings-page-shortcuts">
          <button class="wi-settings-back" type="button" data-nav="main">${icons.chevronL}<span>快捷键设置</span></button>
          <div class="wi-settings-section">
            <div class="wi-shortcut-tabs">
              ${Object.keys(SHORTCUT_LABELS).map((n, i) => `<button class="wi-shortcut-tab${i === 0 ? ' wi-tab-active' : ''}" data-sc="${n}">${SHORTCUT_LABELS[n]}</button>`).join('')}
            </div>
            <div class="wi-shortcut-desc">按下新的快捷键组合(需包含修饰键)</div>
            <div class="wi-shortcut-kbd">${formatShortcut(settings.shortcuts[Object.keys(SHORTCUT_LABELS)[0]])}</div>
            <div class="wi-shortcut-actions">
              <button class="wi-sc-cancel">取消</button>
              <button class="wi-sc-save" disabled>保存</button>
            </div>
          </div>
        </div>
      </div>`;
  }

  function openSettings() {
    closeSettings();
    settingsPage = 'main';
    settingsPanelEl = document.createElement('div');
    settingsPanelEl.className = `wi-settings ${settings.darkMode ? '' : 'wi-light'}`;
    settingsPanelEl.style.setProperty('--wi-accent', settings.annotationColor);
    settingsPanelEl.innerHTML = buildSettingsHTML();
    settingsPanelEl.addEventListener('click', e => e.stopPropagation());

    // Position: above toolbar by default, below if near top
    container.style.position = 'relative';
    container.appendChild(settingsPanelEl);

    requestAnimationFrame(() => settingsPanelEl.classList.add('wi-settings-enter'));
    settingsVisible = true;
    bindSettingsEvents();
  }

  function closeSettings() {
    if (!settingsPanelEl) return;
    settingsPanelEl.classList.remove('wi-settings-enter');
    settingsPanelEl.classList.add('wi-settings-exit');
    const el = settingsPanelEl;
    setTimeout(() => el.remove(), 100);
    settingsPanelEl = null;
    settingsVisible = false;
    container.style.position = '';
  }

  function refreshSettings() {
    if (!settingsPanelEl) return;
    const page = settingsPage;
    settingsPanelEl.className = `wi-settings wi-settings-enter ${settings.darkMode ? '' : 'wi-light'}`;
    settingsPanelEl.style.setProperty('--wi-accent', settings.annotationColor);
    settingsPanelEl.innerHTML = buildSettingsHTML();
    bindSettingsEvents();
    // Restore page
    if (page === 'webhooks' || page === 'shortcuts') {
      navigateSettingsPage(page);
    }
  }

  function navigateSettingsPage(page) {
    settingsPage = page;
    if (!settingsPanelEl) return;
    const mainPage = settingsPanelEl.querySelector('.wi-settings-page-main');
    const webhooksPage = settingsPanelEl.querySelector('.wi-settings-page-webhooks');
    const shortcutsPage = settingsPanelEl.querySelector('.wi-settings-page-shortcuts');
    const pages = settingsPanelEl.querySelector('.wi-settings-pages');
    if (page === 'webhooks' || page === 'shortcuts') {
      pages.classList.add('wi-transitioning');
      mainPage.classList.add('wi-slide-left');
      if (page === 'webhooks') webhooksPage.classList.add('wi-slide-in');
      else shortcutsPage.classList.add('wi-slide-in');
    } else {
      mainPage.classList.remove('wi-slide-left');
      webhooksPage.classList.remove('wi-slide-in');
      shortcutsPage.classList.remove('wi-slide-in');
      setTimeout(() => pages.classList.remove('wi-transitioning'), 350);
    }
  }

  function bindSettingsEvents() {
    if (!settingsPanelEl) return;

    // Theme toggle
    const themeBtn = settingsPanelEl.querySelector('.wi-theme-toggle');
    themeBtn?.addEventListener('click', () => {
      settings.darkMode = !settings.darkMode;
      saveSettings(settings);
      applyTheme();
      refreshSettings();
    });

    // Color options
    settingsPanelEl.querySelectorAll('.wi-color-ring').forEach(ring => {
      ring.addEventListener('click', () => {
        settings.annotationColor = ring.dataset.color;
        saveSettings(settings);
        applyAnnotationColor();
        refreshSettings();
      });
    });

    // Checkboxes
    settingsPanelEl.querySelectorAll('input[data-setting]').forEach(input => {
      input.addEventListener('change', () => {
        const key = input.dataset.setting;
        if (key === 'webhooksEnabled') {
          settings[key] = input.checked;
        } else {
          settings[key] = input.checked;
        }
        saveSettings(settings);
        refreshSettings();
      });
    });

    // Nav links
    settingsPanelEl.querySelectorAll('[data-nav]').forEach(btn => {
      btn.addEventListener('click', () => {
        navigateSettingsPage(btn.dataset.nav);
      });
    });

    // Webhook URL input
    const webhookInput = settingsPanelEl.querySelector('.wi-webhook-input');
    webhookInput?.addEventListener('input', () => {
      settings.webhookUrl = webhookInput.value;
      saveSettings(settings);
      // Enable/disable toggle
      const toggle = settingsPanelEl.querySelector('.wi-toggle-switch');
      const enableInput = settingsPanelEl.querySelector('input[data-setting="webhooksEnabled"]');
      if (toggle && enableInput) {
        if (settings.webhookUrl.trim()) {
          toggle.classList.remove('wi-disabled');
          enableInput.disabled = false;
        } else {
          toggle.classList.add('wi-disabled');
          enableInput.disabled = true;
        }
      }
    });

    // Shortcut recording
    const scNames = Object.keys(SHORTCUT_LABELS);
    const scTabs = settingsPanelEl.querySelectorAll('.wi-shortcut-tab');
    const scKbd = settingsPanelEl.querySelector('.wi-shortcut-kbd');
    const scSave = settingsPanelEl.querySelector('.wi-sc-save');
    const scCancel = settingsPanelEl.querySelector('.wi-sc-cancel');
    if (scKbd && scSave) {
      let activeScName = scNames[0];
      const pendings = {};

      const switchScTab = (name) => {
        activeScName = name;
        scTabs.forEach(t => t.classList.toggle('wi-tab-active', t.dataset.sc === name));
        const display = pendings[name] || settings.shortcuts[name];
        scKbd.textContent = formatShortcut(display);
        scKbd.classList.toggle('wi-recording', !!pendings[name]);
        scSave.disabled = !Object.keys(pendings).length;
      };

      scTabs.forEach(t => t.addEventListener('click', () => switchScTab(t.dataset.sc)));

      const onScKeyDown = (e) => {
        if (!settingsPanelEl?.querySelector('.wi-settings-page-shortcuts')) return;
        e.preventDefault();
        e.stopPropagation();
        if (['Alt','Shift','Control','Meta'].includes(e.key)) return;
        if (!e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) return;
        const shortcut = { altKey: e.altKey, shiftKey: e.shiftKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, code: e.code };
        pendings[activeScName] = shortcut;
        scKbd.textContent = formatShortcut(shortcut);
        scKbd.classList.add('wi-recording');
        scSave.disabled = false;
      };

      document.addEventListener('keydown', onScKeyDown, true);

      const cleanupSc = () => {
        document.removeEventListener('keydown', onScKeyDown, true);
      };

      scSave.addEventListener('click', () => {
        for (const [name, shortcut] of Object.entries(pendings)) {
          settings.shortcuts[name] = shortcut;
        }
        saveSettings(settings);
        cleanupSc();
        navigateSettingsPage('main');
      });

      scCancel.addEventListener('click', () => {
        cleanupSc();
        navigateSettingsPage('main');
      });
    }
  }
  function showToast(msg) {
    const t = document.createElement('div');
    t.className = 'wi-toast';
    t.textContent = msg;
    document.body.appendChild(t);
    requestAnimationFrame(() => t.classList.add('wi-toast-show'));
    setTimeout(() => { t.classList.remove('wi-toast-show'); setTimeout(() => t.remove(), 300); }, 2000);
  }

  function copyAnnotations() {
    const data = annotations.map((ann) => {
      const el = ann.targetEl;
      return {
        url: location.href,
        element: `<${el.tagName.toLowerCase()}${el.id ? ` id="${el.id}"` : ''}${el.className ? ` class="${el.className}"` : ''}>`,
        path: getElementPath(el),
        note: ann.text,
        innerText: el.innerText?.substring(0, 200) || '',
        component: findSourceByWalkUp(el),
      };
    });

    navigator.clipboard.writeText(JSON.stringify(data, null, 2)).then(() => {
      showToast('已复制到剪贴板');
    }).catch(err => {
      showToast('复制失败');
      console.warn('[WI] Clipboard write failed:', err);
    });

    stopInspect();

    if (settings.autoClearAfterSend) {
      cancelPendingPopup();
      annotations.forEach(ann => {
        ann.markerEl.classList.add('wi-marker-exit');
        setTimeout(() => ann.markerEl.remove(), 200);
      });
      annotations = [];
      markersVisible = true;
      eyeBtn.innerHTML = icons.eye;
      eyeBtn.setAttribute('data-active', 'false');
      eyeTip.textContent = '隐藏标注';
      updateAnnotationButtons();
      console.log('[WI] Annotations cleared after copy');
    }
  }

  // Close settings when clicking outside
  document.addEventListener('click', (e) => {
    if (settingsVisible && settingsPanelEl && !settingsPanelEl.contains(e.target) && !settingsBtn.contains(e.target)) {
      closeSettings();
    }
  });

  // =========================================================================
  // Button event listeners
  // =========================================================================
  inspectBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    if (inspectState === 'off') startInspect();
    else stopInspect();
  });

  // Eye: toggle markers visibility
  eyeBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    markersVisible = !markersVisible;
    annotations.forEach(ann => {
      ann.markerEl.style.display = markersVisible ? '' : 'none';
    });
    eyeBtn.innerHTML = markersVisible ? icons.eye : icons.eyeOff;
    eyeBtn.setAttribute('data-active', markersVisible ? 'false' : 'true');
    eyeTip.textContent = markersVisible ? '隐藏标注' : '显示标注';
  });

  // Copy: copy annotations to clipboard
  copyBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    copyAnnotations();
  });

  // Trash: delete all annotations
  trashBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    cancelPendingPopup();
    annotations.forEach(ann => {
      ann.markerEl.classList.add('wi-marker-exit');
      setTimeout(() => ann.markerEl.remove(), 200);
    });
    annotations = [];
    markersVisible = true;
    eyeBtn.innerHTML = icons.eye;
    eyeBtn.setAttribute('data-active', 'false');
    eyeTip.textContent = '隐藏标注';
    updateAnnotationButtons();
    console.log('[WI] All annotations deleted');
  });

  // Settings: toggle panel
  settingsBtn.addEventListener('click', (e) => {
    e.stopPropagation();
    if (settingsVisible) closeSettings();
    else openSettings();
  });

  // Close also stops inspect + closes settings
  const origCollapse = collapse;
  collapse = function() {
    stopInspect();
    closeSettings();
    origCollapse();
  };

  // =========================================================================
  // Global Shortcuts
  // =========================================================================
  document.addEventListener('keydown', (e) => {
    if (settingsVisible) return;
    const sc = settings.shortcuts;
    if (matchShortcut(e, sc.inspect)) {
      e.preventDefault();
      e.stopPropagation();
      if (!expanded) expand();
      if (inspectState === 'off') startInspect();
      else stopInspect();
    }
    if (matchShortcut(e, sc.copy)) {
      e.preventDefault();
      e.stopPropagation();
      if (annotations.length > 0) copyAnnotations();
    }
  });
})();