[object Object]

← back to Dw Yolo Loop

Artmura site (B/catalog): dominant-color extraction → true-hue color sort + color dots + color filter bar

3e3a3bd4ee8bfb8d022b06b5a242af566ee03993 · 2026-06-12 09:25:17 -0700 · Steve Abrams

- extract_colors.py: PIL dominant hex+hue+bucket per swatch (161); neutrals-first color-wheel sort
- color dot on every card; color-filter chip bar (grey 62, orange 26, white 20, teal 10...) wired to /api/facets
- honors standing color-sort rule with real per-SKU hue, not name-matching

Files touched

Diff

commit 3e3a3bd4ee8bfb8d022b06b5a242af566ee03993
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jun 12 09:25:17 2026 -0700

    Artmura site (B/catalog): dominant-color extraction → true-hue color sort + color dots + color filter bar
    
    - extract_colors.py: PIL dominant hex+hue+bucket per swatch (161); neutrals-first color-wheel sort
    - color dot on every card; color-filter chip bar (grey 62, orange 26, white 20, teal 10...) wired to /api/facets
    - honors standing color-sort rule with real per-SKU hue, not name-matching
---
 artmura-site/public/index.html                   |   34 +-
 artmura-site/public/styles.css                   |   13 +-
 artmura-site/server.js                           |   10 +-
 scripts/artmura-onboard/data/artmura-colors.json | 1129 ++++++++++++++++++++++
 scripts/artmura-onboard/extract_colors.py        |   55 ++
 5 files changed, 1231 insertions(+), 10 deletions(-)

diff --git a/artmura-site/public/index.html b/artmura-site/public/index.html
index ac64e93..9edd064 100644
--- a/artmura-site/public/index.html
+++ b/artmura-site/public/index.html
@@ -65,6 +65,9 @@
   </div>
 </div>
 
+<!-- COLOR FILTER -->
+<div class="colorbar" id="colorbar"></div>
+
 <!-- GRID -->
 <main class="grid-wrap">
   <div class="grid" id="grid"></div>
@@ -84,8 +87,9 @@
 
 <script>
 const $=s=>document.querySelector(s), grid=$('#grid');
-let ALL=[], view=[], activeBook=null;
+let ALL=[], view=[], activeBook=null, activeColor=null, FACETS=null;
 const LS={sort:'artmura.sort', dens:'artmura.density'};
+const BUCKET_HEX={white:'#efeae0',grey:'#9e978c',black:'#2a2722',red:'#a23b2e',orange:'#c8763a',brown:'#7b5a3c',gold:'#b8923f',green:'#5d7150',teal:'#3f8a86',blue:'#5a7494',purple:'#7a5f86',pink:'#bb8aa0'};
 
 const heroBg = url => `linear-gradient(0deg,rgba(0,0,0,0),rgba(0,0,0,0)),url("${url}")`;
 
@@ -103,14 +107,19 @@ function startHero(items){
   let i=0; if(slides.length>1) setInterval(()=>{slides[i].classList.remove('on');i=(i+1)%slides.length;slides[i].classList.add('on');},5200);
 }
 
