Greasy Fork 还支持 简体中文。

Pinterest Power Menu

All-in-one Pinterest power tool: original quality, download fixer, closeup image/video downloads, visible text translation, GIF hover/auto-play, remove videos, hide UI elements, declutter, scroll preservation

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Pinterest Power Menu
// @description  All-in-one Pinterest power tool: original quality, download fixer, closeup image/video downloads, visible text translation, GIF hover/auto-play, remove videos, hide UI elements, declutter, scroll preservation
// @version      1.4.0
// @author       Angel
// @namespace    https://github.com/Angel2mp3
// @homepageURL  https://angelmakes.software
// @icon         https://www.pinterest.com/favicon.ico
// @match        https://www.pinterest.com/*
// @match        https://pinterest.com/*
// @match        https://*.pinterest.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @connect      *
// @run-at       document-start
// ==/UserScript==

(function () {
  'use strict';

  // ═══════════════════════════════════════════════════════════════════
  //  SETTINGS
  // ═══════════════════════════════════════════════════════════════════
  const SETTINGS_KEY = 'pe_settings_v1';
  const SCRIPT_VERSION = '1.4.0';
  const UPDATE_NOTES_HIGHLIGHTS = [
    'Quick Download button on every pin closeup — works for videos too, not just images.',
    'Reverse Image Search button on closeups — Google, Yandex, SauceNAO, TinEye.',
    'Auto-Play Visible Videos — more reliable, with canplay waits, retries, and tab-visibility resume.',
    'New: hover Download button on every pin in feed/search/discovery grids — no closeup needed.',
  ];

  // ── Mobile / touch detection ─────────────────────────────────────────
  // Declared early so DEFAULTS can reference it (contextMenu off on mobile).
  // Gates features that are mouse-only or cause jank on touch devices.
  const IS_MOBILE = /android|iphone|ipad|ipod|mobile/i.test(navigator.userAgent)
    || (navigator.maxTouchPoints > 1 && /macintel/i.test(navigator.platform));
  const USER_LANG = ((navigator.language || navigator.userLanguage || 'en').split('-')[0] || 'en').toLowerCase();

  function isMobilePinCloseupPage() {
    return IS_MOBILE && /\/pin\/\d/i.test(location.pathname);
  }

  const DEFAULTS = {
    originalQuality:  true,
    downloadFixer:    true,
    gifHover:         true,
    hideVisitSite:    true,
    boardDownloader:  true,
    declutter:        true,
    declutterShopTheLook: true,
    declutterSearchAdvisory: false,
    contextMenu:      !IS_MOBILE,  // mouse-only feature; off by default on mobile
    hideUpdates:      false,
    hideMessages:     false,
    hideShare:        false,
    gifAutoPlay:      false,
    videoAutoPlay:    false,
    infiniteLoopVideo: false,
    darkMode:         'auto',
    removeVideos:     false,
    hideShopPosts:    false,
    hideComments:     false,
    hideCommentButton: false,
    hideReactButton:  false,
    hideReactionCount: false,
    hideUploadImageButton: false,
    hideSearchImageButton: false,
    hideSearchSuggestions: false,
    hideViewLargerButton: false,
    hideMoreOptionsButton: false,
    hideReverseImageSearchButton: false,
    hideCommentEmojiButton: false,
    hideCommentStickerButton: false,
    hideCommentPhotoButton: false,
    autoTranslate:    false,
    autoTranslateTitles: false,
    autoTranslateDescriptions: false,
    autoTranslateComments: false,
    autoTranslateCommentMode: 'visible',
    autoTranslateTarget: 'browser',
    titleTranslationDisplay: 'translated',
    customPinterestLogoUrl: '',
    customPinterestLogoSize: 32,
    customPinterestLogoCircle: true,
    reverseImageSearchButton: true,
    updateNotesDisabled: false,
    lastUpdateNotesVersion: '',
  };

  let _cfg = null;

  function loadCfg() {
    try {
      const raw = GM_getValue(SETTINGS_KEY, null);
      const saved = raw ? JSON.parse(raw) : {};
      _cfg = { ...DEFAULTS, ...saved };
      if (saved.autoTranslate === true) {
        if (saved.autoTranslateTitles === undefined) _cfg.autoTranslateTitles = true;
        if (saved.autoTranslateDescriptions === undefined) _cfg.autoTranslateDescriptions = true;
        if (saved.autoTranslateComments === undefined) _cfg.autoTranslateComments = true;
      }
      if (saved.hideComments === true && saved.hideCommentButton === undefined) _cfg.hideCommentButton = true;
      if (saved.autoTranslateTarget === undefined) _cfg.autoTranslateTarget = DEFAULTS.autoTranslateTarget;
      if (saved.autoTranslateCommentMode === undefined) _cfg.autoTranslateCommentMode = DEFAULTS.autoTranslateCommentMode;
      _cfg.showManualTranslateButtons = false;
      rememberMissingDefaultPrefs(saved);
    } catch (_) {
      _cfg = { ...DEFAULTS };
    }
  }

  function saveCfg() {
    try { GM_setValue(SETTINGS_KEY, JSON.stringify(_cfg)); } catch (_) {}
  }

  function rememberMissingDefaultPrefs(saved) {
    let changed = false;
    Object.keys(DEFAULTS).forEach(key => {
      if (Object.prototype.hasOwnProperty.call(saved, key)) return;
      if (_cfg[key] === undefined) _cfg[key] = DEFAULTS[key];
      changed = true;
    });
    if (changed) saveCfg();
  }

  function get(key) {
    if (!_cfg) loadCfg();
    return key in _cfg ? _cfg[key] : DEFAULTS[key];
  }

  function set(key, val) {
    if (!_cfg) loadCfg();
    _cfg[key] = val;
    saveCfg();
  }

  function shouldShowUpdateNotes() {
    return !get('updateNotesDisabled') && get('lastUpdateNotesVersion') !== SCRIPT_VERSION;
  }

  function escapeUpdateNoteText(value) {
    return String(value || '').replace(/[&<>"']/g, ch => ({
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#39;',
    }[ch]));
  }

  function dismissUpdateNotesPopup() {
    document.getElementById('pe-update-notes-layer')?.remove();
    set('lastUpdateNotesVersion', SCRIPT_VERSION);
  }

  function disableUpdateNotesForever() {
    document.getElementById('pe-update-notes-layer')?.remove();
    set('updateNotesDisabled', true);
    set('lastUpdateNotesVersion', SCRIPT_VERSION);
  }

  function createUpdateNotesPopup() {
    if (!document.body || !shouldShowUpdateNotes()) return;
    if (document.getElementById('pe-update-notes-layer')) return;

    const layer = document.createElement('div');
    layer.id = 'pe-update-notes-layer';
    layer.setAttribute('data-pe-ui', 'true');
    layer.innerHTML = `
      <div id="pe-update-notes-card" role="dialog" aria-modal="false" aria-label="Pinterest Power Menu update">
        <button id="pe-update-notes-close" type="button" aria-label="Close update notes">
          <svg viewBox="0 0 14 14" aria-hidden="true" width="14" height="14"><path d="M2 2 L12 12 M12 2 L2 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/></svg>
        </button>
        <div id="pe-update-notes-eyebrow">Updated to ${escapeUpdateNoteText(SCRIPT_VERSION)}</div>
        <div id="pe-update-notes-title">What's new</div>
        <ul id="pe-update-notes-list">
          ${UPDATE_NOTES_HIGHLIGHTS.map(note => `<li>${escapeUpdateNoteText(note)}</li>`).join('')}
        </ul>
        <button id="pe-update-notes-never" type="button">Never show me updates</button>
      </div>
    `;

    layer.addEventListener('click', e => {
      if (e.target === layer) dismissUpdateNotesPopup();
    });
    layer.querySelector('#pe-update-notes-close')?.addEventListener('click', dismissUpdateNotesPopup);
    layer.querySelector('#pe-update-notes-never')?.addEventListener('click', disableUpdateNotesForever);
    document.body.appendChild(layer);

    try {
      const mode = get('darkMode');
      let dark = false;
      if (mode === 'dark') dark = true;
      else if (mode === 'auto' && typeof isPinterestDarkTheme === 'function') dark = isPinterestDarkTheme();
      layer.querySelector('#pe-update-notes-card')?.classList.toggle('pe-dark', dark);
    } catch (_) {}
  }

  loadCfg();

  function injectEarlyDeclutterStyles() {
    if (document.getElementById('pe-declutter-early-styles')) return;
    const style = document.createElement('style');
    style.id = 'pe-declutter-early-styles';
    style.textContent = `
      html.pe-declutter-enabled div[role="list"] > div[role="listitem"]:has(div[title="Sponsored"]),
      html.pe-declutter-enabled div[role="list"] > div[role="listitem"]:has([aria-label="Shoppable Pin indicator"]),
      html.pe-declutter-enabled div[role="list"] > div[role="listitem"]:has([data-test-id="product-price-text"]),
      html.pe-declutter-enabled div[role="list"] > div[role="listitem"]:has([data-test-id="pincard-product-with-link"]) {
        height: 0 !important;
        width: 0 !important;
        margin: 0 !important;
        padding: 0 !important;
        border: none !important;
        overflow: hidden !important;
        opacity: 0 !important;
        min-height: 0 !important;
        min-width: 0 !important;
        pointer-events: none !important;
      }
      html.pe-declutter-enabled.pe-declutter-shop-look-enabled [data-test-id="duplo-shopping-module"],
      html.pe-declutter-enabled.pe-declutter-shop-look-enabled [data-test-id="ShopTheLookSimilarProducts"],
      html.pe-declutter-enabled.pe-declutter-shop-look-enabled [data-test-id="visual-search-shopping-bar"],
      html.pe-declutter-enabled.pe-declutter-shop-look-enabled [data-test-id="related-products"],
      html.pe-declutter-enabled.pe-declutter-shop-look-enabled [data-test-id="ShopTheLookAnnotations"],
      html.pe-declutter-enabled.pe-declutter-shop-look-enabled [data-test-id="shopping-module"] {
        height: 0 !important;
        width: 0 !important;
        margin: 0 !important;
        padding: 0 !important;
        border: none !important;
        overflow: hidden !important;
        opacity: 0 !important;
        min-height: 0 !important;
        min-width: 0 !important;
        pointer-events: none !important;
      }
      html.pe-declutter-enabled.pe-declutter-advisory-enabled [data-test-id="search-advisory"],
      html.pe-declutter-enabled.pe-declutter-advisory-enabled [data-test-id="fresh-search-advisory"] {
        height: 0 !important;
        width: 0 !important;
        margin: 0 !important;
        padding: 0 !important;
        border: none !important;
        overflow: hidden !important;
        opacity: 0 !important;
        min-height: 0 !important;
        min-width: 0 !important;
        pointer-events: none !important;
      }
      html.pe-declutter-enabled [data-test-id="pin-action-bar-container"]:has([data-test-id="visit-button-mobile-inline"]),
      html.pe-declutter-enabled [data-test-id="visit-button-mobile-inline"],
      html.pe-declutter-enabled [data-test-id="main-pin-section-visit-button"] {
        height: 0 !important;
        width: 0 !important;
        margin: 0 !important;
        padding: 0 !important;
        border: none !important;
        overflow: hidden !important;
        opacity: 0 !important;
        min-height: 0 !important;
        min-width: 0 !important;
        pointer-events: none !important;
      }
    `;
    (document.head || document.documentElement).appendChild(style);
  }

  function applyDeclutterToggle() {
    document.documentElement.classList.toggle('pe-declutter-enabled', get('declutter'));
    document.documentElement.classList.toggle('pe-declutter-shop-look-enabled', get('declutter') && get('declutterShopTheLook'));
    document.documentElement.classList.toggle('pe-declutter-advisory-enabled', get('declutter') && get('declutterSearchAdvisory'));
  }

  injectEarlyDeclutterStyles();
  applyDeclutterToggle();

  // ─── Video URL interceptor ──────────────────────────────────────────────
  // On desktop, Pinterest uses HLS.js which sets video.src to a blob:
  // MediaSource URL — findPinterestVideoSrc() cannot read the actual CDN URL
  // from the DOM.  Intercept XHR/fetch at document-start to capture
  // v1.pinimg.com video URLs as they are requested by HLS.js, then use them
  // as a fallback for the Quick Download button.
  function extractPinterestVideoHashFromText(value) {
    const text = String(value || '');
    const path = text.match(/(?:^|\/)([a-f0-9]{2})\/([a-f0-9]{2})\/([a-f0-9]{2})\/([a-f0-9]{32})(?=[._/?#]|$)/i);
    if (path) return `${path[1].toLowerCase()}/${path[2].toLowerCase()}/${path[3].toLowerCase()}/${path[4].toLowerCase()}`;
    const bare = text.match(/\b([a-f0-9]{32})\b/i)?.[1];
    if (!bare) return '';
    const hash = bare.toLowerCase();
    return `${hash.slice(0, 2)}/${hash.slice(2, 4)}/${hash.slice(4, 6)}/${hash}`;
  }

  function getPinterestVideoCdnBucket(value) {
    return String(value || '').match(/v1\.pinimg\.com\/videos\/(mc|iht)\//i)?.[1]?.toLowerCase() || '';
  }

  const _interceptedVideoUrls = [];   // most-recently-seen first
  const _interceptedVideoUrlsByHash = new Map();
  const _mobilePinVideoDownloadCache = new Map();
  let _onVideoUrlCapture = null;      // set by Quick Download startup
  (function () {
    function captureVideoUrl(url) {
      if (typeof url !== 'string') return;
      if (!/v1\.pinimg\.com\/videos/i.test(url)) return;
      const idx = _interceptedVideoUrls.indexOf(url);
      if (idx !== -1) _interceptedVideoUrls.splice(idx, 1);
      _interceptedVideoUrls.unshift(url);                // newest first
      if (_interceptedVideoUrls.length > 20) _interceptedVideoUrls.pop();
      const hash = extractPinterestVideoHashFromText(url);
      if (hash) {
        const urls = _interceptedVideoUrlsByHash.get(hash) || [];
        const hashIdx = urls.indexOf(url);
        if (hashIdx !== -1) urls.splice(hashIdx, 1);
        urls.unshift(url);
        if (urls.length > 8) urls.pop();
        _interceptedVideoUrlsByHash.set(hash, urls);
      }
      if (typeof _onVideoUrlCapture === 'function') _onVideoUrlCapture();
    }
    const _xOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (m, url, ...a) {
      captureVideoUrl(String(url));
      return _xOpen.call(this, m, url, ...a);
    };
    const _oFetch = window.fetch;
    if (typeof _oFetch === 'function') {
      window.fetch = function (input) {
        captureVideoUrl(typeof input === 'string' ? input : (input && input.url) || '');
        return _oFetch.apply(this, arguments);
      };
    }
  })();

  // Utility: returns a debounced version of fn (resets timer on every call).
  function debounce(fn, ms) {
    let t;
    return function () { clearTimeout(t); t = setTimeout(fn, ms); };
  }

  function isPowerMenuNode(node) {
    if (!node || node.nodeType !== 1) return false;
    return !!node.closest?.(
      '[data-pe-ui="true"], #pe-settings-wrap, #pe-ctx-menu, #pe-bd-fab, ' +
      '#pe-reverse-image-search-menu, #pe-toast'
    );
  }

  function isPowerMenuEvent(e) {
    return isPowerMenuNode(e?.target);
  }

  function hasOnlyPowerMenuMutations(records) {
    return !!records?.length && records.every(record => {
      if (isPowerMenuNode(record.target)) return true;
      const nodes = [...record.addedNodes, ...record.removedNodes]
        .filter(node => node.nodeType === 1);
      return nodes.length > 0 && nodes.every(isPowerMenuNode);
    });
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: ORIGINAL QUALITY  (fast – no probe, no popup)
  // ═══════════════════════════════════════════════════════════════════
  // Directly rewrite pinimg.com thumbnail URLs → /originals/ with
  // an inline onerror fallback to /736x/ so zero extra requests are
  // made upfront and the "Optimizing…" overlay is never shown.

  const OQ_RE = /^(https?:\/\/i\.pinimg\.com)\/\d+x(\/[0-9a-f]{2}\/[0-9a-f]{2}\/[0-9a-f]{2}\/[0-9a-f]{32}\.(?:jpg|jpeg|png|gif|webp))$/i;

  function upgradeImg(img) {
    if (!get('originalQuality')) return;
    if (img.__peOQ || img.tagName !== 'IMG' || !img.src) return;
    const m = img.src.match(OQ_RE);
    if (!m) return;
    img.__peOQ = true;
    const origSrc = m[1] + '/originals' + m[2];
    const fallSrc = m[1] + '/736x'      + m[2];
    img.onerror = function () {
      if (img.src === origSrc) { img.onerror = null; img.src = fallSrc; }
    };
    if (img.getAttribute('data-src') === img.src) img.setAttribute('data-src', origSrc);
    img.src = origSrc;
  }

  function scanOQ(node) {
    if (!node || node.nodeType !== 1) return;
    if (node.tagName === 'IMG') upgradeImg(node);
    else node.querySelectorAll('img[src*="pinimg.com"]').forEach(upgradeImg);
  }

  // Start MutationObserver immediately (document-start) so we catch
  // images before they fire their first load event.
  const oqObs = new MutationObserver(records => {
    if (!get('originalQuality')) return;
    const process = () => records.forEach(r => {
      if (r.attributeName === 'src') upgradeImg(r.target);
      else r.addedNodes.forEach(scanOQ);
    });
    // On mobile, yield to the browser's render pipeline so scroll stays smooth
    if (IS_MOBILE && typeof requestIdleCallback === 'function') {
      requestIdleCallback(process, { timeout: 300 });
    } else {
      process();
    }
  });
  oqObs.observe(document.documentElement, {
    childList: true, subtree: true,
    attributes: true, attributeFilter: ['src'],
  });


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: HIDE VISIT SITE
  // ═══════════════════════════════════════════════════════════════════
  // Uses CSS classes on <body> so toggles are instant and zero-cost.
  function applyVisitSiteToggle() {
    if (!document.body) return;
    document.body.classList.toggle('pe-hide-visit', get('hideVisitSite'));
  }

  function applyNavToggles() {
    if (!document.body) return;
    applyDeclutterToggle();
    document.body.classList.toggle('pe-hide-updates',    get('hideUpdates'));
    document.body.classList.toggle('pe-hide-messages',   get('hideMessages'));
    document.body.classList.toggle('pe-hide-share',      get('hideShare'));
    document.body.classList.toggle('pe-hide-comments',   get('hideComments'));
    document.body.classList.toggle('pe-hide-comment-button', get('hideCommentButton'));
    document.body.classList.toggle('pe-hide-react',      get('hideReactButton'));
    document.body.classList.toggle('pe-hide-reaction-count', get('hideReactionCount'));
    document.body.classList.toggle('pe-hide-upload-image', !IS_MOBILE && get('hideUploadImageButton'));
    document.body.classList.toggle('pe-hide-search-image', get('hideSearchImageButton'));
    document.body.classList.toggle('pe-hide-search-suggestions', get('hideSearchSuggestions'));
    document.body.classList.toggle('pe-hide-view-larger', get('hideViewLargerButton'));
    document.body.classList.toggle('pe-hide-more-options', get('hideMoreOptionsButton'));
    document.body.classList.toggle('pe-hide-reverse-image-search', get('hideReverseImageSearchButton'));
    document.body.classList.toggle('pe-hide-comment-emoji', get('hideCommentEmojiButton'));
    document.body.classList.toggle('pe-hide-comment-sticker', get('hideCommentStickerButton'));
    document.body.classList.toggle('pe-hide-comment-photo', get('hideCommentPhotoButton'));
  }

  // Physically removes the Messages nav button from the DOM (not just hidden with CSS).
  // A MutationObserver re-removes it whenever Pinterest re-renders the nav (SPA navigation).
  let _messagesRemoverObs = null;
  function initMessagesRemover() {
    if (!get('hideMessages')) return;
    if (_messagesRemoverObs) return; // already running
    const SELS = [
      'div[aria-label="Messages"]',
      '[data-test-id="nav-bar-speech-ellipsis"]',
    ];
    function removeNow(root) {
      SELS.forEach(sel => {
        (root.querySelectorAll ? root.querySelectorAll(sel) : []).forEach(el => el.remove());
      });
    }
    removeNow(document);
    _messagesRemoverObs = new MutationObserver(recs => {
      if (hasOnlyPowerMenuMutations(recs)) return;
      if (!get('hideMessages')) { _messagesRemoverObs.disconnect(); _messagesRemoverObs = null; return; }
      recs.forEach(r => r.addedNodes.forEach(n => { if (n.nodeType === 1) removeNow(n); }));
    });
    _messagesRemoverObs.observe(document.documentElement, { childList: true, subtree: true });
  }

  // JS-based "Visit site" link removal – catches links that CSS alone misses
  // (e.g. <a rel="nofollow"><div>Visit site</div></a>)
  function initVisitSiteHider() {
    function hideInTree(root) {
      if (!get('hideVisitSite') || !root) return;
      const links = root.querySelectorAll ? root.querySelectorAll('a') : [];
      links.forEach(a => {
        if (a.__peVisitHidden) return;
        const text = a.textContent.trim();
        if (/^visit\s*site$/i.test(text)) {
          a.__peVisitHidden = true;
          a.style.setProperty('display', 'none', 'important');
        }
      });
    }
    hideInTree(document);
    new MutationObserver(recs => {
      if (hasOnlyPowerMenuMutations(recs)) return;
      if (!get('hideVisitSite')) return;
      recs.forEach(r => r.addedNodes.forEach(n => {
        if (n.nodeType === 1) hideInTree(n);
      }));
    }).observe(document.documentElement, { childList: true, subtree: true });
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: SHARE URL OVERRIDE
  // ═══════════════════════════════════════════════════════════════════
  // Replaces Pinterest's shortened pin.it URLs in the share dialog
  // with the actual pin URL.  On closeup pages that's location.href;
  // on the grid we walk up from the share button to find the pin link.
  // Also intercepts "Copy link" and clicks on the URL input box.

  function initShareOverride() {
    const nativeSetter = Object.getOwnPropertyDescriptor(
      HTMLInputElement.prototype, 'value'
    ).set;

    let _sharePinUrl = null;

    // 1) Track share/send button clicks to capture the pin's real URL
    document.addEventListener('click', e => {
      if (isPowerMenuEvent(e)) return;
      const shareBtn = e.target.closest(
        '[data-test-id="sendPinButton"], button[aria-label="Send"], ' +
        '[data-test-id="closeup-share-button"], div[aria-label="Share"], ' +
        'button[aria-label="Share"]'
      );
      if (!shareBtn) return;

      // On a pin closeup page, location.href IS the pin URL
      if (/\/pin\/\d+/.test(location.pathname)) {
        _sharePinUrl = location.href;
        return;
      }

      // On grid: walk up from the share button to find the pin card link
      _sharePinUrl = null;
      let el = shareBtn;
      for (let i = 0; i < 30 && el; i++) {
        if (el.querySelector) {
          const link = el.querySelector('a[href*="/pin/"]');
          if (link) {
            _sharePinUrl = new URL(link.href, location.origin).href;
            break;
          }
        }
        el = el.parentElement;
      }
      if (!_sharePinUrl) _sharePinUrl = location.href;
    }, true);

    // 2) Watch for the share-popup URL input and override its value
    function fixShareInputs() {
      const realUrl = _sharePinUrl || location.href;
      document.querySelectorAll(
        'input#url-text, ' +
        '[data-test-id="copy-link-share-icon-auth"] input[type="text"], ' +
        'input[readonly][value*="pin.it"], ' +
        'input[readonly][value*="pinterest.com/pin/"]'
      ).forEach(input => {
        // Always re-fix if value doesn't match
        if (input.value !== realUrl) {
          nativeSetter.call(input, realUrl);
          input.dispatchEvent(new Event('input', { bubbles: true }));
        }
        if (!input.__peShareClick) {
          input.__peShareClick = true;
          // Intercept clicks on the input box itself
          input.addEventListener('click', ev => {
            ev.stopPropagation();
            const url = _sharePinUrl || location.href;
            navigator.clipboard.writeText(url).catch(() => {
              const ta = document.createElement('textarea');
              ta.value = url;
              ta.style.cssText = 'position:fixed;left:-9999px';
              document.body.appendChild(ta);
              ta.select();
              document.execCommand('copy');
              ta.remove();
            });
          }, true);
          // Re-fix if React re-renders the value
          new MutationObserver(() => {
            const url = _sharePinUrl || location.href;
            if (input.value !== url) {
              nativeSetter.call(input, url);
              input.dispatchEvent(new Event('input', { bubbles: true }));
            }
          }).observe(input, { attributes: true, attributeFilter: ['value'] });
        }
      });
    }

    new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      fixShareInputs();
    })
      .observe(document.documentElement, { childList: true, subtree: true });

    // 3) Intercept "Copy link" button clicks
    document.addEventListener('click', e => {
      if (isPowerMenuEvent(e)) return;
      const copyBtn = e.target.closest(
        'button[aria-label="Copy link"], ' +
        '[data-test-id="copy-link-share-icon-auth"] button'
      );
      if (!copyBtn) return;
      e.preventDefault();
      e.stopPropagation();
      e.stopImmediatePropagation();
      const realUrl = _sharePinUrl || location.href;
      navigator.clipboard.writeText(realUrl).then(() => {
        const txt = copyBtn.querySelector('div');
        if (txt) {
          const orig = txt.textContent;
          txt.textContent = 'Copied!';
          setTimeout(() => { txt.textContent = orig; }, 1500);
        }
      }).catch(() => {
        const ta = document.createElement('textarea');
        ta.value = realUrl;
        ta.style.cssText = 'position:fixed;left:-9999px';
        document.body.appendChild(ta);
        ta.select();
        document.execCommand('copy');
        ta.remove();
      });
    }, true);
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: GIF / VIDEO HOVER PLAY
  // ═══════════════════════════════════════════════════════════════════
  // In the pin grid, Pinterest renders GIFs as static <img> elements
  // (showing a .jpg thumbnail) with the real .gif URL hidden in
  // srcset at "4x".  There is no <video> in the grid.
  //
  // Strategy:
  //  • On mouseover – walk up to [data-test-id="pinWrapper"], find
  //    img[srcset*=".gif"], extract the .gif URL, swap img.src to it.
  //  • On mouseout – restore the original .jpg src.
  //  • Only ONE gif plays at a time (previous is restored before new starts).
  //  • <video> elements (pin closeup / detail page) are still kept paused
  //    via the MutationObserver so they don't auto-play in the background.

  // Selector matching any img that carries a GIF URL in srcset, live src, or lazy data-src.
  // Used by both hover-play and auto-play modules.
  const GIF_IMG_SEL = 'img[srcset*=".gif"], img[src*=".gif"], img[data-src*=".gif"]';
  const GIF_PIN_CONTAINER_SEL = [
    '[data-test-id="pinWrapper"]',
    '[data-grid-item="true"]',
    '[data-test-id="pin"]',
    'div[role="listitem"]',
    '[data-test-id="pin-closeup-image"]',
  ].join(', ');

  let _gifActiveImg     = null;   // <img> currently showing a .gif
  let _gifOrigSrc       = null;   // original src to restore on leave
  let _gifOrigSrcset    = null;   // original srcset to restore on leave
  let _gifActiveCont    = null;   // pinWrapper of the active gif
  let _gifActiveVid     = null;   // <video> currently playing a GIF (mobile hover/tap)

  // Pinterest uses different card wrappers across home/search/closeup pages,
  // especially on mobile. Resolve the nearest usable pin container defensively.
  function findGifContainer(node) {
    if (!node || node.nodeType !== 1) return null;
    return node.closest(GIF_PIN_CONTAINER_SEL);
  }

  // Resolve a video source even when Pinterest lazy-loads into data-* attrs.
  function getVideoSrc(video) {
    if (!video) return '';
    const source = video.querySelector && video.querySelector('source');
    return video.src
      || video.getAttribute('src')
      || video.getAttribute('data-src')
      || (source && (source.src || source.getAttribute('src') || source.getAttribute('data-src')))
      || '';
  }

  // Ensure lazy mobile GIF videos have a concrete src before play() attempts.
  function hydrateVideoSource(video) {
    if (!video) return;
    if (!video.getAttribute('src')) {
      const ds = video.getAttribute('data-src');
      if (ds) video.setAttribute('src', ds);
    }
    const source = video.querySelector && video.querySelector('source');
    if (source && !source.getAttribute('src')) {
      const ds = source.getAttribute('data-src');
      if (ds) source.setAttribute('src', ds);
    }
  }

  // Classify whether a <video> is a GIF-like pin media.
  // Some mobile layouts use i.pinimg.com sources, others expose only
  // a PinTypeIdentifier badge with text "GIF".
  function isGifVideo(video, container) {
    if (!video) return false;
    const src = getVideoSrc(video);
    if (/i\.pinimg\.com/i.test(src)) return true;
    const wrap = container || findGifContainer(video);
    const badge = wrap && wrap.querySelector('[data-test-id="PinTypeIdentifier"]');
    if (!badge) return false;
    const t = (badge.textContent || '').trim().toLowerCase();
    if (t === 'gif' || t.includes('animated')) return true;
    if (t === 'video' || t.includes('watch')) return false;
    return false;
  }

  // Detect the mobile/touch layout GIF pin — Pinterest renders these with
  // JPEG-only srcset; the GIF container data-test-ids identify them reliably.
  function isMobileGifPin(container) {
    if (!container) return false;
    if (container.querySelector('[data-test-id="inp-perf-pinType-gif"]')) return true;
    if (container.querySelector('[data-test-id="pincard-gif-without-link"]')) return true;
    const badge = container.querySelector('[data-test-id="PinTypeIdentifier"]');
    if (badge) {
      const t = (badge.textContent || '').trim().toLowerCase();
      if (t === 'gif' || t.includes('animated')) return true;
    }
    return false;
  }

  // Convert a pinimg.com JPEG/WebP thumbnail URL to the /originals/ GIF URL.
  // e.g. …/236x/ab/cd/ef/hash.jpg → …/originals/ab/cd/ef/hash.gif
  function deriveGifUrl(jpegUrl) {
    if (!jpegUrl) return null;
    const m = jpegUrl.match(/^(https?:\/\/i\.pinimg\.com)\/[^/]+(\/.+?)(?:\.jpe?g|\.webp)(\?.*)?$/i);
    if (!m) return null;
    return m[1] + '/originals' + m[2] + '.gif';
  }

  // Extract the .gif URL from an img element, checking srcset, live src, and data-src.
  // On mobile Pinterest uses JPEG-only srcset for GIF pins; derive the .gif URL when needed.
  function getGifSrcFromImg(img) {
    if (!img) return null;
    // Prefer srcset (Pinterest hides the GIF at "4x"; also stored in __peAutoOrigSrcset)
    const srcset = img.getAttribute('srcset') || img.__peAutoOrigSrcset || '';
    for (const part of srcset.split(',')) {
      const url = part.trim().split(/\s+/)[0];
      if (url && /\.gif(\?|$)/i.test(url)) return url;
    }
    // GIF already in src (srcset was cleared and .gif URL was applied)
    if (/\.gif(\?|$)/i.test(img.src)) return img.src;
    // Lazy-loaded src attribute
    const ds = img.getAttribute('data-src') || '';
    if (/\.gif(\?|$)/i.test(ds)) return ds;
    // Mobile layout: GIF pins have JPEG-only srcset but carry inp-perf-pinType-gif /
    // pincard-gif-without-link in their container. Derive the originals .gif URL.
    const wrap = img.closest('[data-test-id="pinWrapper"], [data-grid-item="true"], [data-test-id="pin"]');
    if (isMobileGifPin(wrap)) {
      const jpegSrc = img.getAttribute('src') || img.src || '';
      if (jpegSrc) {
        const d = deriveGifUrl(jpegSrc);
        if (d) return d;
      }
      // Fallback: try highest-res srcset entry
      const parts = srcset.split(',').map(p => p.trim().split(/\s+/)[0]).filter(Boolean);
      for (let i = parts.length - 1; i >= 0; i--) {
        const d = deriveGifUrl(parts[i]);
        if (d) return d;
      }
    }
    return null;
  }

  function pauseActiveGif() {
    if (_gifActiveImg) {
      // Restore srcset FIRST so the browser doesn't re-pick from it
      // before we restore src
      if (_gifOrigSrcset !== null) _gifActiveImg.setAttribute('srcset', _gifOrigSrcset);
      if (_gifOrigSrc    !== null) _gifActiveImg.src = _gifOrigSrc;
    }
    if (_gifActiveVid) {
      try { _gifActiveVid.pause(); } catch (_) {}
      _gifActiveVid = null;
    }
    const prevCont    = _gifActiveCont;
    _gifActiveImg     = null;
    _gifOrigSrc       = null;
    _gifOrigSrcset    = null;
    _gifActiveCont    = null;
    // If GIF auto-play is active, let it take over this wrapper
    if (prevCont && get('gifAutoPlay') && _gifAutoIO) {
      setTimeout(() => {
        const r = prevCont.getBoundingClientRect();
        if (r.top < window.innerHeight && r.bottom > 0) startGifInView(prevCont);
      }, 50);
    }
  }

  // Keep any <video> elements (pin detail/closeup page) paused so they
  // don't auto-play in the background.
  function pauseVidOnAdd(v) {
    if (v.__pePaused || v.__peGifVid) return;
    // GIFs rendered as <video src="i.pinimg.com/…"> on mobile must NOT be paused here —
    // the GIF hover / auto-play modules manage those independently.
    const getSrc = () => getVideoSrc(v);
    const src = getSrc();
    const initialWrap = findGifContainer(v);
    if (isGifVideo(v, initialWrap)) {
      v.__peGifVid = true;
      return;
    }
    // src not yet assigned (lazy-load): observe for when it is set before deciding to pause.
    // Without this, Pinterest's async src assignment races with auto-play on mobile —
    // the deferred kill() calls would pause the video after auto-play had already started it.
    if (!src) {
      if (v.__peVidSrcObs) return; // observer already attached
      v.__peVidSrcObs = true;
      const obs = new MutationObserver(() => {
        const s = getSrc();
        if (!s) return; // still not set – keep waiting
        obs.disconnect();
        v.__peVidSrcObs = false;
        const wrap = findGifContainer(v);
        if (isGifVideo(v, wrap)) {
          // It's a mobile GIF video – let hover / auto-play manage it; never pause it
          v.__peGifVid = true;
          const pw = wrap;
          if (pw && _gifAutoIO) { pw.__peAutoObs = false; observeGifPins(); }
        } else {
          pauseVidOnAdd(v); // real video – go ahead and pause it
        }
      });
      obs.observe(v, { attributes: true, attributeFilter: ['src'], childList: true });
      return;
    }
    v.__pePaused = true;
    v.muted = true;
    // Respect videoAutoPlay: don't fight it by killing playback.
    if (get('videoAutoPlay')) return;
    const kill = () => {
      try { v.pause(); } catch (_) {}
    };
    kill(); setTimeout(kill, 60); setTimeout(kill, 250);
  }

  new MutationObserver(records => {
    if (hasOnlyPowerMenuMutations(records)) return;
    records.forEach(r => r.addedNodes.forEach(function scan(n) {
      if (!n || n.nodeType !== 1) return;
      if (n.tagName === 'VIDEO') pauseVidOnAdd(n);
      n.querySelectorAll && n.querySelectorAll('video').forEach(pauseVidOnAdd);
    }));
  }).observe(document.documentElement, { childList: true, subtree: true });

  function initGifHover() {
    document.addEventListener('mouseover', e => {
      if (!get('gifHover')) return;

      const pinWrapper = findGifContainer(e.target);
      if (!pinWrapper || pinWrapper === _gifActiveCont) return;

      // Look for a GIF image inside this pin card (incl. mobile JPEG-srcset GIF pins)
      const img = pinWrapper.querySelector(GIF_IMG_SEL)
               || (isMobileGifPin(pinWrapper) ? pinWrapper.querySelector('img') : null);
      if (!img) return;
      const gifUrl = getGifSrcFromImg(img);
      if (!gifUrl) return;

      // Stop the previous gif first
      pauseActiveGif();

      // Start the new one.
      // IMPORTANT: browsers use srcset over src, so we must clear srcset
      // before setting src to the gif URL, otherwise src change is ignored.
      _gifActiveImg     = img;
      _gifOrigSrc       = img.src;
      _gifOrigSrcset    = img.getAttribute('srcset');
      _gifActiveCont    = pinWrapper;
      img.removeAttribute('srcset');   // prevent srcset overriding our src
      img.src = gifUrl;
    }, { passive: true });

    document.addEventListener('mouseout', e => {
      if (!get('gifHover') || !_gifActiveCont) return;
      const to = e.relatedTarget;
      // If the mouse moved to another element still inside the pin wrapper, keep playing
      if (to && _gifActiveCont.contains(to)) return;
      pauseActiveGif();
    }, { passive: true });

    // ── Touch: tap to preview GIF on mobile ──────────────────────────
    // First tap on a GIF pin starts playback; second tap (or tap elsewhere) stops it.
    // Scrolling never accidentally triggers GIF playback.
    let _gifTouchStartY = 0, _gifTouchScrolled = false;

    document.addEventListener('touchstart', e => {
      _gifTouchStartY   = e.touches[0].clientY;
      _gifTouchScrolled = false;
    }, { passive: true });

    document.addEventListener('touchmove', e => {
      if (Math.abs(e.touches[0].clientY - _gifTouchStartY) > 8) _gifTouchScrolled = true;
    }, { passive: true });

    document.addEventListener('touchend', e => {
      if (!get('gifHover') || _gifTouchScrolled) return;
      // Don't interfere when the context menu is open
      if (document.getElementById('pe-ctx-menu')) return;
      const touch = e.changedTouches[0];
      const el = document.elementFromPoint(touch.clientX, touch.clientY);
      if (!el) return;
      const pinWrapper = findGifContainer(el);
      if (!pinWrapper) { pauseActiveGif(); return; }
      const img    = pinWrapper.querySelector(GIF_IMG_SEL)
                  || (isMobileGifPin(pinWrapper) ? pinWrapper.querySelector('img') : null);
      const gifUrl = img ? getGifSrcFromImg(img) : null;
      if (!gifUrl) {
        // No img-based GIF – check for a mobile video-based GIF
        const vid   = pinWrapper.querySelector('video');
        if (vid) hydrateVideoSource(vid);
        if (!vid || !isGifVideo(vid, pinWrapper)) { pauseActiveGif(); return; }
        // Second tap on the same video GIF = stop
        if (pinWrapper === _gifActiveCont) { pauseActiveGif(); return; }
        pauseActiveGif();
        _gifActiveCont = pinWrapper;
        _gifActiveVid  = vid;
        vid.muted = true;
        vid.loop  = true;
        vid.playsInline = true;
        if (vid.readyState === 0) {
          try { vid.load(); } catch (_) {}
        }
        try { vid.play(); } catch (_) {}
        return;
      }
      // Second tap on the same GIF pin = stop
      if (pinWrapper === _gifActiveCont) { pauseActiveGif(); return; }
      pauseActiveGif();
      _gifActiveImg     = img;
      _gifOrigSrc       = img.src;
      _gifOrigSrcset    = img.getAttribute('srcset');
      _gifActiveCont    = pinWrapper;
      img.removeAttribute('srcset');
      img.src = gifUrl;
    }, { passive: true });
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: GIF AUTO-PLAY (viewport-based)
  // ═══════════════════════════════════════════════════════════════════
  // Uses IntersectionObserver to play all GIFs currently visible on
  // screen and stop them when scrolled out of view to save CPU/memory.

  let _gifAutoIO = null;   // IntersectionObserver
  let _gifAutoMO = null;   // MutationObserver for new pins

  function startGifInView(wrapper) {
    // ── img-based GIF (desktop + most mobile, including mobile JPEG-srcset GIFs) ──
    const img = wrapper.querySelector(GIF_IMG_SEL)
             || (isMobileGifPin(wrapper) ? wrapper.querySelector('img') : null);
    if (img && !img.__peAutoPlaying) {
      const gifUrl = getGifSrcFromImg(img);
      if (gifUrl) {
        img.__peAutoOrigSrc    = img.src;
        img.__peAutoOrigSrcset = img.getAttribute('srcset');
        img.removeAttribute('srcset');
        img.src = gifUrl;
        img.__peAutoPlaying = true;
        return;
      }
    }
    // ── video-based GIF (mobile) ──
    const vid = wrapper.querySelector('video');
    if (vid && !vid.__peAutoPlaying) {
      hydrateVideoSource(vid);
      if (isGifVideo(vid, wrapper)) {
        vid.__peAutoPlaying = true;
        vid.muted = true;
        vid.loop  = true;
        vid.playsInline = true;
        if (vid.readyState === 0) {
          try { vid.load(); } catch (_) {}
        }
        try { vid.play(); } catch (_) {}
      }
    }
  }

  function stopGifInView(wrapper) {
    wrapper.querySelectorAll('img').forEach(img => {
      if (!img.__peAutoPlaying) return;
      // Don't interfere if hover is currently managing this img
      if (img === _gifActiveImg) { img.__peAutoPlaying = false; return; }
      if (img.__peAutoOrigSrcset) img.setAttribute('srcset', img.__peAutoOrigSrcset);
      if (img.__peAutoOrigSrc)    img.src = img.__peAutoOrigSrc;
      img.__peAutoPlaying = false;
    });
    // Stop video-based GIFs (mobile)
    wrapper.querySelectorAll('video').forEach(vid => {
      if (!vid.__peAutoPlaying) return;
      vid.__peAutoPlaying = false;
      if (vid === _gifActiveVid) return; // hover/tap is managing this video
      try { vid.pause(); } catch (_) {}
    });
  }

  function observeGifPin(wrapper) {
    if (!_gifAutoIO || !wrapper || wrapper.__peAutoObs) return;
    // Detect img-based GIF, video-based GIF, or mobile JPEG-srcset GIF
    const hasGifImg = !!wrapper.querySelector(GIF_IMG_SEL);
    const hasGifVid = (() => {
      const vid = wrapper.querySelector('video');
      if (!vid) return false;
      if (vid.__peGifVid) return true; // already confirmed as a GIF video
      return isGifVideo(vid, wrapper);
    })();
    const hasMobileGif = !hasGifImg && !hasGifVid && isMobileGifPin(wrapper);
    if (!hasGifImg && !hasGifVid && !hasMobileGif) return;
    wrapper.__peAutoObs = true;
    _gifAutoIO.observe(wrapper);
  }

  function observeGifPins(root = document) {
    if (!_gifAutoIO) return;
    if (root.matches && root.matches(GIF_PIN_CONTAINER_SEL)) observeGifPin(root);
    if (root.querySelectorAll) root.querySelectorAll(GIF_PIN_CONTAINER_SEL).forEach(observeGifPin);
  }

  function initGifAutoPlay() {
    if (_gifAutoIO) return;
    _gifAutoIO = new IntersectionObserver(entries => {
      // Skip when feature is off or tab is hidden (avoids playing on inactive tabs)
      if (!get('gifAutoPlay') || document.hidden) return;
      entries.forEach(entry => {
        if (entry.isIntersecting) startGifInView(entry.target);
        else                      stopGifInView(entry.target);
      });
    }, { threshold: 0.1 });

    observeGifPins();
    _gifAutoMO = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      records.forEach(r => r.addedNodes.forEach(n => {
        if (n && n.nodeType === 1) observeGifPins(n);
      }));
    });
    _gifAutoMO.observe(document.documentElement, { childList: true, subtree: true });
  }

  function stopGifAutoPlay() {
    if (_gifAutoIO) { _gifAutoIO.disconnect(); _gifAutoIO = null; }
    if (_gifAutoMO) { _gifAutoMO.disconnect(); _gifAutoMO = null; }
    document.querySelectorAll(GIF_PIN_CONTAINER_SEL).forEach(wrapper => {
      stopGifInView(wrapper);
      wrapper.__peAutoObs = false;
    });
  }

  // Pause all auto-playing GIFs when the tab/window is hidden to save resources,
  // and resume them when the user comes back.
  document.addEventListener('visibilitychange', () => {
    if (!get('gifAutoPlay')) return;
    if (document.hidden) {
      document.querySelectorAll(GIF_PIN_CONTAINER_SEL).forEach(stopGifInView);
    } else if (_gifAutoIO) {
      // Re-start GIFs that are still in the viewport
      document.querySelectorAll(GIF_PIN_CONTAINER_SEL).forEach(wrapper => {
        if (!wrapper.__peAutoObs) return;
        const r = wrapper.getBoundingClientRect();
        if (r.top < window.innerHeight && r.bottom > 0) startGifInView(wrapper);
      });
    }
  });


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: VIDEO AUTO-PLAY (viewport-based)
  // ═══════════════════════════════════════════════════════════════════
  // Mirrors GIF auto-play for non-GIF <video> elements. Browsers require
  // muted auto-play, so all auto-played videos are muted.

  let _vidAutoIO = null;
  let _vidAutoMO = null;
  const _vidAutoPending = new Set();

  // A real pin video — not a GIF rendered as <video>, not our own chrome.
  function isRealVideo(v) {
    if (!v || v.tagName !== 'VIDEO') return false;
    if (v.__peGifVid) return false;
    const wrap = findGifContainer(v);
    if (isGifVideo(v, wrap)) return false;
    return true;
  }

  function startVidInView(v) {
    if (!isRealVideo(v) || v.__peAutoVidPlaying) return;
    v.__peAutoVidPlaying = true;
    v.muted = true;
    v.playsInline = true;

    const doPlay = () => {
      if (!v.__peAutoVidPlaying) return;
      let p;
      try { p = v.play(); } catch (_) {}
      if (p && typeof p.catch === 'function') {
        p.catch(() => {
          setTimeout(() => {
            if (v.__peAutoVidPlaying && v.paused && !document.hidden) {
              try { v.play(); } catch (_) {}
            }
          }, 500);
        });
      }
    };

    if (v.readyState >= 2) {
      doPlay();
    } else {
      if (v.readyState === 0) { try { v.load(); } catch (_) {} }
      v.addEventListener('canplay', doPlay, { once: true });
    }
  }

  function stopVidInView(v) {
    if (!v || !v.__peAutoVidPlaying) return;
    v.__peAutoVidPlaying = false;
    try { v.pause(); } catch (_) {}
  }

  function observeVideo(v) {
    if (!_vidAutoIO || !v || v.__peAutoVidObs) return;
    if (!isRealVideo(v)) return;
    v.__peAutoVidObs = true;
    _vidAutoIO.observe(v);
  }

  function observeVideos(root = document) {
    if (!_vidAutoIO) return;
    if (root.tagName === 'VIDEO') observeVideo(root);
    if (root.querySelectorAll) root.querySelectorAll('video').forEach(observeVideo);
  }

  function initVideoAutoPlay() {
    if (_vidAutoIO) return;
    _vidAutoIO = new IntersectionObserver(entries => {
      if (!get('videoAutoPlay')) return;
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          if (document.hidden) { _vidAutoPending.add(entry.target); return; }
          startVidInView(entry.target);
        } else {
          _vidAutoPending.delete(entry.target);
          stopVidInView(entry.target);
        }
      });
    }, { threshold: 0.25 });

    observeVideos();
    _vidAutoMO = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      records.forEach(r => r.addedNodes.forEach(n => {
        if (n && n.nodeType === 1) observeVideos(n);
      }));
    });
    _vidAutoMO.observe(document.documentElement, { childList: true, subtree: true });
  }

  function stopVideoAutoPlay() {
    if (_vidAutoIO) { _vidAutoIO.disconnect(); _vidAutoIO = null; }
    if (_vidAutoMO) { _vidAutoMO.disconnect(); _vidAutoMO = null; }
    _vidAutoPending.clear();
    document.querySelectorAll('video').forEach(v => {
      stopVidInView(v);
      v.__peAutoVidObs = false;
    });
  }

  document.addEventListener('visibilitychange', () => {
    if (!get('videoAutoPlay')) return;
    if (document.hidden) {
      document.querySelectorAll('video').forEach(stopVidInView);
    } else if (_vidAutoIO) {
      _vidAutoPending.forEach(v => {
        const r = v.getBoundingClientRect();
        if (r.top < window.innerHeight && r.bottom > 0) startVidInView(v);
      });
      _vidAutoPending.clear();
      document.querySelectorAll('video').forEach(v => {
        if (!v.__peAutoVidObs) return;
        const r = v.getBoundingClientRect();
        if (r.top < window.innerHeight && r.bottom > 0) startVidInView(v);
      });
    }
  });


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: VIDEO MUTE-STATE PRESERVATION + INFINITE LOOP
  // ═══════════════════════════════════════════════════════════════════
  // Pinterest resets `muted = true` when a closeup video replays via the
  // "Watch again" button. We track the user's last explicit unmute via
  // volumechange, then restore it on play. Always on — fixes native bug.
  //
  // initInfiniteLoopVideo: opt-in setting that sets video.loop=true on
  // every real closeup video so the "Watch again" overlay never appears.

  // Desktop closeup containers PLUS the mobile/story closeup containers.
  // The mobile pin-closeup wraps the <video> in visual-content-container /
  // story-pin-video-block / [data-video-signature] (NOT the desktop
  // closeup-visual-container), so without these the loop flag was never
  // applied on mobile and Pinterest's "Watch again / Share" end-screen showed.
  const CLOSEUP_VIDEO_SELECTOR =
    '[data-test-id="closeup-visual-container"], ' +
    '[data-test-id="closeup-video-with-visibility"], ' +
    '[data-test-id="visual-content-container"], ' +
    '[data-test-id="story-pin-video-block"], ' +
    '[data-test-id="closeup-body-image-container"], ' +
    '[data-test-id="pin-closeup-image"], ' +
    '[data-video-signature]';

  let _loopVideoObs = null;
  const _loopVideoOriginalState = new Map();

  function applyLoopFlagToVideo(v) {
    if (!v || v.tagName !== 'VIDEO') return;
    if (!isRealVideo(v)) return;
    if (!v.closest || !v.closest(CLOSEUP_VIDEO_SELECTOR)) return;
    if (!_loopVideoOriginalState.has(v)) _loopVideoOriginalState.set(v, v.loop);
    if (v.loop) return;
    try { v.loop = true; } catch (_) {}
  }

  function applyLoopFlagToAllVideos(root) {
    const scope = root || document;
    if (!scope.querySelectorAll) return;
    scope.querySelectorAll('video').forEach(applyLoopFlagToVideo);
  }

  function trackCloseupVideoMuteState() {
    // Snapshot mute state at end. Pinterest's "Watch again" handler resets
    // `muted = true` *before* the next play event fires, so reading the
    // current value at play() time is too late — we must capture on ended.
    document.addEventListener('ended', e => {
      const v = e.target;
      if (!v || v.tagName !== 'VIDEO' || !isRealVideo(v)) return;
      v.__peWasUnmutedBeforeEnd = !v.muted;
    }, true);

    document.addEventListener('play', e => {
      const v = e.target;
      if (!v || v.tagName !== 'VIDEO' || !isRealVideo(v)) return;
      if (v.__peWasUnmutedBeforeEnd && v.muted) {
        try { v.muted = false; } catch (_) {}
      }
      v.__peWasUnmutedBeforeEnd = false;
    }, true);
  }

  // Native video.loop is the primary mechanism, but Pinterest's mobile player
  // drives playback through its own React/HLS layer and can pause at the end
  // (showing the "Watch again / Share" end-screen) instead of honoring the
  // attribute — in that case an `ended` event still fires. Force a replay so
  // the closeup video loops regardless of how Pinterest stopped it.
  let _loopEndedBound = false;

  function bindLoopEndedFallback() {
    if (_loopEndedBound) return;
    _loopEndedBound = true;
    document.addEventListener('ended', e => {
      if (!get('infiniteLoopVideo')) return;
      const v = e.target;
      if (!v || v.tagName !== 'VIDEO' || !isRealVideo(v)) return;
      if (!v.closest || !v.closest(CLOSEUP_VIDEO_SELECTOR)) return;
      try {
        v.loop = true;
        v.currentTime = 0;
        const p = v.play();
        if (p && p.catch) p.catch(() => {});
      } catch (_) {}
    }, true);
  }

  function initInfiniteLoopVideo() {
    applyLoopFlagToAllVideos();
    bindLoopEndedFallback();
    if (_loopVideoObs) return;
    _loopVideoObs = new MutationObserver(records => {
      for (const r of records) {
        for (const n of r.addedNodes) {
          if (!n || n.nodeType !== 1) continue;
          if (n.tagName === 'VIDEO') applyLoopFlagToVideo(n);
          else applyLoopFlagToAllVideos(n);
        }
      }
    });
    _loopVideoObs.observe(document.documentElement, { childList: true, subtree: true });
  }

  function stopInfiniteLoopVideo() {
    if (_loopVideoObs) { _loopVideoObs.disconnect(); _loopVideoObs = null; }
    _loopVideoOriginalState.forEach((wasLooping, v) => {
      try { v.loop = wasLooping; } catch (_) {}
    });
    _loopVideoOriginalState.clear();
  }

  function applyInfiniteLoopVideoToggle() {
    const on = !!get('infiniteLoopVideo');
    document.body && document.body.classList.toggle('pe-loop-video', on);
  }

  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: DECLUTTER  (no ads, no shopping, no blank spaces)
  // ═══════════════════════════════════════════════════════════════════
  // Collapses unwanted elements to zero size instead of display:none
  // so the masonry grid reflows cleanly with no empty slots.
  // Sets grid-auto-flow:dense on pin-list containers once per container.

  function collapseEl(el) {
    if (!el) return;
    el.style.setProperty('height',     '0',       'important');
    el.style.setProperty('width',      '0',       'important');
    el.style.setProperty('margin',     '0',       'important');
    el.style.setProperty('padding',    '0',       'important');
    el.style.setProperty('border',     'none',    'important');
    el.style.setProperty('overflow',   'hidden',  'important');
    el.style.setProperty('opacity',    '0',       'important');
    el.style.setProperty('min-height', '0',       'important');
    el.style.setProperty('min-width',  '0',       'important');
    // Make the parent grid fill the gap
    const grid = el.closest('div[role="list"]');
    if (grid && !grid.dataset.peDense) {
      grid.style.setProperty('grid-auto-flow', 'dense', 'important');
      grid.dataset.peDense = '1';
    }
  }

  const SHOP_THE_LOOK_DIRECT_SELECTORS = [
    '[data-test-id="duplo-shopping-module"]',
    '[data-test-id="ShopTheLookSimilarProducts"]',
    '[data-test-id="visual-search-shopping-bar"]',
    '[data-test-id="related-products"]',
    '[data-test-id="ShopTheLookAnnotations"]',
    '[data-test-id="shopping-module"]',
  ];

  function isShopTheLookDeclutterEnabled() {
    return !!get('declutter') && !!get('declutterShopTheLook');
  }

  function isSafeShopTheLookRoot(el) {
    if (!el || el === document.body || el === document.documentElement) return false;
    if (el.matches?.('[data-test-id="closeup-body"], [data-test-id="closeup-body-style"], [data-test-id="closeup-lego-container"], [data-test-id="description-content-container"]')) return false;
    if (el.querySelector?.('[data-test-id="closeup-visual-container"]') && el.querySelector?.('[data-test-id="description-content-container"]')) return false;
    return true;
  }

  function textLooksLikeShopTheLook(text) {
    return /^(shop the look|shop similar|shop products|more products)\b/i.test(String(text || '').trim());
  }

  function getShopTheLookModuleRoot(el) {
    if (!el || el.nodeType !== 1) return null;
    const direct = el.closest?.(SHOP_THE_LOOK_DIRECT_SELECTORS.join(','));
    if (direct) {
      if (direct.matches('[data-test-id="shopping-module"]')) {
        return direct.closest('div[role="listitem"]') || direct.closest('[data-grid-item="true"]') || direct;
      }
      if (direct.parentElement && direct.parentElement.children.length === 1 && isSafeShopTheLookRoot(direct.parentElement)) {
        return direct.parentElement;
      }
      return direct;
    }

    const titleNode = el.matches?.('div[title="Shop the look"]') ? el : el.querySelector?.('div[title="Shop the look"]');
    if (titleNode) {
      const titleRoot = titleNode.closest('[data-test-id="duplo-shopping-module"], [data-test-id="collapsible-layout"]');
      if (titleRoot && isSafeShopTheLookRoot(titleRoot)) return titleRoot;
    }

    const headings = [
      ...(el.matches?.('h2,[data-test-id="collapsible-header"]') ? [el] : []),
      ...Array.from(el.querySelectorAll?.('h2,[data-test-id="collapsible-header"]') || []),
    ];
    const shopHeading = headings.find(node => textLooksLikeShopTheLook(node.textContent));
    if (!shopHeading) return null;

    const moduleRoot = shopHeading.closest('[data-test-id="duplo-shopping-module"]');
    if (moduleRoot && isSafeShopTheLookRoot(moduleRoot)) return moduleRoot;

    const layout = shopHeading.closest('[data-test-id="collapsible-layout"]');
    if (layout && isSafeShopTheLookRoot(layout.parentElement)) return layout.parentElement;
    if (layout && isSafeShopTheLookRoot(layout)) return layout;
    return null;
  }

  function collapseShopTheLookModule(el) {
    const root = getShopTheLookModuleRoot(el);
    if (!root || root.__peShopTheLookHidden) return false;
    root.__peShopTheLookHidden = true;
    collapseEl(root);
    return true;
  }

  function hideShopTheLookModules(root = document) {
    if (!isShopTheLookDeclutterEnabled()) return false;
    const scope = root?.nodeType === 1 ? root : document;
    let matched = false;
    if (scope.matches?.(SHOP_THE_LOOK_DIRECT_SELECTORS.join(',')) || scope.matches?.('div[title="Shop the look"], h2, [data-test-id="collapsible-header"]')) {
      matched = collapseShopTheLookModule(scope) || matched;
    }
    scope.querySelectorAll?.(`${SHOP_THE_LOOK_DIRECT_SELECTORS.join(',')}, div[title="Shop the look"]`).forEach(el => {
      matched = collapseShopTheLookModule(el) || matched;
    });
    if (scope !== document || !isMobilePinCloseupPage()) {
      scope.querySelectorAll?.('h2, [data-test-id="collapsible-header"]').forEach(el => {
        matched = collapseShopTheLookModule(el) || matched;
      });
    }
    return matched;
  }

  function hideDeclutterMobileInlineVisitButtons(root = document) {
    if (!get('declutter')) return false;
    const scope = root?.nodeType === 1 ? root : document;
    const nodes = new Set();
    if (scope.matches?.('[data-test-id="visit-button-mobile-inline"], [data-test-id="main-pin-section-visit-button"]')) {
      nodes.add(scope);
    }
    scope.querySelectorAll?.('[data-test-id="visit-button-mobile-inline"], [data-test-id="main-pin-section-visit-button"]').forEach(el => nodes.add(el));

    let matched = false;
    nodes.forEach(el => {
      const actionContainer = el.closest('[data-test-id="pin-action-bar-container"]');
      const wrapper = actionContainer?.parentElement && actionContainer.parentElement.children.length === 1
        ? actionContainer.parentElement
        : actionContainer;
      collapseEl(wrapper || el);
      matched = true;
    });
    return matched;
  }

  function isDeclutterPin(pin) {
    // Sponsored
    if (pin.querySelector('div[title="Sponsored"]')) return true;
    // Shoppable Pin indicator
    if (pin.querySelector('[aria-label="Shoppable Pin indicator"]')) return true;
    // Shopping cards / "Shop" headings
    const h2 = pin.querySelector('h2#comments-heading');
    if (h2 && h2.textContent.trim().toLowerCase().startsWith('shop')) return true;
    for (const heading of pin.querySelectorAll('h2')) {
      if ((heading.textContent || '').trim().toLowerCase().startsWith('shop')) return true;
    }
    const aLink = pin.querySelector('a');
    if (aLink && (aLink.getAttribute('aria-label') || '').toLowerCase().startsWith('shop')) return true;
    // Featured boards / window shopping promos
    const text = pin.textContent.trim().toLowerCase();
    if (text.startsWith('explore featured boards')) return true;
    if (text.startsWith('still window shopping'))  return true;
    // Quiz posts
    if (/\bquiz\b/i.test(pin.textContent)) return true;
    // Deleted / unavailable pins
    if (pin.querySelector('[data-test-id="unavailable-pin"]')) return true;
    // Product cards with price tags (individual Shop the look items)
    if (pin.querySelector('[data-test-id="product-price-text"]')) return true;
    if (pin.querySelector('[data-test-id="pincard-product-with-link"]')) return true;
    if (pin.querySelector('div[title="Shop the look"]')) return true;
    return false;
  }

  function collapseDeclutterPin(pin) {
    if (!pin || pin.__peDecluttered) return false;
    if (!isDeclutterPin(pin)) return false;
    pin.__peDecluttered = true;
    collapseEl(pin);
    return true;
  }

  function scanDeclutterNode(node) {
    if (!node || node.nodeType !== 1) return false;
    let matched = false;
    matched = hideDeclutterMobileInlineVisitButtons(node) || matched;
    matched = hideShopTheLookModules(node) || matched;
    const closestPin = node.closest?.('div[role="listitem"]');
    if (closestPin) matched = collapseDeclutterPin(closestPin) || matched;
    if (node.matches?.('div[role="listitem"]')) matched = collapseDeclutterPin(node) || matched;
    node.querySelectorAll?.('div[role="listitem"]').forEach(pin => {
      matched = collapseDeclutterPin(pin) || matched;
    });
    return matched;
  }

  function scanDeclutterMutationRecords(records) {
    let matched = false;
    records.forEach(record => {
      if (record.type === 'attributes') {
        matched = hideDeclutterMobileInlineVisitButtons(record.target) || matched;
        matched = hideShopTheLookModules(record.target) || matched;
        matched = collapseDeclutterPin(record.target.closest?.('div[role="listitem"]')) || matched;
        return;
      }
      record.addedNodes.forEach(node => {
        matched = scanDeclutterNode(node) || matched;
      });
    });
    return matched;
  }

  function filterPins(container) {
    if (!get('declutter')) return;
    hideDeclutterMobileInlineVisitButtons(container);
    hideShopTheLookModules(container);
    container.querySelectorAll('div[role="listitem"]').forEach(pin => {
      collapseDeclutterPin(pin);
    });
  }

  function getDirectChildOf(parent, node) {
    let current = node;
    while (current && current.parentElement !== parent) current = current.parentElement;
    return current || null;
  }

  function removeDeclutterOneoffs() {
    if (!get('declutter')) return;
    hideShopTheLookModules(document);
    hideDeclutterMobileInlineVisitButtons(document);
    if (isMobilePinCloseupPage()) return;
    // Shop tab on board tools bar
    document.querySelectorAll('[data-test-id="board-tools"] [data-test-id="Shop"]')
      .forEach(el => collapseEl(el.closest('div')));
    // Shop-by / sf-header banners
    document.querySelectorAll('[data-test-id="sf-header-heading"]').forEach(el => {
      collapseEl(el.closest('div[role="listitem"]') || el.parentElement);
    });
    // Download upsell popover
    document.querySelectorAll('[data-test-id="post-download-upsell-popover"]')
      .forEach(collapseEl);
    // Ad blocker modal
    document.querySelectorAll('div[aria-label="Ad blocker modal"]').forEach(el => {
      collapseEl(el);
      if (document.body.style.overflow === 'hidden') document.body.style.overflow = '';
    });
    // Explore-tab notification badge
    const todayTab = document.querySelector('a[data-test-id="today-tab"]');
    if (todayTab) {
      const iconWrap = todayTab.closest('div');
      const sidebarItem = iconWrap?.parentElement?.parentElement;
      const badge = sidebarItem?.parentElement?.querySelector('.MIw[style*="pointer-events: none"]');
      if (badge) collapseEl(badge);
    }
    // Pin card notification badges (the floating status dot on pins)
    document.querySelectorAll('[aria-label="Notifications"][role="status"]').forEach(el => {
      collapseEl(el.parentElement || el);
    });
    // Shopping spotlight carousel section
    document.querySelectorAll('[data-test-id="carousel-bubble-wrapper-shopping_spotlight"]').forEach(el => {
      collapseEl(el.closest('div[role="listitem"]') || el.parentElement?.parentElement?.parentElement || el.parentElement || el);
    });
    // Shop the look sections that Pinterest renders outside normal pin cards
    document.querySelectorAll('div[title="Shop the look"]').forEach(el => {
      if (collapseShopTheLookModule(el)) return;
      const buttonWrapper = el.closest('[role="button"]');
      collapseEl(buttonWrapper?.parentElement || el.closest('div[role="listitem"]') || el.parentElement || el);
    });
    // Board/search product banners
    document.querySelectorAll('h2').forEach(el => {
      const text = (el.textContent || '').trim().toLowerCase();
      if (!text.startsWith('more products') && !text.startsWith('shop products')) return;
      const baseGrid = el.closest('[data-test-id="base-board-pin-grid"]');
      collapseEl(
        (baseGrid && getDirectChildOf(baseGrid, el)) ||
        el.closest('div[role="listitem"]') ||
        el.closest('[data-grid-item="true"]') ||
        el.parentElement ||
        el
      );
    });
    // Curated spotlight section (search page immersive header carousel)
    document.querySelectorAll('[data-test-id="search-story-suggestions-container"]:has([data-test-id="search-suggestion-curated-board-bubble"])').forEach(el => {
      collapseEl(el);
    });
    // Shop similar / Shop the look sections on pin closeup
    document.querySelectorAll(
      '[data-test-id="ShopTheLookSimilarProducts"],' +
      '[data-test-id="visual-search-shopping-bar"],' +
      '[data-test-id="related-products"],' +
      '[data-test-id="ShopTheLookAnnotations"]'
    ).forEach(el => {
      if (collapseShopTheLookModule(el)) return;
      collapseEl(el.closest('div[role="listitem"]') || el.parentElement || el);
    });
    // Shop the look carousel grid items (full-width shopping module in feed)
    document.querySelectorAll('[data-test-id="shopping-module"]').forEach(el => {
      if (collapseShopTheLookModule(el)) return;
      collapseEl(el.closest('div[role="listitem"]') || el.closest('[data-grid-item="true"]') || el.parentElement || el);
    });
  }

  let _declutterListObs = null;

  function initDeclutter() {
    if (!get('declutter')) return;

    // Observe the pin grid list(s) for new list items
    function attachListObserver(listEl) {
      if (listEl.__peDeclutterObs) return;
      listEl.__peDeclutterObs = true;
      filterPins(listEl);
      const onMutate = IS_MOBILE ? debounce(() => filterPins(listEl), 200) : () => filterPins(listEl);
      new MutationObserver(records => {
        if (hasOnlyPowerMenuMutations(records)) return;
        const matched = scanDeclutterMutationRecords(records);
        if (matched) return;
        onMutate();
      })
        .observe(listEl, {
          childList: true,
          subtree: true,
          attributes: true,
          attributeFilter: ['title', 'aria-label', 'data-test-id'],
        });
    }

    // Attach to any already-present lists
    document.querySelectorAll('div[role="list"]').forEach(attachListObserver);
    removeDeclutterOneoffs();

    // Watch for new lists added by SPA navigation or lazy load
    if (_declutterListObs) return;
    _declutterListObs = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      document.querySelectorAll('div[role="list"]').forEach(attachListObserver);
      removeDeclutterOneoffs();
    });
    _declutterListObs.observe(document.documentElement, { childList: true, subtree: true });
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: REMOVE VIDEOS (collapse to avoid blank spaces)
  // ═══════════════════════════════════════════════════════════════════
  // Detects video pins via their duration label (PinTypeIdentifier)
  // and collapses them using the same technique as Declutter to
  // avoid blank spaces in the grid.

  function isVideoPin(pin) {
    // PinTypeIdentifier badge appears on both GIFs and videos — check its text
    const badge = pin.querySelector('[data-test-id="PinTypeIdentifier"]');
    if (badge) {
      const t = badge.textContent.trim().toLowerCase();
      if (t === 'gif' || t.includes('animated')) return false; // it's a GIF, not a video
      if (t === 'video' || t.includes('watch')) return true;
    }
    // <video> elements: GIFs use i.pinimg.com, real videos use v.pinimg.com
    const vid = pin.querySelector('video');
    if (vid) {
      const src = vid.src
        || (vid.querySelector('source') && vid.querySelector('source').src)
        || '';
      if (/v\.pinimg\.com/i.test(src)) return true;  // Pinterest-hosted video
      if (/i\.pinimg\.com/i.test(src)) return false; // GIF rendered as video
      // Unknown CDN (e.g. YouTube embed inside an iframe) — treat as video
      if (src) return true;
    }
    // Explicit video-only indicators
    if (pin.querySelector('[data-test-id="video-pin-indicator"], [data-test-id="PinVideoIdentifier"]')) return true;
    return false;
  }

  function filterVideoPins(container) {
    if (!get('removeVideos')) return;
    container.querySelectorAll('div[role="listitem"]').forEach(pin => {
      if (!pin.__peVideoRemoved && isVideoPin(pin)) {
        pin.__peVideoRemoved = true;
        collapseEl(pin);
      }
    });
  }

  let _removeVideosObs = null;

  function initRemoveVideos() {
    if (!get('removeVideos') || _removeVideosObs) return;

    function attachListObserver(listEl) {
      if (listEl.__peVideoObs) return;
      listEl.__peVideoObs = true;
      filterVideoPins(listEl);
      const onMutate = IS_MOBILE ? debounce(() => filterVideoPins(listEl), 200) : () => filterVideoPins(listEl);
      new MutationObserver(records => {
        if (hasOnlyPowerMenuMutations(records)) return;
        onMutate();
      })
        .observe(listEl, { childList: true, subtree: true });
    }

    document.querySelectorAll('div[role="list"]').forEach(attachListObserver);

    _removeVideosObs = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      document.querySelectorAll('div[role="list"]').forEach(attachListObserver);
    });
    _removeVideosObs.observe(document.documentElement, { childList: true, subtree: true });
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: HIDE SHOP POSTS (TeePublic, Redbubble, AliExpress, etc.)
  // ═══════════════════════════════════════════════════════════════════
  const SHOP_DOMAINS = [
    'teepublic.com', 'redbubble.com',
    'aliexpress.com', 'aliexpress.us', 'aliexpress.ru',
    'amazon.com', 'amazon.co.uk', 'amazon.ca', 'amazon.com.au', 'amazon.de',
    'etsy.com',
    'ebay.com', 'ebay.co.uk', 'ebay.ca', 'ebay.com.au',
  ];

  function isShopPost(pin) {
    const links = pin.querySelectorAll('a[href]');
    for (const a of links) {
      const href = (a.href || '').toLowerCase();
      if (SHOP_DOMAINS.some(d => href.includes(d))) return true;
    }
    const text = (pin.textContent || '').toLowerCase();
    return ['teepublic', 'redbubble', 'aliexpress', 'amazon', 'etsy', 'ebay'].some(name => text.includes(name));
  }

  const _hiddenShopPosts = new Map();
  const _hideShopPostListObservers = new Map();
  let _hideShopPostsObs = null;

  function hideShopPin(pin) {
    if (pin.__peShopHidden) return;
    pin.__peShopHidden = true;
    _hiddenShopPosts.set(pin, {
      display: pin.style.display,
      visibility: pin.style.visibility,
      height: pin.style.height,
      minHeight: pin.style.minHeight,
      overflow: pin.style.overflow,
    });
    collapseEl(pin);
  }

  function restoreShopPosts() {
    _hiddenShopPosts.forEach((style, pin) => {
      if (!pin || !pin.style) return;
      pin.style.display = style.display;
      pin.style.visibility = style.visibility;
      pin.style.height = style.height;
      pin.style.minHeight = style.minHeight;
      pin.style.overflow = style.overflow;
      delete pin.__peShopHidden;
    });
    _hiddenShopPosts.clear();
  }

  function filterShopPosts(container) {
    if (!get('hideShopPosts') || !get('declutter')) return;
    container.querySelectorAll('div[role="listitem"]').forEach(pin => {
      if (isShopPost(pin)) hideShopPin(pin);
    });
  }

  function attachShopPostListObserver(listEl) {
    if (_hideShopPostListObservers.has(listEl)) return;
    filterShopPosts(listEl);
    const onMutate = IS_MOBILE ? debounce(() => filterShopPosts(listEl), 200) : () => filterShopPosts(listEl);
    const obs = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      onMutate();
    });
    obs.observe(listEl, { childList: true, subtree: true });
    _hideShopPostListObservers.set(listEl, obs);
  }

  function stopHideShopPosts({ restore = true } = {}) {
    if (_hideShopPostsObs) { _hideShopPostsObs.disconnect(); _hideShopPostsObs = null; }
    _hideShopPostListObservers.forEach((obs, listEl) => {
      obs.disconnect();
      if (listEl) delete listEl.__peShopObs;
    });
    _hideShopPostListObservers.clear();
    if (restore) restoreShopPosts();
  }

  function initHideShopPosts() {
    if (!get('hideShopPosts') || !get('declutter')) return;

    document.querySelectorAll('div[role="list"]').forEach(attachShopPostListObserver);

    if (_hideShopPostsObs) return;
    _hideShopPostsObs = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      document.querySelectorAll('div[role="list"]').forEach(attachShopPostListObserver);
    });
    _hideShopPostsObs.observe(document.documentElement, { childList: true, subtree: true });
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: HIDE COMMENTS
  // ═══════════════════════════════════════════════════════════════════
  function hideCommentEditorWrapper() {
    if (!get('hideComments')) return;
    // Walk up from the known comment editor container ID to find
    // its bordered outer wrapper and hide the whole thing
    ['dweb-comment-editor-container', 'mweb-comment-editor-container'].forEach(id => {
      const el = document.getElementById(id);
      if (!el) return;
      let p = el.parentElement;
      for (let i = 0; i < 10 && p && p !== document.body; i++) {
        const style = p.getAttribute('style') || '';
        if (style.includes('border-color')) {
          p.style.setProperty('display', 'none', 'important');
          return;
        }
        p = p.parentElement;
      }
      el.style.setProperty('display', 'none', 'important');
    });
    // Hide mobile comment preview ("View all comments" text + snippet above it)
    document.querySelectorAll('div,span,a').forEach(el => {
      if (!el.children.length && /^view all comments$/i.test(el.textContent.trim())) {
        const container = el.parentElement && el.parentElement.parentElement;
        if (container && container !== document.body) {
          container.style.setProperty('display', 'none', 'important');
        } else if (el.parentElement) {
          el.parentElement.style.setProperty('display', 'none', 'important');
        }
      }
    });
  }

  let _hideCommentsObs = null;
  const scheduleHideComments = debounce(hideCommentEditorWrapper, 150);

  function initHideComments() {
    if (!get('hideComments')) return;
    hideCommentEditorWrapper();
    if (_hideCommentsObs) return;
    _hideCommentsObs = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      scheduleHideComments();
    });
    _hideCommentsObs.observe(document.documentElement, { childList: true, subtree: true });
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: AUTO TRANSLATE
  // ═══════════════════════════════════════════════════════════════════
  const CLOSEUP_TITLE_SELECTORS = [
    '[data-test-id="closeup-title"] h1',
    '[data-test-id="closeup-title-card"] h1',
    '[data-test-id="pin-title-wrapper"] h1',
    '[data-test-id="pin-title"]',
  ].join(',');

  const PIN_CARD_TITLE_SELECTORS = [
    '[data-test-id="pinrep-title"]',
    '[data-test-id="pinrep-title"] span',
    '[data-test-id="pinrep-title"] div',
    '[data-test-id="grid-item"] [data-test-id="pin-title"]',
    '[data-grid-item-idx] [data-test-id="pin-title"]',
    '[data-test-id="pin"] [data-test-id="pin-title"]',
    '[data-test-id="pinWrapper"] [data-test-id="pin-title"]',
  ].join(',');

  const TITLE_TRANSLATE_SELECTORS = [
    '[data-test-id="closeup-title"] h1',
    '[data-test-id="closeup-title-card"] h1',
    '[data-test-id="pin-title-wrapper"] h1',
    '[data-test-id="pin-title"]',
    PIN_CARD_TITLE_SELECTORS,
  ].join(',');

  const CLOSEUP_DESCRIPTION_SELECTORS = [
    '[data-test-id="description-content-container"] [data-test-id="text-container"]',
    '[data-test-id="rich-pin-information"] [data-test-id="text-container"]',
    '[data-test-id="pin-closeup-description"]',
    '[data-test-id="closeup-description"]',
    '[data-test-id="pin-description"]',
  ].join(',');

  const PIN_CARD_DESCRIPTION_SELECTORS = [
    '[data-test-id="pinrep-description"]',
    '[data-test-id="pinrep-description"] span',
    '[data-test-id="pinrep-description"] div',
    '[data-test-id="grid-item"] [data-test-id="pin-description"]',
    '[data-grid-item-idx] [data-test-id="pin-description"]',
    '[data-test-id="pin"] [data-test-id="pin-description"]',
    '[data-test-id="pinWrapper"] [data-test-id="pin-description"]',
  ].join(',');

  const DESCRIPTION_TRANSLATE_SELECTORS = [
    '[data-test-id="description-content-container"] [data-test-id="text-container"]',
    '[data-test-id="rich-pin-information"] [data-test-id="text-container"]',
    '[data-test-id="pin-closeup-description"]',
    '[data-test-id="closeup-description"]',
    '[data-test-id="pin-description"]',
    PIN_CARD_DESCRIPTION_SELECTORS,
  ].join(',');

  const COMMENT_TRANSLATE_SELECTORS = [
    '[data-test-id="commentThread-comment"] [data-test-id="text-container"]',
  ].join(',');

  const AUTO_TRANSLATE_SELECTORS = [
    TITLE_TRANSLATE_SELECTORS,
    DESCRIPTION_TRANSLATE_SELECTORS,
    COMMENT_TRANSLATE_SELECTORS,
  ].join(',');

  const _translateCache = new Map();
  const _translateQueue = [];
  const TRANSLATE_MAX_CONCURRENT = IS_MOBILE ? 2 : 4;
  const TRANSLATE_CONSERVATIVE_COMMENT_LIMIT = IS_MOBILE ? 1 : 2;
  let _translateActive = 0;
  let _autoTranslateIO = null;
  let _autoTranslateMO = null;
  let _autoTranslateRescan = null;
  let _manualTranslateMO = null;
  let _manualTranslateRescan = null;

  function getAutoTranslateTargetLang() {
    const raw = String(get('autoTranslateTarget') || 'browser').toLowerCase();
    if (raw === 'browser') return USER_LANG || 'en';
    return /^[a-z]{2,3}(?:-[a-z0-9]+)?$/i.test(raw) ? raw.split('-')[0] : 'en';
  }

  function isSameLanguage(detectedLanguage, targetLanguage) {
    const detected = String(detectedLanguage || '').split('-')[0].toLowerCase();
    const target = String(targetLanguage || '').split('-')[0].toLowerCase();
    return !!detected && !!target && detected === target;
  }

  function hasAnyAutoTranslateEnabled() {
    return !!(get('autoTranslateTitles') || get('autoTranslateDescriptions') || get('autoTranslateComments'));
  }

  function getTranslateElementType(el) {
    if (!el?.matches) return null;
    if (el.matches(TITLE_TRANSLATE_SELECTORS)) return 'title';
    if (el.matches(COMMENT_TRANSLATE_SELECTORS)) return 'comment';
    if (el.matches(DESCRIPTION_TRANSLATE_SELECTORS)) return 'description';
    if (el.closest?.('[data-test-id="commentThread-comment"]') && el.matches('[data-test-id="text-container"]')) return 'comment';
    if (el.closest?.('[data-test-id="closeup-title-card"], [data-test-id="pin-title-wrapper"], [data-test-id="pinrep-title"]')) return 'title';
    if (el.closest?.('[data-test-id="description-content-container"], [data-test-id="rich-pin-information"], [data-test-id="pinrep-description"]')) return 'description';
    return null;
  }

  function isAutoTranslateEnabledForType(type) {
    if (type === 'title') return get('autoTranslateTitles');
    if (type === 'description') return get('autoTranslateDescriptions');
    if (type === 'comment') return get('autoTranslateComments');
    return false;
  }

  function shouldShowManualTranslateForType(type) {
    return !!get('showManualTranslateButtons') && !!type;
  }

  function isElementActuallyVisible(el) {
    if (!el || !el.isConnected) return false;
    if (el.closest('[hidden], [aria-hidden="true"]')) return false;
    const style = getComputedStyle(el);
    if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
    const rect = el.getBoundingClientRect();
    return rect.width > 0 && rect.height > 0 &&
      rect.bottom > 0 && rect.right > 0 &&
      rect.top < window.innerHeight && rect.left < window.innerWidth;
  }

  function findCommentToggleForList(list) {
    if (!list?.id) return document.querySelector('[data-test-id="canonical-card-tap-area"][aria-expanded]');
    for (const el of document.querySelectorAll('[aria-controls]')) {
      if (el.getAttribute('aria-controls') === list.id) return el;
    }
    return document.querySelector('[data-test-id="canonical-card-tap-area"][aria-expanded]');
  }

  function isCommentElementTranslatable(el) {
    const comment = el.closest('[data-test-id="commentThread-comment"]');
    if (!comment) return true;
    if (get('hideComments')) return false;
    const list = el.closest('[data-test-id="aggregated-comment-list"]');
    if (!list || !isElementActuallyVisible(list)) return false;
    const toggle = findCommentToggleForList(list);
    if (toggle && toggle.getAttribute('aria-expanded') === 'false') return false;
    return isElementActuallyVisible(comment);
  }

  function normalizeTranslateText(el) {
    if (!el || isTranslateCandidateExcluded(el) || el.closest('[contenteditable="true"], textarea, input, [data-test-id="comment-editor-container"], #dweb-comment-editor-container, #mweb-comment-editor-container')) {
      return null;
    }
    const text = (el.textContent || '').replace(/\s+/g, ' ').trim();
    if (text.length < 3 || text.length > 800) return null;
    if (/^https?:\/\//i.test(text)) return null;
    if (/^[\d\s.,:;!?()[\]{}'"`~_\-+%#/@$&|]+$/.test(text)) return null;
    if (/^(save|reply|share|more|comments?|view larger|search image)$/i.test(text)) return null;
    return text;
  }

  function isTranslateCandidateExcluded(el) {
    if (!el || !el.isConnected) return true;
    if (el.closest(
      '#pe-settings-wrap, #pe-bd-fab, #pe-ctx-menu, ' +
      '.pe-manual-translate-btn, .pe-manual-translate-mount, ' +
      '[contenteditable="true"], textarea, input, select, option, ' +
      '[data-test-id="closeup-action-items"], [data-test-id="pin-action-bar"], ' +
      '[data-test-id="creator-card-profile"], [data-test-id="creator-avatar"], ' +
      '[data-test-id="creator-profile-link"], [data-test-id="creator-profile-name"], ' +
      '[data-test-id="comment-editor-container"], #dweb-comment-editor-container, #mweb-comment-editor-container'
    )) return true;
    if (el.matches('button, [role="button"], [role="menuitem"], svg, path')) return true;
    const knownText = el.matches(TITLE_TRANSLATE_SELECTORS + ',' + DESCRIPTION_TRANSLATE_SELECTORS + ',' + COMMENT_TRANSLATE_SELECTORS) ||
      el.closest('[data-test-id="pinrep-title"], [data-test-id="pinrep-description"], [data-test-id="pin-title-wrapper"], [data-test-id="description-content-container"], [data-test-id="rich-pin-information"]');
    const control = el.closest('button, [role="button"], [role="menuitem"], [aria-haspopup="true"]');
    if (control && !knownText && !el.closest('[data-test-id="commentThread-comment"]')) return true;
    return false;
  }

  function rememberTranslation(key, value) {
    if (_translateCache.has(key)) _translateCache.delete(key);
    _translateCache.set(key, value);
    while (_translateCache.size > 500) {
      const oldest = _translateCache.keys().next().value;
      _translateCache.delete(oldest);
    }
  }

  function normalizeTranslationResponse(text, target, responseText) {
    let translatedText = text;
    let detectedLanguage = '';
    try {
      const data = JSON.parse(responseText);
      const parts = Array.isArray(data?.[0]) ? data[0] : [];
      translatedText = parts.map(part => Array.isArray(part) ? part[0] : '').join('').trim() || text;
      detectedLanguage = String(data?.[2] || data?.[8]?.[0]?.[0] || '').toLowerCase();
    } catch (_) {}

    const result = {
      translatedText,
      detectedLanguage,
      targetLanguage: target,
      status: 'translated',
    };
    if (isSameLanguage(result.detectedLanguage, target)) {
      return { ...result, translatedText: text, status: 'same-language' };
    }
    if (!translatedText || translatedText === text) return { ...result, translatedText: text, status: 'unchanged' };
    return result;
  }

  function requestTranslation(text) {
    const target = getAutoTranslateTargetLang();
    const key = `${target}\n${text}`;
    if (_translateCache.has(key)) return Promise.resolve(_translateCache.get(key));

    return new Promise(resolve => {
      const url = 'https://translate.googleapis.com/translate_a/single' +
        '?client=gtx&sl=auto&tl=' + encodeURIComponent(target) +
        '&dt=t&q=' + encodeURIComponent(text);
      GM_xmlhttpRequest({
        method: 'GET',
        url,
        timeout: 10000,
        onload: r => {
          const result = normalizeTranslationResponse(text, target, r.responseText);
          rememberTranslation(key, result);
          resolve(result);
        },
        onerror: () => {
          resolve({ translatedText: text, detectedLanguage: '', targetLanguage: target, status: 'error' });
        },
        ontimeout: () => {
          resolve({ translatedText: text, detectedLanguage: '', targetLanguage: target, status: 'error' });
        },
      });
    });
  }

  function applyTitleTranslation(el, original, translated) {
    const displayMode = get('titleTranslationDisplay');
    if (displayMode === 'both') {
      el.textContent = translated;
      const originalLine = document.createElement('span');
      originalLine.className = 'pe-title-original-line';
      originalLine.textContent = original;
      el.appendChild(originalLine);
      return;
    }
    el.classList.add('pe-title-mode-translated-only');
    el.textContent = translated;
  }

  function applyTranslatedText(el, original, result, options = {}) {
    if (!result) return 'retry';
    const type = options.type || getTranslateElementType(el);
    const source = options.source || 'auto';
    const target = result?.targetLanguage || getAutoTranslateTargetLang();
    if (isSameLanguage(result?.detectedLanguage, target)) {
      result.status = 'same-language';
      return 'done';
    }
    const translated = result?.translatedText || '';
    if (!translated || translated === original || result.status === 'unchanged') return 'done';
    if (result.status === 'error') return 'retry';
    if (result.status !== 'translated') return 'retry';
    if (source !== 'manual' && !isAutoTranslateEnabledForType(type)) return 'done';
    if (!el.isConnected) return 'retry';
    if (!isElementActuallyVisible(el) || !isCommentElementTranslatable(el)) return 'retry';
    const current = normalizeTranslateText(el);
    if (current !== original) return 'retry';
    el.setAttribute('data-pe-auto-translate-original', original);
    el.setAttribute('data-pe-auto-translate-title', el.getAttribute('title') || '');
    el.setAttribute('data-pe-auto-translate-kind', type || '');
    if (type === 'title') applyTitleTranslation(el, original, translated);
    else el.textContent = translated;
    el.title = 'Original: ' + original;
    el.classList.add('pe-translated-text');
    removeManualTranslateButtonFor(el);
    return 'done';
  }

  function getTranslateConcurrencyLimit(item) {
    if (item?.type === 'comment' && get('autoTranslateCommentMode') === 'conservative') {
      return Math.min(TRANSLATE_MAX_CONCURRENT, TRANSLATE_CONSERVATIVE_COMMENT_LIMIT);
    }
    return TRANSLATE_MAX_CONCURRENT;
  }

  function finishQueuedTranslation(item, state) {
    if (item?.el?.isConnected) item.el.__peTranslateState = state === 'done' ? 'done' : null;
    _translateActive--;
    pumpTranslateQueue();
  }

  function pumpTranslateQueue() {
    while (_translateQueue.length) {
      const item = _translateQueue[0];
      if (_translateActive >= getTranslateConcurrencyLimit(item)) return;
      _translateQueue.shift();
      if (!item?.el?.isConnected) continue;
      if (item.source !== 'manual' && !isAutoTranslateEnabledForType(item.type)) {
        item.el.__peTranslateState = null;
        continue;
      }
      _translateActive++;
      requestTranslation(item.text)
        .then(result => applyTranslatedText(item.el, item.text, result, item))
        .then(state => finishQueuedTranslation(item, state))
        .catch(() => finishQueuedTranslation(item, 'retry'));
    }
  }

  function queueTranslateElement(el, source = 'auto') {
    const type = getTranslateElementType(el);
    if (!type) return;
    if (source !== 'manual' && !isAutoTranslateEnabledForType(type)) return;
    if (!el || el.__peTranslateState === 'queued' || el.__peTranslateState === 'done') return;
    if (el.hasAttribute('data-pe-auto-translate-original')) return;
    if (!isElementActuallyVisible(el) || !isCommentElementTranslatable(el)) return;
    const text = normalizeTranslateText(el);
    if (!text) return;
    el.__peTranslateState = 'queued';
    _translateQueue.push({ el, text, type, source });
    pumpTranslateQueue();
  }

  async function translateElementNow(el, source = 'manual') {
    const type = getTranslateElementType(el);
    if (!type || !el || el.hasAttribute('data-pe-auto-translate-original')) return null;
    if (!isElementActuallyVisible(el) || !isCommentElementTranslatable(el)) return null;
    const text = normalizeTranslateText(el);
    if (!text) return null;
    el.__peTranslateState = 'queued';
    const result = await requestTranslation(text);
    const state = applyTranslatedText(el, text, result, { type, source });
    el.__peTranslateState = state === 'done' ? 'done' : null;
    return result;
  }

  function addTranslateCandidate(nodes, el) {
    if (!el || nodes.has(el)) return;
    if (!getTranslateElementType(el)) return;
    if (!normalizeTranslateText(el)) return;
    nodes.add(el);
  }

  function collectExplicitTranslateCandidates(root) {
    const nodes = new Set();
    if (!root) return [];
    if (root.matches?.(AUTO_TRANSLATE_SELECTORS)) addTranslateCandidate(nodes, root);
    if (root.querySelectorAll) {
      root.querySelectorAll(AUTO_TRANSLATE_SELECTORS).forEach(el => addTranslateCandidate(nodes, el));
    }
    return [...nodes];
  }

  function collectHeuristicTranslateCandidates(root) {
    const nodes = new Set();
    if (!root?.querySelectorAll) return [];
    const closeupContainers = [
      '[data-test-id="closeup-title-card"]',
      '[data-test-id="description-content-container"]',
      '[data-test-id="rich-pin-information"]',
    ].join(',');
    const cardContainers = [
      '[data-test-id="pinrep-title"]',
      '[data-test-id="pinrep-description"]',
      '[data-test-id="grid-item"]',
      '[data-grid-item-idx]',
      '[data-test-id="pin"]',
      '[data-test-id="pinWrapper"]',
    ].join(',');
    const containers = new Set();
    if (root.matches?.(closeupContainers + ',' + cardContainers)) containers.add(root);
    root.querySelectorAll(closeupContainers + ',' + cardContainers).forEach(el => containers.add(el));
    containers.forEach(container => {
      container.querySelectorAll?.(
        'h1, h2, h3, [data-test-id="text-container"], [data-test-id*="title" i], [data-test-id*="description" i], [itemprop="name"], [itemprop="description"]'
      ).forEach(el => addTranslateCandidate(nodes, el));
    });
    return [...nodes];
  }

  function collectTranslateCandidates(root) {
    const nodes = new Set();
    collectExplicitTranslateCandidates(root).forEach(el => nodes.add(el));
    collectHeuristicTranslateCandidates(root).forEach(el => nodes.add(el));
    return [...nodes];
  }

  function scanAutoTranslateCandidates(root) {
    if (!hasAnyAutoTranslateEnabled() || !root) return;
    const nodes = collectTranslateCandidates(root);

    nodes.forEach(el => {
      if (el.__peTranslateObserved || el.hasAttribute('data-pe-auto-translate-original')) return;
      const type = getTranslateElementType(el);
      if (!isAutoTranslateEnabledForType(type)) return;
      el.__peTranslateObserved = true;
      if (_autoTranslateIO) _autoTranslateIO.observe(el);
      else queueTranslateElement(el);
    });
  }

  function restoreAutoTranslations() {
    document.querySelectorAll('[data-pe-auto-translate-original]').forEach(el => {
      el.textContent = el.getAttribute('data-pe-auto-translate-original') || el.textContent;
      const priorTitle = el.getAttribute('data-pe-auto-translate-title') || '';
      if (priorTitle) el.setAttribute('title', priorTitle);
      else el.removeAttribute('title');
      el.removeAttribute('data-pe-auto-translate-original');
      el.removeAttribute('data-pe-auto-translate-title');
      el.removeAttribute('data-pe-auto-translate-kind');
      el.classList.remove('pe-translated-text');
      el.classList.remove('pe-title-mode-translated-only');
      el.__peTranslateState = null;
      el.__peTranslateObserved = null;
    });
  }

  function clearTranslateCandidateState() {
    document.querySelectorAll(AUTO_TRANSLATE_SELECTORS).forEach(el => {
      el.__peTranslateState = null;
      el.__peTranslateObserved = null;
    });
  }

  function stopAutoTranslate() {
    if (_autoTranslateIO) { _autoTranslateIO.disconnect(); _autoTranslateIO = null; }
    if (_autoTranslateMO) { _autoTranslateMO.disconnect(); _autoTranslateMO = null; }
    _translateQueue.length = 0;
    clearTranslateCandidateState();
    restoreAutoTranslations();
  }

  function initAutoTranslate() {
    if (!hasAnyAutoTranslateEnabled()) return;
    if (!_autoTranslateIO && 'IntersectionObserver' in window) {
      _autoTranslateIO = new IntersectionObserver(entries => {
        entries.forEach(entry => {
          if (entry.isIntersecting) queueTranslateElement(entry.target);
        });
      }, { rootMargin: '220px 0px', threshold: 0.01 });
    }
    scanAutoTranslateCandidates(document);
    if (_autoTranslateMO) return;
    _autoTranslateRescan = debounce(() => scanAutoTranslateCandidates(document), IS_MOBILE ? 700 : 300);
    _autoTranslateMO = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      _autoTranslateRescan();
    });
    _autoTranslateMO.observe(document.documentElement, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['aria-expanded'],
    });
  }

  function removeManualTranslateButtonFor(el) {
    if (el?.__peManualTranslateButton) {
      const mount = el.__peManualTranslateButton.__peManualTranslateMount;
      if (mount) mount.remove();
      else el.__peManualTranslateButton.remove();
      el.__peManualTranslateButton = null;
    }
  }

  function createManualTranslateButton(el, type) {
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = `pe-manual-translate-btn pe-manual-translate-${type}`;
    btn.setAttribute('data-pe-ui', 'true');
    btn.setAttribute('aria-label', 'Translate');
    btn.title = 'Translate';
    btn.innerHTML = `
      <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true">
        <path d="M12.87 15.07 10.33 12l.03-.03A17.5 17.5 0 0 0 14.07 5H17V3h-7V1H8v2H1v2h11.17a15.8 15.8 0 0 1-2.82 5.35A15.2 15.2 0 0 1 7.3 7H5.3a17.5 17.5 0 0 0 2.7 5l-5.08 5.02L4.34 18.43 9.33 13.5l3.11 3.73zM17.5 10h-2L11 22h2l1.12-3h4.74L20 22h2zm-2.62 7 1.62-4.33L18.12 17z"/>
      </svg>`;
    btn.addEventListener('click', async e => {
      e.preventDefault();
      e.stopPropagation();
      if (btn.disabled) return;
      btn.disabled = true;
      btn.classList.add('pe-busy');
      try {
        const result = await translateElementNow(el, 'manual');
        if (result?.status === 'translated') removeManualTranslateButtonFor(el);
      } finally {
        if (btn.isConnected) {
          btn.disabled = false;
          btn.classList.remove('pe-busy');
        }
      }
    });
    return btn;
  }

  function createManualTranslateMount(btn, type) {
    const mount = document.createElement('span');
    mount.className = `pe-manual-translate-mount pe-manual-translate-mount-${type}`;
    mount.setAttribute('data-pe-ui', 'true');
    mount.appendChild(btn);
    btn.__peManualTranslateMount = mount;
    return mount;
  }

  function placeManualTranslateButton(el, btn) {
    const mount = createManualTranslateMount(btn, getTranslateElementType(el));
    const title = getTranslateElementType(el) === 'title';
    if (title && el.parentElement) {
      el.insertAdjacentElement('afterend', mount);
      return;
    }
    el.insertAdjacentElement('afterend', mount);
  }

  function scanManualTranslateCandidates(root) {
    if (!get('showManualTranslateButtons') || !root) return;
    collectTranslateCandidates(root).forEach(el => {
      const type = getTranslateElementType(el);
      if (!shouldShowManualTranslateForType(type) ||
          el.hasAttribute('data-pe-auto-translate-original') ||
          !isElementActuallyVisible(el) ||
          !isCommentElementTranslatable(el)) {
        removeManualTranslateButtonFor(el);
        return;
      }
      if (el.__peManualTranslateButton?.isConnected) return;
      const btn = createManualTranslateButton(el, type);
      el.__peManualTranslateButton = btn;
      placeManualTranslateButton(el, btn);
    });
  }

  function stopManualTranslateButtons() {
    if (_manualTranslateMO) { _manualTranslateMO.disconnect(); _manualTranslateMO = null; }
    document.querySelectorAll('.pe-manual-translate-mount').forEach(mount => mount.remove());
    document.querySelectorAll('.pe-manual-translate-btn').forEach(btn => btn.remove());
    document.querySelectorAll(AUTO_TRANSLATE_SELECTORS).forEach(el => { el.__peManualTranslateButton = null; });
  }

  function initManualTranslateButtons() {
    if (!get('showManualTranslateButtons')) {
      stopManualTranslateButtons();
      return;
    }
    scanManualTranslateCandidates(document);
    if (_manualTranslateMO) return;
    _manualTranslateRescan = debounce(() => scanManualTranslateCandidates(document), IS_MOBILE ? 700 : 300);
    _manualTranslateMO = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      _manualTranslateRescan();
    });
    _manualTranslateMO.observe(document.documentElement, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['aria-expanded'],
    });
  }

  function refreshTranslationFeatures() {
    stopAutoTranslate();
    clearTranslateCandidateState();
    if (hasAnyAutoTranslateEnabled()) initAutoTranslate();
    stopManualTranslateButtons();
    if (get('showManualTranslateButtons')) initManualTranslateButtons();
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: SCROLL PRESERVATION
  //  Saves home-feed scroll position when navigating away and restores
  //  it on browser back (popstate).  Does NOT restore on explicit
  //  home-link clicks so fresh home navigation always goes to top.
  // ═══════════════════════════════════════════════════════════════════
  function initScrollPreservation() {
    let _homeScrollY = 0;
    let _homeClickIntent = false;

    // Continuously save scroll Y while on the home feed
    window.addEventListener('scroll', () => {
      if (location.pathname === '/') _homeScrollY = window.scrollY;
    }, { passive: true });

    // When the user explicitly clicks a home nav link, clear saved scroll
    // so that intentional "go home" always scrolls to top
    document.addEventListener('click', e => {
      if (isPowerMenuEvent(e)) return;
      const homeLink = e.target.closest(
        'a[href="/"], [data-test-id="home-tab"], [aria-label="Home"]'
      );
      if (homeLink) {
        _homeClickIntent = true;
        _homeScrollY = 0;
      }
    }, true);

    // On browser back/forward (popstate), restore scroll if returning to home
    window.addEventListener('popstate', () => {
      if (location.pathname === '/' && _homeScrollY > 0 && !_homeClickIntent) {
        // Delay so React finishes rendering the feed before we scroll
        setTimeout(() => window.scrollTo(0, _homeScrollY), 400);
      }
      _homeClickIntent = false;
    });
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: DOWNLOAD FIXER  (original Angel2mp3 logic, intact)
  // ═══════════════════════════════════════════════════════════════════
  function detectFileType(arr) {
    if (arr.length < 12) return '.jpg';
    if (arr[0]===0x89 && arr[1]===0x50 && arr[2]===0x4E && arr[3]===0x47) return '.png';
    if (arr[0]===0xFF && arr[1]===0xD8 && arr[2]===0xFF) return '.jpg';
    if (arr[0]===0x47 && arr[1]===0x49 && arr[2]===0x46 && arr[3]===0x38) return '.gif';
    if (arr[0]===0x52 && arr[1]===0x49 && arr[2]===0x46 && arr[3]===0x46 &&
        arr[8]===0x57 && arr[9]===0x45 && arr[10]===0x42 && arr[11]===0x50) return '.webp';
    if (arr[4]===0x66 && arr[5]===0x74 && arr[6]===0x79 && arr[7]===0x70) return '.mp4';
    return '.jpg';
  }

  function sanitizeFilename(n) {
    if (!n) return null;
    let s = String(n).replace(/[<>:"/\\|?*\x00-\x1f\x80-\x9f]/g, '').trim();
    if (s.length > 200) s = s.slice(0, 200);
    return s.length ? s : null;
  }

  // Remove any trailing known image/video extension from a base name so that
  // the binary-detected extension is always the final (and only) one.
  // e.g. "photo.jpg" → "photo"  |  "photo.jpg.png" → "photo.jpg"  |  "jpg" → "jpg"
  // If stripping would leave an empty string we keep the original to avoid
  // producing a bare extension file (e.g. ".jpg").
  function stripKnownExt(name) {
    if (!name) return name;
    const stripped = name.replace(/\.(jpe?g|png|gif|webp|mp4|bmp|tiff?)$/i, '').trim();
    return stripped.length ? stripped : name;
  }

  function randDigits(len) {
    let r = '';
    for (let i = 0; i < len; i++) r += String(Math.floor(Math.random() * 10));
    return r;
  }

  function makeFallbackPinName() {
    return `Pin - ${randDigits(8)}`;
  }

  const CLOSEUP_PIN_TITLE_SELECTORS = [
    '[data-test-id="closeup-title-card"] h1',
    '[data-test-id="rich-pin-information"] [data-test-id="pin-title-wrapper"] h1',
    '[data-test-id="pin-title-wrapper"] h1',
    '[data-test-id="closeup-title"] h1',
    '[data-test-id="closeup-title"]',
    '[data-test-id="pin-title"] h1',
    '[data-test-id="pin-title"]',
    'h1[itemprop="name"]',
  ];

  const PIN_TITLE_SELECTORS = [
    ...CLOSEUP_PIN_TITLE_SELECTORS,
    '[data-test-id="pinrep-footer-organic-title"] a',
    '[data-test-id="pinrep-footer-organic-title"] h2',
  ];

  function getPinTitleTextFromElement(el) {
    if (!el) return null;
    return sanitizeFilename(
      el.getAttribute?.('data-pe-auto-translate-original') ||
      el.getAttribute?.('title')?.replace(/^Original:\s*/i, '') ||
      el.textContent?.trim()
    );
  }

  function extractPinTitleFromScope(scope, selectors = PIN_TITLE_SELECTORS) {
    if (!scope || !scope.querySelector) return null;
    for (const s of selectors) {
      const el = scope.querySelector(s);
      const t = getPinTitleTextFromElement(el);
      if (t) return t;
    }
    return null;
  }

  function extractPinTitle() {
    return extractPinTitleFromScope(document);
  }

  // Upgrade any pinimg thumbnail URL to /originals/ for max quality
  function upgradeToOriginal(url) {
    if (!url) return url;
    const m = url.match(OQ_RE);
    return m ? m[1] + '/originals' + m[2] : url;
  }

  function getBestCloseupImageUrl(img) {
    if (!img) return null;
    const gifUrl = getGifSrcFromImg(img);
    if (gifUrl) return gifUrl;

    const gifBadge = document.querySelector('[data-test-id="PinTypeIdentifier"]');
    if (gifBadge && /gif|animated/i.test(gifBadge.textContent)) {
      const derived = deriveGifUrl(img.currentSrc || img.src);
      if (derived) return derived;
    }

    const srcset = img.getAttribute('srcset');
    if (srcset) {
      const best = srcset.split(',')
        .map(p => p.trim().split(/\s+/))
        .filter(p => p[0])
        .sort((a, b) => (parseInt(b[1]) || 0) - (parseInt(a[1]) || 0))[0];
      if (best) return upgradeToOriginal(best[0]);
    }
    return upgradeToOriginal(img.currentSrc || img.src);
  }

  function addPinimgUrl(urls, value) {
    if (!value) return;
    String(value).split(/\s*,\s*/).forEach(piece => {
      const url = piece.trim().split(/\s+/)[0].replace(/&amp;/g, '&');
      if (/^https?:\/\/[iv]\d*\.pinimg\.com\//i.test(url) || /^https?:\/\/i\.pinimg\.com\//i.test(url)) {
        urls.add(upgradeToOriginal(url));
      }
    });
  }

  function addPinimgUrlsFromText(urls, text) {
    if (!text) return;
    const re = /https?:\/\/(?:i|v\d*)\.pinimg\.com\/[^"'()<>\s\\]+/gi;
    let match;
    while ((match = re.exec(String(text)))) addPinimgUrl(urls, match[0]);
  }

  function pinimgDedupeKey(url) {
    const clean = String(url || '').split(/[?#]/)[0];
    const imageMatch = clean.match(/pinimg\.com\/(?:originals|\d+x)\/([0-9a-f]{2}\/[0-9a-f]{2}\/[0-9a-f]{2}\/[^/]+)$/i);
    if (imageMatch) return imageMatch[1].toLowerCase();
    const videoMatch = clean.match(/v\d*\.pinimg\.com\/videos\/[^/]+\/(?:expMp4|720p|hls)\/(.+)$/i);
    return videoMatch ? videoMatch[1].toLowerCase() : clean.toLowerCase();
  }

  function pinimgQualityScore(url) {
    const clean = String(url || '');
    if (/\/originals\//i.test(clean)) return 4000;
    const sized = clean.match(/\/(\d+)x\//i);
    return sized ? Number(sized[1]) || 0 : 0;
  }

  function dedupePinimgUrls(values) {
    const order = [];
    const bestByKey = new Map();
    (values || []).forEach(value => {
      if (!value) return;
      const url = upgradeToOriginal(String(value).replace(/&amp;/g, '&'));
      const key = pinimgDedupeKey(url);
      if (!key) return;
      if (!bestByKey.has(key)) order.push(key);
      const current = bestByKey.get(key);
      if (!current || pinimgQualityScore(url) >= pinimgQualityScore(current)) bestByKey.set(key, url);
    });
    return order.map(key => bestByKey.get(key)).filter(Boolean);
  }

  function getElementArea(el) {
    if (!el?.getBoundingClientRect) return 0;
    const rect = el.getBoundingClientRect();
    return Math.max(0, rect.width) * Math.max(0, rect.height);
  }

  function scoreFocusedCloseupRoot(root) {
    if (!root?.querySelector) return -1;
    if (!root.querySelector('[data-test-id="closeup-image"], [data-test-id="closeup-visual-container"], [data-test-id="visual-content-container"]')) return -1;
    let score = 0;
    if (root.matches?.('[data-grid-item-idx], [data-test-id="closeup-body"], [data-test-id="closeup-body-style"]')) score += 6;
    if (root.querySelector('[data-test-id="closeup-action-bar"], [data-test-id="closeup-action-items"]')) score += 6;
    if (root.querySelector('[data-test-id="closeup-visual-container"]')) score += 4;
    if (root.querySelector('[data-test-id="closeup-image"] img, [data-test-id="closeup-image"] video')) score += 4;
    if (isElementActuallyVisible(root.querySelector('[data-test-id="closeup-visual-container"], [data-test-id="closeup-image"]') || root)) score += 8;
    score += Math.min(8, getElementArea(root) / 100000);
    return score;
  }

  function getFocusedCloseupRoot(anchor) {
    if (anchor?.closest) {
      let anchoredRoot = anchor.closest('[data-grid-item-idx], [data-test-id="closeup-body"], [data-test-id="closeup-body-style"]');
      while (anchoredRoot) {
        if (scoreFocusedCloseupRoot(anchoredRoot) >= 0) return anchoredRoot;
        anchoredRoot = anchoredRoot.parentElement?.closest?.('[data-grid-item-idx], [data-test-id="closeup-body"], [data-test-id="closeup-body-style"]');
      }
    }

    const candidates = new Set();
    document.querySelectorAll('[data-test-id="closeup-image"], [data-test-id="closeup-visual-container"]').forEach(el => {
      candidates.add(el);
      const closeupRoot = el.closest('[data-grid-item-idx], [data-test-id="closeup-body"], [data-test-id="closeup-body-style"]');
      if (closeupRoot) candidates.add(closeupRoot);
      const actionRoot = el.closest('[data-test-id="closeup-body-style"], [data-test-id="closeup-body"]');
      if (actionRoot) candidates.add(actionRoot);
    });
    document.querySelectorAll('[data-grid-item-idx], [data-test-id="closeup-body"], [data-test-id="closeup-body-style"]').forEach(el => {
      if (el.querySelector('[data-test-id="closeup-image"], [data-test-id="closeup-visual-container"]')) candidates.add(el);
    });
    return [...candidates]
      .map(el => ({ el, score: scoreFocusedCloseupRoot(el) }))
      .filter(item => item.score >= 0)
      .sort((a, b) => b.score - a.score)[0]?.el || null;
  }

  function extractFocusedPinTitle(anchor) {
    const focusedRoot = getFocusedCloseupRoot(anchor);
    const focusedTitle = extractPinTitleFromScope(focusedRoot);
    if (focusedTitle) return focusedTitle;
    const documentTitle = extractPinTitleFromScope(document, CLOSEUP_PIN_TITLE_SELECTORS);
    if (documentTitle) return documentTitle;
    const pinId = location.pathname.match(/\/pin\/(\d+)/i)?.[1];
    return pinId ? `Pin - ${pinId}` : makeFallbackPinName();
  }

  function getCloseupScopePart(root, selector) {
    if (!root) return null;
    if (root.matches?.(selector)) return root;
    return root.querySelector?.(selector) || null;
  }

  function getCloseupVisualScope(anchor) {
    const focusedRoot = getFocusedCloseupRoot(anchor);
    if (focusedRoot) {
      return getCloseupScopePart(focusedRoot,
        '[data-test-id="closeup-visual-container"], ' +
        '[data-test-id="visual-content-container"], ' +
        '[data-test-id="pin-closeup-image"], ' +
        '[data-test-id="closeup-image"]'
      ) || focusedRoot;
    }
    return document.querySelector(
      '[data-test-id="closeup-visual-container"], ' +
      '[data-test-id="visual-content-container"], ' +
      '[data-test-id="pin-closeup-image"], ' +
      '[data-test-id="closeup-image"]'
    ) || document;
  }

  function getCurrentCarouselSlide(scope) {
    const root = scope?.querySelectorAll ? scope : document;
    const scroller = root.querySelector(
      '[data-test-id="closeup-image"] ul[class*="carousel"], ' +
      '[data-test-id="closeup-image"] ul, ' +
      'ul[class*="carousel"]'
    );
    if (!scroller) return null;
    const slides = [...scroller.children].filter(slide =>
      slide.querySelector?.('img[src*="pinimg.com"], img[srcset*="pinimg.com"], video, [style*="pinimg.com"]')
    );
    if (!slides.length) return null;

    const scrollerRect = scroller.getBoundingClientRect();
    const hasLayout = scrollerRect.width > 0 && scrollerRect.height > 0;
    if (hasLayout) {
      const centerX = scrollerRect.left + scrollerRect.width / 2;
      const visible = slides.map(slide => {
        const rect = slide.getBoundingClientRect();
        const overlapWidth = Math.max(0, Math.min(rect.right, scrollerRect.right) - Math.max(rect.left, scrollerRect.left));
        const overlapHeight = Math.max(0, Math.min(rect.bottom, scrollerRect.bottom) - Math.max(rect.top, scrollerRect.top));
        const visibleArea = overlapWidth * overlapHeight;
        const centerDistance = Math.abs((rect.left + rect.width / 2) - centerX);
        return { slide, visibleArea, centerDistance };
      }).sort((a, b) => (b.visibleArea - a.visibleArea) || (a.centerDistance - b.centerDistance));
      if (visible[0]?.visibleArea > 0) return visible[0].slide;
    }

    const transitionSlide = slides.find(slide => slide.querySelector('[style*="view-transition-name: image"]'));
    if (transitionSlide) return transitionSlide;

    const scrollLeft = scroller.scrollLeft || 0;
    return slides
      .map(slide => ({ slide, distance: Math.abs((slide.offsetLeft || 0) - scrollLeft) }))
      .sort((a, b) => a.distance - b.distance)[0]?.slide || slides[0];
  }

  function getLargestVisibleCloseupImage(scope) {
    const root = scope?.querySelectorAll ? scope : document;
    return [...root.querySelectorAll('[data-test-id="closeup-image"] img, [data-test-id="closeup-visual-container"] img, [data-test-id="visual-content-container"] img')]
      .map(img => ({ img, area: isElementActuallyVisible(img) ? getElementArea(img) : 0 }))
      .filter(item => item.area > 0)
      .sort((a, b) => b.area - a.area)[0]?.img || null;
  }

  function findCurrentCloseupImageUrl(anchor) {
    const urls = new Set();
    const scope = getCloseupVisualScope(anchor);
    const currentSlide = getCurrentCarouselSlide(scope);
    if (currentSlide) {
      collectImageUrlsFromScope(currentSlide, urls);
      const currentSlideUrl = dedupePinimgUrls([...urls])[0];
      if (currentSlideUrl) return currentSlideUrl;
    }

    const currentImage = getLargestVisibleCloseupImage(scope);
    if (currentImage) {
      addPinimgUrl(urls, getBestCloseupImageUrl(currentImage));
      addPinimgUrl(urls, currentImage.currentSrc);
      addPinimgUrl(urls, currentImage.src);
      addPinimgUrl(urls, currentImage.getAttribute('srcset'));
      const currentImageUrl = dedupePinimgUrls([...urls])[0];
      if (currentImageUrl) return currentImageUrl;
    }

    collectImageUrlsFromScope(scope, urls);
    return dedupePinimgUrls([...urls])[0] || null;
  }

  function collectFocusedCarouselSlideUrls(scope, urls) {
    const root = scope?.querySelectorAll ? scope : document;
    root.querySelectorAll('[data-test-id="closeup-image"] ul li, ul[class*="carousel"] li').forEach(slide => {
      collectImageUrlsFromScope(slide, urls);
    });
    return urls;
  }

  function collectImageUrlsFromScope(scope, urls = new Set()) {
    if (!scope) return urls;
    const root = scope.querySelectorAll ? scope : document;
    root.querySelectorAll(
      '[data-test-id="pin-closeup-image"] video, ' +
      '[data-test-id="closeup-image"] video, ' +
      '[elementtiming*="MainPinImage"] ~ video, video'
    ).forEach(video => {
      addPinimgUrl(urls, video.poster);
      addPinimgUrl(urls, video.currentSrc);
      addPinimgUrl(urls, video.src);
    });

    const imgs = new Set();
    if (scope.matches?.('img')) imgs.add(scope);
    [
      '[data-test-id="closeup-image"] img',
      '[data-test-id="closeup-visual-container"] img',
      '[data-test-id="visual-content-container"] img',
      '[data-test-id="pin-closeup-image"] img',
      'img[elementtiming*="MainPinImage"]',
      'img[fetchpriority="high"]',
      'img[src*="pinimg.com"]',
      'img[srcset*="pinimg.com"]',
    ].forEach(selector => {
      root.querySelectorAll(selector).forEach(img => imgs.add(img));
    });

    imgs.forEach(img => {
      addPinimgUrl(urls, getBestCloseupImageUrl(img));
      addPinimgUrl(urls, img.currentSrc);
      addPinimgUrl(urls, img.src);
      addPinimgUrl(urls, img.getAttribute('src'));
      addPinimgUrl(urls, img.getAttribute('srcset'));
      addPinimgUrl(urls, img.getAttribute('data-src'));
      addPinimgUrl(urls, img.getAttribute('data-srcset'));
      [...img.attributes].forEach(attr => {
        if (/src|url|image/i.test(attr.name)) addPinimgUrlsFromText(urls, attr.value);
      });
    });

    root.querySelectorAll?.('[style*="pinimg.com"], source[srcset*="pinimg.com"], source[src*="pinimg.com"], [data-src*="pinimg.com"], [data-srcset*="pinimg.com"]').forEach(el => {
      addPinimgUrl(urls, el.getAttribute('src'));
      addPinimgUrl(urls, el.getAttribute('srcset'));
      addPinimgUrl(urls, el.getAttribute('data-src'));
      addPinimgUrl(urls, el.getAttribute('data-srcset'));
      addPinimgUrlsFromText(urls, el.getAttribute('style'));
    });
    addPinimgUrlsFromText(urls, root.innerHTML);
    return urls;
  }

  function findCarouselScroller(scope) {
    const root = scope?.querySelectorAll ? scope : document;
    const preferred = root.querySelector(
      '[data-test-id="closeup-image"] ul[class*="carousel"], ' +
      '[data-test-id="closeup-image"] ul, ' +
      'ul[class*="carousel"], ' +
      '[data-test-id="closeup-image"] [style*="overflow-x"]'
    );
    if (preferred && preferred.scrollWidth > preferred.clientWidth + 4) return preferred;
    return [...root.querySelectorAll('*')].find(el => {
      const style = getComputedStyle(el);
      return el.scrollWidth > el.clientWidth + 4 && /auto|scroll/.test(style.overflowX || '');
    }) || null;
  }

  function collectCloseupImageUrlsSync() {
    const urls = new Set();
    const scope = getCloseupVisualScope();
    collectFocusedCarouselSlideUrls(scope, urls);
    collectImageUrlsFromScope(scope, urls);
    if (!urls.size && scope !== document && !getFocusedCloseupRoot()) collectImageUrlsFromScope(document, urls);
    return dedupePinimgUrls([...urls]);
  }

  async function collectCloseupImageUrls() {
    const scope = getCloseupVisualScope();
    const urls = new Set(collectCloseupImageUrlsSync());
    collectFocusedCarouselSlideUrls(scope, urls);
    collectImageUrlsFromScope(scope, urls);
    return dedupePinimgUrls([...urls]);
  }

  function findMainImageUrl(anchor) {
    return findCurrentCloseupImageUrl(anchor);
  }

  function fetchBinary(url) {
    return new Promise((res, rej) => {
      GM_xmlhttpRequest({
        method: 'GET', url, responseType: 'arraybuffer',
        // Referer is required — without it Pinterest's CDN returns 403
        headers: {
          'Referer': location.href,
          'Accept':  'image/webp,image/apng,image/*,*/*;q=0.8',
        },
        onload:  r => (r.status >= 200 && r.status < 300)
          ? res(r.response)
          : rej(new Error('HTTP ' + r.status)),
        onerror: e => rej(new Error('Network error: ' + (e && e.error || e))),
      });
    });
  }

  function wait(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // Build a descending-quality URL queue for a pinimg.com image.
  // Tries originals first, then 736x, then 564x so we always get *something*
  // even when the /originals/ path is access-restricted for a given pin.
  // Converts any v1.pinimg.com video URL to the highest reliably available quality.
  // mc channel → 720p direct MP4; iht channel (Idea Pins) → 720w expMp4.
  function getHighestQualityVideoUrl(src) {
    const m = src.match(/v1\.pinimg\.com\/videos\/(mc|iht)\/(?:expMp4|720p|hls)\/([a-f0-9]{2}\/[a-f0-9]{2}\/[a-f0-9]{2}\/[a-f0-9]{32,})/i);
    if (!m) return src;
    const [, channel, hash] = m;
    return channel === 'iht'
      ? `https://v1.pinimg.com/videos/iht/expMp4/${hash}_720w.mp4`
      : `https://v1.pinimg.com/videos/mc/720p/${hash}.mp4`;
  }

  function pinimgFallbackQueue(url) {
    if (!url) return [url];
    const m = url.match(
      /^(https?:\/\/i\.pinimg\.com)\/(?:originals|\d+x)(\/[0-9a-f]{2}\/[0-9a-f]{2}\/[0-9a-f]{2}\/.+)$/i
    );
    if (!m) return [url];
    const [, base, path] = m;
    // Deduplicate while preserving order
    return [
      base + '/originals' + path,
      base + '/736x'      + path,
      base + '/564x'      + path,
    ].filter((u, i, a) => a.indexOf(u) === i);
  }

  async function fetchBestImageBuffer(imageUrl) {
    for (const u of pinimgFallbackQueue(imageUrl)) {
      try { return await fetchBinary(u); } catch (_) {}
    }
    return null;
  }

  function buildImageDownloadName(buf, filename) {
    const ext = detectFileType(new Uint8Array(buf));
    const explicitTitle = stripKnownExt(sanitizeFilename(filename || ''));
    const pageTitle = stripKnownExt(extractPinTitle() || '');
    const basePart = explicitTitle || pageTitle || makeFallbackPinName();
    return basePart + ext;
  }

  function saveImageBuffer(buf, filename) {
    if (!buf) return false;
    try {
      const a = document.createElement('a');
      a.href = URL.createObjectURL(new Blob([buf]));
      a.download = buildImageDownloadName(buf, filename);
      a.style.display = 'none';
      document.body.appendChild(a);
      a.click();
      setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 10000);
      return true;
    } catch (_) {
      return false;
    }
  }

  async function downloadSingle(imageUrl, filename) {
    if (!imageUrl) return false;
    const buf = await fetchBestImageBuffer(imageUrl);
    return saveImageBuffer(buf, filename);
  }

  async function downloadCloseupImages(urls, title, onProgress) {
    urls = dedupePinimgUrls((urls || []).filter(Boolean));
    if (!urls.length) return 0;
    const base = stripKnownExt(sanitizeFilename(title || extractPinTitle() || makeFallbackPinName()));
    let saved = 0;
    for (let i = 0; i < urls.length; i++) {
      const name = urls.length > 1 ? `${base} - ${i + 1}` : base;
      onProgress?.('fetch', i + 1, urls.length);
      if (await downloadSingle(urls[i], name)) saved++;
      onProgress?.('saved', saved, urls.length);
      if (urls.length > 1) await wait(300);
    }
    onProgress?.('done', saved, urls.length);
    return saved;
  }

  function initDownloadFixer() {
    if (!get('downloadFixer')) return;
    document.addEventListener('click', e => {
      if (isPowerMenuEvent(e)) return;
      if (!get('downloadFixer')) return;
      const target = e.target.closest(
        '[data-test-id*="download"], [aria-label*="ownload" i], ' +
        'button[id*="download"], [role="menuitem"]'
      );
      if (!target) return;
      if (target.closest('#pe-closeup-image-dl-slot, #pe-reverse-image-search-slot, #pe-reverse-image-search-menu')) return;
      const text   = (target.textContent || '').toLowerCase();
      const testId = target.getAttribute('data-test-id') || '';
      const aria   = (target.getAttribute('aria-label') || '').toLowerCase();
      const isDownload = text.includes('download') || testId.includes('download') || aria.includes('download');
      if (!isDownload) return;
      const url = findMainImageUrl(target);
      // Only intercept if we found the image URL; otherwise let Pinterest's native handler work
      if (url) {
        e.preventDefault();
        e.stopPropagation();
        downloadSingle(url, extractFocusedPinTitle(target));
      }
    }, true);
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: QUICK DOWNLOAD BUTTON
  // ═══════════════════════════════════════════════════════════════════
  let _closeupImageDlObs = null;
  let _mobileCloseupActionObs = null;
  let _mobileCloseupActionObservedRoot = null;
  let _mobileCloseupActionRefresh = null;
  let _mobileCloseupActionSignature = '';
  let _pinCardQuickDownloadObs = null;
  let _pinCardQuickDownloadRescan = null;
  let _pinCardQuickDownloadPendingRoots = new Set();

  const PIN_CARD_QUICK_DOWNLOAD_SELECTOR = '[data-test-id="pin"], [data-grid-item="true"], [data-test-id="pinWrapper"]';
  const MOBILE_DEFAULT_ACTION_SLOT_SELECTORS = [
    '[data-test-id="react-button"], [data-test-id="reaction-count"]',
    '[data-test-id="comment-button"]',
    '[data-test-id="share-button-group"], [data-test-id="share-button-no-animation"]',
    '[data-test-id="context-menu-button"], [data-test-id="ellipsis-button"], [data-test-id="more-actions-button"]',
  ];

  function getDesktopCloseupActionItems() {
    return document.querySelector(
      '[data-test-id="closeup-action-items"][role="list"], ' +
      '[data-test-id="closeup-action-items"], ' +
      '[data-test-id="closeupActionBar"] [role="list"]'
    );
  }

  function getMobileCloseupActionItems() {
    return document.querySelector('[data-test-id="closeup-pin-action-items"]');
  }

  function getCloseupActionItems() {
    return IS_MOBILE ? getMobileCloseupActionItems() : getDesktopCloseupActionItems();
  }

  function supportsCloseupActionBarEnhancements() {
    return /\/pin\/\d/i.test(location.pathname);
  }

  function getCloseupActionIconRow() {
    const actionItems = getCloseupActionItems();
    if (!actionItems) return null;
    if (IS_MOBILE) return actionItems;
    const rows = [...actionItems.children].filter(el => el.querySelector?.('[role="listitem"]'));
    return rows.find(row => row.querySelector(
      '[data-test-id="react-button"], button[aria-label="Comments"], ' +
      '[data-test-id="closeup-share-button"], [data-test-id="closeup-more-options"]'
    )) || rows[0] || actionItems;
  }

  function findCloseupActionSlot(row, selector) {
    if (!row?.querySelectorAll) return null;
    for (const slot of row.querySelectorAll(':scope > .oRZ5_s')) {
      if (slot.querySelector(selector)) return slot;
    }
    const found = row.querySelector(selector)?.closest('.oRZ5_s, [role="listitem"]') || null;
    if (!found) return null;
    let direct = found;
    while (direct && direct.parentElement !== row) direct = direct.parentElement;
    return direct || null;
  }

  function isMobileCloseupActionRow(row) {
    return IS_MOBILE && !!row?.matches?.('[data-test-id="closeup-pin-action-items"]');
  }

  function isVisibleActionSlot(slot) {
    if (!slot?.isConnected) return false;
    const style = getComputedStyle(slot);
    if (style.display === 'none' || style.visibility === 'hidden') return false;
    const rect = slot.getBoundingClientRect();
    return rect.width > 0 && rect.height > 0;
  }

  function getMobileVisibleDefaultActionSlotCount(row = getMobileCloseupActionItems()) {
    if (!isMobileCloseupActionRow(row)) return 0;
    return MOBILE_DEFAULT_ACTION_SLOT_SELECTORS.reduce((count, selector) => {
      const slot = findCloseupActionSlot(row, selector);
      return count + (isVisibleActionSlot(slot) ? 1 : 0);
    }, 0);
  }

  function getMobileCloseupActionSignature(row = getMobileCloseupActionItems(), wantsDownload = true, wantsReverse = true, showReverse = false) {
    if (!isMobileCloseupActionRow(row)) return 'no-row';
    const defaultCount = getMobileVisibleDefaultActionSlotCount(row);
    const hasDownload = !!document.getElementById('pe-closeup-image-dl-slot');
    const hasReverse = !!document.getElementById('pe-reverse-image-search-slot');
    return [
      defaultCount,
      wantsDownload ? 1 : 0,
      wantsReverse ? 1 : 0,
      showReverse ? 1 : 0,
      hasDownload ? 1 : 0,
      hasReverse ? 1 : 0,
      row.childElementCount,
    ].join(':');
  }

  function shouldShowMobileReverseImageSearchButton(row = getMobileCloseupActionItems()) {
    if (!IS_MOBILE) return true;
    if (!supportsCloseupActionBarEnhancements()) return false;
    if (!get('reverseImageSearchButton') || get('hideReverseImageSearchButton')) return false;
    if (!isMobileCloseupActionRow(row)) return false;
    return getMobileVisibleDefaultActionSlotCount(row) < MOBILE_DEFAULT_ACTION_SLOT_SELECTORS.length;
  }

  function findMobileMoreActionSlot(row) {
    return findCloseupActionSlot(row,
      '[data-test-id="context-menu-button"], [data-test-id="ellipsis-button"], [data-test-id="more-actions-button"], button[aria-label="More actions"]'
    );
  }

  function insertCloseupActionSlot(iconRow, slot, kind) {
    if (isMobileCloseupActionRow(iconRow)) {
      const moreSlot = findMobileMoreActionSlot(iconRow);
      if (kind === 'reverse') {
        const downloadSlot = document.getElementById('pe-closeup-image-dl-slot');
        if (downloadSlot && downloadSlot.parentElement === iconRow) {
          iconRow.insertBefore(slot, downloadSlot.nextSibling);
          return;
        }
      } else {
        const reverseSlot = document.getElementById('pe-reverse-image-search-slot');
        if (reverseSlot && reverseSlot.parentElement === iconRow) {
          iconRow.insertBefore(slot, reverseSlot);
          return;
        }
      }
      iconRow.insertBefore(slot, moreSlot || null);
      return;
    }

    const shareSlot = findCloseupActionSlot(iconRow,
      '[data-test-id="closeup-share-button"], button[aria-label*="Share" i], div[aria-label="Share"]'
    );
    const moreSlot = findCloseupActionSlot(iconRow,
      '[data-test-id="closeup-more-options"], [data-test-id="closeup-action-bar-button"], button[aria-label="More actions"]'
    );
    iconRow.insertBefore(slot, shareSlot || moreSlot || null);
  }

  function stopCloseupActionEvent(e) {
    e.stopPropagation();
    e.stopImmediatePropagation?.();
  }

  function absorbCloseupActionEvents(el) {
    if (!el || el.__peActionEventsAbsorbed) return;
    el.__peActionEventsAbsorbed = true;
    ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'touchstart', 'touchend'].forEach(type => {
      el.addEventListener(type, stopCloseupActionEvent, { capture: true });
    });
  }

  function removeCloseupImageDownloadButton() {
    const el = document.getElementById('pe-closeup-image-dl-slot');
    if (el) el.remove();
  }

  function getFocusedCloseupVideoScope(anchor) {
    const focusedRoot = getFocusedCloseupRoot(anchor);
    if (!focusedRoot) return null;
    return getCloseupScopePart(focusedRoot,
      '[data-test-id="closeup-visual-container"], ' +
      '[data-test-id="visual-content-container"], ' +
      '[data-test-id="story-pin-video-block"], ' +
      '[data-test-id="pin-closeup-image"], ' +
      '[data-test-id="closeup-image"]'
    ) || focusedRoot;
  }

  function getFocusedVideoHash(vid) {
    if (!vid) return '';
    const values = [
      vid.closest?.('[data-video-signature]')?.getAttribute('data-video-signature'),
      vid.getAttribute?.('data-video-signature'),
      vid.getAttribute?.('poster'),
      vid.poster,
      findPinterestVideoSrc(vid),
      getVideoSrc(vid),
    ];
    for (const source of vid.querySelectorAll?.('source') || []) {
      values.push(source.getAttribute('src'), source.getAttribute('data-src'));
    }
    for (const value of values) {
      const hash = extractPinterestVideoHashFromText(value);
      if (hash) return hash;
    }
    return '';
  }

  function getMatchingInterceptedVideoUrls(focusedHash) {
    if (!focusedHash) return [];
    return (_interceptedVideoUrlsByHash.get(focusedHash) || [])
      .filter(url => extractPinterestVideoHashFromText(url) === focusedHash)
      .map(url => getHighestQualityVideoUrl(url))
      .filter((url, i, arr) => url && !/\.m3u8/i.test(url) && arr.indexOf(url) === i);
  }

  function getFocusedCloseupVideoElement(anchor) {
    const scope = getFocusedCloseupVideoScope(anchor);
    if (!scope) return null;
    const selectors = [
      'video[data-test-id="duplo-hls-video"]',
      '[data-test-id="pin-closeup-image"] video',
      '[data-test-id="duplo-hls-video"] video',
      '[data-test-id="story-pin-video-block"] video',
      '[data-test-id="pinrep-video"] video',
      '[data-test-id="closeup-expanded-view"] video',
      '[data-test-id="closeup-image"] video',
      '[data-test-id="closeup-visual-container"] video',
      'video',
    ].join(', ');
    return [...(scope.querySelectorAll?.(selectors) || [])]
      .map(vid => ({
        vid,
        area: getElementArea(vid),
        hasHash: !!getFocusedVideoHash(vid),
        hasUsableSrc: !!(findPinterestVideoSrc(vid) || getVideoSrc(vid)),
        visible: isElementActuallyVisible(vid),
      }))
      .filter(item => item.hasHash || item.hasUsableSrc)
      .sort((a, b) => Number(b.visible) - Number(a.visible) || b.area - a.area)[0]?.vid || null;
  }

  function buildPinterestVideoDownloadUrlsFromHash(focusedHash, preferredBucket) {
    if (!focusedHash) return [];
    const buckets = preferredBucket ? [preferredBucket] : ['mc', 'iht'];
    const urls = [];
    buckets.forEach(bucket => {
      const variants = IS_MOBILE
        ? [
            ...(bucket === 'iht' ? [`https://v1.pinimg.com/videos/iht/expMp4/${focusedHash}_720w.mp4`] : []),
            `https://v1.pinimg.com/videos/${bucket}/expMp4/${focusedHash}_t1.mp4`,
            `https://v1.pinimg.com/videos/${bucket}/expMp4/${focusedHash}_t2.mp4`,
            `https://v1.pinimg.com/videos/${bucket}/expMp4/${focusedHash}_t3.mp4`,
            `https://v1.pinimg.com/videos/${bucket}/expMp4/${focusedHash}_t4.mp4`,
            `https://v1.pinimg.com/videos/${bucket}/720p/${focusedHash}.mp4`,
          ]
        : [
            `https://v1.pinimg.com/videos/${bucket}/720p/${focusedHash}.mp4`,
            `https://v1.pinimg.com/videos/${bucket}/expMp4/${focusedHash}_t4.mp4`,
            `https://v1.pinimg.com/videos/${bucket}/expMp4/${focusedHash}_t3.mp4`,
            `https://v1.pinimg.com/videos/${bucket}/expMp4/${focusedHash}_t2.mp4`,
            `https://v1.pinimg.com/videos/${bucket}/expMp4/${focusedHash}_t1.mp4`,
          ];
      urls.push(...variants);
    });
    return urls.filter((u, i, a) => u && a.indexOf(u) === i);
  }

  function buildPinterestVideoDownloadUrls(rawSrc) {
    const rawText = String(rawSrc || '');
    if (!rawText || (/i\.pinimg\.com/i.test(rawText) && !rawText.toLowerCase().includes('videos/thumbnails/originals'))) return [];
    const bestUrl = getHighestQualityVideoUrl(rawText);
    const safeRawSrc = rawText && !/\.m3u8/i.test(rawText) ? rawText : null;
    const m = rawText.match(/v1\.pinimg\.com\/videos\/(mc|iht)\/(?:expMp4|720p|hls)\/([a-f0-9]{2}\/[a-f0-9]{2}\/[a-f0-9]{2}\/[a-f0-9]{32,})/i);
    const focusedHash = extractPinterestVideoHashFromText(rawText);
    const bucket = getPinterestVideoCdnBucket(rawText);
    if (m && m[1] === 'mc') {
      return (IS_MOBILE
        ? [
            `https://v1.pinimg.com/videos/mc/expMp4/${m[2]}_t1.mp4`,
            `https://v1.pinimg.com/videos/mc/expMp4/${m[2]}_t2.mp4`,
            `https://v1.pinimg.com/videos/mc/expMp4/${m[2]}_t3.mp4`,
            `https://v1.pinimg.com/videos/mc/expMp4/${m[2]}_t4.mp4`,
            `https://v1.pinimg.com/videos/mc/720p/${m[2]}.mp4`,
            safeRawSrc,
          ]
        : [
            `https://v1.pinimg.com/videos/mc/720p/${m[2]}.mp4`,
            `https://v1.pinimg.com/videos/mc/expMp4/${m[2]}_t4.mp4`,
            `https://v1.pinimg.com/videos/mc/expMp4/${m[2]}_t3.mp4`,
            `https://v1.pinimg.com/videos/mc/expMp4/${m[2]}_t2.mp4`,
            `https://v1.pinimg.com/videos/mc/expMp4/${m[2]}_t1.mp4`,
            safeRawSrc,
          ]).filter((u, i, a) => u && a.indexOf(u) === i);
    }
    if (focusedHash) {
      return [...buildPinterestVideoDownloadUrlsFromHash(focusedHash, bucket), bestUrl, safeRawSrc]
        .filter((u, i, a) => u && !/\.m3u8/i.test(u) && !/i\.pinimg\.com/i.test(u) && a.indexOf(u) === i);
    }
    return [bestUrl, safeRawSrc].filter((u, i, a) => u && !/\.m3u8/i.test(u) && a.indexOf(u) === i);
  }

  function getCurrentPinIdFromLocation() {
    return String(location.pathname || '').match(/\/pin\/(\d+)/i)?.[1] || '';
  }

  function parsePinterestRelayCompletedCall(text) {
    const raw = String(text || '');
    const marker = '__PWS_RELAY_REGISTER_COMPLETED_REQUEST__(';
    const start = raw.indexOf(marker);
    if (start === -1) return null;

    const args = [];
    let depth = 0;
    let quote = '';
    let escaped = false;
    let argStart = start + marker.length;

    for (let i = argStart; i < raw.length; i += 1) {
      const ch = raw[i];
      if (quote) {
        if (escaped) escaped = false;
        else if (ch === '\\') escaped = true;
        else if (ch === quote) quote = '';
        continue;
      }
      if (ch === '"' || ch === "'") {
        quote = ch;
        continue;
      }
      if (ch === '{' || ch === '[' || ch === '(') {
        depth += 1;
        continue;
      }
      if (ch === '}' || ch === ']' || ch === ')') {
        if (ch === ')' && depth === 0) {
          args.push(raw.slice(argStart, i).trim());
          break;
        }
        depth = Math.max(0, depth - 1);
        continue;
      }
      if (ch === ',' && depth === 0) {
        args.push(raw.slice(argStart, i).trim());
        argStart = i + 1;
      }
    }

    if (args.length < 2) return null;
    try {
      const requestArg = JSON.parse(args[0]);
      const requestText = typeof requestArg === 'string' ? decodeURIComponent(requestArg) : JSON.stringify(requestArg);
      const request = JSON.parse(requestText);
      const response = JSON.parse(args[1]);
      return { request, variables: request?.variables || {}, response };
    } catch {
      return null;
    }
  }

  function findCurrentPinDataFromRelayScripts(pinId) {
    if (!pinId) return null;
    for (const script of document.querySelectorAll('script[data-relay-completed-request="true"]')) {
      const parsed = parsePinterestRelayCompletedCall(script.textContent || '');
      const variables = parsed?.variables;
      if (String(variables?.pinId || '') !== pinId) continue;
      const candidates = [
        parsed.response?.data?.v3GetPinQueryv2?.data,
        parsed.response?.data?.v3GetPinQuery?.data,
        parsed.response?.data?.pin,
        parsed.response?.resource_response?.data,
      ].filter(Boolean);
      for (const candidate of candidates) {
        if (candidate?.entityId && String(candidate.entityId) !== pinId) continue;
        return candidate;
      }
    }
    return null;
  }

  function collectPinterestDataStrings(pinData) {
    const strings = [];
    const seenObjects = new WeakSet();

    function walk(value) {
      if (!value) return;
      if (typeof value === 'string') {
        strings.push(value);
        return;
      }
      if (typeof value !== 'object') return;
      if (seenObjects.has(value)) return;
      seenObjects.add(value);
      if (Array.isArray(value)) {
        value.forEach(walk);
        return;
      }
      Object.keys(value).forEach(key => walk(value[key]));
    }

    walk(pinData);
    return strings;
  }

  function collectVideoDownloadUrlsFromPinterestData(pinData) {
    if (!pinData) return [];
    const roots = [
      pinData.videoList720P,
      pinData.videoDataV2?.videoList720P,
      pinData.storyPinData,
      pinData.videoDataV2,
      pinData.videos,
      pinData.video,
    ].filter(Boolean);
    const strings = roots.flatMap(collectPinterestDataStrings);

    const directUrls = [];
    const fallbackUrls = [];
    strings.forEach(value => {
      const text = String(value || '');
      if (/v1\.pinimg\.com\/videos/i.test(text)) {
        if (/\.mp4(?:[?#]|$)/i.test(text)) {
          directUrls.push(text, ...buildPinterestVideoDownloadUrls(text));
        } else {
          fallbackUrls.push(...buildPinterestVideoDownloadUrls(text));
        }
      }
      if (/videos\/thumbnails\/originals/i.test(text)) {
        const hash = extractPinterestVideoHashFromText(text);
        if (hash) {
          fallbackUrls.push(
            ...buildPinterestVideoDownloadUrlsFromHash(hash, 'iht'),
            ...buildPinterestVideoDownloadUrlsFromHash(hash, 'mc')
          );
        }
      }
    });

    return [...directUrls, ...fallbackUrls]
      .filter((url, i, arr) => url && !/^blob:/i.test(url) && !/\.m3u8(?:[?#]|$)/i.test(url) && !/i\.pinimg\.com/i.test(url) && arr.indexOf(url) === i);
  }

  // ── Real network API (SPA-safe) ─────────────────────────────────────
  // Embedded relay scripts only describe the originally-loaded pin, so after
  // in-app navigation they have nothing for the tapped pin and the closeup
  // <video> is often not rendered yet. The pin id in the URL is always
  // correct, so fetching Pinterest's own PinResource API by that id resolves
  // the real video regardless of DOM/relay staleness.
  const _apiPinDataCache = new Map();

  function readCsrfCookie() {
    const m = String(document.cookie || '').match(/(?:^|;\s*)csrftoken=([^;]*)/);
    return m ? decodeURIComponent(m[1]) : '';
  }

  function fetchPinResourceData(pinId) {
    return new Promise(resolve => {
      if (!pinId) { resolve(null); return; }
      if (_apiPinDataCache.has(pinId)) { resolve(_apiPinDataCache.get(pinId)); return; }
      const data = JSON.stringify({
        options: { id: String(pinId), field_set_key: 'detailed' }, context: {}
      });
      const url = location.origin +
        '/resource/PinResource/get/?source_url=' +
        encodeURIComponent('/pin/' + pinId + '/') +
        '&data=' + encodeURIComponent(data);
      GM_xmlhttpRequest({
        method: 'GET', url,
        headers: {
          'Accept': 'application/json, text/javascript, */*; q=0.01',
          'X-Requested-With': 'XMLHttpRequest',
          'X-CSRFToken': readCsrfCookie(),
        },
        onload: r => {
          let parsed = null;
          try {
            if (r.status >= 200 && r.status < 300) {
              const json = JSON.parse(r.responseText);
              parsed = (json && json.resource_response && json.resource_response.data) || null;
            }
          } catch (_) { parsed = null; }
          if (parsed) _apiPinDataCache.set(pinId, parsed);
          resolve(parsed);
        },
        onerror:   () => resolve(null),
        ontimeout: () => resolve(null),
        timeout: 4000,
      });
    });
  }

  // True when the API data clearly describes a video/idea-video pin (used to
  // refuse the poster-image fallback for a confirmed video).
  function apiDataIsVideoPin(pinData) {
    if (!pinData) return false;
    if (collectVideoDownloadUrlsFromPinterestData(pinData).length) return true;
    return collectPinterestDataStrings(pinData).some(text =>
      /videos\/thumbnails\/originals|v1\.pinimg\.com\/videos|videoDataV2|videoList720P/i.test(String(text || '')));
  }

  async function fetchApiVideoDownload(pinId) {
    const pinData = await fetchPinResourceData(pinId);
    if (!pinData) return null;
    const urls = collectVideoDownloadUrlsFromPinterestData(pinData);
    if (!urls.length) return null;
    return { urls, rawSrc: urls[0], pinId, source: 'api' };
  }

  function cacheMobilePinVideoDownload(pinId, download) {
    if (!pinId || !download?.urls?.length) return download;
    _mobilePinVideoDownloadCache.set(pinId, download);
    while (_mobilePinVideoDownloadCache.size > 6) {
      const oldestKey = _mobilePinVideoDownloadCache.keys().next().value;
      _mobilePinVideoDownloadCache.delete(oldestKey);
    }
    return download;
  }

  function findMobileCurrentPinVideoDownload(anchor) {
    if (!IS_MOBILE) return null;
    const pinId = getCurrentPinIdFromLocation();
    if (!pinId) return null;
    const cached = _mobilePinVideoDownloadCache.get(pinId);
    if (cached?.urls?.length) return cached;

    const pinData = findCurrentPinDataFromRelayScripts(pinId);
    const urls = collectVideoDownloadUrlsFromPinterestData(pinData);
    if (!urls.length) return null;
    return cacheMobilePinVideoDownload(pinId, {
      urls,
      rawSrc: urls[0],
      pinId,
      source: 'mobile-relay',
      anchor,
    });
  }

  function findCurrentCloseupVideoDownload(anchor) {
    const vid = getFocusedCloseupVideoElement(anchor);
    if (!vid) return null;
    const rawSrc = findPinterestVideoSrc(vid);
    const focusedHash = getFocusedVideoHash(vid);
    const urls = [
      ...getMatchingInterceptedVideoUrls(focusedHash),
      ...buildPinterestVideoDownloadUrls(rawSrc),
      ...buildPinterestVideoDownloadUrlsFromHash(focusedHash, getPinterestVideoCdnBucket(rawSrc)),
    ].filter((u, i, a) => u && !/^blob:/i.test(u) && !/i\.pinimg\.com/i.test(u) && a.indexOf(u) === i);
    return urls.length ? { urls, rawSrc: rawSrc || urls[0], element: vid, focusedHash } : null;
  }

  function focusedScopeHasGifSignal(scope) {
    if (!scope?.querySelector) return false;
    if (isMobileGifPin(scope)) return true;
    const gifVideo = scope.querySelector('video');
    if (gifVideo && isGifVideo(gifVideo, scope)) return true;
    return false;
  }

  function focusedPinDataHasVideoSignal() {
    const pinId = getCurrentPinIdFromLocation();
    const pinData = findCurrentPinDataFromRelayScripts(pinId);
    if (!pinData) return false;
    if (collectVideoDownloadUrlsFromPinterestData(pinData).length) return true;
    return collectPinterestDataStrings(pinData).some(text => /videos\/thumbnails\/originals|v1\.pinimg\.com\/videos|videoDataV2|videoList720P/i.test(text));
  }

  // Is the focused closeup a video pin? Uses signals available *before* the
  // HLS blob attaches so we can block poster-image fallback while video data resolves.
  function focusedCloseupIsVideoPin(anchor) {
    const scope = getFocusedCloseupVideoScope(anchor) || getFocusedCloseupRoot(anchor);
    if (!scope || !scope.querySelector) return false;
    if (focusedScopeHasGifSignal(scope)) return false;
    if (scope.querySelector(
      'video[data-test-id="duplo-hls-video"], ' +
      '[data-test-id="duplo-hls-video"], ' +
      '[data-video-signature], ' +
      'video[poster], ' +
      'img[src*="/videos/thumbnails/"], ' +
      'img[srcset*="/videos/thumbnails/"]'
    )) return true;
    const badge = scope.querySelector('[data-test-id="PinTypeIdentifier"]');
    if (badge && /video|watch/i.test(badge.textContent || '')) return true;
    try { if (isVideoPin(scope)) return true; } catch (_) {}
    return focusedPinDataHasVideoSignal();
  }

  // Derive the Pinterest video hash from the focused closeup scope directly
  // (signature attr / video poster / thumbnail URL) — synchronously available
  // even before getFocusedCloseupVideoElement can resolve a usable <video>.
  function deriveFocusedCloseupVideoHash(anchor) {
    const scope = getFocusedCloseupVideoScope(anchor) || getFocusedCloseupRoot(anchor);
    if (!scope || !scope.querySelectorAll) return '';
    const vidEl = scope.querySelector('video[data-test-id="duplo-hls-video"], video');
    if (vidEl) { const h = getFocusedVideoHash(vidEl); if (h) return h; }
    const texts = [];
    scope.querySelectorAll('[data-video-signature]')
      .forEach(el => texts.push(el.getAttribute('data-video-signature')));
    scope.querySelectorAll('video[poster], img[src*="/videos/"]')
      .forEach(el => texts.push(el.getAttribute('poster') || el.getAttribute('src')));
    for (const t of texts) { const h = extractPinterestVideoHashFromText(t); if (h) return h; }
    return '';
  }

  // Confirmed-video pins must never fall through to the poster still. Retry
  // briefly: the hash/relay data and the HLS src often appear a few hundred ms
  // after the closeup opens, which is exactly the race that produced random
  // still-frame downloads on mobile.
  async function resolveFocusedVideoDownloadWithRetry(anchor) {
    const pinId = getCurrentPinIdFromLocation();
    for (let attempt = 0; attempt < 6; attempt++) {
      const direct = (IS_MOBILE ? findMobileCurrentPinVideoDownload(anchor) : null)
        || findCurrentCloseupVideoDownload(anchor);
      if (direct?.urls?.length) return direct;
      const hash = deriveFocusedCloseupVideoHash(anchor);
      if (hash) {
        const urls = [
          ...getMatchingInterceptedVideoUrls(hash),
          ...buildPinterestVideoDownloadUrlsFromHash(hash),
        ].filter((u, i, a) => u && !/^blob:/i.test(u) && !/i\.pinimg\.com/i.test(u) && a.indexOf(u) === i);
        if (urls.length) return { urls, rawSrc: urls[0], focusedHash: hash };
      }
      // The in-page hash path covers nearly all cases instantly. Only fall
      // back to the network API on the final attempts so a slow/dead API
      // never stalls the fast path (pin id from URL is SPA-safe).
      if (attempt >= 4) {
        const apiDownload = await fetchApiVideoDownload(pinId);
        if (apiDownload?.urls?.length) return apiDownload;
      }
      await new Promise(r => setTimeout(r, 250));
    }
    return null;
  }

  async function downloadCurrentCloseupMedia(btn) {
    const title = extractFocusedPinTitle(btn);

    // Fast path: a real <video>/intercepted URL already in the DOM.
    let videoDownload = (IS_MOBILE ? findMobileCurrentPinVideoDownload(btn) : null) || findCurrentCloseupVideoDownload(btn);

    // No DOM video yet: use the resolver that derives the video hash straight
    // from the page (signature/poster/thumbnail) and the in-page interceptor,
    // converting the HLS hash to mp4 with no dependence on the network API.
    if (!videoDownload) {
      videoDownload = await resolveFocusedVideoDownloadWithRetry(btn);
    }
    if (videoDownload?.urls?.length) {
      await downloadVideoFile(videoDownload.urls, title, (loaded, total) => {
        if (total > 0 && btn?.isConnected) btn.title = `${Math.round(loaded / total * 100)}%`;
      });
      return true;
    }

    const currentImageUrl = findCurrentCloseupImageUrl(btn);
    // Never silently save the poster of something that is actually a video.
    const isVideo = focusedCloseupIsVideoPin(btn) ||
      (!!currentImageUrl && /\/videos\/thumbnails\//i.test(currentImageUrl)) ||
      !currentImageUrl;
    if (isVideo) {
      showPowerMenuToast('Could not get the video — tap download again');
      return false;
    }
    return currentImageUrl ? downloadSingle(currentImageUrl, title) : false;
  }

  function isEligiblePinCardQuickDownloadCard(card) {
    if (IS_MOBILE || !card?.querySelector) return false;
    if (!card.matches?.('[data-test-id="pin"], [data-test-id="pinWrapper"]') &&
        !card.querySelector?.('[data-test-id="pin"], [data-test-id="pinWrapper"], a[href*="/pin/"]')) return false;
    if (card.closest?.(
      '[data-test-id="closeup-action-items"], ' +
      '[data-test-id="closeup-pin-action-items"], ' +
      '[data-test-id="closeup-visual-container"], ' +
      '[data-test-id="closeup-image"]'
    )) return false;
    return !!card.querySelector('img[src*="pinimg.com"], img[srcset*="pinimg.com"], video, [style*="pinimg.com"]');
  }

  function getPinCardQuickDownloadCard(node) {
    if (!node?.closest && !node?.matches) return null;
    const card = node.matches?.('[data-test-id="pin"]')
      ? node
      : node.closest?.('[data-test-id="pin"]') ||
        node.closest?.('[data-grid-item="true"]') ||
        node.closest?.('[data-test-id="pinWrapper"]') ||
        null;
    return isEligiblePinCardQuickDownloadCard(card) ? card : null;
  }

  function getPinCardQuickDownloadCards(root = document) {
    if (IS_MOBILE) return [];
    const cards = new Set();
    const scope = root?.querySelectorAll ? root : document;
    const add = node => {
      const card = getPinCardQuickDownloadCard(node);
      if (card) cards.add(card);
    };
    if (scope.matches?.(PIN_CARD_QUICK_DOWNLOAD_SELECTOR)) add(scope);
    scope.querySelectorAll?.(PIN_CARD_QUICK_DOWNLOAD_SELECTOR).forEach(add);
    return [...cards];
  }

  function getPinCardMediaWrapper(card) {
    if (!card?.querySelector) return null;
    return card.querySelector(
      '.PinCard__imageWrapper, ' +
      '[data-test-id="pinWrapper"], ' +
      '[data-test-id^="pincard-gif"], ' +
      '[data-test-id="pinrep-image"], ' +
      '[data-test-id="non-story-pin-image"]'
    ) || card.querySelector('img[src*="pinimg.com"], img[srcset*="pinimg.com"]')?.closest?.(
      '.PinCard__imageWrapper, [data-test-id="pinWrapper"], [data-test-id="pinrep-image"], [data-test-id="non-story-pin-image"], a'
    ) || null;
  }

  function getPinCardFromDownloadButton(anchor) {
    return anchor?.closest?.('[data-pe-pin-card-download-card="true"]') ||
      getPinCardQuickDownloadCard(anchor) ||
      null;
  }

  function getPinCardVideoElement(anchor) {
    const card = getPinCardFromDownloadButton(anchor);
    if (!card) return null;
    const selectors = [
      'video[data-test-id="duplo-hls-video"]',
      '[data-test-id="duplo-hls-video"] video',
      '[data-test-id="pinrep-video"] video',
      '[data-test-id^="pincard-gif"] video',
      'video',
    ].join(', ');
    return [...card.querySelectorAll(selectors)]
      .map(vid => ({
        vid,
        area: getElementArea(vid),
        hasHash: !!getFocusedVideoHash(vid),
        hasUsableSrc: !!(findPinterestVideoSrc(vid) || getVideoSrc(vid)),
        visible: isElementActuallyVisible(vid),
      }))
      .filter(item => item.hasHash || item.hasUsableSrc)
      .sort((a, b) => Number(b.visible) - Number(a.visible) || b.area - a.area)[0]?.vid || null;
  }

  function findCurrentPinCardVideoDownload(anchor) {
    const vid = getPinCardVideoElement(anchor);
    if (!vid) return null;
    const rawSrc = findPinterestVideoSrc(vid);
    const focusedHash = getFocusedVideoHash(vid);
    const urls = [
      ...getMatchingInterceptedVideoUrls(focusedHash),
      ...buildPinterestVideoDownloadUrls(rawSrc),
      ...buildPinterestVideoDownloadUrlsFromHash(focusedHash, getPinterestVideoCdnBucket(rawSrc)),
    ].filter((u, i, a) => u && !/^blob:/i.test(u) && !/i\.pinimg\.com/i.test(u) && a.indexOf(u) === i);
    return urls.length ? { urls, rawSrc: rawSrc || urls[0], element: vid, focusedHash } : null;
  }

  function findCurrentPinCardImageUrl(anchor) {
    const card = getPinCardFromDownloadButton(anchor);
    if (!card) return null;
    const urls = new Set();
    collectImageUrlsFromScope(card, urls);
    return dedupePinimgUrls([...urls])[0] || null;
  }

  async function downloadCurrentPinCardMedia(btn) {
    const card = getPinCardFromDownloadButton(btn);
    if (!card) return false;
    const title = extractPinTitleFromScope(card) || makeFallbackPinName();
    const videoDownload = findCurrentPinCardVideoDownload(btn);
    if (videoDownload) {
      await downloadVideoFile(videoDownload.urls, title, (loaded, total) => {
        if (total > 0 && btn?.isConnected) btn.title = `${Math.round(loaded / total * 100)}%`;
      });
      return true;
    }
    const currentUrl = findCurrentPinCardImageUrl(btn);
    return currentUrl ? downloadSingle(currentUrl, title) : false;
  }

  function stopPinCardQuickDownloadPointerEvent(e) {
    e.stopPropagation();
    e.stopImmediatePropagation?.();
  }

  function absorbPinCardQuickDownloadEvents(el) {
    if (!el || el.__pePinCardDownloadEventsAbsorbed) return;
    el.__pePinCardDownloadEventsAbsorbed = true;
    ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'touchstart', 'touchend'].forEach(type => {
      el.addEventListener(type, stopPinCardQuickDownloadPointerEvent, { capture: true });
    });
  }

  function createPinCardQuickDownloadButton(card) {
    if (!isEligiblePinCardQuickDownloadCard(card)) return false;
    const host = getPinCardMediaWrapper(card);
    if (!host) return false;
    card.dataset.pePinCardDownloadCard = 'true';
    host.classList.add('pe-pin-card-download-host');

    const existing = card.querySelector('.pe-pin-card-download-wrap');
    if (existing && existing.parentElement === host) return true;
    existing?.remove();

    const wrap = document.createElement('div');
    wrap.className = 'pe-pin-card-download-wrap';
    wrap.setAttribute('data-pe-ui', 'true');
    absorbPinCardQuickDownloadEvents(wrap);

    const btn = document.createElement('button');
    btn.className = 'pe-pin-card-download-btn';
    btn.type = 'button';
    btn.setAttribute('aria-label', 'Download');
    btn.title = 'Download';
    btn.innerHTML = `
      <svg viewBox="0 0 24 24" width="24" height="24" fill="currentColor" aria-hidden="true">
        <path d="M11 4h2v8.59l2.3-2.3L16.7 11.7 12 16.4l-4.7-4.7 1.4-1.41 2.3 2.3V4zM5 19h14v2H5z"/>
      </svg>
    `;
    btn.addEventListener('click', async e => {
      e.preventDefault();
      e.stopPropagation();
      e.stopImmediatePropagation?.();
      if (btn.disabled) return;
      btn.disabled = true;
      btn.title = 'Downloading...';
      let saved = false;
      try {
        saved = await downloadCurrentPinCardMedia(btn);
      } catch (_) {}
      if (!saved) {
        btn.classList.add('pe-missing');
        btn.title = 'No media found';
        setTimeout(() => {
          btn.classList.remove('pe-missing');
          btn.title = 'Download';
        }, 1200);
        btn.disabled = false;
        return;
      }
      btn.title = 'Downloaded';
      btn.disabled = false;
      setTimeout(() => { if (btn.isConnected) btn.title = 'Download'; }, 1500);
    }, true);
    wrap.appendChild(btn);
    host.appendChild(wrap);
    return true;
  }

  function refreshDesktopPinCardQuickDownloadButtons(root = document) {
    if (IS_MOBILE) return;
    getPinCardQuickDownloadCards(root).forEach(createPinCardQuickDownloadButton);
  }

  function initDesktopPinCardQuickDownloadButton() {
    if (IS_MOBILE) return;
    refreshDesktopPinCardQuickDownloadButtons();
    if (_pinCardQuickDownloadObs) return;
    _pinCardQuickDownloadRescan = debounce(() => {
      const roots = [..._pinCardQuickDownloadPendingRoots];
      _pinCardQuickDownloadPendingRoots.clear();
      roots.forEach(root => refreshDesktopPinCardQuickDownloadButtons(root));
    }, 120);
    _pinCardQuickDownloadObs = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      records.forEach(record => {
        record.addedNodes?.forEach(node => {
          if (node?.nodeType === 1) _pinCardQuickDownloadPendingRoots.add(node);
        });
      });
      if (!_pinCardQuickDownloadPendingRoots.size) return;
      _pinCardQuickDownloadRescan();
    });
    _pinCardQuickDownloadObs.observe(document.documentElement, { childList: true, subtree: true });
  }

  function createCloseupImageDownloadButton() {
    if (!supportsCloseupActionBarEnhancements()) {
      removeCloseupImageDownloadButton();
      return;
    }

    const iconRow = getCloseupActionIconRow();
    if (!iconRow) return;

    const existing = document.getElementById('pe-closeup-image-dl-slot');
    if (existing && iconRow.contains(existing)) return;
    if (existing) existing.remove();

    const slot = document.createElement('div');
    slot.id = 'pe-closeup-image-dl-slot';
    slot.className = 'oRZ5_s';
    slot.classList.add('pe-closeup-action-slot');
    if (IS_MOBILE) slot.classList.add('pe-mobile-closeup-action-slot');
    slot.dataset.peCloseupAction = 'download';
    slot.setAttribute('data-pe-ui', 'true');
    absorbCloseupActionEvents(slot);

    const item = document.createElement('div');
    item.className = 'ADXRXN';
    item.setAttribute('role', 'listitem');
    item.innerHTML = `
      <button id="pe-closeup-image-dl-btn" class="euRXRl" type="button" aria-label="Download" title="Download">
        <svg viewBox="0 0 24 24" width="26" height="26" fill="currentColor" aria-hidden="true">
          <path d="M11 4h2v8.59l2.3-2.3L16.7 11.7 12 16.4l-4.7-4.7 1.4-1.41 2.3 2.3V4zM5 19h14v2H5z"/>
        </svg>
      </button>
    `;
    slot.appendChild(item);

    const btn = slot.querySelector('#pe-closeup-image-dl-btn');
    btn.addEventListener('click', async e => {
      e.preventDefault();
      e.stopPropagation();
      e.stopImmediatePropagation?.();
      if (btn.disabled) return;
      btn.disabled = true;
      btn.title = 'Downloading...';
      let saved = false;
      try {
        saved = await downloadCurrentCloseupMedia(btn);
      } catch (_) {}
      if (!saved) {
        btn.classList.add('pe-missing');
        btn.title = 'No media found';
        setTimeout(() => {
          btn.classList.remove('pe-missing');
          btn.title = 'Download';
        }, 1200);
        btn.disabled = false;
        return;
      }
      btn.title = 'Downloaded';
      btn.disabled = false;
      setTimeout(() => { if (btn.isConnected) btn.title = 'Download'; }, 1500);
    }, true);

    insertCloseupActionSlot(iconRow, slot, 'download');
  }

  function refreshMobileCloseupActionButtons() {
    if (!IS_MOBILE) return;
    if (!supportsCloseupActionBarEnhancements()) {
      removeCloseupImageDownloadButton();
      removeReverseImageSearchButton();
      disconnectMobileCloseupActionObserver();
      _mobileCloseupActionSignature = '';
      return;
    }
    const wantsDownload = true;
    const wantsReverse = !!get('reverseImageSearchButton') && !get('hideReverseImageSearchButton');
    const row = getMobileCloseupActionItems();
    const showReverse = wantsReverse && shouldShowMobileReverseImageSearchButton(row);
    const signature = getMobileCloseupActionSignature(row, wantsDownload, wantsReverse, showReverse);
    if (signature === _mobileCloseupActionSignature) return;
    createCloseupImageDownloadButton();
    if (showReverse) createReverseImageSearchButton();
    else removeReverseImageSearchButton();
    observeMobileCloseupActionBar();
    _mobileCloseupActionSignature = getMobileCloseupActionSignature(row, wantsDownload, wantsReverse, showReverse);
  }

  function scheduleMobileCloseupActionButtonsRefresh() {
    if (!IS_MOBILE) return;
    if (!_mobileCloseupActionRefresh)
      _mobileCloseupActionRefresh = debounce(refreshMobileCloseupActionButtons, 250);
    _mobileCloseupActionRefresh();
  }

  function disconnectMobileCloseupActionObserver() {
    if (_mobileCloseupActionObs) _mobileCloseupActionObs.disconnect();
    _mobileCloseupActionObs = null;
    _mobileCloseupActionObservedRoot = null;
    _mobileCloseupActionSignature = '';
  }

  function observeMobileCloseupActionBar() {
    if (!IS_MOBILE) return false;
    const row = getMobileCloseupActionItems();
    const root = row?.closest?.('[data-test-id="closeup-pin-action-bar-container"]') || row;
    if (!root) return false;
    if (_mobileCloseupActionObs && _mobileCloseupActionObservedRoot === root) return true;
    disconnectMobileCloseupActionObserver();
    _mobileCloseupActionObservedRoot = root;
    _mobileCloseupActionObs = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      scheduleMobileCloseupActionButtonsRefresh();
    });
    _mobileCloseupActionObs.observe(root, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['class', 'style', 'hidden', 'aria-hidden'],
    });
    return true;
  }

  function initMobileCloseupActionButtons() {
    refreshMobileCloseupActionButtons();
    observeMobileCloseupActionBar();
  }

  function initCloseupImageDownloadButton() {
    if (!supportsCloseupActionBarEnhancements()) {
      removeCloseupImageDownloadButton();
      return;
    }
    if (IS_MOBILE) {
      initMobileCloseupActionButtons();
      return;
    }
    createCloseupImageDownloadButton();
    if (_closeupImageDlObs) return;
    const retry = debounce(createCloseupImageDownloadButton, 150);
    _closeupImageDlObs = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      retry();
    });
    _closeupImageDlObs.observe(document.documentElement, { childList: true, subtree: true });
  }

  const REVERSE_IMAGE_SEARCH_PROVIDERS = [
    {
      id: 'google-lens',
      name: 'Google Lens',
      mode: 'open',
      build: url => 'https://www.google.com/searchbyimage?image_url=' + encodeURIComponent(url) + '&hl=' + encodeURIComponent(USER_LANG || 'en'),
    },
    {
      id: 'yandex',
      name: 'Yandex',
      mode: 'open',
      build: url => 'https://yandex.com/images/search?rpt=imageview&url=' + encodeURIComponent(url),
    },
    {
      id: 'saucenao',
      name: 'SauceNAO (copy URL)',
      mode: 'copy-open',
      homeUrl: 'https://saucenao.com/',
      copiedMessage: 'SauceNAO opened. Image URL copied.',
    },
    {
      id: 'tineye',
      name: 'TinEye (copy URL)',
      mode: 'copy-open',
      homeUrl: 'https://tineye.com/',
      copiedMessage: 'TinEye opened. Image URL copied.',
    },
  ];
  let _reverseImageSearchObs = null;

  function getReverseImageSearchProvider(providerId) {
    return REVERSE_IMAGE_SEARCH_PROVIDERS.find(p => p.id === providerId) || REVERSE_IMAGE_SEARCH_PROVIDERS[0];
  }

  function copyTextToClipboard(text) {
    if (!text) return Promise.resolve(false);
    try {
      if (typeof GM_setClipboard === 'function') {
        GM_setClipboard(text, 'text');
        return Promise.resolve(true);
      }
    } catch (_) {}
    if (navigator.clipboard?.writeText) {
      return navigator.clipboard.writeText(text).then(() => true).catch(() => false);
    }
    try {
      const ta = document.createElement('textarea');
      ta.value = text;
      ta.style.position = 'fixed';
      ta.style.opacity = '0';
      document.body.appendChild(ta);
      ta.select();
      const ok = document.execCommand('copy');
      ta.remove();
      return Promise.resolve(!!ok);
    } catch (_) {
      return Promise.resolve(false);
    }
  }

  function showPowerMenuToast(message) {
    if (!message) return;
    document.getElementById('pe-toast')?.remove();
    const toast = document.createElement('div');
    toast.id = 'pe-toast';
    toast.setAttribute('data-pe-ui', 'true');
    toast.textContent = message;
    document.body.appendChild(toast);
    setTimeout(() => toast.remove(), 2600);
  }

  async function openReverseImageSearchProvider(providerId, imageUrl) {
    const provider = getReverseImageSearchProvider(providerId);
    if (provider.mode === 'copy-open') {
      window.open(provider.homeUrl, '_blank', 'noopener');
      await copyTextToClipboard(imageUrl);
      showPowerMenuToast(provider.copiedMessage || 'Image URL copied.');
      return;
    }
    window.open(provider.build(imageUrl), '_blank', 'noopener');
  }

  function removeReverseImageSearchMenu() {
    document.getElementById('pe-reverse-image-search-menu')?.remove();
  }

  function removeReverseImageSearchButton() {
    removeReverseImageSearchMenu();
    document.getElementById('pe-reverse-image-search-slot')?.remove();
  }

  function showReverseImageSearchMenu(anchor, imageUrl) {
    removeReverseImageSearchMenu();
    if (!imageUrl) return;
    const menu = document.createElement('div');
    menu.id = 'pe-reverse-image-search-menu';
    menu.setAttribute('data-pe-ui', 'true');
    menu.innerHTML = REVERSE_IMAGE_SEARCH_PROVIDERS.map(provider => `
      <button type="button" data-provider="${provider.id}">${provider.name}</button>
    `).join('');
    document.body.appendChild(menu);
    const rect = anchor.getBoundingClientRect();
    menu.style.top = Math.max(8, rect.bottom + 6) + 'px';
    menu.style.left = Math.max(8, Math.min(Math.max(8, window.innerWidth - 184), rect.left - 64)) + 'px';
    menu.querySelectorAll('button').forEach(btn => {
      btn.addEventListener('click', async e => {
        e.preventDefault();
        e.stopPropagation();
        await openReverseImageSearchProvider(btn.dataset.provider, imageUrl);
        removeReverseImageSearchMenu();
      });
    });
    setTimeout(() => {
      document.addEventListener('click', removeReverseImageSearchMenu, { once: true, capture: true });
    }, 0);
  }

  function createReverseImageSearchButton() {
    if (!supportsCloseupActionBarEnhancements() || !get('reverseImageSearchButton') || get('hideReverseImageSearchButton')) {
      removeReverseImageSearchButton();
      return;
    }

    const iconRow = getCloseupActionIconRow();
    if (!iconRow) return;
    if (IS_MOBILE && !shouldShowMobileReverseImageSearchButton(iconRow)) {
      removeReverseImageSearchButton();
      return;
    }

    const existing = document.getElementById('pe-reverse-image-search-slot');
    if (existing && iconRow.contains(existing)) return;
    if (existing) existing.remove();

    const slot = document.createElement('div');
    slot.id = 'pe-reverse-image-search-slot';
    slot.className = 'oRZ5_s';
    slot.classList.add('pe-closeup-action-slot');
    if (IS_MOBILE) slot.classList.add('pe-mobile-closeup-action-slot');
    slot.dataset.peCloseupAction = 'reverse-search';
    slot.setAttribute('data-pe-ui', 'true');
    absorbCloseupActionEvents(slot);

    const item = document.createElement('div');
    item.className = 'ADXRXN';
    item.setAttribute('role', 'listitem');
    item.innerHTML = `
      <button id="pe-reverse-image-search-btn" class="euRXRl" type="button" aria-label="Reverse image search" title="Reverse image search">
        <svg viewBox="0 0 24 24" width="26" height="26" fill="currentColor" aria-hidden="true">
          <path d="M10.5 4a6.5 6.5 0 0 1 5.17 10.44l4.45 4.45-1.41 1.41-4.45-4.45A6.5 6.5 0 1 1 10.5 4zm0 2a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9zm8.5-4 .46 1.54L21 4l-1.54.46L19 6l-.46-1.54L17 4l1.54-.46z"/>
        </svg>
      </button>
    `;
    slot.appendChild(item);

    const btn = slot.querySelector('#pe-reverse-image-search-btn');
    btn.addEventListener('click', async e => {
      e.preventDefault();
      e.stopPropagation();
      e.stopImmediatePropagation?.();
      if (btn.disabled) return;
      btn.disabled = true;
      try {
        const imageUrl = findCurrentCloseupImageUrl(btn);
        if (imageUrl) showReverseImageSearchMenu(btn, imageUrl);
      } finally {
        btn.disabled = false;
      }
    }, true);

    insertCloseupActionSlot(iconRow, slot, 'reverse');
  }

  function initReverseImageSearchButton() {
    if (!supportsCloseupActionBarEnhancements()) {
      removeReverseImageSearchButton();
      return;
    }
    if (IS_MOBILE) {
      initMobileCloseupActionButtons();
      return;
    }
    if (!get('reverseImageSearchButton') || get('hideReverseImageSearchButton')) {
      removeReverseImageSearchButton();
      return;
    }
    createReverseImageSearchButton();
    if (_reverseImageSearchObs) return;
    const retry = debounce(createReverseImageSearchButton, 150);
    _reverseImageSearchObs = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      retry();
    });
    _reverseImageSearchObs.observe(document.documentElement, { childList: true, subtree: true });
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: BOARD DOWNLOADER
  // ═══════════════════════════════════════════════════════════════════
  function isBoardPage() {
    // URL heuristic: /username/boardname/  (exactly 2 non-empty path segments)
    const parts = location.pathname.replace(/\/$/, '').split('/').filter(Boolean);
    const skip  = new Set([
      'search','pin','_','settings','ideas','today','following',
      'explore','business','login','logout','create','about',
      'help','careers','news','collage-creation-tool',
    ]);
    const urlMatch = parts.length === 2 && !skip.has(parts[0]);
    // DOM confirmation: Pinterest board header is present
    const domMatch = !!document.querySelector(
      '[data-test-id="board-header-with-image"], [data-test-id="board-header-details"], [data-test-id="board-tools"]'
    );
    return urlMatch || domMatch;
  }

  // Snapshot whatever pin images are currently in the DOM into the
  // accumulator set.  Called repeatedly while scrolling so we catch
  // images before Pinterest's virtual list recycles those DOM nodes.
  // Also captures pin titles from title elements in each pin card.
  function snapshotPinUrls(seen, urls, names) {
    document.querySelectorAll('img[src*="i.pinimg.com"]').forEach(img => {
      // Skip tiny avatars/icons
      const w = img.naturalWidth || img.width;
      if (w && w < 80) return;
      // Skip images inside the "More Ideas" / suggested section at the bottom of boards
      if (img.closest('.moreIdeasOnBoard, [href*="more-ideas"], [href*="/_tools/"]')) return;
      let url = img.src;
      const m = url.match(OQ_RE);
      if (m) url = m[1] + '/originals' + m[2];
      if (!seen.has(url)) {
        const pinScope = img.closest(
          '[data-test-id="pinWrapper"], [data-grid-item="true"], [data-test-id="pin"], div[role="listitem"]'
        );
        seen.add(url);
        urls.push(url);
        names.set(url, extractPinTitleFromScope(pinScope));
      }
    });
  }

  // Snapshot video pins currently in the DOM into the accumulator.
  // Called alongside snapshotPinUrls so videos are captured before virtual-list recycling.
  function snapshotVideoUrls(vidSeen, vidItems) {
    document.querySelectorAll('video').forEach(vid => {
      const src = findPinterestVideoSrc(vid);
      if (!src || /i\.pinimg\.com/.test(src)) return; // skip GIFs
      const m = src.match(/v1\.pinimg\.com\/videos\/(mc|iht)\/(?:expMp4|720p|hls)\/([a-f0-9]{2}\/[a-f0-9]{2}\/[a-f0-9]{2}\/[a-f0-9]{32,})/i);
      if (!m) return;
      const key = m[1] + '/' + m[2];
      if (vidSeen.has(key)) return;
      vidSeen.add(key);
      const pinScope = vid.closest(
        '[data-test-id="pinWrapper"], [data-grid-item="true"], [data-test-id="pin"], div[role="listitem"]'
      );
      vidItems.push({ channel: m[1], hash: m[2], title: extractPinTitleFromScope(pinScope) });
    });
  }

  // Scroll to the bottom, snapshotting URLs at each tick so virtualised
  // DOM nodes are captured before they get removed.  Returns accumulated
  // URL array.  Stall threshold is intentionally generous (12 × 900ms =
  // 10.8 s) because Pinterest's lazy load can pause for several seconds.
  async function autoScrollAndCollect(setStatus) {
    const seen     = new Set();
    const urls     = [];
    const names    = new Map();
    const vidSeen  = new Set();
    const vidItems = [];
    return new Promise(resolve => {
      let lastH = 0, stall = 0;
      const t = setInterval(() => {
        snapshotPinUrls(seen, urls, names);            // grab current DOM before scroll
        snapshotVideoUrls(vidSeen, vidItems);
        window.scrollTo(0, document.body.scrollHeight);
        const h = document.body.scrollHeight;
        setStatus('scroll', urls.length + vidItems.length, 0);
        if (h === lastH) {
          stall++;
          if (stall >= 12) {
            snapshotPinUrls(seen, urls, names);        // final grab
            snapshotVideoUrls(vidSeen, vidItems);
            clearInterval(t);
            window.scrollTo(0, 0);
            resolve({ urls, names, vidItems });
          }
        } else {
          stall = 0;
          lastH = h;
        }
      }, 900);
    });
  }

  // ─── collect + scroll helpers (shared by both download modes) ──────
  async function collectAllPins(setStatus) {
    setStatus('scroll', 0, 0);
    return autoScrollAndCollect(setStatus);
  }

  // Fetch up to `concurrency` URLs in parallel, calling onProgress after each.
  async function fetchParallel(urls, concurrency, onProgress) {
    const results = new Array(urls.length).fill(null);
    let nextIdx = 0, finished = 0;
    async function worker() {
      while (nextIdx < urls.length) {
        const i = nextIdx++;
        try { results[i] = await fetchBinary(urls[i]); } catch (_) {}
        onProgress(++finished, urls.length);
      }
    }
    await Promise.all(Array.from({ length: Math.min(concurrency, urls.length) }, worker));
    return results;
  }

  // ─── Save all board images + videos as named downloads ──────────
  async function downloadBoardFolder(setStatus) {
    const { urls, names, vidItems } = await collectAllPins(setStatus);
    const totalItems = urls.length + vidItems.length;
    if (!totalItems) { alert('[Pinterest Power Menu] No images or videos found on this board.'); return; }

    // Use pin title only. If unavailable, use: "Pin - 12345678".
    function makeFileName(url, ext) {
      let pinName = stripKnownExt(sanitizeFilename(names.get(url) || ''));
      if (!pinName) pinName = makeFallbackPinName();
      if (pinName.length > 120) pinName = pinName.slice(0, 120).trimEnd();
      return `${pinName}${ext}`;
    }

    // ── Download board image files ────────────────────────────────
    const bufs = await fetchParallel(urls, 5, (done, _) =>
      setStatus('fetch', done, totalItems)
    );

    let saved = 0;
    for (let i = 0; i < bufs.length; i++) {
      const buf = bufs[i];
      if (!buf) continue;
      const ext      = detectFileType(new Uint8Array(buf));
      const fileName = makeFileName(urls[i], ext);
      try {
        const a    = document.createElement('a');
        a.href     = URL.createObjectURL(new Blob([buf]));
        a.download = fileName;
        a.click();
        setTimeout(() => URL.revokeObjectURL(a.href), 200);
        await new Promise(r => setTimeout(r, 300));
        saved++;
      } catch (_) {}
      setStatus('fetch', saved, totalItems);
    }

    // ── Download videos ───────────────────────────────────────────
    for (const vi of vidItems) {
      const fallbackUrls = vi.channel === 'mc'
        ? [
            `https://v1.pinimg.com/videos/mc/720p/${vi.hash}.mp4`,
            `https://v1.pinimg.com/videos/mc/expMp4/${vi.hash}_t4.mp4`,
            `https://v1.pinimg.com/videos/mc/expMp4/${vi.hash}_t3.mp4`,
            `https://v1.pinimg.com/videos/mc/expMp4/${vi.hash}_t2.mp4`,
            `https://v1.pinimg.com/videos/mc/expMp4/${vi.hash}_t1.mp4`,
          ]
        : [`https://v1.pinimg.com/videos/iht/expMp4/${vi.hash}_720w.mp4`];
      const title = stripKnownExt(sanitizeFilename(vi.title || '')) || makeFallbackPinName();
      try {
        await downloadVideoFile(fallbackUrls, title, null);
        saved++;
      } catch (_) {}
      setStatus('fetch', saved, totalItems);
    }

    setStatus('done', saved, totalItems);
  }

  // ─── Board downloader button (lives inside #pe-settings-wrap) ───
  function removeBoardDownloaderUI() {
    // Remove button, menu, and any legacy outer wrapper
    ['pe-bd-btn', 'pe-bd-menu', 'pe-bd-fab'].forEach(id => {
      const el = document.getElementById(id);
      if (el) { if (el._bdCleanup) el._bdCleanup(); el.remove(); }
    });
  }

  function createBoardDownloaderUI() {
    if (document.getElementById('pe-bd-fab')) return;
    if (!get('boardDownloader') || !isBoardPage()) return;
    removeBoardDownloaderUI();

    // Standalone fixed container — independent of #pe-settings-wrap to avoid
    // timing/race issues with the MutationObserver that calls this function.
    const fab = document.createElement('div');
    fab.id = 'pe-bd-fab';
    fab.setAttribute('data-pe-ui', 'true');

    // Popup menu (appears above the button)
    const menu = document.createElement('div');
    menu.id = 'pe-bd-menu';
    menu.style.display = 'none';
    menu.innerHTML = `
      <div id="pe-bd-status" style="display:none"></div>
      <button class="pe-bd-opt" id="pe-bd-folder">
        <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
          <path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 19v2h14v-2H5z"/>
        </svg>
        Download All
      </button>
    `;

    // Circular board download button
    const btn = document.createElement('button');
    btn.id = 'pe-bd-btn';
    btn.title = 'Download Board';
    btn.innerHTML = `<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 19v2h14v-2H5z"/></svg>`;

    fab.appendChild(menu);
    fab.appendChild(btn);
    document.body.appendChild(fab);

    const status = menu.querySelector('#pe-bd-status');
    const dirBtn = menu.querySelector('#pe-bd-folder');

    let menuOpen = false;
    function toggleMenu() {
      menuOpen = !menuOpen;
      menu.style.display = menuOpen ? 'block' : 'none';
    }
    btn.addEventListener('click', e => { e.stopPropagation(); toggleMenu(); });

    function onOutsideClick(e) {
      if (isPowerMenuEvent(e) && !fab.contains(e.target)) return;
      if (menuOpen && !fab.contains(e.target)) { menuOpen = false; menu.style.display = 'none'; }
    }
    document.addEventListener('click', onOutsideClick);
    // Store cleanup on fab so removeBoardDownloaderUI can detach the listener
    fab._bdCleanup = () => document.removeEventListener('click', onOutsideClick);

    function setStatus(phase, a, b) {
      if (phase === 'cancelled') {
        status.style.display = 'none';
        dirBtn.disabled = false;
        return;
      }
      status.style.display = 'block';
      if (phase === 'scroll')      status.textContent = `Scrolling… ${a} items found`;
      else if (phase === 'fetch')  status.textContent = `Saving ${a}/${b} (${b ? Math.round(a/b*100) : 0}%)`;
      else if (phase === 'done') {
        status.textContent = `✓ Done – ${a} files saved`;
        setTimeout(() => {
          status.style.display = 'none';
          dirBtn.disabled = false;
          menuOpen = false; menu.style.display = 'none';
        }, 3000);
      }
    }

    dirBtn.addEventListener('click', async () => {
      dirBtn.disabled = true;
      await downloadBoardFolder(setStatus);
    });
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: QUICK DOWNLOAD HELPERS
  //  Reuses Pinterest video URLs for the row Download button.
  // ═══════════════════════════════════════════════════════════════════

  // Find the best downloadable video URL from a <video> element.
  // Checks all <source> elements and attributes; prefers direct MP4 over HLS.
  function findPinterestVideoSrc(vid) {
    const candidates = [];
    // Collect all <source> src attrs first (more reliable than currentSrc when HLS.js is active)
    vid.querySelectorAll('source').forEach(s => {
      const u = s.getAttribute('src') || s.getAttribute('data-src') || '';
      if (u) candidates.push(u);
    });
    // Then currentSrc / src attributes
    candidates.push(vid.currentSrc || '', vid.getAttribute('src') || '', vid.getAttribute('data-src') || '');
    // Prefer direct v1.pinimg.com MP4 (non-m3u8)
    for (const u of candidates) {
      if (/v1\.pinimg\.com\/videos/.test(u) && !/\.m3u8/.test(u)) return u;
    }
    // Fall back to any v1.pinimg.com URL (incl. HLS, so we can still extract hash)
    for (const u of candidates) {
      if (/v1\.pinimg\.com\/videos/.test(u)) return u;
    }
    return null;
  }

  // Download a video file with progress feedback.
  // Tries every URL in order; on any error (network, timeout, or non-2xx) moves to the next.
  // Mobile uses responseType:'blob' (streamed to disk) to avoid loading the whole file into RAM.
  function downloadVideoFile(urls, filename, onProgress) {
    return new Promise((resolve, reject) => {
      let idx = 0;
      function tryNext() {
        if (idx >= urls.length) { reject(new Error('all URLs failed')); return; }
        const url = urls[idx++];
        // settled + timer prevent double-calls when abort races with onerror/ontimeout
        let settled = false;
        let timer;
        function finish(fn) {
          if (settled) return;
          settled = true;
          clearTimeout(timer);
          fn();
        }
        const req = GM_xmlhttpRequest({
          method: 'GET', url,
          // blob for mobile — wider support on iOS/Android userscript managers than arraybuffer
          responseType: IS_MOBILE ? 'blob' : 'arraybuffer',
          // Spoof desktop UA so Pinterest CDN doesn't reject the request based on mobile UA
          headers: {
            'Referer':    location.href,
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
            'Accept':     'video/mp4,video/*;q=0.9,*/*;q=0.8',
          },
          onprogress: e => { if (e.lengthComputable && onProgress) onProgress(e.loaded, e.total); },
          onload: r => {
            if (r.status >= 200 && r.status < 300) {
              finish(() => {
                const base = stripKnownExt(sanitizeFilename(filename || '')) || makeFallbackPinName();
                const blob = IS_MOBILE ? r.response : new Blob([r.response], { type: 'video/mp4' });
                const a    = document.createElement('a');
                a.href     = URL.createObjectURL(blob);
                a.download = base + '.mp4';
                document.body.appendChild(a);
                a.click();
                setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 10000);
                resolve();
              });
            } else {
              finish(tryNext);
            }
          },
          onerror:   () => finish(tryNext),
          ontimeout: () => finish(tryNext),
        });
        // Manual 45s deadline — mobile connections sometimes hang indefinitely
        timer = setTimeout(() => finish(() => { try { req.abort(); } catch(_){} tryNext(); }), 45000);
      }
      tryNext();
    });
  }

  // When the XHR interceptor captures a video URL, lightly refresh the row button.
  // The button is already visible; this only helps late-rendered closeup rows.
  _onVideoUrlCapture = function () {
    if (!/\/pin\/\d/i.test(location.pathname)) return;
    if (IS_MOBILE) scheduleMobileCloseupActionButtonsRefresh();
    else if (supportsCloseupActionBarEnhancements() && !document.getElementById('pe-closeup-image-dl-slot'))
      setTimeout(createCloseupImageDownloadButton, 50);
  };


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: CUSTOM PINTEREST LOGO
  // ═══════════════════════════════════════════════════════════════════
  let _customLogoObs = null;
  let _customLogoRescan = null;

  function normalizeCustomLogoUrl(value) {
    const url = String(value || '').trim();
    if (!url) return '';
    if (/^(https?:\/\/|data:image\/|blob:)/i.test(url)) return url;
    return '';
  }

  function getCustomPinterestLogoSize() {
    const size = Number(get('customPinterestLogoSize'));
    return Number.isFinite(size) ? Math.max(8, Math.round(size)) : 32;
  }

  function getCustomPinterestLogoSizeFromInput(input) {
    const size = Number(input?.value);
    return Number.isFinite(size) ? Math.max(8, Math.round(size)) : 32;
  }

  function removeCustomPinterestLogo(root = document) {
    root.querySelectorAll?.('.pe-custom-logo-img').forEach(img => img.remove());
    root.querySelectorAll?.('[data-test-id="pinterest-logo-home-button"] svg').forEach(svg => {
      svg.style.removeProperty('display');
    });
  }

  function applyCustomPinterestLogo(root = document) {
    const url = normalizeCustomLogoUrl(get('customPinterestLogoUrl'));
    if (!url) {
      removeCustomPinterestLogo(root);
      return;
    }

    const buttons = root.querySelectorAll?.(
      '[data-test-id="pinterest-logo-home-button"] a[aria-label="Home"], ' +
      '[data-test-id="pinterest-logo-home-button"] [aria-label="Home"]'
    ) || [];
    const size = getCustomPinterestLogoSize();
    const circle = !!get('customPinterestLogoCircle');
    buttons.forEach(home => {
      const frame = home.querySelector('.VHreRh') || home.firstElementChild || home;
      frame.style.setProperty('--pe-custom-logo-size', size + 'px');
      frame.querySelectorAll('svg').forEach(svg => {
        svg.style.setProperty('display', 'none', 'important');
      });
      let img = frame.querySelector(':scope > .pe-custom-logo-img');
      if (!img) {
        img = document.createElement('img');
        img.className = 'pe-custom-logo-img';
        img.alt = 'Home';
        frame.appendChild(img);
      }
      if (img.src !== url) img.src = url;
      img.style.setProperty('--pe-custom-logo-size', size + 'px');
      img.classList.toggle('pe-custom-logo-circle', circle);
    });
  }

  function stopCustomPinterestLogo() {
    if (_customLogoObs) { _customLogoObs.disconnect(); _customLogoObs = null; }
    removeCustomPinterestLogo(document);
  }

  function initCustomPinterestLogo() {
    if (IS_MOBILE) {
      stopCustomPinterestLogo();
      return;
    }
    const url = normalizeCustomLogoUrl(get('customPinterestLogoUrl'));
    if (!url) {
      stopCustomPinterestLogo();
      return;
    }
    applyCustomPinterestLogo(document);
    if (_customLogoObs) return;
    _customLogoRescan = debounce(() => applyCustomPinterestLogo(document), 250);
    _customLogoObs = new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      _customLogoRescan();
    });
    _customLogoObs.observe(document.documentElement, { childList: true, subtree: true });
  }


  // ═══════════════════════════════════════════════════════════════════
  //  SETTINGS PANEL UI  –  circle gear FAB, popup above it
  // ═══════════════════════════════════════════════════════════════════
  const FEATURES = [
    { key: 'originalQuality', label: 'Original Quality',       desc: 'Full-res images instead of thumbnails',                    reload: true  },
    { key: 'downloadFixer',   label: 'Download Fixer',         desc: 'Proper filenames & format detection',                      reload: true  },
    { key: 'gifHover',        label: 'GIF Hover Play',         desc: 'GIFs play on hover, pause on leave',                       reload: false },
    { key: 'gifAutoPlay',     label: 'Auto-Play Visible GIFs', desc: 'Auto-play all GIFs on screen, stop when scrolled away',    reload: false },
    { key: 'videoAutoPlay',   label: 'Auto-Play Visible Videos', desc: 'Auto-play all pin videos on screen (muted), pause when scrolled away', reload: false },
    { key: 'infiniteLoopVideo', label: 'Loop Closeup Videos',  desc: 'Auto-replay closeup videos instead of showing the "Watch again" button', reload: false },
    { key: 'boardDownloader', label: 'Board Downloader',       desc: 'Download all images from the current board',              reload: true  },
    { key: 'declutter',       label: 'Declutter',              desc: 'Remove ads, quizzes, sponsored & shopping pins',           reload: false },
    { key: 'removeVideos',    label: 'Remove Videos',          desc: 'Remove all video pins from the feed',                      reload: false },
    { key: 'contextMenu',     label: 'Image Context Menu',     desc: 'Right-click pins to copy, open or save the original',      reload: false },
    { key: 'reverseImageSearchButton', label: 'Reverse Image Search Button', desc: 'Show reverse search providers above closeup images', reload: false },
  ];
  const VISIBLE_FEATURES = FEATURES;

  const DECLUTTER_FEATURES = [
    { key: 'declutterShopTheLook', label: 'Hide Shop The Look Modules', desc: 'Collapse Shop the Look shopping carousels and product modules', reload: false },
    { key: 'declutterSearchAdvisory', label: 'Hide Search Support Advisory', desc: 'Collapse Pinterest support advisory cards in search results', reload: false },
    { key: 'hideShopPosts', label: 'Hide Shop Posts', desc: 'Collapse pins from shops (Amazon, Etsy, eBay, TeePublic, Redbubble, AliExpress)', reload: false },
  ];

  const TRANSLATE_FEATURES = [
    { key: 'autoTranslateTitles', label: 'Translate Pin Titles', desc: 'Auto-translate visible closeup titles', reload: false },
    { key: 'autoTranslateDescriptions', label: 'Translate Pin Descriptions', desc: 'Auto-translate visible pin descriptions', reload: false },
    { key: 'autoTranslateComments', label: 'Translate Comments', desc: 'Auto-translate visible expanded comments', reload: false },
  ];

  const TRANSLATE_LANG_OPTIONS = [
    { value: 'browser', label: 'Browser default' },
    { value: 'en', label: 'English' },
    { value: 'es', label: 'Spanish' },
    { value: 'fr', label: 'French' },
    { value: 'de', label: 'German' },
    { value: 'it', label: 'Italian' },
    { value: 'pt', label: 'Portuguese' },
    { value: 'ja', label: 'Japanese' },
    { value: 'ko', label: 'Korean' },
    { value: 'zh', label: 'Chinese' },
  ];

  const TITLE_TRANSLATION_DISPLAY_OPTIONS = [
    { value: 'translated', label: 'Translated only', mode: 'translated-only' },
    { value: 'both', label: 'Original + translated', mode: 'original + translated' },
  ];

  const COMMENT_TRANSLATION_MODE_OPTIONS = [
    { value: 'visible', label: 'Visible comments only (current)' },
    { value: 'conservative', label: 'Conservative / fewer at once' },
  ];

  const DARK_MODE_OPTIONS = [
    { value: 'auto',  label: 'Auto (follow Pinterest)' },
    { value: 'light', label: 'Light' },
    { value: 'dark',  label: 'Dark' },
  ];

  const HIDE_FEATURES = [
    { key: 'hideVisitSite',  label: 'Hide Visit Site',          desc: 'Remove all "Visit site" buttons',                         reload: false },
    { key: 'hideUpdates',    label: 'Hide Updates Bell',        desc: 'Hide the Updates / notifications button',                 reload: false },
    { key: 'hideMessages',   label: 'Hide Messages Button',     desc: 'Hide the Messages / notifications button in the nav',     reload: false },
    { key: 'hideShare',      label: 'Hide Share Button',        desc: 'Hide the Share / Send button on pins',                    reload: false },
    { key: 'hideReactButton', label: 'Hide React Button',        desc: 'Hide the heart and reaction count above closeup images',  reload: false },
    { key: 'hideReactionCount', label: 'Hide Reaction Count',    desc: 'Hide only the numeric reaction count beside React',       reload: false },
    { key: 'hideUploadImageButton', label: 'Hide Upload Image Button', desc: 'Hide the Lens upload image button in search',      reload: false },
    { key: 'hideSearchImageButton', label: 'Hide Search Image Button', desc: 'Hide the visual search overlay button on images',  reload: false },
    { key: 'hideSearchSuggestions', label: 'Hide Search Suggestions', desc: 'Hide related search suggestion chips and cards',    reload: false },
    { key: 'hideViewLargerButton', label: 'Hide View Larger Button', desc: 'Hide the media viewer overlay button on images',     reload: false },
    { key: 'hideMoreOptionsButton', label: 'Hide More Options Button', desc: 'Hide the closeup More actions button',             reload: false },
    { key: 'hideReverseImageSearchButton', label: 'Hide Reverse Image Search Button', desc: 'Hide the custom reverse image search button', reload: false },
    { key: 'hideCommentButton', label: 'Hide Comment Button',   desc: 'Hide only the Comments button in action rows',           reload: false },
    { key: 'hideComments',   label: 'Hide Comment Section',     desc: 'Hide comment sections and comment input on pins',        reload: false },
    { key: 'hideCommentEmojiButton', label: 'Hide Comment Emoji Button', desc: 'Hide the emoji picker in comment composer',      reload: false },
    { key: 'hideCommentStickerButton', label: 'Hide Comment Sticker Button', desc: 'Hide the sticker picker in comment composer', reload: false },
    { key: 'hideCommentPhotoButton', label: 'Hide Comment Photo Button', desc: 'Hide the photo picker in comment composer',      reload: false },
  ];
  const VISIBLE_HIDE_FEATURES = IS_MOBILE ? HIDE_FEATURES.filter(f => f.key !== 'hideUploadImageButton') : HIDE_FEATURES;

  function escapeAttr(value) {
    return String(value || '').replace(/[&<>"']/g, ch => ({
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#39;',
    })[ch]);
  }

  function renderOptions(options, currentValue) {
    return options.map(opt =>
      `<option value="${opt.value}" data-mode="${opt.mode || ''}" ${currentValue === opt.value ? 'selected' : ''}>${opt.label}</option>`
    ).join('');
  }

  function createSettingsPanel() {
    if (document.getElementById('pe-settings-wrap')) return;
    const wrap = document.createElement('div');
    wrap.id = 'pe-settings-wrap';
    wrap.setAttribute('data-pe-ui', 'true');
    const customizeGroupHtml = IS_MOBILE ? '' : `
        <div class="pe-group">
          <div class="pe-group-header" id="pe-group-customize-hdr">
            <div class="pe-info">
              <span class="pe-name">Customize</span>
              <span class="pe-desc">Change the Pinterest logo image</span>
            </div>
            <svg class="pe-chevron" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
              <path d="M7 10l5 5 5-5z"/>
            </svg>
          </div>
          <div class="pe-group-body" id="pe-group-customize-body" style="display:none">
            <div class="pe-row pe-sub-row pe-input-row">
              <div class="pe-info">
                <span class="pe-name">Pinterest Logo URL</span>
                <span class="pe-desc">Paste an image link, or clear it to restore</span>
              </div>
              <input id="pe-custom-logo-input" class="pe-setting-input" type="url" placeholder="https://example.com/logo.png" value="${escapeAttr(get('customPinterestLogoUrl'))}">
            </div>
            <div class="pe-row pe-sub-row pe-input-row">
              <div class="pe-info">
                <span class="pe-name">Logo Size</span>
                <span class="pe-desc">Pixel size for the custom logo</span>
              </div>
              <input id="pe-custom-logo-size" class="pe-setting-input pe-setting-number" type="number" min="8" step="1" value="${escapeAttr(getCustomPinterestLogoSize())}">
            </div>
            <div class="pe-row pe-sub-row">
              <div class="pe-info">
                <span class="pe-name">Circle Logo Crop</span>
                <span class="pe-desc">Crop boxy images into a round logo</span>
              </div>
              <label class="pe-switch">
                <input id="pe-custom-logo-circle" type="checkbox" data-key="customPinterestLogoCircle" data-reload="false" ${get('customPinterestLogoCircle') ? 'checked' : ''}>
                <span class="pe-knob"></span>
              </label>
            </div>
          </div>
        </div>`;
    wrap.innerHTML = `
      <div id="pe-settings-panel" style="display:none">
        <div id="pe-settings-title">Pinterest Power Menu <span id="pe-settings-by">By <a id="pe-settings-author" href="https://github.com/Angel2mp3" target="_blank" rel="noopener">Angel</a></span></div>
        ${VISIBLE_FEATURES.map(f => `
          <div class="pe-row">
            <div class="pe-info">
              <span class="pe-name">${f.label}</span>
              <span class="pe-desc">${f.desc}</span>
            </div>
            <label class="pe-switch">
              <input type="checkbox" data-key="${f.key}" data-reload="${f.reload}" ${get(f.key) ? 'checked' : ''}>
              <span class="pe-knob"></span>
            </label>
          </div>`).join('')}
        <div class="pe-group">
          <div class="pe-group-header" id="pe-group-declutter-hdr">
            <div class="pe-info">
              <span class="pe-name">Declutter Options</span>
              <span class="pe-desc">Extra cleanup controlled by Declutter</span>
            </div>
            <svg class="pe-chevron" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
              <path d="M7 10l5 5 5-5z"/>
            </svg>
          </div>
          <div class="pe-group-body" id="pe-group-declutter-body" style="display:none">
            ${DECLUTTER_FEATURES.map(f => `
              <div class="pe-row pe-sub-row">
                <div class="pe-info">
                  <span class="pe-name">${f.label}</span>
                  <span class="pe-desc">${f.desc}</span>
                </div>
                <label class="pe-switch">
                  <input type="checkbox" data-key="${f.key}" data-reload="${f.reload}" ${get(f.key) ? 'checked' : ''}>
                  <span class="pe-knob"></span>
                </label>
              </div>`).join('')}
          </div>
        </div>
        <div class="pe-group">
          <div class="pe-group-header" id="pe-group-translate-hdr">
            <div class="pe-info">
              <span class="pe-name">Translate</span>
              <span class="pe-desc">Auto translation controls</span>
            </div>
            <svg class="pe-chevron" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
              <path d="M7 10l5 5 5-5z"/>
            </svg>
          </div>
          <div class="pe-group-body" id="pe-group-translate-body" style="display:none">
            ${TRANSLATE_FEATURES.map(f => `
              <div class="pe-row pe-sub-row">
                <div class="pe-info">
                  <span class="pe-name">${f.label}</span>
                  <span class="pe-desc">${f.desc}</span>
                </div>
                <label class="pe-switch">
                  <input type="checkbox" data-key="${f.key}" data-reload="${f.reload}" ${get(f.key) ? 'checked' : ''}>
                  <span class="pe-knob"></span>
                </label>
              </div>`).join('')}
            <div class="pe-row pe-sub-row pe-select-row">
              <div class="pe-info">
                <span class="pe-name">Target Language</span>
                <span class="pe-desc">Browser language by default</span>
              </div>
              <select class="pe-setting-select" data-key="autoTranslateTarget">
                ${renderOptions(TRANSLATE_LANG_OPTIONS, get('autoTranslateTarget'))}
              </select>
            </div>
            <div class="pe-row pe-sub-row pe-select-row">
              <div class="pe-info">
                <span class="pe-name">Title Display</span>
                <span class="pe-desc">Choose translated-only or original + translated</span>
              </div>
              <select class="pe-setting-select" data-key="titleTranslationDisplay">
                ${renderOptions(TITLE_TRANSLATION_DISPLAY_OPTIONS, get('titleTranslationDisplay'))}
              </select>
            </div>
            <div class="pe-row pe-sub-row pe-select-row">
              <div class="pe-info">
                <span class="pe-name">Comment Translation Mode</span>
                <span class="pe-desc">Control how aggressively comments are queued</span>
              </div>
              <select class="pe-setting-select" data-key="autoTranslateCommentMode">
                ${renderOptions(COMMENT_TRANSLATION_MODE_OPTIONS, get('autoTranslateCommentMode'))}
              </select>
            </div>
          </div>
        </div>
        ${customizeGroupHtml}
        <div class="pe-group">
          <div class="pe-group-header" id="pe-group-hide-hdr">
            <div class="pe-info">
              <span class="pe-name">Hide UI Elements</span>
              <span class="pe-desc">Hide buttons & interface elements</span>
            </div>
            <svg class="pe-chevron" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
              <path d="M7 10l5 5 5-5z"/>
            </svg>
          </div>
          <div class="pe-group-body" id="pe-group-hide-body" style="display:none">
            ${VISIBLE_HIDE_FEATURES.map(f => `
              <div class="pe-row pe-sub-row">
                <div class="pe-info">
                  <span class="pe-name">${f.label}</span>
                  <span class="pe-desc">${f.desc}</span>
                </div>
                <label class="pe-switch">
                  <input type="checkbox" data-key="${f.key}" data-reload="${f.reload}" ${get(f.key) ? 'checked' : ''}>
                  <span class="pe-knob"></span>
                </label>
              </div>`).join('')}
          </div>
        </div>
        <div class="pe-row pe-select-row">
          <div class="pe-info">
            <span class="pe-name">Dark Mode</span>
            <span class="pe-desc">Appearance of the settings panel and FAB</span>
          </div>
          <select class="pe-setting-select" data-key="darkMode">
            ${renderOptions(DARK_MODE_OPTIONS, get('darkMode'))}
          </select>
        </div>
        <div id="pe-notice" style="display:none">
          <span>↺ Reload to apply</span>
          <button id="pe-reload-btn">Reload now</button>
        </div>
      </div>
      <button id="pe-settings-btn" title="Pinterest Power Menu Settings">
        <svg viewBox="0 0 24 24" width="17" height="17" fill="currentColor">
          <path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.92c.04-.36.07-.72.07-1.08s-.03-.73-.07-1.08l2.32-1.82c.21-.16.27-.45.13-.69l-2.2-3.81a.51.51 0 0 0-.63-.22l-2.74 1.1c-.57-.44-1.18-.81-1.85-1.09l-.42-2.91A.51.51 0 0 0 13.5 1h-3c-.27 0-.5.19-.54.46l-.41 2.91c-.67.28-1.28.64-1.85 1.09L4.97 4.37a.51.51 0 0 0-.63.22L2.14 8.4c-.14.24-.08.53.13.69l2.32 1.82C4.55 11.27 4.5 11.63 4.5 12s.04.73.09 1.08l-2.32 1.82c-.21.16-.27.45-.13.69l2.2 3.81c.13.24.42.32.63.22l2.74-1.1c.57.44 1.18.8 1.85 1.09l.41 2.91c.04.27.27.46.54.46h3c.27 0 .5-.19.54-.46l.41-2.91c.67-.28 1.28-.65 1.85-1.09l2.74 1.1a.5.5 0 0 0 .63-.22l2.2-3.81c.14-.24.08-.53-.13-.69z"/>
        </svg>
      </button>
    `;
    document.body.appendChild(wrap);

    const panel  = wrap.querySelector('#pe-settings-panel');
    const btn    = wrap.querySelector('#pe-settings-btn');
    let panelOpen = false;

    function stopSettingsPanelEventBubble(e) {
      e.stopPropagation();
    }

    function togglePanel() {
      panelOpen = !panelOpen;
      panel.style.display = panelOpen ? 'block' : 'none';
      btn.classList.toggle('pe-settings-open', panelOpen);
    }
    panel.addEventListener('wheel', stopSettingsPanelEventBubble, { passive: true });
    panel.addEventListener('touchmove', stopSettingsPanelEventBubble, { passive: true });
    wrap.addEventListener('click', stopSettingsPanelEventBubble);
    wrap.addEventListener('pointerdown', stopSettingsPanelEventBubble);
    wrap.addEventListener('touchstart', stopSettingsPanelEventBubble, { passive: true });
    btn.addEventListener('click', e => { e.stopPropagation(); togglePanel(); });
    document.addEventListener('click', e => {
      if (panelOpen && !wrap.contains(e.target)) { panelOpen = false; panel.style.display = 'none'; btn.classList.remove('pe-settings-open'); }
    });

    // Collapsible settings groups
    const declutterHdr  = wrap.querySelector('#pe-group-declutter-hdr');
    const declutterBody = wrap.querySelector('#pe-group-declutter-body');
    declutterHdr.addEventListener('click', () => {
      const open = declutterBody.style.display !== 'none';
      declutterBody.style.display = open ? 'none' : 'block';
      declutterHdr.classList.toggle('pe-group-open', !open);
    });

    const translateHdr  = wrap.querySelector('#pe-group-translate-hdr');
    const translateBody = wrap.querySelector('#pe-group-translate-body');
    translateHdr.addEventListener('click', () => {
      const open = translateBody.style.display !== 'none';
      translateBody.style.display = open ? 'none' : 'block';
      translateHdr.classList.toggle('pe-group-open', !open);
    });

    const customizeHdr  = wrap.querySelector('#pe-group-customize-hdr');
    const customizeBody = wrap.querySelector('#pe-group-customize-body');
    if (customizeHdr && customizeBody) {
      customizeHdr.addEventListener('click', () => {
        const open = customizeBody.style.display !== 'none';
        customizeBody.style.display = open ? 'none' : 'block';
        customizeHdr.classList.toggle('pe-group-open', !open);
      });
    }

    const hideHdr  = wrap.querySelector('#pe-group-hide-hdr');
    const hideBody = wrap.querySelector('#pe-group-hide-body');
    hideHdr.addEventListener('click', () => {
      const open = hideBody.style.display !== 'none';
      hideBody.style.display = open ? 'none' : 'block';
      hideHdr.classList.toggle('pe-group-open', !open);
    });

    // Toggle switches
    wrap.querySelectorAll('input[type="checkbox"]').forEach(cb => {
      cb.addEventListener('change', () => {
        const key = cb.dataset.key;
        set(key, cb.checked);
        if (key === 'hideVisitSite') applyVisitSiteToggle();
        if (key === 'gifHover') { pauseActiveGif(); document.querySelectorAll('video').forEach(pauseVidOnAdd); }
        if (key === 'gifAutoPlay') { if (cb.checked) initGifAutoPlay(); else stopGifAutoPlay(); }
        if (key === 'videoAutoPlay') {
          if (cb.checked) initVideoAutoPlay();
          else { stopVideoAutoPlay(); document.querySelectorAll('video').forEach(pauseVidOnAdd); }
        }
        if (key === 'infiniteLoopVideo') {
          applyInfiniteLoopVideoToggle();
          if (cb.checked) initInfiniteLoopVideo(); else stopInfiniteLoopVideo();
        }
        if (key === 'declutter') { applyDeclutterToggle(); if (cb.checked) { hideShopTheLookModules(document); hideDeclutterMobileInlineVisitButtons(document); initDeclutter(); if (get('hideShopPosts')) initHideShopPosts(); } else stopHideShopPosts({ restore: true }); }
        if (key === 'declutterShopTheLook') { applyDeclutterToggle(); if (get('declutter')) hideShopTheLookModules(document); }
        if (key === 'declutterSearchAdvisory') applyDeclutterToggle();
        if (key === 'removeVideos') { if (cb.checked) initRemoveVideos(); }
        if (key === 'contextMenu') { if (cb.checked) initImageContextMenu(); else stopImageContextMenu(); }
        if (key === 'hideUpdates' || key === 'hideMessages' || key === 'hideShare' || key === 'hideReactButton' || key === 'hideUploadImageButton' || key === 'hideSearchImageButton' || key === 'hideViewLargerButton' || key === 'hideMoreOptionsButton' || key === 'hideReverseImageSearchButton' || key === 'hideCommentButton') {
          applyNavToggles();
          scheduleMobileCloseupActionButtonsRefresh();
        }
        if (key === 'hideReactionCount' || key === 'hideSearchSuggestions' || key === 'hideCommentEmojiButton' || key === 'hideCommentStickerButton' || key === 'hideCommentPhotoButton') {
          applyNavToggles();
          scheduleMobileCloseupActionButtonsRefresh();
        }
        if (key === 'hideMessages' && cb.checked) initMessagesRemover();
        if (key === 'hideShopPosts') { if (cb.checked && get('declutter')) initHideShopPosts(); else stopHideShopPosts({ restore: true }); }
        if (key === 'hideComments') { applyNavToggles(); if (cb.checked) initHideComments(); }
        if (TRANSLATE_FEATURES.some(f => f.key === key)) refreshTranslationFeatures();
        if (key === 'reverseImageSearchButton') { if (cb.checked) initReverseImageSearchButton(); else { removeReverseImageSearchButton(); scheduleMobileCloseupActionButtonsRefresh(); } }
        if (key === 'hideReverseImageSearchButton') { if (cb.checked) removeReverseImageSearchButton(); else if (get('reverseImageSearchButton')) initReverseImageSearchButton(); scheduleMobileCloseupActionButtonsRefresh(); }
        if (key === 'customPinterestLogoCircle') initCustomPinterestLogo();
        if (cb.dataset.reload === 'true')
          wrap.querySelector('#pe-notice').style.display = 'flex';
      });
    });

    wrap.querySelectorAll('.pe-setting-select').forEach(sel => {
      sel.addEventListener('change', () => {
        const k = sel.dataset.key;
        set(k, sel.value);
        if (k === 'darkMode') applyDarkMode();
        else refreshTranslationFeatures();
      });
    });

    const logoInput = wrap.querySelector('#pe-custom-logo-input');
    if (logoInput) {
      const saveLogoUrl = debounce(() => {
        set('customPinterestLogoUrl', logoInput.value.trim());
        initCustomPinterestLogo();
      }, 350);
      logoInput.addEventListener('input', saveLogoUrl);
      logoInput.addEventListener('change', () => {
        set('customPinterestLogoUrl', logoInput.value.trim());
        initCustomPinterestLogo();
      });
    }

    const logoSizeInput = wrap.querySelector('#pe-custom-logo-size');
    if (logoSizeInput) {
      const saveLogoSize = debounce(() => {
        set('customPinterestLogoSize', getCustomPinterestLogoSizeFromInput(logoSizeInput));
        initCustomPinterestLogo();
      }, 150);
      logoSizeInput.addEventListener('input', saveLogoSize);
      logoSizeInput.addEventListener('change', () => {
        set('customPinterestLogoSize', getCustomPinterestLogoSizeFromInput(logoSizeInput));
        initCustomPinterestLogo();
      });
    }

    wrap.querySelector('#pe-reload-btn').addEventListener('click', () => location.reload());
    applyDarkMode();
  }

  let _peDarkMql = null;
  let _peDarkObs = null;

  function isPinterestDarkTheme() {
    const html = document.documentElement;
    if (!html) return false;
    const scheme = html.getAttribute('data-color-scheme') || html.getAttribute('data-theme') || '';
    if (/dark/i.test(scheme)) return true;
    if (html.classList && (html.classList.contains('darkMode') || html.classList.contains('dark-mode') || html.classList.contains('dark'))) return true;
    try { return window.matchMedia('(prefers-color-scheme: dark)').matches; } catch (_) { return false; }
  }

  function applyDarkMode() {
    const wrap = document.getElementById('pe-settings-wrap');
    if (!wrap) return;
    const mode = get('darkMode');
    let dark = false;
    if (mode === 'dark') dark = true;
    else if (mode === 'auto') dark = isPinterestDarkTheme();
    wrap.classList.toggle('pe-dark', dark);

    if (!_peDarkMql) {
      try {
        _peDarkMql = window.matchMedia('(prefers-color-scheme: dark)');
        _peDarkMql.addEventListener('change', () => { if (get('darkMode') === 'auto') applyDarkMode(); });
      } catch (_) {}
    }
    if (!_peDarkObs) {
      _peDarkObs = new MutationObserver(() => { if (get('darkMode') === 'auto') applyDarkMode(); });
      _peDarkObs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-color-scheme', 'data-theme', 'class'] });
    }
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: IMAGE RIGHT-CLICK CONTEXT MENU
  // ═══════════════════════════════════════════════════════════════════
  // Intercepts right-clicks on (or near) any pinimg.com image and shows
  // a custom menu with options to copy/save the original-quality version.
  // Replaces the native browser menu only when a Pinterest image is
  // under the cursor; other right-clicks fall through normally.

  let _imageContextMenuStop = null;

  function initImageContextMenu() {
    // The custom context menu is mouse-only. On mobile the long-press handler
    // would compete with native browser actions (text selection, system menus),
    // so we skip the entire module on touch devices.
    if (IS_MOBILE) return;
    if (_imageContextMenuStop || !get('contextMenu')) return;

    let _ctxMenu = null;
    let _cleanupCtxMenu = null;

    function removeCtxMenu() {
      if (_cleanupCtxMenu) _cleanupCtxMenu();
      if (_ctxMenu) { _ctxMenu.remove(); _ctxMenu = null; }
    }

    function getMediaInfo(target) {
      let card = target.closest ? target.closest('[data-test-id="pin"], [data-grid-item="true"], [data-test-id="pin-closeup-image"], .PinCard') : null;
      let wrap = target.closest ? target.closest('[data-test-id="pinWrapper"], [data-test-id="pin-closeup-image"]') : null;
      let title = extractPinTitleFromScope(card || wrap);

      if (wrap) {
        // Video
        const vid = wrap.querySelector('video');
        if (vid) {
          const src = vid.src || (vid.querySelector('source') && vid.querySelector('source').src);
          if (src && !/i\.pinimg\.com/.test(src)) return { url: getHighestQualityVideoUrl(src), type: 'video', title };
        }
      }

      // Try finding nearest image
      let img = target;
      for (let i = 0; i < 15 && img && img !== document.body; i++) {
        if (img.tagName === 'IMG' && img.src && /pinimg\.com/i.test(img.src)) {
          break;
        }
        img = img.parentElement;
      }
      
      if (!img || img.tagName !== 'IMG' || !/pinimg\.com/i.test(img.src)) {
        if (wrap) {
           img = wrap.querySelector('img[src*="pinimg.com"]');
        } else if (card) {
           img = card.querySelector('img[src*="pinimg.com"]');
        } else {
           img = null;
        }
      }

      if (!img) return null;

      // Now determine if it's a GIF or Image
      // 1. Is it actively playing a GIF? (hover/auto-play swaps src)
      if (/\.gif(\?|$)/i.test(img.src)) {
        return { url: img.src, type: 'gif', title };
      }
      
      // 2. Does it have a GIF in its original srcset?
      const origSrcset = img.__peAutoOrigSrcset || img.getAttribute('srcset') || '';
      for (const part of origSrcset.split(',')) {
        const url = part.trim().split(/\s+/)[0];
        if (url && /\.gif(\?|$)/i.test(url)) return { url: url, type: 'gif', title };
      }

      // Otherwise, it's a standard image. Return original quality URL.
      return { url: getBestUrl(img), type: 'image', title };
    }

    // Return the best original-quality URL for an img element.
    function getBestUrl(img) {
      const base = img.__peAutoOrigSrc || img.src;
      const m = base.match(OQ_RE);
      return m ? m[1] + '/originals' + m[2] : base;
    }

    async function copyMediaToClipboard(origUrl, type) {
      const fallbackToText = () => copyTextToClipboard(origUrl);

      if (type === 'video' || type === 'gif') {
        // We cannot reliably put video or animated gif binaries into the OS clipboard 
        // without causing bugs like Discord pasting "message.txt".
        // Instead, copy the direct URL so it auto-embeds natively.
        fallbackToText();
        return;
      }

      const buf  = await fetchBinary(origUrl);
      const arr  = new Uint8Array(buf);
      const ext  = detectFileType(arr);
      const mime = ext === '.png' ? 'image/png'
                 : ext === '.gif' ? 'image/gif'
                 : ext === '.webp' ? 'image/webp'
                 : 'image/jpeg';

      if (mime === 'image/gif' || mime === 'image/webp') {
        fallbackToText();
        return;
      }

      let blob = new Blob([buf], { type: mime });

      if (mime !== 'image/png') {
        blob = await new Promise(res => {
          const bUrl = URL.createObjectURL(blob);
          const tmp  = new Image();
          tmp.crossOrigin = 'anonymous';
          tmp.onload = () => {
            const cv = document.createElement('canvas');
            cv.width  = tmp.naturalWidth;
            cv.height = tmp.naturalHeight;
            cv.getContext('2d').drawImage(tmp, 0, 0);
            cv.toBlob(b => { URL.revokeObjectURL(bUrl); res(b); }, 'image/png');
          };
          tmp.onerror = () => { URL.revokeObjectURL(bUrl); res(null); };
          tmp.src = bUrl;
        });
      }

      if (blob) {
        try {
          await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
        } catch (_) {
          fallbackToText();
        }
      } else {
        fallbackToText();
      }
    }

    // Long-press state for mobile context menu
    let _lpJustShown = false;
    let _lpTimer     = null;
    let _lpScrolled  = false;
    let _lpStartX = 0, _lpStartY = 0;

    // Extracted so both right-click and long-press can reuse the same menu logic.
    // isTouch = true adds a longer grace period before outside-click dismissal,
    // preventing the finger-lift tap from instantly closing the menu.
    function showCtxMenuAt(x, y, media, isTouch) {
      removeCtxMenu();
      const { url: origUrl, type, title } = media;
      const menuX = Math.min(x, window.innerWidth  - 236);
      const menuY = Math.min(y, window.innerHeight - 200);

      const menu = document.createElement('div');
      menu.id = 'pe-ctx-menu';
      menu.style.cssText = `left:${menuX}px;top:${menuY}px`;

      function addItem(svgD, label, action) {
        const item = document.createElement('button');
        item.className = 'pe-ctx-item';
        item.innerHTML =
          `<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">${svgD}</svg>` +
          `<span>${label}</span>`;
        item.addEventListener('click', e => { e.stopPropagation(); action(); removeCtxMenu(); });
        menu.appendChild(item);
      }

      // ── Copy media ──────────────────────────────────────────────────
      addItem(
        '<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>',
        'Copy Original Media',
        async () => {
          try {
            await copyMediaToClipboard(origUrl, type);
          } catch (_) {}
        }
      );

      // ── Copy URL ────────────────────────────────────────────────────
      addItem(
        '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
        'Copy Media URL',
        () => { copyTextToClipboard(origUrl); }
      );

      // ── Open in new tab ─────────────────────────────────────────────
      addItem(
        '<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>',
        'Open Media in New Tab',
        () => window.open(origUrl, '_blank', 'noopener')
      );

      // ── Save / download ─────────────────────────────────────────────
      addItem(
        '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
        'Save Original Media',
        () => downloadSingle(origUrl, title)
      );

      _ctxMenu = menu;
      document.body.appendChild(menu);

      const onClose = ev => {
        if (menu.contains(ev.target)) return;
        removeCtxMenu();
      };

      const onEsc = ev => {
        if (ev.key === 'Escape') removeCtxMenu();
      };

      _cleanupCtxMenu = () => {
        document.removeEventListener('click',       onClose);
        document.removeEventListener('contextmenu', onClose);
        document.removeEventListener('keydown',     onEsc);
        _cleanupCtxMenu = null;
      };

      // On touch, use a longer delay so the finger-lift tap doesn't
      // immediately close the menu before the user can read it.
      setTimeout(() => {
        if (!_cleanupCtxMenu) return;
        document.addEventListener('click',       onClose);
        document.addEventListener('contextmenu', onClose);
        document.addEventListener('keydown',     onEsc);
      }, isTouch ? 300 : 0);
    }

    const onContextMenu = e => {
      if (isPowerMenuEvent(e)) return;
      if (!get('contextMenu')) { removeCtxMenu(); return; }
      // Suppress native contextmenu on Android when our long-press already fired
      if (_lpJustShown) { e.preventDefault(); return; }
      const media = getMediaInfo(e.target);
      if (!media) { removeCtxMenu(); return; }
      e.preventDefault();
      showCtxMenuAt(e.clientX, e.clientY, media, false);
    };

    const onTouchStart = e => {
      if (!get('contextMenu')) return;
      const touch = e.touches[0];
      _lpStartX   = touch.clientX;
      _lpStartY   = touch.clientY;
      _lpScrolled = false;
      clearTimeout(_lpTimer);
      _lpTimer = setTimeout(() => {
        _lpTimer = null;
        if (_lpScrolled) return;
        const el = document.elementFromPoint(_lpStartX, _lpStartY);
        if (!el) return;
        const media = getMediaInfo(el);
        if (!media) return;
        // Prevent the Android contextmenu event (fired ~20 ms later) from
        // duplicating the menu we're about to show.
        _lpJustShown = true;
        setTimeout(() => { _lpJustShown = false; }, 400);
        showCtxMenuAt(_lpStartX, _lpStartY, media, true);
        if (navigator.vibrate) navigator.vibrate(30);
      }, 600);
    };

    const onTouchMove = e => {
      if (_lpScrolled) return;
      const touch = e.changedTouches[0];
      if (Math.abs(touch.clientX - _lpStartX) > 10 || Math.abs(touch.clientY - _lpStartY) > 10) {
        _lpScrolled = true;
        clearTimeout(_lpTimer);
        _lpTimer = null;
      }
    };

    const onTouchEnd = () => {
      clearTimeout(_lpTimer);
      _lpTimer = null;
    };

    document.addEventListener('contextmenu', onContextMenu, true);
    document.addEventListener('touchstart', onTouchStart, { passive: true });
    document.addEventListener('touchmove', onTouchMove, { passive: true });
    document.addEventListener('touchend', onTouchEnd, { passive: true });

    _imageContextMenuStop = () => {
      removeCtxMenu();
      clearTimeout(_lpTimer);
      _lpTimer = null;
      document.removeEventListener('contextmenu', onContextMenu, true);
      document.removeEventListener('touchstart', onTouchStart);
      document.removeEventListener('touchmove', onTouchMove);
      document.removeEventListener('touchend', onTouchEnd);
      _imageContextMenuStop = null;
    };
  }

  function stopImageContextMenu() {
    if (_imageContextMenuStop) _imageContextMenuStop();
  }


  // ═══════════════════════════════════════════════════════════════════
  //  MODULE: MOBILE LAZY-LOAD FIX
  // ═══════════════════════════════════════════════════════════════════
  // Pinterest on mobile aggressively defers image loading via loading="lazy"
  // and data-src attributes. On large feeds or slow devices many images that
  // are already visible on screen never actually load.
  // Uses IntersectionObserver with a generous 600 px rootMargin so images
  // are fetched well before reaching the viewport edge.
  // Also force-copies data-src → src for GIF images that are already
  // visible but whose lazy-loader hasn't fired yet.
  function initMobileLazyFix() {
    if (!IS_MOBILE) return;

    const io = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (!entry.isIntersecting) return;
        const el = entry.target;

        if (el.tagName === 'IMG') {
          // Lift native lazy-loading so the browser fetches immediately
          if (el.getAttribute('loading') === 'lazy') el.setAttribute('loading', 'eager');
          // Copy data-src → src if Pinterest's own lazy-loader hasn't fired yet
          const ds = el.getAttribute('data-src');
          if (ds && (!el.src || el.src === location.href)) el.src = ds;
          io.unobserve(el);
          return;
        }

        if (el.tagName === 'VIDEO') {
          // Mobile GIFs are often <video> with lazy data-src values.
          hydrateVideoSource(el);
          el.preload = 'auto';
          el.playsInline = true;
          if (el.readyState === 0) {
            try { el.load(); } catch (_) {}
          }
          // Mark as GIF-video when applicable so GIF modules can manage it.
          if (isGifVideo(el, findGifContainer(el))) el.__peGifVid = true;
          io.unobserve(el);
        }
      });
    }, { rootMargin: '600px 0px', threshold: 0 });

    function observeMedia(root) {
      if (!root || !root.querySelectorAll) return;
      root.querySelectorAll('img[loading="lazy"], img[data-src*="pinimg.com"], video').forEach(el => {
        // Only observe videos that look like Pinterest GIF media.
        if (el.tagName === 'VIDEO') {
          const hasLazySource = !!el.querySelector('source[data-src]');
          const src = getVideoSrc(el);
          if (!hasLazySource && !/pinimg\.com/i.test(src)) return;
        }
        if (el.__peLazyObs) return;
        el.__peLazyObs = true;
        io.observe(el);
      });
    }

    observeMedia(document);

    new MutationObserver(records => {
      records.forEach(r => r.addedNodes.forEach(n => {
        if (!n || n.nodeType !== 1) return;
        if (n.tagName === 'IMG') {
          if (!n.__peLazyObs) { n.__peLazyObs = true; io.observe(n); }
        } else if (n.tagName === 'VIDEO') {
          const hasLazySource = !!n.querySelector('source[data-src]');
          const src = getVideoSrc(n);
          if ((hasLazySource || /pinimg\.com/i.test(src)) && !n.__peLazyObs) {
            n.__peLazyObs = true;
            io.observe(n);
          }
        } else {
          observeMedia(n);
        }
      }));
    }).observe(document.documentElement, { childList: true, subtree: true });
  }


  // ═══════════════════════════════════════════════════════════════════
  //  STYLES
  // ═══════════════════════════════════════════════════════════════════
  function injectStyles() {
    const s = document.createElement('style');
    s.id = 'pe-styles';
    s.textContent = `
      /* ──────── Theme variables (settings panel + FAB) ──────── */
      #pe-settings-wrap {
        --pe-bg:            #fff;
        --pe-surface:       #fafafa;
        --pe-text:          #111;
        --pe-text-muted:    #767676;
        --pe-border:        #f2f2f2;
        --pe-row-hover:     #f5f5f5;
        --pe-accent:        #e60023;
        --pe-accent-hover:  #b5001b;
        --pe-knob-off:      #d1d1d1;
        --pe-input-bg:      #fff;
        --pe-input-border:  #ddd;
        --pe-notice-bg:     #fff9e6;
        --pe-notice-border: #ffe180;
        --pe-notice-text:   #7a5800;
        --pe-title-text:    #fff;
        color: var(--pe-text);
      }
      #pe-settings-wrap.pe-dark {
        --pe-bg:            #1e1e1e;
        --pe-surface:       #2a2a2a;
        --pe-text:          #e8e8e8;
        --pe-text-muted:    #9a9a9a;
        --pe-border:        rgba(255,255,255,.08);
        --pe-row-hover:     #333;
        --pe-accent:        #e60023;
        --pe-accent-hover:  #ff3355;
        --pe-knob-off:      #555;
        --pe-input-bg:      #2a2a2a;
        --pe-input-border:  #444;
        --pe-notice-bg:     #332b00;
        --pe-notice-border: #665500;
        --pe-notice-text:   #ffd54f;
      }

      /* ──────── Fix browser flash of black on <video> elements ──────── */
      video { background: transparent !important; }

      /* ──────── Hide Pinterest "Watch again" overlay when looping ──────── */
      body.pe-loop-video [data-test-id="story-pin-closeup-replay"],
      body.pe-loop-video [data-test-id="closeup-replay-button"],
      body.pe-loop-video [aria-label="Watch again"],
      body.pe-loop-video [aria-label="Replay"] {
        display: none !important;
      }
      /* Mobile end-screen: the desktop selectors above only kill the button.
         Mobile renders a full-cover overlay (a still <img>, a black backdrop,
         and a Share + Watch-again row) that the JS ended-event fallback drops
         by replaying — but collapse the whole overlay so it never flashes.
         Identified by the unique pairing of a Watch-again AND a Share control,
         scoped under the known closeup containers so it can't escape upward.
         pointer-events:none keeps any momentary frame non-interactive. */
      body.pe-loop-video :is(
        [data-test-id="visual-content-container"],
        [data-test-id="story-pin-video-block"],
        [data-test-id="closeup-body-image-container"],
        [data-test-id="pin-closeup-image"],
        [data-video-signature]
      ) div:has(> * [aria-label="Watch again"]):has(> * [aria-label="Share"]) {
        display: none !important;
        pointer-events: none !important;
      }

      /* ──────── Always hide "Open app" search autocomplete suggestions ──────── */
      [data-test-type="app_upsell_autocomplete"] { display: none !important; }

      /* ──────── Hide Visit Site ──────── */
      body.pe-hide-visit [data-test-id="visit-button"],
      body.pe-hide-visit .domain-link-button,
      body.pe-hide-visit [aria-label="Visit site"],
      body.pe-hide-visit a[rel="nofollow"][href*="://"] {
        display: none !important;
      }

      /* ──────── Hide Updates bell ──────── */
      body.pe-hide-updates [role="listitem"]:has([data-test-id="bell-icon"]),
      body.pe-hide-updates [data-test-id="bell-icon"] {
        display: none !important;
      }

      /* ──────── Hide Messages nav button ──────── */
      body.pe-hide-messages [role="listitem"]:has(div[aria-label="Messages"]),
      body.pe-hide-messages [role="listitem"]:has([data-test-id="nav-bar-speech-ellipsis"]),
      body.pe-hide-messages div[aria-label="Messages"],
      body.pe-hide-messages [data-test-id="notifications-button"],
      body.pe-hide-messages [data-test-id="nav-bar-speech-ellipsis"],
      body.pe-hide-messages a[href="/notifications/"] {
        display: none !important;
      }

      /* ──────── Hide Share / Send button ──────── */
      body.pe-hide-share [data-test-id="mobile-modal-heading"]:has(.WuRgKB),
      body.pe-hide-share .H2DtUH:has(a[aria-label^="Share via"]),
      body.pe-hide-share .H2DtUH:has([data-test-id="copy-link-share-icon"]),
      body.pe-hide-share .H2DtUH:has([data-test-id="copy-link-share-icon-auth"]),
      body.pe-hide-share .H2DtUH:has([data-test-id="message-share-button"]),
      body.pe-hide-share .H2DtUH:has([data-test-id="fbmessenger-share-icon"]),
      body.pe-hide-share .H2DtUH:has([data-test-id="whatsapp-share-icon"]),
      body.pe-hide-share .H2DtUH:has([data-test-id="facebook-share-icon"]),
      body.pe-hide-share .H2DtUH:has([data-test-id="twitter-share-icon"]),
      body.pe-hide-share .BVzdUh.Nt6yCq.i1hWBD:has(> hr.V619SU.FlxG2v),
      body.pe-hide-share [data-test-id="closeup-action-items"] .oRZ5_s:has([data-test-id="closeup-share-button"]),
      body.pe-hide-share [data-test-id="closeup-action-items"] .oRZ5_s:has(button[aria-label*="Share" i]),
      body.pe-hide-share [data-test-id="closeup-pin-action-items"] .oRZ5_s:has([data-test-id="share-button-group"]),
      body.pe-hide-share [data-test-id="closeup-pin-action-items"] .oRZ5_s:has([data-test-id="share-button-no-animation"]),
      body.pe-hide-share [role="listitem"]:has([data-test-id="sendPinButton"]),
      body.pe-hide-share [data-test-id="closeup-share-button"],
      body.pe-hide-share div[aria-label="Share"],
      body.pe-hide-share button[aria-label="Send"],
      body.pe-hide-share [data-test-id="sendPinButton"],
      body.pe-hide-share [aria-label="Send"][role="button"],
      body.pe-hide-share [data-test-id="share-button-no-animation"],
      body.pe-hide-share [style*="ANIMATE_SHARE_container"] {
        display: none !important;
      }

      /* ──────── Hide closeup React heart ──────── */
      body.pe-hide-react [data-test-id="closeup-action-items"] .oRZ5_s:has([data-test-id="react-button"]),
      body.pe-hide-react [data-test-id="closeup-action-items"] [role="listitem"]:has(button[data-test-id="react-button"]),
      body.pe-hide-react [data-test-id="closeup-action-items"] [role="listitem"]:has(button[aria-label="React"][aria-pressed]),
      body.pe-hide-react [data-test-id="closeup-pin-action-items"] .oRZ5_s:has([data-test-id="react-button"]),
      body.pe-hide-react [data-test-id="closeup-pin-action-items"] .oRZ5_s:has([data-test-id="reaction-count"]),
      body.pe-hide-react [data-test-id="closeup-action-items"] [data-test-id="reactions-count"] {
        display: none !important;
      }

      body.pe-hide-reaction-count [data-test-id="closeup-action-items"] [data-test-id="reactions-count"],
      body.pe-hide-reaction-count [data-test-id="closeup-pin-action-items"] [data-test-id="reactions-count"] {
        display: none !important;
      }

      body.pe-hide-search-suggestions [data-root-margin="search-one-bar"] .oRZ5_s:has([data-test-id="one-bar-module-3"]),
      body.pe-hide-search-suggestions [data-test-id="scrollable-one-bar-root"] .oRZ5_s:has([data-test-id="one-bar-module-3"]) {
        display: none !important;
      }

      body.pe-hide-search-suggestions div[role="listitem"]:has([data-test-id="search-suggestion"]),
      body.pe-hide-search-suggestions div[data-grid-item="true"]:has([data-test-id="search-suggestion"]) {
        height: 0 !important;
        width: 0 !important;
        margin: 0 !important;
        padding: 0 !important;
        border: none !important;
        overflow: hidden !important;
        opacity: 0 !important;
        min-height: 0 !important;
        min-width: 0 !important;
        pointer-events: none !important;
      }

      /* ──────── Hide Lens upload image button ──────── */
      body.pe-hide-upload-image [aria-label="Upload image"] {
        display: none !important;
      }

      /* ──────── Hide closeup overlay/action buttons ──────── */
      body.pe-hide-search-image [data-test-id="visual-search-icon"],
      body.pe-hide-search-image [data-test-id="closeup-image-overlay-layer-flashlight-button"],
      body.pe-hide-search-image [data-test-id="flashlight"],
      body.pe-hide-search-image [aria-label="Search image"][role="button"],
      body.pe-hide-search-image [data-test-id="shop-button"] {
        display: none !important;
      }
      body.pe-hide-view-larger [data-test-id="closeup-image-overlay-layer-media-viewer-button-overlay"],
      body.pe-hide-view-larger [aria-label="View larger"][role="button"],
      body.pe-hide-view-larger [data-test-id="media-viewer-button"] {
        display: none !important;
      }
      body.pe-hide-more-options [data-test-id="closeup-action-items"] .oRZ5_s:has([data-test-id="closeup-more-options"]),
      body.pe-hide-more-options [data-test-id="closeup-action-items"] .oRZ5_s:has(button[aria-label="More actions"]),
      body.pe-hide-more-options [data-test-id="closeup-pin-action-items"] .oRZ5_s:has([data-test-id="context-menu-button"]),
      body.pe-hide-more-options [data-test-id="closeup-pin-action-items"] .oRZ5_s:has([data-test-id="ellipsis-button"]),
      body.pe-hide-more-options [data-test-id="closeup-pin-action-items"] .oRZ5_s:has([data-test-id="more-actions-button"]),
      body.pe-hide-more-options [data-test-id="closeup-more-options"] {
        display: none !important;
      }
      body.pe-hide-reverse-image-search #pe-reverse-image-search-slot {
        display: none !important;
      }

      /* ──────── Hide Comments ──────── */
      body.pe-hide-comments #canonical-card,
      body.pe-hide-comments [data-test-id="comment-editor-container"],
      body.pe-hide-comments [data-test-id="editor-with-mentions"],
      body.pe-hide-comments #dweb-comment-editor-container,
      body.pe-hide-comments #mweb-comment-editor-container,
      body.pe-hide-comments [data-test-id="closeup-metadata-details-divider"] {
        display: none !important;
      }

      body.pe-hide-comment-emoji [data-test-id="inline-comment-composer-container"] [data-test-id="emoji-selector"],
      body.pe-hide-comment-sticker [data-test-id="inline-comment-composer-container"] button[aria-label="Select a sticker"],
      body.pe-hide-comment-photo [data-test-id="inline-comment-composer-container"] button[aria-label="Select a photo"] {
        display: none !important;
      }

      /* ──────── Hide Comment Button ──────── */
      body.pe-hide-comment-button [data-test-id="closeup-action-items"] .oRZ5_s:has(button[aria-label="Comments"]),
      body.pe-hide-comment-button [data-test-id="closeup-action-items"] [role="listitem"]:has(button[aria-label="Comments"]),
      body.pe-hide-comment-button [data-test-id="closeup-pin-action-items"] .oRZ5_s:has([data-test-id="comment-button"]),
      body.pe-hide-comment-button [data-test-id="closeup-pin-action-items"] [data-test-id="comment-button"],
      body.pe-hide-comment-button button[aria-label="Comments"],
      body.pe-hide-comment-button button[aria-label="comments"] {
        display: none !important;
      }

      @media (hover: hover) and (pointer: fine) {
        /* ──────── Remove dark hover overlay on desktop pin cards ──────── */
        /* The overlay is an empty div that siblings [data-test-id="pinrep-image"] */
        [data-test-id="pinrep-image"] ~ div:not([data-test-id]) {
          background: transparent !important;
          background-color: transparent !important;
          background-image: none !important;
          opacity: 0 !important;
          display: none !important;
        }
        /* contentLayer gradient (the hover tint behind buttons) */
        [data-test-id="contentLayer"],
        [data-test-id="contentLayer"]::before,
        [data-test-id="contentLayer"]::after {
          background: transparent !important;
          background-color: transparent !important;
          background-image: none !important;
          box-shadow: none !important;
        }
        /* Any divs inside the image wrapper that could be overlays */
        [data-test-id^="pincard-gif"] > div > [data-test-id="pinrep-image"] ~ * {
          background: transparent !important;
          opacity: 0 !important;
          pointer-events: none !important;
        }
        .pe-pin-card-download-host {
          position: relative !important;
        }
        .pe-pin-card-download-wrap {
          position: absolute;
          bottom: 8px;
          left: 8px;
          z-index: 40;
          opacity: 0;
          pointer-events: none;
          transition: opacity .14s ease;
        }
        .pe-pin-card-download-host:hover .pe-pin-card-download-wrap,
        .pe-pin-card-download-host:focus-within .pe-pin-card-download-wrap,
        [data-pe-pin-card-download-card="true"]:hover .pe-pin-card-download-wrap,
        [data-pe-pin-card-download-card="true"]:focus-within .pe-pin-card-download-wrap {
          opacity: 1;
          pointer-events: auto;
        }
        .pe-pin-card-download-host:hover .pe-pin-card-download-btn,
        .pe-pin-card-download-host:focus-within .pe-pin-card-download-btn {
          opacity: 1;
        }
        .pe-pin-card-download-btn {
          width: 36px;
          height: 36px;
          border-radius: 50%;
          border: none;
          background: #fff;
          color: #111;
          display: inline-flex;
          align-items: center;
          justify-content: center;
          cursor: pointer;
          box-shadow: 0 2px 8px rgba(0,0,0,.22);
          opacity: .98;
          padding: 0;
          touch-action: manipulation;
        }
        .pe-pin-card-download-btn:hover {
          background: #f1f1f1;
        }
        .pe-pin-card-download-btn:active {
          transform: scale(.94);
        }
        .pe-pin-card-download-btn:disabled {
          opacity: .65;
          cursor: wait;
          transform: none !important;
        }
        .pe-pin-card-download-btn.pe-missing {
          color: #e60023;
        }
        .pe-pin-card-download-btn svg {
          width: 24px;
          height: 24px;
        }
        /* Remove the desktop hover gradient on pin image wrappers. */
        [data-test-id^="pincard"] > div > div:last-child:not([data-test-id]),
        .PinCard__imageWrapper > div > div:last-child:empty {
          display: none !important;
        }
      }

      /* ──────── Settings circle FAB ──────── */
      #pe-settings-wrap {
        position: fixed;
        bottom: 6px;
        right: 6px;
        z-index: 2147483647;
        contain: layout style paint;
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        gap: 10px;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
        user-select: none;
      }
      #pe-settings-btn {
        width: 40px; height: 40px;
        border-radius: 50%;
        background: var(--pe-accent); color: #fff; border: none;
        cursor: pointer;
        display: flex; align-items: center; justify-content: center;
        box-shadow: 0 4px 18px rgba(230,0,35,.45);
        transition: background .18s, box-shadow .18s, transform .25s;
        flex-shrink: 0;
      }
      #pe-settings-btn:hover { background: var(--pe-accent-hover); box-shadow: 0 6px 24px rgba(230,0,35,.55); transform: scale(1.08); }
      #pe-settings-btn:active { transform: scale(.92); }
      #pe-settings-btn.pe-settings-open { transform: rotate(45deg); }
      #pe-settings-btn.pe-settings-open:hover { transform: rotate(45deg) scale(1.08); }

      #pe-settings-panel {
        background: var(--pe-bg);
        color: var(--pe-text);
        border-radius: 12px;
        box-shadow: 0 4px 28px rgba(0,0,0,.16), 0 1px 4px rgba(0,0,0,.08);
        border: 1px solid var(--pe-border);
        min-width: 230px;
        max-height: min(70dvh, 520px);
        contain: layout style paint;
        overflow-y: auto;
        overflow-x: hidden;
        overscroll-behavior: contain;
        -webkit-overflow-scrolling: touch;
        touch-action: pan-y;
        animation: pe-bd-pop .15s ease-out;
      }
      #pe-settings-title {
        padding: 8px 12px 7px;
        background: var(--pe-accent);
        color: #fff;
        font-weight: 700;
        font-size: 13px;
        letter-spacing: .02em;
        display: flex;
        align-items: baseline;
        gap: 6px;
      }
      #pe-settings-by {
        font-weight: 700;
        font-size: 11px;
        opacity: .85;
        margin-left: auto;
      }
      #pe-settings-author {
        color: #fff;
        text-decoration: underline;
        text-underline-offset: 2px;
      }
      #pe-settings-author:hover { opacity: .75; }

      .pe-row {
        display: flex; align-items: center; justify-content: space-between;
        padding: 7px 12px; gap: 10px;
        transition: background .12s;
        border-top: 1px solid var(--pe-border);
      }
      .pe-row:hover { background: var(--pe-row-hover); }

      .pe-info { flex: 1; min-width: 0; }
      .pe-name {
        display: block; font-weight: 600; color: var(--pe-text);
        font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
      }
      .pe-desc {
        display: block; font-size: 10px; color: var(--pe-text-muted); margin-top: 1px;
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
      }

      /* Toggle switch */
      .pe-switch { position: relative; display: inline-block; width: 36px; height: 20px; flex-shrink: 0; }
      .pe-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
      .pe-knob {
        position: absolute; inset: 0; background: var(--pe-knob-off);
        border-radius: 20px; cursor: pointer;
        transition: background .2s;
      }
      .pe-knob::before {
        content: ''; position: absolute;
        width: 14px; height: 14px; left: 3px; bottom: 3px;
        background: #fff; border-radius: 50%;
        transition: transform .2s;
        box-shadow: 0 1px 3px rgba(0,0,0,.22);
      }
      .pe-switch input:checked ~ .pe-knob { background: var(--pe-accent); }
      .pe-switch input:checked ~ .pe-knob::before { transform: translateX(16px); }
      .pe-switch input:focus-visible ~ .pe-knob { outline: 2px solid var(--pe-accent); outline-offset: 2px; }

      /* ──────── Collapsible settings group ──────── */
      .pe-group { border-top: 1px solid var(--pe-border); }
      .pe-group-body { contain: layout style paint; }
      .pe-group-header {
        display: flex; align-items: center; justify-content: space-between;
        padding: 7px 12px; gap: 10px; cursor: pointer; transition: background .12s;
      }
      .pe-group-header:hover { background: var(--pe-row-hover); }
      .pe-chevron { transition: transform .2s; flex-shrink: 0; color: var(--pe-text-muted); }
      .pe-group-open .pe-chevron { transform: rotate(180deg); }
      #pe-group-hide-body {
        max-height: min(42dvh, 320px);
        overflow-y: auto;
        overflow-x: hidden;
        overscroll-behavior: contain;
        -webkit-overflow-scrolling: touch;
      }
      #pe-group-declutter-body {
        max-height: min(32dvh, 220px);
        overflow-y: auto;
        overflow-x: hidden;
        overscroll-behavior: contain;
        -webkit-overflow-scrolling: touch;
      }
      #pe-group-translate-body {
        max-height: min(42dvh, 320px);
        overflow-y: auto;
        overflow-x: hidden;
        overscroll-behavior: contain;
        -webkit-overflow-scrolling: touch;
      }
      #pe-group-customize-body {
        max-height: min(36dvh, 260px);
        overflow-y: auto;
        overflow-x: hidden;
        overscroll-behavior: contain;
        -webkit-overflow-scrolling: touch;
      }
      .pe-group-body { border-top: 1px solid var(--pe-border); }
      .pe-sub-row { padding-left: 28px !important; background: var(--pe-surface); }
      .pe-sub-row:hover { background: var(--pe-row-hover) !important; }
      .pe-select-row { align-items: center; }
      .pe-input-row { align-items: center; }
      .pe-setting-select {
        max-width: 110px;
        min-width: 94px;
        border: 1px solid var(--pe-input-border);
        border-radius: 7px;
        background: var(--pe-input-bg);
        color: var(--pe-text);
        font-size: 11px;
        font-weight: 600;
        padding: 4px 6px;
        outline: none;
      }
      .pe-setting-select:focus-visible {
        border-color: var(--pe-accent);
        box-shadow: 0 0 0 2px rgba(230,0,35,.16);
      }
      .pe-setting-input {
        width: 128px;
        border: 1px solid var(--pe-input-border);
        border-radius: 7px;
        background: var(--pe-input-bg);
        color: var(--pe-text);
        font-size: 11px;
        padding: 4px 6px;
        outline: none;
      }
      .pe-setting-input:focus-visible {
        border-color: var(--pe-accent);
        box-shadow: 0 0 0 2px rgba(230,0,35,.16);
      }
      .pe-setting-number {
        width: 64px;
      }

      /* Reload notice */
      #pe-notice {
        display: flex; align-items: center; justify-content: space-between;
        background: var(--pe-notice-bg); border-top: 1px solid var(--pe-notice-border);
        padding: 7px 14px; gap: 8px;
        font-size: 12px; color: var(--pe-notice-text);
      }
      #pe-reload-btn {
        background: var(--pe-accent); color: #fff; border: none;
        border-radius: 6px; font-size: 11px; font-weight: 700;
        padding: 3px 10px; cursor: pointer; white-space: nowrap;
        transition: background .15s;
      }
      #pe-reload-btn:hover { background: var(--pe-accent-hover); }

      /* ──────── Board Downloader FAB (standalone, above #pe-settings-wrap) ──────── */
      #pe-bd-fab {
        position: fixed;
        bottom: 56px;
        right: 6px;
        z-index: 2147483647;
        display: flex;
        flex-direction: column;
        align-items: flex-end;
        gap: 10px;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
        user-select: none;
      }
      #pe-bd-btn {
        width: 40px; height: 40px;
        border-radius: 50%;
        background: #e60023; color: #fff; border: none;
        cursor: pointer;
        display: flex; align-items: center; justify-content: center;
        box-shadow: 0 4px 18px rgba(230,0,35,.45);
        transition: background .18s, box-shadow .18s, transform .12s;
        flex-shrink: 0;
        touch-action: manipulation;
      }
      #pe-bd-btn:hover {
        background: #b5001b;
        box-shadow: 0 6px 24px rgba(230,0,35,.55);
        transform: scale(1.08);
      }
      #pe-bd-btn:active { transform: scale(.92); }

      /* ──────── Closeup image action-bar download ──────── */
      #pe-closeup-image-dl-slot,
      #pe-reverse-image-search-slot {
        display: flex;
        align-items: center;
        justify-content: center;
        pointer-events: auto;
      }
      #pe-closeup-image-dl-btn,
      #pe-reverse-image-search-btn {
        width: 48px;
        height: 48px;
        border-radius: 50%;
        border: none;
        background: transparent;
        color: currentColor;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0;
        touch-action: manipulation;
      }
      #pe-closeup-image-dl-btn:hover { background: rgba(0,0,0,.06); }
      #pe-closeup-image-dl-btn:active { transform: scale(.94); }
      #pe-closeup-image-dl-btn:disabled { opacity: .55; cursor: wait; transform: none !important; }
      #pe-closeup-image-dl-btn.pe-missing { color: #e60023; }
      #pe-closeup-image-dl-btn svg {
        width: 26px;
        height: 26px;
      }
      #pe-reverse-image-search-btn svg {
        width: 26px;
        height: 26px;
      }
      #pe-reverse-image-search-btn:hover { background: rgba(0,0,0,.06); }
      #pe-reverse-image-search-btn:active { transform: scale(.94); }
      #pe-reverse-image-search-btn:disabled { opacity: .55; cursor: wait; transform: none !important; }
      [data-test-id="closeup-pin-action-items"] #pe-closeup-image-dl-slot,
      [data-test-id="closeup-pin-action-items"] #pe-reverse-image-search-slot {
        min-width: 40px;
        flex: 0 0 auto;
      }
      [data-test-id="closeup-pin-action-items"] #pe-closeup-image-dl-btn,
      [data-test-id="closeup-pin-action-items"] #pe-reverse-image-search-btn {
        width: 40px;
        height: 40px;
      }
      [data-test-id="closeup-pin-action-items"] #pe-closeup-image-dl-btn svg {
        width: 26px;
        height: 26px;
      }
      [data-test-id="closeup-pin-action-items"] #pe-reverse-image-search-btn svg {
        width: 26px;
        height: 26px;
      }
      #pe-reverse-image-search-menu {
        position: fixed;
        z-index: 2147483647;
        min-width: 176px;
        padding: 6px;
        border: 1px solid rgba(0,0,0,.08);
        border-radius: 10px;
        background: #fff;
        box-shadow: 0 6px 24px rgba(0,0,0,.16);
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
      }
      #pe-reverse-image-search-menu button {
        width: 100%;
        border: none;
        border-radius: 7px;
        background: transparent;
        color: #111;
        cursor: pointer;
        display: block;
        font-size: 12px;
        font-weight: 600;
        padding: 7px 8px;
        text-align: left;
      }
      #pe-reverse-image-search-menu button:hover { background: #f3f3f3; }
      #pe-toast {
        position: fixed;
        z-index: 2147483647;
        left: 50%;
        bottom: 28px;
        transform: translateX(-50%);
        max-width: min(360px, calc(100vw - 32px));
        padding: 10px 14px;
        border-radius: 999px;
        background: rgba(17,17,17,.92);
        color: #fff;
        font-size: 12px;
        font-weight: 600;
        line-height: 1.25;
        box-shadow: 0 8px 28px rgba(0,0,0,.22);
        pointer-events: none;
        text-align: center;
      }
      #pe-update-notes-layer {
        position: fixed;
        z-index: 2147483647;
        inset: 0;
        display: flex;
        align-items: flex-end;
        justify-content: flex-end;
        padding: 18px;
        pointer-events: auto;
        background: transparent;
        box-sizing: border-box;
      }
      @keyframes pe-update-notes-in {
        from { opacity: 0; transform: translateY(8px); }
        to   { opacity: 1; transform: translateY(0); }
      }
      #pe-update-notes-card {
        --pe-un-bg:        #fff;
        --pe-un-text:      #111;
        --pe-un-muted:     #767676;
        --pe-un-border:    rgba(0,0,0,.08);
        --pe-un-divider:   rgba(0,0,0,.06);
        --pe-un-btn-bg:    transparent;
        --pe-un-btn-hover: #f5f5f5;
        --pe-un-shadow:    0 12px 36px rgba(0,0,0,.12);
        position: relative;
        width: 100%;
        max-width: min(360px, calc(100vw - 32px));
        padding: 16px 16px 14px;
        border: 1px solid var(--pe-un-border);
        border-radius: 16px;
        background: var(--pe-un-bg);
        color: var(--pe-un-text);
        box-shadow: var(--pe-un-shadow);
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
        font-size: 13px;
        line-height: 1.45;
        animation: pe-update-notes-in 180ms ease-out;
      }
      #pe-update-notes-card.pe-dark {
        --pe-un-bg:        #1e1e1e;
        --pe-un-text:      #e8e8e8;
        --pe-un-muted:     #9a9a9a;
        --pe-un-border:    rgba(255,255,255,.10);
        --pe-un-divider:   rgba(255,255,255,.08);
        --pe-un-btn-bg:    transparent;
        --pe-un-btn-hover: rgba(255,255,255,.06);
        --pe-un-shadow:    0 12px 36px rgba(0,0,0,.45);
      }
      #pe-update-notes-close {
        position: absolute;
        top: 8px;
        right: 8px;
        width: 28px;
        height: 28px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        border: none;
        border-radius: 999px;
        background: var(--pe-un-btn-bg);
        color: var(--pe-un-muted);
        cursor: pointer;
        padding: 0;
        line-height: 0;
        transition: background-color 120ms ease, color 120ms ease;
      }
      #pe-update-notes-close svg { display: block; }
      #pe-update-notes-close:hover { background: var(--pe-un-btn-hover); color: var(--pe-un-text); }
      #pe-update-notes-eyebrow {
        margin-right: 32px;
        color: #e60023;
        font-size: 11px;
        font-weight: 700;
        letter-spacing: 0.04em;
        text-transform: uppercase;
      }
      #pe-update-notes-title {
        margin: 4px 32px 10px 0;
        padding-bottom: 10px;
        border-bottom: 1px solid var(--pe-un-divider);
        font-size: 16px;
        font-weight: 700;
        color: var(--pe-un-text);
      }
      #pe-update-notes-list {
        margin: 0 0 14px;
        padding-left: 18px;
        color: var(--pe-un-text);
      }
      #pe-update-notes-list li { margin: 6px 0; line-height: 1.45; }
      #pe-update-notes-never {
        width: 100%;
        min-height: 34px;
        border: 1px solid var(--pe-un-border);
        border-radius: 999px;
        background: transparent;
        color: var(--pe-un-muted);
        cursor: pointer;
        font-size: 12px;
        font-weight: 600;
        transition: background-color 120ms ease, color 120ms ease;
      }
      #pe-update-notes-never:hover { background: var(--pe-un-btn-hover); color: var(--pe-un-text); }
      @media (max-width: 600px) {
        #pe-update-notes-layer {
          align-items: flex-end;
          justify-content: center;
          padding: 12px;
        }
        #pe-update-notes-card {
          max-width: calc(100vw - 24px);
          padding: 14px 14px 12px;
          border-radius: 14px;
          font-size: 12px;
        }
        #pe-update-notes-title { font-size: 15px; }
      }

      .pe-custom-logo-img {
        width: var(--pe-custom-logo-size, 32px);
        height: var(--pe-custom-logo-size, 32px);
        object-fit: contain;
        display: block;
        pointer-events: none;
      }
      .pe-custom-logo-img.pe-custom-logo-circle {
        border-radius: 50%;
        object-fit: cover;
      }

      .pe-translated-text {
        overflow-wrap: anywhere;
      }
      .pe-title-original-line {
        display: block;
        margin-top: 4px;
        font-size: .55em;
        font-weight: 500;
        line-height: 1.25;
        color: #767676;
      }
      .pe-manual-translate-btn {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 22px;
        height: 22px;
        margin: 0 0 0 6px;
        border: 1px solid rgba(0,0,0,.1);
        border-radius: 999px;
        background: #fff;
        color: #555;
        box-shadow: 0 1px 3px rgba(0,0,0,.08);
        cursor: pointer;
        vertical-align: middle;
        touch-action: manipulation;
      }
      .pe-manual-translate-btn:hover { color: #e60023; background: #fff5f7; }
      .pe-manual-translate-btn:disabled { opacity: .58; cursor: wait; }
      .pe-manual-translate-mount {
        display: inline-flex;
        align-items: center;
        vertical-align: middle;
      }

      #pe-bd-menu {
        background: #fff;
        border-radius: 12px;
        box-shadow: 0 4px 24px rgba(0,0,0,.15), 0 1px 4px rgba(0,0,0,.07);
        border: 1px solid rgba(0,0,0,.07);
        overflow: hidden;
        min-width: 192px;
        animation: pe-bd-pop .15s ease-out;
      }
      @keyframes pe-bd-pop {
        from { opacity:0; transform: scale(.9) translateY(6px); }
        to   { opacity:1; transform: scale(1) translateY(0); }
      }
      #pe-bd-status {
        padding: 7px 14px;
        font-size: 11px;
        color: #555;
        background: #f8f8f8;
        border-bottom: 1px solid #eee;
        white-space: nowrap;
      }
      .pe-bd-opt {
        display: flex; align-items: center; gap: 10px;
        padding: 11px 16px;
        font-size: 13px; font-weight: 600; color: #111;
        background: none; border: none; width: 100%;
        cursor: pointer; text-align: left;
        transition: background .12s;
      }
      .pe-bd-opt:hover { background: #f5f5f5; }
      .pe-bd-opt:disabled { color: #aaa; cursor: not-allowed; background: none; }
      .pe-bd-opt + .pe-bd-opt { border-top: 1px solid #f0f0f0; }

      /* ──────── Image right-click context menu ──────── */
      #pe-ctx-menu {
        position: fixed;
        background: #fff;
        border-radius: 10px;
        box-shadow: 0 4px 28px rgba(0,0,0,.18), 0 1px 6px rgba(0,0,0,.1);
        border: 1px solid rgba(0,0,0,.09);
        z-index: 2147483647;
        min-width: 220px;
        overflow: hidden;
        padding: 4px 0;
        animation: pe-bd-pop .12s ease-out;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        user-select: none;
      }
      .pe-ctx-item {
        display: flex; align-items: center; gap: 10px;
        padding: 8px 14px;
        font-size: 13px; font-weight: 500; color: #111;
        background: none; border: none; width: 100%;
        cursor: pointer; text-align: left;
        transition: background .1s;
      }
      .pe-ctx-item:hover { background: #f5f5f5; }
      .pe-ctx-item + .pe-ctx-item { border-top: 1px solid #f0f0f0; }
      .pe-ctx-item svg { flex-shrink: 0; color: #555; }

      /* ──────── Mobile / Touch support ──────── */
      /* Remove 300ms tap delay on all interactive elements */
      #pe-settings-btn, #pe-bd-btn, #pe-reload-btn,
      .pe-ctx-item, .pe-row, .pe-bd-opt, .pe-group-header, .pe-switch {
        touch-action: manipulation;
      }

      #pe-settings-panel { max-width: calc(100vw - 12px); }

      /* Board downloader menu: same treatment */
      #pe-bd-menu {
        max-height: calc(100dvh - 130px);
        overflow-y: auto;
        overscroll-behavior: contain;
        -webkit-overflow-scrolling: touch;
        max-width: calc(100vw - 12px);
      }

      /* ──────── Touch / mobile overrides ──────── */
      @media (pointer: coarse) {
        /* Slightly smaller FABs on touch so they don't obscure pins */
        #pe-settings-btn { width: 32px; height: 32px; }
        #pe-bd-btn        { width: 32px; height: 32px; }
        /* Adjust board fab bottom: 32px (mobile settings btn) + 6px + 10px gap = 48px */
        #pe-bd-fab        { bottom: 48px; }

        /* ── Compact settings panel on mobile ── */
        /* Cap height to ~62% of screen and use a narrower width */
        #pe-settings-panel {
          max-height: min(62dvh, 420px);
          min-width: 220px;
          max-width: calc(100vw - 14px);
          border-radius: 12px;
        }
        #pe-group-hide-body { max-height: min(34dvh, 260px); }
        /* Smaller title bar */
        #pe-settings-title {
          font-size: 13px;
          padding: 8px 12px 7px;
        }
        #pe-settings-by { font-size: 10px; }

        /* Compact rows — still large enough to tap, but not 48px tall */
        .pe-row {
          padding: 6px 12px;
          min-height: 38px;
          gap: 10px;
        }
        .pe-group-header {
          padding: 6px 12px;
          min-height: 38px;
          gap: 10px;
        }
        .pe-sub-row {
          min-height: 36px;
          padding-left: 20px !important;
        }

        /* Smaller text inside the settings panel */
        .pe-name  { font-size: 12px; }
        .pe-desc  { font-size: 10px; }

        /* Slightly smaller toggle switch */
        .pe-switch { width: 30px; height: 17px; }
        .pe-knob::before { width: 11px; height: 11px; left: 3px; bottom: 3px; }
        .pe-switch input:checked ~ .pe-knob::before { transform: translateX(13px); }

        /* Compact reload notice */
        #pe-notice { padding: 5px 12px; font-size: 11px; }
        #pe-reload-btn { font-size: 10px; padding: 3px 8px; }

        /* Context menu + board downloader keep generous tap targets */
        .pe-ctx-item { padding: 13px 16px; min-height: 48px; }
        .pe-bd-opt   { min-height: 48px; padding: 13px 16px; }
      }

      /* Prevent panels exceeding viewport width on very narrow screens */
      /* Backup compact panel for narrow screens where pointer:coarse may not fire */
      @media (max-width: 600px) {
        #pe-settings-panel {
          max-height: min(62dvh, 420px);
          min-width: 220px;
          max-width: calc(100vw - 14px);
        }
        #pe-group-hide-body { max-height: min(34dvh, 260px); }
      }

      @media (max-width: 320px) {
        #pe-settings-panel { min-width: unset; width: calc(100vw - 12px); }
        #pe-ctx-menu       { min-width: unset; width: calc(100vw - 24px); }
      }

      /* ──────── Mobile performance: reduce GPU over-composition ──────── */
      @media (pointer: coarse) {
        /* Pinterest promotes every pin card to its own GPU compositing layer
           via will-change, which exhausts GPU memory and causes scroll jank.
           Resetting it lets the browser decide when a layer is actually needed. */
        [data-test-id="pinWrapper"] {
          will-change: auto !important;
        }
        /* Async image decoding keeps the main thread free while the user scrolls */
        [data-test-id="pinWrapper"] img {
          decoding: async;
        }
      }
    `;
    (document.head || document.documentElement).appendChild(s);
  }


  // ═══════════════════════════════════════════════════════════════════
  //  INIT – run on DOMContentLoaded (UI) while OQ/modal observers
  //         are already running from document-start.
  // ═══════════════════════════════════════════════════════════════════
  function safeInit(name, fn) {
    try {
      fn();
    } catch (err) {
      console.warn('[Pinterest Power Menu] Feature startup failed:', name, err);
    }
  }

  function ensureSettingsPanel() {
    if (!document.body) return;
    if (!document.getElementById('pe-styles')) injectStyles();
    createSettingsPanel();
  }

  function onReady() {
    safeInit('settingsPanel', ensureSettingsPanel);
    safeInit('updateNotesPopup', createUpdateNotesPopup);

    // Upgrade any images already in DOM
    safeInit('originalQuality', () => {
      if (!get('originalQuality')) return;
      document.querySelectorAll('img[src*="pinimg.com"]').forEach(upgradeImg);
    });

    // GIF hover – pause any videos already in DOM, start delegation
    safeInit('gifHover', () => {
      document.querySelectorAll('video').forEach(pauseVidOnAdd);
      initGifHover();
    });

    // Apply hide-visit-site + nav-hide CSS classes
    safeInit('uiToggles', () => {
      applyVisitSiteToggle();
      applyNavToggles();
      initVisitSiteHider();
      initMessagesRemover();
      initShareOverride();
    });

    // Declutter
    safeInit('declutter', initDeclutter);

    // Remove videos
    safeInit('removeVideos', initRemoveVideos);

    // GIF auto-play
    safeInit('gifAutoPlay', () => { if (get('gifAutoPlay')) initGifAutoPlay(); });

    // Video auto-play (non-GIF <video> elements)
    safeInit('videoAutoPlay', () => { if (get('videoAutoPlay')) initVideoAutoPlay(); });

    // Track user's mute state so "Watch again" doesn't strip audio
    safeInit('videoMuteState', trackCloseupVideoMuteState);

    // Optional: loop closeup videos instead of showing "Watch again"
    safeInit('infiniteLoopVideo', () => { if (get('infiniteLoopVideo')) initInfiniteLoopVideo(); });
    applyInfiniteLoopVideoToggle();

    // Image right-click context menu
    safeInit('contextMenu', initImageContextMenu);

    // Download fixer event listener
    safeInit('downloadFixer', initDownloadFixer);

    // Board downloader button
    safeInit('boardDownloader', createBoardDownloaderUI);

    // Closeup action-bar buttons use separate desktop and mobile row resolvers.
    safeInit('closeupDownload', initCloseupImageDownloadButton);
    safeInit('reverseImageSearchButton', initReverseImageSearchButton);
    safeInit('pinCardQuickDownload', initDesktopPinCardQuickDownloadButton);

    // Custom nav logo
    if (!IS_MOBILE) safeInit('customPinterestLogo', initCustomPinterestLogo);

    // Hide shop posts
    safeInit('hideShopPosts', initHideShopPosts);

    // Hide comments
    safeInit('hideComments', initHideComments);

    // Visible text translation
    safeInit('autoTranslate', initAutoTranslate);
    safeInit('manualTranslateButtons', initManualTranslateButtons);

    // Scroll preservation (restores position on browser back)
    safeInit('scrollPreservation', initScrollPreservation);

    // Mobile: pre-load lazy images and fix GIF loading
    safeInit('mobileLazyFix', initMobileLazyFix);

    setTimeout(() => safeInit('settingsPanelRetry', ensureSettingsPanel), 1000);
  }

  if (document.readyState === 'loading')
    document.addEventListener('DOMContentLoaded', onReady);
  else
    onReady();

  // ═══════════════════════════════════════════════════════════════════
  //  SPA NAVIGATION WATCHER
  //  Pinterest never does a real page reload when you navigate.
  //  Intercept history.pushState / replaceState and popstate so we
  //  can show/hide the board FAB whenever the URL changes.
  // ═══════════════════════════════════════════════════════════════════
  (function () {
    let _lastPath = location.pathname;

    function onNavigate() {
      const newPath = location.pathname;
      if (newPath === _lastPath) return;
      _lastPath = newPath;

      // Clear stale intercepted video URLs from the previous pin so they
      // can't be picked up by the row Download button on the new pin page
      _interceptedVideoUrls.length = 0;
      _interceptedVideoUrlsByHash.clear();

      // Give Pinterest's React a moment to render the new page
      setTimeout(() => {
        removeBoardDownloaderUI();
        if (get('boardDownloader') && isBoardPage()) createBoardDownloaderUI();
        removeCloseupImageDownloadButton();
        removeReverseImageSearchButton();
        if (IS_MOBILE) {
          scheduleMobileCloseupActionButtonsRefresh();
        } else if (supportsCloseupActionBarEnhancements()) {
          createCloseupImageDownloadButton();
          if (get('reverseImageSearchButton')) createReverseImageSearchButton();
        }
        if (hasAnyAutoTranslateEnabled()) scanAutoTranslateCandidates(document);
        if (get('showManualTranslateButtons')) scanManualTranslateCandidates(document);
      }, 600);

      // Further attempts with increasing delays — mobile video src can arrive late
      [1800, 3500].forEach(ms => setTimeout(() => {
        if (!document.getElementById('pe-bd-btn') && get('boardDownloader') && isBoardPage())
          createBoardDownloaderUI();
        if (IS_MOBILE) {
          scheduleMobileCloseupActionButtonsRefresh();
        } else {
          if (supportsCloseupActionBarEnhancements() && !document.getElementById('pe-closeup-image-dl-slot'))
            createCloseupImageDownloadButton();
          if (supportsCloseupActionBarEnhancements() && !document.getElementById('pe-reverse-image-search-slot') && get('reverseImageSearchButton'))
            createReverseImageSearchButton();
        }
        if (hasAnyAutoTranslateEnabled()) scanAutoTranslateCandidates(document);
        if (get('showManualTranslateButtons')) scanManualTranslateCandidates(document);
      }, ms));
    }

    // Wrap history methods
    const _push    = history.pushState.bind(history);
    const _replace = history.replaceState.bind(history);
    history.pushState    = function (...a) { _push(...a);    onNavigate(); };
    history.replaceState = function (...a) { _replace(...a); onNavigate(); };
    window.addEventListener('popstate', onNavigate);

    // Also watch for the board header / video element appearing in the DOM (handles cases
    // where the URL change fires before React has rendered the new page content)
    new MutationObserver(records => {
      if (hasOnlyPowerMenuMutations(records)) return;
      if (!document.getElementById('pe-bd-btn') && get('boardDownloader') && isBoardPage())
        createBoardDownloaderUI();
      if (IS_MOBILE) {
        if (getMobileCloseupActionItems()) scheduleMobileCloseupActionButtonsRefresh();
      } else {
        if (supportsCloseupActionBarEnhancements() && !document.getElementById('pe-closeup-image-dl-slot'))
          createCloseupImageDownloadButton();
        if (supportsCloseupActionBarEnhancements() && !document.getElementById('pe-reverse-image-search-slot') && get('reverseImageSearchButton'))
          createReverseImageSearchButton();
      }
      if (hasAnyAutoTranslateEnabled() && _autoTranslateRescan) _autoTranslateRescan();
      if (get('showManualTranslateButtons') && _manualTranslateRescan) _manualTranslateRescan();
    }).observe(document.documentElement, { childList: true, subtree: true });
  })();

})();