[object Object]

← back to Consulting Designerwallcoverings Com

chore: lint, refactor, v0.1.1 (session close)

95aeb4a733bad4d6851ed1685b498518b81a3971 · 2026-07-26 16:40:43 -0700 · Steve Abrams

Files touched

Diff

commit 95aeb4a733bad4d6851ed1685b498518b81a3971
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 26 16:40:43 2026 -0700

    chore: lint, refactor, v0.1.1 (session close)
---
 data/dw-analysis.json    | 80 ++++++++++++++++++++++++------------------------
 package-lock.json        |  4 +--
 package.json             |  6 ++--
 public/portal.html       | 62 ++++++++++++++++++-------------------
 scripts/collect-dw.mjs   | 16 +++++++---
 scripts/seed-buckets.mjs | 18 ++++++++---
 server.js                |  6 ++--
 7 files changed, 107 insertions(+), 85 deletions(-)

diff --git a/data/dw-analysis.json b/data/dw-analysis.json
index 5ca06ae..3544f01 100644
--- a/data/dw-analysis.json
+++ b/data/dw-analysis.json
@@ -1,5 +1,5 @@
 {
-  "collected_at": "2026-07-26T16:31:09.962Z",
+  "collected_at": "2026-07-26T23:39:04.258Z",
   "sources": {
     "mirror": "dw_unified local mirror (host=/tmp) — READ ONLY",
     "mcc": "marketing.designerwallcoverings.com/api/channels/status",
@@ -9,17 +9,17 @@
   },
   "catalog": {
     "statuses": {
-      "ACTIVE": 80542,
+      "ACTIVE": 80728,
       "ARCHIVED": 80473,
+      "DRAFT": 20653,
       "DELETED_FROM_SHOPIFY": 16205,
-      "DRAFT": 16035,
       "UNLISTED": 1
     },
     "vendors_active": 174,
     "top_vendors": [
       {
         "vendor": "Phillipe Romano",
-        "active": 11270
+        "active": 11377
       },
       {
         "vendor": "Rebel Walls",
@@ -39,11 +39,11 @@
       },
       {
         "vendor": "Thibaut",
-        "active": 2987
+        "active": 2996
       },
       {
         "vendor": "Jeffrey Stevens",
-        "active": 2899
+        "active": 2912
       },
       {
         "vendor": "Brunschwig & Fils",
@@ -77,11 +77,11 @@
     "types": [
       {
         "type": "Wallcovering",
-        "active": 58298
+        "active": 58475
       },
       {
         "type": "Fabric",
-        "active": 16200
+        "active": 16209
       },
       {
         "type": "Commercial Fabric",
@@ -118,11 +118,11 @@
     ],
     "five_field": {
       "missing_sample": 2298,
-      "missing_sellable": 26742,
+      "missing_sellable": 26858,
       "missing_description": 172,
       "missing_image": 333,
       "zero_price": 1444,
-      "active_total": 80542
+      "active_total": 80728
     },
     "activation_cadence": [
       {
@@ -155,15 +155,15 @@
       },
       {
         "week": "07-06",
-        "added": 4260
+        "added": 4269
       },
       {
         "week": "07-13",
-        "added": 1486
+        "added": 1499
       },
       {
         "week": "07-20",
-        "added": 3377
+        "added": 3541
       }
     ],
     "price_bands": [
@@ -189,17 +189,17 @@
       }
     ],
     "price_bands_basis": "products whose minimum variant exceeds the $4.25 memo sample — the priced subset of the mirror (~3.8k); most actives resolve min-variant to the sample, and full variant-level pricing lives Shopify-side",
-    "drafts_total": 16035,
-    "drafts_stageable": 2778,
+    "drafts_total": 20653,
+    "drafts_stageable": 2784,
     "sample_only": {
-      "quote_only_by_design": 3790,
-      "total": 26742,
+      "quote_only_by_design": 3906,
+      "total": 26858,
       "backlog": 22952
     },
     "sample_only_vendors": [
       {
         "vendor": "Phillipe Romano",
-        "count": 5924
+        "count": 6031
       },
       {
         "vendor": "Phillip Jeffries",
@@ -232,17 +232,17 @@
     ],
     "sample_roll": {
       "funnel": {
-        "sampleable": 78244,
-        "converted": 51664,
-        "gap": 26580,
-        "gap_quote_only": 3790,
+        "sampleable": 78430,
+        "converted": 51734,
+        "gap": 26696,
+        "gap_quote_only": 3906,
         "gap_backlog": 22790,
         "conversion_pct": 66
       },
       "vendors": [
         {
           "vendor": "Phillipe Romano",
-          "sampleable": 9988,
+          "sampleable": 10095,
           "converted": 4064,
           "leak": 3521
         },
@@ -349,13 +349,13 @@
       },
       {
         "month": "Jul",
-        "added": 10349
+        "added": 10535
       }
     ],
     "needs_tags": [
       {
         "tag": "Needs-Price",
-        "count": 19246
+        "count": 19268
       },
       {
         "tag": "Needs-Image",
@@ -373,7 +373,7 @@
     "style_mix": [
       {
         "style": "Contemporary",
-        "count": 32580
+        "count": 32588
       },
       {
         "style": "Textured",
@@ -381,15 +381,15 @@
       },
       {
         "style": "Traditional",
-        "count": 22704
+        "count": 22744
       },
       {
         "style": "Geometric",
-        "count": 11748
+        "count": 11749
       },
       {
         "style": "Floral",
-        "count": 10286
+        "count": 10297
       },
       {
         "style": "Grasscloth",
@@ -401,7 +401,7 @@
       },
       {
         "style": "Mural",
-        "count": 4769
+        "count": 4817
       },
       {
         "style": "Metallic",
@@ -416,52 +416,52 @@
       {
         "color": "Beige",
         "hex": "#d9c9a8",
-        "count": 22079
+        "count": 22093
       },
       {
         "color": "White",
         "hex": "#f2efe8",
-        "count": 22061
+        "count": 22063
       },
       {
         "color": "Gray",
         "hex": "#9a9a99",
-        "count": 12163
+        "count": 12169
       },
       {
         "color": "Blue",
         "hex": "#5a7fa8",
-        "count": 8551
+        "count": 8555
       },
       {
         "color": "Green",
         "hex": "#6b8f6d",
-        "count": 7749
+        "count": 7755
       },
       {
         "color": "Brown",
         "hex": "#8a6a4c",
-        "count": 5192
+        "count": 5197
       },
       {
         "color": "Black",
         "hex": "#2b2b2b",
-        "count": 4883
+        "count": 4885
       },
       {
         "color": "Gold",
         "hex": "#c9a24b",
-        "count": 4326
+        "count": 4328
       },
       {
         "color": "Pink",
         "hex": "#d3a0a6",
-        "count": 3456
+        "count": 3458
       },
       {
         "color": "Red",
         "hex": "#a84a44",
-        "count": 2946
+        "count": 2947
       }
     ]
   },
diff --git a/package-lock.json b/package-lock.json
index ac66da0..25a9d7c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "consulting-designerwallcoverings-com",
-  "version": "0.1.0",
+  "version": "0.1.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "consulting-designerwallcoverings-com",
-      "version": "0.1.0",
+      "version": "0.1.1",
       "dependencies": {
         "dotenv": "^16.4.7",
         "express": "^4.21.2"
diff --git a/package.json b/package.json
index 45f3e6f..483e203 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "consulting-designerwallcoverings-com",
-  "version": "0.1.0",
+  "version": "0.1.1",
   "private": true,
   "description": "Designer Wallcoverings — consulting portal (site concepts + integrated-social growth command center)",
   "main": "server.js",
@@ -13,5 +13,7 @@
     "express": "^4.21.2",
     "dotenv": "^16.4.7"
   },
-  "engines": { "node": ">=18" }
+  "engines": {
+    "node": ">=18"
+  }
 }
diff --git a/public/portal.html b/public/portal.html
index dfa2aa1..e598aa9 100644
--- a/public/portal.html
+++ b/public/portal.html
@@ -128,7 +128,7 @@ footer{padding:56px clamp(24px,7vw,120px);border-top:1px solid var(--line);color
 @media(max-width:640px){.metrics{gap:26px}}
 </style></head><body>
 <header class="bar">
-  <div class="stamp"><span>Data collected Jul 26, 2026, 9:31 AM</span><button id="refbtn" title="Re-run the read-only collector">↻ Refresh data</button></div>
+  <div class="stamp"><span>Data collected Jul 26, 2026, 4:39 PM</span><button id="refbtn" title="Re-run the read-only collector">↻ Refresh data</button></div>
   <button class="burger" onclick="document.body.classList.toggle('nav')" aria-label="Menu" aria-expanded="false" aria-controls="portalmenu"><span></span><span></span><span></span></button>
 </header>
 <div class="scrim" onclick="document.body.classList.remove('nav')"></div>
@@ -143,12 +143,12 @@ footer{padding:56px clamp(24px,7vw,120px);border-top:1px solid var(--line);color
   <div class="in">
     <div class="kick">Los Angeles · nationwide e-commerce + trade · Confidential strategy brief · prepared for Steve Abrams</div>
     <h1 class="disp">Designer <span>Wallcoverings</span></h1>
-    <p>Thirty years of trade authority, <b>80,542 live SKUs</b> across <b>174 vendor lines</b>, a <b>281-site</b> discovery fleet, and <b>7 connected social channels</b> — the deepest designer-wallcovering operation online. The opportunity isn't building an audience; it's converting the catalog's dormant depth (a 22,952-product sellable-variant backlog, 16,035 staged drafts) into measured revenue. This brief maps exactly that, from the live data.</p>
+    <p>Thirty years of trade authority, <b>80,728 live SKUs</b> across <b>174 vendor lines</b>, a <b>281-site</b> discovery fleet, and <b>7 connected social channels</b> — the deepest designer-wallcovering operation online. The opportunity isn't building an audience; it's converting the catalog's dormant depth (a 22,952-product sellable-variant backlog, 20,653 staged drafts) into measured revenue. This brief maps exactly that, from the live data.</p>
     <div class="metrics">
-      <div class="m"><div class="v" data-c="80542">0</div><div class="l">Live SKUs (active)</div></div>
+      <div class="m"><div class="v" data-c="80728">0</div><div class="l">Live SKUs (active)</div></div>
       <div class="m"><div class="v" data-c="174">0</div><div class="l">Vendor lines</div></div>
       <div class="m"><div class="v" data-c="281">0</div><div class="l">Microsite fleet</div></div>
-      <div class="m"><div class="v" data-c="16035">0</div><div class="l">Staged drafts (fuel)</div></div>
+      <div class="m"><div class="v" data-c="20653">0</div><div class="l">Staged drafts (fuel)</div></div>
     </div>
   </div>
 </div>
@@ -156,18 +156,18 @@ footer{padding:56px clamp(24px,7vw,120px);border-top:1px solid var(--line);color
 <div class="accbar"><button id="xall">Expand all</button><button id="call">Collapse all</button></div>
 
 <section id="swot"><div class="eyebrow">Strategic Snapshot · SWOT</div><h2 class="disp">The whole board on one screen.</h2><div class="accbody">
-  <p class="lead2">Four lenses over the live snapshot — internal <b>Strengths</b> and <b>Weaknesses</b>, external <b>Opportunities</b> and <b>Threats</b> — so the strategy that follows starts from one honest picture. Every number here is from the Jul 26, 2026, 9:31 AM collection.</p>
-  <div class="swotgrid"><div class="swotq pos"><h3>Strengths</h3><ul><li>80,542 live SKUs across 174 vendor lines — one of the deepest designer-wallcovering catalogs online</li><li>A 281-site microsite fleet feeding discovery back to the flagship</li><li>7/7 social channels connected through one marketing command center</li><li>Private-label lines with owned margin (Phillipe Romano, Hollywood Wallcoverings, Malibu Wallpaper)</li><li>30+ years of trade relationships; memo-sample program ($4.25) as a proven funnel entry</li><li>Automated activation drip already shipping ~500 products/day, textures-first</li></ul></div><div class="swotq neg"><h3>Weaknesses</h3><ul><li>22,952 active products are sample-only WITHOUT a quote-only designation — browseable but not buyable</li><li>1,444 actives carry zero/null pricing; 333 lack a featured image</li><li>Social reach is broad (80 FB pages, 34 IG accounts) but thin — distribution without engagement depth</li><li>No social→revenue attribution discipline (UTM/coupon per channel) yet</li><li>Flagship UX carries catalog weight — 80k SKUs need stronger guided curation for retail buyers</li></ul></div><div class="swotq pos"><h3>Opportunities</h3><ul><li>Convert the 22,952 sample-only backlog into sellable variants — the single largest dormant-revenue pool in the business</li><li>16,035 staged drafts (2,778 fully stageable today) = months of "New Arrivals" marketing fuel</li><li>AEO/AI-answer citations for wallcovering queries — the flagship already ships an AEO block; extend it fleet-wide</li><li>Sample-order → full-roll conversion funnel instrumentation (order sync to the back office already exists)</li><li>Licensing original design IP (settlement-gated) into marketplaces as a zero-inventory revenue line</li></ul></div><div class="swotq neg"><h3>Threats</h3><ul><li>DTC-native entrants own the "approachable wallpaper" narrative with modern brand voices</li><li>Marketplace aggregators (Amazon, Wayfair, Etsy) commoditize mid-tier patterns</li><li>AI answer engines could disintermediate catalog-depth advantages if citations go to competitors</li><li>Vendor MAP / brand-control tightening limits pricing latitude on marquee lines</li></ul></div></div>
+  <p class="lead2">Four lenses over the live snapshot — internal <b>Strengths</b> and <b>Weaknesses</b>, external <b>Opportunities</b> and <b>Threats</b> — so the strategy that follows starts from one honest picture. Every number here is from the Jul 26, 2026, 4:39 PM collection.</p>
+  <div class="swotgrid"><div class="swotq pos"><h3>Strengths</h3><ul><li>80,728 live SKUs across 174 vendor lines — one of the deepest designer-wallcovering catalogs online</li><li>A 281-site microsite fleet feeding discovery back to the flagship</li><li>7/7 social channels connected through one marketing command center</li><li>Private-label lines with owned margin (Phillipe Romano, Hollywood Wallcoverings, Malibu Wallpaper)</li><li>30+ years of trade relationships; memo-sample program ($4.25) as a proven funnel entry</li><li>Automated activation drip already shipping ~500 products/day, textures-first</li></ul></div><div class="swotq neg"><h3>Weaknesses</h3><ul><li>22,952 active products are sample-only WITHOUT a quote-only designation — browseable but not buyable</li><li>1,444 actives carry zero/null pricing; 333 lack a featured image</li><li>Social reach is broad (80 FB pages, 34 IG accounts) but thin — distribution without engagement depth</li><li>No social→revenue attribution discipline (UTM/coupon per channel) yet</li><li>Flagship UX carries catalog weight — 80k SKUs need stronger guided curation for retail buyers</li></ul></div><div class="swotq pos"><h3>Opportunities</h3><ul><li>Convert the 22,952 sample-only backlog into sellable variants — the single largest dormant-revenue pool in the business</li><li>20,653 staged drafts (2,784 fully stageable today) = months of "New Arrivals" marketing fuel</li><li>AEO/AI-answer citations for wallcovering queries — the flagship already ships an AEO block; extend it fleet-wide</li><li>Sample-order → full-roll conversion funnel instrumentation (order sync to the back office already exists)</li><li>Licensing original design IP (settlement-gated) into marketplaces as a zero-inventory revenue line</li></ul></div><div class="swotq neg"><h3>Threats</h3><ul><li>DTC-native entrants own the "approachable wallpaper" narrative with modern brand voices</li><li>Marketplace aggregators (Amazon, Wayfair, Etsy) commoditize mid-tier patterns</li><li>AI answer engines could disintermediate catalog-depth advantages if citations go to competitors</li><li>Vendor MAP / brand-control tightening limits pricing latitude on marquee lines</li></ul></div></div>
   <div class="flagbox" style="margin-top:34px"><div class="i">⚡</div><div><h3 class="disp">The one-sentence thesis</h3><p>DW's moat is <b>catalog depth + trade authority</b>; its constraint is <b>conversion infrastructure</b>. The fastest wins activate what already exists — the sample-only backlog, the staged-draft drip, the connected-but-quiet channels — before building anything new.</p></div></div>
 </div></section>
 
 <section id="empire"><div class="eyebrow">01 · The Catalog Empire</div><h2 class="disp">Not a store. A catalog empire.</h2><div class="accbody">
-  <p class="lead2">The live mirror shows <b>80,542</b> active products, <b>16,035</b> staged drafts, and <b>80,473</b> archived — a merchandising engine with more inventory in reserve than most competitors have in total. Distribution runs through the flagship plus a <b>281-site</b> microsite fleet.</p>
+  <p class="lead2">The live mirror shows <b>80,728</b> active products, <b>20,653</b> staged drafts, and <b>80,473</b> archived — a merchandising engine with more inventory in reserve than most competitors have in total. Distribution runs through the flagship plus a <b>281-site</b> microsite fleet.</p>
   <div class="chartwrap tall"><div class="ct">Top vendor lines by live products</div><canvas id="vendorChart"></canvas></div>
   <div class="chartwrap dgt"><div class="ct">Live catalog by product type</div><div class="cwrow"><div class="cwcanvas"><canvas id="typeChart"></canvas></div><ul class="hleg" id="typeChart-legend"></ul></div></div>
-  <table><thead><tr><th>#</th><th>Vendor line</th><th>Active products</th></tr></thead><tbody><tr><td class="mo">01</td><td><b>Phillipe Romano</b></td><td>11,270</td></tr><tr><td class="mo">02</td><td><b>Rebel Walls</b></td><td>3,770</td></tr><tr><td class="mo">03</td><td><b>Clarke And Clarke</b></td><td>3,570</td></tr><tr><td class="mo">04</td><td><b>Hollywood Wallcoverings</b></td><td>3,515</td></tr><tr><td class="mo">05</td><td><b>Malibu Wallpaper</b></td><td>3,232</td></tr><tr><td class="mo">06</td><td><b>Thibaut</b></td><td>2,987</td></tr><tr><td class="mo">07</td><td><b>Jeffrey Stevens</b></td><td>2,899</td></tr><tr><td class="mo">08</td><td><b>Brunschwig & Fils</b></td><td>2,761</td></tr><tr><td class="mo">09</td><td><b>Kravet</b></td><td>2,616</td></tr><tr><td class="mo">10</td><td><b>Phillip Jeffries</b></td><td>2,469</td></tr><tr><td class="mo">11</td><td><b>Koroseal</b></td><td>2,455</td></tr><tr><td class="mo">12</td><td><b>Gaston Y Daniela</b></td><td>2,148</td></tr><tr><td class="mo">13</td><td><b>Lee Jofa</b></td><td>2,040</td></tr><tr><td class="mo">14</td><td><b>Los Angeles Fabrics</b></td><td>1,833</td></tr></tbody></table>
+  <table><thead><tr><th>#</th><th>Vendor line</th><th>Active products</th></tr></thead><tbody><tr><td class="mo">01</td><td><b>Phillipe Romano</b></td><td>11,377</td></tr><tr><td class="mo">02</td><td><b>Rebel Walls</b></td><td>3,770</td></tr><tr><td class="mo">03</td><td><b>Clarke And Clarke</b></td><td>3,570</td></tr><tr><td class="mo">04</td><td><b>Hollywood Wallcoverings</b></td><td>3,515</td></tr><tr><td class="mo">05</td><td><b>Malibu Wallpaper</b></td><td>3,232</td></tr><tr><td class="mo">06</td><td><b>Thibaut</b></td><td>2,996</td></tr><tr><td class="mo">07</td><td><b>Jeffrey Stevens</b></td><td>2,912</td></tr><tr><td class="mo">08</td><td><b>Brunschwig & Fils</b></td><td>2,761</td></tr><tr><td class="mo">09</td><td><b>Kravet</b></td><td>2,616</td></tr><tr><td class="mo">10</td><td><b>Phillip Jeffries</b></td><td>2,469</td></tr><tr><td class="mo">11</td><td><b>Koroseal</b></td><td>2,455</td></tr><tr><td class="mo">12</td><td><b>Gaston Y Daniela</b></td><td>2,148</td></tr><tr><td class="mo">13</td><td><b>Lee Jofa</b></td><td>2,040</td></tr><tr><td class="mo">14</td><td><b>Los Angeles Fabrics</b></td><td>1,833</td></tr></tbody></table>
   <div class="eyebrow" style="margin-top:52px">Catalog DNA — what the shelves actually hold</div>
-  <p class="lead2">Tag analysis across all 80,542 actives. The palette runs <b>neutrals-first</b> — Beige + White alone account for ~44,140 products — a texture-led, trade-calibrated assortment. The merchandising read: DW owns the "quiet luxury" wall; bolder saturated stories (the DTC entrants' lane) are the underweighted shelf.</p>
+  <p class="lead2">Tag analysis across all 80,728 actives. The palette runs <b>neutrals-first</b> — Beige + White alone account for ~44,156 products — a texture-led, trade-calibrated assortment. The merchandising read: DW owns the "quiet luxury" wall; bolder saturated stories (the DTC entrants' lane) are the underweighted shelf.</p>
   <div class="duo">
     <div class="chartwrap"><div class="ct">Style mix — tag-matched across actives</div><canvas id="styleChart"></canvas></div>
     <div class="chartwrap dgt"><div class="ct">Color mix — rendered in the actual hues</div><div class="cwrow"><div class="cwcanvas"><canvas id="colorChart"></canvas></div><ul class="hleg" id="colorChart-legend"></ul></div></div>
@@ -180,13 +180,13 @@ footer{padding:56px clamp(24px,7vw,120px);border-top:1px solid var(--line);color
 </div></section>
 
 <section id="integrity"><div class="eyebrow">02 · Catalog Integrity — the 5-Field Read</div><h2 class="disp">Where the catalog leaks revenue.</h2><div class="accbody">
-  <p class="lead2">The house rule: every active product needs a sample variant, a sellable variant, complete pricing, a description, and tags — plus a featured image. Against 80,542 actives, the live counts:</p>
+  <p class="lead2">The house rule: every active product needs a sample variant, a sellable variant, complete pricing, a description, and tags — plus a featured image. Against 80,728 actives, the live counts:</p>
   <div class="chartwrap"><div class="ct">Actives failing each field (live mirror counts)</div><canvas id="ffChart"></canvas></div>
-  <div class="flagbox"><div class="i">◆</div><div><h3 class="disp">The 22,952-product finding</h3><p><b>26,742</b> actives are sample-only. <b>3,790</b> of those are quote-only lines by design. The remaining <b>22,952</b> — a third of the live catalog — can be browsed and sampled but not bought. Every one is paid-for merchandising with the register removed. This is the single largest dormant-revenue pool in the business.</p></div></div>
+  <div class="flagbox"><div class="i">◆</div><div><h3 class="disp">The 22,952-product finding</h3><p><b>26,858</b> actives are sample-only. <b>3,906</b> of those are quote-only lines by design. The remaining <b>22,952</b> — a third of the live catalog — can be browsed and sampled but not bought. Every one is paid-for merchandising with the register removed. This is the single largest dormant-revenue pool in the business.</p></div></div>
   <div class="chartwrap"><div class="ct">Sample-only actives — where they sit (top lines)</div><canvas id="soChart"></canvas></div>
-  <table><thead><tr><th>Vendor line</th><th>Sample-only actives</th></tr></thead><tbody><tr><td><b>Phillipe Romano</b></td><td>5,924</td></tr><tr><td><b>Phillip Jeffries</b></td><td>2,461</td></tr><tr><td><b>Koroseal</b></td><td>2,454</td></tr><tr><td><b>China Seas</b></td><td>1,776</td></tr><tr><td><b>Coordonné</b></td><td>1,300</td></tr><tr><td><b>Hollywood Wallcoverings</b></td><td>1,089</td></tr><tr><td><b>Designers Guild</b></td><td>840</td></tr><tr><td><b>Osborne & Little</b></td><td>820</td></tr></tbody></table>
+  <table><thead><tr><th>Vendor line</th><th>Sample-only actives</th></tr></thead><tbody><tr><td><b>Phillipe Romano</b></td><td>6,031</td></tr><tr><td><b>Phillip Jeffries</b></td><td>2,461</td></tr><tr><td><b>Koroseal</b></td><td>2,454</td></tr><tr><td><b>China Seas</b></td><td>1,776</td></tr><tr><td><b>Coordonné</b></td><td>1,300</td></tr><tr><td><b>Hollywood Wallcoverings</b></td><td>1,089</td></tr><tr><td><b>Designers Guild</b></td><td>840</td></tr><tr><td><b>Osborne & Little</b></td><td>820</td></tr></tbody></table>
   <div class="eyebrow" style="margin-top:52px">The catalog's own to-do list</div>
-  <p class="lead2">The workflow tags tell the story without an audit: <b>19,656</b> actives are tagged Needs-Width, <b>19,246</b> Needs-Price, <b>16,531</b> Needs-Image. These self-reported queues ARE the integrity sprint's work orders — pre-triaged, machine-readable, waiting.</p>
+  <p class="lead2">The workflow tags tell the story without an audit: <b>19,656</b> actives are tagged Needs-Width, <b>19,268</b> Needs-Price, <b>16,531</b> Needs-Image. These self-reported queues ARE the integrity sprint's work orders — pre-triaged, machine-readable, waiting.</p>
   <div class="chartwrap"><div class="ct">Workflow-tag backlog on active products (live tag counts)</div><canvas id="needsChart"></canvas></div>
 </div></section>
 
@@ -226,14 +226,14 @@ footer{padding:56px clamp(24px,7vw,120px);border-top:1px solid var(--line);color
 </div></section>
 
 <section id="timing"><div class="eyebrow">06 · Timing &amp; Cadence</div><h2 class="disp">The drip is live. Point it at revenue.</h2><div class="accbody">
-  <p class="lead2">The growth curve, measured: monthly activations ramped from <b>223</b> in Jan to <b>10,349</b> in Jul — a 46× ramp as the activation engine came online. The merchandising calendar below aligns campaigns to the category's natural intent cycle.</p>
+  <p class="lead2">The growth curve, measured: monthly activations ramped from <b>223</b> in Jan to <b>10,535</b> in Jul — a 47× ramp as the activation engine came online. The merchandising calendar below aligns campaigns to the category's natural intent cycle.</p>
   <div class="chartwrap"><div class="ct">The ramp — new actives per month, last 7 months (live)</div><canvas id="rampChart"></canvas></div>
   <div class="chartwrap"><div class="ct">New active products per week — last 12 weeks (live)</div><canvas id="cadenceChart"></canvas></div>
   <table><thead><tr><th>Month</th><th>What the market is doing</th><th>The DW play</th></tr></thead><tbody><tr><td class="mo">Jan</td><td>New-year refresh intent peaks</td><td class="act">Push "one statement wall" entry offers; retarget Q4 samplers</td></tr><tr><td class="mo">Feb</td><td>Design-award season</td><td class="act">Editorial: trend reports off the catalog's own tag data</td></tr><tr><td class="mo">Mar</td><td>Spring project starts</td><td class="act">Trade outreach wave; hospitality spec season begins</td></tr><tr><td class="mo">Apr</td><td>High-intent renovation search</td><td class="act">Paid search on pattern + brand terms; AEO content sprint</td></tr><tr><td class="mo">May</td><td>Peak sample-ordering</td><td class="act">Sample-bundle promos; instrument sample→roll conversion</td></tr><tr><td class="mo">Jun</td><td>Mid-year new-arrivals surge</td><td class="act">The activation drip = daily "new" stories across channels</td></tr><tr><td class="mo">Jul</td><td>Summer install season</td><td class="act">Installer-partner co-marketing; project galleries</td></tr><tr><td class="mo">Aug</td><td>Pre-fall trade buying</td><td class="act">Designer-net program push; hospitality lookbook release</td></tr><tr><td class="mo">Sep</td><td>Fall flagship season</td><td class="act">Concept-direction launch (the new front page ships here)</td></tr><tr><td class="mo">Oct</td><td>Q4 project commits</td><td class="act">Retarget spring samplers; gift-adjacent art-line cross-sell</td></tr><tr><td class="mo">Nov</td><td>Holiday statement walls</td><td class="act">Peel-and-stick line forward; fast-ship messaging</td></tr><tr><td class="mo">Dec</td><td>Planning season</td><td class="act">Year-in-review design content; January intent capture</td></tr></tbody></table>
 </div></section>
 
 <section id="audit"><div class="eyebrow">07 · Website Audit</div><h2 class="disp">A strong engine that hides its best features.</h2><div class="accbody">
-  <p class="lead2">Analyst audit of the live flagship (Shopify + Boost search): the merchandising infrastructure is genuinely advanced — the gap is guided discovery for the retail buyer facing 80,542 choices.</p>
+  <p class="lead2">Analyst audit of the live flagship (Shopify + Boost search): the merchandising infrastructure is genuinely advanced — the gap is guided discovery for the retail buyer facing 80,728 choices.</p>
   <div class="cards2">
     <div class="c"><h3>Working well</h3><p class="good">Left-rail collection navigation · shop-by-color wheel with type pills · AEO content blocks on collections · sort + density controls on grids · New Arrivals / Trending smart collections refreshed by daily tag-sync · $4.25 memo-sample funnel on every product.</p></div>
     <div class="c"><h3>Gaps</h3><p class="flag">Sample-only products present a dead-end buy box (22,952 products) · zero-price actives surface in feeds (1,444) · 333 actives lack a featured image · curation layers (rooms, moods, designer picks) are thin relative to catalog depth.</p></div>
@@ -294,8 +294,8 @@ footer{padding:56px clamp(24px,7vw,120px);border-top:1px solid var(--line);color
   <p class="lead2">Before any new audience is bought, six revenue pools sit inside the existing operation:</p>
   <div class="sugg">
     <div class="s"><div class="p">Pool 1 · largest</div><h4>The sellable-variant backlog</h4><p>22,952 active products browseable but not buyable. Even a 1% conversion at category AOV is a six-figure annual pool. Remediation is data work, not marketing spend.</p></div>
-    <div class="s"><div class="p">Pool 2 · compounding</div><h4>Sample → roll funnel</h4><p>The $4.25 memo program is the top of the funnel. <b>78,244</b> active products can be memo-sampled, but only <b>66%</b> have a roll a customer can then buy — leaving <b>22,790</b> recoverable dead-ends. Quantified below.</p></div>
-    <div class="s"><div class="p">Pool 3 · scheduled</div><h4>The staged-draft drip</h4><p>16,035 drafts activating ~500/day = a permanent "new arrivals" marketing engine. Sync email + social to the drip and every day ships its own campaign.</p></div>
+    <div class="s"><div class="p">Pool 2 · compounding</div><h4>Sample → roll funnel</h4><p>The $4.25 memo program is the top of the funnel. <b>78,430</b> active products can be memo-sampled, but only <b>66%</b> have a roll a customer can then buy — leaving <b>22,790</b> recoverable dead-ends. Quantified below.</p></div>
+    <div class="s"><div class="p">Pool 3 · scheduled</div><h4>The staged-draft drip</h4><p>20,653 drafts activating ~500/day = a permanent "new arrivals" marketing engine. Sync email + social to the drip and every day ships its own campaign.</p></div>
     <div class="s"><div class="p">Pool 4 · zero-inventory</div><h4>Original design licensing</h4><p>Settlement-gated original pattern IP licensed to print-on-demand marketplaces — revenue without inventory, already piloted in the wider operation.</p></div>
     <div class="s"><div class="p">Pool 5 · trade</div><h4>Designer-net portal</h4><p>Formalize trade pricing behind a designer login. Trade buyers are the highest-LTV segment and currently buy through the retail flow.</p></div>
     <div class="s"><div class="p">Pool 6 · projects</div><h4>Hospitality / contract desk</h4><p>Type II contract lines already in catalog. A spec-sheet + quote desk turns one hotel corridor into 400 rolls.</p></div>
@@ -304,12 +304,12 @@ footer{padding:56px clamp(24px,7vw,120px);border-top:1px solid var(--line);color
   <h3 class="disp" style="margin-top:38px">Pool 2, quantified — the sample → roll funnel</h3>
   <p class="lead2">The $4.25 memo sample is a daily purchase-intent signal. The question is whether that intent has anywhere to convert. Measured from the live catalog mirror:</p>
   <div class="kpis">
-    <div class="kpi"><div class="kv">78,244</div><div class="kl">Products a customer can memo-sample</div></div>
-    <div class="kpi"><div class="kv">66%</div><div class="kl">…that also have a buyable roll (51,664)</div></div>
+    <div class="kpi"><div class="kv">78,430</div><div class="kl">Products a customer can memo-sample</div></div>
+    <div class="kpi"><div class="kv">66%</div><div class="kl">…that also have a buyable roll (51,734)</div></div>
     <div class="kpi"><div class="kv">22,790</div><div class="kl">Recoverable dead-ends (sampled intent, no roll)</div></div>
-    <div class="kpi"><div class="kv">3,790</div><div class="kl">Quote-only by design (excluded — not a leak)</div></div>
+    <div class="kpi"><div class="kv">3,906</div><div class="kl">Quote-only by design (excluded — not a leak)</div></div>
   </div>
-  <p class="lead2">Two-thirds of the sampleable catalog converts cleanly; the remaining <b>26,580</b> products a customer can pay $4.25 to sample but cannot then buy. Of those, <b>3,790</b> are intentionally quote-only, leaving a <b>22,790</b>-product recoverable pool — concentrated in a handful of lines. Fixing the roll variant on a line a customer already sampled is data work, not ad spend.</p>
+  <p class="lead2">Two-thirds of the sampleable catalog converts cleanly; the remaining <b>26,696</b> products a customer can pay $4.25 to sample but cannot then buy. Of those, <b>3,906</b> are intentionally quote-only, leaving a <b>22,790</b>-product recoverable pool — concentrated in a handful of lines. Fixing the roll variant on a line a customer already sampled is data work, not ad spend.</p>
   <div class="chartwrap"><div class="ct">Sample → roll by line (top 8 by recoverable backlog): gold = memo converts to a buyable roll · rose = memo hits a dead end</div><canvas id="srChart"></canvas></div>
   <div class="flagbox"><div class="i">◆</div><div><h3 class="disp">Provenance — tiered honestly</h3><p><b>Tier A (measured, catalog structure):</b> the funnel counts above come straight from the read-only <code>dw_unified</code> mirror — every ACTIVE product's memo-sample and roll variant flags. Solid.<br><b>Tier B (proxy, labeled):</b> "recoverable backlog" is <i>sampleable-but-not-buyable minus the 'quotes'-tagged lines</i>. That tag under-captures reality — contract lines like Koroseal and Phillip Jeffries are quote-only in practice but untagged — so <b>22,790 is an upper bound</b>; the truly recoverable share is a subset, weighted toward the merchant private-label and residential lines.<br><b>Tier C (absent — the recommendation):</b> real memo-order-vs-roll-order <i>behavioral</i> conversion is <b>not in this mirror</b>. It is a catalog mirror — no <code>shopify_orders</code> table; <code>mp_orders</code> is 6 seed rows and <code>digital_sample_orders</code> only 25 thin digital-only rows. So the true conversion <i>rate</i> can't be computed here — <b>instrumenting the memo→roll path is itself the first move</b> (Roadmap, Weeks 3–6).</p></div></div>
 
@@ -317,11 +317,11 @@ footer{padding:56px clamp(24px,7vw,120px);border-top:1px solid var(--line);color
 </div></section>
 
 <section id="desk"><div class="eyebrow">12 · Live Commerce Desk</div><h2 class="disp">The operation, on one screen.</h2><div class="accbody">
-  <p class="lead2">Live KPIs from the Jul 26, 2026, 9:31 AM snapshot — the numbers a weekly growth review should open with. Hit ↻ Refresh (top right) to re-collect on demand.</p>
+  <p class="lead2">Live KPIs from the Jul 26, 2026, 4:39 PM snapshot — the numbers a weekly growth review should open with. Hit ↻ Refresh (top right) to re-collect on demand.</p>
   <div class="kpis">
-    <div class="kpi"><div class="kv">80,542</div><div class="kl">Active products</div></div>
-    <div class="kpi"><div class="kv">16,035</div><div class="kl">Staged drafts</div></div>
-    <div class="kpi"><div class="kv">2,778</div><div class="kl">Stageable today (variant + image)</div></div>
+    <div class="kpi"><div class="kv">80,728</div><div class="kl">Active products</div></div>
+    <div class="kpi"><div class="kv">20,653</div><div class="kl">Staged drafts</div></div>
+    <div class="kpi"><div class="kv">2,784</div><div class="kl">Stageable today (variant + image)</div></div>
     <div class="kpi"><div class="kv">22,952</div><div class="kl">Sample-only backlog (non-quote)</div></div>
     <div class="kpi"><div class="kv">1,444</div><div class="kl">Zero-price actives</div></div>
     <div class="kpi"><div class="kv">333</div><div class="kl">Image-less actives</div></div>
@@ -333,7 +333,7 @@ footer{padding:56px clamp(24px,7vw,120px);border-top:1px solid var(--line);color
 
 <section id="roadmap"><div class="eyebrow">13 · 90-Day Roadmap</div><h2 class="disp">What to do, in order.</h2><div class="accbody">
   <ul class="road">
-    <li><div class="w">Weeks 1–2</div><div class="d"><b>Integrity sprint:</b> triage the 22,952 sample-only backlog by vendor (top 8 lines = 16,664 products); fix zero-price and image-less actives. Wire per-channel UTM + coupon attribution. Pick a concept direction from Section 09.<br><span style="color:var(--gold);font-size:14px">KPI · 2,500+ sellable variants added · zero-price actives &lt; 500 · UTM live on all 7 channels · concept direction chosen (Week-2 decision gate)</span></div></li>
+    <li><div class="w">Weeks 1–2</div><div class="d"><b>Integrity sprint:</b> triage the 22,952 sample-only backlog by vendor (top 8 lines = 16,771 products); fix zero-price and image-less actives. Wire per-channel UTM + coupon attribution. Pick a concept direction from Section 09.<br><span style="color:var(--gold);font-size:14px">KPI · 2,500+ sellable variants added · zero-price actives &lt; 500 · UTM live on all 7 channels · concept direction chosen (Week-2 decision gate)</span></div></li>
     <li><div class="w">Weeks 3–6</div><div class="d"><b>Convert intent:</b> ship the sampled-but-didn't-buy email sequence off the memo funnel. Sync daily social posting to the activation drip. Start the designer-net trade portal build.<br><span style="color:var(--gold);font-size:14px">KPI · first attributed sample→roll conversion · sequence live with ≥25% open rate · 7 drip-synced posts/week shipping · re-collect this brief and re-baseline (↻)</span></div></li>
     <li><div class="w">Weeks 6–10</div><div class="d"><b>Ship the face:</b> launch the chosen flagship direction (guided discovery: rooms, moods, designer picks). Extend AEO blocks fleet-wide. Sign 3 unlikely-partner deals (one installer network, one stager program, one hospitality designer).<br><span style="color:var(--gold);font-size:14px">KPI · new front page live · AEO blocks on 100+ fleet sites · 3 signed partner agreements · backlog under 15,000</span></div></li>
     <li><div class="w">Weeks 10–13</div><div class="d"><b>Measure &amp; double down:</b> channel-level revenue review off the new attribution; scale the winner. Open the contract/hospitality quote desk. Publish the first licensing-line results.<br><span style="color:var(--gold);font-size:14px">KPI · every channel shows attributed revenue (or is cut) · first contract quote issued · Week-13 review: backlog, conversion rate, channel ROI vs this brief's baseline</span></div></li>
@@ -341,13 +341,13 @@ footer{padding:56px clamp(24px,7vw,120px);border-top:1px solid var(--line);color
 </div></section>
 
 <section id="sources"><div class="eyebrow">14 · Sources &amp; Methodology</div><h2 class="disp">Where every number comes from.</h2><div class="accbody">
-  <p class="lead2">Snapshot collected <b>Jul 26, 2026, 9:31 AM</b> by a read-only collector. Live rows are measured; analyst rows are authored judgment; anything illustrative says so.</p>
+  <p class="lead2">Snapshot collected <b>Jul 26, 2026, 4:39 PM</b> by a read-only collector. Live rows are measured; analyst rows are authored judgment; anything illustrative says so.</p>
   <div class="srclegend"><span class="srcb r">Live snapshot</span><span class="srcb o">Observed live</span><span class="srcb a">Analyst view</span><span class="srcb d">Computed</span><span class="srcb e">Illustrative</span></div>
-  <table><thead><tr><th>Section</th><th>Where it comes from</th><th>How it's produced</th><th>Provenance</th></tr></thead><tbody><tr><td><b>Catalog KPIs — statuses, vendors, types, integrity, cadence</b></td><td>The dw_unified local mirror (READ-ONLY psql over the /tmp socket)</td><td>Collected by <code>scripts/collect-dw.mjs</code> at <b>Jul 26, 2026, 9:31 AM</b>; every number on this page renders from that snapshot</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>5-field / sample-only split</b></td><td>Mirror rollup columns (has_sample_variant, has_product_variant, has_description, image_url) + the quotes tag</td><td>Direct SQL counts; quote-only lines (by-design sample-only) separated from true backlog</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Social channel status</b></td><td>The DW marketing command center API (marketing.designerwallcoverings.com)</td><td>Authenticated status read at collection time — connected flags + account counts per platform</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Microsite fleet</b></td><td>The fleet aggregator directory API (all.designerwallcoverings.com)</td><td>281 sites enumerated at collection time</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Competitor watch-list</b></td><td>The connie_competitors table (the DW competitor-scan registry)</td><td>Rendered verbatim: domain, watch status, products found by prior scans</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Competitor paid-ads table</b></td><td>A $0 homepage pixel scan of each tracked domain (Google/Meta/TikTok/Pinterest/Bing tag signatures)</td><td>Measured at seed time by <code>scripts/seed-buckets.mjs</code>; unreachable homepages say so rather than guessing</td><td class="srccell"><span class="srcb o">Observed live</span></td></tr><tr><td><b>Competitor scoring chart + map pins</b></td><td>Analyst assessment over public sites; HQ geo compiled from public records</td><td>Authored consulting view — NOT scraped metrics; the map is oriented, not surveyed</td><td class="srccell"><span class="srcb a">Analyst view</span></td></tr><tr><td><b>Price-band chart</b></td><td>products whose minimum variant exceeds the $4.25 memo sample — the priced subset of the mirror (~3.8k); most actives resolve min-variant to the sample, and full variant-level pricing lives Shopify-side</td><td>Partial-basis chart, labeled as such — full variant pricing lives Shopify-side</td><td class="srccell"><span class="srcb d">Computed</span></td></tr><tr><td><b>Website & social audits, partners, revenue plays, roadmap</b></td><td>Analyst strategy grounded in the operating knowledge of the DW stack</td><td>Consulting recommendations — models and plays, not measurements</td><td class="srccell"><span class="srcb a">Analyst view</span></td></tr><tr><td><b>Catalog DNA — style, color & workflow-tag charts</b></td><td>Tag-match SQL over all actives (the same tag buckets the storefront sort uses)</td><td>Live counts per style/color/Needs-* tag; the color doughnut renders in representative hues of the tag names</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Monthly activation ramp</b></td><td>created_at_shopify over the last 7 calendar months, live mirror</td><td>Direct SQL; the ramp multiplier in the copy is computed from the same series</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Product mosaic + concept-grid imagery</b></td><td>The store's own CDN (one current ACTIVE product per vendor line, forbidden-vendor-filtered)</td><td>Already customer-facing on the live store; hotlinked at thumbnail width</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Traffic & Search Reality</b></td><td>Google Search Console via the cached DW SEO report (age-labeled in the section footer)</td><td>Totals, branded split, striking-distance and page tables rendered verbatim; the decline copy is computed from the same monthly series</td><td class="srccell"><span class="srcb o">Observed live</span></td></tr><tr><td><b>Hero imagery</b></td><td>Unsplash editorial stock (luxury materials, per the luxe-only imagery rule)</td><td>Hotlinked, decorative only — carries no data meaning</td><td class="srccell"><span class="srcb e">Illustrative</span></td></tr></tbody></table>
+  <table><thead><tr><th>Section</th><th>Where it comes from</th><th>How it's produced</th><th>Provenance</th></tr></thead><tbody><tr><td><b>Catalog KPIs — statuses, vendors, types, integrity, cadence</b></td><td>The dw_unified local mirror (READ-ONLY psql over the /tmp socket)</td><td>Collected by <code>scripts/collect-dw.mjs</code> at <b>Jul 26, 2026, 4:39 PM</b>; every number on this page renders from that snapshot</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>5-field / sample-only split</b></td><td>Mirror rollup columns (has_sample_variant, has_product_variant, has_description, image_url) + the quotes tag</td><td>Direct SQL counts; quote-only lines (by-design sample-only) separated from true backlog</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Social channel status</b></td><td>The DW marketing command center API (marketing.designerwallcoverings.com)</td><td>Authenticated status read at collection time — connected flags + account counts per platform</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Microsite fleet</b></td><td>The fleet aggregator directory API (all.designerwallcoverings.com)</td><td>281 sites enumerated at collection time</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Competitor watch-list</b></td><td>The connie_competitors table (the DW competitor-scan registry)</td><td>Rendered verbatim: domain, watch status, products found by prior scans</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Competitor paid-ads table</b></td><td>A $0 homepage pixel scan of each tracked domain (Google/Meta/TikTok/Pinterest/Bing tag signatures)</td><td>Measured at seed time by <code>scripts/seed-buckets.mjs</code>; unreachable homepages say so rather than guessing</td><td class="srccell"><span class="srcb o">Observed live</span></td></tr><tr><td><b>Competitor scoring chart + map pins</b></td><td>Analyst assessment over public sites; HQ geo compiled from public records</td><td>Authored consulting view — NOT scraped metrics; the map is oriented, not surveyed</td><td class="srccell"><span class="srcb a">Analyst view</span></td></tr><tr><td><b>Price-band chart</b></td><td>products whose minimum variant exceeds the $4.25 memo sample — the priced subset of the mirror (~3.8k); most actives resolve min-variant to the sample, and full variant-level pricing lives Shopify-side</td><td>Partial-basis chart, labeled as such — full variant pricing lives Shopify-side</td><td class="srccell"><span class="srcb d">Computed</span></td></tr><tr><td><b>Website & social audits, partners, revenue plays, roadmap</b></td><td>Analyst strategy grounded in the operating knowledge of the DW stack</td><td>Consulting recommendations — models and plays, not measurements</td><td class="srccell"><span class="srcb a">Analyst view</span></td></tr><tr><td><b>Catalog DNA — style, color & workflow-tag charts</b></td><td>Tag-match SQL over all actives (the same tag buckets the storefront sort uses)</td><td>Live counts per style/color/Needs-* tag; the color doughnut renders in representative hues of the tag names</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Monthly activation ramp</b></td><td>created_at_shopify over the last 7 calendar months, live mirror</td><td>Direct SQL; the ramp multiplier in the copy is computed from the same series</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Product mosaic + concept-grid imagery</b></td><td>The store's own CDN (one current ACTIVE product per vendor line, forbidden-vendor-filtered)</td><td>Already customer-facing on the live store; hotlinked at thumbnail width</td><td class="srccell"><span class="srcb r">Live snapshot</span></td></tr><tr><td><b>Traffic & Search Reality</b></td><td>Google Search Console via the cached DW SEO report (age-labeled in the section footer)</td><td>Totals, branded split, striking-distance and page tables rendered verbatim; the decline copy is computed from the same monthly series</td><td class="srccell"><span class="srcb o">Observed live</span></td></tr><tr><td><b>Hero imagery</b></td><td>Unsplash editorial stock (luxury materials, per the luxe-only imagery rule)</td><td>Hotlinked, decorative only — carries no data meaning</td><td class="srccell"><span class="srcb e">Illustrative</span></td></tr></tbody></table>
   <div class="srcnote"><div class="i">&#9881;</div><div><h4>How this document is built</h4><p>Generated, not hand-typed. A read-only collector (<code>scripts/collect-dw.mjs</code>) snapshots the DW systems into <code>data/dw-analysis.json</code>; the build (<code>build.mjs</code>) renders that snapshot into this page. The ↻ Refresh button re-runs collector + build on demand. Nothing writes to any DW production system.</p></div></div>
 </div></section>
 
-<footer><span>Designer Wallcoverings · Growth Strategy Brief</span><span>Data as of Jul 26, 2026, 9:31 AM</span><span>Confidential — for discussion</span></footer>
+<footer><span>Designer Wallcoverings · Growth Strategy Brief</span><span>Data as of Jul 26, 2026, 4:39 PM</span><span>Confidential — for discussion</span></footer>
 
 <script>
 (function(){var s=document.querySelectorAll('.hero .slide'),i=0;if(s.length<2)return;setInterval(function(){s[i].classList.remove('on');i=(i+1)%s.length;s[i].classList.add('on');},5200);})();
@@ -374,7 +374,7 @@ function initCharts(){if(charted||!window.Chart)return;charted=true;
   var RM=window.matchMedia&&window.matchMedia('(prefers-reduced-motion:reduce)').matches;
   Chart.defaults.animation=RM?false:{duration:1150,easing:'easeInOutQuart'};
   if(!RM){Chart.defaults.animations={y:{from:function(c){return c.chart.scales.y?c.chart.scales.y.getPixelForValue(0):0;}}};}
-  var D={"cadence":{"labels":["05-11","05-25","06-01","06-08","06-15","06-22","06-29","07-06","07-13","07-20"],"added":[490,3,492,924,1993,1510,1854,4260,1486,3377]},"vendors":{"labels":["Phillipe Romano","Rebel Walls","Clarke And Clarke","Hollywood Wallcoverings","Malibu Wallpaper","Thibaut","Jeffrey Stevens","Brunschwig & Fils","Kravet","Phillip Jeffries"],"counts":[11270,3770,3570,3515,3232,2987,2899,2761,2616,2469]},"types":{"labels":["Wallcovering","Fabric","Commercial Fabric","Mural","Wallcoverings","Memo Sample"],"counts":[58298,16200,1797,1305,453,451]},"integrity":{"labels":["Missing sellable variant","Missing sample variant","Zero / null price","Missing image","Missing description"],"counts":[26742,2298,1444,333,172]},"bands":{"labels":["Under $50","$50–150","$150–300","$300–600","$600+"],"counts":[2118,699,435,443,129]},"channels":{"labels":["Facebook Pages","Instagram","TikTok","YouTube","Bluesky","Threads","LinkedIn"],"accounts":[80,34,1,1,1,1,1]},"sampleOnlyVendors":{"labels":["Phillipe Romano","Phillip Jeffries","Koroseal","China Seas","Coordonné","Hollywood Wallcoverings","Designers Guild","Osborne & Little"],"counts":[5924,2461,2454,1776,1300,1089,840,820]},"sampleRoll":{"labels":["Phillipe Romano","Phillip Jeffries","Koroseal","China Seas","Coordonné","Hollywood Wallcoverings","Designers Guild","Osborne & Little"],"converted":[4064,8,1,0,131,2426,0,111],"leak":[3521,2461,2368,1776,1300,842,839,820]},"comp":{"labels":["Designer Wallcoverings","DecoratorsBest","Burke Decor","Mahones","Finest Wallpaper","Steve's B&W"],"catalog":[96,62,48,55,40,45],"digital":[58,82,78,60,42,55]},"monthly":{"labels":["Jan","Feb","Mar","Apr","May","Jun","Jul"],"added":[223,1363,4578,3416,493,5547,10349]},"needs":{"labels":["Needs Price","Needs Image","Needs Width","Needs Description"],"counts":[19246,16531,19656,27]},"styles":{"labels":["Contemporary","Textured","Traditional","Geometric","Floral","Grasscloth","Stripe","Mural","Metallic","Damask"],"counts":[32580,29302,22704,11748,10286,9846,9153,4769,3212,1926]},"colors":{"labels":["Beige","White","Gray","Blue","Green","Brown","Black","Gold","Pink","Red"],"counts":[22079,22061,12163,8551,7749,5192,4883,4326,3456,2946],"hex":["#d9c9a8","#f2efe8","#9a9a99","#5a7fa8","#6b8f6d","#8a6a4c","#2b2b2b","#c9a24b","#d3a0a6","#a84a44"]}};
+  var D={"cadence":{"labels":["05-11","05-25","06-01","06-08","06-15","06-22","06-29","07-06","07-13","07-20"],"added":[490,3,492,924,1993,1510,1854,4269,1499,3541]},"vendors":{"labels":["Phillipe Romano","Rebel Walls","Clarke And Clarke","Hollywood Wallcoverings","Malibu Wallpaper","Thibaut","Jeffrey Stevens","Brunschwig & Fils","Kravet","Phillip Jeffries"],"counts":[11377,3770,3570,3515,3232,2996,2912,2761,2616,2469]},"types":{"labels":["Wallcovering","Fabric","Commercial Fabric","Mural","Wallcoverings","Memo Sample"],"counts":[58475,16209,1797,1305,453,451]},"integrity":{"labels":["Missing sellable variant","Missing sample variant","Zero / null price","Missing image","Missing description"],"counts":[26858,2298,1444,333,172]},"bands":{"labels":["Under $50","$50–150","$150–300","$300–600","$600+"],"counts":[2118,699,435,443,129]},"channels":{"labels":["Facebook Pages","Instagram","TikTok","YouTube","Bluesky","Threads","LinkedIn"],"accounts":[80,34,1,1,1,1,1]},"sampleOnlyVendors":{"labels":["Phillipe Romano","Phillip Jeffries","Koroseal","China Seas","Coordonné","Hollywood Wallcoverings","Designers Guild","Osborne & Little"],"counts":[6031,2461,2454,1776,1300,1089,840,820]},"sampleRoll":{"labels":["Phillipe Romano","Phillip Jeffries","Koroseal","China Seas","Coordonné","Hollywood Wallcoverings","Designers Guild","Osborne & Little"],"converted":[4064,8,1,0,131,2426,0,111],"leak":[3521,2461,2368,1776,1300,842,839,820]},"comp":{"labels":["Designer Wallcoverings","DecoratorsBest","Burke Decor","Mahones","Finest Wallpaper","Steve's B&W"],"catalog":[96,62,48,55,40,45],"digital":[58,82,78,60,42,55]},"monthly":{"labels":["Jan","Feb","Mar","Apr","May","Jun","Jul"],"added":[223,1363,4578,3416,493,5547,10535]},"needs":{"labels":["Needs Price","Needs Image","Needs Width","Needs Description"],"counts":[19268,16531,19656,27]},"styles":{"labels":["Contemporary","Textured","Traditional","Geometric","Floral","Grasscloth","Stripe","Mural","Metallic","Damask"],"counts":[32588,29302,22744,11749,10297,9846,9153,4817,3212,1926]},"colors":{"labels":["Beige","White","Gray","Blue","Green","Brown","Black","Gold","Pink","Red"],"counts":[22093,22063,12169,8555,7755,5197,4885,4328,3458,2947],"hex":["#d9c9a8","#f2efe8","#9a9a99","#5a7fa8","#6b8f6d","#8a6a4c","#2b2b2b","#c9a24b","#d3a0a6","#a84a44"]}};
   var TRD={"labels":["Jan","Feb","Mar","Apr","May","Jun","Jul"],"clicks":[4790,8903,7318,5477,4908,4249,2335]};
   function mk(id,cfg){var el=document.getElementById(id);if(el)new Chart(el,cfg);}
   // Custom HTML legend for the doughnuts — thin 2x16px line markers instead of box swatches, click-to-toggle
@@ -409,7 +409,7 @@ function initCharts(){if(charted||!window.Chart)return;charted=true;
 }
 var mapped=false;
 function initMap(){if(mapped||!window.L||!document.getElementById('compmap'))return;mapped=true;
-  var pts=[{"name":"Designer Wallcoverings — Los Angeles","lat":34.076,"lng":-118.377,"you":true,"note":"HQ · showroom · 80,542 live SKUs"},{"name":"DecoratorsBest","lat":40.752,"lng":-73.993,"note":"New York, NY — designer-brand e-commerce"},{"name":"Burke Decor","lat":41.099,"lng":-80.649,"note":"Youngstown, OH — home decor + wallpaper"},{"name":"Mahone's Wallpaper Shop","lat":37.089,"lng":-76.473,"note":"Newport News, VA — wallpaper specialist"},{"name":"Finest Wallpaper","lat":43.09,"lng":-79.08,"note":"Ontario, Canada — designer lines, US/CA pricing"},{"name":"Steve's Blinds & Wallpaper","lat":42.58,"lng":-83.03,"note":"Sterling Heights, MI — discount volume"}];
+  var pts=[{"name":"Designer Wallcoverings — Los Angeles","lat":34.076,"lng":-118.377,"you":true,"note":"HQ · showroom · 80,728 live SKUs"},{"name":"DecoratorsBest","lat":40.752,"lng":-73.993,"note":"New York, NY — designer-brand e-commerce"},{"name":"Burke Decor","lat":41.099,"lng":-80.649,"note":"Youngstown, OH — home decor + wallpaper"},{"name":"Mahone's Wallpaper Shop","lat":37.089,"lng":-76.473,"note":"Newport News, VA — wallpaper specialist"},{"name":"Finest Wallpaper","lat":43.09,"lng":-79.08,"note":"Ontario, Canada — designer lines, US/CA pricing"},{"name":"Steve's Blinds & Wallpaper","lat":42.58,"lng":-83.03,"note":"Sterling Heights, MI — discount volume"}];
   var m=L.map('compmap',{scrollWheelZoom:false}).setView([38.5,-96],4);
   L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',{attribution:'© OpenStreetMap © CARTO',maxZoom:18}).addTo(m);
   pts.forEach(function(p){var mk=L.circleMarker([p.lat,p.lng],{radius:p.you?10:7,color:p.you?'#c9a24b':'#78aaff',fillColor:p.you?'#c9a24b':'#78aaff',fillOpacity:.85,weight:1});
diff --git a/scripts/collect-dw.mjs b/scripts/collect-dw.mjs
index d8b4c0d..872de21 100644
--- a/scripts/collect-dw.mjs
+++ b/scripts/collect-dw.mjs
@@ -15,8 +15,17 @@ const OUT = join(HERE, '..', 'data');
 mkdirSync(OUT, { recursive: true });
 
 const AUTH = 'Basic ' + Buffer.from('admin:DW2024!').toString('base64');
-// One shared forbidden-name lens for every ingested surface (hosts, notes, gallery)
-const FORBIDDEN_NAMES = /schumacher|momentum|versa|wallquest|chesapeake|seabrook|nextwall|command54|greenland|rigo|tokiwa|justin.?david/i;
+// One canonical forbidden-name list. Every downstream filter (product rows, fleet
+// hostnames, competitor note scrubbing) derives its regex from this single source
+// so additions here propagate everywhere automatically.
+const FORBIDDEN_TERMS = ['schumacher', 'momentum', 'versa', 'wallquest', 'chesapeake',
+  'seabrook', 'nextwall', 'command54', 'greenland', 'rigo', 'tokiwa', 'justin.?david'];
+// General name match — for product vendor/title strings (allows "justin david" with space).
+const FORBIDDEN_NAMES = new RegExp(FORBIDDEN_TERMS.join('|'), 'i');
+// Word-boundary form — for scrubbing forbidden terms out of competitor note text.
+const SCRUB = new RegExp('\\b(' + FORBIDDEN_TERMS.join('|') + ')\\b,?\\s*', 'gi');
+// Hostname form — no spaces in URLs; collapse justin.?david to the no-space variants.
+const FORBIDDEN = new RegExp(FORBIDDEN_TERMS.map(t => t.replace('justin.?david', 'justindavid|justin-david')).join('|'), 'i');
 
 function sql(q) {
   try {
@@ -160,7 +169,6 @@ const priceBands = sql(`select band, count(*) from (
 const drafts = (statuses.DRAFT || 0);
 const staged = sql(`select count(*) from shopify_products where upper(status)='DRAFT' and coalesce(has_product_variant,false) and image_url is not null`)[0]?.[0];
 
-const SCRUB = /\b(schumacher|momentum|versa|wallquest|chesapeake|seabrook|nextwall|command54|greenland|rigo|tokiwa|justin david)\b,?\s*/gi;
 const competitors = sql(`select name, domain, enabled, coalesce(products_found,0), coalesce(notes,'') from connie_competitors order by products_found desc`)
   .map(([name, domain, enabled, found, notes]) => ({ name, domain, enabled: enabled === 't', products_found: +found, notes: String(notes).replace(SCRUB, '').replace(/,\s*$/, '').replace(/\s{2,}/g, ' ') }));
 
@@ -184,7 +192,7 @@ const siteList = Array.isArray(microsites) ? microsites
 // Hard rule: private-label upstream / internal-only vendor names must never
 // render on any front end, gated or not — filter at the DATA layer so no
 // downstream surface (fleet chips, JSON export) can leak them.
-const FORBIDDEN = /schumacher|momentum|versa|wallquest|chesapeake|seabrook|nextwall|command54|greenland|rigo|tokiwa|justindavid|justin-david/i;
+// FORBIDDEN is derived from FORBIDDEN_TERMS (hostname form) at the top of this file.
 const fleet = (Array.isArray(siteList) ? siteList : []).map(s => {
   // Raw product arrays (titles + upstream image URLs) must never enter the
   // snapshot; and an array length is just the feed's page size (24), not real
diff --git a/scripts/seed-buckets.mjs b/scripts/seed-buckets.mjs
index 2238344..3b7bc91 100644
--- a/scripts/seed-buckets.mjs
+++ b/scripts/seed-buckets.mjs
@@ -23,7 +23,7 @@ const socials = {
   platforms: (SNAP.channels || []).map(c => ({
     key: c.key, label: c.label || c.key, connected: !!c.connected,
     accounts: c.accounts || 0,
-    url: socialUrls[c.key] || socialUrls[c.key === 'facebook' ? 'facebook' : c.key] || '',
+    url: socialUrls[c.key] || '',
   })),
 };
 
@@ -61,10 +61,14 @@ async function scanDomain(domain) {
   } catch (e) { return { ok: false, pixels: [], error: String(e.message || e).slice(0, 80) }; }
 }
 
+// Extract paid-ad pixels (everything except analytics-only 'ga4').
+const adPixels = scan => scan.pixels.filter(p => p !== 'ga4');
+
 const comps = [];
 const ads = [];
 for (const c of (SNAP.competitors || [])) {
   const scan = await scanDomain(c.domain);
+  const paid = adPixels(scan);
   comps.push({
     id: 'comp-' + c.domain, name: c.name, domain: c.domain,
     watch: c.enabled ? 'active' : 'paused', products_found: c.products_found,
@@ -72,10 +76,10 @@ for (const c of (SNAP.competitors || [])) {
   });
   ads.push({
     domain: c.domain, name: c.name,
-    ad_pixels: scan.pixels.filter(p => p !== 'ga4'),
+    ad_pixels: paid,
     analytics: scan.pixels.includes('ga4'),
     read: scan.ok
-      ? (scan.pixels.filter(p => p !== 'ga4').length ? 'Paying to advertise (' + scan.pixels.filter(p => p !== 'ga4').join(', ') + ')' : 'No paid-ad pixels detected on homepage')
+      ? (paid.length ? 'Paying to advertise (' + paid.join(', ') + ')' : 'No paid-ad pixels detected on homepage')
       : 'Homepage unreachable to scanner (' + (scan.error || '') + ')',
     scanned_at: new Date().toISOString(),
     method: '$0 homepage pixel scan (advertising-signals pattern)',
@@ -84,7 +88,13 @@ for (const c of (SNAP.competitors || [])) {
 }
 // DW itself, same lens
 const self = await scanDomain('www.designerwallcoverings.com');
-ads.unshift({ domain: 'designerwallcoverings.com', name: 'Designer Wallcoverings (you)', ad_pixels: self.pixels.filter(p => p !== 'ga4'), analytics: self.pixels.includes('ga4'), read: self.pixels.filter(p => p !== 'ga4').length ? 'Running: ' + self.pixels.filter(p => p !== 'ga4').join(', ') : 'No paid-ad pixels detected on homepage', scanned_at: new Date().toISOString(), method: '$0 homepage pixel scan' });
+const selfPaid = adPixels(self);
+ads.unshift({
+  domain: 'designerwallcoverings.com', name: 'Designer Wallcoverings (you)',
+  ad_pixels: selfPaid, analytics: self.pixels.includes('ga4'),
+  read: selfPaid.length ? 'Running: ' + selfPaid.join(', ') : 'No paid-ad pixels detected on homepage',
+  scanned_at: new Date().toISOString(), method: '$0 homepage pixel scan',
+});
 
 /* ---- content calendar: DRAFT-ONLY, drip-aligned ----------------------------- */
 const cal = [];
diff --git a/server.js b/server.js
index 9324155..4f5eab4 100644
--- a/server.js
+++ b/server.js
@@ -16,6 +16,7 @@
 const fs = require('fs');
 const path = require('path');
 const express = require('express');
+const { execFile } = require('node:child_process');
 try { require('dotenv').config(); } catch { /* dotenv optional */ }
 
 const app = express();
@@ -73,13 +74,14 @@ let refreshing = false;
 app.post('/api/refresh', gate, (_req, res) => {
   if (refreshing) return res.status(429).json({ error: 'refresh already running' });
   refreshing = true;
-  const { execFile } = require('node:child_process');
   execFile('node', [path.join(__dirname, 'scripts', 'collect-dw.mjs')], { timeout: 120000 }, (e1, out1) => {
     if (e1) { refreshing = false; return res.status(500).json({ error: 'collect failed', detail: String(e1).slice(0, 200) }); }
     execFile('node', [path.join(__dirname, 'build.mjs')], { timeout: 120000 }, (e2) => {
       refreshing = false;
       if (e2) return res.status(500).json({ error: 'build failed', detail: String(e2).slice(0, 200) });
-      res.json({ ok: true, collected: JSON.parse(out1 || '{}') });
+      let collected = {};
+      try { collected = JSON.parse(out1 || '{}'); } catch { /* stray psql warning polluted stdout */ }
+      res.json({ ok: true, collected });
     });
   });
 });

← f0b7063 TK-17 Phase 0: per-line GO/NO-GO decision sheet for the 22,9  ·  back to Consulting Designerwallcoverings Com  ·  portal P2 polish: gold-styled density slider (matches select bd9f352 →