[object Object]

← back to Designer Wallcoverings

Refine PDP color-palette: fix invisible density slider, dedup near-duplicate dots, premium slider + color-index grid chrome

7fa268bff96e8ae231a40cee2c828a3d86948910 · 2026-07-09 16:02:07 -0700 · Steve (DW)

- CRITICAL live bug fix: wireDensity() multi-color path never cleared the markup's
  'hidden' attr, so the density slider was invisible on every >1-color product.
  Now unhides in the multi-color branch.
- Distribution: ΔE76 (CIELAB) adjacent-dedup of interpolated dots (JND ~2.3) — a
  4-grey palette at slider 64 now renders 22 meaningful swatches, not 64 near-dupes.
  Base (real) colors are never deduped.
- Slider visuals: caption-under-title layout, hairline track, smaller ink thumb w/
  hairline ring, letter-spaced Lora label, focus-visible rings (keyboard a11y).
- Swatches: softer hairline base ring (was heavy dark hoop), material inner-shadow,
  larger radius, 3px gaps.
- Color-index grid: hairline-ruled header, Lora title at palette scale, soft reveal
  animation, even gutters, hairline media, Esc-to-close + focus mgmt.
- All prior behavior preserved: localStorage persistence, 80ms debounce, single-color
  disabled+hint, 10% tolerance, CLIP strip, family fallback href.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 7fa268bff96e8ae231a40cee2c828a3d86948910
Author: Steve (DW) <steve@designerwallcoverings.com>
Date:   Thu Jul 9 16:02:07 2026 -0700

    Refine PDP color-palette: fix invisible density slider, dedup near-duplicate dots, premium slider + color-index grid chrome
    
    - CRITICAL live bug fix: wireDensity() multi-color path never cleared the markup's
      'hidden' attr, so the density slider was invisible on every >1-color product.
      Now unhides in the multi-color branch.
    - Distribution: ΔE76 (CIELAB) adjacent-dedup of interpolated dots (JND ~2.3) — a
      4-grey palette at slider 64 now renders 22 meaningful swatches, not 64 near-dupes.
      Base (real) colors are never deduped.
    - Slider visuals: caption-under-title layout, hairline track, smaller ink thumb w/
      hairline ring, letter-spaced Lora label, focus-visible rings (keyboard a11y).
    - Swatches: softer hairline base ring (was heavy dark hoop), material inner-shadow,
      larger radius, 3px gaps.
    - Color-index grid: hairline-ruled header, Lora title at palette scale, soft reveal
      animation, even gutters, hairline media, Esc-to-close + focus mgmt.
    - All prior behavior preserved: localStorage persistence, 80ms debounce, single-color
      disabled+hint, 10% tolerance, CLIP strip, family fallback href.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 .../wallquest-refresh/cp-refine-capture-local.cjs  |  44 +++++++
 scripts/wallquest-refresh/cp-scrape-data.cjs       |  18 +++
 .../snippets/color-palette.liquid                  | 131 +++++++++++++++------
 3 files changed, 156 insertions(+), 37 deletions(-)

