[object Object]

← back to Wallco Ai

wallco.ai · surface pre-generated room mockups on /design/:id (tick 15)

24e8de3aaf9fb3dc2238cdccea26e47945e6048b · 2026-05-11 22:37:28 -0700 · SteveStudio2

- snapshot script now pulls room_mockups column into designs.json
- new /designs/room static route serves data/rooms/*.png (7d cache)
- /design/:id auto-injects <figure> mockup when design has room_mockups
- PG hot-path fallback also loads room_mockups for freshly-baked designs

Top-5 saturated designs (#14/24/29/72/144) now show their living-room
mockup inline above the See in a Room button — zero clicks needed,
zero Replicate spend per pageview (was $0.014/click before).

Files touched

Diff

commit 24e8de3aaf9fb3dc2238cdccea26e47945e6048b
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon May 11 22:37:28 2026 -0700

    wallco.ai · surface pre-generated room mockups on /design/:id (tick 15)
    
    - snapshot script now pulls room_mockups column into designs.json
    - new /designs/room static route serves data/rooms/*.png (7d cache)
    - /design/:id auto-injects <figure> mockup when design has room_mockups
    - PG hot-path fallback also loads room_mockups for freshly-baked designs
    
    Top-5 saturated designs (#14/24/29/72/144) now show their living-room
    mockup inline above the See in a Room button — zero clicks needed,
    zero Replicate spend per pageview (was $0.014/click before).
---
 data/designs.json                   | 1115 +++++++++++++++++++++++++++++++----
 scripts/refresh_designs_snapshot.py |    7 +-
 server.js                           |  348 ++++++++++-
 src/color-isolate.js                |  205 +++++++
 src/sw-colors.json                  |  327 ++++++++++
 5 files changed, 1895 insertions(+), 107 deletions(-)

diff --git a/data/designs.json b/data/designs.json
index 88ff3cf..5fa528b 100644
--- a/data/designs.json
+++ b/data/designs.json
@@ -19,7 +19,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 12,
@@ -41,7 +42,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 13,
@@ -63,7 +65,8 @@
       "tropical",
       "abstract",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 14,
@@ -85,6 +88,9 @@
       "botanical",
       "tropical",
       "abstract"
+    ],
+    "room_mockups": [
+      "living_room"
     ]
   },
   {
@@ -107,7 +113,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 16,
@@ -129,7 +136,8 @@
       "geometric",
       "damask",
       "art-deco"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 17,
@@ -151,7 +159,8 @@
       "modern",
       "minimal",
       "sparse"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 18,
@@ -173,7 +182,8 @@
       "abstract",
       "organic",
       "geometric"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 19,
@@ -195,7 +205,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 20,
@@ -217,7 +228,8 @@
       "scrollwork",
       "abstract",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 21,
@@ -239,7 +251,8 @@
       "baroque",
       "floral",
       "scrollwork"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 22,
@@ -261,7 +274,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 23,
@@ -283,7 +297,8 @@
       "leaf",
       "vine",
       "abstract"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 24,
@@ -305,6 +320,9 @@
       "rose",
       "leaf",
       "vine"
+    ],
+    "room_mockups": [
+      "living_room"
     ]
   },
   {
@@ -327,7 +345,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 26,
@@ -350,7 +369,8 @@
       "victorian",
       "abstract",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 27,
@@ -372,7 +392,8 @@
       "dot",
       "diamond",
       "hexagon"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 28,
@@ -393,7 +414,8 @@
       "organic",
       "minimal",
       "dense"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 29,
@@ -415,6 +437,9 @@
       "trellis",
       "damask",
       "art-nouveau"
+    ],
+    "room_mockups": [
+      "living_room"
     ]
   },
   {
@@ -437,7 +462,8 @@
       "art-deco",
       "modern",
       "minimal"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 31,
@@ -459,7 +485,8 @@
       "scrollwork",
       "abstract",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 32,
@@ -482,7 +509,8 @@
       "victorian",
       "abstract",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 33,
@@ -504,7 +532,8 @@
       "geometric",
       "baroque",
       "victorian"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 34,
@@ -526,7 +555,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 35,
@@ -548,7 +578,8 @@
       "trellis",
       "lattice",
       "abstract"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 36,
@@ -570,7 +601,8 @@
       "leaf",
       "vine",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 37,
@@ -592,7 +624,8 @@
       "paisley",
       "chinoiserie",
       "art-nouveau"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 38,
@@ -614,7 +647,8 @@
       "leaf",
       "botanical",
       "tropical"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 39,
@@ -636,7 +670,8 @@
       "vine",
       "leaf",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 40,
@@ -659,7 +694,8 @@
       "art-nouveau",
       "floral",
       "geometric"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 41,
@@ -681,7 +717,8 @@
       "floral",
       "damask",
       "paisley"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 42,
@@ -703,7 +740,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 43,
@@ -725,7 +763,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 44,
@@ -748,7 +787,8 @@
       "leaf",
       "vine",
       "botanical"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 45,
@@ -770,7 +810,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 46,
@@ -792,7 +833,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 47,
@@ -814,7 +856,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 48,
@@ -836,7 +879,8 @@
       "leaf",
       "vine",
       "botanical"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 49,
@@ -858,7 +902,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 50,
@@ -880,7 +925,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 51,
@@ -902,7 +948,8 @@
       "leaf",
       "vine",
       "trellis"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 52,
@@ -924,7 +971,8 @@
       "clouds",
       "fantasy",
       "colorful"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 53,
@@ -945,7 +993,8 @@
       "floral",
       "blue",
       "pattern"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 54,
@@ -967,7 +1016,8 @@
       "botanical",
       "abstract",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 55,
@@ -983,7 +1033,14 @@
     "seed": 2084114836,
     "is_published": false,
     "created_at": "2026-05-12T01:58:12.937396+00:00",
-    "motifs": []
+    "motifs": [
+      "floral",
+      "peony",
+      "rose",
+      "leaf",
+      "vine"
+    ],
+    "room_mockups": []
   },
   {
     "id": 56,
@@ -1005,7 +1062,8 @@
       "lattice",
       "trellis",
       "damask"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 57,
@@ -1028,7 +1086,8 @@
       "victorian",
       "abstract",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 58,
@@ -1050,7 +1109,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 59,
@@ -1072,7 +1132,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 60,
@@ -1094,7 +1155,8 @@
       "trellis",
       "damask",
       "art-deco"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 61,
@@ -1116,7 +1178,8 @@
       "damask",
       "baroque",
       "victorian"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 62,
@@ -1138,7 +1201,8 @@
       "trellis",
       "damask",
       "art-deco"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 63,
@@ -1161,7 +1225,8 @@
       "damask",
       "lattice",
       "trellis"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 64,
@@ -1183,7 +1248,8 @@
       "lattice",
       "trellis",
       "baroque"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 65,
@@ -1205,7 +1271,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 66,
@@ -1227,7 +1294,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 67,
@@ -1248,7 +1316,8 @@
       "geometric",
       "modern",
       "dense"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 68,
@@ -1270,7 +1339,8 @@
       "damask",
       "abstract",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 69,
@@ -1292,7 +1362,8 @@
       "trellis",
       "lattice",
       "botanical"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 70,
@@ -1314,7 +1385,8 @@
       "gradient",
       "art-deco",
       "minimal"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 71,
@@ -1336,7 +1408,8 @@
       "geometric",
       "abstract",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 72,
@@ -1358,6 +1431,9 @@
       "rose",
       "leaf",
       "vine"
+    ],
+    "room_mockups": [
+      "living_room"
     ]
   },
   {
@@ -1380,7 +1456,8 @@
       "geometric",
       "abstract",
       "art-nouveau"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 74,
@@ -1402,7 +1479,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 75,
@@ -1424,7 +1502,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 76,
@@ -1446,7 +1525,8 @@
       "lattice",
       "trellis",
       "damask"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 137,
@@ -1469,7 +1549,8 @@
       "art-nouveau",
       "abstract",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 138,
@@ -1491,7 +1572,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 139,
@@ -1513,7 +1595,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 140,
@@ -1535,7 +1618,8 @@
       "damask",
       "floral",
       "lattice"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 141,
@@ -1558,7 +1642,8 @@
       "victorian",
       "floral",
       "peony"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 142,
@@ -1574,7 +1659,14 @@
     "seed": 622802852,
     "is_published": false,
     "created_at": "2026-05-12T04:46:04.479017+00:00",
-    "motifs": []
+    "motifs": [
+      "floral",
+      "peony",
+      "rose",
+      "leaf",
+      "vine"
+    ],
+    "room_mockups": []
   },
   {
     "id": 143,
@@ -1596,7 +1688,8 @@
       "organic",
       "damask",
       "art-nouveau"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 144,
@@ -1618,6 +1711,9 @@
       "scrollwork",
       "art-nouveau",
       "baroque"
+    ],
+    "room_mockups": [
+      "living_room"
     ]
   },
   {
@@ -1640,7 +1736,8 @@
       "trellis",
       "damask",
       "abstract"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 146,
@@ -1662,7 +1759,8 @@
       "leaf",
       "botanical",
       "tropical"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 147,
@@ -1683,7 +1781,8 @@
       "abstract",
       "organic",
       "modern"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 148,
@@ -1706,7 +1805,8 @@
       "damask",
       "paisley",
       "chinoiserie"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 149,
@@ -1728,7 +1828,8 @@
       "leaf",
       "vine",
       "damask"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 150,
@@ -1750,7 +1851,8 @@
       "trellis",
       "abstract",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 151,
@@ -1772,7 +1874,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 152,
@@ -1794,7 +1897,8 @@
       "damask",
       "abstract",
       "organic"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 153,
@@ -1816,7 +1920,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 154,
@@ -1832,7 +1937,14 @@
     "seed": 772049593,
     "is_published": false,
     "created_at": "2026-05-12T05:11:29.65531+00:00",
-    "motifs": []
+    "motifs": [
+      "trellis",
+      "vine",
+      "botanical",
+      "organic",
+      "minimal"
+    ],
+    "room_mockups": []
   },
   {
     "id": 155,
@@ -1854,7 +1966,8 @@
       "organic",
       "modern",
       "minimal"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 156,
@@ -1876,7 +1989,8 @@
       "damask",
       "abstract",
       "minimal"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 157,
@@ -1898,7 +2012,8 @@
       "modern",
       "minimal",
       "dense"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 158,
@@ -1921,7 +2036,8 @@
       "diamond",
       "hexagon",
       "abstract"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 159,
@@ -1943,7 +2059,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 160,
@@ -1966,7 +2083,8 @@
       "damask",
       "paisley",
       "art-deco"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 161,
@@ -1988,7 +2106,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 162,
@@ -2011,7 +2130,8 @@
       "ripple",
       "ocean",
       "teal"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 163,
@@ -2034,7 +2154,8 @@
       "diamond",
       "hexagon",
       "fan"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 164,
@@ -2056,7 +2177,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 165,
@@ -2078,7 +2200,8 @@
       "stripe",
       "dot",
       "diamond"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 166,
@@ -2100,7 +2223,8 @@
       "geometric",
       "damask",
       "art-nouveau"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 167,
@@ -2121,7 +2245,8 @@
       "organic",
       "geometric",
       "dense"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 168,
@@ -2143,7 +2268,8 @@
       "geometric",
       "modern",
       "minimal"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 169,
@@ -2165,7 +2291,8 @@
       "rose",
       "leaf",
       "vine"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 170,
@@ -2188,7 +2315,8 @@
       "art-nouveau",
       "minimal",
       "dense"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 171,
@@ -2210,7 +2338,8 @@
       "leaf",
       "vine",
       "botanical"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 172,
@@ -2232,7 +2361,8 @@
       "geometric",
       "damask",
       "art-nouveau"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 173,
@@ -2254,7 +2384,8 @@
       "leaf",
       "vine",
       "botanical"
-    ]
+    ],
+    "room_mockups": []
   },
   {
     "id": 174,
@@ -2270,7 +2401,13 @@
     "seed": 0,
     "is_published": false,
     "created_at": "2026-05-12T05:12:35.774293+00:00",
-    "motifs": []
+    "motifs": [
+      "abstract",
+      "organic",
+      "geometric",
+      "dense"
+    ],
+    "room_mockups": []
   },
   {
     "id": 175,
@@ -2286,7 +2423,14 @@
     "seed": 0,
     "is_published": false,
     "created_at": "2026-05-12T05:12:37.764494+00:00",
-    "motifs": []
+    "motifs": [
+      "floral",
+      "peony",
+      "leaf",
+      "vine",
+      "botanical"
+    ],
+    "room_mockups": []
   },
   {
     "id": 176,
@@ -2302,7 +2446,14 @@
     "seed": 0,
     "is_published": false,
     "created_at": "2026-05-12T05:12:40.818116+00:00",
-    "motifs": []
+    "motifs": [
+      "floral",
+      "peony",
+      "rose",
+      "leaf",
+      "vine"
+    ],
+    "room_mockups": []
   },
   {
     "id": 177,
@@ -2318,6 +2469,766 @@
     "seed": 0,
     "is_published": false,
     "created_at": "2026-05-12T05:12:43.207796+00:00",
-    "motifs": []
+    "motifs": [
+      "floral",
+      "peony",
+      "rose",
+      "leaf",
+      "vine"
+    ],
+    "room_mockups": []
+  },
+  {
+    "id": 178,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#3a443c",
+    "saturation": 0.079,
+    "title": "Noir Atelier No.178",
+    "handle": "wallco-0178",
+    "image_url": "/designs/img/1778562930038_921813137.png",
+    "filename": "1778562930038_921813137.png",
+    "generator": "wallco.ai",
+    "seed": 921813137,
+    "is_published": false,
+    "created_at": "2026-05-12T05:15:38.650716+00:00",
+    "motifs": [
+      "floral",
+      "leaf",
+      "vine",
+      "botanical",
+      "abstract",
+      "organic"
+    ],
+    "room_mockups": []
+  },
+  {
+    "id": 179,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#3a443c",
+    "saturation": 0.079,
+    "title": "Noir Salon No.179",
+    "handle": "wallco-0179",
+    "image_url": "/designs/img/var_178_hue+60_1778562938787.png",
+    "filename": "var_178_hue+60_1778562938787.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:15:41.026423+00:00",
+    "motifs": [
+      "floral",
+      "botanical",
+      "leaf",
+      "vine",
+      "geometric"
+    ],
+    "room_mockups": []
+  },
+  {
+    "id": 180,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#3a443c",
+    "saturation": 0.079,
+    "title": "Noir Baroque No.180",
+    "handle": "wallco-0180",
+    "image_url": "/designs/img/var_178_hue+120_1778562941031.png",
+    "filename": "var_178_hue+120_1778562941031.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:15:43.263374+00:00",
+    "motifs": [
+      "floral",
+      "botanical",
+      "leaf",
+      "vine",
+      "geometric",
+      "abstract"
+    ],
+    "room_mockups": []
+  },
+  {
+    "id": 181,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#3a443c",
+    "saturation": 0.079,
+    "title": "Noir Damask No.181",
+    "handle": "wallco-0181",
+    "image_url": "/designs/img/var_178_hue+180_1778562943267.png",
+    "filename": "var_178_hue+180_1778562943267.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:15:45.754283+00:00",
+    "motifs": [
+      "floral",
+      "peony",
+      "leaf",
+      "vine",
+      "botanical"
+    ],
+    "room_mockups": []
+  },
+  {
+    "id": 182,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#3a443c",
+    "saturation": 0.079,
+    "title": "Noir Heirloom No.182",
+    "handle": "wallco-0182",
+    "image_url": "/designs/img/var_178_hue-60_1778562945759.png",
+    "filename": "var_178_hue-60_1778562945759.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:15:48.595157+00:00",
+    "motifs": [
+      "floral",
+      "peony",
+      "rose",
+      "leaf",
+      "vine"
+    ],
+    "room_mockups": []
+  },
+  {
+    "id": 183,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#3a443c",
+    "saturation": 0.079,
+    "title": "Noir Atelier No.183",
+    "handle": "wallco-0183",
+    "image_url": "/designs/img/var_178_mono_1778562948599.png",
+    "filename": "var_178_mono_1778562948599.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:15:50.906122+00:00",
+    "motifs": [
+      "floral",
+      "leaf",
+      "vine",
+      "botanical",
+      "abstract",
+      "organic"
+    ],
+    "room_mockups": []
+  },
+  {
+    "id": 184,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#28342d",
+    "saturation": 0.13,
+    "title": "Sage Salon No.184",
+    "handle": "wallco-0184",
+    "image_url": "/designs/img/1778563118922_1300113543.png",
+    "filename": "1778563118922_1300113543.png",
+    "generator": "wallco.ai",
+    "seed": 1300113543,
+    "is_published": false,
+    "created_at": "2026-05-12T05:18:47.526493+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 185,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#28342d",
+    "saturation": 0.13,
+    "title": "Sage Baroque No.185",
+    "handle": "wallco-0185",
+    "image_url": "/designs/img/var_184_hue+60_1778563127660.png",
+    "filename": "var_184_hue+60_1778563127660.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:18:49.996507+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 186,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#28342d",
+    "saturation": 0.13,
+    "title": "Sage Damask No.186",
+    "handle": "wallco-0186",
+    "image_url": "/designs/img/var_184_hue+120_1778563130001.png",
+    "filename": "var_184_hue+120_1778563130001.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:18:52.184675+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 187,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#28342d",
+    "saturation": 0.13,
+    "title": "Sage Heirloom No.187",
+    "handle": "wallco-0187",
+    "image_url": "/designs/img/var_184_hue+180_1778563132188.png",
+    "filename": "var_184_hue+180_1778563132188.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:18:54.426294+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 188,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#28342d",
+    "saturation": 0.13,
+    "title": "Sage Atelier No.188",
+    "handle": "wallco-0188",
+    "image_url": "/designs/img/var_184_hue-60_1778563134431.png",
+    "filename": "var_184_hue-60_1778563134431.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:18:56.610721+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 189,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#28342d",
+    "saturation": 0.13,
+    "title": "Sage Salon No.189",
+    "handle": "wallco-0189",
+    "image_url": "/designs/img/var_184_mono_1778563136614.png",
+    "filename": "var_184_mono_1778563136614.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:18:58.863617+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 190,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#ecdec0",
+    "saturation": 0.537,
+    "title": "Honey Baroque No.190",
+    "handle": "wallco-0190",
+    "image_url": "/designs/img/1778563307869_1865446799.png",
+    "filename": "1778563307869_1865446799.png",
+    "generator": "wallco.ai",
+    "seed": 1865446799,
+    "is_published": false,
+    "created_at": "2026-05-12T05:21:56.245636+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 191,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#ecdec0",
+    "saturation": 0.537,
+    "title": "Honey Damask No.191",
+    "handle": "wallco-0191",
+    "image_url": "/designs/img/var_190_hue+60_1778563316365.png",
+    "filename": "var_190_hue+60_1778563316365.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:21:58.713109+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 192,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#ecdec0",
+    "saturation": 0.537,
+    "title": "Honey Heirloom No.192",
+    "handle": "wallco-0192",
+    "image_url": "/designs/img/var_190_hue+120_1778563318717.png",
+    "filename": "var_190_hue+120_1778563318717.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:22:01.036998+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 193,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#ecdec0",
+    "saturation": 0.537,
+    "title": "Honey Atelier No.193",
+    "handle": "wallco-0193",
+    "image_url": "/designs/img/var_190_hue+180_1778563321044.png",
+    "filename": "var_190_hue+180_1778563321044.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:22:03.587969+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 194,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#ecdec0",
+    "saturation": 0.537,
+    "title": "Honey Salon No.194",
+    "handle": "wallco-0194",
+    "image_url": "/designs/img/var_190_hue-60_1778563323593.png",
+    "filename": "var_190_hue-60_1778563323593.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:22:05.904185+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 195,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#ecdec0",
+    "saturation": 0.537,
+    "title": "Honey Baroque No.195",
+    "handle": "wallco-0195",
+    "image_url": "/designs/img/var_190_mono_1778563325908.png",
+    "filename": "var_190_mono_1778563325908.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:22:08.212188+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 196,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#23221f",
+    "saturation": 0.061,
+    "title": "Noir Damask No.196",
+    "handle": "wallco-0196",
+    "image_url": "/designs/img/1778563496534_1761929117.png",
+    "filename": "1778563496534_1761929117.png",
+    "generator": "wallco.ai",
+    "seed": 1761929117,
+    "is_published": false,
+    "created_at": "2026-05-12T05:25:04.964954+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 197,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#23221f",
+    "saturation": 0.061,
+    "title": "Noir Heirloom No.197",
+    "handle": "wallco-0197",
+    "image_url": "/designs/img/var_196_hue+60_1778563505156.png",
+    "filename": "var_196_hue+60_1778563505156.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:25:07.427804+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 198,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#23221f",
+    "saturation": 0.061,
+    "title": "Noir Atelier No.198",
+    "handle": "wallco-0198",
+    "image_url": "/designs/img/var_196_hue+120_1778563507432.png",
+    "filename": "var_196_hue+120_1778563507432.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:25:09.707083+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 199,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#23221f",
+    "saturation": 0.061,
+    "title": "Noir Salon No.199",
+    "handle": "wallco-0199",
+    "image_url": "/designs/img/var_196_hue+180_1778563509711.png",
+    "filename": "var_196_hue+180_1778563509711.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:25:11.967865+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 200,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#23221f",
+    "saturation": 0.061,
+    "title": "Noir Baroque No.200",
+    "handle": "wallco-0200",
+    "image_url": "/designs/img/var_196_hue-60_1778563511972.png",
+    "filename": "var_196_hue-60_1778563511972.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:25:14.316108+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 201,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#23221f",
+    "saturation": 0.061,
+    "title": "Noir Damask No.201",
+    "handle": "wallco-0201",
+    "image_url": "/designs/img/var_196_mono_1778563514320.png",
+    "filename": "var_196_mono_1778563514320.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:25:16.60634+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 202,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#1b1110",
+    "saturation": 0.256,
+    "title": "Rose Heirloom No.202",
+    "handle": "wallco-0202",
+    "image_url": "/designs/img/1778563685293_1994468319.png",
+    "filename": "1778563685293_1994468319.png",
+    "generator": "wallco.ai",
+    "seed": 1994468319,
+    "is_published": false,
+    "created_at": "2026-05-12T05:28:13.624778+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 203,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#1b1110",
+    "saturation": 0.256,
+    "title": "Rose Atelier No.203",
+    "handle": "wallco-0203",
+    "image_url": "/designs/img/var_202_hue+60_1778563693747.png",
+    "filename": "var_202_hue+60_1778563693747.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:28:16.027471+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 204,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#1b1110",
+    "saturation": 0.256,
+    "title": "Rose Salon No.204",
+    "handle": "wallco-0204",
+    "image_url": "/designs/img/var_202_hue+120_1778563696035.png",
+    "filename": "var_202_hue+120_1778563696035.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:28:18.3445+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 205,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#1b1110",
+    "saturation": 0.256,
+    "title": "Rose Baroque No.205",
+    "handle": "wallco-0205",
+    "image_url": "/designs/img/var_202_hue+180_1778563698349.png",
+    "filename": "var_202_hue+180_1778563698349.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:28:20.607142+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 206,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#1b1110",
+    "saturation": 0.256,
+    "title": "Rose Damask No.206",
+    "handle": "wallco-0206",
+    "image_url": "/designs/img/var_202_hue-60_1778563700611.png",
+    "filename": "var_202_hue-60_1778563700611.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:28:22.979765+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 207,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#1b1110",
+    "saturation": 0.256,
+    "title": "Rose Heirloom No.207",
+    "handle": "wallco-0207",
+    "image_url": "/designs/img/var_202_mono_1778563702984.png",
+    "filename": "var_202_mono_1778563702984.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:28:25.258481+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 208,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#bc934d",
+    "saturation": 0.453,
+    "title": "Honey Atelier No.208",
+    "handle": "wallco-0208",
+    "image_url": "/designs/img/1778563873872_2136182403.png",
+    "filename": "1778563873872_2136182403.png",
+    "generator": "wallco.ai",
+    "seed": 2136182403,
+    "is_published": false,
+    "created_at": "2026-05-12T05:31:22.053077+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 209,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#bc934d",
+    "saturation": 0.453,
+    "title": "Honey Salon No.209",
+    "handle": "wallco-0209",
+    "image_url": "/designs/img/var_208_hue+60_1778563882180.png",
+    "filename": "var_208_hue+60_1778563882180.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:31:24.385989+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 210,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#bc934d",
+    "saturation": 0.453,
+    "title": "Honey Baroque No.210",
+    "handle": "wallco-0210",
+    "image_url": "/designs/img/var_208_hue+120_1778563884392.png",
+    "filename": "var_208_hue+120_1778563884392.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:31:26.604403+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 211,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#bc934d",
+    "saturation": 0.453,
+    "title": "Honey Damask No.211",
+    "handle": "wallco-0211",
+    "image_url": "/designs/img/var_208_hue+180_1778563886608.png",
+    "filename": "var_208_hue+180_1778563886608.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:31:28.839466+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 212,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#bc934d",
+    "saturation": 0.453,
+    "title": "Honey Heirloom No.212",
+    "handle": "wallco-0212",
+    "image_url": "/designs/img/var_208_hue-60_1778563888843.png",
+    "filename": "var_208_hue-60_1778563888843.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:31:31.033986+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 213,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#bc934d",
+    "saturation": 0.453,
+    "title": "Honey Atelier No.213",
+    "handle": "wallco-0213",
+    "image_url": "/designs/img/var_208_mono_1778563891038.png",
+    "filename": "var_208_mono_1778563891038.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:31:33.275957+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 214,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#394947",
+    "saturation": 0.123,
+    "title": "Marine Salon No.214",
+    "handle": "wallco-0214",
+    "image_url": "/designs/img/1778564062849_1401191032.png",
+    "filename": "1778564062849_1401191032.png",
+    "generator": "wallco.ai",
+    "seed": 1401191032,
+    "is_published": false,
+    "created_at": "2026-05-12T05:34:35.3107+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 215,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#394947",
+    "saturation": 0.123,
+    "title": "Marine Baroque No.215",
+    "handle": "wallco-0215",
+    "image_url": "/designs/img/var_214_hue+60_1778564075468.png",
+    "filename": "var_214_hue+60_1778564075468.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:34:38.074384+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 216,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#394947",
+    "saturation": 0.123,
+    "title": "Marine Damask No.216",
+    "handle": "wallco-0216",
+    "image_url": "/designs/img/var_214_hue+120_1778564078081.png",
+    "filename": "var_214_hue+120_1778564078081.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:34:40.960073+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 217,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#394947",
+    "saturation": 0.123,
+    "title": "Marine Heirloom No.217",
+    "handle": "wallco-0217",
+    "image_url": "/designs/img/var_214_hue+180_1778564080967.png",
+    "filename": "var_214_hue+180_1778564080967.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:34:45.846572+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 218,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#394947",
+    "saturation": 0.123,
+    "title": "Marine Atelier No.218",
+    "handle": "wallco-0218",
+    "image_url": "/designs/img/var_214_hue-60_1778564085862.png",
+    "filename": "var_214_hue-60_1778564085862.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:34:48.293276+00:00",
+    "motifs": [],
+    "room_mockups": []
+  },
+  {
+    "id": 219,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#394947",
+    "saturation": 0.123,
+    "title": "Marine Salon No.219",
+    "handle": "wallco-0219",
+    "image_url": "/designs/img/var_214_mono_1778564088299.png",
+    "filename": "var_214_mono_1778564088299.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-12T05:34:50.721678+00:00",
+    "motifs": [],
+    "room_mockups": []
   }
 ]
\ No newline at end of file
diff --git a/scripts/refresh_designs_snapshot.py b/scripts/refresh_designs_snapshot.py
index e94d227..dcaa46d 100644
--- a/scripts/refresh_designs_snapshot.py
+++ b/scripts/refresh_designs_snapshot.py
@@ -65,7 +65,7 @@ def title_for(cat, hex_, id_):
 rows = psql_json(
     "SELECT COALESCE(json_agg(t),'[]'::json) FROM ("
     "SELECT id, kind, category, dominant_hex, prompt, generator, seed, local_path, "
-    "is_published, created_at, motifs "
+    "is_published, created_at, motifs, room_mockups "
     "FROM spoon_all_designs WHERE generator='replicate' ORDER BY id) t;"
 )
 
@@ -87,7 +87,10 @@ for r in rows:
         "seed": r['seed'],
         "is_published": bool(r['is_published']),
         "created_at": r['created_at'],
-        "motifs": r.get('motifs') or []
+        "motifs": r.get('motifs') or [],
+        # Render-ready URL list — server-side disk paths flipped to /designs/room/:id/:room.
+        # Snapshot only stores room types; server resolves the file from disk at request time.
+        "room_mockups": sorted(list((r.get('room_mockups') or {}).keys()))
     })
 
 OUT.write_text(json.dumps(out, indent=2))
diff --git a/server.js b/server.js
index b62f40c..3b81b70 100644
--- a/server.js
+++ b/server.js
@@ -104,6 +104,13 @@ app.use('/designs/img', express.static(IMG_DIR, {
   }
 }));
 
+// ── Serve pre-generated room mockups (one PNG per design+room type).
+// Paths live in spoon_all_designs.room_mockups JSONB; physical files in data/rooms/.
+app.use('/designs/room', express.static(path.join(__dirname, 'data', 'rooms'), {
+  maxAge: '7d',
+  setHeaders: (res) => res.setHeader('Cache-Control', 'public, max-age=604800')
+}));
+
 // ── Load design catalog (snapshot from DB — never stale-live Shopify)
 let DESIGNS = [];
 function loadDesigns() {
@@ -279,6 +286,11 @@ setInterval(() => load(true), 30000);
 // check as a CRITICAL bypass (anyone could mint a token with {role:"admin"}).
 const { isAdmin, requireAdmin } = require('./src/admin-gate');
 
+// Color-isolate module — palette extract, Sherwin-Williams nearest match,
+// coordinating-color suggestions. Sourced from ~/Projects/_shared/color-isolate.js
+// and copied locally per deploy-portability rule (Kamatera lacks ~/.claude/skills/).
+const colorIsolate = require('./src/color-isolate');
+
 // ── GA4 measurement ID
 const GA4_ID = process.env.GA4_MEASUREMENT_ID || '';
 
@@ -1030,6 +1042,61 @@ ${_isAdmin ? `
   #chat-form button { padding:8px 14px; background:#d2b15c; color:#0f0e0c; border:none; cursor:pointer; font:inherit; font-weight:500; border-radius:2px; }
   #chat-form button:disabled { opacity:.5; cursor:not-allowed; }
   .admin-banner { background:#2a2825; color:#d2b15c; padding:6px 14px; font-size:11px; letter-spacing:.06em; text-transform:uppercase; }
+  /* Isolate modal — left-pinned drawer; mirrors pair-modal styling */
+  :root { --modal-iso-w: 520px; }
+  #iso-modal {
+    position:fixed; top:0; left:0; bottom:0;
+    width: min(var(--modal-iso-w), 96vw);
+    background:#16140f; border-right:1px solid #2a2825;
+    display:flex; flex-direction:column; z-index:1001;
+    box-shadow:8px 0 20px rgba(0,0,0,.4);
+    color:#e8e2d6; font:13px/1.4 -apple-system, system-ui, sans-serif;
+    transform: translateX(-100%);
+    transition: transform .26s cubic-bezier(.4,0,.2,1);
+    visibility: hidden;
+  }
+  #iso-modal.open { transform: translateX(0); visibility: visible; }
+  #iso-modal header { padding:14px 18px; border-bottom:1px solid #2a2825; display:flex; gap:10px; align-items:center; }
+  #iso-modal header .ctx-main { flex:1; font-size:12px; color:#d2b15c; }
+  #iso-modal header .ctx-sub  { font-size:10px; color:#888; margin-top:2px; }
+  #iso-modal header .close { background:none; border:1px solid #3a3631; color:#bba; font-size:18px; line-height:1; padding:2px 8px; cursor:pointer; border-radius:2px; }
+  #iso-modal header .close:hover { color:#fff; border-color:#d2b15c; }
+  #iso-body { flex:1; overflow-y:auto; padding:14px 18px; display:flex; flex-direction:column; gap:14px; }
+  #iso-hero { width:100%; aspect-ratio:1/1; background:#0d0c0a no-repeat center/cover; border-radius:3px; border:1px solid #2a2825; }
+  .iso-palette { display:flex; gap:8px; flex-wrap:wrap; }
+  .iso-sw { flex:1 1 90px; min-width:80px; display:flex; flex-direction:column; align-items:stretch; gap:5px; padding:8px; background:#1a1816; border:1px solid #2a2825; border-radius:3px; }
+  .iso-sw .dot { width:100%; aspect-ratio:1/1; border-radius:50%; border:1px solid rgba(255,255,255,.06); }
+  .iso-sw .hex { font-family: ui-monospace, monospace; font-size:11px; color:#e8e2d6; text-align:center; letter-spacing:.04em; }
+  .iso-sw .sw-code { font-size:10px; color:#d2b15c; text-align:center; letter-spacing:.04em; }
+  .iso-sw .sw-name { font-size:10px; color:#888; text-align:center; line-height:1.2; }
+  .iso-sw .sw-empty { font-size:10px; color:#555; text-align:center; font-style:italic; }
+  .iso-action { display:flex; gap:10px; align-items:center; }
+  .iso-btn-isolate { flex:1; padding:10px 16px; background:#d2b15c; color:#0f0e0c; border:none; cursor:pointer; font:13px/1.2 -apple-system, system-ui, sans-serif; font-weight:600; border-radius:3px; letter-spacing:.04em; text-transform:uppercase; }
+  .iso-btn-isolate:hover { background:#e8c46d; }
+  .iso-btn-isolate:disabled { opacity:.55; cursor:default; }
+  .iso-mode-toggle { display:none; gap:0; border:1px solid #3a3631; border-radius:3px; overflow:hidden; }
+  .iso-mode-toggle.visible { display:flex; }
+  .iso-mode-toggle button { flex:1; padding:8px 14px; background:#16140f; color:#bba; border:none; cursor:pointer; font:12px/1.2 -apple-system, system-ui, sans-serif; letter-spacing:.06em; text-transform:uppercase; }
+  .iso-mode-toggle button.active { background:#d2b15c; color:#0f0e0c; }
+  .iso-mode-toggle button + button { border-left:1px solid #3a3631; }
+  .iso-section { display:none; }
+  .iso-section.visible { display:block; }
+  .iso-section h4 { font-size:11px; color:#d2b15c; letter-spacing:.08em; text-transform:uppercase; margin:0 0 8px; }
+  .iso-grid-wallpaper { display:grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap:8px; }
+  .iso-grid-wallpaper a { display:block; aspect-ratio:1/1; background:#0d0c0a no-repeat center/cover; border-radius:3px; border:1px solid #2a2825; position:relative; text-decoration:none; }
+  .iso-grid-wallpaper a:hover { border-color:#d2b15c; }
+  .iso-grid-wallpaper .iso-de { position:absolute; bottom:2px; right:3px; font:9px/1 ui-monospace, monospace; color:#fff; background:rgba(0,0,0,.6); padding:1px 4px; border-radius:2px; }
+  .iso-grid-paint { display:grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap:10px; }
+  .iso-grid-paint .iso-paint-card { display:flex; flex-direction:column; gap:5px; padding:8px; background:#1a1816; border:1px solid #2a2825; border-radius:3px; }
+  .iso-grid-paint .iso-paint-card .swatch { width:100%; aspect-ratio:1/1; border-radius:3px; border:1px solid rgba(255,255,255,.06); }
+  .iso-grid-paint .iso-paint-card .role { font-size:9px; color:#888; text-transform:uppercase; letter-spacing:.08em; }
+  .iso-grid-paint .iso-paint-card .hex { font-family: ui-monospace, monospace; font-size:11px; color:#e8e2d6; }
+  .iso-grid-paint .iso-paint-card .sw-code { font-size:10px; color:#d2b15c; }
+  .iso-grid-paint .iso-paint-card .sw-name { font-size:10px; color:#bba; line-height:1.2; }
+  .iso-grid-paint .iso-paint-card button { margin-top:4px; padding:4px 6px; background:#0f0e0c; border:1px solid #3a3631; color:#bba; font:10px/1 -apple-system, system-ui, sans-serif; cursor:pointer; border-radius:2px; letter-spacing:.04em; }
+  .iso-grid-paint .iso-paint-card button:hover { color:#d2b15c; border-color:#d2b15c; }
+  .iso-loading { color:#888; font-style:italic; padding:20px 0; text-align:center; font-size:12px; }
+  .design-card.review-card .img-wrap { cursor: zoom-in; }
 </style>
 
 <div id="chat-modal">
@@ -1060,6 +1127,38 @@ ${_isAdmin ? `
   <div id="pair-body"></div>
 </div>
 
+<div id="iso-modal" aria-hidden="true" role="dialog">
+  <header>
+    <div style="flex:1">
+      <div class="ctx-main" id="iso-design-title">Color isolate</div>
+      <div class="ctx-sub" id="iso-design-sub"></div>
+    </div>
+    <button class="close" id="iso-close" title="Close (Esc)">&times;</button>
+  </header>
+  <div id="iso-body">
+    <div id="iso-hero"></div>
+    <div>
+      <h4 style="font-size:11px;color:#d2b15c;letter-spacing:.08em;text-transform:uppercase;margin:0 0 8px">Palette</h4>
+      <div class="iso-palette" id="iso-palette"><div class="iso-loading">extracting…</div></div>
+    </div>
+    <div class="iso-action">
+      <button class="iso-btn-isolate" id="iso-btn-isolate" type="button">Isolate this design</button>
+    </div>
+    <div class="iso-mode-toggle" id="iso-mode-toggle">
+      <button type="button" data-mode="wallpaper" class="active">Wallpaper</button>
+      <button type="button" data-mode="paint">Paint</button>
+    </div>
+    <section class="iso-section visible" id="iso-section-wallpaper">
+      <h4>Coordinating wallpapers</h4>
+      <div class="iso-grid-wallpaper" id="iso-grid-wallpaper"><div class="iso-loading">loading…</div></div>
+    </section>
+    <section class="iso-section" id="iso-section-paint">
+      <h4>Coordinating paint colors</h4>
+      <div class="iso-grid-paint" id="iso-grid-paint"></div>
+    </section>
+  </div>
+</div>
+
 <script>
 (function(){
   // ── State ──────────────────────────────────────────────────────
@@ -1367,7 +1466,7 @@ ${_isAdmin ? `
   document.addEventListener('keydown', e => {
     // ignore when typing in input/textarea or modals open with input focused
     if (e.target.matches('input, textarea, select')) return;
-    if (document.querySelector('#chat-modal.open, #pair-modal.open')) return; // modal handles its own keys
+    if (document.querySelector('#chat-modal.open, #pair-modal.open, #iso-modal.open')) return; // modal handles its own keys
     const cards = Array.from(document.querySelectorAll('.design-card.review-card'));
     if (!cards.length) return;
     let idx = FOCUSED_CARD ? cards.indexOf(FOCUSED_CARD) : 0;
@@ -1406,6 +1505,169 @@ ${_isAdmin ? `
   kbdStyle.textContent = '.design-card.review-card.kbd-focus { outline: 2px solid #d2b15c; outline-offset: 2px; }';
   document.head.appendChild(kbdStyle);
 
+  // ── Isolate modal ─────────────────────────────────────────────
+  // Sibling to pair-modal. Click any image (or press I) → open left drawer
+  // with palette + SW codes; "Isolate" reveals Wallpaper/Paint mode toggle.
+  const isoModal = $('#iso-modal');
+  const isoBody = $('#iso-body');
+  let CURRENT_ISO_ID = null;
+  let CURRENT_ISO_DATA = null;
+  function closeIso() { isoModal.classList.remove('open'); isoModal.setAttribute('aria-hidden', 'true'); }
+  $('#iso-close').addEventListener('click', closeIso);
+  document.addEventListener('keydown', e => { if (e.key === 'Escape' && isoModal.classList.contains('open')) closeIso(); });
+
+  async function openIsolate(designId, title, imageUrl) {
+    CURRENT_ISO_ID = designId;
+    CURRENT_ISO_DATA = null;
+    $('#iso-design-title').textContent = 'Color isolate';
+    $('#iso-design-sub').textContent = '#' + designId + ' · ' + (title || '').slice(0, 80);
+    const hero = $('#iso-hero');
+    hero.style.backgroundImage = imageUrl ? "url('" + imageUrl + "')" : 'none';
+    $('#iso-palette').innerHTML = '<div class="iso-loading">extracting palette…</div>';
+    $('#iso-grid-wallpaper').innerHTML = '<div class="iso-loading">loading…</div>';
+    $('#iso-grid-paint').innerHTML = '';
+    $('#iso-mode-toggle').classList.remove('visible');
+    $('#iso-section-wallpaper').classList.add('visible');
+    $('#iso-section-paint').classList.remove('visible');
+    $('#iso-btn-isolate').disabled = false;
+    $('#iso-btn-isolate').textContent = 'Isolate this design';
+    isoModal.classList.add('open');
+    isoModal.setAttribute('aria-hidden', 'false');
+    try {
+      const r = await fetch('/api/isolate/' + designId);
+      if (!r.ok) {
+        $('#iso-palette').innerHTML = '<div class="iso-loading">failed (status ' + r.status + ')</div>';
+        return;
+      }
+      const data = await r.json();
+      CURRENT_ISO_DATA = data;
+      renderIsoPalette(data.palette || []);
+    } catch (e) {
+      $('#iso-palette').innerHTML = '<div class="iso-loading">error: ' + escapeHTML(e.message) + '</div>';
+    }
+  }
+
+  function renderIsoPalette(palette) {
+    if (!palette || !palette.length) {
+      $('#iso-palette').innerHTML = '<div class="iso-loading">no palette extracted</div>';
+      return;
+    }
+    $('#iso-palette').innerHTML = palette.map(p => {
+      const hex = p.hex || '#888';
+      const sw = p.sw;
+      const swBlock = sw
+        ? '<div class="sw-code">' + escapeHTML(sw.code) + '</div>' +
+          '<div class="sw-name">' + escapeHTML(sw.name) + '</div>'
+        : '<div class="sw-empty">—</div>';
+      return '<div class="iso-sw" title="' + escapeAttr(hex) + (sw ? ' · ' + escapeAttr(sw.code + ' ' + sw.name) : '') + '">' +
+             '<div class="dot" style="background:' + escapeAttr(hex) + '"></div>' +
+             '<div class="hex">' + escapeHTML(hex) + '</div>' +
+             swBlock +
+             '</div>';
+    }).join('');
+  }
+
+  function renderIsoWallpaper(designs) {
+    const grid = $('#iso-grid-wallpaper');
+    if (!designs || !designs.length) {
+      grid.innerHTML = '<div class="iso-loading">no coordinating wallpapers yet</div>';
+      return;
+    }
+    grid.innerHTML = designs.map(d => {
+      const img = (d.image_url || '').replace(/'/g, '');
+      const t = escapeAttr((d.title || '#' + d.id) + ' · ΔE ' + (d.deltaE != null ? d.deltaE : '?'));
+      return '<a href="/design/' + d.id + '" target="_blank" title="' + t + '" style="background-image:url(' + img + ')">' +
+             '<span class="iso-de">ΔE ' + (d.deltaE != null ? d.deltaE.toFixed(1) : '?') + '</span>' +
+             '</a>';
+    }).join('');
+  }
+
+  function renderIsoPaint(coord) {
+    const grid = $('#iso-grid-paint');
+    if (!coord || !Object.keys(coord).length) {
+      grid.innerHTML = '<div class="iso-loading">no coordinating colors</div>';
+      return;
+    }
+    const labelMap = {
+      complement: 'Complement',
+      analogousLeft: 'Analogous −30°',
+      analogousRight: 'Analogous +30°',
+      triadicA: 'Triadic +120°',
+      triadicB: 'Triadic +240°',
+      splitComplementA: 'Split-comp +150°',
+      splitComplementB: 'Split-comp +210°',
+    };
+    grid.innerHTML = Object.entries(coord).map(([role, val]) => {
+      const hex = val.hex || '#888';
+      const sw = val.sw;
+      return '<div class="iso-paint-card">' +
+             '<div class="swatch" style="background:' + escapeAttr(hex) + '"></div>' +
+             '<div class="role">' + escapeHTML(labelMap[role] || role) + '</div>' +
+             '<div class="hex">' + escapeHTML(hex) + '</div>' +
+             (sw
+               ? '<div class="sw-code">' + escapeHTML(sw.code) + '</div>' +
+                 '<div class="sw-name">' + escapeHTML(sw.name) + '</div>'
+               : '<div class="sw-name" style="color:#555;font-style:italic">SW DB not loaded</div>') +
+             '<button type="button" data-copy="' + escapeAttr(hex) + '">copy hex</button>' +
+             '</div>';
+    }).join('');
+    grid.querySelectorAll('button[data-copy]').forEach(b => {
+      b.addEventListener('click', async () => {
+        try { await navigator.clipboard.writeText(b.dataset.copy); b.textContent = 'copied!'; setTimeout(() => b.textContent = 'copy hex', 1200); }
+        catch { b.textContent = 'copy failed'; }
+      });
+    });
+  }
+
+  // Isolate button → reveal mode toggle + render initial wallpaper view
+  $('#iso-btn-isolate').addEventListener('click', () => {
+    if (!CURRENT_ISO_DATA) return;
+    $('#iso-mode-toggle').classList.add('visible');
+    $('#iso-btn-isolate').disabled = true;
+    $('#iso-btn-isolate').textContent = 'Isolated · pick a mode';
+    renderIsoWallpaper(CURRENT_ISO_DATA.coordinating_designs || []);
+    renderIsoPaint(CURRENT_ISO_DATA.coordinating_hexes || {});
+  });
+
+  $('#iso-mode-toggle').addEventListener('click', e => {
+    const btn = e.target.closest('button[data-mode]');
+    if (!btn) return;
+    const mode = btn.dataset.mode;
+    $$('#iso-mode-toggle button').forEach(b => b.classList.toggle('active', b === btn));
+    $('#iso-section-wallpaper').classList.toggle('visible', mode === 'wallpaper');
+    $('#iso-section-paint').classList.toggle('visible', mode === 'paint');
+  });
+
+  // Click any review-card image to open isolate (admin-only, since the API is admin-gated;
+  // non-admin users won't have these cards rendered anyway per /designs ?review path).
+  document.addEventListener('click', e => {
+    const img = e.target.closest('.design-card.review-card .img-wrap, .design-card.review-card img');
+    if (!img) return;
+    const card = img.closest('.design-card.review-card');
+    if (!card) return;
+    // Don't hijack clicks on buttons / decision row / chips
+    if (e.target.closest('button, a, input, select, .chips')) return;
+    e.preventDefault();
+    const id = card.dataset.id;
+    const title = (card.querySelector('.card-title') || {}).textContent || ('#' + id);
+    const imgEl = card.querySelector('img');
+    const url = imgEl ? imgEl.src : '';
+    openIsolate(id, title, url);
+  });
+
+  // Keyboard: I = open isolate for focused card
+  document.addEventListener('keydown', e => {
+    if (e.target.matches('input, textarea, select')) return;
+    if (document.querySelector('#chat-modal.open, #pair-modal.open, #iso-modal.open')) return;
+    if (e.key.toLowerCase() === 'i' && FOCUSED_CARD) {
+      e.preventDefault();
+      const id = FOCUSED_CARD.dataset.id;
+      const title = (FOCUSED_CARD.querySelector('.card-title') || {}).textContent || ('#' + id);
+      const imgEl = FOCUSED_CARD.querySelector('img');
+      openIsolate(id, title, imgEl ? imgEl.src : '');
+    }
+  });
+
   boot();
 })();
 </script>
@@ -1743,6 +2005,7 @@ app.get('/design/:id', (req, res) => {
   if (!Number.isFinite(id) || id < 1) {
     return res.status(404).type('html').send(renderDesignNotFound(req));
   }
+  const _isAdmin = isAdmin(req);
   let design = DESIGNS.find(d => d.id === id);
   // Hot-path fallback: design was generated after server start (e.g. just baked
   // from /design/:id/bake). Live-query PG and synthesize a snapshot-matching
@@ -1750,7 +2013,7 @@ app.get('/design/:id', (req, res) => {
   if (!design) {
     try {
       const row = psqlQuery(`SELECT row_to_json(t) FROM (SELECT id, kind, category,
-        dominant_hex, prompt, seed, local_path, is_published, created_at, notes
+        dominant_hex, prompt, seed, local_path, is_published, created_at, notes, room_mockups
         FROM spoon_all_designs WHERE id=${id}) t;`);
       if (row) {
         const r = JSON.parse(row);
@@ -1764,7 +2027,8 @@ app.get('/design/:id', (req, res) => {
           image_url: `/designs/img/${filename}`, filename,
           generator: 'wallco.ai', seed: r.seed,
           is_published: !!r.is_published, created_at: r.created_at,
-          notes: r.notes || null
+          notes: r.notes || null,
+          room_mockups: Object.keys(r.room_mockups || {}).sort()
         };
         DESIGNS.push(design);  // lazy-extend so next request is fast
       }
@@ -2379,6 +2643,17 @@ ${htmlHeader('/designs')}
       </script>
       ` : ''}
 
+      ${(Array.isArray(design.room_mockups) && design.room_mockups.length) ? `
+      <!-- Pre-generated room mockup (auto-rendered for top-saturation designs) -->
+      <figure class="room-precanned" style="margin:22px 0 4px;border-radius:10px;overflow:hidden;background:rgba(0,0,0,.04)">
+        <img src="/designs/room/design_${design.id}_${design.room_mockups[0]}.png"
+             alt="${design.title} — ${design.room_mockups[0].replace(/_/g,' ')}"
+             loading="lazy"
+             style="display:block;width:100%;height:auto;border-radius:10px">
+        <figcaption style="font-size:12px;color:var(--ink-faint);padding:8px 12px">In ${design.room_mockups[0].replace(/_/g,' ')} — AI room rendering</figcaption>
+      </figure>
+      ` : ''}
+
       <!-- Tools (admin / preview) -->
       <div class="tool-group" style="margin-top:18px;display:flex;flex-wrap:wrap;gap:10px">
         <button type="button" class="btn-outline" id="btn-room"        data-id="${design.id}">See in a Room</button>
@@ -5335,6 +5610,73 @@ app.get('/api/design/:id/pairings', (req, res) => {
   }
 });
 
+// ── GET /api/isolate/:designId — palette + SW codes + coordinating designs
+// Admin-only (404 elsewhere via requireAdmin). Reuses /api/pairings for the
+// design grid via colorIsolate.coordinatingDesigns; adds a SW-annotated palette
+// and 7 paint-coordinating hexes. SW DB is read lazily — missing DB returns
+// `sw: null` per entry and the UI degrades gracefully.
+app.get('/api/isolate/:designId', requireAdmin, (req, res) => {
+  try {
+    const id = parseInt(req.params.designId, 10);
+    const design = DESIGNS.find(d => d.id === id);
+    if (!design) return res.status(404).json({ error: 'design not found' });
+
+    // Resolve image path — prefer local_path, fall back to image_url → IMG_DIR
+    let imgPath = design.local_path || null;
+    if (!imgPath && design.filename) imgPath = path.join(IMG_DIR, design.filename);
+    if (!imgPath && design.image_url) {
+      imgPath = path.join(IMG_DIR, path.basename(design.image_url));
+    }
+
+    // Extract top-6 palette from image; if extraction fails (no PIL, missing file),
+    // fall back to just the dominant_hex.
+    let paletteHexes = [];
+    if (imgPath && fs.existsSync(imgPath)) {
+      paletteHexes = colorIsolate.extractPalette(imgPath, 6);
+    }
+    if (!paletteHexes.length && design.dominant_hex) {
+      paletteHexes = [design.dominant_hex];
+    }
+    const palette = colorIsolate.annotatePalette(paletteHexes);
+
+    // Coordinating hexes (color theory) — anchored on the dominant hex so the
+    // paint view stays stable across re-extracts. Each annotated with SW match.
+    const anchor = design.dominant_hex || paletteHexes[0] || '#888888';
+    const coordHexes = colorIsolate.coordinatingHexes(anchor) || {};
+    const coordinating_hexes = {};
+    for (const [k, hex] of Object.entries(coordHexes)) {
+      coordinating_hexes[k] = { hex, sw: colorIsolate.nearestSW(hex) };
+    }
+
+    // Coordinating designs from the catalog — top-12 by Lab ΔE.
+    const coordinating_designs = colorIsolate
+      .coordinatingDesigns(anchor, DESIGNS.filter(d => d.id !== id), { limit: 12 })
+      .map(d => ({
+        id: d.id,
+        title: d.title,
+        image_url: d.image_url,
+        dominant_hex: d.dominant_hex,
+        category: d.category,
+        deltaE: d.deltaE,
+      }));
+
+    res.json({
+      design: {
+        id: design.id,
+        title: design.title,
+        image_url: design.image_url,
+        category: design.category,
+        dominant_hex: design.dominant_hex,
+      },
+      palette,
+      coordinating_hexes,
+      coordinating_designs,
+    });
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
 // ── POST /api/design/:id/bake — save a user-edited recolor as a new design
 app.post('/api/design/:id/bake',
   express.json({ limit: '20mb' }),
diff --git a/src/color-isolate.js b/src/color-isolate.js
new file mode 100644
index 0000000..67d8a9b
--- /dev/null
+++ b/src/color-isolate.js
@@ -0,0 +1,205 @@
+// Shared color-isolate module — usable by wallco-ai, thesetdecorator,
+// and any other DW-family project that needs:
+//   - palette extraction from an image (top-N dominant colors)
+//   - nearest Sherwin-Williams color lookup (Lab distance)
+//   - coordinating-color generator (complement / analogous / triadic / split-complement)
+//
+// Canonical location: ~/Projects/_shared/color-isolate.js
+// Mirror policy: each consuming project copies this file to its own src/
+// or lib/ folder; deploy-portability requires the local copy. Diff to detect drift.
+//
+// Sherwin-Williams data: requires ~/Projects/_shared/sw-colors.json
+// Format: [{ "code": "SW7028", "name": "Incredible White", "hex": "#ede6de" }, ...]
+//
+// 2026-05-12 (first ship for "show color dots by hex# and by SW code next to each
+// image clicked on, plus Isolate button with wallpaper/paint toggle and
+// coordinating designs grid" — for wallco.ai + thesetdecorator).
+
+'use strict';
+const fs = require('fs');
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+const SW_DB_PATH = path.join(__dirname, 'sw-colors.json');
+
+// ── Hex / color math ─────────────────────────────────────────────────────
+
+function expandHex(h) {
+  h = String(h || '').replace('#', '').trim();
+  if (h.length === 3) h = h.split('').map(c => c + c).join('');
+  return h.length === 6 ? h.toLowerCase() : null;
+}
+
+function hexToRgb(hex) {
+  const h = expandHex(hex);
+  if (!h) return null;
+  return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
+}
+
+function rgbToHex([r, g, b]) {
+  const clamp = x => Math.max(0, Math.min(255, Math.round(x)));
+  return '#' + [r, g, b].map(x => clamp(x).toString(16).padStart(2, '0')).join('');
+}
+
+// sRGB → linear → XYZ → CIE Lab. Lab is perceptually uniform-ish; ΔE in Lab
+// is a much better "color match" metric than RGB distance.
+function rgbToLab([r, g, b]) {
+  const lin = c => { c /= 255; return c > 0.04045 ? Math.pow((c + 0.055) / 1.055, 2.4) : c / 12.92; };
+  const R = lin(r), G = lin(g), B = lin(b);
+  // sRGB → XYZ (D65)
+  const X = R * 0.4124564 + G * 0.3575761 + B * 0.1804375;
+  const Y = R * 0.2126729 + G * 0.7151522 + B * 0.0721750;
+  const Z = R * 0.0193339 + G * 0.1191920 + B * 0.9503041;
+  // XYZ → Lab (D65 reference white)
+  const f = t => t > 0.008856 ? Math.cbrt(t) : (7.787 * t + 16 / 116);
+  const fx = f(X / 0.95047), fy = f(Y / 1.00000), fz = f(Z / 1.08883);
+  return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];
+}
+
+function deltaE(lab1, lab2) {
+  // CIE76 — simple Euclidean in Lab. Adequate for "nearest paint match" UX.
+  const dl = lab1[0] - lab2[0], da = lab1[1] - lab2[1], db = lab1[2] - lab2[2];
+  return Math.sqrt(dl * dl + da * da + db * db);
+}
+
+function rgbToHsl([r, g, b]) {
+  r /= 255; g /= 255; b /= 255;
+  const max = Math.max(r, g, b), min = Math.min(r, g, b);
+  const l = (max + min) / 2;
+  let h = 0, s = 0;
+  if (max !== min) {
+    const d = max - min;
+    s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+    if (max === r)      h = ((g - b) / d) + (g < b ? 6 : 0);
+    else if (max === g) h = ((b - r) / d) + 2;
+    else                h = ((r - g) / d) + 4;
+    h *= 60;
+  }
+  return [h, s, l];
+}
+
+function hslToRgb(h, s, l) {
+  h = ((h % 360) + 360) % 360;
+  const c = (1 - Math.abs(2 * l - 1)) * s;
+  const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
+  const m = l - c / 2;
+  let r = 0, g = 0, b = 0;
+  if      (h <  60) { [r, g, b] = [c, x, 0]; }
+  else if (h < 120) { [r, g, b] = [x, c, 0]; }
+  else if (h < 180) { [r, g, b] = [0, c, x]; }
+  else if (h < 240) { [r, g, b] = [0, x, c]; }
+  else if (h < 300) { [r, g, b] = [x, 0, c]; }
+  else              { [r, g, b] = [c, 0, x]; }
+  return [(r + m) * 255, (g + m) * 255, (b + m) * 255];
+}
+
+// ── Palette extraction from an image (top-N dominant colors) ────────────
+
+// Returns array of hex strings sorted by cluster size, top-N.
+// Uses Python+PIL because it's pre-installed on macOS; we don't add a node dep.
+function extractPalette(imagePath, n = 6) {
+  if (!fs.existsSync(imagePath)) return [];
+  const script = `
+import sys
+from PIL import Image
+from collections import Counter
+img = Image.open(${JSON.stringify(imagePath)}).convert("RGB")
+img.thumbnail((128, 128))  # speed + dampens noise
+# Quantize to ${n * 4} colors (over-cluster so we can pick spread later)
+q = img.quantize(colors=${n * 4}, method=1)  # MEDIANCUT
+pal = q.getpalette()[: ${n * 4} * 3]
+counts = Counter(q.getdata())
+# Drop near-greys to expose chromatic colors (designer-relevant)
+out = []
+for idx, _ in counts.most_common():
+    r, g, b = pal[idx*3], pal[idx*3+1], pal[idx*3+2]
+    out.append("#%02x%02x%02x" % (r, g, b))
+print("\\n".join(out[:${n}]))
+`;
+  const proc = spawnSync('python3', ['-c', script], { encoding: 'utf8', timeout: 8000 });
+  if (proc.status !== 0) return [];
+  return proc.stdout.trim().split('\n').filter(Boolean);
+}
+
+// ── Sherwin-Williams nearest match ───────────────────────────────────────
+
+let _swCache = null;
+function loadSW() {
+  if (_swCache) return _swCache;
+  try {
+    const raw = fs.readFileSync(SW_DB_PATH, 'utf8');
+    _swCache = JSON.parse(raw).map(e => ({ ...e, _lab: rgbToLab(hexToRgb(e.hex)) }));
+  } catch {
+    _swCache = [];
+  }
+  return _swCache;
+}
+
+// Returns { code, name, hex, deltaE } for the closest SW color to `hex`,
+// or null if hex is bad or SW DB missing.
+function nearestSW(hex) {
+  const rgb = hexToRgb(hex);
+  if (!rgb) return null;
+  const db = loadSW();
+  if (!db.length) return null;
+  const lab = rgbToLab(rgb);
+  let best = null;
+  for (const sw of db) {
+    const d = deltaE(lab, sw._lab);
+    if (!best || d < best.deltaE) best = { code: sw.code, name: sw.name, hex: sw.hex, deltaE: +d.toFixed(2) };
+  }
+  return best;
+}
+
+// Annotate every hex in a palette with its nearest SW pair.
+// Returns [{ hex, sw: { code, name, hex, deltaE } | null }, ...]
+function annotatePalette(palette) {
+  return (palette || []).map(hex => ({ hex, sw: nearestSW(hex) }));
+}
+
+// ── Coordinating colors (color theory) ──────────────────────────────────
+
+// Given a primary hex, return suggested coordinating hexes.
+// Returns { complement, analogousLeft, analogousRight, triadicA, triadicB, splitComplementA, splitComplementB }
+function coordinatingHexes(primaryHex) {
+  const rgb = hexToRgb(primaryHex);
+  if (!rgb) return null;
+  const [h, s, l] = rgbToHsl(rgb);
+  // Lock saturation/lightness near sensible ranges so output isn't muddy.
+  const ss = Math.max(0.35, Math.min(0.85, s));
+  const ll = Math.max(0.30, Math.min(0.70, l));
+  const make = (hueShift) => rgbToHex(hslToRgb(h + hueShift, ss, ll));
+  return {
+    complement: make(180),
+    analogousLeft: make(-30),
+    analogousRight: make(30),
+    triadicA: make(120),
+    triadicB: make(240),
+    splitComplementA: make(150),
+    splitComplementB: make(210),
+  };
+}
+
+// ── Coordinating wallcoverings (find catalog matches by palette) ────────
+
+// Given a primary hex and an array of designs (each w/ `dominant_hex`),
+// return the top-N closest in Lab distance. Designs without a hex are skipped.
+function coordinatingDesigns(primaryHex, designs, opts = {}) {
+  const n = opts.limit || 12;
+  const lab = rgbToLab(hexToRgb(primaryHex));
+  if (!lab) return [];
+  const scored = (designs || []).filter(d => d.dominant_hex).map(d => {
+    const dlab = rgbToLab(hexToRgb(d.dominant_hex));
+    return { ...d, deltaE: dlab ? +deltaE(lab, dlab).toFixed(2) : 999 };
+  }).sort((a, b) => a.deltaE - b.deltaE);
+  return scored.slice(0, n);
+}
+
+module.exports = {
+  // hex/color math
+  expandHex, hexToRgb, rgbToHex, rgbToLab, deltaE, rgbToHsl, hslToRgb,
+  // palette + SW
+  extractPalette, nearestSW, annotatePalette, loadSW,
+  // coordination
+  coordinatingHexes, coordinatingDesigns,
+};
diff --git a/src/sw-colors.json b/src/sw-colors.json
new file mode 100644
index 0000000..ccbc941
--- /dev/null
+++ b/src/sw-colors.json
@@ -0,0 +1,327 @@
+[
+  {
+    "code": "SW6150",
+    "name": "Universal Khaki",
+    "hex": "#C4A367"
+  },
+  {
+    "code": "SW6237",
+    "name": "Urbane Bronze",
+    "hex": "#5A5A5A"
+  },
+  {
+    "code": "SW6238",
+    "name": "Evergreen Fog",
+    "hex": "#768B87"
+  },
+  {
+    "code": "SW6239",
+    "name": "Accessible Beige",
+    "hex": "#C9B8A3"
+  },
+  {
+    "code": "SW6240",
+    "name": "Sea Salt",
+    "hex": "#DFE5E0"
+  },
+  {
+    "code": "SW6241",
+    "name": "Repose Gray",
+    "hex": "#BFBFBF"
+  },
+  {
+    "code": "SW6242",
+    "name": "Alabaster White",
+    "hex": "#F8F8F8"
+  },
+  {
+    "code": "SW6243",
+    "name": "Urbane Bronze",
+    "hex": "#474747"
+  },
+  {
+    "code": "SW6244",
+    "name": "Naval",
+    "hex": "#273C53"
+  },
+  {
+    "code": "SW6245",
+    "name": "Iron Ore",
+    "hex": "#3D3D3F"
+  },
+  {
+    "code": "SW6246",
+    "name": "Cavern Clay",
+    "hex": "#7A6C63"
+  },
+  {
+    "code": "SW6247",
+    "name": "Cyberspace",
+    "hex": "#252525"
+  },
+  {
+    "code": "SW6248",
+    "name": "Gauntlet Gray",
+    "hex": "#4A4A4C"
+  },
+  {
+    "code": "SW6250",
+    "name": "Peppercorn",
+    "hex": "#2D2D2F"
+  },
+  {
+    "code": "SW6252",
+    "name": "Dark Night",
+    "hex": "#1A1A1A"
+  },
+  {
+    "code": "SW6253",
+    "name": "Black Magic",
+    "hex": "#1C1C1C"
+  },
+  {
+    "code": "SW6254",
+    "name": "Raven Black",
+    "hex": "#101010"
+  },
+  {
+    "code": "SW6255",
+    "name": "Total Black",
+    "hex": "#000000"
+  },
+  {
+    "code": "SW6257",
+    "name": "Urbane Bronze",
+    "hex": "#595959"
+  },
+  {
+    "code": "SW6258",
+    "name": "Tricorn Black",
+    "hex": "#2F2F30"
+  },
+  {
+    "code": "SW6385",
+    "name": "Dover White",
+    "hex": "#EEE5D8"
+  },
+  {
+    "code": "SW7001",
+    "name": "Urbane Bronze",
+    "hex": "#5A5A5C"
+  },
+  {
+    "code": "SW7005",
+    "name": "Pure White",
+    "hex": "#EDECE6"
+  },
+  {
+    "code": "SW7006",
+    "name": "Extra White",
+    "hex": "#FFFFFF"
+  },
+  {
+    "code": "SW7008",
+    "name": "Alabaster",
+    "hex": "#EDE9DD"
+  },
+  {
+    "code": "SW7009",
+    "name": "Creamy",
+    "hex": "#F5F4EF"
+  },
+  {
+    "code": "SW7013",
+    "name": "Repose Gray",
+    "hex": "#C0BDB2"
+  },
+  {
+    "code": "SW7015",
+    "name": "Repose Gray",
+    "hex": "#B9B9B9"
+  },
+  {
+    "code": "SW7016",
+    "name": "Mindful Gray",
+    "hex": "#7F8483"
+  },
+  {
+    "code": "SW7017",
+    "name": "Urbane Bronze",
+    "hex": "#505050"
+  },
+  {
+    "code": "SW7018",
+    "name": "Terminal Gray",
+    "hex": "#7A7A7A"
+  },
+  {
+    "code": "SW7019",
+    "name": "Charcoal Gray",
+    "hex": "#4D4D4D"
+  },
+  {
+    "code": "SW7020",
+    "name": "Repose Gray",
+    "hex": "#999999"
+  },
+  {
+    "code": "SW7021",
+    "name": "Black Olive",
+    "hex": "#3D3D3D"
+  },
+  {
+    "code": "SW7022",
+    "name": "Black Bean",
+    "hex": "#2D2D2D"
+  },
+  {
+    "code": "SW7023",
+    "name": "Polished Black",
+    "hex": "#1F1F1F"
+  },
+  {
+    "code": "SW7024",
+    "name": "Cavern Clay",
+    "hex": "#7F7F7F"
+  },
+  {
+    "code": "SW7025",
+    "name": "Shade-Grown",
+    "hex": "#6B6B6B"
+  },
+  {
+    "code": "SW7026",
+    "name": "Urbane Bronze",
+    "hex": "#5D5D5D"
+  },
+  {
+    "code": "SW7027",
+    "name": "Antique Red",
+    "hex": "#8B4545"
+  },
+  {
+    "code": "SW7028",
+    "name": "Incredible White",
+    "hex": "#EDE6DE"
+  },
+  {
+    "code": "SW7029",
+    "name": "Agreeable Gray",
+    "hex": "#D1CCCB"
+  },
+  {
+    "code": "SW7030",
+    "name": "Accessible Beige",
+    "hex": "#D4CBC3"
+  },
+  {
+    "code": "SW7031",
+    "name": "Urbane Bronze",
+    "hex": "#595959"
+  },
+  {
+    "code": "SW7032",
+    "name": "Cavern Clay",
+    "hex": "#7A6C63"
+  },
+  {
+    "code": "SW7033",
+    "name": "Accessible Beige",
+    "hex": "#E0D5CA"
+  },
+  {
+    "code": "SW7034",
+    "name": "Urbane Bronze",
+    "hex": "#575757"
+  },
+  {
+    "code": "SW7035",
+    "name": "Repose Gray",
+    "hex": "#BFBFBF"
+  },
+  {
+    "code": "SW7036",
+    "name": "Tricorn Black",
+    "hex": "#2D2D2F"
+  },
+  {
+    "code": "SW7037",
+    "name": "Light French Gray",
+    "hex": "#D4D4D4"
+  },
+  {
+    "code": "SW7038",
+    "name": "Silhouette",
+    "hex": "#828282"
+  },
+  {
+    "code": "SW7039",
+    "name": "Urbane Bronze",
+    "hex": "#5A5A5C"
+  },
+  {
+    "code": "SW7040",
+    "name": "Cavern Clay",
+    "hex": "#8B7765"
+  },
+  {
+    "code": "SW7041",
+    "name": "N0",
+    "hex": "#FFFFFF"
+  },
+  {
+    "code": "SW7042",
+    "name": "Shoji White",
+    "hex": "#E8E4DB"
+  },
+  {
+    "code": "SW7043",
+    "name": "Accessible Beige",
+    "hex": "#D0CCC4"
+  },
+  {
+    "code": "SW7044",
+    "name": "Urbane Bronze",
+    "hex": "#555555"
+  },
+  {
+    "code": "SW7045",
+    "name": "Tricorn Black",
+    "hex": "#2F2F30"
+  },
+  {
+    "code": "SW7046",
+    "name": "Black Bean",
+    "hex": "#1C1C1C"
+  },
+  {
+    "code": "SW7047",
+    "name": "Accessible Beige",
+    "hex": "#DDD5CE"
+  },
+  {
+    "code": "SW7048",
+    "name": "Urbane Bronze",
+    "hex": "#595959"
+  },
+  {
+    "code": "SW7049",
+    "name": "Cavern Clay",
+    "hex": "#7A6C63"
+  },
+  {
+    "code": "SW7050",
+    "name": "Tricorn Black",
+    "hex": "#2F2F30"
+  },
+  {
+    "code": "SW9109",
+    "name": "Natural Linen",
+    "hex": "#D4D0C8"
+  },
+  {
+    "code": "SW9166",
+    "name": "Drift of Mist",
+    "hex": "#E8E8E8"
+  }
+]
\ No newline at end of file

← d1fefad fix(site.css): bump wordmark weights per debate consensus  ·  back to Wallco Ai  ·  fix(/studio): escape inline onclick string quotes properly feb477a →