Shop Wizard / SSW By web crawler 18 installs Rating 0.0 (0) approved

Neopets - SSW Sniper

Neopets SSW Sniper automatically scans the Super Shop Wizard for the cheapest listings so you can buy the best deals before anyone else.
neopets sniper ssw
Install
https://www.scriptneo.com/script/neopets-ssw-sniper

Version selector


SHA256
76c64a5f40336d3579a40fa57533188bea489068cb88d4e1d7427725a3797c73
Static scan
Score: 3
Flags: uses_gm_xmlhttprequest, uses_clipboard

Source code

// ==UserScript==
// @name         Neopets - SSW Sniper
// @namespace    http://tampermonkey.net/
// @version      4.3.3
// @description  Batch-search the Super Shop Wizard with a professional UI: tabs, progress bar, quantity badges, failed-search log, automatic SSW cooldown handling, batch mode and single-item mode with random delay (in ms), full settings panel stored via GM_set/GM_get, pew-pew style sound alert and animated highlight for available items, and a reset-to-defaults button so you never have to edit the code by hand again.
// @author       You
// @match        *://www.neopets.com/*
// @exclude      https://www.neopets.com/neomessages.phtml*
// @exclude      https://www.neopets.com/neomail_block_check.phtml*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      neopets.com
// @noframes
// @downloadURL  https://www.scriptneo.com/scripts/download.php?id=22
// @updateURL    https://www.scriptneo.com/scripts/download.php?id=22
// ==/UserScript==

