[object Object]

← back to Designer Wallcoverings

auto-save: 2026-07-09T09:39:56 (5 files) — pending-approval/boost-filter-consolidation-2026-06-25 pending-approval/color-dot-index-10pct-tolerance-2026-07-09/MEMO.md pending-approval/color-dot-index-10pct-tolerance-2026-07-09/push-to-live.sh shopify/theme-LIVE-591/snippets/color-palette.liquid vendor-scrapers/china-seas-refresh

f4e6d315741b560eaac1487e5d667949127bcb71 · 2026-07-09 09:39:59 -0700 · Steve Abrams

Files touched

Diff

commit f4e6d315741b560eaac1487e5d667949127bcb71
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 9 09:39:59 2026 -0700

    auto-save: 2026-07-09T09:39:56 (5 files) — pending-approval/boost-filter-consolidation-2026-06-25 pending-approval/color-dot-index-10pct-tolerance-2026-07-09/MEMO.md pending-approval/color-dot-index-10pct-tolerance-2026-07-09/push-to-live.sh shopify/theme-LIVE-591/snippets/color-palette.liquid vendor-scrapers/china-seas-refresh
---
 .../MEMO.md                                        | 136 -------------
 .../push-to-live.sh                                |  55 ------
 .../theme-LIVE-591/snippets/color-palette.liquid   | 212 ++++++++++++++++++++-
 3 files changed, 202 insertions(+), 201 deletions(-)