-const COLOR_ORDER=['white','off-white','cream','beige','sand','silver','grey','gray','taupe','gold','golden','green','blue','navy','red','brown','charcoal','black'];
-function colorRank(c){const k=(c||'').toLowerCase();const i=COLOR_ORDER.findIndex(x=>k.includes(x));return i<0?99:i;}
-
+// true color-wheel order: neutrals first (by lightness), then chromatic by hue
+function colorKey(p){
+  const b=p.color_bucket, hue=p.hue??999, sat=p.sat??0;
+  if(b==='white')return [0, p.val??1];
+  if(b==='grey') return [1, p.val??.5];
+  if(b==='black')return [2, -(p.val??0)];
+  return [3, hue]; // chromatic by hue angle
+}
 function sortItems(items,mode){
   const a=[...items];
   const num=p=>p.price||0;
   switch(mode){
-    case 'color': a.sort((x,y)=>colorRank(x.color)-colorRank(y.color)||x.series.localeCompare(y.series));break;
+    case 'color': a.sort((x,y)=>{const kx=colorKey(x),ky=colorKey(y);return kx[0]-ky[0]||kx[1]-ky[1];});break;
     case 'series': a.sort((x,y)=>(x.series||'').localeCompare(y.series)|| (x.color||'').localeCompare(y.color));break;
     case 'sku': a.sort((x,y)=>(x.sku||'').localeCompare(y.sku));break;
     case 'title': a.sort((x,y)=>(x.title||'').localeCompare(y.title));break;
@@ -122,7 +131,9 @@ function sortItems(items,mode){
 }
 
 function render(){
-  let items = activeBook ? ALL.filter(p=>p.book===activeBook) : ALL;
+  let items = ALL;
+  if(activeBook) items = items.filter(p=>p.book===activeBook);
+  if(activeColor) items = items.filter(p=>p.color_bucket===activeColor);
   items = sortItems(items, $('#sort').value);
   view=items;
   $('#cnt').textContent=items.length;
@@ -133,7 +144,7 @@ function render(){
         <div class="ser">${p.series||''}</div>
         <div class="nm">${p.color||p.title}</div>
         <div class="meta">
-          <span class="col">${p.book||'Artmura'}</span>
+          <span class="col">${p.hex?`<span class="dot" style="background:${p.hex}"></span>`:''}${p.book||'Artmura'}</span>
           <span class="pr">$${(p.price||0).toFixed(0)} <small>/yd</small></span>
         </div>
       </div>
@@ -147,6 +158,13 @@ function chips(facets){
   wrap.innerHTML=all+facets.books.map(([b,n])=>`<div class="chip ${activeBook===b?'active':''}" data-b="${b}">${b}<span class="n">${n}</span></div>`).join('');
   wrap.querySelectorAll('.chip').forEach(ch=>ch.onclick=()=>{activeBook=ch.dataset.b||null;chips(facets);render();$('#catalog').scrollIntoView({behavior:'smooth'});});
 }
+function colorbar(facets){
+  const wrap=$('#colorbar');
+  if(!facets.colors||!facets.colors.length){wrap.style.display='none';return;}
+  const all=`<button class="cdot all ${!activeColor?'active':''}" data-c="" title="All colors">All</button>`;
+  wrap.innerHTML=all+facets.colors.map(([c,n])=>`<button class="cdot ${activeColor===c?'active':''}" data-c="${c}" title="${c} (${n})"><span style="background:${BUCKET_HEX[c]||'#ccc'}"></span>${c}</button>`).join('');
+  wrap.querySelectorAll('.cdot').forEach(b=>b.onclick=()=>{activeColor=b.dataset.c||null;colorbar(facets);render();});
+}
 
 // density + sort persistence
 function applyDensity(v){document.documentElement.style.setProperty('--cols',v);}
@@ -160,7 +178,7 @@ addEventListener('scroll',()=>{document.body.classList.toggle('scrolled',scrollY
   const savedSort=localStorage.getItem(LS.sort); if(savedSort)$('#sort').value=savedSort;
   const savedDens=localStorage.getItem(LS.dens)||'4'; $('#density').value=savedDens; applyDensity(savedDens);
   const [pd,fc]=await Promise.all([fetch('/api/products').then(r=>r.json()),fetch('/api/facets').then(r=>r.json())]);
-  ALL=pd.products; startHero(ALL); chips(fc); render();
+  ALL=pd.products; FACETS=fc; startHero(ALL); chips(fc); colorbar(fc); render();
 })();
 </script>
 </body>
diff --git a/artmura-site/public/styles.css b/artmura-site/public/styles.css
index 88582da..f27a152 100644
--- a/artmura-site/public/styles.css
+++ b/artmura-site/public/styles.css
@@ -91,6 +91,16 @@ body.scrolled .corner{opacity:0;pointer-events:none}
   background-repeat:no-repeat;background-position:right 11px center}
 .ctl input[type=range]{width:120px;accent-color:var(--accent)}
 
+/* ---------- color filter bar ---------- */
+.colorbar{max-width:var(--maxw);margin:0 auto;padding:16px 26px 0;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
+.cdot{display:inline-flex;align-items:center;gap:7px;border:1px solid var(--line);background:var(--paper);
+  border-radius:30px;padding:6px 13px 6px 8px;font-size:11px;letter-spacing:.04em;text-transform:capitalize;
+  color:var(--ink-soft);cursor:pointer;transition:.18s}
+.cdot span{width:14px;height:14px;border-radius:50%;border:1px solid rgba(0,0,0,.15)}
+.cdot.all{padding:6px 14px}
+.cdot:hover{border-color:var(--taupe);color:var(--ink)}
+.cdot.active{background:var(--ink);color:#fff;border-color:var(--ink)}
+
 /* ---------- grid ---------- */
 .grid-wrap{max-width:var(--maxw);margin:0 auto;padding:26px 26px 80px}
 .grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:22px}
@@ -102,7 +112,8 @@ body.scrolled .corner{opacity:0;pointer-events:none}
 .card .ser{font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--gold)}
 .card .nm{font-family:var(--serif);font-size:20px;line-height:1.1;margin:3px 0 2px}
 .card .meta{display:flex;justify-content:space-between;align-items:baseline;margin-top:7px}
-.card .col{font-size:12px;color:var(--ink-soft)}
+.card .col{font-size:12px;color:var(--ink-soft);display:inline-flex;align-items:center;gap:6px}
+.card .col .dot{width:9px;height:9px;border-radius:50%;border:1px solid rgba(0,0,0,.12);flex:0 0 auto}
 .card .pr{font-size:13px;font-variant-numeric:tabular-nums;color:var(--ink)}
 .card .pr small{color:var(--ink-soft);font-size:10px;letter-spacing:.06em}
 
diff --git a/artmura-site/server.js b/artmura-site/server.js
index 1320d82..9b34a70 100644
--- a/artmura-site/server.js
+++ b/artmura-site/server.js
@@ -20,12 +20,18 @@ function localImages(r) {
     return `/images/${r.mfr_sku}-${i + 1}${ext}`;
   });
 }
+const COLORS_PATH = path.join(ROOT, 'scripts/artmura-onboard/data/artmura-colors.json');
 let CATALOG = [];
 function load() {
   const d = JSON.parse(fs.readFileSync(PKG, 'utf8'));
+  let colors = {};
+  try { colors = JSON.parse(fs.readFileSync(COLORS_PATH, 'utf8')); } catch {}
   CATALOG = d.products.map(r => {
     const imgs = localImages(r);
+    const c = colors[r.mfr_sku] || {};
     return {
+      hex: c.hex || null, hue: c.hue ?? null, color_bucket: c.bucket || null,
+      sat: c.sat ?? null, val: c.val ?? null,
       handle: r.handle,
       sku: r.mfr_sku,
       series: r.pattern_series,
@@ -62,15 +68,17 @@ app.get('/api/product/:handle', (req, res) => {
   res.json(p);
 });
 app.get('/api/facets', (_req, res) => {
-  const books = {}, series = {};
+  const books = {}, series = {}, colors = {};
   for (const p of CATALOG) {
     if (p.book) books[p.book] = (books[p.book] || 0) + 1;
     if (p.series) series[p.series] = (series[p.series] || 0) + 1;
+    if (p.color_bucket) colors[p.color_bucket] = (colors[p.color_bucket] || 0) + 1;
   }
   res.json({
     total: CATALOG.length,
     books: Object.entries(books).sort((a, b) => b[1] - a[1]),
     series: Object.entries(series).sort((a, b) => b[1] - a[1]),
+    colors: Object.entries(colors).sort((a, b) => b[1] - a[1]),
   });
 });
 
diff --git a/scripts/artmura-onboard/data/artmura-colors.json b/scripts/artmura-onboard/data/artmura-colors.json
new file mode 100644
index 0000000..348ef1a
--- /dev/null
+++ b/scripts/artmura-onboard/data/artmura-colors.json
@@ -0,0 +1,1129 @@
+{
+ "ARC553": {
+  "hex": "#f9f7e8",
+  "hue": 52.9,
+  "sat": 0.068,
+  "val": 0.976,
+  "bucket": "white"
+ },
+ "ARC555": {
+  "hex": "#c3b4a4",
+  "hue": 31.0,
+  "sat": 0.159,
+  "val": 0.765,
+  "bucket": "orange"
+ },
+ "ARC551": {
+  "hex": "#c1c1c2",
+  "hue": 240.0,
+  "sat": 0.005,
+  "val": 0.761,
+  "bucket": "grey"
+ },
+ "ARC552": {
+  "hex": "#a29d9b",
+  "hue": 17.1,
+  "sat": 0.043,
+  "val": 0.635,
+  "bucket": "grey"
+ },
+ "ARC550": {
+  "hex": "#e7e3db",
+  "hue": 40.0,
+  "sat": 0.052,
+  "val": 0.906,
+  "bucket": "white"
+ },
+ "ARC556": {
+  "hex": "#883437",
+  "hue": 357.9,
+  "sat": 0.618,
+  "val": 0.533,
+  "bucket": "red"
+ },
+ "ARC554": {
+  "hex": "#b1987f",
+  "hue": 30.0,
+  "sat": 0.282,
+  "val": 0.694,
+  "bucket": "orange"
+ },
+ "ARD923": {
+  "hex": "#3f484a",
+  "hue": 190.9,
+  "sat": 0.149,
+  "val": 0.29,
+  "bucket": "teal"
+ },
+ "ARD922": {
+  "hex": "#8b362e",
+  "hue": 5.2,
+  "sat": 0.669,
+  "val": 0.545,
+  "bucket": "red"
+ },
+ "ARD924": {
+  "hex": "#979794",
+  "hue": 60.0,
+  "sat": 0.02,
+  "val": 0.592,
+  "bucket": "grey"
+ },
+ "ARD920": {
+  "hex": "#efebe1",
+  "hue": 42.9,
+  "sat": 0.059,
+  "val": 0.937,
+  "bucket": "white"
+ },
+ "ARD921": {
+  "hex": "#928e89",
+  "hue": 33.3,
+  "sat": 0.062,
+  "val": 0.573,
+  "bucket": "grey"
+ },
+ "COM241": {
+  "hex": "#3c4244",
+  "hue": 195.0,
+  "sat": 0.118,
+  "val": 0.267,
+  "bucket": "grey"
+ },
+ "COM602": {
+  "hex": "#598385",
+  "hue": 182.7,
+  "sat": 0.331,
+  "val": 0.522,
+  "bucket": "teal"
+ },
+ "COM245": {
+  "hex": "#745a43",
+  "hue": 28.2,
+  "sat": 0.422,
+  "val": 0.455,
+  "bucket": "brown"
+ },
+ "COM564": {
+  "hex": "#50463e",
+  "hue": 26.7,
+  "sat": 0.225,
+  "val": 0.314,
+  "bucket": "brown"
+ },
+ "COM603": {
+  "hex": "#545047",
+  "hue": 41.5,
+  "sat": 0.155,
+  "val": 0.329,
+  "bucket": "brown"
+ },
+ "COM566": {
+  "hex": "#777a7d",
+  "hue": 210.0,
+  "sat": 0.048,
+  "val": 0.49,
+  "bucket": "grey"
+ },
+ "COM563": {
+  "hex": "#ac9d81",
+  "hue": 39.1,
+  "sat": 0.25,
+  "val": 0.675,
+  "bucket": "orange"
+ },
+ "COM600": {
+  "hex": "#b6af8b",
+  "hue": 50.2,
+  "sat": 0.236,
+  "val": 0.714,
+  "bucket": "gold"
+ },
+ "COM565": {
+  "hex": "#649f81",
+  "hue": 149.5,
+  "sat": 0.371,
+  "val": 0.624,
+  "bucket": "green"
+ },
+ "COM606": {
+  "hex": "#dcdcdd",
+  "hue": 240.0,
+  "sat": 0.005,
+  "val": 0.867,
+  "bucket": "white"
+ },
+ "COM242": {
+  "hex": "#5c6883",
+  "hue": 221.5,
+  "sat": 0.298,
+  "val": 0.514,
+  "bucket": "blue"
+ },
+ "COM240": {
+  "hex": "#f8f5f0",
+  "hue": 37.5,
+  "sat": 0.032,
+  "val": 0.973,
+  "bucket": "white"
+ },
+ "COM560": {
+  "hex": "#bebead",
+  "hue": 60.0,
+  "sat": 0.089,
+  "val": 0.745,
+  "bucket": "grey"
+ },
+ "COM243": {
+  "hex": "#c5c6c5",
+  "hue": 120.0,
+  "sat": 0.005,
+  "val": 0.776,
+  "bucket": "grey"
+ },
+ "COM604": {
+  "hex": "#fbfbf6",
+  "hue": 60.0,
+  "sat": 0.02,
+  "val": 0.984,
+  "bucket": "white"
+ },
+ "COM601": {
+  "hex": "#7c3e2c",
+  "hue": 13.5,
+  "sat": 0.645,
+  "val": 0.486,
+  "bucket": "red"
+ },
+ "COM561": {
+  "hex": "#979c95",
+  "hue": 102.9,
+  "sat": 0.045,
+  "val": 0.612,
+  "bucket": "grey"
+ },
+ "COM605": {
+  "hex": "#a3a9ab",
+  "hue": 195.0,
+  "sat": 0.047,
+  "val": 0.671,
+  "bucket": "grey"
+ },
+ "COM244": {
+  "hex": "#a5a79c",
+  "hue": 70.9,
+  "sat": 0.066,
+  "val": 0.655,
+  "bucket": "grey"
+ },
+ "COM562": {
+  "hex": "#d5d6d0",
+  "hue": 70.0,
+  "sat": 0.028,
+  "val": 0.839,
+  "bucket": "grey"
+ },
+ "COM246": {
+  "hex": "#b9883a",
+  "hue": 36.9,
+  "sat": 0.686,
+  "val": 0.725,
+  "bucket": "orange"
+ },
+ "CO802": {
+  "hex": "#f0ece1",
+  "hue": 44.0,
+  "sat": 0.063,
+  "val": 0.941,
+  "bucket": "white"
+ },
+ "CO804": {
+  "hex": "#343a3c",
+  "hue": 195.0,
+  "sat": 0.133,
+  "val": 0.235,
+  "bucket": "grey"
+ },
+ "CO800": {
+  "hex": "#6f5b44",
+  "hue": 32.1,
+  "sat": 0.387,
+  "val": 0.435,
+  "bucket": "brown"
+ },
+ "CO803": {
+  "hex": "#9da49c",
+  "hue": 112.5,
+  "sat": 0.049,
+  "val": 0.643,
+  "bucket": "grey"
+ },
+ "CO801": {
+  "hex": "#b77334",
+  "hue": 28.9,
+  "sat": 0.716,
+  "val": 0.718,
+  "bucket": "orange"
+ },
+ "3100": {
+  "hex": "#b5b2a7",
+  "hue": 47.1,
+  "sat": 0.077,
+  "val": 0.71,
+  "bucket": "grey"
+ },
+ "3104": {
+  "hex": "#c3c1c8",
+  "hue": 257.1,
+  "sat": 0.035,
+  "val": 0.784,
+  "bucket": "grey"
+ },
+ "3101": {
+  "hex": "#d5d5d7",
+  "hue": 240.0,
+  "sat": 0.009,
+  "val": 0.843,
+  "bucket": "grey"
+ },
+ "3705": {
+  "hex": "#b2afc0",
+  "hue": 250.6,
+  "sat": 0.089,
+  "val": 0.753,
+  "bucket": "grey"
+ },
+ "3702": {
+  "hex": "#c5c6d6",
+  "hue": 236.5,
+  "sat": 0.079,
+  "val": 0.839,
+  "bucket": "grey"
+ },
+ "WOOD F-004": {
+  "hex": "#1d7b98",
+  "hue": 194.1,
+  "sat": 0.809,
+  "val": 0.596,
+  "bucket": "teal"
+ },
+ "WOOD F-006": {
+  "hex": "#5ba13d",
+  "hue": 102.0,
+  "sat": 0.621,
+  "val": 0.631,
+  "bucket": "green"
+ },
+ "WOOD F-008": {
+  "hex": "#a45243",
+  "hue": 9.3,
+  "sat": 0.591,
+  "val": 0.643,
+  "bucket": "red"
+ },
+ "WOOD F-007": {
+  "hex": "#01a51f",
+  "hue": 131.0,
+  "sat": 0.994,
+  "val": 0.647,
+  "bucket": "green"
+ },
+ "WOOD F-005": {
+  "hex": "#35a3ab",
+  "hue": 184.1,
+  "sat": 0.69,
+  "val": 0.671,
+  "bucket": "teal"
+ },
+ "WOOD F-001": {
+  "hex": "#d1aea1",
+  "hue": 16.3,
+  "sat": 0.23,
+  "val": 0.82,
+  "bucket": "orange"
+ },
+ "WOOD F-009": {
+  "hex": "#71395a",
+  "hue": 324.6,
+  "sat": 0.496,
+  "val": 0.443,
+  "bucket": "pink"
+ },
+ "WOOD F-003": {
+  "hex": "#0093a3",
+  "hue": 185.9,
+  "sat": 1.0,
+  "val": 0.639,
+  "bucket": "teal"
+ },
+ "WOOD F-002": {
+  "hex": "#c7c4bc",
+  "hue": 43.6,
+  "sat": 0.055,
+  "val": 0.78,
+  "bucket": "grey"
+ },
+ "3204": {
+  "hex": "#9b8b89",
+  "hue": 6.7,
+  "sat": 0.116,
+  "val": 0.608,
+  "bucket": "grey"
+ },
+ "3201": {
+  "hex": "#b7b4bb",
+  "hue": 265.7,
+  "sat": 0.037,
+  "val": 0.733,
+  "bucket": "grey"
+ },
+ "3203": {
+  "hex": "#d9d7e1",
+  "hue": 252.0,
+  "sat": 0.044,
+  "val": 0.882,
+  "bucket": "white"
+ },
+ "3200": {
+  "hex": "#a9a5ad",
+  "hue": 270.0,
+  "sat": 0.046,
+  "val": 0.678,
+  "bucket": "grey"
+ },
+ "GOL852": {
+  "hex": "#7e735c",
+  "hue": 40.6,
+  "sat": 0.27,
+  "val": 0.494,
+  "bucket": "brown"
+ },
+ "GOL850": {
+  "hex": "#bb8b49",
+  "hue": 34.7,
+  "sat": 0.61,
+  "val": 0.733,
+  "bucket": "orange"
+ },
+ "GOL851": {
+  "hex": "#946b31",
+  "hue": 35.2,
+  "sat": 0.669,
+  "val": 0.58,
+  "bucket": "orange"
+ },
+ "GOL854": {
+  "hex": "#b4bdb8",
+  "hue": 146.7,
+  "sat": 0.048,
+  "val": 0.741,
+  "bucket": "grey"
+ },
+ "GOL853": {
+  "hex": "#dad7ce",
+  "hue": 45.0,
+  "sat": 0.055,
+  "val": 0.855,
+  "bucket": "white"
+ },
+ "KR901": {
+  "hex": "#354147",
+  "hue": 200.0,
+  "sat": 0.254,
+  "val": 0.278,
+  "bucket": "blue"
+ },
+ "KR900": {
+  "hex": "#7a8485",
+  "hue": 185.5,
+  "sat": 0.083,
+  "val": 0.522,
+  "bucket": "grey"
+ },
+ "KR902": {
+  "hex": "#643818",
+  "hue": 25.3,
+  "sat": 0.76,
+  "val": 0.392,
+  "bucket": "brown"
+ },
+ "KR903": {
+  "hex": "#642523",
+  "hue": 1.8,
+  "sat": 0.65,
+  "val": 0.392,
+  "bucket": "red"
+ },
+ "KR904": {
+  "hex": "#2b7796",
+  "hue": 197.4,
+  "sat": 0.713,
+  "val": 0.588,
+  "bucket": "teal"
+ },
+ "LAC703": {
+  "hex": "#aaafa8",
+  "hue": 102.9,
+  "sat": 0.04,
+  "val": 0.686,
+  "bucket": "grey"
+ },
+ "LAC704": {
+  "hex": "#070707",
+  "hue": 0.0,
+  "sat": 0.0,
+  "val": 0.027,
+  "bucket": "black"
+ },
+ "LAC701": {
+  "hex": "#b7babd",
+  "hue": 210.0,
+  "sat": 0.032,
+  "val": 0.741,
+  "bucket": "grey"
+ },
+ "LAC700": {
+  "hex": "#600d09",
+  "hue": 2.8,
+  "sat": 0.906,
+  "val": 0.376,
+  "bucket": "red"
+ },
+ "LAC702": {
+  "hex": "#f6f6f6",
+  "hue": 0.0,
+  "sat": 0.0,
+  "val": 0.965,
+  "bucket": "white"
+ },
+ "MAT266": {
+  "hex": "#3d413f",
+  "hue": 150.0,
+  "sat": 0.062,
+  "val": 0.255,
+  "bucket": "grey"
+ },
+ "MAT346": {
+  "hex": "#8d6c56",
+  "hue": 24.0,
+  "sat": 0.39,
+  "val": 0.553,
+  "bucket": "orange"
+ },
+ "MAT265": {
+  "hex": "#7e603d",
+  "hue": 32.3,
+  "sat": 0.516,
+  "val": 0.494,
+  "bucket": "brown"
+ },
+ "MAT262": {
+  "hex": "#c9b995",
+  "hue": 41.5,
+  "sat": 0.259,
+  "val": 0.788,
+  "bucket": "orange"
+ },
+ "MAT345": {
+  "hex": "#7d6f4a",
+  "hue": 43.5,
+  "sat": 0.408,
+  "val": 0.49,
+  "bucket": "brown"
+ },
+ "MAT341": {
+  "hex": "#a9afa9",
+  "hue": 120.0,
+  "sat": 0.034,
+  "val": 0.686,
+  "bucket": "grey"
+ },
+ "MAT342": {
+  "hex": "#fcfdf8",
+  "hue": 72.0,
+  "sat": 0.02,
+  "val": 0.992,
+  "bucket": "white"
+ },
+ "MAT260": {
+  "hex": "#f9f8f3",
+  "hue": 50.0,
+  "sat": 0.024,
+  "val": 0.976,
+  "bucket": "white"
+ },
+ "MAT340": {
+  "hex": "#f6f3ec",
+  "hue": 42.0,
+  "sat": 0.041,
+  "val": 0.965,
+  "bucket": "white"
+ },
+ "MAT263": {
+  "hex": "#c1c3c5",
+  "hue": 210.0,
+  "sat": 0.02,
+  "val": 0.773,
+  "bucket": "grey"
+ },
+ "MAT343": {
+  "hex": "#b4bcbe",
+  "hue": 192.0,
+  "sat": 0.053,
+  "val": 0.745,
+  "bucket": "grey"
+ },
+ "MAT344": {
+  "hex": "#8e8c7c",
+  "hue": 53.3,
+  "sat": 0.127,
+  "val": 0.557,
+  "bucket": "grey"
+ },
+ "MAT264": {
+  "hex": "#98c7c6",
+  "hue": 178.7,
+  "sat": 0.236,
+  "val": 0.78,
+  "bucket": "teal"
+ },
+ "MAT261": {
+  "hex": "#fbf6e7",
+  "hue": 45.0,
+  "sat": 0.08,
+  "val": 0.984,
+  "bucket": "white"
+ },
+ "WOOD M-005": {
+  "hex": "#3d3e43",
+  "hue": 230.0,
+  "sat": 0.09,
+  "val": 0.263,
+  "bucket": "grey"
+ },
+ "WOOD M-001": {
+  "hex": "#e4e6e6",
+  "hue": 180.0,
+  "sat": 0.009,
+  "val": 0.902,
+  "bucket": "white"
+ },
+ "WOOD M-002": {
+  "hex": "#cacbc4",
+  "hue": 68.6,
+  "sat": 0.034,
+  "val": 0.796,
+  "bucket": "grey"
+ },
+ "WOOD M-003": {
+  "hex": "#b0b4b5",
+  "hue": 192.0,
+  "sat": 0.028,
+  "val": 0.71,
+  "bucket": "grey"
+ },
+ "WOOD M-004": {
+  "hex": "#4b5054",
+  "hue": 206.7,
+  "sat": 0.107,
+  "val": 0.329,
+  "bucket": "grey"
+ },
+ "WOOD N-008": {
+  "hex": "#9aa3a2",
+  "hue": 173.3,
+  "sat": 0.055,
+  "val": 0.639,
+  "bucket": "grey"
+ },
+ "WOOD N-009": {
+  "hex": "#8ca2b0",
+  "hue": 203.3,
+  "sat": 0.205,
+  "val": 0.69,
+  "bucket": "blue"
+ },
+ "WOOD N-003": {
+  "hex": "#cdd1b0",
+  "hue": 67.3,
+  "sat": 0.158,
+  "val": 0.82,
+  "bucket": "gold"
+ },
+ "WOOD N-007": {
+  "hex": "#cccece",
+  "hue": 180.0,
+  "sat": 0.01,
+  "val": 0.808,
+  "bucket": "grey"
+ },
+ "WOOD N-004": {
+  "hex": "#cfc5a8",
+  "hue": 44.6,
+  "sat": 0.188,
+  "val": 0.812,
+  "bucket": "orange"
+ },
+ "WOOD N-005": {
+  "hex": "#dbdade",
+  "hue": 255.0,
+  "sat": 0.018,
+  "val": 0.871,
+  "bucket": "white"
+ },
+ "WOOD N-002": {
+  "hex": "#cccdca",
+  "hue": 80.0,
+  "sat": 0.015,
+  "val": 0.804,
+  "bucket": "grey"
+ },
+ "WOOD N-001": {
+  "hex": "#e1e2e2",
+  "hue": 180.0,
+  "sat": 0.004,
+  "val": 0.886,
+  "bucket": "white"
+ },
+ "WOOD N-006": {
+  "hex": "#ac816e",
+  "hue": 18.4,
+  "sat": 0.36,
+  "val": 0.675,
+  "bucket": "orange"
+ },
+ "NY823": {
+  "hex": "#b4957a",
+  "hue": 27.9,
+  "sat": 0.322,
+  "val": 0.706,
+  "bucket": "orange"
+ },
+ "NY821": {
+  "hex": "#aaa27d",
+  "hue": 49.3,
+  "sat": 0.265,
+  "val": 0.667,
+  "bucket": "gold"
+ },
+ "NY822": {
+  "hex": "#cdced0",
+  "hue": 220.0,
+  "sat": 0.014,
+  "val": 0.816,
+  "bucket": "grey"
+ },
+ "NY824": {
+  "hex": "#975c53",
+  "hue": 7.9,
+  "sat": 0.45,
+  "val": 0.592,
+  "bucket": "red"
+ },
+ "NY820": {
+  "hex": "#726e63",
+  "hue": 44.0,
+  "sat": 0.132,
+  "val": 0.447,
+  "bucket": "grey"
+ },
+ "OAK754": {
+  "hex": "#415b69",
+  "hue": 201.0,
+  "sat": 0.381,
+  "val": 0.412,
+  "bucket": "blue"
+ },
+ "OAK750": {
+  "hex": "#23121b",
+  "hue": 328.2,
+  "sat": 0.486,
+  "val": 0.137,
+  "bucket": "black"
+ },
+ "OAK753": {
+  "hex": "#9ca6aa",
+  "hue": 197.1,
+  "sat": 0.082,
+  "val": 0.667,
+  "bucket": "grey"
+ },
+ "OAK752": {
+  "hex": "#ceced1",
+  "hue": 240.0,
+  "sat": 0.014,
+  "val": 0.82,
+  "bucket": "grey"
+ },
+ "OAK751": {
+  "hex": "#b07233",
+  "hue": 30.2,
+  "sat": 0.71,
+  "val": 0.69,
+  "bucket": "orange"
+ },
+ "OT893": {
+  "hex": "#151e29",
+  "hue": 213.0,
+  "sat": 0.488,
+  "val": 0.161,
+  "bucket": "black"
+ },
+ "OT892": {
+  "hex": "#3a292d",
+  "hue": 345.9,
+  "sat": 0.293,
+  "val": 0.227,
+  "bucket": "red"
+ },
+ "OT890": {
+  "hex": "#bb9f56",
+  "hue": 43.4,
+  "sat": 0.54,
+  "val": 0.733,
+  "bucket": "orange"
+ },
+ "OT894": {
+  "hex": "#2d8d8b",
+  "hue": 178.7,
+  "sat": 0.681,
+  "val": 0.553,
+  "bucket": "teal"
+ },
+ "OT891": {
+  "hex": "#a06b4b",
+  "hue": 22.6,
+  "sat": 0.531,
+  "val": 0.627,
+  "bucket": "orange"
+ },
+ "ORI453": {
+  "hex": "#c5c0bb",
+  "hue": 30.0,
+  "sat": 0.051,
+  "val": 0.773,
+  "bucket": "grey"
+ },
+ "ORI444": {
+  "hex": "#2f3336",
+  "hue": 205.7,
+  "sat": 0.13,
+  "val": 0.212,
+  "bucket": "grey"
+ },
+ "ORI440": {
+  "hex": "#c0c7c9",
+  "hue": 193.3,
+  "sat": 0.045,
+  "val": 0.788,
+  "bucket": "grey"
+ },
+ "ORI446": {
+  "hex": "#4e453f",
+  "hue": 24.0,
+  "sat": 0.192,
+  "val": 0.306,
+  "bucket": "brown"
+ },
+ "ORI454": {
+  "hex": "#9a8c7c",
+  "hue": 32.0,
+  "sat": 0.195,
+  "val": 0.604,
+  "bucket": "orange"
+ },
+ "ORI441": {
+  "hex": "#655535",
+  "hue": 40.0,
+  "sat": 0.475,
+  "val": 0.396,
+  "bucket": "brown"
+ },
+ "ORI442": {
+  "hex": "#c3b187",
+  "hue": 42.0,
+  "sat": 0.308,
+  "val": 0.765,
+  "bucket": "orange"
+ },
+ "ORI455": {
+  "hex": "#998a63",
+  "hue": 43.3,
+  "sat": 0.353,
+  "val": 0.6,
+  "bucket": "orange"
+ },
+ "ORI456": {
+  "hex": "#699c7e",
+  "hue": 144.7,
+  "sat": 0.327,
+  "val": 0.612,
+  "bucket": "green"
+ },
+ "ORI452": {
+  "hex": "#bfc4c3",
+  "hue": 168.0,
+  "sat": 0.026,
+  "val": 0.769,
+  "bucket": "grey"
+ },
+ "ORI451": {
+  "hex": "#ebe7dd",
+  "hue": 42.9,
+  "sat": 0.06,
+  "val": 0.922,
+  "bucket": "white"
+ },
+ "ORI445": {
+  "hex": "#844936",
+  "hue": 14.6,
+  "sat": 0.591,
+  "val": 0.518,
+  "bucket": "red"
+ },
+ "ORI443": {
+  "hex": "#979e9d",
+  "hue": 171.4,
+  "sat": 0.044,
+  "val": 0.62,
+  "bucket": "grey"
+ },
+ "ORI450": {
+  "hex": "#b5b4a9",
+  "hue": 55.0,
+  "sat": 0.066,
+  "val": 0.71,
+  "bucket": "grey"
+ },
+ "3303": {
+  "hex": "#bca189",
+  "hue": 28.2,
+  "sat": 0.271,
+  "val": 0.737,
+  "bucket": "orange"
+ },
+ "3301": {
+  "hex": "#bdbdca",
+  "hue": 240.0,
+  "sat": 0.064,
+  "val": 0.792,
+  "bucket": "grey"
+ },
+ "3305": {
+  "hex": "#bdbed0",
+  "hue": 236.8,
+  "sat": 0.091,
+  "val": 0.816,
+  "bucket": "grey"
+ },
+ "3300": {
+  "hex": "#c3c5d2",
+  "hue": 232.0,
+  "sat": 0.071,
+  "val": 0.824,
+  "bucket": "grey"
+ },
+ "3004": {
+  "hex": "#ded3cb",
+  "hue": 25.3,
+  "sat": 0.086,
+  "val": 0.871,
+  "bucket": "white"
+ },
+ "3007": {
+  "hex": "#e5b16e",
+  "hue": 33.8,
+  "sat": 0.52,
+  "val": 0.898,
+  "bucket": "orange"
+ },
+ "3001": {
+  "hex": "#aeb6c9",
+  "hue": 222.2,
+  "sat": 0.134,
+  "val": 0.788,
+  "bucket": "grey"
+ },
+ "3002": {
+  "hex": "#acb9db",
+  "hue": 223.4,
+  "sat": 0.215,
+  "val": 0.859,
+  "bucket": "blue"
+ },
+ "3003": {
+  "hex": "#cac6c7",
+  "hue": 345.0,
+  "sat": 0.02,
+  "val": 0.792,
+  "bucket": "grey"
+ },
+ "3000": {
+  "hex": "#bbbac5",
+  "hue": 245.5,
+  "sat": 0.056,
+  "val": 0.773,
+  "bucket": "grey"
+ },
+ "TABC-001": {
+  "hex": "#a15e2d",
+  "hue": 25.3,
+  "sat": 0.72,
+  "val": 0.631,
+  "bucket": "orange"
+ },
+ "TABC-002": {
+  "hex": "#848473",
+  "hue": 60.0,
+  "sat": 0.129,
+  "val": 0.518,
+  "bucket": "grey"
+ },
+ "TABL-001": {
+  "hex": "#d1c4b4",
+  "hue": 33.1,
+  "sat": 0.139,
+  "val": 0.82,
+  "bucket": "grey"
+ },
+ "TABL-007": {
+  "hex": "#2f2f2f",
+  "hue": 0.0,
+  "sat": 0.0,
+  "val": 0.184,
+  "bucket": "grey"
+ },
+ "TABL-003": {
+  "hex": "#9d9f9c",
+  "hue": 100.0,
+  "sat": 0.019,
+  "val": 0.624,
+  "bucket": "grey"
+ },
+ "TABL-002": {
+  "hex": "#c6aea5",
+  "hue": 16.4,
+  "sat": 0.167,
+  "val": 0.776,
+  "bucket": "orange"
+ },
+ "TABL-006": {
+  "hex": "#722c1c",
+  "hue": 11.2,
+  "sat": 0.754,
+  "val": 0.447,
+  "bucket": "red"
+ },
+ "TABL-005": {
+  "hex": "#154042",
+  "hue": 182.7,
+  "sat": 0.682,
+  "val": 0.259,
+  "bucket": "teal"
+ },
+ "TABL-004": {
+  "hex": "#c7b64a",
+  "hue": 51.8,
+  "sat": 0.628,
+  "val": 0.78,
+  "bucket": "gold"
+ },
+ "TABM-002": {
+  "hex": "#bead95",
+  "hue": 35.1,
+  "sat": 0.216,
+  "val": 0.745,
+  "bucket": "orange"
+ },
+ "TABM-007": {
+  "hex": "#202122",
+  "hue": 210.0,
+  "sat": 0.059,
+  "val": 0.133,
+  "bucket": "black"
+ },
+ "TABM-005": {
+  "hex": "#8795a0",
+  "hue": 206.4,
+  "sat": 0.156,
+  "val": 0.627,
+  "bucket": "blue"
+ },
+ "TABM-003": {
+  "hex": "#87755e",
+  "hue": 33.7,
+  "sat": 0.304,
+  "val": 0.529,
+  "bucket": "orange"
+ },
+ "TABM-006": {
+  "hex": "#ce9687",
+  "hue": 12.7,
+  "sat": 0.345,
+  "val": 0.808,
+  "bucket": "red"
+ },
+ "TABM-004": {
+  "hex": "#838079",
+  "hue": 42.0,
+  "sat": 0.076,
+  "val": 0.514,
+  "bucket": "grey"
+ },
+ "TABM-001": {
+  "hex": "#e9e9ea",
+  "hue": 240.0,
+  "sat": 0.004,
+  "val": 0.918,
+  "bucket": "white"
+ },
+ "TABW-007": {
+  "hex": "#57554e",
+  "hue": 46.7,
+  "sat": 0.103,
+  "val": 0.341,
+  "bucket": "grey"
+ },
+ "TABW-006": {
+  "hex": "#4f674e",
+  "hue": 117.6,
+  "sat": 0.243,
+  "val": 0.404,
+  "bucket": "green"
+ },
+ "TABW-001": {
+  "hex": "#acafa0",
+  "hue": 72.0,
+  "sat": 0.086,
+  "val": 0.686,
+  "bucket": "grey"
+ },
+ "TABW-004": {
+  "hex": "#581f36",
+  "hue": 335.8,
+  "sat": 0.648,
+  "val": 0.345,
+  "bucket": "pink"
+ },
+ "TABW-005": {
+  "hex": "#0b6d8a",
+  "hue": 193.7,
+  "sat": 0.92,
+  "val": 0.541,
+  "bucket": "teal"
+ },
+ "TABW-003": {
+  "hex": "#bc5c48",
+  "hue": 10.3,
+  "sat": 0.617,
+  "val": 0.737,
+  "bucket": "red"
+ },
+ "TABW-002": {
+  "hex": "#bf862b",
+  "hue": 36.9,
+  "sat": 0.775,
+  "val": 0.749,
+  "bucket": "orange"
+ }
+}
\ No newline at end of file
diff --git a/scripts/artmura-onboard/extract_colors.py b/scripts/artmura-onboard/extract_colors.py
new file mode 100644
index 0000000..090033c
--- /dev/null
+++ b/scripts/artmura-onboard/extract_colors.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+"""Extract dominant color (hex + hue + coarse bucket) from each Artmura swatch image.
+Writes data/artmura-colors.json {sku: {hex, hue, bucket}} for the site to merge in."""
+import json, os, glob, colorsys
+from collections import Counter
+from PIL import Image
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+IMG = os.path.join(HERE, "..", "..", ".newwall-ref", "artmura", "images")
+PKG = json.load(open(os.path.join(HERE, "data", "artmura.json")))
+
+def dominant(path):
+    im = Image.open(path).convert("RGB").resize((64, 64))
+    # quantize to 16 colors, take the most common that isn't near-pure-white background noise
+    q = im.quantize(colors=16).convert("RGB")
+    cnt = Counter(q.getdata())
+    ranked = cnt.most_common()
+    # prefer the most saturated/among top to avoid flat near-white winning on tonal swatches
+    def score(item):
+        (r, g, b), n = item
+        h, s, v = colorsys.rgb_to_hsv(r/255, g/255, b/255)
+        # weight count but boost saturated colors so the swatch's character shows
+        return n * (0.5 + s)
+    (r, g, b), _ = max(ranked[:6], key=score)
+    return r, g, b
+
+def bucket(h, s, v):
+    if v < 0.18: return "black"
+    if s < 0.12 and v > 0.85: return "white"
+    if s < 0.14: return "grey"
+    deg = h * 360
+    if deg < 16 or deg >= 345: return "red"
+    if deg < 45: return "orange" if v > 0.5 else "brown"
+    if deg < 70: return "gold"
+    if deg < 160: return "green"
+    if deg < 200: return "teal"
+    if deg < 255: return "blue"
+    if deg < 290: return "purple"
+    return "pink"
+
+out = {}
+for p in PKG["products"]:
+    sku = p["mfr_sku"]
+    cand = sorted(glob.glob(os.path.join(IMG, f"{sku}-1.*")) or glob.glob(os.path.join(IMG, f"{sku}-*.*")))
+    if not cand:
+        continue
+    r, g, b = dominant(cand[0])
+    h, s, v = colorsys.rgb_to_hsv(r/255, g/255, b/255)
+    out[sku] = {"hex": f"#{r:02x}{g:02x}{b:02x}", "hue": round(h*360, 1),
+                "sat": round(s, 3), "val": round(v, 3), "bucket": bucket(h, s, v)}
+
+open(os.path.join(HERE, "data", "artmura-colors.json"), "w").write(json.dumps(out, indent=1))
+from collections import Counter as C
+print(f"extracted {len(out)} colors")
+print("buckets:", dict(C(v["bucket"] for v in out.values()).most_common()))

← ecdd0e4 Artmura site (C/production-readiness): APCA hero fix + perf  ·  back to Dw Yolo Loop  ·  Artmura site (B): 'Pairs well with' recs + per-product JSON- 528cdf4 →