(function () {
  'use strict';

  /*** SETTINGS DEFAULTS ***/
  const SETTINGS_KEY = 'sswBatchSearchSettings';

  const DEFAULT_DISCOUNT_THRESHOLD = 0.60;      // 60 percent of second price
  const DEFAULT_SHOW_SINGLES = true;            // accept when only one listing is returned
  const DEFAULT_BATCH_SIZE = 50;                // concurrent searches per batch
  const DEFAULT_MAX_RETRIES_PER_ITEM = 1;       // network or parse retries
  const DEFAULT_COOLDOWN_FALLBACK_MIN = 30;     // if SSW does not provide minutes
  const DEFAULT_BATCH_DELAY_SECONDS = 30;       // delay between batches in seconds
  const DEFAULT_SINGLE_DELAY_MIN_MS = 100;      // min ms for single mode delay
  const DEFAULT_SINGLE_DELAY_MAX_MS = 2000;     // default max ms for single mode delay
  const DEFAULT_ICON_SIZE = 50;                 // launcher icon size in pixels
  const DEFAULT_SOUND_ALERT_ENABLED = true;     // play sound when a new item is available
  const DEFAULT_FANCY_AVAILABLE_STYLE_ENABLED = true; // animated highlight for available item names

  /*** STATE (MUTABLE SETTINGS) ***/
  let discountThreshold = DEFAULT_DISCOUNT_THRESHOLD;
  let showSingles = DEFAULT_SHOW_SINGLES;
  let batchSize = DEFAULT_BATCH_SIZE;
  let maxRetriesPerItem = DEFAULT_MAX_RETRIES_PER_ITEM;
  let cooldownFallbackMin = DEFAULT_COOLDOWN_FALLBACK_MIN;
  let batchDelaySeconds = DEFAULT_BATCH_DELAY_SECONDS;
  let singleDelayMinMs = DEFAULT_SINGLE_DELAY_MIN_MS;
  let singleDelayMaxMs = DEFAULT_SINGLE_DELAY_MAX_MS;
  let iconSize = DEFAULT_ICON_SIZE;
  let soundAlertEnabled = DEFAULT_SOUND_ALERT_ENABLED;
  let fancyAvailableStyleEnabled = DEFAULT_FANCY_AVAILABLE_STYLE_ENABLED;

  let searchMode = 'batch'; // 'batch' or 'single'

  /*** RUN STATE ***/
  let isPaused = false;
  let banCooldownTimeout = null;
  let itemsQueued = 0;
  let itemsSearchedCount = 0;

  const failedItemsRaw = [];         // final failures (for counts)
  const availableCounts = new Map(); // itemName -> count of appearances in INPUT
  const availableMeta = new Map();   // itemName -> { prices, linksHTML, qty }

  /*** PERSISTENCE ***/
  function loadSettings() {
    try {
      if (typeof GM_getValue !== 'function') return;
      const raw = GM_getValue(SETTINGS_KEY, null);
      if (!raw) return;
      const data = JSON.parse(raw);

      if (typeof data.discountThreshold === 'number') {
        discountThreshold = data.discountThreshold;
      }
      if (typeof data.showSingles === 'boolean') {
        showSingles = data.showSingles;
      }
      if (typeof data.batchSize === 'number') {
        batchSize = data.batchSize;
      }
      if (typeof data.maxRetriesPerItem === 'number') {
        maxRetriesPerItem = data.maxRetriesPerItem;
      }
      if (typeof data.cooldownFallbackMin === 'number') {
        cooldownFallbackMin = data.cooldownFallbackMin;
      }
      if (typeof data.batchDelaySeconds === 'number') {
        batchDelaySeconds = data.batchDelaySeconds;
      }

      if (typeof data.singleDelayMinMs === 'number') {
        singleDelayMinMs = data.singleDelayMinMs;
      } else if (typeof data.singleDelayMinSeconds === 'number') {
        singleDelayMinMs = data.singleDelayMinSeconds * 1000;
      }

      if (typeof data.singleDelayMaxMs === 'number') {
        singleDelayMaxMs = data.singleDelayMaxMs;
      } else if (typeof data.singleDelayMaxSeconds === 'number') {
        singleDelayMaxMs = data.singleDelayMaxSeconds * 1000;
      }

      if (typeof data.iconSize === 'number') {
        iconSize = data.iconSize;
      }
      if (data.searchMode === 'batch' || data.searchMode === 'single') {
        searchMode = data.searchMode;
      }
      if (typeof data.soundAlertEnabled === 'boolean') {
        soundAlertEnabled = data.soundAlertEnabled;
      }
      if (typeof data.fancyAvailableStyleEnabled === 'boolean') {
        fancyAvailableStyleEnabled = data.fancyAvailableStyleEnabled;
      }
    } catch (e) {
      // ignore bad settings object
    }
  }

  function saveSettings() {
    try {
      if (typeof GM_setValue !== 'function') return;
      const data = {
        discountThreshold,
        showSingles,
        batchSize,
        maxRetriesPerItem,
        cooldownFallbackMin,
        batchDelaySeconds,
        singleDelayMinMs,
        singleDelayMaxMs,
        iconSize,
        searchMode,
        soundAlertEnabled,
        fancyAvailableStyleEnabled
      };
      GM_setValue(SETTINGS_KEY, JSON.stringify(data));
    } catch (e) {
      // ignore
    }
  }

  loadSettings();

  /*** UTIL ***/
  const wait = (ms) => new Promise(r => setTimeout(r, ms));
  const addCommas = (n) => n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');

  // Respect small delays
  const waitWithPause = async (msTotal, label = 'delay') => {
    if (msTotal <= 300) {
      await wait(msTotal);
      if (isPaused) await waitUntilNotPaused(label);
      return;
    }
    const tick = 250;
    const start = Date.now();
    while (Date.now() - start < msTotal) {
      const elapsed = Date.now() - start;
      const remaining = msTotal - elapsed;
      if (remaining <= 0) break;
      await wait(Math.min(tick, remaining));
      if (isPaused) await waitUntilNotPaused(label);
    }
  };

  const groupCounts = (arr) => {
    const map = new Map();
    for (const x of arr) map.set(x, (map.get(x) || 0) + 1);
    return map;
  };

  const normalizeLines = (str) => str.replace(/\r\n/g, '\n').replace(/\r/g, '\n');

  // Random delay in ms, min at least 100, max no hard cap
  const getRandomSingleDelayMs = () => {
    let minMs = singleDelayMinMs;
    let maxMs = singleDelayMaxMs;

    if (!Number.isFinite(minMs)) minMs = DEFAULT_SINGLE_DELAY_MIN_MS;
    if (!Number.isFinite(maxMs)) maxMs = DEFAULT_SINGLE_DELAY_MAX_MS;

    if (minMs < 100) minMs = 100;
    if (maxMs < minMs) maxMs = minMs;

    const low = Math.round(minMs);
    const high = Math.round(maxMs);
    if (high === low) return low;
    return Math.floor(Math.random() * (high - low + 1)) + low;
  };

  /*** SOUND ALERT (pew pew style) ***/
  let audioCtx = null;
  function playPew() {
    if (!soundAlertEnabled) return;
    try {
      const Ctor = window.AudioContext || window.webkitAudioContext;
      if (!Ctor) return;
      if (!audioCtx) {
        audioCtx = new Ctor();
      }
      const now = audioCtx.currentTime;

      const osc1 = audioCtx.createOscillator();
      const gain1 = audioCtx.createGain();
      osc1.type = 'square';
      osc1.frequency.setValueAtTime(1400, now);
      osc1.frequency.exponentialRampToValueAtTime(600, now + 0.15);
      gain1.gain.setValueAtTime(0.0001, now);
      gain1.gain.linearRampToValueAtTime(0.4, now + 0.01);
      gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.18);
      osc1.connect(gain1);
      gain1.connect(audioCtx.destination);
      osc1.start(now);
      osc1.stop(now + 0.2);

      const osc2 = audioCtx.createOscillator();
      const gain2 = audioCtx.createGain();
      const start2 = now + 0.1;
      osc2.type = 'square';
      osc2.frequency.setValueAtTime(1600, start2);
      osc2.frequency.exponentialRampToValueAtTime(700, start2 + 0.13);
      gain2.gain.setValueAtTime(0.0001, start2);
      gain2.gain.linearRampToValueAtTime(0.35, start2 + 0.01);
      gain2.gain.exponentialRampToValueAtTime(0.001, start2 + 0.16);
      osc2.connect(gain2);
      gain2.connect(audioCtx.destination);
      osc2.start(start2);
      osc2.stop(start2 + 0.18);
    } catch (e) {
      // ignore audio errors
    }
  }

  /*** FLOATING LAUNCHER ICON ***/
  const launcher = document.createElement('img');
  launcher.src = 'https://images.neopets.com/premium/shopwizard/ssw-icon.svg';
  Object.assign(launcher.style, {
    position: 'fixed',
    bottom: '12px',
    right: '12px',
    width: iconSize + 'px',
    height: iconSize + 'px',
    zIndex: '2147483647',
    borderRadius: '12px',
    background: 'linear-gradient(#f6e250,#ebb233)',
    border: '1px solid rgba(0,0,0,0.3)',
    padding: '6px',
    boxShadow: '0 6px 18px rgba(0,0,0,0.25)',
    cursor: 'pointer'
  });
  document.body.appendChild(launcher);

  /*** SHADOW DOM PANEL ***/
  const host = document.createElement('div');
  host.style.position = 'fixed';
  host.style.inset = '0';
  host.style.zIndex = '2147483646';
  host.style.pointerEvents = 'none';
  document.body.appendChild(host);
  const root = host.attachShadow({ mode: 'open' });

  root.innerHTML = `
    <style>
      :host { all: initial; }
      * { box-sizing: border-box; font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; }

      .panel {
        position: fixed;
        top: 10vh;
        right: 4vw;
        width: min(720px, 92vw);
        max-height: 78vh;
        display: none;
        pointer-events: auto;
        background: #ffffff;
        color: #222;
        border: 1px solid #e6e6e6;
        border-radius: 14px;
        box-shadow: 0 12px 40px rgba(0,0,0,0.22);
        overflow: hidden;
      }

      .panel.dark {
        background: #111418;
        color: #e6e8ea;
        border-color: #20242a;
      }

      .header {
        display:flex; align-items:center; gap:10px; padding:12px 14px;
        background: linear-gradient(180deg, rgba(0,0,0,0.04), transparent);
        border-bottom: 1px solid rgba(0,0,0,0.06);
        cursor: move;
      }
      .dark .header { border-bottom-color:#1d2228; background: linear-gradient(180deg, #14181f, #0f1318); }

      .title {
        font-weight: 700; letter-spacing: 0.2px; font-size: 15px; flex: 1 1 auto;
      }
      .chip {
        padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 600;
        border: 1px solid #ececec; background:#fff;
      }
      .dark .chip { background:#171b21; border-color:#242a33; }

      .controls { display:flex; gap:8px; align-items:center; }
      .btn {
        appearance:none; border:1px solid #dcdcdc; background:#fff; color:#111; font-weight:600;
        padding:8px 12px; border-radius:10px; cursor:pointer; transition:all 0.12s ease;
      }
      .btn:hover { transform: translateY(-1px); box-shadow: 0 4px 10px rgba(0,0,0,0.08); }
      .btn:disabled { opacity:.6; cursor:not-allowed; transform:none; box-shadow:none; }
      .btn.primary { background: #1a73e8; border-color:#1a73e8; color:#fff; }
      .btn.green   { background: #15a34a; border-color:#15a34a; color:#fff; }
      .btn.red     { background: #dc2626; border-color:#dc2626; color:#fff; }
      .btn.ghost   { background: transparent; border-color: #cfcfcf; }
      .dark .btn { background:#14181f; border-color:#242b34; color:#e7eaee; }
      .dark .btn.primary { background:#2563eb; border-color:#2563eb; color:#fff; }
      .dark .btn.green   { background:#16a34a; border-color:#16a34a; }
      .dark .btn.red     { background:#dc2626; border-color:#dc2626; }

      .body { display:flex; gap:12px; padding:12px; }
      .col { display:flex; flex-direction:column; gap:10px; }

      .card {
        border:1px solid #ebebeb; border-radius:12px; padding:10px; background:#fff;
      }
      .dark .card { border-color:#222933; background:#0f1318; }

      .row { display:flex; gap:10px; }
      .row.wrap { flex-wrap:wrap; }
      .grow { flex: 1 1 0; min-width: 0; }

      textarea.input {
        width:100%; min-height:120px; resize:vertical; padding:10px 12px; border-radius:10px;
        border:1px solid #d7d7d7; outline:none; font-size:14px; line-height:1.35;
      }
      .dark textarea.input { background:#0c1015; border-color:#222933; color:#e7eaee; }

      .statbar { display:flex; gap:8px; flex-wrap:wrap; }
      .stat {
        display:flex; align-items:center; gap:6px; padding:8px 10px; border-radius:10px;
        border:1px solid #ececec; background:#fafafa; font-size:13px; font-weight:600;
      }
      .dark .stat { border-color:#1f2630; background:#12171e; }

      .progress {
        position:relative; height:10px; width:100%; border-radius:999px; background:#eee; overflow:hidden;
      }
      .progress > .fill { position:absolute; inset:0; width:0%; background: linear-gradient(90deg, #4f46e5, #22c55e); }

      .tabs { display:flex; gap:6px; border-bottom:1px solid #eee; }
      .dark .tabs { border-bottom-color:#1f2630; }
      .tab {
        padding:8px 10px; border:1px solid transparent; border-radius:10px 10px 0 0; cursor:pointer; font-weight:600;
      }
      .tab.active {
        border-color:#e6e6e6; border-bottom-color:transparent; background:#fff;
      }
      .dark .tab.active { border-color:#242a33; background:#0f1318; }
      .tabpanes { padding:10px 0 0 0; }

      .list { max-height:220px; overflow:auto; border:1px solid #ececec; border-radius:10px; padding:6px; background:#fff; }
      .dark .list { border-color:#1f2630; background:#0f1318; }
      .list ul { list-style: none; margin:0; padding:0; }
      .list li { padding:6px 8px; border-bottom:1px solid #f0f0f0; }
      .dark .list li { border-bottom-color:#1f2630; }

      .qty {
        display:inline-block; font-size:12px; padding:1px 8px; border-radius:12px;
        border:1px solid #f1c232; background:#fff3bf; margin-left:6px; vertical-align:baseline;
      }
      .dark .qty { background:#2a2111; border-color:#8a6a1a; }

      .available-highlight {
        position:relative;
        display:inline-block;
        padding:1px 6px;
        border-radius:999px;
        background:linear-gradient(90deg,#fb923c,#facc15);
        color:#111827;
        box-shadow:0 0 0 rgba(0,0,0,0.0);
        animation: availablePulse 1.6s ease-out 1;
      }
      .dark .available-highlight {
        color:#020617;
      }
      @keyframes availablePulse {
        0%   { transform:scale(1);   box-shadow:0 0 0 rgba(251,146,60,0); }
        30%  { transform:scale(1.06); box-shadow:0 0 18px rgba(250,204,21,0.7); }
        100% { transform:scale(1);   box-shadow:0 0 0 rgba(251,146,60,0); }
      }

      .settings-title {
        font-size:12px;
        font-weight:700;
        text-transform:uppercase;
        letter-spacing:0.08em;
        opacity:0.75;
        margin-bottom:4px;
      }
      .settings-row {
        display:flex;
        flex-wrap:wrap;
        gap:8px 16px;
        align-items:center;
        margin-top:6px;
        font-size:13px;
      }
      .settings-row label {
        display:flex;
        align-items:center;
        gap:6px;
      }
      .settings-row input[type="number"] {
        width:90px;
        padding:4px 6px;
        border-radius:8px;
        border:1px solid #d0d0d0;
        font-size:13px;
      }
      .dark .settings-row input[type="number"] {
        background:#0c1015;
        border-color:#222933;
        color:#e7eaee;
      }

      .toggle-btn {
        position:relative;
        width:40px;
        height:20px;
        border-radius:999px;
        border:1px solid #cfd2d6;
        background:#e5e7eb;
        padding:0;
        cursor:pointer;
        display:inline-flex;
        align-items:center;
        transition:background 0.15s ease, border-color 0.15s ease;
      }
      .toggle-knob {
        position:relative;
        width:16px;
        height:16px;
        border-radius:999px;
        background:#ffffff;
        margin-left:2px;
        transition:transform 0.15s ease;
        box-shadow:0 1px 3px rgba(0,0,0,0.25);
      }
      .toggle-btn.on {
        background:#22c55e;
        border-color:#16a34a;
      }
      .toggle-btn.on .toggle-knob {
        transform:translateX(18px);
      }
      .dark .toggle-btn {
        background:#1f2933;
        border-color:#3b4251;
      }
      .dark .toggle-btn.on {
        background:#16a34a;
        border-color:#15803d;
      }

      .copy-icon {
        display:inline-flex;
        align-items:center;
        justify-content:center;
        width:18px;
        height:18px;
        margin-left:6px;
        border-radius:4px;
        border:1px solid #d0d0d0;
        font-size:11px;
        cursor:pointer;
        user-select:none;
        transition:background 0.12s ease, transform 0.12s ease, opacity 0.12s ease;
      }
      .copy-icon:hover {
        background:#f3f4f6;
        transform:translateY(-1px);
      }
      .dark .copy-icon {
        border-color:#2a313b;
        background:#151a21;
      }
      .dark .copy-icon:hover {
        background:#1f2630;
      }

      .linkicons img { vertical-align:middle; height:16px; margin-left:6px; filter: drop-shadow(0 0 0 rgba(0,0,0,0)); }

      .log { max-height:220px; overflow:auto; border:1px solid #ececec; border-radius:10px; padding:6px; background:#fff; }
      .dark .log { border-color:#1f2630; background:#0f1318; }
      .notice { padding:6px 8px; border-radius:8px; margin:6px 0; font-size:13px; }
      .neutral { background:#f6f7f8; border:1px solid #ececec; }
      .positive{ background:#e6f7ee; border:1px solid #b5e0c6; }
      .warning { background:#fff7e6; border:1px solid #f8e2b1; }
      .error   { background:#fdecec; border:1px solid #f4bdbd; }
      .dark .neutral { background:#141920; border-color:#1f2630; }
      .dark .positive{ background:#11261a; border-color:#1f3a28; }
      .dark .warning { background:#2a2417; border-color:#4d3c1d; }
      .dark .error   { background:#2a1717; border-color:#4a2121; }

      .ban-notice {
        display:flex;
        align-items:center;
        justify-content:space-between;
        gap:8px;
        margin-bottom:8px;
        padding:6px 8px;
        border-radius:8px;
        background:#fee2e2;
        border:1px solid #fecaca;
        color:#7f1d1d;
        font-size:13px;
      }
      .ban-close {
        appearance:none;
        border:none;
        background:transparent;
        color:inherit;
        cursor:pointer;
        padding:2px 6px;
        border-radius:4px;
        font-size:13px;
        font-weight:600;
      }
      .ban-close:hover {
        background:rgba(0,0,0,0.06);
      }
      .dark .ban-notice {
        background:#451a1a;
        border-color:#7f1d1d;
        color:#fee2e2;
      }
      .dark .ban-close:hover {
        background:rgba(255,255,255,0.12);
      }

      .footer {
        display:flex; justify-content:space-between; align-items:center;
        padding:10px 12px; border-top:1px solid #ececec; background: #fafafa;
      }
      .dark .footer { border-top-color:#1f2630; background:#0f1318; }

      .minbtn {
        width:26px; height:26px; border-radius:6px; border:1px solid #d0d0d0; background:#fff; cursor:pointer;
      }
      .dark .minbtn { background:#151a21; border-color:#242b34; }

      @media (max-width: 640px) {
        .body { flex-direction:column; }
      }
    </style>

    <div class="panel" id="panel">
      <div class="header" id="dragbar">
        <div class="title">SSW Batch Search</div>
        <div class="chip" id="modeChip">LIVE</div>
        <div class="controls">
          <button class="btn ghost" id="btnTheme" title="Toggle dark or light">Theme</button>
          <button class="btn ghost" id="btnSettings" title="Show or hide settings">Settings</button>
          <button class="btn" id="btnPause">Pause</button>
          <button class="btn green" id="btnResume">Resume</button>
          <button class="btn red" id="btnClear">Clear</button>
          <button class="btn primary" id="btnStart">Start</button>
          <button class="minbtn" id="btnMin" title="Minimize">-</button>
          <button class="btn red" id="btnClose" title="Close">&times;</button>
        </div>
      </div>

      <div class="body">
        <div class="col grow">

          <div class="card" id="settingsCard" style="display:none;">
            <div class="settings-title">Search settings</div>

            <div class="settings-row">
              <span>Mode</span>
              <span id="modeLabel" style="font-weight:600;">Search in batches</span>
            </div>

            <div class="settings-row">
              <span>Search one item at a time</span>
              <button type="button" id="singleToggle" class="toggle-btn" aria-pressed="false">
                <span class="toggle-knob"></span>
              </button>
              <span id="singleToggleState">Off</span>
            </div>

            <div class="settings-row">
              <label>
                <span>Single search delay range (minimum 100 ms, random)</span>
                <input type="number" id="singleDelayMinInput" min="100" step="50" value="100">
                <span>to</span>
                <input type="number" id="singleDelayMaxInput" min="100" step="50" value="2000">
              </label>
            </div>

            <div class="settings-row">
              <label>
                <span>Discount threshold percent (vs second price)</span>
                <input type="number" id="discountInput" min="1" max="100" step="1" value="60">
              </label>
            </div>

            <div class="settings-row">
              <label>
                <input type="checkbox" id="showSinglesInput">
                <span>Include items with only one listing</span>
              </label>
            </div>

            <div class="settings-row">
              <label>
                <span>Batch size</span>
                <input type="number" id="batchSizeInput" min="1" max="200" step="1" value="50">
              </label>
              <label>
                <span>Batch delay (seconds)</span>
                <input type="number" id="batchDelayInput" min="0" max="300" step="1" value="30">
              </label>
            </div>

            <div class="settings-row">
              <label>
                <span>Max retries per item</span>
                <input type="number" id="maxRetriesInput" min="0" max="5" step="1" value="1">
              </label>
              <label>
                <span>Fallback cooldown minutes</span>
                <input type="number" id="cooldownInput" min="1" max="120" step="1" value="30">
              </label>
            </div>

            <div class="settings-row">
              <label>
                <span>Launcher icon size (px)</span>
                <input type="number" id="iconSizeInput" min="24" max="120" step="2" value="50">
              </label>
            </div>

            <div class="settings-row">
              <label>
                <input type="checkbox" id="soundAlertInput">
                <span>Play sound when a new item is available</span>
              </label>
            </div>

            <div class="settings-row">
              <label>
                <input type="checkbox" id="fancyStyleInput">
                <span>Highlight available item names with animated style</span>
              </label>
            </div>

            <div class="settings-row" style="justify-content:flex-end; margin-top:10px;">
              <button type="button" id="resetSettingsBtn" class="btn ghost">Reset to defaults</button>
            </div>
          </div>

          <div class="card">
            <textarea id="itemInput" class="input" placeholder="Enter items, one per line"></textarea>
            <div class="row wrap" style="margin-top:10px;">
              <div class="statbar grow">
                <div class="stat">Queued&nbsp;<span id="statQueued">0</span></div>
                <div class="stat">Searched&nbsp;<span id="statSearched">0</span></div>
                <div class="stat">Available&nbsp;<span id="statAvailable">0</span></div>
                <div class="stat">Failed&nbsp;<span id="statFailed">0</span></div>
              </div>
            </div>
            <div class="progress" style="margin-top:10px;">
              <div class="fill" id="progressFill"></div>
            </div>
          </div>

          <div class="card">
            <div id="banNotice" class="ban-notice" style="display:none;">
              <span id="banNoticeText">You are currently SSW banned.</span>
              <button type="button" id="banNoticeClose" class="ban-close" title="Hide this notice">&times;</button>
            </div>

            <div class="tabs">
              <div class="tab active" data-tab="available">Available</div>
              <div class="tab" data-tab="failed">Failed</div>
              <div class="tab" data-tab="log">Log</div>
            </div>
            <div class="tabpanes">
              <div class="pane" data-pane="available">
                <div class="list">
                  <ul id="availableList"></ul>
                </div>
              </div>
              <div class="pane" data-pane="failed" style="display:none;">
                <div class="list">
                  <ul id="failedList"></ul>
                </div>
              </div>
              <div class="pane" data-pane="log" style="display:none;">
                <div class="log" id="log"></div>
              </div>
            </div>
          </div>

        </div>
      </div>

      <div class="footer">
        <div style="font-size:12px; opacity:.8;" id="footerBatchDelay">
          Handles SSW cooldowns automatically. 30s delay between batches.
        </div>
        <div style="display:flex; gap:8px;">
          <span class="chip" id="chipBatchSize">Batch size: 50</span>
          <span class="chip" id="chipDiscount">Discount at 60 percent of #2</span>
        </div>
      </div>
    </div>
  `;

  /*** SHADOW DOM REFS ***/
  const $ = (sel) => root.querySelector(sel);
  const panel = $('#panel');
  const dragbar = $('#dragbar');
  const btnStart = $('#btnStart');
  const btnPause = $('#btnPause');
  const btnResume = $('#btnResume');
  const btnClear = $('#btnClear');
  const btnClose = $('#btnClose');
  const btnTheme = $('#btnTheme');
  const btnSettings = $('#btnSettings');
  const btnMin = $('#btnMin');
  const modeChip = $('#modeChip');

  const itemInput = $('#itemInput');
  const statQueued = $('#statQueued');
  const statSearched = $('#statSearched');
  const statAvailable = $('#statAvailable');
  const statFailed = $('#statFailed');
  const progressFill = $('#progressFill');

  const availableList = $('#availableList');
  const failedList = $('#failedList');
  const logBox = $('#log');

  const tabs = Array.from(root.querySelectorAll('.tab'));
  const panes = Array.from(root.querySelectorAll('.pane'));

  const settingsCard = $('#settingsCard');

  const singleDelayMinInput = $('#singleDelayMinInput');
  const singleDelayMaxInput = $('#singleDelayMaxInput');
  const discountInput = $('#discountInput');
  const showSinglesInput = $('#showSinglesInput');
  const batchSizeInput = $('#batchSizeInput');
  const batchDelayInput = $('#batchDelayInput');
  const maxRetriesInput = $('#maxRetriesInput');
  const cooldownInput = $('#cooldownInput');
  const iconSizeInput = $('#iconSizeInput');
  const soundAlertInput = $('#soundAlertInput');
  const fancyStyleInput = $('#fancyStyleInput');
  const singleToggle = $('#singleToggle');
  const modeLabel = $('#modeLabel');
  const singleToggleState = $('#singleToggleState');
  const resetSettingsBtn = $('#resetSettingsBtn');

  const footerBatchDelay = $('#footerBatchDelay');
  const chipBatchSize = $('#chipBatchSize');
  const chipDiscount = $('#chipDiscount');

  const banNotice = $('#banNotice');
  const banNoticeText = $('#banNoticeText');
  const banNoticeClose = $('#banNoticeClose');

  /*** BAN NOTICE HELPERS ***/
  function showBanNotice(minutes) {
    if (!banNotice || !banNoticeText) return;
    const mins = Math.max(1, Math.round(minutes));
    banNoticeText.textContent = `You are currently SSW banned for ${mins} minute${mins === 1 ? '' : 's'}. Script will resume after ban.`;
    banNotice.style.display = 'flex';
  }
  function hideBanNotice() {
    if (!banNotice) return;
    banNotice.style.display = 'none';
  }
  if (banNoticeClose) {
    banNoticeClose.addEventListener('click', hideBanNotice);
  }

  /*** TAB LOGIC ***/
  tabs.forEach(tab => {
    tab.addEventListener('click', () => {
      tabs.forEach(t => t.classList.toggle('active', t === tab));
      panes.forEach(p => {
        p.style.display = (p.dataset.pane === tab.dataset.tab) ? '' : 'none';
      });
    });
  });

  /*** THEME TOGGLE ***/
  let dark = false;
  const applyTheme = () => {
    panel.classList.toggle('dark', dark);
  };
  btnTheme.addEventListener('click', () => {
    dark = !dark;
    applyTheme();
  });

  /*** DRAGGABLE + MINIMIZE ***/
  let dragging = false, startX = 0, startY = 0, startTop = 0, startLeft = 0;
  const onMouseDown = (e) => {
    dragging = true;
    startX = e.clientX;
    startY = e.clientY;
    const rect = panel.getBoundingClientRect();
    startTop = rect.top;
    startLeft = rect.left;
    document.addEventListener('mousemove', onMouseMove);
    document.addEventListener('mouseup', onMouseUp);
  };
  const onMouseMove = (e) => {
    if (!dragging) return;
    const dx = e.clientX - startX;
    const dy = e.clientY - startY;
    panel.style.top = Math.max(8, startTop + dy) + 'px';
    panel.style.left = Math.max(8, startLeft + dx) + 'px';
    panel.style.right = 'auto';
  };
  const onMouseUp = () => {
    dragging = false;
    document.removeEventListener('mousemove', onMouseMove);
    document.removeEventListener('mouseup', onMouseUp);
  };
  dragbar.addEventListener('mousedown', onMouseDown);

  let minimized = false;
  btnMin.addEventListener('click', () => {
    minimized = !minimized;
    panel.style.maxHeight = minimized ? '48px' : '78vh';
    panel.style.height = minimized ? '48px' : '';
    panel.querySelector('.body').style.display = minimized ? 'none' : '';
    panel.querySelector('.footer').style.display = minimized ? 'none' : '';
  });

  /*** OPEN/CLOSE ***/
  const showPanel = () => { panel.style.display = 'block'; host.style.pointerEvents = 'auto'; };
  const hidePanel = () => { panel.style.display = 'none'; host.style.pointerEvents = 'none'; };
  launcher.addEventListener('click', showPanel);
  btnClose.addEventListener('click', hidePanel);
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && panel.style.display === 'block') hidePanel();
  });

  /*** LOG + STATS UI ***/
  const log = (html, cls = 'neutral') => {
    const row = document.createElement('div');
    row.className = `notice ${cls}`;
    row.innerHTML = html;
    logBox.appendChild(row);
    logBox.scrollTop = logBox.scrollHeight;
  };

  const updateStats = () => {
    statQueued.textContent = String(itemsQueued);
    statSearched.textContent = String(itemsSearchedCount);

    let totalAvailableListings = 0;
    for (const meta of availableMeta.values()) totalAvailableListings += meta.qty || 1;
    statAvailable.textContent = String(totalAvailableListings);

    statFailed.textContent = String(failedItemsRaw.length);
    const total = Math.max(itemsQueued, 1);
    const pct = Math.min(100, Math.round((itemsSearchedCount / total) * 100));
    progressFill.style.width = pct + '%';
  };

  /*** FLOW HELPERS ***/
  const setPaused = (v) => {
    isPaused = v;
    modeChip.textContent = v ? 'PAUSED' : 'LIVE';
    modeChip.style.background = v ? '#fff7e6' : '';
    modeChip.style.borderColor = v ? '#f8e2b1' : '';
  };

  const waitUntilNotPaused = async (note) => {
    if (isPaused) log(`Awaiting resume <i>${note}</i>`, 'warning');
    while (isPaused) await wait(500);
  };

  /*** SETTINGS UI HELPERS ***/
  function clamp(val, min, max, fallback) {
    let n = parseFloat(val);
    if (!Number.isFinite(n)) n = fallback;
    if (n < min) n = min;
    if (n > max) n = max;
    return n;
  }

  let settingsVisible = false;
  const updateSettingsVisibility = () => {
    if (!settingsCard) return;
    settingsCard.style.display = settingsVisible ? '' : 'none';
    btnSettings.textContent = settingsVisible ? 'Settings ?' : 'Settings ?';
  };

  const applySettingsUI = () => {
    const isSingle = (searchMode === 'single');
    singleDelayMinInput.disabled = !isSingle;
    singleDelayMaxInput.disabled = !isSingle;

    if (isSingle) {
      singleToggle.classList.add('on');
      singleToggle.setAttribute('aria-pressed', 'true');
      modeLabel.textContent = 'Search one item at a time';
      singleToggleState.textContent = 'On';
    } else {
      singleToggle.classList.remove('on');
      singleToggle.setAttribute('aria-pressed', 'false');
      modeLabel.textContent = 'Search in batches';
      singleToggleState.textContent = 'Off';
    }
  };

  const updateSettingsDisplay = () => {
    if (footerBatchDelay) {
      footerBatchDelay.textContent = `Handles SSW cooldowns automatically. ${batchDelaySeconds}s delay between batches.`;
    }
    if (chipBatchSize) {
      chipBatchSize.textContent = `Batch size: ${batchSize}`;
    }
    if (chipDiscount) {
      chipDiscount.textContent = `Discount at ${Math.round(discountThreshold * 100)} percent of #2`;
    }
    launcher.style.width = iconSize + 'px';
    launcher.style.height = iconSize + 'px';
  };

  const syncSettingsToInputs = () => {
    if (!Number.isFinite(singleDelayMinMs) || singleDelayMinMs < 100) {
      singleDelayMinMs = DEFAULT_SINGLE_DELAY_MIN_MS;
    }
    if (!Number.isFinite(singleDelayMaxMs) || singleDelayMaxMs < singleDelayMinMs) {
      singleDelayMaxMs = singleDelayMinMs;
    }

    singleDelayMinInput.value = String(singleDelayMinMs);
    singleDelayMaxInput.value = String(singleDelayMaxMs);

    discountInput.value = String(Math.round(discountThreshold * 100));
    showSinglesInput.checked = !!showSingles;
    batchSizeInput.value = String(batchSize);
    batchDelayInput.value = String(batchDelaySeconds);
    maxRetriesInput.value = String(maxRetriesPerItem);
    cooldownInput.value = String(cooldownFallbackMin);
    iconSizeInput.value = String(iconSize);
    soundAlertInput.checked = !!soundAlertEnabled;
    fancyStyleInput.checked = !!fancyAvailableStyleEnabled;

    applySettingsUI();
    updateSettingsDisplay();
    updateSettingsVisibility();
  };

  function resetSettingsToDefaults() {
    discountThreshold = DEFAULT_DISCOUNT_THRESHOLD;
    showSingles = DEFAULT_SHOW_SINGLES;
    batchSize = DEFAULT_BATCH_SIZE;
    maxRetriesPerItem = DEFAULT_MAX_RETRIES_PER_ITEM;
    cooldownFallbackMin = DEFAULT_COOLDOWN_FALLBACK_MIN;
    batchDelaySeconds = DEFAULT_BATCH_DELAY_SECONDS;
    singleDelayMinMs = DEFAULT_SINGLE_DELAY_MIN_MS;
    singleDelayMaxMs = DEFAULT_SINGLE_DELAY_MAX_MS;
    iconSize = DEFAULT_ICON_SIZE;
    soundAlertEnabled = DEFAULT_SOUND_ALERT_ENABLED;
    fancyAvailableStyleEnabled = DEFAULT_FANCY_AVAILABLE_STYLE_ENABLED;
    searchMode = 'batch';
    saveSettings();
    syncSettingsToInputs();
    log('Settings reset to defaults.', 'neutral');
  }

  singleToggle.addEventListener('click', () => {
    searchMode = (searchMode === 'single') ? 'batch' : 'single';
    applySettingsUI();
    saveSettings();
  });

  singleDelayMinInput.addEventListener('change', () => {
    let n = parseFloat(singleDelayMinInput.value);
    if (!Number.isFinite(n)) n = DEFAULT_SINGLE_DELAY_MIN_MS;
    if (n < 100) n = 100;
    singleDelayMinMs = n;
    singleDelayMinInput.value = String(singleDelayMinMs);

    if (!Number.isFinite(singleDelayMaxMs) || singleDelayMaxMs < singleDelayMinMs) {
      singleDelayMaxMs = singleDelayMinMs;
      singleDelayMaxInput.value = String(singleDelayMaxMs);
    }
    saveSettings();
  });

  singleDelayMaxInput.addEventListener('change', () => {
    let n = parseFloat(singleDelayMaxInput.value);
    if (!Number.isFinite(n)) n = DEFAULT_SINGLE_DELAY_MAX_MS;
    if (n < singleDelayMinMs) n = singleDelayMinMs;
    singleDelayMaxMs = n;
    singleDelayMaxInput.value = String(singleDelayMaxMs);
    saveSettings();
  });

  discountInput.addEventListener('change', () => {
    const pct = clamp(discountInput.value, 1, 100, 60);
    discountInput.value = String(pct);
    discountThreshold = pct / 100;
    updateSettingsDisplay();
    saveSettings();
  });

  showSinglesInput.addEventListener('change', () => {
    showSingles = !!showSinglesInput.checked;
    saveSettings();
  });

  batchSizeInput.addEventListener('change', () => {
    batchSize = clamp(batchSizeInput.value, 1, 200, DEFAULT_BATCH_SIZE);
    batchSizeInput.value = String(batchSize);
    updateSettingsDisplay();
    saveSettings();
  });

  batchDelayInput.addEventListener('change', () => {
    batchDelaySeconds = clamp(batchDelayInput.value, 0, 300, DEFAULT_BATCH_DELAY_SECONDS);
    batchDelayInput.value = String(batchDelaySeconds);
    updateSettingsDisplay();
    saveSettings();
  });

  maxRetriesInput.addEventListener('change', () => {
    maxRetriesPerItem = clamp(maxRetriesInput.value, 0, 5, DEFAULT_MAX_RETRIES_PER_ITEM);
    maxRetriesInput.value = String(maxRetriesPerItem);
    saveSettings();
  });

  cooldownInput.addEventListener('change', () => {
    cooldownFallbackMin = clamp(cooldownInput.value, 1, 120, DEFAULT_COOLDOWN_FALLBACK_MIN);
    cooldownInput.value = String(cooldownFallbackMin);
    saveSettings();
  });

  iconSizeInput.addEventListener('change', () => {
    iconSize = clamp(iconSizeInput.value, 24, 120, DEFAULT_ICON_SIZE);
    iconSizeInput.value = String(iconSize);
    updateSettingsDisplay();
    saveSettings();
  });

  soundAlertInput.addEventListener('change', () => {
    soundAlertEnabled = !!soundAlertInput.checked;
    saveSettings();
  });

  fancyStyleInput.addEventListener('change', () => {
    fancyAvailableStyleEnabled = !!fancyStyleInput.checked;
    rerenderAvailableList();
    saveSettings();
  });

  btnSettings.addEventListener('click', () => {
    settingsVisible = !settingsVisible;
    updateSettingsVisibility();
  });

  resetSettingsBtn.addEventListener('click', () => {
    if (confirm('Reset all SSW batch search settings to their defaults?')) {
      resetSettingsToDefaults();
    }
  });

  syncSettingsToInputs();

  /*** COPY TO CLIPBOARD HELPERS ***/
  function flashCopyIcon(icon) {
    if (!icon) return;
    const original = icon.textContent || '??';
    icon.textContent = '?';
    icon.style.opacity = '0.9';

    setTimeout(() => {
      icon.textContent = original;
      icon.style.opacity = '';
    }, 900);
  }

  function fallbackCopyText(text, icon) {
    const ta = document.createElement('textarea');
    ta.value = text;
    ta.style.position = 'fixed';
    ta.style.left = '-9999px';
    document.body.appendChild(ta);
    ta.focus();
    ta.select();
    try {
      document.execCommand('copy');
    } catch (e) {
      // ignore
    } finally {
      document.body.removeChild(ta);
      flashCopyIcon(icon);
    }
  }

  function copyToClipboard(text, icon) {
    if (!text) return;
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(text)
        .then(() => {
          flashCopyIcon(icon);
        })
        .catch(() => {
          fallbackCopyText(text, icon);
        });
    } else {
      fallbackCopyText(text, icon);
    }
  }

  /*** AVAILABLE LIST RENDER ***/
  const buildLinksHTML = (item) => {
    const jn = `https://items.jellyneo.net/search/?name=${encodeURIComponent(item)}&name_type=3`;
    const wiz = `https://www.neopets.com/shops/wizard.phtml?string=${encodeURIComponent(item)}`;
    const tp = `https://www.neopets.com/island/tradingpost.phtml?type=browse&criteria=item_exact&sort_by=newest&search_string=${encodeURIComponent(item)}`;
    return `
      <span class="linkicons">
        <a target="_blank" href="${jn}" title="Jellyneo"><img src="https://i.imgur.com/Ol4rYY1.png"></a>
        <a target="_blank" href="${wiz}" title="Shop Wizard"><img src="https://images.neopets.com/themes/h5/basic/images/shopwizard-icon.png"></a>
        <a target="_blank" href="${tp}" title="Trading Post"><img src="https://images.neopets.com/themes/h5/basic/images/tradingpost-icon.png"></a>
      </span>
    `;
  };

  const renderAvailableLI = (name) => {
    const meta = availableMeta.get(name) || {};
    const qty = meta.qty || 1;
    const li = document.createElement('li');
    li.dataset.item = name;

    const qtyBadge = qty > 1 ? `<span class="qty">&times;${qty}</span>` : '';
    const copyIcon = `<span class="copy-icon" title="Copy item name">copy</span>`;
    const nameHTML = fancyAvailableStyleEnabled
      ? `<span class="available-highlight"><b>${name}</b></span>`
      : `<b>${name}</b>`;

    li.innerHTML = `
      ${nameHTML}${qtyBadge}${copyIcon}
      ${meta.linksHTML || ''}
      ${meta.prices ? `: ${meta.prices.map(p => `${addCommas(p)} NP`).join(', ')}` : ''}
    `;
    return li;
  };

  function rerenderAvailableList() {
    const frag = document.createDocumentFragment();
    for (const name of availableMeta.keys()) {
      frag.appendChild(renderAvailableLI(name));
    }
    availableList.replaceChildren(frag);
  }

  const addAvailable = (name, prices, linksHTML, qtyFromSSW = 1) => {
    const current = availableCounts.get(name) || 0;
    availableCounts.set(name, current + 1);

    const prev = availableMeta.get(name) || {};
    const merged = {
      prices,
      linksHTML,
      qty: Math.max(qtyFromSSW, prev.qty || 1)
    };
    availableMeta.set(name, merged);

    const existing = availableList.querySelector(`li[data-item="${CSS.escape(name)}"]`);
    const node = renderAvailableLI(name);
    if (existing) {
      existing.replaceWith(node);
    } else {
      availableList.appendChild(node);
      playPew();
    }
    updateStats();
  };

  /*** FAILED LIST RENDER ***/
  const refreshFailedList = () => {
    const counts = groupCounts(failedItemsRaw);
    const frag = document.createDocumentFragment();
    counts.forEach((qty, name) => {
      const li = document.createElement('li');
      li.innerHTML = qty > 1 ? `<b>${name}</b> <span class="qty">&times;${qty}</span>` : `<b>${name}</b>`;
      frag.appendChild(li);
    });
    failedList.replaceChildren(frag);
    updateStats();
  };

  /*** SSW BAN / COOLDOWN ***/
  const parseCooldownMinutes = (html) => {
    const m = html.match(/<b>(\d+)<\/b>/);
    return m ? parseInt(m[1], 10) : cooldownFallbackMin;
  };

  const handleBan = (itemName, banHTML) => {
    const mins = parseCooldownMinutes(banHTML);
    if (!isPaused) {
      setPaused(true);
      log(`SSW ban during <b>${itemName}</b>. Cooldown <b>${mins}</b> minute(s).`, 'warning');
      showBanNotice(mins);
      if (banCooldownTimeout) clearTimeout(banCooldownTimeout);
      banCooldownTimeout = setTimeout(() => {
        setPaused(false);
        banCooldownTimeout = null;
        hideBanNotice();
        log('Cooldown finished. Resuming.', 'positive');
      }, mins * 60 * 1000);
    }
  };

  /*** FILTER ***/
  const qualifiesByDiscount = (pricesArr) => {
    if (pricesArr.length < 2) return false;
    const sorted = pricesArr.slice(0).sort((a, b) => a - b);
    return sorted[0] <= Math.floor(sorted[1] * discountThreshold);
  };

  /*** CORE SEARCH ***/
  const searchItem = async (itemName) => {
    let retries = 0;

    while (true) {
      await waitUntilNotPaused(itemName);

      const result = await new Promise((resolve) => {
        GM_xmlhttpRequest({
          method: 'GET',
          url: `https://www.neopets.com/shops/ssw/ssw_query.php?q=${encodeURIComponent(itemName)}&priceOnly=1&context=0&partial=0&min_price=0&max_price=999999&json=1`,
          onload: (resp) => {
            try {
              const json = JSON.parse(resp.responseText);
              const banHTML = json.html || '';
              if (banHTML.includes('Whoa there, too many searches!')) {
                handleBan(itemName, banHTML);
                resolve('banned');
                return;
              }

              const data = json.data;
              if (data && Array.isArray(data.prices)) {
                const prices = data.prices
                  .map(p => parseInt(p, 10))
                  .filter(n => Number.isFinite(n) && n >= 0);

                const rowCount = (data && Number.isFinite(data.rowcount)) ? data.rowcount : prices.length;

                const accept = (prices.length === 1 && showSingles)
                             || (prices.length >= 2 && qualifiesByDiscount(prices))
                             || (prices.length <= 4);

                if (accept && prices.length) {
                  const topPrices = prices.slice(0).sort((a, b) => a - b).slice(0, 4);
                  addAvailable(itemName, topPrices, buildLinksHTML(itemName), rowCount);
                } else if (accept && !prices.length && rowCount > 0) {
                  addAvailable(itemName, [], buildLinksHTML(itemName), rowCount);
                }
              }

              resolve('ok');
            } catch (e) {
              log(`Parse error for <b>${itemName}</b>: ${e.message}`, 'error');
              resolve('error');
            }
          },
          onerror: () => {
            log(`Network error for <b>${itemName}</b>.`, 'error');
            resolve('error');
          },
        });
      });

      if (result === 'banned') {
        continue;
      }
      if (result === 'error') {
        if (retries < maxRetriesPerItem) {
          retries++;
          log(`Retrying <b>${itemName}</b> (attempt ${retries + 1}/${maxRetriesPerItem + 1}).`, 'warning');
          await wait(500);
          continue;
        } else {
          failedItemsRaw.push(itemName);
          refreshFailedList();
        }
      }
      break;
    }
  };

  /*** COMMON RESET FOR A RUN ***/
  const resetRunState = (items) => {
    itemsQueued = items.length;
    itemsSearchedCount = 0;
    availableCounts.clear();
    availableMeta.clear();
    failedItemsRaw.length = 0;
    availableList.textContent = '';
    failedList.textContent = '';
    logBox.textContent = '';
    updateStats();
  };

  /*** BATCH RUNNER ***/
  const runBatches = async (items) => {
    resetRunState(items);

    log('<span style="color:#15803d;font-weight:700;">Starting batch search in batch mode.</span>', 'positive');

    const batches = [];
    for (let i = 0; i < items.length; i += batchSize) {
      batches.push(items.slice(i, i + batchSize));
    }

    for (let b = 0; b < batches.length; b++) {
      await waitUntilNotPaused(`batch ${b + 1}/${batches.length}`);
      log(`Searching batch <b>${b + 1}</b> of <b>${batches.length}</b> concurrently.`, 'neutral');

      const batch = batches[b];
      await Promise.all(batch.map(async (name) => {
        itemsSearchedCount++;
        updateStats();
        await searchItem(name);
        updateStats();
      }));

      if (b < batches.length - 1 && batchDelaySeconds > 0) {
        log(`Waiting <b>${batchDelaySeconds}s</b> before starting the next batch.`, 'neutral');
        await waitWithPause(batchDelaySeconds * 1000, 'between batches');
      }
    }

    log('<span style="color:#15803d;font-weight:700;">All batches completed.</span>', 'positive');
  };

  /*** SINGLE-ITEM RUNNER (RANDOM MS DELAY) ***/
  const runSingles = async (items) => {
    resetRunState(items);

    log('<span style="color:#15803d;font-weight:700;">Starting search in single-item mode with random delays.</span>', 'positive');

    for (let i = 0; i < items.length; i++) {
      const name = items[i];
      await waitUntilNotPaused(`item ${i + 1}/${items.length}`);
      itemsSearchedCount++;
      updateStats();
      await searchItem(name);
      updateStats();

      if (i < items.length - 1) {
        const delayMs = getRandomSingleDelayMs();
        if (delayMs > 0) {
          log(`Searched <b>${name}</b> waiting <b>${delayMs}ms</b> before next item.`, 'neutral');
          await waitWithPause(delayMs, 'between items');
        }
      }
    }

    log('<span style="color:#15803d;font-weight:700;">All items completed.</span>', 'positive');
  };

  /*** BUTTONS ***/
  btnStart.addEventListener('click', () => {
    const raw = normalizeLines((itemInput.value || '').trim());
    if (!raw) {
      alert('Please enter at least one item.');
      return;
    }
    const items = raw.split('\n').map(s => s.trim()).filter(Boolean);
    if (!items.length) {
      alert('Please enter at least one item.');
      return;
    }

    if (settingsVisible) {
      settingsVisible = false;
      updateSettingsVisibility();
    }

    if (searchMode === 'single') {
      runSingles(items);
    } else {
      runBatches(items);
    }
  });

  btnPause.addEventListener('click', () => setPaused(true));
  btnResume.addEventListener('click', () => setPaused(false));
  btnClear.addEventListener('click', () => {
    itemInput.value = '';
    availableList.textContent = '';
    failedList.textContent = '';
    logBox.textContent = '';
    availableCounts.clear();
    availableMeta.clear();
    failedItemsRaw.length = 0;
    itemsQueued = 0;
    itemsSearchedCount = 0;
    updateStats();
  });

  /*** COPY ICON CLICK HANDLER ***/
  availableList.addEventListener('click', (e) => {
    const icon = e.target.closest('.copy-icon');
    if (!icon) return;
    const li = icon.closest('li');
    if (!li) return;
    const name = li.dataset.item || '';
    copyToClipboard(name, icon);
  });

  /*** INITIAL STATE ***/
  applyTheme();
  launcher.addEventListener('click', () => { minimized = false; });

})();