diff --git a/pending-approval/color-dot-index-10pct-tolerance-2026-07-09/MEMO.md b/pending-approval/color-dot-index-10pct-tolerance-2026-07-09/MEMO.md
deleted file mode 100644
index 87e26f84..00000000
--- a/pending-approval/color-dot-index-10pct-tolerance-2026-07-09/MEMO.md
+++ /dev/null
@@ -1,136 +0,0 @@
-# PENDING APPROVAL — Color-dot click → in-page INDEX of many products within 10% tolerance
-
-**Date:** 2026-07-09
-**Owner:** vp-dw-commerce
-**Status:** committed locally, verified, STAGED — NOT pushed. Two Steve-gated steps.
-**Supersedes behavior of:** `DONE-color-dot-specific-product-deeplink-2026-07-09/`
-(the single-product deeplink, commit 6ba5016). This replaces "click → ONE product"
-with "click → GRID of MANY products in that color."
-
-## Steve's feedback (verbatim)
-> "bring up an index of many images, not just that exact hex. tolerance 10% for color to pull more"
-
-## What it does (before → after)
-- **BEFORE (6ba5016, live):** clicking a color dot deep-linked to the ONE nearest
-  specific product+variant of that hue and navigated away.
-- **AFTER (staged):** clicking a color dot opens an **in-page expanding grid**
-  right below the palette — an index of MANY products (up to 36) whose dominant
-  color is within a perceptual **10% tolerance (CIELAB ΔE76)** of the clicked hex,
-  drawn **CATALOG-WIDE** (not one collection). Header shows the clicked color +
-  a close (×); cards are image + pattern + vendor, each linking to
-  `/products/<handle>` (the roll variant — **never** the $4.25 Sample). Graceful
-  empty/error states. No navigation away (matches the CLIP-strip pattern).
-- **Unchanged / intact:** the swatch hex fill, the interpolated spectrum, the
-  legend + proportion bar, and the separate CLIP **"More Wallcoverings Like This"**
-  strip. Family-collection href kept as the plain `href` so middle-click / new-tab
-  / crawlers still get a valid destination.
-
-## Data source (proof) + why
-**Catalog-wide color-index endpoint** (the source the task explicitly prefers),
-NOT the narrow one-collection products.json client sample.
-
-- New endpoint `POST /apps/color-index` on the dwphoto app (:9912), alongside the
-  existing `/apps/similar`. Same public-but-tightly-bounded posture (CORS
-  allowlist to the storefront origins, input allowlist `{hex,k}` only, output
-  allowlist of public catalog fields).
-- Backed by a static `data/color-index.json` — **one dominant color per ACTIVE
-  product with a PRECOMPUTED CIELAB LAB** — built from `dw_unified.product_colors`
-  (which already stores `lab_l/lab_a/lab_b`) by
-  `dw-photo-capture/scripts/build-color-index.cjs`. **60,960 products** in the
-  index. No DB at request time; the endpoint loads the index once and hot-reloads
-  on mtime change. `$0 (local)`.
-- Why static-JSON not live-PG: the dwphoto prod app carries no `pg` driver and is
-  intentionally dependency-light; the static index keeps it that way while still
-  being genuinely catalog-wide. The generator runs where PG lives (or with
-  `NODE_PATH` pointed at a node_modules that has `pg`).
-
-## The 10% perceptual metric (named tunable constant)
-- ONE knob: `COLOR_INDEX_TOLERANCE_PCT = 0.10` (in `server.js`).
-- Mapped to a **ΔE76 ceiling** = `TOLERANCE_PCT × 100 = 10`, where 100 is the
-  practical perceptual ΔE range for two clearly-different colors. So "10%" = a
-  ΔE76 ceiling of 10 — tight enough to stay same-family, loose enough to pull
-  MANY products. Retune by changing the one constant; the ceiling derives from it.
-- Perceptual (CIELAB), not raw hex/RGB math, as required. Product LAB is
-  precomputed in `product_colors`; the clicked hex is converted sRGB→LAB in JS
-  (`hexToLab`, matching the upstream computation).
-
-## Local verification (all $0 local)
-- `node --check` on `server.js`, `build-color-index.cjs`, and BOTH extracted
-  `<script>` blocks of the snippet → all OK.
-- `shopify theme check` on `color-palette.liquid` → **5 warnings, all pre-existing**
-  (OrphanedSnippet, 3× VariableName on the CLIP block's liquid vars, DeprecatedFilter
-  `img_url` on the untouched featured_image line). **0 new offenses, 0 errors** —
-  same baseline as the prior deeplink task.
-- Endpoint smoke: forest `#3C5B49`→97 in tol, blush `#DE5D83`→80, gold `#D4AF37`→41,
-  navy `#1B2A4A`→235; bad-input/405/CORS-preflight all correct.
-- **Headless click proof (puppeteer, real snippet markup+JS over http):** clicked
-  the forest-green base dot on a rendered palette → the in-page grid opened with
-  **36 product cards**, ALL linking to `/products/<handle>` (roll, no Sample),
-  spanning 7+ vendors (Phillipe Romano, Boråstapeter, Sandberg, Hollywood, Kravet,
-  Coordonné, Rebel Walls, Lee Jofa Modern). Zero console errors in the flow.
-
-### Tolerance proof (clicked hex → resolved product hexes + ΔE + count)
-Clicked **#3C5B49** → `total_in_tolerance=97` (ΔE≤10), grid shows nearest-first:
-
-| product hex | ΔE from clicked | vendor | title |
-|---|---|---|---|
-| #3c5b49 | 0.0 | Phillipe Romano | Crushed Coral - Forest |
-| #3f5f4d | 1.6 | Boråstapeter | Umbrella Leaves |
-| #375f50 | 3.6 | Sandberg | Igor jade |
-| #445d4f | 3.8 | Hollywood | Ashbourne Type II Vinyl |
-| #436354 | 3.9 | Sandberg | Raphael green |
-| #3f594d | 4.3 | Kravet Design | W3863-30 Sage |
-| #3a5447 | 4.4 | Coordonné | Cherry Blossom Emerald |
-| #35543c | 5.0 | Rebel Walls | Opulence, Jade |
-| … | … | … | (max ΔE among 36 returned = 6.3, all ≤ ceiling 10) |
-
-## Commit hashes
-- dwphoto (endpoint + generator): `602bb7f` in `~/Projects/dw-photo-capture`
-  (`server.js`, `scripts/build-color-index.cjs`, `.gitignore`). The 16MB generated
-  `data/color-index.json` is gitignored — rebuild it, don't commit it.
-- shopify theme (snippet): `7b0ae3d` in `~/Projects/Designer-Wallcoverings/shopify`
-  (`theme-LIVE-591-freshpull-20260709/snippets/color-palette.liquid` +
-  `theme-LIVE-591/snippets/color-palette.liquid`).
-
-## TWO Steve-gated ship steps
-
-### STEP 1 (required first) — deploy the color-index endpoint publicly
-The theme grid fetches `https://photo.designerwallcoverings.com/apps/color-index`.
-That endpoint is built + verified LOCAL only. To go live it must (a) exist on the
-public dwphoto host and (b) have a freshly generated index there:
-
-```bash
-# on the box that serves photo.designerwallcoverings.com (dwphoto :9912):
-#   1. pull the new server.js + scripts/build-color-index.cjs (commit 602bb7f)
-#   2. generate the index where PG (dw_unified) is reachable:
-cd <dwphoto-dir> && node scripts/build-color-index.cjs      # writes data/color-index.json
-#   3. restart dwphoto so the new /apps/color-index route + index load:
-pm2 restart dwphoto   # (or the process manager in use)
-#   4. smoke test:
-curl -s -X POST https://photo.designerwallcoverings.com/apps/color-index \
-  -H 'Content-Type: application/json' -d '{"hex":"#3C5B49","k":6}' | head
-```
-Also schedule `build-color-index.cjs` (e.g. nightly) so the index tracks catalog
-changes. Keep loopback-only services loopback; this endpoint is the only public
-surface added.
-
-### STEP 2 — push the theme snippet to the live theme
-Only ship AFTER Step 1 is live (otherwise the grid shows the graceful
-"could not load matches" empty state). Equivalent to the prior deeplink push:
-
-```bash
-cd ~/Projects/Designer-Wallcoverings/shopify/theme-LIVE-591-freshpull-20260709
-SHOPIFY_CLI_THEME_TOKEN=$SHOPIFY_THEME_TOKEN \
-  shopify theme push --allow-live --theme 144076931123 \
-  --store designer-laboratory-sandbox.myshopify.com \
-  --only snippets/color-palette.liquid
-```
-Or run the ready script (backs up the current live snippet to
-`shopify/theme-backups/` first, then PUTs via Asset API):
-```bash
-bash ~/Projects/Designer-Wallcoverings/pending-approval/color-dot-index-10pct-tolerance-2026-07-09/push-to-live.sh
-```
-
-**Revert:** PUT the `theme-backups/color-palette.liquid.LIVE-144076931123.<ts>.bak`
-back to `snippets/color-palette.liquid` on theme 144076931123 (restores the
-current live deeplink behavior).
diff --git a/pending-approval/color-dot-index-10pct-tolerance-2026-07-09/push-to-live.sh b/pending-approval/color-dot-index-10pct-tolerance-2026-07-09/push-to-live.sh
deleted file mode 100755
index 2fddc4c8..00000000
--- a/pending-approval/color-dot-index-10pct-tolerance-2026-07-09/push-to-live.sh
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env bash
-# STEVE-GATED — STEP 2 of 2. Push the color-palette "index of many" fix to LIVE.
-# Ships snippets/color-palette.liquid ONLY. Backs up the current live version first.
-#
-# ⚠️ RUN STEP 1 FIRST: the /apps/color-index endpoint must be LIVE on
-# photo.designerwallcoverings.com (deploy dwphoto commit 602bb7f + generate the
-# index + restart) or the in-page grid shows the graceful empty state. See MEMO.md.
-#
-# Change: a color-dot click now opens an IN-PAGE grid of MANY products whose
-# dominant color is within 10% (CIELAB deltaE76) of the clicked hex, catalog-wide,
-# instead of deep-linking to ONE product. (Steve 2026-07-09b)
-set -euo pipefail
-
-STORE="designer-laboratory-sandbox.myshopify.com"
-API="2024-10"
-TID="144076931123"   # live theme 5.9.1 [Dev] hero-appt-CTA
-SRC="$HOME/Projects/Designer-Wallcoverings/shopify/theme-LIVE-591-freshpull-20260709/snippets/color-palette.liquid"
-SECRETS="$HOME/Projects/secrets-manager/.env"
-
-# quick pre-flight: warn (don't block) if the public endpoint isn't answering yet
-if ! curl -s -m 8 -X POST "https://photo.designerwallcoverings.com/apps/color-index" \
-     -H 'Content-Type: application/json' -d '{"hex":"#3C5B49","k":1}' | grep -q '"ok"'; then
-  echo "⚠️  WARNING: /apps/color-index is not returning ok yet — run STEP 1 (deploy dwphoto) first,"
-  echo "    or the grid will show the graceful empty state. Continuing in 5s (Ctrl-C to abort)…"
-  sleep 5
-fi
-
-TOK="$(grep -hE '^SHOPIFY_THEME_TOKEN=' "$SECRETS" | head -1 | cut -d= -f2- | tr -d '"'"'"'')"
-[ -z "$TOK" ] && { echo "no theme token"; exit 1; }
-[ -f "$SRC" ] || { echo "source snippet missing: $SRC"; exit 1; }
-echo "token …${TOK: -4} | live theme $TID"
-
-BK="$HOME/Projects/Designer-Wallcoverings/shopify/theme-backups/color-palette.liquid.LIVE-$TID.$(date +%Y%m%d-%H%M%S).bak"
-mkdir -p "$(dirname "$BK")"
-curl -s "https://$STORE/admin/api/$API/themes/$TID/assets.json?asset%5Bkey%5D=snippets/color-palette.liquid" \
-  -H "X-Shopify-Access-Token: $TOK" \
-  | python3 -c "import sys,json;open('$BK','w').write(json.load(sys.stdin)['asset']['value'])"
-echo "backup: $BK ($(wc -c < "$BK") bytes)"
-
-# PUT the new snippet
-python3 - "$STORE" "$API" "$TID" "$TOK" "$SRC" <<'PY'
-import sys, json, urllib.request
-store, api, tid, tok, src = sys.argv[1:6]
-val = open(src).read()
-body = json.dumps({"asset": {"key": "snippets/color-palette.liquid", "value": val}}).encode()
-r = urllib.request.Request(f"https://{store}/admin/api/{api}/themes/{tid}/assets.json",
-                           data=body, method="PUT",
-                           headers={"X-Shopify-Access-Token": tok, "Content-Type": "application/json"})
-resp = urllib.request.urlopen(r)
-print("PUT", resp.status, "OK", f"({len(val)} bytes)")
-PY
-
-echo "verify on any PDP with a color palette:"
-echo "  click a color dot → an in-page grid of many products in that color opens below the palette"
-echo "revert: PUT $BK back to snippets/color-palette.liquid on theme $TID"
diff --git a/shopify/theme-LIVE-591/snippets/color-palette.liquid b/shopify/theme-LIVE-591/snippets/color-palette.liquid
index 33057aac..1ac4d6b5 100644
--- a/shopify/theme-LIVE-591/snippets/color-palette.liquid
+++ b/shopify/theme-LIVE-591/snippets/color-palette.liquid
@@ -1,9 +1,20 @@
 {% comment %}
   color-palette — "Colors In This Pattern" swatch spectrum.
   Shows the pattern's real colors AND ~20 interpolated colors between each pair,