diff --git a/scripts/wallquest-refresh/cp-refine-capture-local.cjs b/scripts/wallquest-refresh/cp-refine-capture-local.cjs
new file mode 100644
index 00000000..f85c2794
--- /dev/null
+++ b/scripts/wallquest-refresh/cp-refine-capture-local.cjs
@@ -0,0 +1,44 @@
+const puppeteer=require('/Users/macstudio3/Projects/Designer-Wallcoverings/scripts/wallquest-refresh/node_modules/puppeteer');
+const path=require('path');
+const OUT='/Users/macstudio3/Projects/Designer-Wallcoverings/scripts/wallquest-refresh/cp-refine-shots';
+const HARNESS='file://'+path.join('/Users/macstudio3/Projects/Designer-Wallcoverings/scripts/wallquest-refresh','cp-harness.html');
+const TAG='after';
+(async()=>{
+  const b=await puppeteer.launch({headless:'new',args:['--no-sandbox','--disable-web-security']});
+  const p=await b.newPage();
+  await p.setViewport({width:820,height:1600,deviceScaleFactor:2});
+  p.on('console',m=>{const t=m.text();if(/error|fail/i.test(t))console.log('  [c]',t.slice(0,140));});
+  await p.goto(HARNESS,{waitUntil:'networkidle2',timeout:30000});
+  await new Promise(r=>setTimeout(r,2500));
+  const pal='[data-dw-color-palette]';
+  const el=await p.$(pal);
+  if(!el){console.log('NO PALETTE');await b.close();process.exit(2);}
+  const st=await p.evaluate(()=>{const r=document.querySelector('[data-dw-color-palette]');const c=r.querySelector('[data-dw-color-palette-cards]');const rg=r.querySelector('[data-dw-cp-range]');const d=r.querySelector('[data-dw-palette-density]');return{dots:c?c.children.length:0,base:c?c.querySelectorAll('.dw-swatch--base').length:0,sliderVal:rg?rg.value:null,sliderMin:rg?rg.min:null,sliderMax:rg?rg.max:null,densityHidden:d?d.hasAttribute('hidden'):null,densityVisible:d?(getComputedStyle(d).display!=='none'):null};});
+  console.log('REST:',JSON.stringify(st));
+  await el.screenshot({path:path.join(OUT,`${TAG}-01-rest.png`)});
+  // low
+  await p.evaluate(()=>{const r=document.querySelector('[data-dw-cp-range]');if(r&&!r.disabled){r.value=r.min;r.dispatchEvent(new Event('input',{bubbles:true}));r.dispatchEvent(new Event('change',{bubbles:true}));}});
+  await new Promise(r=>setTimeout(r,300));
+  const lowN=await p.evaluate(()=>document.querySelector('[data-dw-color-palette-cards]').children.length);
+  await el.screenshot({path:path.join(OUT,`${TAG}-02-low.png`)});
+  // high
+  await p.evaluate(()=>{const r=document.querySelector('[data-dw-cp-range]');if(r&&!r.disabled){r.value=r.max;r.dispatchEvent(new Event('input',{bubbles:true}));r.dispatchEvent(new Event('change',{bubbles:true}));}});
+  await new Promise(r=>setTimeout(r,300));
+  const highN=await p.evaluate(()=>document.querySelector('[data-dw-color-palette-cards]').children.length);
+  await el.screenshot({path:path.join(OUT,`${TAG}-03-high.png`)});
+  console.log('low dots:',lowN,'high dots(after dedup):',highN);
+  // persistence check: reload, read localStorage-driven value
+  const persisted=await p.evaluate(()=>{try{return localStorage.getItem('dw_palette_dot_count');}catch(e){return 'ERR';}});
+  console.log('persisted dw_palette_dot_count:',persisted);
+  // click base dot -> index
+  const clicked=await p.evaluate(()=>{const b=document.querySelector('[data-dw-color-palette-cards] .dw-swatch--base');if(b){b.click();return true;}return false;});
+  await new Promise(r=>setTimeout(r,4500));
+  const ci=await p.evaluate(()=>{const c=document.querySelector('[data-dw-color-index]');if(!c)return{present:false};const g=c.querySelector('[data-dw-ci-grid]');return{present:true,hidden:c.hasAttribute('hidden'),isOpen:c.classList.contains('is-open'),cards:g?g.querySelectorAll('.dw-color-index__card').length:0,empty:g?!!g.querySelector('.dw-color-index__empty'):false,loading:g?!!g.querySelector('.dw-color-index__loading'):false};});
+  console.log('clicked:',clicked,'INDEX:',JSON.stringify(ci));
+  await p.evaluate(()=>document.querySelector('[data-dw-color-index]').scrollIntoView({block:'start'}));
+  await new Promise(r=>setTimeout(r,400));
+  const ciEl=await p.$('[data-dw-color-index]');
+  if(ciEl) await ciEl.screenshot({path:path.join(OUT,`${TAG}-04-index-open.png`)});
+  await b.close();
+  console.log('DONE');
+})().catch(e=>{console.error('ERR',e.message);process.exit(1);});
diff --git a/scripts/wallquest-refresh/cp-scrape-data.cjs b/scripts/wallquest-refresh/cp-scrape-data.cjs
new file mode 100644
index 00000000..7f36ed97
--- /dev/null
+++ b/scripts/wallquest-refresh/cp-scrape-data.cjs
@@ -0,0 +1,18 @@
+const puppeteer=require('/Users/macstudio3/Projects/Designer-Wallcoverings/scripts/wallquest-refresh/node_modules/puppeteer');
+(async()=>{
+  const b=await puppeteer.launch({headless:'new',args:['--no-sandbox']});
+  const p=await b.newPage();
+  await p.goto('https://www.designerwallcoverings.com/products/crayford-paisley-porcelain',{waitUntil:'networkidle2',timeout:60000});
+  await p.waitForSelector('[data-dw-color-palette]',{timeout:20000}).catch(()=>{});
+  const data=await p.evaluate(()=>{
+    const r=document.querySelector('[data-dw-color-palette]');
+    const o={};
+    if(r){ for(const a of r.attributes){ o[a.name]=a.value; } }
+    // grab a gallery img src for canvas fallback
+    const g=document.querySelector('.product-gallery img, .product__media img, img[src*="cdn/shop"]');
+    o._galleryImg = g?g.src:'';
+    return o;
+  });
+  console.log(JSON.stringify(data,null,1));
+  await b.close();
+})();
diff --git a/shopify/theme-refine-colorpalette-20260709/snippets/color-palette.liquid b/shopify/theme-refine-colorpalette-20260709/snippets/color-palette.liquid
index 6525af08..8ee04b1f 100644
--- a/shopify/theme-refine-colorpalette-20260709/snippets/color-palette.liquid
+++ b/shopify/theme-refine-colorpalette-20260709/snippets/color-palette.liquid
@@ -100,19 +100,23 @@
 </div>
 
 <style>
