[object Object]

← back to Secrets Manager

auto-save: 2026-07-22T13:14:23 (4 files) — dw-scroll-debug.js dw-scroll-debug2.js dw-scroll-debug3.js dw-scroll-verify.js

367cf90c4d074e5e7faf3c137451b8b12b2fd8cf · 2026-07-22 13:14:23 -0700 · Steve Abrams

Files touched

Diff

commit 367cf90c4d074e5e7faf3c137451b8b12b2fd8cf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 22 13:14:23 2026 -0700

    auto-save: 2026-07-22T13:14:23 (4 files) — dw-scroll-debug.js dw-scroll-debug2.js dw-scroll-debug3.js dw-scroll-verify.js
---
 dw-scroll-debug.js  | 480 ++++++++++++++++++++++++++++++++++++++++++++++++
 dw-scroll-debug2.js | 513 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 dw-scroll-debug3.js | 312 ++++++++++++++++++++++++++++++++
 dw-scroll-verify.js | 201 ++++++++++++++++++++
 4 files changed, 1506 insertions(+)

diff --git a/dw-scroll-debug.js b/dw-scroll-debug.js
new file mode 100644
index 0000000..abd2685
--- /dev/null
+++ b/dw-scroll-debug.js
@@ -0,0 +1,480 @@
+/**
+ * dw-scroll-debug.js
+ * Headless Playwright investigation of the infinite-scroll glitch on
+ * https://www.designerwallcoverings.com/collections/decoglass-wallpaper?page=6
+ *
+ * Instruments:
+ *  - dw-card-hover.js run() fire count (via page.evaluate injection)
+ *  - .dw-hover-overlay duplicate detection per card
+ *  - MutationObserver callback count
+ *  - Performance long-tasks (PerformanceObserver)
+ *  - Boost network requests (XHR/fetch) for infinite-scroll API calls
+ *  - Card count growth across scroll steps
+ *  - Console errors
+ */
+
+const { chromium } = require('playwright');
+const path = require('path');
+
+const TARGET_URL = 'https://www.designerwallcoverings.com/collections/decoglass-wallpaper?page=6';
+const SCROLL_STEPS = 12;
+const SCROLL_PAUSE_MS = 2500;  // wait between scrolls for Boost to load
+const VIEWPORT = { width: 1440, height: 900 };
+
+(async () => {
+  const browser = await chromium.launch({
+    headless: true,
+    args: [
+      '--no-sandbox',
+      '--disable-dev-shm-usage',
+      '--disable-blink-features=AutomationControlled',
+    ],
+  });
+
+  const context = await browser.newContext({
+    viewport: VIEWPORT,
+    userAgent:
+      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
+      '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
+    extraHTTPHeaders: {
+      'Accept-Language': 'en-US,en;q=0.9',
+    },
+  });
+
+  const page = await context.newPage();
+
+  // ── Capture console messages ──────────────────────────────────────────────
+  const consoleErrors = [];
+  const consoleWarnings = [];
+  page.on('console', (msg) => {
+    if (msg.type() === 'error') consoleErrors.push(msg.text());
+    if (msg.type() === 'warning') consoleWarnings.push(msg.text());
+  });
+
+  // ── Capture network activity ──────────────────────────────────────────────
+  const boostRequests = [];
+  page.on('request', (req) => {
+    const url = req.url();
+    if (url.includes('boost') || url.includes('filter') || url.includes('search') || url.includes('product')) {
+      boostRequests.push({ url: url.substring(0, 150), method: req.method(), type: req.resourceType() });
+    }
+  });
+
+  const networkErrors = [];
+  page.on('requestfailed', (req) => {
+    networkErrors.push({ url: req.url().substring(0, 120), err: req.failure()?.errorText });
+  });
+
+  let httpStatus = null;
+  page.on('response', (resp) => {
+    if (resp.url() === TARGET_URL || resp.url().startsWith(TARGET_URL.split('?')[0])) {
+      httpStatus = resp.status();
+    }
+  });
+
+  console.log('=== DW Infinite-Scroll Debug ===');
+  console.log(`Target: ${TARGET_URL}`);
+  console.log(`Viewport: ${VIEWPORT.width}x${VIEWPORT.height}`);
+  console.log(`Scroll steps: ${SCROLL_STEPS}, pause: ${SCROLL_PAUSE_MS}ms each\n`);
+
+  // ── Navigate ──────────────────────────────────────────────────────────────
+  console.log('[1] Navigating to page...');
+  try {
+    const resp = await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
+    httpStatus = resp?.status();
+    console.log(`    HTTP status: ${httpStatus}`);
+    if (httpStatus === 429) {
+      console.log('    [RATE-LIMITED 429] Waiting 15s before retry...');
+      await page.waitForTimeout(15000);
+      await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
+    }
+  } catch (e) {
+    console.log(`    Navigation error: ${e.message}`);
+  }
+
+  // ── Wait for Boost product cards ──────────────────────────────────────────
+  console.log('[2] Waiting for .boost-sd__product-item cards...');
+  let initialCardCount = 0;
+  try {
+    await page.waitForSelector('.boost-sd__product-item', { timeout: 20000 });
+    initialCardCount = await page.locator('.boost-sd__product-item').count();
+    console.log(`    Initial card count: ${initialCardCount}`);
+  } catch (e) {
+    console.log(`    Could not find .boost-sd__product-item: ${e.message}`);
+    // Check for alternate selectors
+    const alt1 = await page.locator('.product-list-item').count();
+    const alt2 = await page.locator('[data-product-id]').count();
+    console.log(`    .product-list-item: ${alt1}, [data-product-id]: ${alt2}`);
+    const html = await page.content();
+    const snippet = html.substring(0, 3000);
+    console.log(`    Page HTML snippet: ${snippet}`);
+  }
+
+  // ── Wait for dw-card-hover.js to initialize ───────────────────────────────
+  console.log('[3] Waiting for dw-card-hover.js to initialize...');
+  await page.waitForTimeout(2000);
+  const dwHoverActive = await page.evaluate(() => !!window.__dwCardHover);
+  console.log(`    window.__dwCardHover present: ${dwHoverActive}`);
+
+  // ── Instrument: patch run() to count calls ────────────────────────────────
+  console.log('[4] Patching run() counter and MutationObserver counter...');
+  const instrumentResult = await page.evaluate(() => {
+    // We cannot directly wrap the private run() function inside the IIFE.
+    // Instead, we instrument the MutationObserver callback indirectly:
+    // 1. Count how many times .dw-hover-overlay nodes are INSERTED (net new)
+    //    which is a proxy for run() doing work on new cards.
+    // 2. Count MutationObserver callbacks firing on document.body.
+
+    window.__dwRunCount = 0;
+    window.__dwOverlayInsertions = 0;
+    window.__mutObsCallbackCount = 0;
+    window.__longTaskCount = 0;
+    window.__longTaskTotalMs = 0;
+
+    // Count overlay insertions via a second MutationObserver
+    const overlayWatcher = new MutationObserver((muts) => {
+      for (const m of muts) {
+        for (const node of m.addedNodes) {
+          if (node.nodeType === 1) {
+            if (node.classList && node.classList.contains('dw-hover-overlay')) {
+              window.__dwOverlayInsertions++;
+            }
+            // Also count overlays inside added subtrees
+            if (node.querySelectorAll) {
+              window.__dwOverlayInsertions += node.querySelectorAll('.dw-hover-overlay').length;
+            }
+          }
+        }
+      }
+    });
+    overlayWatcher.observe(document.body, { childList: true, subtree: true });
+
+    // Measure long tasks
+    try {
+      const po = new PerformanceObserver((list) => {
+        for (const entry of list.getEntries()) {
+          window.__longTaskCount++;
+          window.__longTaskTotalMs += entry.duration;
+        }
+      });
+      po.observe({ entryTypes: ['longtask'] });
+    } catch (e) { /* longtask not supported */ }
+
+    // Count body-level MutationObserver firings indirectly:
+    // Wrap MutationObserver constructor to intercept calls on document.body
+    // This needs to be done BEFORE dw-card-hover.js runs — too late now.
+    // Instead, we count body childList mutations ourselves.
+    const bodyMutWatcher = new MutationObserver((muts) => {
+      window.__mutObsCallbackCount++;
+    });
+    bodyMutWatcher.observe(document.body, { childList: true, subtree: true });
+
+    return { patched: true, overlayWatcher: 'installed', bodyMutWatcher: 'installed' };
+  });
+  console.log(`    Instrumentation: ${JSON.stringify(instrumentResult)}`);
+
+  // ── Baseline overlay counts ────────────────────────────────────────────────
+  console.log('[5] Baseline card and overlay counts...');
+  const baseline = await page.evaluate(() => {
+    const cards = document.querySelectorAll('.boost-sd__product-item, .product-list-item');
+    let withOverlay = 0, withMultipleOverlays = 0;
+    for (const card of cards) {
+      const overlays = card.querySelectorAll('.dw-hover-overlay');
+      // Also check siblings (v5 places overlay as sibling, not inside card)
+      const parent = card.parentElement;
+      const siblingOverlays = parent ? parent.querySelectorAll(':scope > .dw-hover-overlay') : [];
+      const total = overlays.length + siblingOverlays.length;
+      if (total >= 1) withOverlay++;
+      if (total > 1) withMultipleOverlays++;
+    }
+    // Count all .dw-hover-overlay in the document
+    const allOverlays = document.querySelectorAll('.dw-hover-overlay').length;
+    return {
+      cards: cards.length,
+      withOverlay,
+      withMultipleOverlays,
+      allOverlays,
+      overlaysInDom: allOverlays
+    };
+  });
+  console.log(`    Cards: ${baseline.cards}, with overlay: ${baseline.withOverlay}, duplicates (>1): ${baseline.withMultipleOverlays}`);
+  console.log(`    Total .dw-hover-overlay in DOM: ${baseline.allOverlays}`);
+
+  // ── Scroll steps ──────────────────────────────────────────────────────────
+  console.log(`[6] Scrolling ${SCROLL_STEPS} times to trigger infinite-scroll...`);
+  const snapshots = [];
+
+  for (let step = 0; step < SCROLL_STEPS; step++) {
+    // Scroll to bottom
+    await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
+    await page.waitForTimeout(SCROLL_PAUSE_MS);
+
+    // Capture state
+    const snap = await page.evaluate((stepNum) => {
+      const cards = document.querySelectorAll('.boost-sd__product-item, .product-list-item');
+      let withOverlay = 0, withMultipleOverlays = 0, noOverlay = 0;
+      const dupCards = [];
+
+      for (const card of cards) {
+        // v5 places overlay as a sibling AFTER the image column, so check
+        // both inside the card and as an immediate sibling
+        const insideOverlays = card.querySelectorAll('.dw-hover-overlay').length;
+        // Traverse parent to find sibling overlays adjacent to this card
+        let sibCount = 0;
+        let sib = card.nextSibling;
+        while (sib) {
+          if (sib.nodeType === 1 && sib.classList && sib.classList.contains('dw-hover-overlay')) {
+            sibCount++;
+          }
+          sib = sib.nodeType === 1 ? null : sib.nextSibling; // stop at next element node
+          break; // only check first next sibling
+        }
+        // Also check inside card's image wrapper area
+        const total = insideOverlays + sibCount;
+        if (total >= 1) withOverlay++;
+        else noOverlay++;
+        if (total > 1 || insideOverlays > 1) {
+          withMultipleOverlays++;
+          dupCards.push(card.getAttribute('data-product-id') || card.className.substring(0, 40));
+        }
+      }
+
+      const allOverlays = document.querySelectorAll('.dw-hover-overlay').length;
+
+      return {
+        step: stepNum,
+        cardCount: cards.length,
+        withOverlay,
+        noOverlay,
+        withMultipleOverlays,
+        dupCards: dupCards.slice(0, 5),
+        allOverlays,
+        scrollY: window.scrollY,
+        docHeight: document.body.scrollHeight,
+        mutObsCallbacks: window.__mutObsCallbackCount,
+        overlayInsertions: window.__dwOverlayInsertions,
+        longTaskCount: window.__longTaskCount,
+        longTaskTotalMs: Math.round(window.__longTaskTotalMs),
+      };
+    }, step + 1);
+
+    console.log(
+      `    Step ${step + 1}: cards=${snap.cardCount} overlays=${snap.allOverlays} ` +
+      `dups=${snap.withMultipleOverlays} noOverlay=${snap.noOverlay} ` +
+      `mutCBs=${snap.mutObsCallbacks} overlayInserts=${snap.overlayInsertions} ` +
+      `longTasks=${snap.longTaskCount}(${snap.longTaskTotalMs}ms) ` +
+      `scrollY=${snap.scrollY}/${snap.docHeight}`
+    );
+    if (snap.dupCards.length > 0) {
+      console.log(`      DUPLICATE OVERLAY CARDS: ${snap.dupCards.join(', ')}`);
+    }
+    snapshots.push(snap);
+
+    // Check if we stopped getting new cards (stall detection)
+    if (step > 2 && snap.cardCount === snapshots[step - 1].cardCount && snap.scrollY > 0) {
+      console.log(`    [STALL DETECTED] Card count unchanged at ${snap.cardCount} for 2 steps`);
+    }
+  }
+
+  // ── Final analysis ────────────────────────────────────────────────────────
+  console.log('\n[7] Final DOM analysis...');
+
+  const finalAnalysis = await page.evaluate(() => {
+    const cards = document.querySelectorAll('.boost-sd__product-item, .product-list-item');
+    const allOverlays = document.querySelectorAll('.dw-hover-overlay');
+
+    // Check for overlays outside of card contexts (orphaned)
+    let orphanedOverlays = 0;
+    for (const ov of allOverlays) {
+      const inCard = ov.closest('.boost-sd__product-item, .product-list-item');
+      // v5: overlay is sibling, walk up to see if it's near a card
+      if (!inCard) {
+        // Check previous sibling chain for a card
+        let prev = ov.previousElementSibling;
+        let foundCard = false;
+        while (prev && !foundCard) {
+          if (prev.matches('.boost-sd__product-item, .product-list-item')) foundCard = true;
+          // Check inside prev for an image layout wrapper
+          if (prev.matches('.boost-sd__product-item-grid-view-layout-image')) foundCard = true;
+          prev = prev.previousElementSibling;
+        }
+        if (!foundCard) orphanedOverlays++;
+      }
+    }
+
+    // Count per-card overlays using the v5 sibling placement logic
+    const cardOverlayCounts = {};
+    for (const card of cards) {
+      const pid = card.getAttribute('data-product-id') ||
+                  card.getAttribute('data-id') ||
+                  Array.from(card.classList).find(c => c.includes('boost')) || 'unknown';
+
+      // Inside the card
+      let count = card.querySelectorAll('.dw-hover-overlay').length;
+
+      // v5 places overlay after the image-col sibling; the overlay is a child
+      // of the card's parent (the grid item row/wrapper), not inside the card itself.
+      // Let's check by scanning ALL overlays and seeing how many follow THIS card
+      // in the DOM parent.
+      const parent = card.parentElement;
+      if (parent) {
+        const children = Array.from(parent.children);
+        const cardIdx = children.indexOf(card);
+        // Count consecutive .dw-hover-overlay siblings right after card
+        for (let i = cardIdx + 1; i < children.length; i++) {
+          if (children[i].classList.contains('dw-hover-overlay')) count++;
+          else break;
+        }
+      }
+
+      cardOverlayCounts[pid] = count;
+    }
+
+    const duplicatedCards = Object.entries(cardOverlayCounts).filter(([, c]) => c > 1);
+    const zeroOverlayCards = Object.entries(cardOverlayCounts).filter(([, c]) => c === 0);
+
+    // Check if Boost infinite scroll sentinel/loader exists
+    const loaders = document.querySelectorAll(
+      '[class*="boost-sd__load-more"], [class*="boost-sd__infinite"], [class*="load-more"], .boost-sd__button-load-more'
+    );
+
+    // Check for any CSS that might block scroll
+    const bodyOverflow = window.getComputedStyle(document.body).overflow;
+    const htmlOverflow = window.getComputedStyle(document.documentElement).overflow;
+
+    return {
+      finalCardCount: cards.length,
+      finalOverlayCount: allOverlays.length,
+      orphanedOverlays,
+      duplicatedCards: duplicatedCards.slice(0, 10).map(([id, c]) => `${id}:${c}`),
+      duplicatedCardCount: duplicatedCards.length,
+      zeroOverlayCardCount: zeroOverlayCards.length,
+      boostLoaderElements: loaders.length,
+      boostLoaderClasses: Array.from(loaders).map(el => el.className).slice(0, 5),
+      bodyOverflow,
+      htmlOverflow,
+      mutObsCallbacks: window.__mutObsCallbackCount,
+      overlayInsertions: window.__dwOverlayInsertions,
+      longTaskCount: window.__longTaskCount,
+      longTaskTotalMs: Math.round(window.__longTaskTotalMs),
+      dwHoverActive: window.__dwCardHover,
+    };
+  });
+
+  console.log('\n=== FINAL ANALYSIS ===');
+  console.log(`Cards in DOM: ${finalAnalysis.finalCardCount}`);
+  console.log(`Total .dw-hover-overlay in DOM: ${finalAnalysis.finalOverlayCount}`);
+  console.log(`Orphaned overlays (not near any card): ${finalAnalysis.orphanedOverlays}`);
+  console.log(`Cards with DUPLICATE overlays: ${finalAnalysis.duplicatedCardCount}`);
+  if (finalAnalysis.duplicatedCards.length > 0) {
+    console.log(`  Duplicate samples: ${finalAnalysis.duplicatedCards.join(', ')}`);
+  }
+  console.log(`Cards with ZERO overlays: ${finalAnalysis.zeroOverlayCardCount}`);
+  console.log(`Boost loader/sentinel elements: ${finalAnalysis.boostLoaderElements}`);
+  if (finalAnalysis.boostLoaderClasses.length > 0) {
+    console.log(`  Loader classes: ${finalAnalysis.boostLoaderClasses.join(', ')}`);
+  }
+  console.log(`Body overflow: ${finalAnalysis.bodyOverflow}, HTML overflow: ${finalAnalysis.htmlOverflow}`);
+  console.log(`\nMutationObserver body callbacks: ${finalAnalysis.mutObsCallbacks}`);
+  console.log(`run() proxy (overlay insertions): ${finalAnalysis.overlayInsertions}`);
+  console.log(`Long tasks: ${finalAnalysis.longTaskCount} totaling ${finalAnalysis.longTaskTotalMs}ms`);
+  console.log(`window.__dwCardHover: ${finalAnalysis.dwHoverActive}`);
+
+  // ── Scroll growth analysis ────────────────────────────────────────────────
+  console.log('\n=== SCROLL GROWTH ANALYSIS ===');
+  let prevCards = baseline.cards;
+  let stalls = 0;
+  for (const snap of snapshots) {
+    const delta = snap.cardCount - prevCards;
+    const status = delta > 0 ? `+${delta} new cards` : (delta === 0 ? 'STALL - no new cards' : `SHRINK by ${Math.abs(delta)}`);
+    const ratio = snap.allOverlays > 0 ? (snap.allOverlays / snap.cardCount).toFixed(2) : '0.00';
+    console.log(`  Step ${snap.step}: ${snap.cardCount} cards (${status}), ${snap.allOverlays} overlays, ratio=${ratio}, longTasks=${snap.longTaskCount}(${snap.longTaskTotalMs}ms)`);
+    if (delta === 0) stalls++;
+    prevCards = snap.cardCount;
+  }
+
+  // ── MutationObserver thrash analysis ─────────────────────────────────────
+  console.log('\n=== MUTATION OBSERVER ANALYSIS ===');
+  const finalSnap = snapshots[snapshots.length - 1];
+  console.log(`Total body MutationObserver callbacks fired: ${finalSnap.mutObsCallbacks}`);
+  console.log(`Total overlay insertions by run(): ${finalSnap.overlayInsertions}`);
+  if (finalSnap.mutObsCallbacks > 0 && finalAnalysis.finalCardCount > 0) {
+    const callsPerCard = (finalSnap.mutObsCallbacks / finalAnalysis.finalCardCount).toFixed(1);
+    console.log(`Average MutObs callbacks per card: ${callsPerCard}`);
+    // Each run() call processes ALL cards and inserts overlays for new ones.
+    // If run() fires N times and inserts M overlays, N > M suggests many no-op re-scans.
+    const efficiency = finalSnap.overlayInsertions > 0
+      ? (finalSnap.mutObsCallbacks / finalSnap.overlayInsertions).toFixed(1)
+      : 'N/A';
+    console.log(`Callback:overlay-insertion ratio: ${efficiency}:1 (higher = more wasted re-scans)`);
+  }
+
+  // ── SELF-REINFORCING LOOP DETECTION ──────────────────────────────────────
+  console.log('\n=== SELF-REINFORCING LOOP DETECTION ===');
+  console.log('Key question: Does run() inserting .dw-hover-overlay nodes trigger MORE observer callbacks?');
+  console.log('(If yes, each overlay insertion fires the observer, which calls run() again,');
+  console.log(' which already skips labeled cards — but still does a full querySelectorAll scan)');
+  console.log(`Cards at baseline: ${baseline.cards}`);
+  console.log(`Overlays inserted by run(): ${finalSnap.overlayInsertions}`);
+  console.log(`Observer callbacks fired: ${finalSnap.mutObsCallbacks}`);
+  if (finalSnap.overlayInsertions > 0 && finalSnap.mutObsCallbacks > finalSnap.overlayInsertions * 1.5) {
+    console.log('FINDING: Observer callbacks >> overlay insertions — observer is firing for NON-run()-insertions too');
+    console.log('  This means Boost DOM mutations are also triggering the observer, causing repeated full-grid scans');
+  }
+
+  // ── Console errors ────────────────────────────────────────────────────────
+  if (consoleErrors.length > 0) {
+    console.log(`\n=== CONSOLE ERRORS (${consoleErrors.length}) ===`);
+    consoleErrors.slice(0, 10).forEach((e, i) => console.log(`  [${i + 1}] ${e}`));
+  }
+  if (consoleWarnings.length > 0) {
+    console.log(`\n=== CONSOLE WARNINGS (${consoleWarnings.length}) ===`);
+    consoleWarnings.slice(0, 5).forEach((e, i) => console.log(`  [${i + 1}] ${e}`));
+  }
+  if (networkErrors.length > 0) {
+    console.log(`\n=== NETWORK ERRORS (${networkErrors.length}) ===`);
+    networkErrors.slice(0, 5).forEach((e, i) => console.log(`  [${i + 1}] ${e.url} — ${e.err}`));
+  }
+
+  // ── Boost API requests ────────────────────────────────────────────────────
+  const boostApiReqs = boostRequests.filter(r => r.url.includes('boost') && r.type === 'fetch' || r.url.includes('search') || r.url.includes('filter'));
+  if (boostApiReqs.length > 0) {
+    console.log(`\n=== BOOST API REQUESTS (${boostApiReqs.length}) ===`);
+    boostApiReqs.slice(0, 10).forEach((r, i) => console.log(`  [${i + 1}] ${r.method} ${r.url}`));
+  }
+
+  // ── Final verdict ─────────────────────────────────────────────────────────
+  console.log('\n=== ROOT CAUSE VERDICT ===');
+  const overlapRatio = finalAnalysis.finalOverlayCount / Math.max(finalAnalysis.finalCardCount, 1);
+  const hasOverlayDuplicates = finalAnalysis.duplicatedCardCount > 0;
+  const hasMutThrash = finalSnap.mutObsCallbacks > 200;
+  const hasLongTasks = finalAnalysis.longTaskCount > 5;
+  const cardGrowthStopped = stalls >= 3;
+
+  if (hasOverlayDuplicates) {
+    console.log(`CONFIRMED: ${finalAnalysis.duplicatedCardCount} cards have DUPLICATE .dw-hover-overlay elements`);
+    console.log('  Root cause: run() is inserting overlays faster than its duplicate-check catches');
+    console.log('  OR: labelCard() sibling-insertion logic places overlay outside the card,');
+    console.log('  so card.querySelector(".dw-hover-overlay") returns null on next run() call');
+    console.log('  (the guard `if (card.querySelector(".dw-hover-overlay")) return;` misses sibling overlays)');
+  }
+  if (hasMutThrash) {
+    console.log(`CONFIRMED: MutationObserver fired ${finalSnap.mutObsCallbacks} times during scrolling`);
+    console.log('  Each fire schedules a full document.querySelectorAll() scan with 80ms debounce.');
+    console.log('  With Boost appending dozens of nodes per infinite-scroll batch, this fires continuously.');
+  }
+  if (hasLongTasks) {
+    console.log(`CONFIRMED: ${finalAnalysis.longTaskCount} long tasks (>${finalAnalysis.longTaskTotalMs}ms total)`);
+    console.log('  Long tasks block scroll-event processing, causing visible jank.');
+  }
+  if (cardGrowthStopped) {
+    console.log(`CONFIRMED: Infinite scroll STALLED — card count stopped growing for ${stalls} consecutive steps`);
+  }
+  if (!hasOverlayDuplicates && !hasMutThrash && !cardGrowthStopped) {
+    console.log('No dramatic failures found — glitch may be intermittent or require more scroll steps');
+    console.log(`Overlay/card ratio: ${overlapRatio.toFixed(2)} (should be ~1.0)`);
+  }
+
+  await browser.close();
+  console.log('\nDone.');
+})();
diff --git a/dw-scroll-debug2.js b/dw-scroll-debug2.js
new file mode 100644
index 0000000..909a957
--- /dev/null
+++ b/dw-scroll-debug2.js
@@ -0,0 +1,513 @@
+/**
+ * dw-scroll-debug2.js — Deep dive:
+ * 1. Why does scrollY freeze at 1564/2464 — is body overflow:hidden blocking scroll?
+ * 2. What is the Boost pagination mode (infinite vs load-more button)?
+ * 3. Does clicking the load-more button work?
+ * 4. What is Boost loading — does it load more products, or is page=6 end-of-collection?
+ * 5. Detailed self-reinforcing loop test: inject a fake card, watch observer thrash.
+ */
+
+const { chromium } = require('playwright');
+
+const TARGET_URL = 'https://www.designerwallcoverings.com/collections/decoglass-wallpaper?page=6';
+const VIEWPORT = { width: 1440, height: 900 };
+
+(async () => {
+  const browser = await chromium.launch({
+    headless: true,
+    args: ['--no-sandbox', '--disable-dev-shm-usage'],
+  });
+  const context = await browser.newContext({
+    viewport: VIEWPORT,
+    userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
+  });
+  const page = await context.newPage();
+
+  const consoleErrors = [];
+  page.on('console', msg => { if (msg.type() === 'error') consoleErrors.push(msg.text()); });
+
+  // Capture all Boost API filter/search responses
+  const boostApiResponses = [];
+  page.on('response', async (resp) => {
+    const url = resp.url();
+    if (url.includes('bc-sf-filter') || url.includes('boostcommerce') && url.includes('filter')) {
+      try {
+        const status = resp.status();
+        let body = '';
+        try { body = await resp.text(); } catch(e) { body = '[could not read]'; }
+        boostApiResponses.push({ url: url.substring(0, 200), status, bodySnippet: body.substring(0, 500) });
+      } catch(e) {}
+    }
+  });
+
+  console.log('=== DW Scroll Debug — Round 2 ===');
+  console.log(`URL: ${TARGET_URL}`);
+
+  await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
+  await page.waitForTimeout(3000); // let Boost fully render
+
+  // ── 1. Investigate scroll container ──────────────────────────────────────
+  console.log('\n[1] Scroll container investigation...');
+  const scrollInfo = await page.evaluate(() => {
+    // Find what element is actually scrollable
+    function isScrollable(el) {
+      const style = window.getComputedStyle(el);
+      const overflow = style.overflow + style.overflowY + style.overflowX;
+      return /auto|scroll/.test(overflow) && (el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth);
+    }
+
+    const scrollableEls = [];
+    function walk(el, depth) {
+      if (depth > 8) return;
+      if (isScrollable(el)) {
+        scrollableEls.push({
+          tag: el.tagName,
+          id: el.id,
+          className: (el.className || '').substring(0, 60),
+          scrollHeight: el.scrollHeight,
+          clientHeight: el.clientHeight,
+          scrollTop: el.scrollTop,
+          overflow: window.getComputedStyle(el).overflow,
+          overflowY: window.getComputedStyle(el).overflowY,
+        });
+      }
+      for (const child of el.children) walk(child, depth + 1);
+    }
+    walk(document.documentElement, 0);
+
+    return {
+      bodyScrollHeight: document.body.scrollHeight,
+      bodyClientHeight: document.body.clientHeight,
+      bodyOverflow: window.getComputedStyle(document.body).overflow,
+      bodyOverflowY: window.getComputedStyle(document.body).overflowY,
+      htmlOverflow: window.getComputedStyle(document.documentElement).overflow,
+      htmlOverflowY: window.getComputedStyle(document.documentElement).overflowY,
+      windowScrollY: window.scrollY,
+      windowInnerHeight: window.innerHeight,
+      documentScrollHeight: document.documentElement.scrollHeight,
+      scrollableEls: scrollableEls.slice(0, 10),
+    };
+  });
+
+  console.log(`  body: scrollHeight=${scrollInfo.bodyScrollHeight}, clientHeight=${scrollInfo.bodyClientHeight}`);
+  console.log(`  body overflow: "${scrollInfo.bodyOverflow}", overflowY: "${scrollInfo.bodyOverflowY}"`);
+  console.log(`  html overflow: "${scrollInfo.htmlOverflow}", overflowY: "${scrollInfo.htmlOverflowY}"`);
+  console.log(`  document.documentElement.scrollHeight: ${scrollInfo.documentScrollHeight}`);
+  console.log(`  window.innerHeight: ${scrollInfo.windowInnerHeight}, scrollY: ${scrollInfo.windowScrollY}`);
+  if (scrollInfo.scrollableEls.length > 0) {
+    console.log('  Scrollable elements found:');
+    scrollInfo.scrollableEls.forEach(el => {
+      console.log(`    <${el.tag}> id="${el.id}" class="${el.className}"`);
+      console.log(`      scrollHeight=${el.scrollHeight} clientHeight=${el.clientHeight} scrollTop=${el.scrollTop}`);
+      console.log(`      overflow: "${el.overflow}" overflowY: "${el.overflowY}"`);
+    });
+  }
+
+  // ── 2. Boost pagination mode detection ───────────────────────────────────
+  console.log('\n[2] Boost pagination mode...');
+  const paginationInfo = await page.evaluate(() => {
+    // Look for load-more button
+    const loadMoreBtn = document.querySelector(
+      '.boost-sd__pagination-button--load-more, [class*="load-more"], [data-type="load-more"]'
+    );
+    // Look for infinite scroll sentinel
+    const infiniteSentinel = document.querySelector(
+      '[class*="infinite"], [class*="sentinel"], [class*="waypoint"]'
+    );
+    // Look for numbered pagination
+    const pageNumbers = document.querySelectorAll('.boost-sd__pagination-item, .boost-sd__pagination-number');
+    // Check Boost config
+    const boostConfig = window.boost_pf_config || window.boostConfig || window.boostSdConfig;
+
+    // Get the load-more button HTML
+    const loadMoreHTML = loadMoreBtn ? loadMoreBtn.outerHTML.substring(0, 300) : null;
+    const loadMoreVisible = loadMoreBtn ? window.getComputedStyle(loadMoreBtn).display !== 'none' : false;
+    const loadMoreRect = loadMoreBtn ? loadMoreBtn.getBoundingClientRect() : null;
+
+    return {
+      hasLoadMoreBtn: !!loadMoreBtn,
+      loadMoreBtnText: loadMoreBtn ? (loadMoreBtn.textContent || '').trim() : null,
+      loadMoreHTML,
+      loadMoreVisible,
+      loadMoreRect: loadMoreRect ? { top: loadMoreRect.top, bottom: loadMoreRect.bottom, height: loadMoreRect.height } : null,
+      hasInfiniteSentinel: !!infiniteSentinel,
+      pageNumberCount: pageNumbers.length,
+      boostConfigKeys: boostConfig ? Object.keys(boostConfig).slice(0, 20) : null,
+      // Try to read Boost's current pagination type from any global
+      boostPaginationType: (window.BOOST_SD || {}).paginationType || 'unknown',
+    };
+  });
+
+  console.log(`  Has load-more button: ${paginationInfo.hasLoadMoreBtn}`);
+  if (paginationInfo.hasLoadMoreBtn) {
+    console.log(`  Button text: "${paginationInfo.loadMoreBtnText}"`);
+    console.log(`  Button visible: ${paginationInfo.loadMoreVisible}`);
+    console.log(`  Button position: ${JSON.stringify(paginationInfo.loadMoreRect)}`);
+    console.log(`  Button HTML: ${paginationInfo.loadMoreHTML}`);
+  }
+  console.log(`  Has infinite-scroll sentinel: ${paginationInfo.hasInfiniteSentinel}`);
+  console.log(`  Page number elements: ${paginationInfo.pageNumberCount}`);
+  console.log(`  Boost pagination type: ${paginationInfo.boostPaginationType}`);
+  console.log(`  Boost config keys: ${JSON.stringify(paginationInfo.boostConfigKeys)}`);
+
+  // ── 3. Try scrolling within the actual scrollable container ──────────────
+  console.log('\n[3] Attempting scroll on the correct scrollable container...');
+  // The body has "overflow: hidden auto" which means overflow-y: auto on body
+  // but maybe the main element or a wrapper is the scroll container
+  // Try scrolling the <main> element or the html element
+
+  const scrollAttempts = await page.evaluate(async () => {
+    const results = [];
+
+    // Try window.scrollTo first
+    const before = { scrollY: window.scrollY, docH: document.documentElement.scrollHeight };
+
+    window.scrollTo(0, 99999);
+    await new Promise(r => setTimeout(r, 100));
+    results.push({ method: 'window.scrollTo(0,99999)', scrollY: window.scrollY, docH: document.documentElement.scrollHeight });
+
+    // Try scrolling body
+    document.body.scrollTop = 99999;
+    await new Promise(r => setTimeout(r, 100));
+    results.push({ method: 'document.body.scrollTop=99999', bodyScrollTop: document.body.scrollTop });
+
+    // Try scrolling html
+    document.documentElement.scrollTop = 99999;
+    await new Promise(r => setTimeout(r, 100));
+    results.push({ method: 'document.documentElement.scrollTop=99999', scrollTop: document.documentElement.scrollTop });
+
+    // Find and scroll the actual overflowing container
+    function findScrollContainer() {
+      const candidates = document.querySelectorAll('main, #main, .main, [role="main"], #shopify-section-main, .boost-sd, .boost-sd__container');
+      for (const el of candidates) {
+        const style = window.getComputedStyle(el);
+        if (/auto|scroll/.test(style.overflowY) || /auto|scroll/.test(style.overflow)) {
+          return el;
+        }
+      }
+      return null;
+    }
+    const scrollEl = findScrollContainer();
+    if (scrollEl) {
+      scrollEl.scrollTop = 99999;
+      await new Promise(r => setTimeout(r, 100));
+      results.push({ method: `${scrollEl.tagName}#${scrollEl.id || ''}.${(scrollEl.className||'').substring(0,30)}.scrollTop=99999`, scrollTop: scrollEl.scrollTop, scrollHeight: scrollEl.scrollHeight });
+    }
+
+    return { before, results };
+  });
+
+  console.log(`  Before: scrollY=${scrollAttempts.before.scrollY}, docH=${scrollAttempts.before.docH}`);
+  scrollAttempts.results.forEach(r => console.log(`  ${r.method}: scrollY=${r.scrollY || r.bodyScrollTop || r.scrollTop || '?'}`));
+
+  // ── 4. Use page.evaluate to fire native scroll events ────────────────────
+  console.log('\n[4] Firing native scroll events + checking Boost load...');
+  const cardsBefore = await page.locator('.boost-sd__product-item').count();
+
+  // Try using Playwright's keyboard PageDown
+  await page.keyboard.press('End');
+  await page.waitForTimeout(2000);
+
+  const afterEnd = await page.evaluate(() => ({
+    scrollY: window.scrollY,
+    cardCount: document.querySelectorAll('.boost-sd__product-item').count || document.querySelectorAll('.boost-sd__product-item').length,
+    bodyScrollTop: document.body.scrollTop,
+    htmlScrollTop: document.documentElement.scrollTop,
+  }));
+  console.log(`  After End key: scrollY=${afterEnd.scrollY}, bodyScrollTop=${afterEnd.bodyScrollTop}, htmlScrollTop=${afterEnd.htmlScrollTop}`);
+  const cardsAfterEnd = await page.locator('.boost-sd__product-item').count();
+  console.log(`  Cards: ${cardsBefore} → ${cardsAfterEnd}`);
+
+  // Try clicking the load-more button directly
+  console.log('\n[5] Clicking load-more button directly...');
+  const loadMoreSel = '.boost-sd__pagination-button--load-more, [class*="load-more"]';
+  const loadMoreExists = await page.locator(loadMoreSel).count();
+  if (loadMoreExists > 0) {
+    console.log(`  Found ${loadMoreExists} load-more button(s), clicking...`);
+    try {
+      await page.locator(loadMoreSel).first().scrollIntoViewIfNeeded();
+      await page.waitForTimeout(500);
+      await page.locator(loadMoreSel).first().click();
+      await page.waitForTimeout(3000);
+      const cardsAfterClick = await page.locator('.boost-sd__product-item').count();
+      console.log(`  Cards after click: ${cardsBefore} → ${cardsAfterClick}`);
+
+      // Now count run() effect: new overlays
+      const overlayCount = await page.locator('.dw-hover-overlay').count();
+      console.log(`  Overlays after click: ${overlayCount}`);
+
+      // Check for duplicates on new cards
+      const dupCheck = await page.evaluate(() => {
+        const cards = document.querySelectorAll('.boost-sd__product-item');
+        let dups = 0;
+        for (const card of cards) {
+          const insideOvs = card.querySelectorAll('.dw-hover-overlay').length;
+          // Check sibling overlays (v5 placement)
+          let sibOvs = 0;
+          const parent = card.parentElement;
+          if (parent) {
+            const children = Array.from(parent.children);
+            const idx = children.indexOf(card);
+            for (let i = idx + 1; i < children.length; i++) {
+              if (children[i].classList && children[i].classList.contains('dw-hover-overlay')) sibOvs++;
+              else break;
+            }
+          }
+          if (insideOvs + sibOvs > 1) dups++;
+        }
+        return { dups, totalCards: cards.length, totalOverlays: document.querySelectorAll('.dw-hover-overlay').length };
+      });
+      console.log(`  After load-more click: ${dupCheck.totalCards} cards, ${dupCheck.totalOverlays} overlays, ${dupCheck.dups} with duplicates`);
+
+      // ── Now watch observer thrash during Boost load ──────────────────────
+      console.log('\n[6] Measuring observer thrash during load-more sequence...');
+      await page.evaluate(() => {
+        window.__mutCbSince = 0;
+        window.__runSince = 0;
+        const watcher = new MutationObserver((muts) => {
+          window.__mutCbSince++;
+          // Count how many nodes were added
+          for (const m of muts) window.__runSince += m.addedNodes.length;
+        });
+        watcher.observe(document.body, { childList: true, subtree: true });
+        window.__mutWatcher2 = watcher;
+      });
+
+      // Click load-more again if available
+      const loadMoreExists2 = await page.locator(loadMoreSel).count();
+      if (loadMoreExists2 > 0) {
+        const cardsBefore2 = await page.locator('.boost-sd__product-item').count();
+        await page.locator(loadMoreSel).first().scrollIntoViewIfNeeded();
+        await page.locator(loadMoreSel).first().click();
+        // Watch mutations fire in real-time for 5 seconds
+        for (let t = 0; t < 5; t++) {
+          await page.waitForTimeout(1000);
+          const snap = await page.evaluate(() => ({
+            mutCbs: window.__mutCbSince,
+            addedNodes: window.__runSince,
+            cards: document.querySelectorAll('.boost-sd__product-item').length,
+            overlays: document.querySelectorAll('.dw-hover-overlay').length,
+          }));
+          console.log(`  t+${t+1}s: mutCbs=${snap.mutCbs} addedNodes=${snap.addedNodes} cards=${snap.cards} overlays=${snap.overlays}`);
+        }
+        const cardsAfter2 = await page.locator('.boost-sd__product-item').count();
+        const overlays2 = await page.locator('.dw-hover-overlay').count();
+        console.log(`  Load #2: cards ${cardsBefore2} → ${cardsAfter2}, overlays: ${overlays2}`);
+
+        // Final duplicate check
+        const dupCheck2 = await page.evaluate(() => {
+          const cards = document.querySelectorAll('.boost-sd__product-item');
+          let dups = 0;
+          const dupList = [];
+          for (const card of cards) {
+            const insideOvs = card.querySelectorAll('.dw-hover-overlay').length;
+            let sibOvs = 0;
+            const parent = card.parentElement;
+            if (parent) {
+              const children = Array.from(parent.children);
+              const idx = children.indexOf(card);
+              for (let i = idx + 1; i < children.length; i++) {
+                if (children[i].classList && children[i].classList.contains('dw-hover-overlay')) sibOvs++;
+                else break;
+              }
+            }
+            if (insideOvs + sibOvs > 1) {
+              dups++;
+              dupList.push(`inside=${insideOvs} sib=${sibOvs}`);
+            }
+          }
+          return {
+            dups,
+            dupList: dupList.slice(0, 5),
+            totalCards: cards.length,
+            totalOverlays: document.querySelectorAll('.dw-hover-overlay').length,
+          };
+        });
+        console.log(`  FINAL: ${dupCheck2.totalCards} cards, ${dupCheck2.totalOverlays} overlays, ${dupCheck2.dups} DUPLICATES`);
+        if (dupCheck2.dupList.length) console.log(`  Dup breakdown: ${dupCheck2.dupList.join(', ')}`);
+      }
+    } catch (e) {
+      console.log(`  Error clicking load-more: ${e.message}`);
+    }
+  } else {
+    console.log('  No load-more button found — checking for infinite scroll trigger element...');
+    const pageInfo = await page.evaluate(() => {
+      // Get the full pagination HTML
+      const pagination = document.querySelector('[class*="boost-sd__pagination"], [class*="pagination"]');
+      return {
+        paginationHTML: pagination ? pagination.outerHTML.substring(0, 800) : null,
+        paginationCount: document.querySelectorAll('[class*="boost-sd__pagination"]').length,
+      };
+    });
+    console.log(`  Pagination elements: ${pageInfo.paginationCount}`);
+    if (pageInfo.paginationHTML) console.log(`  Pagination HTML: ${pageInfo.paginationHTML}`);
+  }
+
+  // ── 7. Self-reinforcing loop test: inject fake card, watch cascade ────────
+  console.log('\n[7] Self-reinforcing loop test...');
+  const loopTest = await page.evaluate(() => {
+    // Reset counters
+    window.__loopMutCbs = 0;
+    window.__loopOverlayInserts = 0;
+
+    const loopWatcher = new MutationObserver((muts) => {
+      window.__loopMutCbs++;
+      for (const m of muts) {
+        for (const n of m.addedNodes) {
+          if (n.nodeType === 1 && n.classList && n.classList.contains('dw-hover-overlay')) {
+            window.__loopOverlayInserts++;
+          }
+        }
+      }
+    });
+    loopWatcher.observe(document.body, { childList: true, subtree: true });
+
+    // Find the product grid
+    const grid = document.querySelector('.boost-sd__product-list, .boost-sd__product-grid, [class*="product-grid"], [class*="product-list"]');
+    if (!grid) return { error: 'No grid found', gridCandidates: document.querySelectorAll('[class*="boost"]').length };
+
+    // Clone an existing card to simulate Boost appending
+    const existingCard = document.querySelector('.boost-sd__product-item');
+    if (!existingCard) return { error: 'No existing card found' };
+
+    const clonedCard = existingCard.cloneNode(true);
+    // Remove existing overlays from clone (simulate fresh card from Boost)
+    for (const ov of clonedCard.querySelectorAll('.dw-hover-overlay')) ov.remove();
+    // Remove __dwProcessed markers if any
+    clonedCard.removeAttribute('data-dw-hover');
+
+    const beforeCbs = window.__loopMutCbs;
+    const beforeInserts = window.__loopOverlayInserts;
+
+    // Append the fake card
+    grid.appendChild(clonedCard);
+
+    return new Promise(resolve => {
+      setTimeout(() => {
+        const afterCbs = window.__loopMutCbs - beforeCbs;
+        const afterInserts = window.__loopOverlayInserts - beforeInserts;
+        const allOverlays = document.querySelectorAll('.dw-hover-overlay').length;
+        const allCards = document.querySelectorAll('.boost-sd__product-item').length;
+
+        // Check if the cloned card got an overlay (even though it was cloned from one)
+        const cloneOverlays = clonedCard.querySelectorAll('.dw-hover-overlay').length;
+        let cloneSibOverlays = 0;
+        let sib = clonedCard.nextSibling;
+        while (sib) {
+          if (sib.nodeType === 1 && sib.classList && sib.classList.contains('dw-hover-overlay')) cloneSibOverlays++;
+          if (sib.nodeType === 1) break;
+          sib = sib.nextSibling;
+        }
+
+        resolve({
+          mutCbsFired: afterCbs,
+          overlayInserts: afterInserts,
+          totalCards: allCards,
+          totalOverlays: allOverlays,
+          cloneInsideOverlays: cloneOverlays,
+          cloneSiblingOverlays: cloneSibOverlays,
+          ratio: afterCbs > 0 ? (allCards / afterCbs).toFixed(2) : 'N/A',
+        });
+      }, 500); // wait 500ms for debounce (observer has 80ms debounce + run() time)
+    });
+  });
+  console.log(`  After injecting 1 fake card into grid:`);
+  console.log(`    MutObs callbacks fired: ${loopTest.mutCbsFired}`);
+  console.log(`    Overlay insertions: ${loopTest.overlayInserts}`);
+  console.log(`    Clone got inside overlay: ${loopTest.cloneInsideOverlays}`);
+  console.log(`    Clone got sibling overlay: ${loopTest.cloneSiblingOverlays}`);
+  console.log(`    Total cards: ${loopTest.totalCards}, total overlays: ${loopTest.totalOverlays}`);
+  if (loopTest.error) console.log(`    Error: ${loopTest.error}`);
+
+  // ── 8. Check the guard condition in labelCard ─────────────────────────────
+  console.log('\n[8] Testing labelCard guard against v5 sibling placement...');
+  const guardTest = await page.evaluate(() => {
+    // v5 places the overlay as a SIBLING of the image-col, not inside the card
+    // The guard is: if (card.querySelector('.dw-hover-overlay')) return;
+    // If the overlay is placed OUTSIDE the card, this guard will FAIL to detect
+    // an existing overlay → run() will try to insert another one.
+
+    // Find a card and check where its overlay actually lives
+    const card = document.querySelector('.boost-sd__product-item');
+    if (!card) return { error: 'no card' };
+
+    const insideOverlays = card.querySelectorAll('.dw-hover-overlay').length;
+    const imgCol = card.querySelector('.boost-sd__product-item-grid-view-layout-image, .product-list-item-thumbnail');
+    const imgColOverlays = imgCol ? imgCol.querySelectorAll('.dw-hover-overlay').length : 'no imgCol';
+
+    // Check parent context
+    const parent = card.parentElement;
+    const parentTag = parent ? parent.tagName : 'none';
+    const parentClass = parent ? (parent.className || '').substring(0, 80) : '';
+    const parentChildren = parent ? Array.from(parent.children).map(c => c.tagName + (c.className ? '.' + (c.className||'').split(' ')[0] : '')).join(', ') : '';
+
+    // Where IS the overlay?
+    const allOverlays = document.querySelectorAll('.dw-hover-overlay');
+    let overlayParentClass = '';
+    if (allOverlays.length > 0) {
+      const ov = allOverlays[0];
+      overlayParentClass = (ov.parentElement?.className || '').substring(0, 80);
+    }
+
+    // Critical: does card.querySelector('.dw-hover-overlay') find the overlay?
+    const guardWorks = insideOverlays > 0;
+
+    return {
+      insideOverlays,
+      imgColOverlays,
+      parentTag,
+      parentClass,
+      parentChildrenSummary: parentChildren.substring(0, 200),
+      overlayParentClass,
+      guardWorks,
+      conclusion: guardWorks
+        ? 'GUARD WORKS: overlay is inside card, duplicates prevented'
+        : 'GUARD BROKEN: overlay is NOT inside card — run() cannot detect existing overlay!',
+    };
+  });
+
+  console.log(`  Card inside overlays: ${guardTest.insideOverlays}`);
+  console.log(`  imgCol inside overlays: ${guardTest.imgColOverlays}`);
+  console.log(`  Overlay parent class: "${guardTest.overlayParentClass}"`);
+  console.log(`  Card parent: <${guardTest.parentTag}> class="${guardTest.parentClass}"`);
+  console.log(`  Parent children: ${guardTest.parentChildrenSummary}`);
+  console.log(`  Guard works: ${guardTest.guardWorks}`);
+  console.log(`  CONCLUSION: ${guardTest.conclusion}`);
+
+  // ── Boost API responses ───────────────────────────────────────────────────
+  console.log('\n[9] Boost API responses captured:');
+  boostApiResponses.forEach((r, i) => {
+    console.log(`  [${i+1}] ${r.status} ${r.url}`);
+    console.log(`    Response snippet: ${r.bodySnippet.substring(0, 300)}`);
+  });
+
+  if (consoleErrors.length > 0) {
+    console.log('\n[10] Console errors:');
+    consoleErrors.slice(0, 5).forEach((e, i) => console.log(`  [${i+1}] ${e}`));
+  }
+
+  // ── 10. Check what the Boost filter page=6 actually returns ──────────────
+  console.log('\n[11] Checking collection size / page count...');
+  const collectionInfo = await page.evaluate(() => {
+    // Boost often exposes total count in DOM
+    const totalEl = document.querySelector('[class*="boost-sd__total"], [class*="product-count"], [data-total-products]');
+    const paginationInfo = document.querySelector('[class*="boost-sd__pagination"]');
+    const boostData = window.boostSdConfig || window.boost_pf_config || {};
+
+    return {
+      totalEl: totalEl ? totalEl.textContent.trim() : null,
+      paginationHTML: paginationInfo ? paginationInfo.outerHTML.substring(0, 600) : null,
+      boostDataKeys: Object.keys(boostData).slice(0, 20),
+      currentPage: boostData.page || 'unknown',
+      totalProducts: boostData.totalProducts || 'unknown',
+    };
+  });
+  console.log(`  Total element text: ${collectionInfo.totalEl}`);
+  console.log(`  Current page: ${collectionInfo.currentPage}`);
+  console.log(`  Total products: ${collectionInfo.totalProducts}`);
+  console.log(`  Boost data keys: ${JSON.stringify(collectionInfo.boostDataKeys)}`);
+  if (collectionInfo.paginationHTML) console.log(`  Pagination HTML:\n  ${collectionInfo.paginationHTML}`);
+
+  await browser.close();
+  console.log('\nDone.');
+})();
diff --git a/dw-scroll-debug3.js b/dw-scroll-debug3.js
new file mode 100644
index 0000000..707e8bc
--- /dev/null
+++ b/dw-scroll-debug3.js
@@ -0,0 +1,312 @@
+/**
+ * dw-scroll-debug3.js — Final targeted test:
+ *
+ * Key question from debug2:
+ *  - body.scrollHeight == body.clientHeight == 2464 → the page is NOT taller than viewport
+ *    because the Boost container uses INTERNAL scrolling within a fixed-height wrapper.
+ *  - `body overflow: "hidden auto"` means the BODY itself is the scroll container.
+ *  - window.scrollTo(0,99999) → scrollY=1564 (not 2464 - innerHeight = 1564) — so window IS the scroller,
+ *    but scrollHeight is only 2464, meaning max scroll = 2464 - 900 = 1564. We're already at bottom.
+ *
+ * BUT the Boost button says "Load Previous Page" (not Next Page) and it's loading pages 5, 4...
+ * Boost is in INFINITE SCROLL mode that loads OLDER pages going UP, not down!
+ * The scroll trigger fires when user scrolls to the TOP (not bottom).
+ *
+ * Let me confirm: does scrolling to TOP trigger the infinite-scroll load?
+ * And measure the self-reinforcing observer loop during a real Boost append.
+ */
+
+const { chromium } = require('playwright');
+
+const TARGET_URL = 'https://www.designerwallcoverings.com/collections/decoglass-wallpaper?page=6';
+const VIEWPORT = { width: 1440, height: 900 };
+
+(async () => {
+  const browser = await chromium.launch({
+    headless: true,
+    args: ['--no-sandbox', '--disable-dev-shm-usage'],
+  });
+  const context = await browser.newContext({
+    viewport: VIEWPORT,
+    userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
+  });
+  const page = await context.newPage();
+
+  const boostApiCalls = [];
+  page.on('request', req => {
+    if (req.url().includes('bc-sf-filter')) boostApiCalls.push(req.url().substring(0, 200));
+  });
+
+  console.log('=== DW Scroll Debug — Round 3: Infinite Scroll Direction + Observer Thrash ===\n');
+
+  await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
+  await page.waitForSelector('.boost-sd__product-item', { timeout: 20000 });
+  await page.waitForTimeout(2000);
+
+  const initial = await page.evaluate(() => ({
+    cards: document.querySelectorAll('.boost-sd__product-item').length,
+    overlays: document.querySelectorAll('.dw-hover-overlay').length,
+    scrollY: window.scrollY,
+    docH: document.documentElement.scrollHeight,
+    winH: window.innerHeight,
+    maxScroll: document.documentElement.scrollHeight - window.innerHeight,
+  }));
+  console.log(`Initial state: ${initial.cards} cards, ${initial.overlays} overlays`);
+  console.log(`ScrollY: ${initial.scrollY}, docH: ${initial.docH}, winH: ${initial.winH}, maxScroll: ${initial.maxScroll}`);
+
+  // ── Set up detailed mutation observer BEFORE any interaction ──────────────
+  await page.evaluate(() => {
+    window.__phase = 'idle';
+    window.__phaseMutCbs = {};
+    window.__phaseAddedNodes = {};
+    window.__phaseOverlayInserts = {};
+
+    const tracker = new MutationObserver((muts) => {
+      const phase = window.__phase;
+      if (!window.__phaseMutCbs[phase]) {
+        window.__phaseMutCbs[phase] = 0;
+        window.__phaseAddedNodes[phase] = 0;
+        window.__phaseOverlayInserts[phase] = 0;
+      }
+      window.__phaseMutCbs[phase]++;
+      for (const m of muts) {
+        window.__phaseAddedNodes[phase] += m.addedNodes.length;
+        for (const n of m.addedNodes) {
+          if (n.nodeType === 1) {
+            if (n.classList && n.classList.contains('dw-hover-overlay')) {
+              window.__phaseOverlayInserts[phase]++;
+            }
+            if (n.querySelectorAll) {
+              window.__phaseOverlayInserts[phase] += n.querySelectorAll('.dw-hover-overlay').length;
+            }
+          }
+        }
+      }
+    });
+    tracker.observe(document.body, { childList: true, subtree: true });
+    window.__debugTracker = tracker;
+  });
+
+  // ── Test 1: Scroll to TOP → does Boost load previous pages? ──────────────
+  console.log('\n[TEST 1] Scroll to TOP — does Boost auto-load previous pages?');
+  const boostCallsBefore = boostApiCalls.length;
+  await page.evaluate(() => { window.__phase = 'scroll-to-top'; window.scrollTo(0, 0); });
+  await page.waitForTimeout(3000);
+
+  const afterScrollTop = await page.evaluate(() => ({
+    cards: document.querySelectorAll('.boost-sd__product-item').length,
+    overlays: document.querySelectorAll('.dw-hover-overlay').length,
+    scrollY: window.scrollY,
+    stats: { mutCbs: window.__phaseMutCbs, addedNodes: window.__phaseAddedNodes, overlayInserts: window.__phaseOverlayInserts },
+  }));
+  console.log(`  After scroll to top: ${afterScrollTop.cards} cards, ${afterScrollTop.overlays} overlays, scrollY=${afterScrollTop.scrollY}`);
+  console.log(`  New Boost API calls: ${boostApiCalls.length - boostCallsBefore}`);
+  console.log(`  Observer stats: ${JSON.stringify(afterScrollTop.stats)}`);
+
+  // ── Test 2: Scroll to bottom — does Boost auto-load NEXT pages? ──────────
+  console.log('\n[TEST 2] Scroll to BOTTOM — does Boost auto-load next pages?');
+  await page.evaluate(() => { window.__phase = 'scroll-to-bottom'; window.scrollTo(0, 99999); });
+  await page.waitForTimeout(3000);
+
+  const afterScrollBottom = await page.evaluate(() => ({
+    cards: document.querySelectorAll('.boost-sd__product-item').length,
+    overlays: document.querySelectorAll('.dw-hover-overlay').length,
+    scrollY: window.scrollY,
+    stats: { mutCbs: window.__phaseMutCbs, addedNodes: window.__phaseAddedNodes, overlayInserts: window.__phaseOverlayInserts },
+  }));
+  console.log(`  After scroll to bottom: ${afterScrollBottom.cards} cards, ${afterScrollBottom.overlays} overlays, scrollY=${afterScrollBottom.scrollY}`);
+  console.log(`  New Boost API calls: ${boostApiCalls.length - boostCallsBefore - (afterScrollTop.cards - initial.cards > 0 ? 1 : 0)}`);
+  console.log(`  Observer stats: ${JSON.stringify(afterScrollBottom.stats)}`);
+
+  // ── Test 3: Click load-more, watch mutation cascade closely ───────────────
+  console.log('\n[TEST 3] Click load-more — detailed observer cascade timing...');
+  const cardsBefore3 = await page.locator('.boost-sd__product-item').count();
+  const overlaysBefore3 = await page.locator('.dw-hover-overlay').count();
+  const boostBefore3 = boostApiCalls.length;
+
+  await page.evaluate(() => { window.__phase = 'load-more-click'; });
+  const loadMoreBtn = page.locator('.boost-sd__pagination-button--load-more');
+  await loadMoreBtn.scrollIntoViewIfNeeded();
+  await loadMoreBtn.click();
+
+  // Sample every 200ms for 4 seconds
+  const timeline = [];
+  for (let i = 0; i < 20; i++) {
+    await page.waitForTimeout(200);
+    const snap = await page.evaluate(() => ({
+      cards: document.querySelectorAll('.boost-sd__product-item').length,
+      overlays: document.querySelectorAll('.dw-hover-overlay').length,
+      // Current phase stats
+      mutCbs: window.__phaseMutCbs['load-more-click'] || 0,
+      addedNodes: window.__phaseAddedNodes['load-more-click'] || 0,
+      overlayInserts: window.__phaseOverlayInserts['load-more-click'] || 0,
+    }));
+    timeline.push(snap);
+  }
+
+  console.log('  ms  | cards | overlays | mutCbs | addedNodes | ovInserts | ratio(mutCb:ov)');
+  console.log('  ----|-------|----------|--------|------------|-----------|---------------');
+  timeline.forEach((snap, i) => {
+    const ratio = snap.overlayInserts > 0 ? (snap.mutCbs / snap.overlayInserts).toFixed(1) : 'N/A';
+    console.log(`  ${((i+1)*200).toString().padStart(4)} | ${snap.cards.toString().padStart(5)} | ${snap.overlays.toString().padStart(8)} | ${snap.mutCbs.toString().padStart(6)} | ${snap.addedNodes.toString().padStart(10)} | ${snap.overlayInserts.toString().padStart(9)} | ${ratio}`);
+  });
+
+  const finalSnap3 = timeline[timeline.length - 1];
+  console.log(`\n  SUMMARY: cards ${cardsBefore3}→${finalSnap3.cards} (+${finalSnap3.cards - cardsBefore3})`);
+  console.log(`  overlays ${overlaysBefore3}→${finalSnap3.overlays} (+${finalSnap3.overlays - overlaysBefore3})`);
+  console.log(`  MutObs callbacks: ${finalSnap3.mutCbs}`);
+  console.log(`  Added DOM nodes total: ${finalSnap3.addedNodes}`);
+  console.log(`  Overlay insertions by run(): ${finalSnap3.overlayInserts}`);
+  if (finalSnap3.overlayInserts > 0) {
+    const wasted = finalSnap3.mutCbs - finalSnap3.overlayInserts;
+    const wastedPct = ((wasted / finalSnap3.mutCbs) * 100).toFixed(0);
+    console.log(`  Wasted run() calls (no new overlays to insert): ${wasted} (${wastedPct}% of callbacks)`);
+    console.log(`  Each overlay insertion triggers ${(finalSnap3.mutCbs / finalSnap3.overlayInserts).toFixed(1)}x observer callbacks`);
+  }
+  console.log(`  Boost API calls during this: ${boostApiCalls.length - boostBefore3}`);
+
+  // ── Test 4: Duplicate overlay check after load-more ──────────────────────
+  console.log('\n[TEST 4] Duplicate overlay analysis after load-more...');
+  const dupAnalysis = await page.evaluate(() => {
+    const cards = document.querySelectorAll('.boost-sd__product-item');
+    let noOverlay = 0, singleOverlay = 0, multiOverlay = 0;
+    const dupDetails = [];
+
+    for (const card of cards) {
+      // The overlay is placed INSIDE the image-wrapper (not as a sibling of the card)
+      // Per debug2: overlayParentClass includes "boost-sd__product-image-wrapper"
+      const insideCard = card.querySelectorAll('.dw-hover-overlay').length;
+      if (insideCard === 0) noOverlay++;
+      else if (insideCard === 1) singleOverlay++;
+      else {
+        multiOverlay++;
+        dupDetails.push({
+          title: (card.querySelector('.boost-sd__product-title')?.textContent || '').trim().substring(0, 40),
+          count: insideCard,
+        });
+      }
+    }
+
+    // Also check for orphan overlays not inside any card
+    const allOverlays = document.querySelectorAll('.dw-hover-overlay').length;
+    const orphans = allOverlays - singleOverlay - (multiOverlay > 0 ? multiOverlay * 2 : 0); // rough
+
+    return { noOverlay, singleOverlay, multiOverlay, dupDetails: dupDetails.slice(0, 5), totalCards: cards.length, allOverlays };
+  });
+  console.log(`  Cards: ${dupAnalysis.totalCards} total, ${dupAnalysis.singleOverlay} clean, ${dupAnalysis.noOverlay} missing overlay, ${dupAnalysis.multiOverlay} DUPLICATED`);
+  console.log(`  Total overlays in DOM: ${dupAnalysis.allOverlays}`);
+  if (dupAnalysis.dupDetails.length > 0) {
+    console.log(`  Duplicated cards:`);
+    dupAnalysis.dupDetails.forEach(d => console.log(`    "${d.title}": ${d.count} overlays`));
+  }
+
+  // ── Test 5: The REAL scroll glitch — infinite scroll direction mismatch ───
+  console.log('\n[TEST 5] Boost pagination analysis — page direction...');
+  const pageAnalysis = await page.evaluate(() => {
+    // Boost is configured for "infinite scroll" but with a page=6 URL:
+    // - "Load Previous Page" button means Boost loads OLDER products (page 5, 4...)
+    // - The "next page" / bottom trigger would load page 7+
+    // - decoglass-wallpaper has 215 total products, 24/page = ~9 pages
+    // - page=6 has 24 products (pages 6 of ~9), so there IS a next page
+
+    const pagination = document.querySelector('.boost-sd__pagination-infinite-scroll-container');
+    const allButtons = pagination ? Array.from(pagination.querySelectorAll('button')).map(b => ({
+      text: b.textContent.trim(),
+      class: b.className,
+      disabled: b.disabled,
+    })) : [];
+
+    // Check for a "load next" or scroll-to-bottom sentinel
+    const scrollSentinel = document.querySelector('[class*="boost-sd__sentinel"], [class*="infinite-scroll-sentinel"]');
+
+    // Check page URL parameters
+    const urlParams = new URLSearchParams(window.location.search);
+
+    return {
+      currentPageParam: urlParams.get('page'),
+      allPaginationButtons: allButtons,
+      hasScrollSentinel: !!scrollSentinel,
+      sentinelClass: scrollSentinel?.className,
+      paginationHTML: pagination?.outerHTML?.substring(0, 1000),
+      hasInfiniteScrollContainer: !!pagination,
+    };
+  });
+  console.log(`  Current page param: ${pageAnalysis.currentPageParam}`);
+  console.log(`  Pagination buttons: ${JSON.stringify(pageAnalysis.allPaginationButtons)}`);
+  console.log(`  Has scroll sentinel: ${pageAnalysis.hasScrollSentinel}, class: ${pageAnalysis.sentinelClass}`);
+  console.log(`  Pagination container HTML:\n  ${pageAnalysis.paginationHTML}`);
+
+  // ── Test 6: Try triggering bottom-page infinite scroll (Boost next-page) ──
+  console.log('\n[TEST 6] Trying to trigger NEXT-page infinite scroll (scroll to absolute bottom)...');
+  const boostBefore6 = boostApiCalls.length;
+  // Scroll in small increments to ensure scroll events fire
+  await page.evaluate(async () => {
+    const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
+    for (let y = window.scrollY; y <= maxScroll; y += 50) {
+      window.scrollTo(0, y);
+      await new Promise(r => setTimeout(r, 20));
+    }
+    window.scrollTo(0, maxScroll);
+  });
+  await page.waitForTimeout(3000);
+
+  const afterBottom6 = await page.evaluate(() => ({
+    cards: document.querySelectorAll('.boost-sd__product-item').length,
+    overlays: document.querySelectorAll('.dw-hover-overlay').length,
+    scrollY: window.scrollY,
+    docH: document.documentElement.scrollHeight,
+  }));
+  console.log(`  After gradual scroll to bottom: ${afterBottom6.cards} cards, ${afterBottom6.overlays} overlays`);
+  console.log(`  scrollY=${afterBottom6.scrollY}, docH=${afterBottom6.docH}`);
+  console.log(`  New Boost API calls: ${boostApiCalls.length - boostBefore6}`);
+  if (boostApiCalls.length > boostBefore6) {
+    console.log(`  New calls: ${boostApiCalls.slice(boostBefore6).join('\n    ')}`);
+  }
+
+  // ── Test 7: What's blocking infinite scroll trigger at bottom? ────────────
+  console.log('\n[TEST 7] Checking Boost infinite scroll sentinel/waypoint position...');
+  const sentinelInfo = await page.evaluate(() => {
+    // Boost 2.1.17 uses an IntersectionObserver for its infinite scroll sentinel
+    // Let's find ALL elements with "sentinel", "waypoint", "trigger" in their class
+    const candidates = Array.from(document.querySelectorAll('*')).filter(el => {
+      const cls = (el.className || '').toString();
+      return cls.includes('sentinel') || cls.includes('waypoint') || cls.includes('infinite-trigger') || cls.includes('load-next');
+    });
+
+    // Also look for data attributes
+    const dataEls = Array.from(document.querySelectorAll('[data-infinite], [data-sentinel], [data-load-more]'));
+
+    // Check scroll position relative to viewport
+    const all = [...candidates, ...dataEls].slice(0, 5);
+    return {
+      count: candidates.length + dataEls.length,
+      elements: all.map(el => ({
+        tag: el.tagName,
+        class: (el.className || '').toString().substring(0, 80),
+        rect: el.getBoundingClientRect(),
+        dataAttrs: Object.fromEntries(Array.from(el.attributes).filter(a => a.name.startsWith('data-')).map(a => [a.name, a.value.substring(0, 50)])),
+      })),
+    };
+  });
+  console.log(`  Sentinel/waypoint elements found: ${sentinelInfo.count}`);
+  sentinelInfo.elements.forEach((el, i) => {
+    console.log(`  [${i+1}] <${el.tag}> class="${el.class}"`);
+    console.log(`       rect: ${JSON.stringify(el.rect)}`);
+    console.log(`       data: ${JSON.stringify(el.dataAttrs)}`);
+  });
+
+  // ── Summary ───────────────────────────────────────────────────────────────
+  console.log('\n=== COMPREHENSIVE ROOT CAUSE SUMMARY ===');
+
+  const totalBoostCalls = boostApiCalls.length;
+  console.log(`Total Boost API calls during entire session: ${totalBoostCalls}`);
+  console.log('\nBoost API calls breakdown:');
+  boostApiCalls.forEach((url, i) => {
+    const pageMatch = url.match(/page=(\d+)/);
+    const page = pageMatch ? pageMatch[1] : '?';
+    console.log(`  [${i+1}] page=${page}: ${url.substring(0, 100)}`);
+  });
+
+  await browser.close();
+  console.log('\nDone.');
+})();
diff --git a/dw-scroll-verify.js b/dw-scroll-verify.js
new file mode 100644
index 0000000..4cecbe7
--- /dev/null
+++ b/dw-scroll-verify.js
@@ -0,0 +1,201 @@
+/**
+ * dw-scroll-verify.js — Verify the v6 fix works correctly.
+ * Injects the fixed dw-card-hover.js logic and verifies:
+ *  1. No duplicate overlays after load-more
+ *  2. Observer fires only on product-node mutations, not overlay insertions
+ *  3. Infinite scroll loads subsequent pages correctly
+ */
+
+const { chromium } = require('playwright');
+const fs = require('fs');
+const path = require('path');
+
+const TARGET_URL = 'https://www.designerwallcoverings.com/collections/decoglass-wallpaper?page=6';
+const FIXED_JS_PATH = '/Users/macstudio3/Projects/Designer-Wallcoverings/shopify/theme-LIVE-591/assets/dw-card-hover.js';
+const VIEWPORT = { width: 1440, height: 900 };
+
+(async () => {
+  const fixedJs = fs.readFileSync(FIXED_JS_PATH, 'utf8');
+
+  const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] });
+  const context = await browser.newContext({
+    viewport: VIEWPORT,
+    userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
+  });
+
+  // Intercept and replace the live dw-card-hover.js with the fixed version
+  await context.route('**/assets/dw-card-hover.js*', async (route) => {
+    route.fulfill({ status: 200, contentType: 'application/javascript', body: fixedJs });
+  });
+
+  const page = await context.newPage();
+  const boostApiCalls = [];
+  page.on('request', req => { if (req.url().includes('bc-sf-filter')) boostApiCalls.push(req.url()); });
+
+  console.log('=== DW Scroll Verify — v6 Fix Validation ===\n');
+  console.log('Injecting fixed dw-card-hover.js via route interception...\n');
+
+  await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded', timeout: 30000 });
+  await page.waitForSelector('.boost-sd__product-item', { timeout: 20000 });
+  await page.waitForTimeout(2000);
+
+  const initial = await page.evaluate(() => ({
+    cards: document.querySelectorAll('.boost-sd__product-item').length,
+    overlays: document.querySelectorAll('.dw-hover-overlay').length,
+    dwHoverActive: window.__dwCardHover,
+    processedCards: document.querySelectorAll('[data-dw-hover]').length,
+  }));
+  console.log(`Initial: ${initial.cards} cards, ${initial.overlays} overlays, processed=${initial.processedCards}, dwHoverActive=${initial.dwHoverActive}`);
+
+  // Set up mutation tracking
+  await page.evaluate(() => {
+    window.__v6ObsCallbacks = 0;
+    window.__v6OverlayTriggeredCallbacks = 0;
+    window.__v6ProductTriggeredCallbacks = 0;
+    const tracker = new MutationObserver((muts) => {
+      window.__v6ObsCallbacks++;
+      let hasOverlay = false, hasProduct = false;
+      for (const m of muts) {
+        for (const n of m.addedNodes) {
+          if (n.nodeType !== 1) continue;
+          if (n.classList && n.classList.contains('dw-hover-overlay')) hasOverlay = true;
+          if (n.classList && (n.classList.contains('boost-sd__product-item') || n.classList.contains('product-list-item'))) hasProduct = true;
+        }
+      }
+      if (hasOverlay) window.__v6OverlayTriggeredCallbacks++;
+      if (hasProduct) window.__v6ProductTriggeredCallbacks++;
+    });
+    tracker.observe(document.body, { childList: true, subtree: true });
+  });
+
+  // Click load-more 3 times and verify
+  let totalLoadMoreClicks = 0;
+  for (let round = 0; round < 3; round++) {
+    const btnSel = '.boost-sd__pagination-button--load-more';
+    const btnExists = await page.locator(btnSel).count();
+    if (!btnExists) { console.log(`  Round ${round+1}: no load-more button, stopping`); break; }
+
+    const cardsBefore = await page.locator('.boost-sd__product-item').count();
+    const cbsBefore = await page.evaluate(() => window.__v6ObsCallbacks);
+
+    await page.locator(btnSel).first().scrollIntoViewIfNeeded();
+    await page.locator(btnSel).first().click();
+    await page.waitForTimeout(2500);
+    totalLoadMoreClicks++;
+
+    const snap = await page.evaluate(() => {
+      const cards = document.querySelectorAll('.boost-sd__product-item');
+      let dups = 0, missing = 0, processedCount = 0;
+      for (const card of cards) {
+        const insideOvs = card.querySelectorAll('.dw-hover-overlay').length;
+        const isProcessed = card.dataset.dwHover === '1';
+        if (isProcessed) processedCount++;
+        if (insideOvs > 1) dups++;
+        if (insideOvs === 0 && !isProcessed) missing++;
+      }
+      return {
+        cards: cards.length,
+        overlays: document.querySelectorAll('.dw-hover-overlay').length,
+        dups,
+        missing,
+        processedCount,
+        obsCbs: window.__v6ObsCallbacks,
+        overlayTriggered: window.__v6OverlayTriggeredCallbacks,
+        productTriggered: window.__v6ProductTriggeredCallbacks,
+      };
+    });
+
+    const newCbs = snap.obsCbs - cbsBefore;
+    console.log(`Round ${round+1}: cards=${cardsBefore}→${snap.cards} (+${snap.cards - cardsBefore}), overlays=${snap.overlays}, dups=${snap.dups}, missing=${snap.missing}`);
+    console.log(`  Observer: +${newCbs} callbacks this round (overlay-triggered=${snap.overlayTriggered}, product-triggered=${snap.productTriggered})`);
+    console.log(`  data-dw-hover processed: ${snap.processedCount}/${snap.cards}`);
+
+    if (snap.dups > 0) console.log(`  !!! FAIL: ${snap.dups} cards with duplicate overlays`);
+    else console.log(`  PASS: no duplicate overlays`);
+  }
+
+  // Test: inject fake card and verify observer does NOT fire run() for overlay mutations
+  console.log('\n[Self-loop test] Inject card, verify observer fires selectively...');
+  const loopTest = await page.evaluate(() => {
+    window.__v6ObsCallbacks = 0;
+    window.__v6OverlayTriggeredCallbacks = 0;
+
+    const grid = document.querySelector('.boost-sd__product-list');
+    const existingCard = document.querySelector('.boost-sd__product-item');
+    if (!grid || !existingCard) return { error: 'no grid/card' };
+
+    const clone = existingCard.cloneNode(true);
+    // Remove processed flag and overlays to simulate fresh card
+    delete clone.dataset.dwHover;
+    for (const ov of clone.querySelectorAll('.dw-hover-overlay')) ov.remove();
+
+    grid.appendChild(clone);
+
+    return new Promise(resolve => {
+      setTimeout(() => {
+        resolve({
+          obsCbsAfter: window.__v6ObsCallbacks,
+          overlayTriggered: window.__v6OverlayTriggeredCallbacks,
+          totalCards: document.querySelectorAll('.boost-sd__product-item').length,
+          totalOverlays: document.querySelectorAll('.dw-hover-overlay').length,
+          cloneProcessed: clone.dataset.dwHover === '1',
+          cloneInsideOvs: clone.querySelectorAll('.dw-hover-overlay').length,
+        });
+      }, 600);
+    });
+  });
+
+  if (loopTest.error) {
+    console.log(`  Error: ${loopTest.error}`);
+  } else {
+    console.log(`  After inject: cards=${loopTest.totalCards}, overlays=${loopTest.totalOverlays}`);
+    console.log(`  Observer callbacks: ${loopTest.obsCbsAfter} (overlay-triggered: ${loopTest.overlayTriggered})`);
+    console.log(`  Clone processed (data-dw-hover=1): ${loopTest.cloneProcessed}`);
+    console.log(`  Clone inside overlays: ${loopTest.cloneInsideOvs}`);
+
+    if (loopTest.overlayTriggered === 0) {
+      console.log('  PASS: overlay insertions did NOT trigger observer callbacks (self-loop eliminated)');
+    } else {
+      console.log(`  WARN: ${loopTest.overlayTriggered} overlay-triggered callbacks (self-loop may still exist)`);
+    }
+    if (loopTest.cloneProcessed) {
+      console.log('  PASS: clone card was labeled and marked as processed');
+    } else {
+      console.log('  FAIL: clone card was NOT processed by run()');
+    }
+  }
+
+  // Final scroll test: does infinite scroll to bottom still trigger page 7/8?
+  console.log('\n[Scroll test] Verify bottom-scroll still triggers Boost next-page load...');
+  const boostBefore = boostApiCalls.length;
+  await page.evaluate(async () => {
+    const max = document.documentElement.scrollHeight - window.innerHeight;
+    for (let y = window.scrollY; y <= max; y += 100) {
+      window.scrollTo(0, y);
+      await new Promise(r => setTimeout(r, 15));
+    }
+  });
+  await page.waitForTimeout(3000);
+  const newBoostCalls = boostApiCalls.slice(boostBefore);
+  console.log(`  New Boost API calls after scrolling to bottom: ${newBoostCalls.length}`);
+  newBoostCalls.forEach((url, i) => {
+    const m = url.match(/page=(\d+)/);
+    console.log(`  [${i+1}] page=${m?.[1]||'?'}: ${url.substring(0, 100)}`);
+  });
+
+  const finalState = await page.evaluate(() => ({
+    cards: document.querySelectorAll('.boost-sd__product-item').length,
+    overlays: document.querySelectorAll('.dw-hover-overlay').length,
+    dups: Array.from(document.querySelectorAll('.boost-sd__product-item')).filter(c => c.querySelectorAll('.dw-hover-overlay').length > 1).length,
+  }));
+  console.log(`\nFinal state: ${finalState.cards} cards, ${finalState.overlays} overlays, ${finalState.dups} with duplicate overlays`);
+
+  console.log('\n=== VERIFICATION SUMMARY ===');
+  console.log(`Guard fix (data-dw-hover attribute): ${loopTest.cloneProcessed ? 'WORKING' : 'NEEDS CHECK'}`);
+  console.log(`Self-loop elimination: ${loopTest.overlayTriggered === 0 ? 'CONFIRMED — overlay mutations no longer trigger observer' : 'PARTIAL — some overlay triggers remain'}`);
+  console.log(`Infinite scroll integration: ${newBoostCalls.length > 0 ? `WORKING — ${newBoostCalls.length} new page loads triggered` : 'NO NEW LOADS (may be at end of collection or scroll not reaching sentinel)'}`);
+  console.log(`Duplicate overlays at end: ${finalState.dups === 0 ? 'NONE — clean' : finalState.dups + ' DUPLICATES — issue persists'}`);
+
+  await browser.close();
+  console.log('\nDone.');
+})();

← 2b76a66 chore: refactor — rename GOOGLE→GOOGLE_YOUTUBE for clarity (  ·  back to Secrets Manager  ·  auto-save: 2026-07-22T14:44:58 (2 files) — registry.json rou 4604d18 →