-  laid out as uniform swatches centered in even rows. Every swatch (real OR
-  interpolated) links to its color-FAMILY collection, derived from the measured
-  hex — so there is never a dead/unlinked swatch. (Steve 2026-07-08)
+  laid out as uniform swatches centered in even rows.
+
+  DOT CLICK TARGET (Steve 2026-07-09b): "bring up an index of many images, not
+  just that exact hex. tolerance 10% for color to pull more." A swatch click now
+  opens an IN-PAGE expanding grid of MANY products whose dominant color is within
+  a perceptual 10% tolerance (CIELAB ΔE76) of the clicked hex — CATALOG-WIDE, not
+  one collection. The grid is served by the color-index endpoint
+    POST https://photo.designerwallcoverings.com/apps/color-index  {hex, k}
+  (dwphoto → data/color-index.json, one dominant color per ACTIVE product with
+  precomputed LAB). Cards link to /products/<handle> (the roll variant, never the
+  $4.25 Sample). The family-collection href stays as the plain href so
+  middle-click / new-tab / crawlers still get a valid destination, and is the
+  graceful fallback if the endpoint is unreachable.
+  (was: Steve 2026-07-09a, a click deep-linked to the ONE nearest product+variant.)
 
   Data tiers (mirrors color-dots.liquid, kept in sync):
     Tier 1  custom.color_dots_json / custom.color_details  (enriched JSON: [{hex,name,pct}])
