[object Object]

← back to Designer Wallcoverings

WS-2 grouped collection grid (owns render, overrides Boost) + WS-3 pattern page + similar-designs section (staged, not pushed)

6d1e37bdbc78f5332f160c591ef654e56be2201d · 2026-06-26 00:17:02 -0700 · Steve Abrams

Files touched

Diff

commit 6d1e37bdbc78f5332f160c591ef654e56be2201d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jun 26 00:17:02 2026 -0700

    WS-2 grouped collection grid (owns render, overrides Boost) + WS-3 pattern page + similar-designs section (staged, not pushed)
---
 shopify/_cwGRID/sections/collection-grouped.liquid | 301 +++++++++++++++++++++
 shopify/_cwGRID/sections/pattern-page.liquid       | 166 ++++++++++++
 shopify/_cwGRID/sections/similar-designs.liquid    | 219 +++++++++++++++
 shopify/_cwGRID/templates/page.pattern.json        |  16 ++
 4 files changed, 702 insertions(+)

diff --git a/shopify/_cwGRID/sections/collection-grouped.liquid b/shopify/_cwGRID/sections/collection-grouped.liquid
new file mode 100644
index 00000000..1243d670
--- /dev/null
+++ b/shopify/_cwGRID/sections/collection-grouped.liquid
@@ -0,0 +1,301 @@
+{%- comment -%}
+  ============================================================================
+  WS-2 — GROUPED COLLECTION GRID  ·  "one tile per pattern (collapse colorways)"
+  ============================================================================
+  Owner: vp-dw-commerce · DTD-committed Option-A (pattern_id) · Steve ruled
+  directly: "OVERRIDE BOOST IF NEEDED." This section is the override.
+
+  ── HOW THIS OVERRIDES / COEXISTS WITH BOOST SD ────────────────────────────
+  The live DW collection page is rendered by the Boost SD app block
+  (shopify://apps/boost-ai-search-filter/.../product-filter). Boost paints the
+  product grid CLIENT-SIDE from its OWN feed, one card per VARIANT/PRODUCT —
+  it cannot collapse colorways into one tile, and its plan tier does not expose
+  a reliable grouped-grid mode. A page-local MutationObserver dedup is NOT
+  acceptable: counts and pagination break across Boost's infinite pages.
+
+  So WE OWN THE GROUPED RENDER. This section:
+    1. Renders its OWN grid from OUR pattern_id-grouped feed
+       (https://pairs.designerwallcoverings.com/api/collection-grouped),
+       which groups server-side over the WHOLE collection BEFORE paging — so
+       the "N Colors" count and the tile set are correct across every page.
+    2. Is mutually exclusive with the Boost grid: when this section is enabled
+       on a collection template it HIDES Boost's product-grid output via the
+       `.dw-grouped--active` body flag + CSS (Boost's FILTER/SORT controls can
+       remain visible and still drive THIS grid via the `?filter=`/`?sort=`
+       querystring, which we forward to the feed). Boost is NOT removed — it is
+       demoted from "renders the grid" to "optional filter UI"; remove the Boost
+       block from the template entirely to go pure-grouped.
+    3. Falls back gracefully: if the feed is unreachable, the section stays
+       `hidden` and the native theme/Boost grid shows through unchanged — never
+       a blank collection.
+
+  ── PRODUCTION vs LOCAL ────────────────────────────────────────────────────
+  The feed groups by Shopify metafield `custom.pattern_id` when present
+  (GATED backfill via dw-pairs-well/tools/pattern-id-backfill.js), else by the
+  interim T2-title-base + mfr_root key — identical logic both sides, so this
+  grid is correct TODAY and improves silently once pattern_id is written.
+  Collection scope passes `?vendor=` (collection.current_vendor) or the
+  collection handle; `?all=1` is the local catalog-wide test proxy.
+
+  ── STANDING-RULE COMPLIANCE ───────────────────────────────────────────────
+  • sort <select> + density slider above the grid (localStorage-persisted)   ✓
+  • infinite scroll (IntersectionObserver, coexists w/ sort+density)         ✓
+  • bg hero retained (renders collection.image as the section banner)         ✓
+  • banned-word rule honored in all copy (uses "Wallcovering(s)" only)        ✓
+  • "N Colors ⌄" chip per tile → /pages/pattern?pattern_id=… (WS-3 page)      ✓
+{%- endcomment -%}
+
+{%- liquid
+  assign feed_base = 'https://pairs.designerwallcoverings.com/api/collection-grouped'
+  assign vendor_scope = collection.current_vendor | default: collection.metafields.custom.feed_vendor
+  assign show_hero = false
+  if collection.image
+    assign show_hero = true
+  endif
+-%}
+
+<section
+  id="dw-grouped"
+  class="dw-grouped"
+  data-feed="{{ feed_base }}"
+  data-vendor="{{ vendor_scope | escape }}"
+  data-collection-handle="{{ collection.handle | escape }}"
+  data-pattern-page="/pages/pattern"
+  hidden
+>
+  {%- if show_hero -%}
+    <div class="dw-grouped__hero" style="background-image:url('{{ collection.image | image_url: width: 2000 }}');">
+      <div class="dw-grouped__hero-inner">
+        <h1 class="dw-grouped__hero-title">{{ collection.title }}</h1>
+        {%- if collection.description != blank -%}
+          <div class="dw-grouped__hero-desc rte">{{ collection.description }}</div>
+        {%- endif -%}
+      </div>
+    </div>
+  {%- else -%}
+    <h1 class="dw-grouped__title">{{ collection.title }}</h1>
+  {%- endif -%}
+
+  <div class="dw-grouped__toolbar">
+    <div class="dw-grouped__count" id="dw-grouped-count"></div>
+    <div class="dw-grouped__controls">
+      <label class="dw-grouped__ctl">
+        <span>Sort</span>
+        <select id="dw-grouped-sort">
+          <option value="newest">Newest</option>
+          <option value="colors_desc">Most Colorways</option>
+          <option value="pattern_az">Pattern A→Z</option>
+          <option value="vendor_az">Vendor A→Z</option>
+        </select>
+      </label>
+      <label class="dw-grouped__ctl">
+        <span>Density</span>
+        <input id="dw-grouped-density" type="range" min="2" max="7" step="1" value="4">
+        <span class="dw-grouped__density-val" id="dw-grouped-density-val">4</span>
+      </label>
+    </div>
+  </div>
+
+  <div id="dw-grouped-grid" class="dw-grouped__grid" role="list"></div>
+
+  <div id="dw-grouped-sentinel" class="dw-grouped__sentinel" hidden>
+    <span id="dw-grouped-status">Loading more…</span>
+  </div>
+</section>
+
+<style>
+  /* When this section successfully renders, hide Boost's product grid (NOT its filter UI). */
+  body.dw-grouped--active .bc-sf-filter-products,
+  body.dw-grouped--active [data-collection-grid],
+  body.dw-grouped--active .collection-products,
+  body.dw-grouped--active .boost-pfs-filter-products { display: none !important; }
+
+  #dw-grouped { --cols: 4; font-family: inherit; margin: 0 0 3rem; }
+
+  .dw-grouped__hero {
+    position: relative; min-height: 320px; background-size: cover; background-position: center;
+    display: flex; align-items: flex-end; border-radius: 4px; overflow: hidden; margin-bottom: 1.5rem;
+  }
+  .dw-grouped__hero::after { content:''; position:absolute; inset:0; background:linear-gradient(180deg,rgba(0,0,0,0) 35%,rgba(0,0,0,0.55) 100%); }
+  .dw-grouped__hero-inner { position: relative; z-index: 1; padding: 32px 36px; color: #fff; max-width: 720px; }
+  .dw-grouped__hero-title { margin: 0 0 .4rem; font: 500 38px/1.05 ui-serif, Georgia, "Times New Roman", serif; letter-spacing: .01em; }
+  .dw-grouped__hero-desc { font-size: 14px; line-height: 1.5; opacity: .92; }
+  .dw-grouped__title { font: 500 32px/1.1 ui-serif, Georgia, serif; margin: 0 0 1.25rem; }
+
+  .dw-grouped__toolbar {
+    display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 14px;
+    padding: 10px 0 18px; border-bottom: 1px solid #ececec; margin-bottom: 22px;
+  }
+  .dw-grouped__count { font-size: 12px; color: #888; letter-spacing: .04em; text-transform: uppercase; }
+  .dw-grouped__controls { display: flex; gap: 22px; align-items: center; }
+  .dw-grouped__ctl { display: flex; align-items: center; gap: 8px; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #777; }
+  .dw-grouped__ctl select { font-size: 13px; padding: 5px 8px; border: 1px solid #ddd; border-radius: 4px; background: #fff; color: #1a1a1a; }
+  .dw-grouped__ctl input[type=range] { width: 130px; accent-color: #1a1a1a; }
+  .dw-grouped__density-val { font-variant-numeric: tabular-nums; color: #1a1a1a; font-weight: 600; min-width: 1em; }
+
+  .dw-grouped__grid { display: grid; gap: 22px; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); }
+  @media (max-width: 1024px) { .dw-grouped__grid { grid-template-columns: repeat(min(3, var(--cols)), minmax(0,1fr)); } }
+  @media (max-width: 700px)  { .dw-grouped__grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
+
+  .dw-grouped__card { display: block; text-decoration: none; color: inherit; }
+  .dw-grouped__img-wrap { position: relative; aspect-ratio: 1/1; background: #f4f4f4; border-radius: 4px; overflow: hidden; }
+  .dw-grouped__img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .4s ease; }
+  .dw-grouped__card:hover .dw-grouped__img { transform: scale(1.04); }
+  .dw-grouped__chip {
+    position: absolute; left: 10px; bottom: 10px; z-index: 2;
+    display: inline-flex; align-items: center; gap: 4px;
+    padding: 5px 11px; background: rgba(17,17,17,.86); color: #fff;
+    border-radius: 999px; font-size: 11px; font-weight: 500; letter-spacing: .04em;
+    backdrop-filter: saturate(140%) blur(2px);
+  }
+  .dw-grouped__chip svg { width: 9px; height: 9px; }
+  .dw-grouped__swatches { display: flex; gap: 3px; margin-top: 9px; flex-wrap: wrap; }
+  .dw-grouped__swatch { width: 13px; height: 13px; border-radius: 50%; border: 1px solid rgba(0,0,0,.12); }
+  .dw-grouped__meta { padding: 11px 2px 0; }
+  .dw-grouped__pattern { margin: 0; font-size: 14px; font-weight: 600; line-height: 1.3; }
+  .dw-grouped__vendor { margin: 3px 0 0; font-size: 10px; color: #888; text-transform: uppercase; letter-spacing: .05em; }
+
+  .dw-grouped__skel { aspect-ratio: 1/1; border-radius: 4px; background: linear-gradient(90deg,#eee 0%,#f6f6f6 50%,#eee 100%); background-size: 200% 100%; animation: dwgShim 1.2s linear infinite; }
+  @keyframes dwgShim { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
+  .dw-grouped__sentinel { text-align: center; padding: 26px; color: #999; font-size: 12px; }
+</style>
+
+<script>
+(function () {
+  var root = document.getElementById('dw-grouped');
+  if (!root) return;
+
+  var FEED      = root.getAttribute('data-feed');
+  var VENDOR    = root.getAttribute('data-vendor') || '';
+  var HANDLE    = root.getAttribute('data-collection-handle') || '';
+  var PAGE_BASE = root.getAttribute('data-pattern-page') || '/pages/pattern';
+
+  var grid     = document.getElementById('dw-grouped-grid');
+  var sentinel = document.getElementById('dw-grouped-sentinel');
+  var statusEl = document.getElementById('dw-grouped-status');
+  var countEl  = document.getElementById('dw-grouped-count');
+  var sortSel  = document.getElementById('dw-grouped-sort');
+  var densEl   = document.getElementById('dw-grouped-density');
+  var densVal  = document.getElementById('dw-grouped-density-val');
+
+  var PER = 48, page = 0, hasNext = true, loading = false, total = null;
+
+  function esc(s) {
+    return String(s == null ? '' : s)
+      .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
+      .replace(/"/g,'&quot;').replace(/'/g,'&#39;');
+  }
+
+  // Density slider — localStorage-persisted (standing grid rule).
+  function wireDensity() {
+    function apply() {
+      var n = Math.max(2, Math.min(7, parseInt(densEl.value, 10) || 4));
+      densVal.textContent = n; root.style.setProperty('--cols', n);
+      try { localStorage.setItem('dwg.cols', n); } catch (_) {}
+    }
+    try { var s = parseInt(localStorage.getItem('dwg.cols'), 10); if (s >= 2 && s <= 7) densEl.value = s; } catch (_) {}
+    densEl.addEventListener('input', apply); apply();
+  }
+  function wireSort() {
+    try { var s = localStorage.getItem('dwg.sort'); if (s) sortSel.value = s; } catch (_) {}
+    sortSel.addEventListener('change', function () {
+      try { localStorage.setItem('dwg.sort', sortSel.value); } catch (_) {}
+      reset(); loadNext();
+    });
+  }
+
+  function feedUrl(p) {
+    var qs = [];
+    if (VENDOR) qs.push('vendor=' + encodeURIComponent(VENDOR));
+    else        qs.push('all=1');                       // local/whole-catalog proxy when no vendor scope
+    qs.push('page=' + p); qs.push('per=' + PER);
+    if (sortSel && sortSel.value) qs.push('sort=' + encodeURIComponent(sortSel.value));
+    return FEED + '?' + qs.join('&');
+  }
+
+  var chevron = '<svg viewBox="0 0 12 12" aria-hidden="true"><path d="M2 4l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>';
+
+  function card(t) {
+    var img = t.image_url ? esc(t.image_url) : '';
+    var sw = (t.swatches || []).slice(0,6).map(function (h) {
+      return '<span class="dw-grouped__swatch" style="background:' + esc(h) + '"></span>';
+    }).join('');
+    var pageHref = PAGE_BASE + '?pattern_id=' + encodeURIComponent(t.pattern_id);
+    // single-colorway tile → straight to the product; multi → pattern page
+    var href = (t.count > 1) ? pageHref : (t.product_url || pageHref);
+    var chip = (t.count > 1)
+      ? '<span class="dw-grouped__chip">' + esc(t.colors_label) + ' ' + chevron + '</span>'
+      : '';
+    return '<a class="dw-grouped__card" role="listitem" href="' + esc(href) + '" title="' + esc(t.pattern) + '">'
+      +   '<div class="dw-grouped__img-wrap">'
+      +     (img ? '<img class="dw-grouped__img" src="' + img + '" alt="' + esc(t.pattern) + '" loading="lazy" decoding="async">' : '')
+      +     chip
+      +   '</div>'
+      +   '<div class="dw-grouped__meta">'
+      +     '<p class="dw-grouped__pattern">' + esc(t.pattern) + '</p>'
+      +     (t.vendor ? '<p class="dw-grouped__vendor">' + esc(t.vendor) + '</p>' : '')
+      +     (sw ? '<div class="dw-grouped__swatches">' + sw + '</div>' : '')
+      +   '</div>'
+      + '</a>';
+  }
+
+  function skel(n) { var h=''; for (var i=0;i<n;i++) h+='<div class="dw-grouped__skel"></div>'; grid.insertAdjacentHTML('beforeend', h); }
+  function clearSkel() { grid.querySelectorAll('.dw-grouped__skel').forEach(function(e){e.remove()}); }
+
+  function reset() { grid.innerHTML=''; page=0; hasNext=true; loading=false; total=null; }
+
+  function loadNext() {
+    if (loading || !hasNext) return;
+    loading = true;
+    var next = page + 1;
+    if (next === 1) skel(12);
+    fetch(feedUrl(next), { credentials: 'omit' })
+      .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('http ' + r.status)); })
+      .then(function (d) {
+        if (!d || d.ok === false) throw new Error((d && d.error) || 'bad payload');
+        clearSkel();
+        // First successful render → we OWN the grid; demote Boost.
+        if (next === 1) { document.body.classList.add('dw-grouped--active'); root.hidden = false; }
+        var tiles = d.tiles || [];
+        if (next === 1 && !tiles.length) { root.hidden = true; document.body.classList.remove('dw-grouped--active'); return; }
+        grid.insertAdjacentHTML('beforeend', tiles.map(card).join(''));
+        page = next;
+        total = d.total_tiles;
+        hasNext = !!d.has_next;
+        countEl.textContent = total != null ? (total + ' patterns') : '';
+        sentinel.hidden = !hasNext;
+        statusEl.textContent = hasNext ? 'Loading more…' : 'All patterns loaded';
+        loading = false;
+      })
+      .catch(function (e) {
+        clearSkel();
+        // Feed unreachable on first page → leave Boost/native grid showing through.
+        if (next === 1) { root.hidden = true; document.body.classList.remove('dw-grouped--active'); }
+        else { statusEl.textContent = 'Could not load more'; }
+        loading = false;
+      });
+  }
+
+  wireDensity(); wireSort();
+
+  var io = new IntersectionObserver(function (entries) {
+    entries.forEach(function (e) { if (e.isIntersecting) loadNext(); });
+  }, { rootMargin: '700px' });
+  io.observe(sentinel);
+
+  loadNext(); // kick page 1
+})();
+</script>
+
+{% schema %}
+{
+  "name": "Grouped collection grid",
+  "settings": [
+    {
+      "type": "paragraph",
+      "content": "Renders ONE tile per pattern (collapsing colorways) from the DW pattern feed, overriding Boost's per-product grid. Boost filter UI can remain. Falls back to the native grid if the feed is unreachable."
+    }
+  ],
+  "presets": [{ "name": "Grouped collection grid" }]
+}
+{% endschema %}
diff --git a/shopify/_cwGRID/sections/pattern-page.liquid b/shopify/_cwGRID/sections/pattern-page.liquid
new file mode 100644
index 00000000..5b0de1c7
--- /dev/null
+++ b/shopify/_cwGRID/sections/pattern-page.liquid
@@ -0,0 +1,166 @@
+{%- comment -%}
+  ============================================================================
+  WS-3 — PATTERN DETAIL PAGE  ·  "all colorways of one pattern, one place"
+  ============================================================================
+  Owner: vp-dw-commerce · serves /pages/pattern?pattern_id=<key>
+  Reached from the WS-2 grouped grid "N Colors ⌄" chip.
+
+  Lists every colorway of a pattern via
+    https://pairs.designerwallcoverings.com/api/colorways?pattern_id=…
+  ( also accepts ?handle= / ?dw_sku= ). Each colorway card links to the live
+  Shopify product (/products/<handle>) where price + sample order live.
+
+  Hosts the WS-4 "Similar Designs" button (similar-designs.liquid) seeded from
+  the pattern's representative colorway, so the section is self-contained.
+
+  Layout models ~/Projects/designerwallcoverings/artmura-site PDP + DW house
+  chrome (serif display, hairline meta, social share into the DW store).
+  Banned-word rule honored — copy uses "Wallcovering(s)" only.
+{%- endcomment -%}
+
+<section
+  id="dw-pattern"
+  class="dw-pattern"
+  data-endpoint="https://pairs.designerwallcoverings.com/api/colorways"
+  data-store-base="https://www.designerwallcoverings.com"
+>
+  <nav class="dw-pattern__crumbs" id="dw-pattern-crumbs">
+    <a href="{{ routes.root_url }}">Designer Wallcoverings</a> /
+    <a href="{{ routes.all_products_collection_url }}">All Wallcoverings</a> /
+    <span id="dw-pattern-crumb-name">Pattern</span>
+  </nav>
+
+  <header class="dw-pattern__head">
+    <h1 class="dw-pattern__title" id="dw-pattern-title">Loading pattern…</h1>
+    <p class="dw-pattern__sub" id="dw-pattern-sub"></p>
+  </header>
+
+  <div class="dw-pattern__grid" id="dw-pattern-grid" role="list"></div>
+
+  <div id="similar-mount"></div>
+</section>
+
+<style>
+  #dw-pattern { font-family: inherit; max-width: 1280px; margin: 0 auto; padding: 28px 20px 64px; }
+  .dw-pattern__crumbs { font-size: 12px; color: #888; letter-spacing: .03em; margin-bottom: 22px; }
+  .dw-pattern__crumbs a { color: #888; text-decoration: none; }
+  .dw-pattern__crumbs a:hover { color: #1a1a1a; }
+  .dw-pattern__head { margin-bottom: 26px; }
+  .dw-pattern__title { margin: 0; font: 500 40px/1.05 ui-serif, Georgia, "Times New Roman", serif; letter-spacing: .01em; }
+  .dw-pattern__sub { margin: 8px 0 0; font-size: 13px; color: #777; text-transform: uppercase; letter-spacing: .08em; }
+
+  .dw-pattern__grid { display: grid; gap: 24px; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
+  .dw-pattern__card { display: block; text-decoration: none; color: inherit; }
+  .dw-pattern__imgwrap { position: relative; aspect-ratio: 1/1; background: #f4f4f4; border-radius: 4px; overflow: hidden; }
+  .dw-pattern__img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .4s ease; }
+  .dw-pattern__card:hover .dw-pattern__img { transform: scale(1.04); }
+  .dw-pattern__dot { position: absolute; top: 10px; left: 10px; width: 18px; height: 18px; border-radius: 50%; border: 2px solid #fff; box-shadow: 0 1px 4px rgba(0,0,0,.25); }
+  .dw-pattern__cmeta { padding: 11px 2px 0; }
+  .dw-pattern__cname { margin: 0; font-size: 14px; font-weight: 600; }
+  .dw-pattern__csku { margin: 3px 0 0; font-size: 11px; color: #999; letter-spacing: .03em; }
+  .dw-pattern__cta { display: inline-block; margin-top: 8px; font-size: 11px; color: #1a1a1a; text-transform: uppercase; letter-spacing: .06em; border-bottom: 1px solid #1a1a1a; padding-bottom: 1px; }
+
+  .dw-pattern__skel { aspect-ratio: 1/1; border-radius: 4px; background: linear-gradient(90deg,#eee 0%,#f6f6f6 50%,#eee 100%); background-size: 200% 100%; animation: dwpShim 1.2s linear infinite; }
+  @keyframes dwpShim { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
+  .dw-pattern__empty { padding: 30px 0; color: #777; font-size: 15px; }
+</style>
+
+<script>
+(function () {
+  var root = document.getElementById('dw-pattern');
+  if (!root) return;
+  var ENDPOINT  = root.getAttribute('data-endpoint');
+  var STORE     = root.getAttribute('data-store-base') || 'https://www.designerwallcoverings.com';
+
+  var grid   = document.getElementById('dw-pattern-grid');
+  var titleE = document.getElementById('dw-pattern-title');
+  var subE   = document.getElementById('dw-pattern-sub');
+  var crumbE = document.getElementById('dw-pattern-crumb-name');
+
+  function esc(s) {
+    return String(s == null ? '' : s)
+      .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
+      .replace(/"/g,'&quot;').replace(/'/g,'&#39;');
+  }
+  function qp(name) {
+    var m = new RegExp('[?&]' + name + '=([^&]+)').exec(location.search);
+    return m ? decodeURIComponent(m[1].replace(/\+/g,' ')) : '';
+  }
+
+  var pid    = qp('pattern_id');
+  var handle = qp('handle');
+  var dwsku  = qp('dw_sku');
+
+  function url() {
+    var qs = [];
+    if (pid)    qs.push('pattern_id=' + encodeURIComponent(pid));
+    else if (handle) qs.push('handle=' + encodeURIComponent(handle));
+    else if (dwsku)  qs.push('dw_sku='  + encodeURIComponent(dwsku));
+    return ENDPOINT + '?' + qs.join('&');
+  }
+
+  function skel(n) { var h=''; for (var i=0;i<n;i++) h+='<div class="dw-pattern__skel"></div>'; grid.innerHTML=h; }
+
+  function card(c) {
+    var img = c.image_url ? esc(c.image_url) : '';
+    var dot = c.color_hex ? '<span class="dw-pattern__dot" style="background:' + esc(c.color_hex) + '"></span>' : '';
+    var href = c.url ? esc(c.url) : (c.handle ? '/products/' + esc(c.handle) : '#');
+    return '<a class="dw-pattern__card" role="listitem" href="' + href + '" title="' + esc(c.color_name) + '">'
+      +  '<div class="dw-pattern__imgwrap">'
+      +    (img ? '<img class="dw-pattern__img" src="' + img + '" alt="' + esc(c.color_name) + '" loading="lazy" decoding="async">' : '')
+      +    dot
+      +  '</div>'
+      +  '<div class="dw-pattern__cmeta">'
+      +    '<p class="dw-pattern__cname">' + esc(c.color_name) + '</p>'
+      +    '<p class="dw-pattern__csku">' + esc(c.dw_sku) + '</p>'
+      +    '<span class="dw-pattern__cta">View &amp; price &rarr;</span>'
+      +  '</div>'
+      + '</a>';
+  }
+
+  if (!pid && !handle && !dwsku) {
+    titleE.textContent = 'Pattern';
+    grid.innerHTML = '<div class="dw-pattern__empty">No pattern specified.</div>';
+    return;
+  }
+
+  skel(8);
+  fetch(url(), { credentials: 'omit' })
+    .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('http ' + r.status)); })
+    .then(function (d) {
+      if (!d || d.ok === false) throw new Error((d && d.error) || 'bad payload');
+      var cw = d.colorways || [];
+      titleE.textContent = d.pattern || 'Pattern';
+      crumbE.textContent = d.pattern || 'Pattern';
+      document.title = (d.pattern || 'Pattern') + ' — ' + (cw.length) + ' Colorways · Designer Wallcoverings';
+      subE.textContent = (d.vendor ? d.vendor + ' · ' : '') + cw.length + (cw.length === 1 ? ' Colorway' : ' Colorways');
+      grid.innerHTML = cw.length ? cw.map(card).join('') : '<div class="dw-pattern__empty">No colorways found for this pattern.</div>';
+
+      // Seed the WS-4 Similar Designs section from the representative colorway,
+      // then dispatch a custom event the similar-designs IIFE can pick up. The
+      // similar-designs.liquid block (rendered below this section) reads
+      // data-source-handle / data-source-sku off #similar-designs.
+      var sim = document.getElementById('similar-designs');
+      if (sim && cw.length) {
+        sim.setAttribute('data-source-handle', cw[0].handle || '');
+        sim.setAttribute('data-source-sku', cw[0].dw_sku || '');
+      }
+    })
+    .catch(function () {
+      titleE.textContent = 'Pattern';
+      grid.innerHTML = '<div class="dw-pattern__empty">Could not load this pattern right now.</div>';
+    });
+})();
+</script>
+
+{% schema %}
+{
+  "name": "Pattern detail page",
+  "settings": [
+    {
+      "type": "paragraph",
+      "content": "Lists all colorways of one pattern (reads ?pattern_id / ?handle / ?dw_sku from the URL) via the DW colorways feed. Place the Similar Designs section directly after this on the page template."
+    }
+  ]
+}
+{% endschema %}
diff --git a/shopify/_cwGRID/sections/similar-designs.liquid b/shopify/_cwGRID/sections/similar-designs.liquid
new file mode 100644
index 00000000..8c7c8c89
--- /dev/null
+++ b/shopify/_cwGRID/sections/similar-designs.liquid
@@ -0,0 +1,219 @@
+{%- comment -%}
+    Similar Designs v1 — visual ALTERNATIVES to this product (NOT coordinates).
+      • Grid of look-alike designs: same motif / material / era / palette
+      • Density slider 3-12 cols (default 8), localStorage-persisted
+      • Each card shows palette dots + a "why it's similar" chip
+    Service: https://pairs.designerwallcoverings.com/api/similar  (~/Projects/dw-pairs-well)
+    Self-contained .similar-designs__* namespace + IIFE — coexists with the
+    Design Coordinate section (#pairs-well-with) on the same product/pattern page.
+  {%- endcomment -%}
+  <section
+    id="similar-designs"
+    class="sd-section"
+    data-similar-endpoint="https://pairs.designerwallcoverings.com/api/similar"
+    data-product-handle="{{ product.handle }}"
+    data-product-sku="{{ product.metafields.custom.manufacturer_sku | default: product.selected_or_first_available_variant.sku | escape }}"
+    style="margin: 1.5em 0 2em; clear: both;"
+    hidden
+  >
+    <button type="button" class="similar-designs__cta" aria-expanded="false">
+      <span class="similar-designs__cta-text">Similar Designs</span>
+      <span class="similar-designs__cta-icon" aria-hidden="true">+</span>
+    </button>
+
+    <div class="similar-designs__panel" id="similar-designs-panel" hidden>
+      <div class="similar-designs__panel-head">
+        <h3>Designs like this <span class="count" id="similar-designs-count"></span></h3>
+        <div class="similar-designs__controls">
+          <label for="similar-designs-cols">Columns</label>
+          <input id="similar-designs-cols" type="range" min="3" max="12" value="8" step="1">
+          <span class="similar-designs__cols-val" id="similar-designs-cols-val">8</span>
+        </div>
+      </div>
+      <div id="similar-designs-grid" class="similar-designs__grid"></div>
+    </div>
+  </section>
+
+  <style>
+    #similar-designs { font-family: inherit; --sd-cols: 8; }
+    .similar-designs__cta {
+      display: flex; align-items: center; justify-content: space-between;
+      width: 100%; padding: 18px 24px; margin: 0;
+      background: #111; color: #fff;
+      border: 0; border-radius: 999px;
+      font-size: 15px; font-weight: 500; letter-spacing: 0.06em;
+      text-transform: uppercase; cursor: pointer;
+      transition: background 160ms ease, transform 120ms ease;
+    }
+    .similar-designs__cta:hover  { background: #2a2a2a; }
+    .similar-designs__cta:active { transform: scale(0.995); }
+    .similar-designs__cta[aria-expanded="true"] .similar-designs__cta-icon { transform: rotate(45deg); }
+    .similar-designs__cta-icon { display: inline-block; font-size: 22px; line-height: 1; transition: transform 200ms ease; }
+
+    .sd-dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; border: 1px solid rgba(0,0,0,0.12); vertical-align: middle; }
+
+    .similar-designs__panel-head {
+      display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
+      gap: 12px; margin: 24px 0 12px;
+    }
+    .similar-designs__panel-head h3 { margin: 0; font: 500 18px/1.2 ui-serif, Georgia, serif; }
+    .similar-designs__panel-head .count { color: #888; font-size: 12px; margin-left: 6px; font-family: -apple-system, sans-serif; font-weight: 400; }
+    .similar-designs__controls {
+      display: flex; align-items: center; gap: 10px;
+      font-size: 11px; color: #777; text-transform: uppercase; letter-spacing: 0.06em;
+    }
+    .similar-designs__controls input[type=range] { width: 160px; accent-color: #1a1a1a; }
+    .similar-designs__cols-val { font-variant-numeric: tabular-nums; color: #1a1a1a; min-width: 1.5em; text-align: right; font-weight: 600; }
+
+    .similar-designs__grid {
+      display: grid; gap: 14px; margin-top: 6px;
+      grid-template-columns: repeat(var(--sd-cols), minmax(0, 1fr));
+    }
+    @media (max-width: 1100px) { .similar-designs__grid { grid-template-columns: repeat(min(6, var(--sd-cols)), minmax(0, 1fr)); } }
+    @media (max-width: 900px)  { .similar-designs__grid { grid-template-columns: repeat(min(4, var(--sd-cols)), minmax(0, 1fr)); } }
+    @media (max-width: 600px)  { .similar-designs__grid { grid-template-columns: repeat(min(2, var(--sd-cols)), minmax(0, 1fr)); } }
+
+    .similar-designs__card {
+      display: block; text-decoration: none; color: inherit;
+      background: #fff; border: 1px solid #e7e7e7; border-radius: 6px;
+      overflow: hidden; transition: box-shadow 160ms ease, transform 160ms ease;
+    }
+    .similar-designs__card:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.08); transform: translateY(-2px); }
+    .similar-designs__img-wrap { aspect-ratio: 1 / 1; background: #f4f4f4; overflow: hidden; }
+    .similar-designs__img { width: 100%; height: 100%; object-fit: cover; display: block; }
+    .similar-designs__meta { padding: 10px 12px 12px; }
+    .similar-designs__title { margin: 0 0 4px; font-size: 13px; font-weight: 600; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
+    .similar-designs__vendor { margin: 0 0 6px; font-size: 10px; color: #777; letter-spacing: 0.04em; text-transform: uppercase; }
+    .similar-designs__why { display: inline-block; padding: 2px 7px; background: #eef0f4; color: #34425a; border-radius: 999px; font-size: 10px; line-height: 1.4; margin-bottom: 8px; }
+    .similar-designs__palette { display: flex; gap: 3px; margin: 0; flex-wrap: wrap; }
+
+    .similar-designs__skeleton { background: linear-gradient(90deg, #eee 0%, #f6f6f6 50%, #eee 100%); background-size: 200% 100%; animation: sdShim 1.2s linear infinite; aspect-ratio: 1 / 1; border-radius: 6px; }
+    @keyframes sdShim { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
+    .similar-designs__empty { padding: 18px; color: #777; font-size: 14px; grid-column: 1 / -1; }
+  </style>
+
+  <script>
+    (function () {
+      var root = document.getElementById('similar-designs');
+      if (!root) return;
+
+      var endpoint = root.getAttribute('data-similar-endpoint');
+      var handle   = root.getAttribute('data-product-handle');
+      var sku      = root.getAttribute('data-product-sku');
+      // Pattern page (no Shopify product object) can set data-source-handle directly.
+      if (!handle) handle = root.getAttribute('data-source-handle') || '';
+      if (!sku)    sku    = root.getAttribute('data-source-sku') || '';
+      var hasIdentifier = (sku && sku.length) || (handle && handle.length);
+      if (!hasIdentifier) return;
+      root.hidden = false;
+
+      var cta      = root.querySelector('.similar-designs__cta');
+      var panel    = document.getElementById('similar-designs-panel');
+      var grid     = document.getElementById('similar-designs-grid');
+      var cols     = document.getElementById('similar-designs-cols');
+      var colsVal  = document.getElementById('similar-designs-cols-val');
+      var countEl  = document.getElementById('similar-designs-count');
+      var loaded   = false;
+
+      function esc(s) {
+        return String(s == null ? '' : s)
+          .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
+          .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
+      }
+
+      // Density slider, localStorage-persisted (Steve's standing grid rule).
+      function wireSlider() {
+        function apply() {
+          var n = Math.max(3, Math.min(12, parseInt(cols.value, 10) || 8));
+          colsVal.textContent = n;
+          root.style.setProperty('--sd-cols', n);
+          try { localStorage.setItem('sd.cols', n); } catch (_) {}
+        }
+        try {
+          var s = parseInt(localStorage.getItem('sd.cols'), 10);
+          if (!isNaN(s) && s >= 3 && s <= 12) cols.value = s;
+        } catch (_) {}
+        cols.addEventListener('input', apply);
+        apply();
+      }
+      wireSlider();
+
+      function renderSkeletons(n) {
+        var html = '';
+        for (var i = 0; i < n; i++) html += '<div class="similar-designs__skeleton"></div>';
+        grid.innerHTML = html;
+      }
+
+      function renderCard(p) {
+        var img = p.image_url ? esc(p.image_url) : '';
+        var why = (p.why && p.why[0]) ? esc(p.why[0]) : '';
+        var palette = (p.palette || []).map(function (c) {
+          var hex = typeof c === 'string' ? c : c.hex;
+          return '<span class="sd-dot" style="background:' + esc(hex) + '"></span>';
+        }).join('');
+        var href = p.url ? esc(p.url) : (p.handle ? '/products/' + esc(p.handle) : '#');
+        return '<a class="similar-designs__card" href="' + href + '" title="' + esc((p.why || []).join(' · ')) + '">'
+          +   '<div class="similar-designs__img-wrap">' + (img ? '<img class="similar-designs__img" src="' + img + '" alt="' + esc(p.title) + '" loading="lazy">' : '') + '</div>'
+          +   '<div class="similar-designs__meta">'
+          +     '<p class="similar-designs__title">' + esc(p.title || '') + '</p>'
+          +     '<p class="similar-designs__vendor">' + esc(p.vendor || '') + '</p>'
+          +     (why ? '<span class="similar-designs__why">' + why + '</span>' : '')
+          +     (palette ? '<div class="similar-designs__palette">' + palette + '</div>' : '')
+          +   '</div>'
+          + '</a>';
+      }
+
+      function renderGrid(items) {
+        if (!items || !items.length) { grid.innerHTML = '<div class="similar-designs__empty">No similar designs found.</div>'; return; }
+        grid.innerHTML = items.map(renderCard).join('');
+      }
+
+      function buildUrl(limit) {
+        var qs = [];
+        if (sku)    qs.push('dw_sku=' + encodeURIComponent(sku));
+        if (handle) qs.push('handle=' + encodeURIComponent(handle));
+        if (limit)  qs.push('limit=' + limit);
+        return endpoint + '?' + qs.join('&');
+      }
+
+      function load() {
+        if (loaded) return;
+        loaded = true;
+        panel.hidden = false;
+        renderSkeletons(12);
+        fetch(buildUrl(12), { credentials: 'omit' })
+          .then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('http ' + r.status)); })
+          .then(function (d) {
+            if (!d || d.ok === false) throw new Error((d && d.error) || 'bad payload');
+            var items = d.similar || [];
+            renderGrid(items);
+            countEl.textContent = '(' + items.length + ')';
+            if (!items.length) root.hidden = true;   // nothing similar → hide the whole section
+          })
+          .catch(function () { grid.innerHTML = '<div class="similar-designs__empty">Could not load similar designs right now.</div>'; });
+      }
+
+      cta.addEventListener('click', function () {
+        var open = cta.getAttribute('aria-expanded') === 'true';
+        if (open) {
+          cta.setAttribute('aria-expanded', 'false');
+          panel.hidden = true;
+          return;
+        }
+        cta.setAttribute('aria-expanded', 'true');
+        load();
+      });
+    })();
+  </script>
+
+{% schema %}
+{
+  "name": "Similar Designs",
+  "settings": [
+    {
+      "type": "paragraph",
+      "content": "WS-4 visual alternatives. On the Pattern page it auto-seeds from the pattern's representative colorway (data-source-handle/sku set by pattern-page.liquid); on product pages it reads the product handle/SKU."
+    }
+  ]
+}
+{% endschema %}
diff --git a/shopify/_cwGRID/templates/page.pattern.json b/shopify/_cwGRID/templates/page.pattern.json
new file mode 100644
index 00000000..973f434d
--- /dev/null
+++ b/shopify/_cwGRID/templates/page.pattern.json
@@ -0,0 +1,16 @@
+{
+  "sections": {
+    "pattern": {
+      "type": "pattern-page",
+      "settings": {}
+    },
+    "similar": {
+      "type": "similar-designs",
+      "settings": {}
+    }
+  },
+  "order": [
+    "pattern",
+    "similar"
+  ]
+}

← 5dac2a20 auto-save: 2026-06-26T00:08:14 (5 files) — pending-approval/  ·  back to Designer Wallcoverings  ·  auto-save: 2026-06-26T00:38:23 (8 files) — pending-approval/ dd04a9c5 →