-  .dw-color-palette{padding:16px 0 10px;text-align:center;min-height:10px;}
-  .dw-color-palette__title{font-family:Lora,serif;font-size:15px;font-weight:500;color:#3D4246;margin:0 0 12px;letter-spacing:-0.01em;}
-  /* ── DENSITY SLIDER (expand/contract dot count) ── */
-  .dw-color-palette__density{display:flex;align-items:center;justify-content:center;gap:8px;max-width:660px;margin:0 auto 14px;}
+  .dw-color-palette{padding:18px 0 12px;text-align:center;min-height:10px;}
+  .dw-color-palette__title{font-family:Lora,serif;font-size:15px;font-weight:500;color:#3D4246;margin:0 0 4px;letter-spacing:.01em;}
+  /* ── DENSITY SLIDER (expand/contract dot count) — refined to read as a quiet
+     caption UNDER the title, not a utilitarian widget row (refine 2026-07-09). ── */
+  .dw-color-palette__density{display:flex;align-items:center;justify-content:center;gap:10px;max-width:660px;margin:0 auto 15px;}
   .dw-color-palette__density[hidden]{display:none;}
-  .dw-cp-dense__wrap{display:inline-flex;align-items:center;gap:8px;}
-  .dw-cp-dense__lbl{font-family:Lora,serif;font-size:11px;color:#8a8577;letter-spacing:.02em;text-transform:uppercase;white-space:nowrap;}
-  .dw-cp-dense__range{-webkit-appearance:none;appearance:none;width:150px;max-width:38vw;height:3px;border-radius:3px;background:#e0ded9;outline:none;cursor:pointer;margin:0;}
-  .dw-cp-dense__range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:14px;height:14px;border-radius:50%;background:#3D4246;border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.18);cursor:pointer;}
-  .dw-cp-dense__range::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:#3D4246;border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.18);cursor:pointer;}
-  .dw-cp-dense__out{font-family:Lora,serif;font-size:12px;font-weight:600;color:#3D4246;min-width:18px;text-align:right;font-variant-numeric:tabular-nums;}
-  .dw-cp-dense__step{font-family:Lora,serif;font-size:16px;line-height:1;color:#6b6b6b;background:none;border:1px solid #e0ded9;border-radius:50%;width:24px;height:24px;padding:0;cursor:pointer;flex:0 0 auto;}
-  .dw-cp-dense__step:hover{color:#000;border-color:#b8b3a8;}
+  .dw-cp-dense__wrap{display:inline-flex;align-items:center;gap:9px;}
+  .dw-cp-dense__lbl{font-family:Lora,serif;font-size:10px;color:#8a8577;letter-spacing:.14em;text-transform:uppercase;white-space:nowrap;}
+  .dw-cp-dense__range{-webkit-appearance:none;appearance:none;width:150px;max-width:38vw;height:2px;border-radius:2px;background:linear-gradient(#d8d4cc,#d8d4cc) no-repeat;background-size:100% 2px;background-position:0 center;outline:none;cursor:pointer;margin:0;}
+  .dw-cp-dense__range:focus-visible{outline:2px solid #b8b3a8;outline-offset:4px;border-radius:3px;}
+  .dw-cp-dense__range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:12px;height:12px;border-radius:50%;background:#3D4246;border:2px solid #fff;box-shadow:0 0 0 1px rgba(61,66,70,.35),0 1px 2px rgba(0,0,0,.12);cursor:pointer;transition:transform .1s ease;}
+  .dw-cp-dense__range::-webkit-slider-thumb:hover{transform:scale(1.15);}
+  .dw-cp-dense__range::-moz-range-thumb{width:12px;height:12px;border-radius:50%;background:#3D4246;border:2px solid #fff;box-shadow:0 0 0 1px rgba(61,66,70,.35),0 1px 2px rgba(0,0,0,.12);cursor:pointer;}
+  .dw-cp-dense__out{font-family:Lora,serif;font-size:11px;font-weight:600;color:#3D4246;min-width:16px;text-align:right;font-variant-numeric:tabular-nums;letter-spacing:.01em;}
+  .dw-cp-dense__step{font-family:Lora,serif;font-size:15px;line-height:1;color:#8a8577;background:none;border:1px solid #e0ded9;border-radius:50%;width:22px;height:22px;padding:0;cursor:pointer;flex:0 0 auto;transition:color .12s ease,border-color .12s ease;}
+  .dw-cp-dense__step:hover{color:#3D4246;border-color:#b8b3a8;}
+  .dw-cp-dense__step:focus-visible{outline:2px solid #b8b3a8;outline-offset:2px;}
   /* single-dominant-color state: control stays visible but reads as intentionally
      inert (Steve 2026-07-09 fix #2) — dimmed, non-interactive, hinted via title/aria. */
   .dw-color-palette__density.is-single{opacity:.55;}
@@ -127,19 +131,22 @@
   }
   /* Centered EVEN ROWS of uniform swatches: flex-wrap + justify-content:center
      centers every row, including a partial last row. (Steve 2026-07-08) */
-  .dw-color-palette__cards{display:flex;flex-wrap:wrap;justify-content:center;align-content:center;gap:2px;max-width:660px;margin:0 auto;}
-  .dw-swatch{display:block;width:18px;height:18px;border-radius:3px;text-decoration:none;box-sizing:border-box;border:1px solid rgba(0,0,0,.06);transition:transform .12s ease,box-shadow .12s ease,z-index .12s;position:relative;}
+  .dw-color-palette__cards{display:flex;flex-wrap:wrap;justify-content:center;align-content:center;gap:3px;max-width:660px;margin:0 auto;}
+  /* Swatches read as cut material tiles: faint inner shadow + slightly larger radius,
+     hairline edge (not a heavy border). (refine 2026-07-09) */
+  .dw-swatch{display:block;width:18px;height:18px;border-radius:4px;text-decoration:none;box-sizing:border-box;box-shadow:inset 0 0 0 1px rgba(0,0,0,.05),inset 0 -3px 5px rgba(0,0,0,.045);transition:transform .12s ease,box-shadow .12s ease,z-index .12s;position:relative;}
   a.dw-swatch{cursor:pointer;}
-  .dw-swatch:hover{transform:scale(1.55);box-shadow:0 3px 10px rgba(0,0,0,.22);z-index:3;border-color:rgba(0,0,0,.25);}
-  /* the pattern's REAL colors get a ring + a touch more size so they stand out
-     from the interpolated in-between steps, while rows stay even. */
-  .dw-swatch--base{width:22px;height:22px;border-radius:4px;border:1px solid #fff;box-shadow:0 0 0 1.5px #3D4246,0 1px 3px rgba(0,0,0,.14);}
-  .dw-swatch--base:hover{transform:scale(1.4);}
-  .dw-color-palette__bar{display:flex;height:4px;border-radius:3px;overflow:hidden;max-width:660px;margin:14px auto 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.04);}
+  .dw-swatch:hover{transform:scale(1.55);box-shadow:0 3px 10px rgba(0,0,0,.20),inset 0 0 0 1px rgba(0,0,0,.10);z-index:3;}
+  /* the pattern's REAL colors read as anchors: a refined HAIRLINE ink ring + a
+     slightly larger tile — no longer a heavy dark hoop that orphans them at high
+     dot counts. (refine 2026-07-09) */
+  .dw-swatch--base{width:22px;height:22px;border-radius:5px;box-shadow:0 0 0 1px #fff,0 0 0 1.5px rgba(61,66,70,.55),0 1px 3px rgba(0,0,0,.12),inset 0 -3px 6px rgba(0,0,0,.05);}
+  .dw-swatch--base:hover{transform:scale(1.4);box-shadow:0 0 0 1px #fff,0 0 0 1.5px rgba(61,66,70,.85),0 3px 10px rgba(0,0,0,.20);}
+  .dw-color-palette__bar{display:flex;height:4px;border-radius:3px;overflow:hidden;max-width:660px;margin:16px auto 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.04);}
   .dw-color-palette__bar span{display:block;height:100%;}
-  .dw-color-palette__legend{display:flex;flex-wrap:wrap;justify-content:center;gap:6px 14px;max-width:660px;margin:12px auto 0;}
-  .dw-color-palette__legend a,.dw-color-palette__legend span{display:inline-flex;align-items:center;gap:5px;font-family:Lora,serif;font-size:11px;color:#3D4246;text-decoration:none;text-transform:capitalize;}
-  .dw-color-palette__legend i{width:11px;height:11px;border-radius:2px;box-shadow:0 0 0 1px rgba(0,0,0,.12);}
+  .dw-color-palette__legend{display:flex;flex-wrap:wrap;justify-content:center;gap:6px 16px;max-width:660px;margin:13px auto 0;}
+  .dw-color-palette__legend a,.dw-color-palette__legend span{display:inline-flex;align-items:center;gap:6px;font-family:Lora,serif;font-size:11px;color:#3D4246;text-decoration:none;text-transform:capitalize;letter-spacing:.01em;}
+  .dw-color-palette__legend i{width:11px;height:11px;border-radius:3px;box-shadow:0 0 0 1px rgba(0,0,0,.12),inset 0 -2px 3px rgba(0,0,0,.05);}
   .dw-color-palette__legend a:hover{color:#000;text-decoration:underline;}
   .dw-color-palette:empty{display:none;}
   @media (max-width:600px){
@@ -147,20 +154,27 @@
     .dw-swatch{width:15px;height:15px;}
     .dw-swatch--base{width:19px;height:19px;}
   }
-  /* ── In-page color-index grid the dot brings up ── */
-  .dw-color-index{max-width:660px;margin:16px auto 4px;text-align:left;}
+  /* ── In-page color-index grid the dot brings up — refined to read as a curated
+     "shop this colorway" drawer: hairline-ruled header, Lora title at the palette
+     scale, soft reveal, even gutters, hairline media. (refine 2026-07-09) ── */
+  .dw-color-index{max-width:660px;margin:20px auto 6px;text-align:left;border-top:1px solid #ece9e3;padding-top:16px;}
   .dw-color-index[hidden]{display:none;}
-  .dw-color-index__head{display:flex;align-items:center;gap:9px;margin:0 0 12px;}
-  .dw-color-index__swatch{width:20px;height:20px;border-radius:4px;flex:0 0 auto;box-shadow:0 0 0 1px rgba(0,0,0,.14);}
-  .dw-color-index__label{font-family:Lora,serif;font-size:13px;color:#3D4246;flex:1 1 auto;}
-  .dw-color-index__label b{font-weight:600;}
-  .dw-color-index__close{flex:0 0 auto;background:none;border:1px solid #e0ded9;border-radius:50%;width:24px;height:24px;line-height:1;font-size:16px;color:#6b6b6b;cursor:pointer;padding:0;}
-  .dw-color-index__close:hover{color:#000;border-color:#b8b3a8;}
-  .dw-color-index__grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:12px;}
+  @keyframes dwCiReveal{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}}
+  .dw-color-index.is-open{animation:dwCiReveal .28s ease both;}
+  .dw-color-index__head{display:flex;align-items:center;gap:10px;margin:0 0 14px;}
+  .dw-color-index__swatch{width:22px;height:22px;border-radius:5px;flex:0 0 auto;box-shadow:0 0 0 1px rgba(0,0,0,.14),inset 0 -3px 5px rgba(0,0,0,.06);}
+  .dw-color-index__label{font-family:Lora,serif;font-size:14px;color:#3D4246;flex:1 1 auto;letter-spacing:.01em;}
+  .dw-color-index__label b{font-weight:600;text-transform:capitalize;}
+  .dw-color-index__close{flex:0 0 auto;background:none;border:1px solid #e0ded9;border-radius:50%;width:26px;height:26px;line-height:1;font-size:15px;color:#8a8577;cursor:pointer;padding:0;transition:color .12s ease,border-color .12s ease;}
+  .dw-color-index__close:hover{color:#3D4246;border-color:#b8b3a8;}
+  .dw-color-index__close:focus-visible{outline:2px solid #b8b3a8;outline-offset:2px;}
+  .dw-color-index__grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(122px,1fr));gap:16px 14px;}
   .dw-color-index__card{display:block;text-decoration:none;color:inherit;}
-  .dw-color-index__card img{width:100%;aspect-ratio:1;object-fit:cover;border:1px solid #e5e5e5;border-radius:3px;background:#f4f2ee;}
-  .dw-color-index__cap{display:block;font-family:Lora,serif;font-size:11px;color:#3D4246;margin-top:5px;line-height:1.25;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;text-transform:capitalize;}
-  .dw-color-index__cap i{display:block;font-style:normal;color:#8a8577;font-size:10px;text-transform:none;}
+  .dw-color-index__card img{width:100%;aspect-ratio:1;object-fit:cover;box-shadow:0 0 0 1px #ece9e3;border-radius:3px;background:#f4f2ee;transition:box-shadow .14s ease;}
+  .dw-color-index__card:hover img{box-shadow:0 0 0 1px #b8b3a8,0 3px 12px rgba(0,0,0,.10);}
+  .dw-color-index__cap{display:block;font-family:Lora,serif;font-size:11px;color:#3D4246;margin-top:7px;line-height:1.3;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;text-transform:capitalize;}
+  .dw-color-index__card:hover .dw-color-index__cap{text-decoration:underline;text-decoration-color:#d8d4cc;}
+  .dw-color-index__cap i{display:block;font-style:normal;color:#8a8577;font-size:10px;text-transform:none;margin-top:1px;letter-spacing:.02em;}
   .dw-color-index__empty,.dw-color-index__loading{font-family:Lora,serif;font-size:12px;color:#8a8577;padding:8px 0;}
   @media (max-width:600px){
     .dw-color-index__grid{grid-template-columns:repeat(auto-fill,minmax(96px,1fr));gap:8px;}
@@ -285,10 +299,12 @@
       return _ciCache[hex];
     }
     function ciEl(sel){ return CI ? CI.querySelector(sel) : null; }
-    function closeIndex(){ if(CI){ CI.hidden=true; } }
+    function closeIndex(){ if(CI){ CI.classList.remove('is-open'); CI.hidden=true; } }
     if(CI){
       var _cx=ciEl('[data-dw-ci-close]');
       if(_cx) _cx.addEventListener('click', closeIndex);
+      // Esc closes the drawer when it (or something inside it) is focused (a11y).
+      CI.addEventListener('keydown', function(ev){ if(ev.key==='Escape'){ closeIndex(); } });
     }
     /* Open the in-page index for a clicked hex. */
     function openIndex(hex, name){
@@ -299,7 +315,10 @@
       if(lbl) lbl.innerHTML='Wallcoverings in this color — <b>'+(name||fam||hex)+'</b>';
       if(grid) grid.innerHTML='<div class="dw-color-index__loading">Finding matches…</div>';
       CI.hidden=false;
+      // retrigger the soft reveal even on a re-click (refine 2026-07-09)
+      CI.classList.remove('is-open'); void CI.offsetWidth; CI.classList.add('is-open');
       CI.scrollIntoView({behavior:'smooth', block:'nearest'});
+      var _cl=ciEl('[data-dw-ci-close]'); if(_cl){ try{ _cl.focus({preventScroll:true}); }catch(e){} }
       fetchColorIndex(hex).then(function(d){
         if(!grid) return;
         var res=(d&&d.results)||[];
@@ -347,6 +366,30 @@
       var r=Math.round(ca[0]+(cb[0]-ca[0])*t),g=Math.round(ca[1]+(cb[1]-ca[1])*t),bl=Math.round(ca[2]+(cb[2]-ca[2])*t);
       return '#'+[r,g,bl].map(function(x){return x.toString(16).padStart(2,'0');}).join('');
     }
+    /* ── perceptual ΔE (CIELAB, ΔE76) for VISUAL dedup of adjacent interpolated dots.
+       On a low-base, low-chroma pattern (e.g. an all-grey paisley) cranking the slider
+       high produces long runs of visually-identical adjacent swatches — meaningless
+       and it cheapens the curated feel. We skip an INTERPOLATED dot when it is within
+       ΔE < DEDUP_DE of the previously-placed swatch. Base (real) colors are NEVER
+       deduped — they're the pattern's actual colors. (refine 2026-07-09) */
+    function _rgb2lab(rgb){
+      var r=rgb[0]/255,g=rgb[1]/255,b=rgb[2]/255;
+      r=r>0.04045?Math.pow((r+0.055)/1.055,2.4):r/12.92;
+      g=g>0.04045?Math.pow((g+0.055)/1.055,2.4):g/12.92;
+      b=b>0.04045?Math.pow((b+0.055)/1.055,2.4):b/12.92;
+      var x=(r*0.4124+g*0.3576+b*0.1805)/0.95047,
+          y=(r*0.2126+g*0.7152+b*0.0722)/1.0,
+          z=(r*0.0193+g*0.1192+b*0.9505)/1.08883;
+      var f=function(t){return t>0.008856?Math.pow(t,1/3):(7.787*t)+16/116;};
+      x=f(x);y=f(y);z=f(z);
+      return [(116*y)-16, 500*(x-y), 200*(y-z)];
+    }
+    function deltaE(hexA,hexB){
+      var a=hexToRgb(hexA),b=hexToRgb(hexB); if(!a||!b) return 999;
+      var la=_rgb2lab(a),lb=_rgb2lab(b);
+      return Math.sqrt(Math.pow(la[0]-lb[0],2)+Math.pow(la[1]-lb[1],2)+Math.pow(la[2]-lb[2],2));
+    }
+    var DEDUP_DE = 2.3;   // ~1 JND: below this, two swatches are indistinguishable
     /* TOTAL rendered dots are driven by a user DENSITY SLIDER (Steve 2026-07-09d:
        "allow the user to grid slide the amount of dots that they can see..
        expand.. contract"). The slider value is the target TOTAL dot count. We keep
@@ -425,13 +468,22 @@
       var list = _baseList;
       var stepsPerGap = buildSteps(list.length, dotTarget);
       var frag=document.createDocumentFragment();
+      var _lastHex=null;   // last hex actually appended, for adjacent visual dedup
       for(var i=0;i<list.length;i++){
+        // BASE colors are always shown (real pattern colors, never deduped).
         frag.appendChild(makeSwatch(list[i].hex,{base:true,name:list[i].name,pct:list[i].pct}));
+        _lastHex=list[i].hex;
         if(i<list.length-1){
           var steps = stepsPerGap[i] || 0;
           for(var k=1;k<=steps;k++){
             var t=k/(steps+1);
-            frag.appendChild(makeSwatch(mix(list[i].hex,list[i+1].hex,t),{base:false}));
+            var ih=mix(list[i].hex,list[i+1].hex,t);
+            // skip an interpolated dot that's visually identical to the previous one
+            // (dedup near-duplicates on low-chroma / low-base palettes). We still
+            // guarantee the NEXT base color renders because bases bypass this check.
+            if(_lastHex && deltaE(ih,_lastHex) < DEDUP_DE) continue;
+            frag.appendChild(makeSwatch(ih,{base:false}));
+            _lastHex=ih;
           }
         }
       }
@@ -471,6 +523,11 @@
         if(plus)  plus.disabled  = true;
         return;
       }
+      // Multi-color path: REVEAL the control (refine 2026-07-09 — critical live bug:
+      // the density slider markup ships with the `hidden` attr and this branch never
+      // cleared it, so the whole expand/contract control was invisible on every
+      // normal >1-color product). Unhide it here.
+      wrap.hidden = false;
       wrap.classList.remove('is-single');
       wrap.removeAttribute('title');
       range.disabled = false;

← 5718e91a fix(sync): use global fetch in sync-shopify-products — daily  ·  back to Designer Wallcoverings  ·  auto-save: 2026-07-09T16:11:12 (9 files) — pending-approval/ ed16c8e2 →