@@ -39,6 +50,19 @@
 >
   <p class="dw-color-palette__title">Colors In This Pattern</p>
   <div class="dw-color-palette__cards" data-dw-color-palette-cards aria-live="polite"></div>
+
+  {%- comment -%} In-page color-index grid the dot brings up (Steve 2026-07-09b).
+    Hidden until a swatch is clicked; then filled with the catalog-wide products
+    within 10% (CIELAB ΔE76) of the clicked hex. {%- endcomment -%}
+  <div class="dw-color-index" data-dw-color-index hidden
+       data-endpoint="https://photo.designerwallcoverings.com/apps/color-index">
+    <div class="dw-color-index__head">
+      <span class="dw-color-index__swatch" data-dw-ci-swatch></span>
+      <span class="dw-color-index__label" data-dw-ci-label aria-live="polite"></span>
+      <button type="button" class="dw-color-index__close" data-dw-ci-close aria-label="Close color index">&times;</button>
+    </div>
+    <div class="dw-color-index__grid" data-dw-ci-grid aria-live="polite"></div>
+  </div>
 </div>
 
 <style>
@@ -66,6 +90,24 @@
     .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;}
+  .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;}
+  .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__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;}
+  }
 </style>
 
 <script>
@@ -149,14 +191,95 @@
       return 'pink';
     }
     var PRODUCT_STYLE = (root.dataset.productStyle||'').trim();
