DeviantArt Toolbox

Unified toolbox for DeviantArt: hover zoom, hide paid posts, ad blocking and more.

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!)

Advertisement:

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!)

Advertisement:

// ==UserScript==
// @name            DeviantArt Toolbox
// @namespace       https://gitlab.com/Kioraga/da-toolbox
// @version         1.4.1
// @description     Unified toolbox for DeviantArt: hover zoom, hide paid posts, ad blocking and more.
// @author          kioraga
// @match           https://www.deviantart.com/*
// @icon            https://gitlab.com/Kioraga/da-toolbox/-/raw/main/assets/da-icon.png
// @homepageURL     https://gitlab.com/Kioraga/da-toolbox
// @supportURL      https://gitlab.com/Kioraga/da-toolbox/-/issues
// @license         MIT
// @grant           none
// @noframes
// @run-at          document-idle
// ==/UserScript==

(function () {
  "use strict";

  if (window !== window.top) return;

const PANEL_HTML = `      <!-- Drag handle (visible only on mobile) -->
      <div class="hidden max-[600px]:flex justify-center pt-2 pb-0.5">
        <div class="w-7 h-1 rounded-full bg-[var(--md-on-surface-variant)] opacity-40"></div>
      </div>
      <!-- Desktop header -->
      <div class="flex items-center justify-between px-6 pt-4 pb-3 max-[600px]:hidden">
        <span class="flex items-center gap-2 text-base font-medium text-[var(--md-on-surface)]">
          <img src="https://gitlab.com/Kioraga/da-toolbox/-/raw/main/assets/da-icon.svg" width="20" height="20" alt="">
          DA Toolbox
        </span>
        <button id="datb-close" title="Close"
          class="w-10 h-10 rounded-full flex items-center justify-center text-[var(--md-on-surface-variant)] text-lg cursor-pointer bg-transparent border-none transition-colors duration-150 hover:bg-white/10 active:bg-white/20">
          ✕
        </button>
      </div>
      <!-- Mobile header -->
      <div class="hidden max-[600px]:flex items-center px-6 pt-4 pb-3">
        <button class="datb-close w-10 h-10 rounded-full flex items-center justify-center cursor-pointer bg-transparent border border-[var(--md-outline-variant)] text-[var(--md-on-surface-variant)] transition-colors duration-150 hover:bg-white/10 active:bg-white/20" title="Back">
          <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
        </button>
        <span class="flex-1 text-center text-base font-medium text-[var(--md-on-surface)] flex items-center justify-center gap-2">
          <img src="https://gitlab.com/Kioraga/da-toolbox/-/raw/main/assets/da-icon.svg" width="20" height="20" alt="">
          DA Toolbox
        </span>
        <div class="w-10"></div>
      </div>
      <!-- Tab navigation -->
      <nav class="flex gap-1 px-6 py-1 datb-tab-nav">
        <div class="datb-tab-indicator"></div>
        <button class="datb-tab datb-tab-active flex-1 px-3 py-2 text-sm font-medium rounded-full text-[var(--md-on-surface-variant)] bg-transparent border-none cursor-pointer" data-tab="hover">Hover</button>
        <button class="datb-tab flex-1 px-3 py-2 text-sm font-medium rounded-full text-[var(--md-on-surface-variant)] bg-transparent border-none cursor-pointer" data-tab="zoom">Zoom</button>
        <button class="datb-tab flex-1 px-3 py-2 text-sm font-medium rounded-full text-[var(--md-on-surface-variant)] bg-transparent border-none cursor-pointer" data-tab="filters">Filters</button>
        <button class="datb-tab flex-1 px-3 py-2 text-sm font-medium rounded-full text-[var(--md-on-surface-variant)] bg-transparent border-none cursor-pointer" data-tab="layout">Layout</button>
        <button class="datb-tab flex-1 px-3 py-2 text-sm font-medium rounded-full text-[var(--md-on-surface-variant)] bg-transparent border-none cursor-pointer" data-tab="adblock">Adblock</button>
        <button class="datb-tab datb-debug-tab flex-1 px-3 py-2 text-sm font-medium rounded-full text-[var(--md-on-surface-variant)] bg-transparent border-none cursor-pointer" data-tab="debug">Debug</button>
      </nav>
      <!-- Sections -->
      <div class="datb-sections px-6 pt-4 pb-5 overflow-y-auto overflow-x-hidden flex-1 min-h-0">
        <!-- Hover tab -->
        <div class="datb-section datb-section-active" id="datb-sec-hover">
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Enable</span>
            <input type="checkbox" id="datb-zoom-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
          </label>
          <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Enables the hover zoom effect on gallery posts</div>
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Scale</span>
            <div class="flex items-center gap-2.5">
              <input type="range" id="datb-zoom-scale" min="1" max="2" step="0.05" value="1.4" class="w-20 accent-[var(--md-primary)] cursor-pointer">
              <span class="datb-val min-w-[36px] text-right text-[var(--md-primary)] font-medium text-sm" id="datb-zoom-scale-val">1.4x</span>
            </div>
          </label>
          <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">How much the card scales up on hover</div>
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Speed</span>
            <select id="datb-zoom-speed" class="bg-[var(--md-surface-container-high)] text-[var(--md-on-surface)] border border-[var(--md-outline-variant)] rounded-lg px-2 py-1 text-xs cursor-pointer outline-none hover:border-[var(--md-outline)]">
              <option value="fast">Fast</option>
              <option value="normal">Normal</option>
              <option value="slow">Slow</option>
            </select>
          </label>
          <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Transition duration for the zoom animation</div>
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Shadow</span>
            <input type="checkbox" id="datb-zoom-shadow" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
          </label>
          <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Adds a drop shadow to the scaled card</div>
        </div>
        <!-- Zoom tab -->
        <div class="datb-section" id="datb-sec-zoom">
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Enable</span>
            <input type="checkbox" id="datb-zoom-z-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
          </label>
          <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Enables zoom — hold Z to zoom, scroll while held to adjust size</div>
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Full resolution</span>
            <input type="checkbox" id="datb-zoom-fullres" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
          </label>
          <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Try to load the image in full resolution</div>
        </div>
        <!-- Filters tab -->
        <div class="datb-section" id="datb-sec-filters">
          <div class="datb-subheader datb-subheader-first">Paid Posts</div>
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Hide paid posts</span>
            <input type="checkbox" id="datb-hp-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
          </label>
          <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Hides subscribe/sale/buy &amp; unlock gallery posts</div>
          <div class="datb-subheader">Content</div>
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Hide text posts</span>
            <input type="checkbox" id="datb-htp-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
          </label>
          <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Hides journals &amp; status updates in the watch feed</div>
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Hide feed carousels</span>
            <input type="checkbox" id="datb-hc-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
          </label>
          <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Hides carousels &amp; "Recommended for You"</div>
          <div class="datb-subheader">Loading</div>
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Auto-load more results</span>
            <input type="checkbox" id="datb-mr-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
          </label>
          <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Automatically clicks "More Results" to load additional posts</div>
        </div>
        <!-- Layout tab -->
        <div class="datb-section" id="datb-sec-layout">
          <div class="datb-subheader datb-subheader-first">Scroll-to-top button</div>
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Enable</span>
            <input type="checkbox" id="datb-stt-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
          </label>
          <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Shows a button to return to the top of the page</div>
          <div class="datb-subheader">Image only</div>
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Enable</span>
            <input type="checkbox" id="datb-zoom-hide" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
          </label>
          <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Hides UI overlays, only shows deviation images</div>
          <div class="datb-mobile-grid-controls">
            <div class="datb-subheader">Mobile Grid</div>
            <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
              <span>Grid layout</span>
              <input type="checkbox" id="datb-mg-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
            </label>
            <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Replaces DA's natural-scroll row layout with a compact CSS grid</div>
            <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
              <span>Columns</span>
              <select id="datb-mg-columns" class="bg-[var(--md-surface-container-high)] text-[var(--md-on-surface)] border border-[var(--md-outline-variant)] rounded-lg px-2 py-1 text-xs cursor-pointer outline-none hover:border-[var(--md-outline)]">
                <option value="0">Auto</option>
                <option value="2">2</option>
                <option value="3">3</option>
                <option value="4">4</option>
                <option value="5">5</option>
              </select>
            </label>
            <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Number of columns in the grid (Auto fills available space)</div>
          </div>
        </div>
        <!-- Adblock tab -->
        <div class="datb-section" id="datb-sec-adblock">
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Enable</span>
            <input type="checkbox" id="datb-ab-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
          </label>
          <div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Blocks banners, promos and upgrade offers</div>
        </div>
        <!-- Debug tab -->
        <div class="datb-section datb-debug-section" id="datb-sec-debug">
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Enable</span>
            <input type="checkbox" id="datb-debug" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
          </label>
          <label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
            <span>Show hidden sections</span>
            <input type="checkbox" id="datb-debug-showall" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
          </label>
        </div>
      </div>
`;
const PANEL_STYLES = `/* === CSS Custom Properties (MD3-inspired theme) === */
:root {
  --md-primary: #2ecc71;
  --md-on-primary: #003917;
  --md-surface: #111318;
  --md-surface-container: #1b1d22;
  --md-surface-container-high: #26282d;
  --md-on-surface: #e2e2e5;
  --md-on-surface-variant: #c4c6ca;
  --md-outline: #8e9098;
  --md-outline-variant: #44474e;
}
/* === Tailwind-like utility classes === */
#datb-btn,
#datb-panel { font-family: 'Google Sans','Product Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.flex-1 { flex: 1 1 0%; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.gap-2\\.5 { gap: 0.625rem; }
.gap-3 { gap: 0.75rem; }
.hidden { display: none; }
.overflow-y-auto { overflow-y: auto; }
.overflow-x-hidden { overflow-x: hidden; }
.min-h-0 { min-height: 0; }
.w-fit { width: fit-content; }
.w-7 { width: 1.75rem; }
.w-10 { width: 2.5rem; }
.w-12 { width: 3rem; }
.w-20 { width: 5rem; }
.w-\\[1px\\] { width: 1px; }
.h-1 { height: 0.25rem; }
.h-10 { height: 2.5rem; }
.h-12 { height: 3rem; }
.min-w-\\[36px\\] { min-width: 36px; }
.min-w-\\[260px\\] { min-width: 260px; }
.min-h-\\[40px\\] { min-height: 40px; }
.p-0 { padding: 0; }
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-2\\.5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
.pt-2 { padding-top: 0.5rem; }
.pt-4 { padding-top: 1rem; }
.pb-0\\.5 { padding-bottom: 0.125rem; }
.pb-1\\.5 { padding-bottom: 0.375rem; }
.pb-3 { padding-bottom: 0.75rem; }
.pb-5 { padding-bottom: 1.25rem; }
.-mt-0\\.5 { margin-top: -0.125rem; }
.mb-1 { margin-bottom: 0.25rem; }
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-base { font-size: 1rem; line-height: 1.5rem; }
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
.text-center { text-align: center; }
.text-right { text-align: right; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.leading-\\[1\\.4\\] { line-height: 1.4; }
.uppercase { text-transform: uppercase; }
.tracking-\\[\\.05em\\] { letter-spacing: 0.05em; }
.text-\\[11px\\] { font-size: 11px; }
.cursor-pointer { cursor: pointer; }
.rounded-full { border-radius: 9999px; }
.rounded-2xl { border-radius: 1rem; }
.rounded-lg { border-radius: 0.5rem; }
.border { border-width: 1px; border-style: solid; }
.border-0 { border-width: 0; }
.border-none { border-style: none; }
.bg-transparent { background: transparent; }
.bg-white { background: #fff; }
.scale-110 { transform: scale(1.1); }
.opacity-40 { opacity: 0.4; }
.opacity-60 { opacity: 0.6; }
.outline-none { outline: none; }
.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1); }
.inline-flex { display: inline-flex; }
.flex-wrap { flex-wrap: wrap; }
.items-start { align-items: flex-start; }
.text-left { text-align: left; }
.whitespace-nowrap { white-space: nowrap; }
.transition-all { transition-property: all; transition-timing-function: cubic-bezier(.4,0,.2,1); transition-duration: .15s; }
.transition-colors { transition-property: color,background-color,border-color,text-decoration-color,fill,stroke; transition-timing-function: cubic-bezier(.4,0,.2,1); transition-duration: .15s; }
.duration-150 { transition-duration: .15s; }
.duration-200 { transition-duration: .2s; }
.ease-panel { transition-timing-function: cubic-bezier(.2,0,0,1); }
.fixed { position: fixed; }
.absolute { position: absolute; }
.relative { position: relative; }
.bottom-5 { bottom: 1.25rem; }
.right-5 { right: 1.25rem; }
.z-\\[2147483647\\] { z-index: 2147483647; }
.text-\\[var\\(--md-on-surface\\)\\] { color: var(--md-on-surface); }
.text-\\[var\\(--md-on-surface-variant\\)\\] { color: var(--md-on-surface-variant); }
.text-\\[var\\(--md-primary\\)\\] { color: var(--md-primary); }
.bg-\\[var\\(--md-surface-container-high\\)\\] { background: var(--md-surface-container-high); }
.bg-\\[var\\(--md-surface-container\\)\\] { background: var(--md-surface-container); }
.border-\\[var\\(--md-outline-variant\\)\\] { border-color: var(--md-outline-variant); }
.accent-\\[var\\(--md-primary\\)\\] { accent-color: var(--md-primary); }
.hover\\:bg-white\\/10:hover { background: rgba(255,255,255,.1); }
.hover\\:bg-white\\/20:hover { background: rgba(255,255,255,.2); }
.hover\\:border-\\[var\\(--md-outline\\)\\]:hover { border-color: var(--md-outline); }
.hover\\:scale-105:hover { transform: scale(1.05); }
.active\\:bg-white\\/20:active { background: rgba(255,255,255,.2); }
.active\\:scale-95:active { transform: scale(0.95); }
@media (max-width: 600px) {
  .max-\\[600px\\]\\:hidden { display: none !important; }
  .max-\\[600px\\]\\:flex { display: flex !important; }
  .max-\\[600px\\]\\:justify-center { justify-content: center !important; }
  .datb-tab[data-tab="hover"],
  .datb-tab[data-tab="zoom"] { display: none !important; }
  #datb-sec-hover,
  #datb-sec-zoom { display: none !important; }
}
/* === FAB (floating action button) === */
#datb-btn { position: fixed !important; box-shadow: 0 4px 12px rgba(0,0,0,.4); }
#datb-btn:hover { box-shadow: 0 8px 25px rgba(0,0,0,.5); transform: scale(1.05); }
#datb-btn:active { transform: scale(0.95); }
#datb-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--md-primary), 0 4px 12px rgba(0,0,0,.4); }
#datb-btn.datb-fab-open { box-shadow: 0 0 0 3px var(--md-primary), 0 8px 25px rgba(0,0,0,.5) !important; }
#datb-btn img { display: block; width: 22px; height: 22px; }

/* === Scroll-to-top button === */
#datb-stt { position: fixed; z-index: 2147483646; bottom: 80px; right: 20px; width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; border: 0; background: var(--md-surface-container-high); color: #fff; opacity: 0; pointer-events: none; transition: opacity .25s, box-shadow .2s, transform .15s; box-shadow: 0 4px 12px rgba(0,0,0,.4); }
#datb-stt:hover { box-shadow: 0 8px 25px rgba(0,0,0,.5); transform: scale(1.05); }
#datb-stt:active { transform: scale(0.95); }
#datb-stt.datb-stt-visible { opacity: 1; pointer-events: auto; }
/* === Popover panel === */
#datb-panel { display: none !important; }
#datb-panel:popover-open { inset: auto; position: fixed; margin: 0; display: flex !important; height: 480px; }
/* === Tab navigation === */
.datb-tab-nav { position: relative; }
.datb-tab-indicator { position: absolute; top: 4px; bottom: 4px; border-radius: 9999px; background: var(--md-surface-container-high); z-index: 0; box-shadow: 0 1px 3px rgba(0,0,0,.3); pointer-events: none; transition: left .2s ease, width .2s ease; }
.datb-tab { position: relative; z-index: 1; }
.datb-section { display: none; }
.datb-section-active { display: block; }
.datb-section-empty { display: none !important; }
.datb-debug-tab,
.datb-debug-section { display: none !important; }
#datb-panel.datb-debug .datb-debug-tab { display: block !important; }
#datb-panel.datb-debug .datb-debug-section.datb-section-active { display: block !important; }
.datb-mobile-grid-controls { display: none; }
.datb-has-grid .datb-mobile-grid-controls { display: block; }
.datb-subheader { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--md-primary); padding-top: 16px; }
.datb-beta { font-size: 10px; font-weight: 700; background: var(--md-primary); color: var(--md-on-primary); padding: 1px 5px; border-radius: 3px; margin-left: 4px; vertical-align: middle; line-height: 1.4; }
.datb-subheader-first { padding-top: 0; }
.datb-touch-hidden { display: none !important; }
/* === Debug: force-show all hidden sections === */
#datb-panel.datb-debug-showall .datb-debug-tab,
#datb-panel.datb-debug-showall .datb-tab.datb-touch-hidden { display: revert !important; }
#datb-panel.datb-debug-showall .datb-debug-section.datb-section-active,
#datb-panel.datb-debug-showall .datb-section-active.datb-touch-hidden { display: revert !important; }
#datb-panel.datb-debug-showall .datb-mobile-grid-controls { display: revert !important; }
.datb-sections::-webkit-scrollbar { width: 4px; }
.datb-sections::-webkit-scrollbar-thumb { background: var(--md-outline-variant); border-radius: 2px; }
/* === Mobile responsive === */
@media (max-width: 600px) {
  #datb-panel:popover-open {
    top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
    width: 100vw !important; max-width: none !important; height: 100dvh !important;
    border-radius: 0 !important; max-height: none !important; border: none !important;
  }
  .datb-tab-nav {
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  .datb-tab-nav::-webkit-scrollbar { display: none; }
  .datb-tab { flex-shrink: 0; }
  .datb-tab[data-tab="hover"],
  .datb-tab[data-tab="zoom"] { display: none !important; }
  #datb-sec-hover,
  #datb-sec-zoom { display: none !important; }
  /* debug-showall overrides mobile hide: show tabs always, sections only when active */
  #datb-panel.datb-debug-showall .datb-tab[data-tab="hover"],
  #datb-panel.datb-debug-showall .datb-tab[data-tab="zoom"],
  #datb-panel.datb-debug-showall .datb-debug-tab { display: revert !important; }
  #datb-panel.datb-debug-showall #datb-sec-hover.datb-section-active,
  #datb-panel.datb-debug-showall #datb-sec-zoom.datb-section-active { display: revert !important; }
}
`;
  // Namespace prefix and DOM selectors for DeviantArt gallery
  const NS = "datb";
  const STORAGE_KEY = NS + "-settings";
  const ROW_SEL = '[data-testid="content_row"]';
  const CARD_SEL = 'div[style*="inline-block"]';
  const CAROUSEL_CARD_SEL = 'div.RyH8GD.qtdoVZ';
  const BTN_ID = NS + "-btn";
  const PANEL_ID = NS + "-panel";
  const STYLE_ID = NS + "-styles";
  const PANEL_STYLE_ID = NS + "-panel-styles";
  const ACTIVE = NS + "-active";

  // Duration (seconds) per speed setting
  const SPEED_MAP = { fast: 0.1, normal: 0.25, slow: 0.4 };

  // Default settings for all features
  const DEFAULTS = {
    zoomEnabled: true,
    zoomScale: 1.4,
    zoomSpeed: "normal",
    zoomShadow: true,
    zoomHideOverlays: false,
    zoomFullRes: true,
    zoomZEnabled: true,
    hidePaidEnabled: true,
    hideTextPosts: false,
    hideCarousels: false,
    adBlockEnabled: true,
    scrollToTopEnabled: true,
    mobileGridEnabled: false,
    mobileGridColumns: 0,
    moreResultsEnabled: true,
    debug: false,
    debugShowAll: false,
  };

  // Shared mutable state across all modules
  let STATE = {};
  let zHeld = false;
  let wheelScale = null;
  let zClone = null;
  let zCloneCard = null;
  let zSpinner = null;
  let paidScanTimer = null;

  // Conditional debug logger
  function log(...args) {
    if (STATE.debug) console.log("[" + NS + "]", ...args);
  }

  // Load settings from localStorage with migration from old script keys
  function loadSettings() {
    try {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (raw) {
        STATE = { ...DEFAULTS, ...JSON.parse(raw) };
        if (STATE.hideRecommended !== undefined) {
          STATE.hideCarousels = STATE.hideRecommended;
          delete STATE.hideRecommended;
        }
        return;
      }
    } catch (e) {
      log("settings parse error:", e);
    }
    // Migrate from da-hover-zoom settings (dahz-settings)
    const oldHz = localStorage.getItem("dahz-settings");
    if (oldHz) {
      try {
        const parsed = JSON.parse(oldHz);
        STATE = {
          ...DEFAULTS,
          zoomEnabled: parsed.enabled ?? true,
          zoomScale: parsed.scale ?? 1.4,
          zoomSpeed: parsed.speed ?? "normal",
          zoomShadow: parsed.shadow ?? true,
          zoomHideOverlays: parsed.hideOverlays ?? false,
          zoomFullRes: parsed.fullRes ?? true,
          adBlockEnabled: parsed.adBlock ?? true,
          debug: parsed.debug ?? false,
        };
        saveSettings();
        return;
      } catch (e) {
        log("migration error:", e);
      }
    }
    STATE = { ...DEFAULTS };
  }

  // Persist current STATE to localStorage
  function saveSettings() {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(STATE));
  }

  // Toggle html.datb-image-only class based on zoomHideOverlays setting
  function updateImageOnlyClass() {
    document.documentElement.classList.toggle(NS + "-image-only", STATE.zoomHideOverlays);
  }

  // Apply multiple CSS properties with !important to an element
  function setStyles(el, styles) {
    for (const [k, v] of Object.entries(styles)) {
      el.style.setProperty(k, v, "important");
    }
  }

  const WRENCH_ICON = '<img src="https://gitlab.com/Kioraga/da-toolbox/-/raw/main/assets/wrench.svg" width="22" height="22" alt="">';

  // Floating button dimensions and position from bottom-right
  const BTN_BOTTOM = 20;
  const BTN_SIZE = 48;

  // Sync form inputs to STATE after panel HTML is inserted
  function syncPanelState(panel) {
    function setChecked(id) { var el = panel.querySelector("#" + id); if (el) el.checked = true; }
    function setRange(id, val) { var el = panel.querySelector("#" + id); if (el) { el.value = val; } }
    function setSelect(id, val) { var el = panel.querySelector("#" + id); if (el) el.value = val; }
    if (STATE.zoomEnabled) setChecked(NS + "-zoom-enabled");
    if (STATE.zoomShadow) setChecked(NS + "-zoom-shadow");
    if (STATE.zoomFullRes) setChecked(NS + "-zoom-fullres");
    if (STATE.zoomZEnabled) setChecked(NS + "-zoom-z-enabled");
    if (STATE.hidePaidEnabled) setChecked(NS + "-hp-enabled");
    if (STATE.hideTextPosts) setChecked(NS + "-htp-enabled");
    if (STATE.hideCarousels) setChecked(NS + "-hc-enabled");
    if (STATE.scrollToTopEnabled) setChecked(NS + "-stt-enabled");
    if (STATE.zoomHideOverlays) setChecked(NS + "-zoom-hide");
    if (STATE.mobileGridEnabled) setChecked(NS + "-mg-enabled");
    if (STATE.moreResultsEnabled) setChecked(NS + "-mr-enabled");
    if (STATE.adBlockEnabled) setChecked(NS + "-ab-enabled");
    if (STATE.debug) setChecked(NS + "-debug");
    if (STATE.debugShowAll) setChecked(NS + "-debug-showall");
    setRange(NS + "-zoom-scale", STATE.zoomScale);
    var scaleVal = panel.querySelector("#" + NS + "-zoom-scale-val");
    if (scaleVal) scaleVal.textContent = STATE.zoomScale + "x";
    setSelect(NS + "-zoom-speed", STATE.zoomSpeed);
    setSelect(NS + "-mg-columns", String(STATE.mobileGridColumns));
  }

  // Build the settings panel and floating button if not already present
  function buildPanel() {
    if (document.getElementById(BTN_ID)) return togglePanel();

    var btn = document.createElement("button");
    btn.id = BTN_ID;
    btn.title = "DA Toolbox";
    btn.innerHTML = WRENCH_ICON;
    btn.className = [
      "fixed bottom-5 right-5 z-[2147483647]",
      "w-12 h-12 rounded-full",
      "flex items-center justify-center",
      "cursor-pointer p-0 border-0",
      "bg-[var(--md-surface-container-high)] text-white",
      "transition-all duration-200 ease-panel",
    ].join(" ");

    var panel = document.createElement("div");
    panel.id = PANEL_ID;
    panel.setAttribute("popover", "manual");
    panel.className = [
      "fixed",
      "right-5",
      "w-fit max-w-[calc(100vw-40px)] min-w-[260px]",
      "rounded-2xl flex flex-col",
      "bg-[var(--md-surface-container)] text-[var(--md-on-surface)]",
      "text-sm",
    ].join(" ");
    panel.style.bottom = (BTN_BOTTOM + BTN_SIZE + 8) + "px";

    panel.innerHTML = PANEL_HTML;
    syncPanelState(panel);
    if (STATE.debugShowAll) panel.classList.add(NS + "-debug-showall");

    btn.addEventListener("click", function (e) { togglePanel(e); });

    document.documentElement.appendChild(btn);
    document.documentElement.appendChild(panel);

    // Close button (desktop id-based)
    var closeBtn = document.getElementById(NS + "-close");
    if (closeBtn) {
      closeBtn.addEventListener("click", function (e) {
        e.stopPropagation();
        closePanel();
      });
    }
    // Close button (mobile class-based)
    panel.querySelectorAll("." + NS + "-close").forEach(function (btn) {
      btn.addEventListener("click", (e) => {
        e.stopPropagation();
        closePanel();
      });
    });

    // Tab switching
    panel.querySelectorAll("." + NS + "-tab").forEach((tab) => {
      tab.addEventListener("click", () => switchTab(tab.dataset.tab));
    });

    bindHoverControls(panel);
    bindZoomTabControls(panel);
    bindFilterControls(panel);
    bindAdBlockControls(panel);
    bindDebugControls(panel);
    bindMobileGridControls(panel);

    // Touch device detection: hide Hover and Zoom tabs
    var isTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
    if (isTouch) {
      panel.querySelectorAll("." + NS + "-tab[data-tab='hover'], ." + NS + "-tab[data-tab='zoom']").forEach(function (t) {
        t.classList.add(NS + "-touch-hidden");
      });
      document.getElementById(NS + "-sec-hover").classList.add(NS + "-touch-hidden");
      document.getElementById(NS + "-sec-zoom").classList.add(NS + "-touch-hidden");
      var firstVisible = panel.querySelector("." + NS + "-tab:not(." + NS + "-touch-hidden)");
      if (firstVisible) switchTab(firstVisible.dataset.tab);
    }

    // Show mobile grid controls only when grid-row exists
    updateMobileGridControlsVisibility();
    hideEmptySections();

  }

  // Toggle panel open/close state
  function togglePanel(e) {
    const panel = document.getElementById(PANEL_ID);
    if (!panel) return buildPanel();
    if (panel.matches(":popover-open")) {
      closePanel();
    } else {
      openPanel(panel, e && e.shiftKey);
    }
  }

  function openPanel(panel, debugMode) {
    panel.classList.toggle(NS + "-debug", !!debugMode);
    repositionPanel(panel);
    try {
      panel.showPopover();
      hideEmptySections();
      // Auto-switch to Debug tab when opened with shift+click
      if (debugMode) switchTab("debug");
      var activeTab = panel.querySelector("." + NS + "-tab-active");
      // If the active tab is now hidden (debug tab closed without debug), fall back
      if (activeTab && activeTab.offsetHeight === 0) {
        activeTab.classList.remove(NS + "-tab-active");
        document.getElementById(NS + "-sec-" + activeTab.dataset.tab).classList.remove(NS + "-section-active");
        activeTab = null;
      }
      if (activeTab) {
        var indicator = panel.querySelector("." + NS + "-tab-indicator");
        if (indicator) indicator.style.transition = "none";
        updateTabIndicator(activeTab.dataset.tab);
        requestAnimationFrame(function () {
          if (indicator) indicator.style.transition = "";
        });
      } else {
        // No visible tab active — switch to first visible tab
        var firstVisible = Array.from(
          panel.querySelectorAll("." + NS + "-tab")
        ).find(function(t) { return t.offsetHeight > 0; });
        if (firstVisible) switchTab(firstVisible.dataset.tab);
      }
      var btn = document.getElementById(BTN_ID);
      if (btn) btn.classList.add(NS + "-fab-open");
    } catch (e) {
      console.error("[" + NS + "] showPopover failed:", e);
    }
  }

  function closePanel() {
    var panel = document.getElementById(PANEL_ID);
    if (panel && panel.matches(":popover-open")) {
      panel.hidePopover();
      var btn = document.getElementById(BTN_ID);
      if (btn) btn.classList.remove(NS + "-fab-open");
    }
  }

  // Keep panel within viewport bounds (CSS handles mobile full-screen + auto-width)
  function repositionPanel(panel) {
    if (window.innerWidth <= 600) return;

    var vw = window.innerWidth;
    var right = Math.min(20, vw - 260 - 20);
    if (right < 20) right = 20;

    panel.style.setProperty("right", right + "px", "important");
    panel.style.setProperty("left", "auto", "important");
    panel.style.setProperty("bottom", BTN_BOTTOM + BTN_SIZE + 8 + "px", "important");
    panel.style.setProperty("position", "fixed", "important");
  }

  // Animate the tab indicator to the target tab's position
  function updateTabIndicator(id) {
    var nav = document.querySelector("." + NS + "-tab-nav");
    if (!nav) return;
    var indicator = nav.querySelector("." + NS + "-tab-indicator");
    var target = nav.querySelector('[data-tab="' + id + '"]');
    if (!indicator || !target) return;
    indicator.style.left = target.offsetLeft + "px";
    indicator.style.width = target.offsetWidth + "px";
  }

  // Switch the active tab and show its section
  function switchTab(id) {
    updateTabIndicator(id);
    document.querySelectorAll("." + NS + "-tab").forEach((t) => {
      t.classList.toggle(NS + "-tab-active", t.dataset.tab === id);
    });
    document.querySelectorAll("." + NS + "-section").forEach((s) => {
      s.classList.toggle(NS + "-section-active", s.id === NS + "-sec-" + id);
    });
    hideEmptySections();
    // Scroll the target tab into view if the nav overflows
    var target = document.querySelector('.' + NS + '-tab-nav [data-tab="' + id + '"]');
    if (target) target.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
  }

  function addPanelStyles() {
    if (document.getElementById(PANEL_STYLE_ID)) return;
    var s = document.createElement("style");
    s.id = PANEL_STYLE_ID;
    s.textContent = PANEL_STYLES;
    document.head.appendChild(s);
  }

  // Wire up Hover tab controls to STATE and re-inject styles on change
  function bindHoverControls(panel) {
    panel.querySelector("#" + NS + "-zoom-enabled").addEventListener("change", (e) => {
      STATE.zoomEnabled = e.target.checked;
      saveSettings();
      injectZoomStyles();
    });
    panel.querySelector("#" + NS + "-zoom-scale").addEventListener("input", (e) => {
      const v = parseFloat(e.target.value);
      STATE.zoomScale = v;
      panel.querySelector("#" + NS + "-zoom-scale-val").textContent = v + "x";
      saveSettings();
      injectZoomStyles();
    });
    panel.querySelector("#" + NS + "-zoom-speed").addEventListener("change", (e) => {
      STATE.zoomSpeed = e.target.value;
      saveSettings();
      injectZoomStyles();
    });
    panel.querySelector("#" + NS + "-zoom-shadow").addEventListener("change", (e) => {
      STATE.zoomShadow = e.target.checked;
      saveSettings();
      injectZoomStyles();
    });
    panel.querySelector("#" + NS + "-zoom-hide").addEventListener("change", (e) => {
      STATE.zoomHideOverlays = e.target.checked;
      saveSettings();
      updateImageOnlyClass();
      if (typeof injectZoomStyles === "function") injectZoomStyles();
      if (typeof rebuildOverlayGrid === "function") rebuildOverlayGrid();
    });
  }

  // Wire up Zoom tab controls
  function bindZoomTabControls(panel) {
    panel.querySelector("#" + NS + "-zoom-fullres").addEventListener("change", (e) => {
      STATE.zoomFullRes = e.target.checked;
      saveSettings();
    });
    panel.querySelector("#" + NS + "-zoom-z-enabled").addEventListener("change", (e) => {
      STATE.zoomZEnabled = e.target.checked;
      saveSettings();
    });
  }

  // Wire up Filters tab controls
  function bindFilterControls(panel) {
    panel.querySelector("#" + NS + "-hp-enabled").addEventListener("change", (e) => {
      STATE.hidePaidEnabled = e.target.checked;
      saveSettings();
      if (!STATE.hidePaidEnabled) {
        showAllPaid();
      } else {
        injectHidePaidStyles();
        hidePaidScan();
      }
      updateHtmlClass();
      if (typeof rebuildOverlayGrid === "function") rebuildOverlayGrid();
    });
    panel.querySelector("#" + NS + "-htp-enabled").addEventListener("change", (e) => {
      STATE.hideTextPosts = e.target.checked;
      saveSettings();
      filterTextPosts();
    });
    panel.querySelector("#" + NS + "-hc-enabled").addEventListener("change", (e) => {
      STATE.hideCarousels = e.target.checked;
      saveSettings();
      filterCarousels();
    });
    panel.querySelector("#" + NS + "-mr-enabled").addEventListener("change", (e) => {
      STATE.moreResultsEnabled = e.target.checked;
      saveSettings();
      if (STATE.moreResultsEnabled) autoClickMoreResults();
    });
    panel.querySelector("#" + NS + "-stt-enabled").addEventListener("change", (e) => {
      STATE.scrollToTopEnabled = e.target.checked;
      saveSettings();
      if (STATE.scrollToTopEnabled) {
        initScrollBtn();
      } else {
        toggleScrollBtn(false);
        removeScrollListener();
      }
    });
  }

  // Wire up Ad Block tab controls
  function bindAdBlockControls(panel) {
    panel.querySelector("#" + NS + "-ab-enabled").addEventListener("change", (e) => {
      STATE.adBlockEnabled = e.target.checked;
      saveSettings();
      if (STATE.adBlockEnabled) {
        blockAds();
      }
    });
  }

  // Wire up Debug tab controls
  function bindDebugControls(panel) {
    panel.querySelector("#" + NS + "-debug").addEventListener("change", (e) => {
      STATE.debug = e.target.checked;
      saveSettings();
      log("debug mode", STATE.debug ? "enabled" : "disabled");
    });
    panel.querySelector("#" + NS + "-debug-showall").addEventListener("change", (e) => {
      STATE.debugShowAll = e.target.checked;
      saveSettings();
      panel.classList.toggle(NS + "-debug-showall", STATE.debugShowAll);
      // If the active tab is now hidden, fall back to the first visible tab
      if (!STATE.debugShowAll) {
        var active = panel.querySelector("." + NS + "-tab-active");
        if (active && active.offsetHeight === 0) {
          var firstVisible = Array.from(
            panel.querySelectorAll("." + NS + "-tab")
          ).find(function(t) { return t.offsetHeight > 0; });
          if (firstVisible) switchTab(firstVisible.dataset.tab);
        }
      }
    });
  }

  // Wire up Mobile Grid controls
  function bindMobileGridControls(panel) {
    panel.querySelector("#" + NS + "-mg-enabled").addEventListener("change", function (e) {
      STATE.mobileGridEnabled = e.target.checked;
      saveSettings();
      updateMobileGrid();
    });
    panel.querySelector("#" + NS + "-mg-columns").addEventListener("change", function (e) {
      STATE.mobileGridColumns = parseInt(e.target.value, 10);
      saveSettings();
      updateOverlayColumns();
      injectOverlayStyles();
    });

  }

  // Show/hide mobile grid controls based on grid-row presence
  function updateMobileGridControlsVisibility() {
    var panel = document.getElementById(PANEL_ID);
    if (!panel) return;
    var hasGrid = !!document.querySelector('div[data-testid="grid-row"]');
    panel.classList.toggle(NS + "-has-grid", hasGrid);
    hideEmptySections();
  }

  // Hide sections where every child is hidden
  function hideEmptySections() {
    document.querySelectorAll("." + NS + "-section").forEach(function (sec) {
      sec.classList.remove(NS + "-section-empty");
      var style = getComputedStyle(sec);
      if (style.display === "none") return;
      var visible = Array.from(sec.children).some(function (el) {
        return el.offsetHeight > 0 || el.getClientRects().length > 0;
      });
      if (!visible) sec.classList.add(NS + "-section-empty");
    });
  }

  let _lastMgCard = null;

  // Build CSS for card hover scale effect based on current settings
  function cssScale() {
    const dur = SPEED_MAP[STATE.zoomSpeed] || 0.25;
    const scale = wheelScale ?? STATE.zoomScale;
    const hide = STATE.zoomHideOverlays ? `
      html.${NS}-image-only ${CARD_SEL} * {
        visibility: hidden !important;
      }
      html.${NS}-image-only ${CARD_SEL} img {
        visibility: visible !important;
      }
      html.${NS}-image-only ${CARD_SEL} img[alt*="avatar" i],
      html.${NS}-image-only ${CARD_SEL} [class*="Avatar"] img,
      html.${NS}-image-only ${CARD_SEL} [class*="avatar"] img {
        visibility: hidden !important;
      }
      html.${NS}-image-only a.${NS}-mg-card * {
        visibility: hidden !important;
      }
      html.${NS}-image-only a.${NS}-mg-card .${NS}-mg-img {
        visibility: visible !important;
      }
      ${CARD_SEL}.${ACTIVE} *,
      a.${NS}-mg-card.${ACTIVE} * {
        visibility: hidden !important;
      }
      ${CARD_SEL}.${ACTIVE} img,
      a.${NS}-mg-card.${ACTIVE} img {
        visibility: visible !important;
      }
    ` : "";
    return `
      ${CARD_SEL} img,
      a.${NS}-mg-card img {
        max-width: revert !important;
      }
      ${CARD_SEL},
      a.${NS}-mg-card {
        transition: transform ${dur}s cubic-bezier(0.25, 0.46, 0.45, 0.94),
                    box-shadow ${dur}s ease !important;
      }
      ${CARD_SEL}.${ACTIVE},
      a.${NS}-mg-card.${ACTIVE} {
        will-change: transform;
        transform: ${STATE.zoomEnabled ? `scale(${scale})` : "none"} !important;
        z-index: 999999 !important;
        position: relative !important;
        pointer-events: auto !important;
        background: #06070d !important;
        ${STATE.zoomShadow ? "box-shadow: 0 16px 48px rgba(0,0,0,0.5) !important;" : "box-shadow: none !important;"}
      }
      .${ACTIVE} { contain: none !important; }
      ${hide}
    `;
  }

  // Inject or update zoom styles, plus spinner animation for full-res loading
  function injectZoomStyles() {
    let el = document.getElementById(STYLE_ID);
    if (!el) {
      el = document.createElement("style");
      el.id = STYLE_ID;
      document.head.appendChild(el);
    }
    el.textContent = cssScale() + `
      ${ROW_SEL} {
        overflow: visible !important;
        content-visibility: visible !important;
      }
      html.${NS}-zheld ${CARD_SEL}.${ACTIVE} {
        transform: none !important;
        box-shadow: none !important;
      }
      .${NS}-spinner {
        position: fixed !important; z-index: 2147483647 !important;
        top: 50% !important; left: 50% !important;
        width: 36px !important; height: 36px !important;
        margin: -18px 0 0 -18px !important;
        border: 4px solid rgba(255,255,255,0.2) !important;
        border-top-color: #2ecc71 !important;
        border-radius: 50% !important;
        animation: ${NS}-spin 0.7s linear infinite !important;
        pointer-events: none !important;
      }
      @keyframes ${NS}-spin { to { transform: rotate(360deg); } }
    `;
  }



  // On hover: tag card and apply active class with smart transform origin
  function onHover(e) {
    let card = e.target.closest(CARD_SEL);
    if (!card) card = e.target.closest("a." + NS + "-mg-card");
    if (card && card.classList.contains(NS + "-mg-card")) {
      _lastMgCard = card;
    }
    if (!card || card.hasAttribute("data-" + NS)) return;
    // Overlay cards are flat — no row walking needed
    if (card.classList.contains(NS + "-mg-card")) {
      card.setAttribute("data-" + NS, "1");
      card.style.transformOrigin = getSmartOrigin(card);
      card.classList.add(ACTIVE);
      return;
    }
    const row = card.closest(ROW_SEL);
    if (!row) return;
    // Walk up through nested inline-blocks to find the outermost gallery card
    while (card.parentElement && card.parentElement !== row && card.parentElement.closest(CARD_SEL)) {
      card = card.parentElement.closest(CARD_SEL);
    }
    if (card.hasAttribute("data-" + NS)) return;
    card.setAttribute("data-" + NS, "1");
    card.style.transformOrigin = getSmartOrigin(card);
    card.classList.add(ACTIVE);
    row.classList.add(ACTIVE);
  }

  // On unhover: clean up classes, reset wheel scale, remove full-res clone
  function onUnhover(e) {
    const card = e.target.closest(CARD_SEL) || e.target.closest("a." + NS + "-mg-card");
    if (!card) return;
    if (e.relatedTarget && card.contains(e.relatedTarget)) return;
    if (_lastMgCard === card) _lastMgCard = null;
    card.removeAttribute("data-" + NS);
    card.classList.remove(ACTIVE);
    wheelScale = null;
    removeClone();
    if (!card.classList.contains(NS + "-mg-card")) {
      const row = card.closest(ROW_SEL);
      if (row && !row.querySelector("." + ACTIVE)) row.classList.remove(ACTIVE);
    }
    const dur = (SPEED_MAP[STATE.zoomSpeed] || 0.25) * 1000 + 50;
    setTimeout(() => {
      if (!card.classList.contains(ACTIVE)) card.style.transformOrigin = "";
    }, dur);
  }

  // Determine transform origin so the card scales away from row/grid edges
  function getSmartOrigin(card) {
    if (card.classList.contains(NS + "-mg-card")) {
      const p = card.parentNode;
      if (!p) return "center center";
      const cr = card.getBoundingClientRect();
      const pr = p.getBoundingClientRect();
      const isFirst = Math.abs(cr.left - pr.left) < 5;
      const isLast = Math.abs(cr.right - pr.right) < 5;
      const isTop = Math.abs(cr.top - pr.top) < 5;
      const xOrigin = isFirst ? "left" : isLast ? "right" : "center";
      const yOrigin = isTop ? "top" : "center";
      return xOrigin + " " + yOrigin;
    }
    const row = card.closest(ROW_SEL);
    if (!row) return "";
    const cards = row.querySelectorAll(CARD_SEL);
    const idx = Array.from(cards).indexOf(card);
    const isFirst = idx === 0;
    const isLast = idx === cards.length - 1;
    const firstRow = document.querySelector(ROW_SEL);
    const yOrigin = firstRow === row ? "top" : "center";
    const xOrigin = isFirst ? "left" : isLast ? "right" : "center";
    return xOrigin + " " + yOrigin;
  }

  // CSS classes for marking and hiding paid posts
  const HIDDEN_CLASS = NS + "-hp-hidden";
  const POST_CLASS = NS + "-hp-post";

  // Inject CSS rules for hiding paid posts
  function injectHidePaidStyles() {
    if (document.getElementById(NS + "-hp-styles")) return;
    const s = document.createElement("style");
    s.id = NS + "-hp-styles";
    s.textContent = [
      "html:not(." + NS + "-hp-showing) " + CARD_SEL + ":has(img[src*=\"locked-small\"], img[src*=\"locked-large\"]) { display: none !important; }",
      "html:not(." + NS + "-hp-showing) " + CARD_SEL + ":has([class*=\"unlock\"]) { display: none !important; }",
      "html:not(." + NS + "-hp-showing) " + CAROUSEL_CARD_SEL + ":has(img[src*=\"locked-small\"], img[src*=\"locked-large\"]) { display: none !important; }",
      "html:not(." + NS + "-hp-showing) " + CAROUSEL_CARD_SEL + ":has([class*=\"unlock\"]) { display: none !important; }",
      "html:not(." + NS + "-hp-showing) section.NuU4Mu:has(img[src*=\"locked-small\"], img[src*=\"locked-large\"]) { display: none !important; }",
      "html:not(." + NS + "-hp-showing) section.NuU4Mu:has([class*=\"unlock\"]) { display: none !important; }",
      "." + POST_CLASS + "." + HIDDEN_CLASS + " { display: none !important; }",
    ].join("\n");
    document.head.appendChild(s);
  }

  // Find the card container from any descendant, supporting both regular and carousel cards
  function closestCard(el) {
    return el.closest(CARD_SEL) || el.closest(CAROUSEL_CARD_SEL);
  }

  // Check if the button indicates a paid/subscription post
  function isPaymentBtn(btn) {
    if (btn.getAttribute("data-" + NS + "-hp") === "1") return false;
    const overlay = btn.closest('[class*="wGXvLz"]');
    if (!overlay) return false;
    const text = btn.textContent.trim();
    if (!/subscribe|sale|buy/i.test(text)) return false;
    return true;
  }

  // Check if an element indicates an "Unlock Gallery" post
  function isUnlockGallery(el) {
    if (el.getAttribute("data-" + NS + "-hp") === "1") return false;
    const overlay = el.closest('[class*="wGXvLz"]');
    if (!overlay) return false;
    if (!/unlock gallery/i.test(el.textContent)) return false;
    return true;
  }

  // Mark a card as paid and hide it if filtering is active
  function processPaidCard(card) {
    if (card.classList.contains(POST_CLASS)) return;
    card.classList.add(POST_CLASS);
    if (STATE.hidePaidEnabled) {
      card.classList.add(HIDDEN_CLASS);
    }
  }

  // Remove paid-post markers from elements no longer in gallery rows
  function hidePaidCleanup() {
    document.querySelectorAll("." + POST_CLASS).forEach((el) => {
      if (!el.closest(ROW_SEL)) {
        el.classList.remove(POST_CLASS, HIDDEN_CLASS);
      }
    });
  }

  // Scan gallery rows for payment buttons and "Unlock Gallery" posts
  function hidePaidScan() {
    if (!STATE.hidePaidEnabled) return;
    document.querySelectorAll(ROW_SEL + " button").forEach((btn) => {
      if (!isPaymentBtn(btn)) return;
      btn.setAttribute("data-" + NS + "-hp", "1");
      const overlay = btn.closest('[class*="wGXvLz"]');
      const card = closestCard(overlay);
      if (card) processPaidCard(card);
    });
    document.querySelectorAll(ROW_SEL + " " + CARD_SEL + ", " + ROW_SEL + " " + CAROUSEL_CARD_SEL).forEach((card) => {
      if (card.classList.contains(POST_CLASS)) return;
      const unlockEl = card.querySelector('[class*="wGXvLz"] button, [class*="wGXvLz"] a, [class*="unlock"], [class*="gallery-locked"]');
      if (unlockEl && isUnlockGallery(unlockEl)) {
        unlockEl.setAttribute("data-" + NS + "-hp", "1");
        processPaidCard(card);
      }
    });
    // Scan for subscription tier cards (outside gallery rows)
    document.querySelectorAll('a[aria-label*="subscription tier"]').forEach((link) => {
      const card = closestCard(link);
      if (card && !card.classList.contains(POST_CLASS)) {
        processPaidCard(card);
      }
    });
    // Scan for locked posts in mobile grid cards
    document.querySelectorAll('div[data-testid="grid-row"] section.NuU4Mu').forEach((card) => {
      if (card.classList.contains(POST_CLASS)) return;
      if (card.querySelector('img[src*="locked"]')) {
        processPaidCard(card);
      }
    });
  }

  // Reveal all previously hidden paid posts
  function showAllPaid() {
    document.querySelectorAll("." + POST_CLASS).forEach((p) => {
      p.classList.remove(HIDDEN_CLASS);
    });
  }

  // Schedule a paid-post scan on the next animation frame (throttled)
  function hidePaidSchedule() {
    if (paidScanTimer) return;
    paidScanTimer = requestAnimationFrame(() => {
      paidScanTimer = null;
      hidePaidScan();
    });
  }

  // Toggle the HTML-level class that hides paid posts via CSS
  function updateHtmlClass() {
    document.documentElement.classList.toggle(NS + "-hp-showing",
      !STATE.hidePaidEnabled);
  }

  // CSS selectors for known DeviantArt ad, promo and upgrade elements
  const AD_SELECTORS = [
    ".crYUnH", ".YSrWZY", ".DHdA8q", ".jqUgOy", ".p1Al36",
    ".Wd52TD", ".b4COXC", ".HPVs_J",
    ".qlAJA0", ".dgMz3k",
    'img[alt="Banner"]', 'img[src*="offers-assets"]',
    'iframe[src*="ads"]', '[title="DealerAdIframe"]',
    'a[href*="core-membership"]',
    '[class*="upgrade"]', '[class*="promo"]', '[class*="core-offer"]',
  ];

  // Skip elements inside the header/nav or user menu — never block navigation UI
  function inHeader(el) {
    return !!el.closest(
      'header, nav, [role="banner"], [class*="header"], [class*="nav-bar"], [class*="topbar"], #site-header-user-menu',
    );
  }

  // Hard block known promo elements even inside the header (but not in the user menu)
  function blockHeaderPromos() {
    document.querySelectorAll('a[href*="core-membership"], [title="DealerAdIframe"]').forEach((el) => {
      if (el.closest("#site-header-user-menu")) return;
      el.style.setProperty("display", "none", "important");
      const wrapper = el.closest('[class*="m4QFSe"], .ajOBFk');
      if (wrapper) wrapper.style.setProperty("display", "none", "important");
    });
  }

  // Hide all matched ad elements and their wrapper containers
  function blockAds() {
    if (!STATE.adBlockEnabled) return;
    blockHeaderPromos();
    document.querySelectorAll(AD_SELECTORS.join(",")).forEach((el) => {
      if (inHeader(el)) return;
      el.style.setProperty("display", "none", "important");
      const wrapper = el.closest(
        'a, [class*="banner"], [class*="ad"], [class*="promo"], [class*="offer"]',
      );
      if (wrapper) wrapper.style.setProperty("display", "none", "important");
    });
    // Also hide promotional text links
    document.querySelectorAll('a, [class*="PAYVSS"], [class*="cfJSy4"]').forEach((a) => {
      if (inHeader(a)) return;
      if (/upgrade|get core|treat yourself|50% off|premium content/i.test(a.textContent)) {
        a.style.setProperty("display", "none", "important");
      }
    });
  }

  // Hide the "Text Posts" carousel (journals & status updates) in the watch feed
  function filterTextPosts() {
    if (!STATE.hideTextPosts) return;
    document.querySelectorAll('section.VsAf1f.UR2gWo, section.VsAf1f.bxadal, section.VsAf1f.QwWbAQ').forEach((el) => {
      el.style.setProperty("display", "none", "important");
    });
  }

  // Hide feed carousels and "Recommended for You" section
  function filterCarousels() {
    if (!STATE.hideCarousels) return;
    document.querySelectorAll('div[data-testid="content_row"], div[data-testid="grid-row"]').forEach((row) => {
      if (row.querySelector('h3') && /recommended for you|from deviants you watch|explore recent deviations|daily deviations/i.test(row.textContent)) {
        row.style.setProperty("display", "none", "important");
      }
    });
  }

  // Scroll-to-top button
  const STT_ID = NS + "-stt";
  const STT_SHOW_OFFSET = 600;

  const STT_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5m0 0l-7 7m7-7l7 7"/></svg>';

  function buildScrollBtn() {
    if (document.getElementById(STT_ID)) return;
    const btn = document.createElement("button");
    btn.id = STT_ID;
    btn.title = "Scroll to top";
    btn.innerHTML = STT_SVG;
    btn.setAttribute("aria-label", "Scroll to top");
    btn.addEventListener("click", () => {
      window.scrollTo({ top: 0, behavior: "smooth" });
    });
    document.documentElement.appendChild(btn);
  }

  function toggleScrollBtn(show) {
    const btn = document.getElementById(STT_ID);
    if (!btn) return;
    btn.classList.toggle(NS + "-stt-visible", show);
  }

  function onScroll() {
    toggleScrollBtn(window.scrollY > STT_SHOW_OFFSET);
  }

  function removeScrollListener() {
    window.removeEventListener("scroll", onScroll, { passive: true });
  }

  function initScrollBtn() {
    buildScrollBtn();
    window.addEventListener("scroll", onScroll, { passive: true });
    if (window.scrollY > STT_SHOW_OFFSET) toggleScrollBtn(true);
  }

  const OVERLAY_ID = NS + "-mg-overlay";
  const OVERLAY_STYLE_ID = NS + "-mg-overlay-styles";
  const MG_ACTIVE_CLASS = NS + "-mg-active";
  const _posts = [];
  let _scrollSentinel = null;

  // Return CSS grid-template-columns value from user setting
  function getGridTemplate() {
    var n = STATE.mobileGridColumns;
    return n > 0 ? "repeat(" + n + ", 1fr)" : "repeat(auto-fill, minmax(160px, 1fr))";
  }

  // Inject or update overlay grid + card styles
  function injectOverlayStyles() {
    let el = document.getElementById(OVERLAY_STYLE_ID);
    if (!el) {
      el = document.createElement("style");
      el.id = OVERLAY_STYLE_ID;
      document.head.appendChild(el);
    }
    el.textContent = `
      html.${MG_ACTIVE_CLASS} div[data-testid="grid-row"] {
        display: none !important;
      }
      #${OVERLAY_ID} {
        display: grid;
        grid-template-columns: ${getGridTemplate()};
        gap: 4px;
        pointer-events: auto;
      }
      #${OVERLAY_ID} .${NS}-mg-card {
        display: block;
        aspect-ratio: 1 / 1;
        overflow: hidden;
        text-decoration: none;
        position: relative;
      }
      #${OVERLAY_ID} .${NS}-mg-card .${NS}-mg-img {
        display: block;
        width: 100%; height: 100%;
        object-fit: cover;
        object-position: center;
      }
      #${OVERLAY_ID} .${NS}-mg-meta {
        position: absolute; bottom: 0; left: 0; right: 0;
        background: linear-gradient(transparent, rgba(0,0,0,0.85));
        padding: 6px 8px; display: flex; align-items: center;
        justify-content: space-between; pointer-events: none;
      }
      #${OVERLAY_ID} .${NS}-mg-left {
        display: flex; align-items: center; gap: 6px; min-width: 0;
      }
      #${OVERLAY_ID} .${NS}-mg-right {
        display: flex; align-items: center; gap: 3px; flex-shrink: 0;
      }
      #${OVERLAY_ID} .${NS}-mg-avatar {
        width: 24px; height: 24px; border-radius: 50%;
        object-fit: cover; flex-shrink: 0;
      }
      #${OVERLAY_ID} .${NS}-mg-username {
        color: #fff; font-size: 11px; white-space: nowrap;
        overflow: hidden; text-overflow: ellipsis;
      }
      #${OVERLAY_ID} .${NS}-mg-star {
        color: #fff; width: 12px; height: 12px; flex-shrink: 0;
      }
      #${OVERLAY_ID} .${NS}-mg-likes {
        color: #fff; font-size: 11px; opacity: 0.8; font-weight: 700;
      }
    `;
  }

  function removeOverlayStyles() {
    const el = document.getElementById(OVERLAY_STYLE_ID);
    if (el) el.remove();
  }

  // Extract metadata (img, link, avatar, username, likes) from a DA section
  function extractPost(section) {
    if (section.classList.contains(NS + "-hp-hidden")) return null;
    if (section.style.display === "none") return null;
    if (STATE.hidePaidEnabled && section.querySelector('img[src*="locked-small"], img[src*="locked-large"]')) return null;
    if (STATE.hidePaidEnabled && section.querySelector('[class*="unlock"]')) return null;
    var row = section.closest('[data-testid="grid-row"], [data-testid="content_row"]');
    if (row && row.style.display === "none") return null;
    var img = section.querySelector('img[property="contentUrl"]')
           || section.querySelector('img[src]:not([alt*="avatar" i])');
    if (!img) return null;
    var link = section.querySelector('a[href*="/art/"]') || section.closest('a');

    var avatarEl = section.querySelector('img[alt*="avatar" i]');
    var avatarSrc = avatarEl ? (avatarEl.src || avatarEl.getAttribute("src") || "") : "";

    // Username from URL pathname — more reliable than data-username in grid rows
    var username = "";
    if (link) {
      var up = link.pathname.split('/');
      if (up.length >= 4) username = up[1];
    }

    var likes = "";
    var favBtn = section.querySelector('button[aria-label*="Favourite" i]');
    if (favBtn) {
      var cs = favBtn.querySelector(':scope > span:last-child');
      if (cs) likes = cs.textContent.trim();
    }

    return {
      imgSrc: img.src || img.getAttribute("src") || "",
      href: link ? link.href : "",
      avatarSrc: avatarSrc,
      username: username,
      likes: likes,
    };
  }

  // Build a grid-mode card element from extracted data
  function createCard(data) {
    var card = document.createElement("a");
    card.className = NS + "-mg-card";
    if (data.href) card.href = data.href;
    card.target = "_blank";
    card.rel = "noopener noreferrer";

    var wrap = document.createElement("div");
    wrap.style.width = "100%";
    wrap.style.height = "100%";
    wrap.style.position = "relative";

    var img = document.createElement("img");
    img.className = NS + "-mg-img";
    img.src = data.imgSrc;
    img.loading = "lazy";
    wrap.appendChild(img);

    // Metadata overlay — skipped when image-only mode is active
    if (!STATE.zoomHideOverlays && (data.avatarSrc || data.username || data.likes)) {
      var meta = document.createElement("div");
      meta.className = NS + "-mg-meta";

      var left = document.createElement("div");
      left.className = NS + "-mg-left";
      if (data.avatarSrc) {
        var av = document.createElement("img");
        av.className = NS + "-mg-avatar";
        av.src = data.avatarSrc;
        av.loading = "lazy";
        left.appendChild(av);
      }
      if (data.username) {
        var un = document.createElement("span");
        un.className = NS + "-mg-username";
        un.textContent = data.username;
        left.appendChild(un);
      }
      meta.appendChild(left);

      if (data.likes) {
        var right = document.createElement("div");
        right.className = NS + "-mg-right";
        var lk = document.createElement("span");
        lk.className = NS + "-mg-likes";
        lk.textContent = data.likes;
        right.appendChild(lk);
        var star = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        star.setAttribute("class", NS + "-mg-star");
        star.setAttribute("width", "12");
        star.setAttribute("height", "12");
        star.setAttribute("viewBox", "0 0 24 24");
        star.setAttribute("fill", "none");
        star.setAttribute("stroke", "currentColor");
        star.setAttribute("stroke-width", "2.2");
        star.setAttribute("stroke-linecap", "round");
        star.setAttribute("stroke-linejoin", "round");
        var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path.setAttribute("d", "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z");
        star.appendChild(path);
        right.appendChild(star);
        meta.appendChild(right);
      }

      wrap.appendChild(meta);
    }

    card.appendChild(wrap);
    return card;
  }

  // Find the first grid-row parent container
  function getWrapper() {
    var rows = document.querySelectorAll('div[data-testid="grid-row"]');
    return rows.length ? rows[0].parentNode : null;
  }

  // Collect all posts from DA grid rows, deduped by href
  function seedPosts() {
    _posts.length = 0;
    document.querySelectorAll('div[data-testid="grid-row"] section').forEach(function (section) {
      var data = extractPost(section);
      if (data && !_posts.some(function (p) { return p.href === data.href; })) {
        _posts.push(data);
      }
    });
  }

  // Scan for new posts appended by DA infinite scroll and insert them
  function appendNewPosts() {
    var added = 0;
    document.querySelectorAll('div[data-testid="grid-row"]').forEach(function (row) {
      row.querySelectorAll('section').forEach(function (section) {
        var data = extractPost(section);
        if (!data) return;
        if (_posts.some(function (p) { return p.href === data.href; })) return;
        _posts.push(data);
        var overlay = document.getElementById(OVERLAY_ID);
        if (overlay && _scrollSentinel) {
          overlay.insertBefore(createCard(data), _scrollSentinel);
        }
        added++;
      });
    });
    if (added) log("appended", added, "posts");
    return added;
  }

  // Append all seeded cards to the overlay element
  function populateOverlay(overlay) {
    _posts.forEach(function (p) {
      overlay.appendChild(createCard(p));
    });
    _scrollSentinel = document.createElement("div");
    _scrollSentinel.style.height = "1px";
    overlay.appendChild(_scrollSentinel);
  }

  // Create the overlay grid, hide original DA rows
  function buildOverlayGrid() {
    var wrapper = getWrapper();
    if (!wrapper) return;
    if (document.getElementById(OVERLAY_ID)) return;

    document.documentElement.classList.add(MG_ACTIVE_CLASS);

    var overlay = document.createElement("div");
    overlay.id = OVERLAY_ID;
    populateOverlay(overlay);

    wrapper.insertBefore(overlay, wrapper.firstChild);
    log("built overlay with", _posts.length, "posts");

    // Re-sync STT after page reflow from grid replacement
    if (typeof toggleScrollBtn === "function") {
      toggleScrollBtn(window.scrollY > STT_SHOW_OFFSET);
    }
    // Re-create menu button if DA React removed it
    if (typeof buildPanel === "function" && !document.getElementById(BTN_ID)) {
      buildPanel();
    }
  }

  // Remove overlay and restore original DA grid rows
  function destroyOverlayGrid() {
    var overlay = document.getElementById(OVERLAY_ID);
    if (overlay) overlay.remove();
    document.documentElement.classList.remove(MG_ACTIVE_CLASS);
    _scrollSentinel = null;
  }

  // Toggle grid overlay on/off based on STATE.mobileGridEnabled
  function updateMobileGrid() {
    if (STATE.mobileGridEnabled) {
      injectOverlayStyles();
      if (document.querySelector('div[data-testid="grid-row"]')) {
        seedPosts();
        buildOverlayGrid();
      }
    } else {
      destroyOverlayGrid();
      removeOverlayStyles();
      _posts.length = 0;
    }
  }

  // Rebuild all cards in-place (used when toggles affect card content)
  function rebuildOverlayGrid() {
    if (!STATE.mobileGridEnabled) return;
    var overlay = document.getElementById(OVERLAY_ID);
    if (overlay) {
      while (overlay.firstChild) overlay.removeChild(overlay.firstChild);
      seedPosts();
      populateOverlay(overlay);
    } else {
      destroyOverlayGrid();
      seedPosts();
      buildOverlayGrid();
    }
  }

  // Update column count without rebuilding the full overlay
  function updateOverlayColumns() {
    var overlay = document.getElementById(OVERLAY_ID);
    if (overlay) overlay.style.gridTemplateColumns = getGridTemplate();
  }

  // Handle DOM mutations: detect new/removed grid-rows, sync overlay
  function onMobileGridMutation(mutations) {
    if (!STATE.mobileGridEnabled) return;
    var changed = Array.from(mutations).some(function (m) {
      return m.type === "childList" && Array.from(m.addedNodes).concat(Array.from(m.removedNodes)).some(function (n) {
        return n.nodeType === 1 && (n.matches && n.matches('[data-testid="grid-row"]') || n.querySelector && n.querySelector('[data-testid="grid-row"]'));
      });
    });
    if (changed) {
      if (document.getElementById(OVERLAY_ID)) {
        appendNewPosts();
      } else {
        seedPosts();
        buildOverlayGrid();
      }
      if (typeof updateMobileGridControlsVisibility === "function") updateMobileGridControlsVisibility();
    }
  }

  const MR_SEL = '.EVgt_3 a';

  function autoClickMoreResults() {
    if (!STATE.moreResultsEnabled) return;
    var links = document.querySelectorAll(MR_SEL);
    for (var i = 0; i < links.length; i++) {
      var a = links[i];
      if (a.dataset.datbProcessed) continue;
      if (a.textContent.includes("More Results") && a.offsetParent !== null) {
        a.dataset.datbProcessed = "1";
        a.click();
        break;
      }
    }
  }

  // Parse max dimensions from a wixmp JWT token payload
  function getMaxFromToken(url) {
    const m = url.match(/\?token=([^&]+)/);
    if (!m) return null;
    try {
      const parts = m[1].split(".");
      const payload = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"));
      const data = JSON.parse(payload);
      if (data.obj && data.obj[0] && data.obj[0][0]) {
        const dims = data.obj[0][0];
        const maxW = parseInt(String(dims.width).replace(/[^0-9]/g, ""), 10);
        const maxH = parseInt(String(dims.height).replace(/[^0-9]/g, ""), 10);
        if (maxW && maxH) return { w: maxW, h: maxH };
      }
    } catch (e) {
      log("token parse error:", e);
    }
    return null;
  }

  // Rewrite a wixmp image URL to request the highest available resolution
  function getFullResUrl(src, rectW, rectH) {
    log("getFullResUrl input:", src);
    if (!src || !src.includes("wixmp.com")) {
      log("not wixmp, returning as-is");
      return src;
    }
    const max = getMaxFromToken(src);
    const limitW = max ? max.w : Math.round(Math.max(rectW, 2000));
    const limitH = max ? max.h : Math.round(Math.max(rectH, 2000));
    let w, h;
    if (max) {
      w = limitW;
      h = limitH;
    } else {
      const aspect = rectW / rectH;
      if (aspect >= 1) {
        w = limitW;
        h = Math.round(limitW / aspect);
        if (h > limitH) { h = limitH; w = Math.round(limitH * aspect); }
      } else {
        h = limitH;
        w = Math.round(limitH * aspect);
        if (w > limitW) { w = limitW; h = Math.round(limitW / aspect); }
      }
    }
    const changed = src.replace(
      /\/v1\/([^/]+)\/([^/]+)(?=\/[^/]+$)/,
      (match, mode, params) => {
        params = params.replace(/w_\d+/g, "w_" + w);
        params = params.replace(/h_\d+/g, "h_" + h);
        params = params.replace(/q_\d+/g, "q_100");
        return "/v1/" + mode + "/" + params;
      },
    );
    if (changed === src) {
      log("regex did not match, returning as-is");
      return src;
    }
    const cleaned = changed.replace("wixmp.com/intermediary/", "wixmp.com/");
    log("getFullResUrl output:", cleaned);
    return cleaned;
  }

  // Create a centered full-resolution clone of the hovered card's image
  function createClone(card) {
    if (!card) return;
    if (zCloneCard === card) return;
    const img = card.querySelector("img:not([alt*='avatar' i])");
    if (!img) return;
    removeClone();
    const rect = img.getBoundingClientRect();
    const nw = img.naturalWidth || img.width || rect.width;
    const nh = img.naturalHeight || img.height || rect.height;
    const clone = img.cloneNode(true);
    clone.alt = "";
    clone.removeAttribute("width");
    clone.removeAttribute("height");
    clone.style.cssText = [
      "position: fixed !important",
      "z-index: 2147483647 !important",
      "top: 50% !important",
      "left: 50% !important",
      "translate: -50% -50% !important",
      "pointer-events: none !important",
      "contain: layout style !important",
      "object-fit: contain !important",
      "width: " + nw + "px !important",
      "height: " + nh + "px !important",
      "max-width: 95vw !important",
      "max-height: 95vh !important",
    ].join(";");
    const s = wheelScale ?? 2;
    clone.style.transform = "scale(" + s + ")";
    zClone = clone;
    zCloneCard = card;
    document.documentElement.appendChild(clone);

    if (STATE.zoomFullRes) {
      const fullUrl = getFullResUrl(
        img.src || img.getAttribute("src"),
        rect.width,
        rect.height,
      );
      log("createClone img.src:", img.src, "fullUrl:", fullUrl);
      if (fullUrl && fullUrl !== (img.src || img.getAttribute("src"))) {
        zSpinner = document.createElement("div");
        zSpinner.className = NS + "-spinner";
        document.documentElement.appendChild(zSpinner);
        const fullImg = new Image();
        fullImg.onload = () => {
          if (zClone === clone) {
            clone.src = fullImg.src;
            if (zSpinner) { zSpinner.remove(); zSpinner = null; }
          }
        };
        fullImg.onerror = () => {
          if (zSpinner) { zSpinner.remove(); zSpinner = null; }
        };
        fullImg.src = fullUrl;
      }
    }
  }

  function updateClone() {
    if (!zClone) return;
    const s = wheelScale ?? 2;
    zClone.style.transform = "scale(" + s + ")";
  }

  function removeClone() {
    if (zSpinner) { zSpinner.remove(); zSpinner = null; }
    if (!zClone) return;
    zClone.remove();
    zClone = null;
    zCloneCard = null;
  }

  function onWheel(e) {
    if (!zHeld || !STATE.zoomZEnabled) return;
    const card = document.querySelector("." + ACTIVE);
    if (!card && !zClone) return;
    e.preventDefault();
    const delta = e.deltaY > 0 ? -0.2 : 0.2;
    const base = zClone ? 2 : STATE.zoomScale;
    const current = wheelScale ?? base;
    const min = zClone ? 1 : base;
    const max = 100;
    const updated = Math.round(Math.min(max, Math.max(min, current + delta)) * 100) / 100;
    if (updated === current) return;
    wheelScale = updated;
    if (zClone) {
      updateClone();
    } else {
      injectZoomStyles();
    }
  }

  function onKeyDown(e) {
    if ((e.key === "z" || e.key === "Z") && STATE.zoomZEnabled) {
      zHeld = true;
      document.documentElement.classList.add(NS + "-zheld");
      var card = document.querySelector(CARD_SEL + "." + ACTIVE);
      if (!card) card = document.querySelector("a." + NS + "-mg-card." + ACTIVE);
      if (!card) card = document.querySelector("a." + NS + "-mg-card:hover");
      if (!card && _lastMgCard) card = _lastMgCard;
      if (card) createClone(card);
    }
  }

  function onKeyUp(e) {
    if (e.key === "z" || e.key === "Z") {
      zHeld = false;
      wheelScale = null;
      document.documentElement.classList.remove(NS + "-zheld");
      removeClone();
    }
  }

  // Debounce guard for MutationObserver callback
  let _mutationRAF = null;

  // Batch handler for DOM mutations: throttle via rAF, run all feature passes
  function onMutation(mutations) {
    if (_mutationRAF) return;
    _mutationRAF = requestAnimationFrame(() => {
      _mutationRAF = null;
      blockAds();
      if (STATE.hidePaidEnabled) hidePaidSchedule();
      filterTextPosts();
      filterCarousels();
      if (STATE.mobileGridEnabled && typeof onMobileGridMutation === "function") {
        onMobileGridMutation(mutations);
      }
      autoClickMoreResults();
    });
  }

  // Entrypoint — called once when DOM is ready
  function init() {
    loadSettings();
    addPanelStyles();
    injectZoomStyles();
    injectHidePaidStyles();
    buildPanel();
    blockAds();
    filterTextPosts();
    filterCarousels();
    if (STATE.scrollToTopEnabled) initScrollBtn();
    updateHtmlClass();
    updateImageOnlyClass();
    if (STATE.hidePaidEnabled) hidePaidScan();
    updateMobileGrid();
    autoClickMoreResults();

    // Event listeners for zoom/hover and Z-zoom
    document.addEventListener("mouseover", onHover, true);
    document.addEventListener("mouseout", onUnhover, true);
    document.addEventListener("keydown", onKeyDown);
    document.addEventListener("keyup", onKeyUp);
    document.addEventListener("wheel", onWheel, { passive: false, capture: true });

    // Watch for DA's React-driven DOM changes
    const observer = new MutationObserver(onMutation);
    observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["style"] });
  }

  // Auto-boot: wait for DOMContentLoaded if page is still loading
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }



})();