-    function familyHref(hex){
+    /* The color-FAMILY collection handle for a hex (used both as the search space
+       for the nearest-specific-product resolver AND as the graceful fallback). */
+    function familyHandle(hex){
       var fam=familyFromHex(hex); if(!fam) return null;
-      var handle=FAM_COLL[fam]; if(!handle) return null;
+      return FAM_COLL[fam] || null;
+    }
+    /* Fallback collection URL — only used if we can't resolve a single specific
+       product of that hue (edge case). (Steve 2026-07-09: DEFAULT must be a
+       specific product+variant, this is the graceful floor.) */
+    function familyHref(hex){
+      var handle=familyHandle(hex); if(!handle) return null;
       var url='/collections/'+handle+'?sort_by=created-descending';
       if(PRODUCT_STYLE){ url+='&filter.p.m.custom.style='+encodeURIComponent(PRODUCT_STYLE); }
       return url;
     }
 
+    /* ── hex → IN-PAGE INDEX of MANY products within 10% color tolerance ───────
+       Steve 2026-07-09b: "bring up an index of many images, not just that exact
+       hex. tolerance 10% for color to pull more." A dot is a palette-extracted
+       hue; clicking it opens the in-page grid (below the palette) of products
+       whose dominant color is within a perceptual 10% tolerance (CIELAB ΔE76) of
+       the clicked hex, drawn CATALOG-WIDE from the color-index endpoint. Results
+       are cached per hex so re-clicking a swatch is instant. */
+    var CI = root.querySelector('[data-dw-color-index]');
+    var CI_ENDPOINT = (CI && CI.dataset.endpoint) || 'https://photo.designerwallcoverings.com/apps/color-index';
+    var CI_K = 36;                 // cards to show (endpoint caps at 60)
+    var _ciCache = {};             // hex -> Promise<{results,total,tolerance_pct}>
+    function fetchColorIndex(hex){
+      if(_ciCache[hex]) return _ciCache[hex];
+      _ciCache[hex]=fetch(CI_ENDPOINT,{
+        method:'POST', mode:'cors', headers:{'Content-Type':'application/json'},
+        body:JSON.stringify({hex:hex, k:CI_K})
+      }).then(function(r){ return r.ok?r.json():{results:[]}; })
+        .catch(function(){ return {results:[], _err:true}; });
+      return _ciCache[hex];
+    }
+    function ciEl(sel){ return CI ? CI.querySelector(sel) : null; }
+    function closeIndex(){ if(CI){ CI.hidden=true; } }
+    if(CI){
+      var _cx=ciEl('[data-dw-ci-close]');
+      if(_cx) _cx.addEventListener('click', closeIndex);
+    }
+    /* Open the in-page index for a clicked hex. */
+    function openIndex(hex, name){
+      if(!CI) return;
+      var sw=ciEl('[data-dw-ci-swatch]'), lbl=ciEl('[data-dw-ci-label]'), grid=ciEl('[data-dw-ci-grid]');
+      var fam=familyFromHex(hex)||'';
+      if(sw) sw.style.background=hex;
+      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;
+      CI.scrollIntoView({behavior:'smooth', block:'nearest'});
+      fetchColorIndex(hex).then(function(d){
+        if(!grid) return;
+        var res=(d&&d.results)||[];
+        if(!res.length){
+          grid.innerHTML='';
+          var empty=document.createElement('div'); empty.className='dw-color-index__empty';
+          empty.textContent = d && d._err
+            ? 'Could not load matches right now — try another color.'
+            : 'No close matches for this color yet.';
+          grid.appendChild(empty);
+          return;
+        }
+        grid.innerHTML='';
+        var frag=document.createDocumentFragment();
+        res.forEach(function(p){
+          if(!p || !p.handle) return;
+          var a=document.createElement('a'); a.className='dw-color-index__card'; a.rel='nofollow';
+          // link straight to the PDP handle — the roll/first-available variant,
+          // never the $4.25 Sample (Sample is never the default variant).
+          a.href='/products/'+encodeURIComponent(p.handle);
+          var img=document.createElement('img'); img.loading='lazy';
+          img.src=p.image||''; img.alt=p.title||'';
+          var cap=document.createElement('span'); cap.className='dw-color-index__cap';
+          cap.textContent=(p.title||'').replace(/\s*\|\s*.*$/,'');   // pattern part
+          if(p.vendor){ var v=document.createElement('i'); v.textContent=p.vendor; cap.appendChild(v); }
+          a.appendChild(img); a.appendChild(cap); frag.appendChild(a);
+        });
+        grid.appendChild(frag);
+      });
+    }
+    /* Click handler shared by every swatch: open the in-page index for that hue.
+       Modified clicks (new tab) fall through to the plain family-collection href. */
+    function onSwatchClick(hex, ev, name){
+      ev.preventDefault();
+      openIndex(hex, name);
+    }
+
     /* ── color math: normalize + interpolate ── */
     function normHex(hex){
       var rgb=hexToRgb(hex); if(!rgb) return null;
@@ -169,20 +292,30 @@
     }
     var STEPS = 20; /* interpolated colors served BETWEEN each pair (Steve 2026-07-08) */
 
-    /* Build one swatch. Every swatch links to its hex-derived family collection. */
+    /* Build one swatch. Left-click opens the IN-PAGE index of many products
+       within 10% of that hue (onSwatchClick → openIndex); the href is the family
+       collection so middle-click / new-tab / crawlers still get a valid
+       destination. (Steve 2026-07-09b: "bring up an index of many images.") */
     function makeSwatch(hex, opts){
       opts=opts||{};
       var href=familyHref(hex);
       var el=document.createElement(href?'a':'span');
       el.className='dw-swatch'+(opts.base?' dw-swatch--base':'');
       el.style.background=hex;
-      if(href) el.href=href;
+      if(href){
+        el.href=href;
+        el.addEventListener('click', (function(nm){ return function(ev){
+          // let modified clicks (new tab / new window) use the plain-collection href
+          if(ev.metaKey||ev.ctrlKey||ev.shiftKey||ev.altKey||ev.button!==0) return;
+          onSwatchClick(hex, ev, nm);
+        }; })(opts.name));
+      }
       var fam=familyFromHex(hex);
       if(opts.base){
         var lbl=(opts.name?opts.name:(fam||''));
-        el.title='Shop '+(fam||'this')+' wallcoverings'+(lbl&&lbl!==fam?' — '+lbl:'')+' ('+hex+(opts.pct!=null?', '+opts.pct+'% here':'')+')';
+        el.title='Shop this '+(fam||'')+' color'+(lbl&&lbl!==fam?' — '+lbl:'')+' ('+hex+(opts.pct!=null?', '+opts.pct+'% here':'')+')';
       } else {
-        el.title='Shop '+(fam||'this')+' wallcoverings ('+hex+')';
+        el.title='Shop this '+(fam||'')+' color ('+hex+')';
       }
       return el;
     }
@@ -223,7 +356,13 @@
         named.forEach(function(c){
           var href=familyHref(c.hex);
           var it=document.createElement(href?'a':'span');
-          if(href) it.href=href;
+          if(href){
+            it.href=href;
+            (function(hx, nm){ it.addEventListener('click', function(ev){
+              if(ev.metaKey||ev.ctrlKey||ev.shiftKey||ev.altKey||ev.button!==0) return;
+              onSwatchClick(hx, ev, nm);
+            }); })(c.hex, c.name);
+          }
           it.innerHTML='<i style="background:'+c.hex+'"></i>'+c.name+(c.pct!=null?' <b style="font-weight:600;color:#8a8577">'+c.pct+'%</b>':'');
           leg.appendChild(it);
         });
@@ -295,3 +434,56 @@
   if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',init);}else{init();}
 })();
 </script>
+
+{%- comment -%} CLIP "More like this" — PDP color-dot CLIP visual-similarity row.
+  Calls the SAME-BEHAVIOR public proxy /apps/similar (dwphoto → loopback CLIP :9914),
+  which returns the visually-nearest patterns to THIS product, softly re-ranked by the
+  same hex→color-family bucket + style overlap the swatches use. Additive: the swatch
+  spectrum + its color-family links above are unchanged. dw_sku / hex / style come from
+  the product's REAL fields. (Steve "ship the CLIP dots" 2026-07-08) {%- endcomment -%}
+{%- assign _dw_sku = product.selected_or_first_available_variant.sku | default: product.variants.first.sku | remove: '-Sample' -%}
+{%- assign _dw_hex = product.metafields.custom.primary_color_hex.value | default: product.metafields.custom.color_hex.value | default: product.metafields.dwc.dominant_hex.value -%}
+{%- assign _dw_style = product.metafields.custom.style.value | default: product.metafields.global.Style.value | default: product.type -%}
+{%- if _dw_sku != blank -%}
+<div id="dw-similar" class="dw-similar"
+     data-dw-sku="{{ _dw_sku | escape }}"
+     data-hex="{{ _dw_hex | escape }}"
+     data-style="{{ _dw_style | escape }}"
+     data-endpoint="https://photo.designerwallcoverings.com/apps/similar">
+  <h3 class="dw-similar__h">More Wallcoverings Like This</h3>
+  <div class="dw-similar__grid" aria-live="polite"></div>
+</div>
+<script>
+(function(){
+  var root=document.getElementById('dw-similar'); if(!root) return;
+  var sku=(root.dataset.dwSku||'').trim().replace(/-Sample$/i,'');  // base SKU keys the CLIP index; the Sample variant SKU (usually first-available) has no embedding
+  if(!sku){ root.style.display='none'; return; }
+  var ep=root.dataset.endpoint||'https://photo.designerwallcoverings.com/apps/similar';
+  fetch(ep,{method:'POST',mode:'cors',headers:{'Content-Type':'application/json'},
+    body:JSON.stringify({dw_sku:sku, hex:root.dataset.hex||'', style:root.dataset.style||'', k:8})})
+   .then(function(r){return r.ok?r.json():{results:[]};})
+   .then(function(d){
+     var g=root.querySelector('.dw-similar__grid');
+     if(!d.results||!d.results.length){ root.style.display='none'; return; }
+     d.results.forEach(function(p){
+       var a=document.createElement('a'); a.className='dw-similar__card'; a.rel='nofollow';
+       // real PDP link when we have a storefront handle; else fall back to a SKU search (never a name-search)
+       a.href = p.handle ? ('/products/'+encodeURIComponent(p.handle))
+                         : ('/search?q='+encodeURIComponent(p.dw_sku||'')+'&type=product');
+       var img=document.createElement('img'); img.loading='lazy'; img.src=p.image||''; img.alt=p.pattern||'';
+       var cap=document.createElement('span'); cap.className='dw-similar__cap'; cap.textContent=p.pattern||'';
+       a.appendChild(img); a.appendChild(cap); g.appendChild(a);
+     });
+   }).catch(function(){ root.style.display='none'; });
+})();
+</script>
+<style>
+.dw-similar{margin:20px auto 8px;max-width:660px;text-align:center}
+.dw-similar__h{font-family:Lora,serif;font-size:15px;font-weight:500;color:#3D4246;letter-spacing:-0.01em;margin:0 0 12px}
+.dw-similar__grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px}
+.dw-similar__card{display:block;text-decoration:none;color:inherit}
+.dw-similar__card img{width:100%;aspect-ratio:1;object-fit:cover;border:1px solid #e5e5e5;border-radius:3px}
+.dw-similar__cap{display:block;font-family:Lora,serif;font-size:11px;color:#3D4246;margin-top:5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-transform:capitalize}
+.dw-similar:empty{display:none}
+</style>
+{%- endif -%}

← 0da7a73d ship: color-dot 10% CIELAB tolerance grid — STEP 1 (dwphoto  ·  back to Designer Wallcoverings  ·  auto-save: 2026-07-09T10:10:01 (5 files) — pending-approval/ 3816d8b2 →